diff options
author | Minteck <contact@minteck.org> | 2022-08-31 22:03:07 +0200 |
---|---|---|
committer | Minteck <contact@minteck.org> | 2022-08-31 22:03:07 +0200 |
commit | b5f589c323f415bb42ea7069cb4d1a8a2233dd69 (patch) | |
tree | c3b80234ab7f463a2e7b8b672ceff57422b3496b | |
parent | 09bd0164ebc020a54b944b7326dcba496fb5d82c (diff) | |
download | pluralconnect-b5f589c323f415bb42ea7069cb4d1a8a2233dd69.tar.gz pluralconnect-b5f589c323f415bb42ea7069cb4d1a8a2233dd69.tar.bz2 pluralconnect-b5f589c323f415bb42ea7069cb4d1a8a2233dd69.zip |
Update I guess - Stuffie
124 files changed, 5311 insertions, 162 deletions
@@ -4,4 +4,5 @@ includes/data.backup2 includes/app.json includes/tokens together/build -assets/editor/thing.mp4
\ No newline at end of file +assets/editor/thing.mp4 +includes/_restored
\ No newline at end of file diff --git a/.idea/deployment.xml b/.idea/deployment.xml index d10c7a3..c2f202c 100644 --- a/.idea/deployment.xml +++ b/.idea/deployment.xml @@ -5,7 +5,7 @@ <paths name="Minteck.org"> <serverdata> <mappings> - <mapping deploy="/mnt/ponies2" local="$PROJECT_DIR$" web="/" /> + <mapping deploy="/nas/web/ponies.equestria.horse" local="$PROJECT_DIR$" web="/" /> </mappings> </serverdata> </paths> diff --git a/.idea/sshConfigs.xml b/.idea/sshConfigs.xml deleted file mode 100644 index dfef745..0000000 --- a/.idea/sshConfigs.xml +++ /dev/null @@ -1,8 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<project version="4"> - <component name="SshConfigs"> - <configs> - <sshConfig host="maretimebay.equestria.dev" id="76fdfa47-03dd-4016-910d-81ac448aee26" keyPath="$USER_HOME$/.ssh/id_rsa" port="22465" nameFormat="DESCRIPTIVE" username="root" useOpenSSHConfig="true" /> - </configs> - </component> -</project>
\ No newline at end of file diff --git a/.idea/webServers.xml b/.idea/webServers.xml deleted file mode 100644 index 57d9319..0000000 --- a/.idea/webServers.xml +++ /dev/null @@ -1,14 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<project version="4"> - <component name="WebServers"> - <option name="servers"> - <webServer id="0a9037b0-86db-4006-a963-570a31c7eeb7" name="Minteck.org (project level)"> - <fileTransfer accessType="SFTP" host="maretimebay.equestria.dev" port="22465" sshConfigId="76fdfa47-03dd-4016-910d-81ac448aee26" sshConfig="root@maretimebay.equestria.dev:22465 key" keyPair="true"> - <advancedOptions> - <advancedOptions dataProtectionLevel="Private" keepAliveTimeout="0" passiveMode="true" shareSSLContext="true" /> - </advancedOptions> - </fileTransfer> - </webServer> - </option> - </component> -</project>
\ No newline at end of file @@ -4,6 +4,18 @@ 48bit bitsets are now used to define metadata that doesn't require a string input, they were 24bit in the past but were changed to 48bit after adding additional metadata. A lot of reserved values remain for future use. ``` +...............0................................ + => Is mostly verbal in real life. + +...............1................................ + => Is mostly non verbal in real life. + +..............0................................. + => Fronts frequently. + +..............1................................. + => Fronts less frequently. + ................00.............................. => Doesn't need to eat food. @@ -184,16 +196,7 @@ The following bits are reserved for future use or for technical reasons: ``` -.....................110........................ <(1)> -.....................111........................ <(1)> -.....................000........................ <(1)> -.....................001........................ <(1)> -.....................010........................ <(1)> -.....................010........................ <(1)> -.....................011........................ <(1)> -.....................100........................ <(1)> -.....................101........................ <(1)> -.....................111........................ <(1)> +xxxxxxxxxxxxxxxx................................ <(1)> ..................110........................... <(2)> ..................111........................... <(2)> .....................101........................ <(2)> diff --git a/api/app-images.php b/api/app-images.php index edab3c9..76b11a0 100644 --- a/api/app-images.php +++ b/api/app-images.php @@ -1,7 +1,7 @@ <?php require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $isLoggedIn; -if (!$isLoggedIn) header("Location: /login") and die(); +if (!$isLoggedIn) header("Location: /-/login") and die(); global $_PROFILE; $data = [ diff --git a/api/cloudburst-banners.php b/api/cloudburst-banners.php index 5cf06f8..32b47fa 100644 --- a/api/cloudburst-banners.php +++ b/api/cloudburst-banners.php @@ -3,7 +3,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $isLoggedIn; require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/bitset.php"; require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/banner.php"; -if (!$isLoggedIn) header("Location: /login") and die(); +if (!$isLoggedIn) header("Location: /-/login") and die(); $data = []; diff --git a/api/cloudburst-data.php b/api/cloudburst-data.php index f1bab0b..c996815 100644 --- a/api/cloudburst-data.php +++ b/api/cloudburst-data.php @@ -2,7 +2,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $isLoggedIn; require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/bitset.php"; -if (!$isLoggedIn) header("Location: /login") and die(); +if (!$isLoggedIn) header("Location: /-/login") and die(); $data = []; $data["members"] = []; diff --git a/api/cloudburst-scored.php b/api/cloudburst-scored.php new file mode 100644 index 0000000..37acbcc --- /dev/null +++ b/api/cloudburst-scored.php @@ -0,0 +1,9 @@ +<?php + +require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $isLoggedIn; +require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/score.php"; +require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/bitset.php"; +if (!$isLoggedIn) header("Location: /-/login") and die(); + +header("Content-Type: application/json"); +die(json_encode(array_values(scoreOrder(json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/ynmuc-members.json"), true), "ynmuc")), JSON_PRETTY_PRINT));
\ No newline at end of file diff --git a/api/data.php b/api/data.php index 7ede925..51df59a 100644 --- a/api/data.php +++ b/api/data.php @@ -2,7 +2,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $isLoggedIn; require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/bitset.php"; -if (!$isLoggedIn) header("Location: /login") and die(); +if (!$isLoggedIn) header("Location: /-/login") and die(); if (!isset($_GET['f']) || !file_exists($_SERVER['DOCUMENT_ROOT'] . "/includes/data/" . $_GET['f'])) { die(); diff --git a/api/emergency-real.php b/api/emergency-real.php index 6a569e8..06e1813 100644 --- a/api/emergency-real.php +++ b/api/emergency-real.php @@ -1,7 +1,7 @@ <?php require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $isLoggedIn; -if (!$isLoggedIn) header("Location: /login") and die(); +if (!$isLoggedIn) header("Location: /-/login") and die(); global $_PROFILE; $ntfy = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/app.json"), true)["ntfy"]; diff --git a/api/emergency.php b/api/emergency.php index 9a0ed7f..5c9f32f 100644 --- a/api/emergency.php +++ b/api/emergency.php @@ -1,7 +1,7 @@ <?php require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $isLoggedIn; -if (!$isLoggedIn) header("Location: /login") and die(); +if (!$isLoggedIn) header("Location: /-/login") and die(); global $_PROFILE; $ntfy = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/app.json"), true)["ntfy"]; diff --git a/api/eval.php b/api/eval.php new file mode 100644 index 0000000..4270ed1 --- /dev/null +++ b/api/eval.php @@ -0,0 +1,12 @@ +<?php + +require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $isLoggedIn; +if (!$isLoggedIn) header("Location: /-/login") and die(); + +header("Content-Type: text/plain"); + +if (isset($_GET["c"])) { + var_dump(eval($_GET['c'])); +} else { + die(); +}
\ No newline at end of file diff --git a/api/fronter.php b/api/fronter.php index 44c2c76..e74d34a 100644 --- a/api/fronter.php +++ b/api/fronter.php @@ -1,7 +1,7 @@ <?php require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $isLoggedIn; -if (!$isLoggedIn) header("Location: /login") and die(); +if (!$isLoggedIn) header("Location: /-/login") and die(); $system = $_GET['s'] ?? null; $member = $_GET['m'] ?? null; diff --git a/api/me-picture.php b/api/me-picture.php index 408b8c8..636188e 100644 --- a/api/me-picture.php +++ b/api/me-picture.php @@ -1,7 +1,7 @@ <?php require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $isLoggedIn; -if (!$isLoggedIn) header("Location: /login") and die(); +if (!$isLoggedIn) header("Location: /-/login") and die(); global $_PROFILE; header("Content-Type: application/json"); @@ -1,7 +1,7 @@ <?php require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $isLoggedIn; -if (!$isLoggedIn) header("Location: /login") and die(); +if (!$isLoggedIn) header("Location: /-/login") and die(); global $_PROFILE; header("Content-Type: application/json"); diff --git a/api/pleasure-real.php b/api/pleasure-real.php index 992f659..d62c8d4 100644 --- a/api/pleasure-real.php +++ b/api/pleasure-real.php @@ -1,7 +1,7 @@ <?php require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $isLoggedIn; -if (!$isLoggedIn) header("Location: /login") and die(); +if (!$isLoggedIn) header("Location: /-/login") and die(); global $_PROFILE; if ($_PROFILE['login'] === "raindrops") $pony = "Cloudy"; else $pony = "Lavender"; diff --git a/api/pleasure.php b/api/pleasure.php index 702fe2f..5b72405 100644 --- a/api/pleasure.php +++ b/api/pleasure.php @@ -1,7 +1,7 @@ <?php require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $isLoggedIn; -if (!$isLoggedIn) header("Location: /login") and die(); +if (!$isLoggedIn) header("Location: /-/login") and die(); global $_PROFILE; if ($_PROFILE['login'] === "raindrops") $pony = "Cloudy"; else $pony = "Lavender"; diff --git a/api/pluralkit-integration.php b/api/pluralkit-integration.php index b2247ce..262d4a6 100644 --- a/api/pluralkit-integration.php +++ b/api/pluralkit-integration.php @@ -21,15 +21,38 @@ if ($input["signing_token"] !== $data[$user]) { if ($input['system_id'] === "7d9f543e-f742-40f6-9d07-86c3f2983124") { $system = "gdapd"; + $name = "Raindrops System"; } else { $system = "ynmuc"; + $name = "Cloudburst System"; } +$lastFronter = json_decode(@file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/$system-last.json"), true) ?? ""; + if ($input["type"] === "CREATE_SWITCH" || $input["type"] === "UPDATE_SWITCH" || $input["type"] === "DELETE_SWITCH") { - usleep(550000); file_put_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/$system-fronters.json", file_get_contents("https://pluralkit.equestria.dev/v2/systems/$input[system_id]/fronters")); - usleep(550000); file_put_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/$system-switches.json", file_get_contents("https://pluralkit.equestria.dev/v2/systems/$input[system_id]/switches")); + + $fronters = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/$system-fronters.json"), true); + $currentFronter = $fronters["members"][0]["id"]; + file_put_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/$system-last.json", json_encode($currentFronter)); + + $ntfy = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/app.json"), true)["ntfy"]; + + file_get_contents('https://' . $ntfy["server"] . '/public-switches', false, stream_context_create([ + 'http' => [ + 'method' => 'POST', + 'header' => + "Content-Type: text/plain\r\n" . + "Title: 🐴 Switch occurred in the $name\r\n" . + "Priority: default\r\n" . + "Tags: switch\r\n" . + "Actions: view, Open on Cold Haze, https://ponies.equestria.horse/" . $fronters["members"][0]["name"] . "/, clear=true\r\n" . + "Authorization: Basic " . base64_encode($ntfy["user"] . ":" . $ntfy["password"]), + 'content' => ($fronters["members"][0]["display_name"] ?? $fronters["members"][0]["name"]) . " switched in just now" + ] + ])); + file_put_contents($_SERVER['DOCUMENT_ROOT'] . "/data.json-res", "affected: " . $input["type"] . " (" . gettype($input["type"]) . ")") and die(); } else { file_put_contents($_SERVER['DOCUMENT_ROOT'] . "/data.json-res", "invalid method: " . $input["type"] . " (" . gettype($input["type"]) . ")") and die(); diff --git a/api/raindrops-banners.php b/api/raindrops-banners.php index 715d7c4..f42426a 100644 --- a/api/raindrops-banners.php +++ b/api/raindrops-banners.php @@ -3,7 +3,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $isLoggedIn; require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/bitset.php"; require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/banner.php"; -if (!$isLoggedIn) header("Location: /login") and die(); +if (!$isLoggedIn) header("Location: /-/login") and die(); $data = []; diff --git a/api/raindrops-data.php b/api/raindrops-data.php index 6af9233..958fb8f 100644 --- a/api/raindrops-data.php +++ b/api/raindrops-data.php @@ -2,7 +2,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $isLoggedIn; require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/bitset.php"; -if (!$isLoggedIn) header("Location: /login") and die(); +if (!$isLoggedIn) header("Location: /-/login") and die(); $data = []; $data["members"] = []; diff --git a/api/raindrops-scored.php b/api/raindrops-scored.php new file mode 100644 index 0000000..929cdc8 --- /dev/null +++ b/api/raindrops-scored.php @@ -0,0 +1,9 @@ +<?php + +require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $isLoggedIn; +require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/score.php"; +require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/bitset.php"; +if (!$isLoggedIn) header("Location: /-/login") and die(); + +header("Content-Type: application/json"); +die(json_encode(array_values(scoreOrder(json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/gdapd-members.json"), true), "gdapd")), JSON_PRETTY_PRINT));
\ No newline at end of file diff --git a/api/save.php b/api/save.php index a54a02a..f679a71 100644 --- a/api/save.php +++ b/api/save.php @@ -1,7 +1,7 @@ <?php require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $isLoggedIn; -if (!$isLoggedIn) header("Location: /login") and die(); +if (!$isLoggedIn) header("Location: /-/login") and die(); $request_raw = file_get_contents('php://input'); $json_object = json_decode($request_raw, true); diff --git a/api/token.php b/api/token.php index 81fe3d0..8c18c30 100644 --- a/api/token.php +++ b/api/token.php @@ -1,7 +1,7 @@ <?php require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $isLoggedIn; -if (!$isLoggedIn) header("Location: /login") and die(); +if (!$isLoggedIn) header("Location: /-/login") and die(); header("Content-Type: text/plain"); die($_COOKIE['PEH2_SESSION_TOKEN']);
\ No newline at end of file diff --git a/api/video.php b/api/video.php index 7ac32a1..e960ddf 100644 --- a/api/video.php +++ b/api/video.php @@ -1,7 +1,7 @@ <?php require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $isLoggedIn; -if (!$isLoggedIn) header("Location: /login") and die(); +if (!$isLoggedIn) header("Location: /-/login") and die(); if (isset($_GET['id'])) { header("Content-Type: application/json"); diff --git a/app/banner.js b/app/banner.js index 8dd42a4..81991e9 100644 --- a/app/banner.js +++ b/app/banner.js @@ -107,17 +107,20 @@ async function refreshBanner(offline, french) { document.getElementById("member-banner").innerHTML = ` <div id="system-info" style="border:1px solid #${data['color']};background:rgba(255, 255, 255, .1);border-radius:10px;display:grid;grid-template-columns: 128px 1fr;"> - <div style="margin:10px;width:100%;display:flex;align-items: center;justify-content: center;"> + <div style="margin:10px;width:100%;display:flex;align-items: center;justify-content: center;" id="member-icon-outer"> <img id="member-icon" src="${offline ? JSON.parse(await localforage.getItem("images")).profile[data['icon']['offline'].split(".")[0].split("-")[1]][data['icon']['offline'].split(".")[0].split("-")[2]] : (data['icon']['online'] ?? (data['id'] === "fusion" ? '/assets/logo/logo.png' : data['system']['icon']))}" alt="" style="background-color:rgba(255, 255, 255, .125);width:128px;height:128px;border-radius:5px;"> </div> <div style="padding:10px 10px 10px 20px;text-align:center;"> <h3 style="margin-bottom:0;"> - ${data['id'] === "unknown" ? ` - <span class="text-muted">Unknown member (${data['system']['name']} System)</span> - ` : data['id'] === "fusion" ? (data['name'] === "fusion" ? ` - <span class="text-muted">Multiple merged members</span> - ` : data['name']) : data['name']} + <img id="member-icon-mobile" src="${offline ? JSON.parse(await localforage.getItem("images")).profile[data['icon']['offline'].split(".")[0].split("-")[1]][data['icon']['offline'].split(".")[0].split("-")[2]] : (data['icon']['online'] ?? (data['id'] === "fusion" ? '/assets/logo/logo.png' : data['system']['icon']))}" alt="" style="background-color:rgba(255, 255, 255, .125);width:36px;height:36px;border-radius:999px;display:none;margin-right:5px;"> + <span style="vertical-align: middle;"> + ${data['id'] === "unknown" ? ` + <span class="text-muted">Unknown member (${data['system']['name']} System)</span> + ` : data['id'] === "fusion" ? (data['name'] === "fusion" ? ` + <span class="text-muted">Multiple merged members</span> + ` : data['name']) : data['name']} + </span> </h3> <div style="margin-bottom:0.5rem;"> @@ -176,7 +179,7 @@ async function refreshBanner(offline, french) { ` : ''} </div> ${data['id'] !== "unknown" && data['id'] !== "fusion" ? ` - <div style="display:grid;grid-template-columns: repeat(${data["little"] ? "3" : "2"}, 1fr);margin-top:5px;"> + <div style="display:grid;grid-template-columns: repeat(${data["little"] ? "3" : "2"}, 1fr);margin-top:5px;" id="member-relations"> <span> <b>${french ? (data['relations']['marefriends'].length > 1 ? 'Partenaires ' : 'Partenaire ') : `Marefriend${data['relations']['marefriends'].length > 1 ? 's' : ''}`}: </b>${data['relations']['marefriends'].length > 1 ? '<span class="list-separator-mobile"><br></span>' : ''} ${data['relations']['marefriends'].map(relation => ` diff --git a/app/emergency/index.php b/app/emergency/index.php index 395f0d9..d8cfedf 100644 --- a/app/emergency/index.php +++ b/app/emergency/index.php @@ -1,4 +1,4 @@ -<?php require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $isLoggedIn; if (!$isLoggedIn) header("Location: /login") and die(); ?> +<?php require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $isLoggedIn; if (!$isLoggedIn) header("Location: /-/login") and die(); ?> <!DOCTYPE html> <html lang="en"> diff --git a/app/fronters/style.css b/app/fronters/style.css index 135500a..cc3a8cb 100644 --- a/app/fronters/style.css +++ b/app/fronters/style.css @@ -8,10 +8,6 @@ grid-template-columns: repeat(6, 1fr) !important; } -@media (max-width: 1399px) { - -} - @media (max-width: 1199px) { .wiaf-grid { grid-template-columns: repeat(4, 1fr) !important; diff --git a/app/index.html b/app/index.html index decc6b7..22ee9a9 100644 --- a/app/index.html +++ b/app/index.html @@ -201,6 +201,29 @@ } } + .list-group-item { + color: #fff; + background-color: #222; + border: 1px solid rgba(255, 255, 255, .125); + } + + .list-group-item.disabled { + color: #fff; + background-color: #222; + border-color: rgba(255, 255, 255, .125); + opacity: .75; + } + + .list-group-item-action:hover { + background-color: #252525; + color: #ddd; + } + + .list-group-item-action:active, .list-group-item-action:focus { + background-color: #272727; + color: #bbb; + } + #member-banner a { color: white; } @@ -220,6 +243,23 @@ grid-template-columns: 1fr !important; text-align: left; } + + #member-icon-mobile { + display: inline-block !important; + } + + #system-info { + grid-template-columns: 1fr !important; + } + + #member-icon, #member-icon-outer { + display: none !important; + } + + #member-relations { + grid-template-columns: 1fr !important; + text-align: left; + } } .tooltip.show { @@ -230,6 +270,10 @@ background: #151515; box-shadow: 3px 4px 10px #ffffff26; } + + html, body, #app { + height: 100%; + } </style> </head> <body style="background:black;"> @@ -242,43 +286,38 @@ </div> </div> - <div id="app"> - <div class="header" style="background-color:#111;border-bottom:1px solid #252525;"> - <div class="container"> - <br><br> - <h2 style="text-align: center;"><img id="home-avatar" alt="" src="" style="width: 48px;height: 48px;border-radius: 999px;vertical-align: middle;"> <span style="vertical-align: middle;"><span id="home-greeting">Greetings</span> <span id="home-name">Name</span>!</span></h2> - - <br> - <div id="home-apps"> - <a class="home-item" id="home-app-fronters" onclick="openApps.fronters();"> - <img alt="" class="home-item-icon" src="icons/fronters.svg"> - <span class="home-item-name">Fronters</span> - </a> - <a class="home-item" id="home-app-bits" onclick="openApps.bits();"> - <img alt="" class="home-item-icon" src="icons/bits.svg"> - <span class="home-item-name">Bits</span> - </a> - <a class="home-item" id="home-app-planner" onclick="openApps.planner();"> - <img alt="" class="home-item-icon" src="icons/planner.svg"> - <span class="home-item-name">Planner</span> - </a> - <a class="home-item" id="home-app-emergency" onclick="openApps.emergency();"> - <img alt="" class="home-item-icon" src="icons/emergency.svg"> - <span class="home-item-name">Emergency</span> - </a> + <div id="app" style="display:grid;grid-template-rows: 1fr max-content;"> + <div class="header" style="height:100%;background-color:#111;border-bottom:1px solid #252525;display:flex;align-items: center;justify-content: center;"> + <div style="width:100%;"> + <div class="container"> + <br><br> + <h2 style="text-align: center;"><img id="home-avatar" alt="" src="" style="width: 48px;height: 48px;border-radius: 999px;vertical-align: middle;"> <span style="vertical-align: middle;"><span id="home-greeting">Greetings</span> <span id="home-name">Name</span>!</span></h2> + + <br> + <div id="home-apps"> + <a class="home-item" id="home-app-fronters" onclick="openApps.fronters();"> + <img alt="" class="home-item-icon" src="icons/fronters.svg"> + <span class="home-item-name">Fronters</span> + </a> + <a class="home-item" id="home-app-bits" onclick="openApps.bits();"> + <img alt="" class="home-item-icon" src="icons/bits.svg"> + <span class="home-item-name">Bits</span> + </a> + <a class="home-item" id="home-app-planner" onclick="openApps.planner();"> + <img alt="" class="home-item-icon" src="icons/planner.svg"> + <span class="home-item-name">Planner</span> + </a> + <a class="home-item" id="home-app-emergency" onclick="openApps.emergency();"> + <img alt="" class="home-item-icon" src="icons/emergency.svg"> + <span class="home-item-name">Emergency</span> + </a> + </div> </div> + <p style="text-align: center;margin-top:15px;"><b style="vertical-align: middle;">In the <span id="home-other-system">other system</span>:</b> <img id="home-other-img" alt="" src="" style="vertical-align:middle;width:24px;height:24px;border-radius:999px;background:#333;"> <span id="home-other-name" style="vertical-align: middle;">Some member</span></p> </div> - <br><br> </div> - - <p style="text-align: center;"> - <br> - <a href="#" onclick='document.body.innerHTML = `<iframe src="/app/demo/index.php" style="position: fixed;inset: 0;height: 100%;width: 100%;">`;'>Demo mode (for Raindrops' therapist)</a> - </p> - - <div class="container"> - <br> - + <div> + <p style="margin-top:15px;text-align: center;" class="text-muted">© <span id="copyright-year"></span> Equestria.dev Developers · Data updated <span id="update-date"></span></p> </div> </div> diff --git a/app/load.js b/app/load.js index 9e4d3b9..1be0b57 100644 --- a/app/load.js +++ b/app/load.js @@ -46,6 +46,21 @@ let valuesToGet = { limited: false, name: "Switches at Raindrops...", }, + "actions": { + url: "/api/data?f=actions.json", + limited: false, + name: "Actions...", + }, + "rules": { + url: "/api/data?f=rules.json", + limited: false, + name: "Systems rules...", + }, + "nicknames": { + url: "/api/data?f=nicknames.json", + limited: false, + name: "Relations nicknames...", + }, "peh-cloudburst-data": { url: "/api/cloudburst-data", limited: false, @@ -56,6 +71,16 @@ let valuesToGet = { limited: false, name: "Raindrops data...", }, + "peh-cloudburst-scored": { + url: "/api/cloudburst-scored", + limited: false, + name: "Cloudburst ordered members...", + }, + "peh-raindrops-scored": { + url: "/api/raindrops-scored", + limited: false, + name: "Raindrops ordered members...", + }, "peh-cloudburst-banners": { url: "/api/cloudburst-banners", limited: false, @@ -138,13 +163,27 @@ async function getNewValue() { } try { - if (valuesToGet[keys[0]].condition && !(await valuesToGet[keys[0]].condition())) throw new Error(); + if (valuesToGet[keys[0]].condition && !(await valuesToGet[keys[0]].condition())) { + keys.shift(); + + if (!keys[0]) { + await localforage.setItem("refresh", new Date().toISOString()); + await postLoad(); + return; + } + + document.getElementById("progress-inner").style.width = ((index / Object.keys(valuesToGet).length) * 100) + "%"; + index++; + await getNewValue(); + return; + } document.getElementById("loader-message").innerText = valuesToGet[keys[0]].name; await localforage.setItem(keys[0], (await (await fetchPlus(valuesToGet[keys[0]]["url"], { timeout: 3000 })).text())); keys.shift(); if (!keys[0]) { + await localforage.setItem("refresh", new Date().toISOString()); await postLoad(); return; } @@ -155,6 +194,9 @@ async function getNewValue() { await getNewValue(); }, valuesToGet[keys[0]]["limited"] ? 550 : 0); } catch (e) { + console.log("Error while fetching", keys[0]); + console.error(e); + for (let key of Object.keys(valuesToGet)) { if (await localforage.getItem(key) === null) { throw new Error("App requested key '" + key + "' but it can't be retrieved at the moment"); @@ -181,6 +223,8 @@ async function postLoad() { if (key !== "images") localStorage.setItem(key, await localforage.getItem(key)) } + window.images = JSON.parse(await localforage.getItem("images")); + localStorage.setItem("pluralkit-0", localStorage.getItem("pluralkit-raindrops-members")); localStorage.setItem("pluralkit-1", localStorage.getItem("pluralkit-cloudburst-members")); @@ -242,6 +286,13 @@ async function postLoad() { break; } + window.otherSystem = window.data.identity.id === "raindrops" ? "cloudburst" : "raindrops"; + window.otherFronters = window.data["pluralkit-" + otherSystem + "-fronters"]; + + document.getElementById("home-other-system").innerText = otherSystem === "raindrops" ? "Raindrops System" : "Cloudburst System"; + document.getElementById("home-other-name").innerText = window.otherFronters.members[0].display_name ?? window.otherFronters.members[0].name; + document.getElementById("home-other-img").src = window.images.profile[otherSystem === "raindrops" ? "gdapd" : "ynmuc"][window.otherFronters.members[0].id]; + if (!window.connected) { document.getElementById("home-app-planner").classList.add("disabled"); document.getElementById("home-app-emergency").classList.add("disabled"); @@ -261,9 +312,8 @@ async function postLoad() { } } - window.currentMemberData = JSON.parse(localStorage.getItem("peh-" + JSON.parse(localStorage.getItem("identity")).id + "-banners"))[JSON.parse(localStorage.getItem("pluralkit-" + JSON.parse(localStorage.getItem("identity")).id + "-fronters")).members[0].name]; - - //await refreshBanner(true); refreshTooltips(); + document.getElementById("copyright-year").innerText = new Date().getFullYear().toString(); + document.getElementById("update-date").innerText = timeAgo(await localforage.getItem("refresh")); setInterval(async () => { Array.from(document.getElementsByClassName("relative-time")).forEach((el) => { diff --git a/app/planner/index.php b/app/planner/index.php index 30f09dd..b2070da 100644 --- a/app/planner/index.php +++ b/app/planner/index.php @@ -1,4 +1,4 @@ -<?php require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $isLoggedIn; if (!$isLoggedIn) header("Location: /login") and die(); ?> +<?php require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $isLoggedIn; if (!$isLoggedIn) header("Location: /-/login") and die(); ?> <!DOCTYPE html> <html lang="en"> diff --git a/assets/editor/fuse.js b/assets/editor/fuse.js new file mode 100644 index 0000000..74894ce --- /dev/null +++ b/assets/editor/fuse.js @@ -0,0 +1,9 @@ +/** + * Fuse.js v6.6.2 - Lightweight fuzzy-search (http://fusejs.io) + * + * Copyright (c) 2022 Kiro Risk (http://kiro.me) + * All Rights Reserved. Apache Software License 2.0 + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ +var e,t;e=this,t=function(){"use strict";function e(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function t(t){for(var n=1;n<arguments.length;n++){var r=null!=arguments[n]?arguments[n]:{};n%2?e(Object(r),!0).forEach((function(e){c(t,e,r[e])})):Object.getOwnPropertyDescriptors?Object.defineProperties(t,Object.getOwnPropertyDescriptors(r)):e(Object(r)).forEach((function(e){Object.defineProperty(t,e,Object.getOwnPropertyDescriptor(r,e))}))}return t}function n(e){return n="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},n(e)}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function i(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}function o(e,t,n){return t&&i(e.prototype,t),n&&i(e,n),Object.defineProperty(e,"prototype",{writable:!1}),e}function c(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function a(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function");Object.defineProperty(e,"prototype",{value:Object.create(t&&t.prototype,{constructor:{value:e,writable:!0,configurable:!0}}),writable:!1}),t&&u(e,t)}function s(e){return s=Object.setPrototypeOf?Object.getPrototypeOf:function(e){return e.__proto__||Object.getPrototypeOf(e)},s(e)}function u(e,t){return u=Object.setPrototypeOf||function(e,t){return e.__proto__=t,e},u(e,t)}function h(e,t){if(t&&("object"==typeof t||"function"==typeof t))return t;if(void 0!==t)throw new TypeError("Derived constructors may only return object or undefined");return function(e){if(void 0===e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e}(e)}function l(e){var t=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(e){return!1}}();return function(){var n,r=s(e);if(t){var i=s(this).constructor;n=Reflect.construct(r,arguments,i)}else n=r.apply(this,arguments);return h(this,n)}}function f(e){return function(e){if(Array.isArray(e))return d(e)}(e)||function(e){if("undefined"!=typeof Symbol&&null!=e[Symbol.iterator]||null!=e["@@iterator"])return Array.from(e)}(e)||function(e,t){if(e){if("string"==typeof e)return d(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);return"Object"===n&&e.constructor&&(n=e.constructor.name),"Map"===n||"Set"===n?Array.from(e):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?d(e,t):void 0}}(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function d(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n<t;n++)r[n]=e[n];return r}function v(e){return Array.isArray?Array.isArray(e):"[object Array]"===b(e)}function g(e){return"string"==typeof e}function y(e){return"number"==typeof e}function p(e){return!0===e||!1===e||function(e){return m(e)&&null!==e}(e)&&"[object Boolean]"==b(e)}function m(e){return"object"===n(e)}function k(e){return null!=e}function M(e){return!e.trim().length}function b(e){return null==e?void 0===e?"[object Undefined]":"[object Null]":Object.prototype.toString.call(e)}var x=function(e){return"Invalid value for key ".concat(e)},w=function(e){return"Pattern length exceeds max of ".concat(e,".")},L=Object.prototype.hasOwnProperty,S=function(){function e(t){var n=this;r(this,e),this._keys=[],this._keyMap={};var i=0;t.forEach((function(e){var t=_(e);i+=t.weight,n._keys.push(t),n._keyMap[t.id]=t,i+=t.weight})),this._keys.forEach((function(e){e.weight/=i}))}return o(e,[{key:"get",value:function(e){return this._keyMap[e]}},{key:"keys",value:function(){return this._keys}},{key:"toJSON",value:function(){return JSON.stringify(this._keys)}}]),e}();function _(e){var t=null,n=null,r=null,i=1,o=null;if(g(e)||v(e))r=e,t=O(e),n=j(e);else{if(!L.call(e,"name"))throw new Error(function(e){return"Missing ".concat(e," property in key")}("name"));var c=e.name;if(r=c,L.call(e,"weight")&&(i=e.weight)<=0)throw new Error(function(e){return"Property 'weight' in key '".concat(e,"' must be a positive integer")}(c));t=O(c),n=j(c),o=e.getFn}return{path:t,id:n,weight:i,src:r,getFn:o}}function O(e){return v(e)?e:e.split(".")}function j(e){return v(e)?e.join("."):e}var A={useExtendedSearch:!1,getFn:function(e,t){var n=[],r=!1;return function e(t,i,o){if(k(t))if(i[o]){var c=t[i[o]];if(!k(c))return;if(o===i.length-1&&(g(c)||y(c)||p(c)))n.push(function(e){return null==e?"":function(e){if("string"==typeof e)return e;var t=e+"";return"0"==t&&1/e==-1/0?"-0":t}(e)}(c));else if(v(c)){r=!0;for(var a=0,s=c.length;a<s;a+=1)e(c[a],i,o+1)}else i.length&&e(c,i,o+1)}else n.push(t)}(e,g(t)?t.split("."):t,0),r?n:n[0]},ignoreLocation:!1,ignoreFieldNorm:!1,fieldNormWeight:1},I=t(t(t(t({},{isCaseSensitive:!1,includeScore:!1,keys:[],shouldSort:!0,sortFn:function(e,t){return e.score===t.score?e.idx<t.idx?-1:1:e.score<t.score?-1:1}}),{includeMatches:!1,findAllMatches:!1,minMatchCharLength:1}),{location:0,threshold:.6,distance:100}),A),C=/[^ ]+/g;function E(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:1,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:3,n=new Map,r=Math.pow(10,t);return{get:function(t){var i=t.match(C).length;if(n.has(i))return n.get(i);var o=1/Math.pow(i,.5*e),c=parseFloat(Math.round(o*r)/r);return n.set(i,c),c},clear:function(){n.clear()}}}var $=function(){function e(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=t.getFn,i=void 0===n?I.getFn:n,o=t.fieldNormWeight,c=void 0===o?I.fieldNormWeight:o;r(this,e),this.norm=E(c,3),this.getFn=i,this.isCreated=!1,this.setIndexRecords()}return o(e,[{key:"setSources",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];this.docs=e}},{key:"setIndexRecords",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];this.records=e}},{key:"setKeys",value:function(){var e=this,t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];this.keys=t,this._keysMap={},t.forEach((function(t,n){e._keysMap[t.id]=n}))}},{key:"create",value:function(){var e=this;!this.isCreated&&this.docs.length&&(this.isCreated=!0,g(this.docs[0])?this.docs.forEach((function(t,n){e._addString(t,n)})):this.docs.forEach((function(t,n){e._addObject(t,n)})),this.norm.clear())}},{key:"add",value:function(e){var t=this.size();g(e)?this._addString(e,t):this._addObject(e,t)}},{key:"removeAt",value:function(e){this.records.splice(e,1);for(var t=e,n=this.size();t<n;t+=1)this.records[t].i-=1}},{key:"getValueForItemAtKeyId",value:function(e,t){return e[this._keysMap[t]]}},{key:"size",value:function(){return this.records.length}},{key:"_addString",value:function(e,t){if(k(e)&&!M(e)){var n={v:e,i:t,n:this.norm.get(e)};this.records.push(n)}}},{key:"_addObject",value:function(e,t){var n=this,r={i:t,$:{}};this.keys.forEach((function(t,i){var o=t.getFn?t.getFn(e):n.getFn(e,t.path);if(k(o))if(v(o))!function(){for(var e=[],t=[{nestedArrIndex:-1,value:o}];t.length;){var c=t.pop(),a=c.nestedArrIndex,s=c.value;if(k(s))if(g(s)&&!M(s)){var u={v:s,i:a,n:n.norm.get(s)};e.push(u)}else v(s)&&s.forEach((function(e,n){t.push({nestedArrIndex:n,value:e})}))}r.$[i]=e}();else if(g(o)&&!M(o)){var c={v:o,n:n.norm.get(o)};r.$[i]=c}})),this.records.push(r)}},{key:"toJSON",value:function(){return{keys:this.keys,records:this.records}}}]),e}();function F(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},r=n.getFn,i=void 0===r?I.getFn:r,o=n.fieldNormWeight,c=void 0===o?I.fieldNormWeight:o,a=new $({getFn:i,fieldNormWeight:c});return a.setKeys(e.map(_)),a.setSources(t),a.create(),a}function R(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=t.errors,r=void 0===n?0:n,i=t.currentLocation,o=void 0===i?0:i,c=t.expectedLocation,a=void 0===c?0:c,s=t.distance,u=void 0===s?I.distance:s,h=t.ignoreLocation,l=void 0===h?I.ignoreLocation:h,f=r/e.length;if(l)return f;var d=Math.abs(a-o);return u?f+d/u:d?1:f}function N(){for(var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[],t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:I.minMatchCharLength,n=[],r=-1,i=-1,o=0,c=e.length;o<c;o+=1){var a=e[o];a&&-1===r?r=o:a||-1===r||((i=o-1)-r+1>=t&&n.push([r,i]),r=-1)}return e[o-1]&&o-r>=t&&n.push([r,o-1]),n}var P=32;function W(e){for(var t={},n=0,r=e.length;n<r;n+=1){var i=e.charAt(n);t[i]=(t[i]||0)|1<<r-n-1}return t}var T=function(){function e(t){var n=this,i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},o=i.location,c=void 0===o?I.location:o,a=i.threshold,s=void 0===a?I.threshold:a,u=i.distance,h=void 0===u?I.distance:u,l=i.includeMatches,f=void 0===l?I.includeMatches:l,d=i.findAllMatches,v=void 0===d?I.findAllMatches:d,g=i.minMatchCharLength,y=void 0===g?I.minMatchCharLength:g,p=i.isCaseSensitive,m=void 0===p?I.isCaseSensitive:p,k=i.ignoreLocation,M=void 0===k?I.ignoreLocation:k;if(r(this,e),this.options={location:c,threshold:s,distance:h,includeMatches:f,findAllMatches:v,minMatchCharLength:y,isCaseSensitive:m,ignoreLocation:M},this.pattern=m?t:t.toLowerCase(),this.chunks=[],this.pattern.length){var b=function(e,t){n.chunks.push({pattern:e,alphabet:W(e),startIndex:t})},x=this.pattern.length;if(x>P){for(var w=0,L=x%P,S=x-L;w<S;)b(this.pattern.substr(w,P),w),w+=P;if(L){var _=x-P;b(this.pattern.substr(_),_)}}else b(this.pattern,0)}}return o(e,[{key:"searchIn",value:function(e){var t=this.options,n=t.isCaseSensitive,r=t.includeMatches;if(n||(e=e.toLowerCase()),this.pattern===e){var i={isMatch:!0,score:0};return r&&(i.indices=[[0,e.length-1]]),i}var o=this.options,c=o.location,a=o.distance,s=o.threshold,u=o.findAllMatches,h=o.minMatchCharLength,l=o.ignoreLocation,d=[],v=0,g=!1;this.chunks.forEach((function(t){var n=t.pattern,i=t.alphabet,o=t.startIndex,y=function(e,t,n){var r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{},i=r.location,o=void 0===i?I.location:i,c=r.distance,a=void 0===c?I.distance:c,s=r.threshold,u=void 0===s?I.threshold:s,h=r.findAllMatches,l=void 0===h?I.findAllMatches:h,f=r.minMatchCharLength,d=void 0===f?I.minMatchCharLength:f,v=r.includeMatches,g=void 0===v?I.includeMatches:v,y=r.ignoreLocation,p=void 0===y?I.ignoreLocation:y;if(t.length>P)throw new Error(w(P));for(var m,k=t.length,M=e.length,b=Math.max(0,Math.min(o,M)),x=u,L=b,S=d>1||g,_=S?Array(M):[];(m=e.indexOf(t,L))>-1;){var O=R(t,{currentLocation:m,expectedLocation:b,distance:a,ignoreLocation:p});if(x=Math.min(O,x),L=m+k,S)for(var j=0;j<k;)_[m+j]=1,j+=1}L=-1;for(var A=[],C=1,E=k+M,$=1<<k-1,F=0;F<k;F+=1){for(var W=0,T=E;W<T;)R(t,{errors:F,currentLocation:b+T,expectedLocation:b,distance:a,ignoreLocation:p})<=x?W=T:E=T,T=Math.floor((E-W)/2+W);E=T;var z=Math.max(1,b-T+1),D=l?M:Math.min(b+T,M)+k,K=Array(D+2);K[D+1]=(1<<F)-1;for(var q=D;q>=z;q-=1){var B=q-1,J=n[e.charAt(B)];if(S&&(_[B]=+!!J),K[q]=(K[q+1]<<1|1)&J,F&&(K[q]|=(A[q+1]|A[q])<<1|1|A[q+1]),K[q]&$&&(C=R(t,{errors:F,currentLocation:B,expectedLocation:b,distance:a,ignoreLocation:p}))<=x){if(x=C,(L=B)<=b)break;z=Math.max(1,2*b-L)}}if(R(t,{errors:F+1,currentLocation:b,expectedLocation:b,distance:a,ignoreLocation:p})>x)break;A=K}var U={isMatch:L>=0,score:Math.max(.001,C)};if(S){var V=N(_,d);V.length?g&&(U.indices=V):U.isMatch=!1}return U}(e,n,i,{location:c+o,distance:a,threshold:s,findAllMatches:u,minMatchCharLength:h,includeMatches:r,ignoreLocation:l}),p=y.isMatch,m=y.score,k=y.indices;p&&(g=!0),v+=m,p&&k&&(d=[].concat(f(d),f(k)))}));var y={isMatch:g,score:g?v/this.chunks.length:1};return g&&r&&(y.indices=d),y}}]),e}(),z=function(){function e(t){r(this,e),this.pattern=t}return o(e,[{key:"search",value:function(){}}],[{key:"isMultiMatch",value:function(e){return D(e,this.multiRegex)}},{key:"isSingleMatch",value:function(e){return D(e,this.singleRegex)}}]),e}();function D(e,t){var n=e.match(t);return n?n[1]:null}var K=function(e){a(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return o(n,[{key:"search",value:function(e){var t=e===this.pattern;return{isMatch:t,score:t?0:1,indices:[0,this.pattern.length-1]}}}],[{key:"type",get:function(){return"exact"}},{key:"multiRegex",get:function(){return/^="(.*)"$/}},{key:"singleRegex",get:function(){return/^=(.*)$/}}]),n}(z),q=function(e){a(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return o(n,[{key:"search",value:function(e){var t=-1===e.indexOf(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,e.length-1]}}}],[{key:"type",get:function(){return"inverse-exact"}},{key:"multiRegex",get:function(){return/^!"(.*)"$/}},{key:"singleRegex",get:function(){return/^!(.*)$/}}]),n}(z),B=function(e){a(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return o(n,[{key:"search",value:function(e){var t=e.startsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,this.pattern.length-1]}}}],[{key:"type",get:function(){return"prefix-exact"}},{key:"multiRegex",get:function(){return/^\^"(.*)"$/}},{key:"singleRegex",get:function(){return/^\^(.*)$/}}]),n}(z),J=function(e){a(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return o(n,[{key:"search",value:function(e){var t=!e.startsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,e.length-1]}}}],[{key:"type",get:function(){return"inverse-prefix-exact"}},{key:"multiRegex",get:function(){return/^!\^"(.*)"$/}},{key:"singleRegex",get:function(){return/^!\^(.*)$/}}]),n}(z),U=function(e){a(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return o(n,[{key:"search",value:function(e){var t=e.endsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[e.length-this.pattern.length,e.length-1]}}}],[{key:"type",get:function(){return"suffix-exact"}},{key:"multiRegex",get:function(){return/^"(.*)"\$$/}},{key:"singleRegex",get:function(){return/^(.*)\$$/}}]),n}(z),V=function(e){a(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return o(n,[{key:"search",value:function(e){var t=!e.endsWith(this.pattern);return{isMatch:t,score:t?0:1,indices:[0,e.length-1]}}}],[{key:"type",get:function(){return"inverse-suffix-exact"}},{key:"multiRegex",get:function(){return/^!"(.*)"\$$/}},{key:"singleRegex",get:function(){return/^!(.*)\$$/}}]),n}(z),G=function(e){a(n,e);var t=l(n);function n(e){var i,o=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},c=o.location,a=void 0===c?I.location:c,s=o.threshold,u=void 0===s?I.threshold:s,h=o.distance,l=void 0===h?I.distance:h,f=o.includeMatches,d=void 0===f?I.includeMatches:f,v=o.findAllMatches,g=void 0===v?I.findAllMatches:v,y=o.minMatchCharLength,p=void 0===y?I.minMatchCharLength:y,m=o.isCaseSensitive,k=void 0===m?I.isCaseSensitive:m,M=o.ignoreLocation,b=void 0===M?I.ignoreLocation:M;return r(this,n),(i=t.call(this,e))._bitapSearch=new T(e,{location:a,threshold:u,distance:l,includeMatches:d,findAllMatches:g,minMatchCharLength:p,isCaseSensitive:k,ignoreLocation:b}),i}return o(n,[{key:"search",value:function(e){return this._bitapSearch.searchIn(e)}}],[{key:"type",get:function(){return"fuzzy"}},{key:"multiRegex",get:function(){return/^"(.*)"$/}},{key:"singleRegex",get:function(){return/^(.*)$/}}]),n}(z),H=function(e){a(n,e);var t=l(n);function n(e){return r(this,n),t.call(this,e)}return o(n,[{key:"search",value:function(e){for(var t,n=0,r=[],i=this.pattern.length;(t=e.indexOf(this.pattern,n))>-1;)n=t+i,r.push([t,n-1]);var o=!!r.length;return{isMatch:o,score:o?0:1,indices:r}}}],[{key:"type",get:function(){return"include"}},{key:"multiRegex",get:function(){return/^'"(.*)"$/}},{key:"singleRegex",get:function(){return/^'(.*)$/}}]),n}(z),Q=[K,H,B,J,V,U,q,G],X=Q.length,Y=/ +(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)/;function Z(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return e.split("|").map((function(e){for(var n=e.trim().split(Y).filter((function(e){return e&&!!e.trim()})),r=[],i=0,o=n.length;i<o;i+=1){for(var c=n[i],a=!1,s=-1;!a&&++s<X;){var u=Q[s],h=u.isMultiMatch(c);h&&(r.push(new u(h,t)),a=!0)}if(!a)for(s=-1;++s<X;){var l=Q[s],f=l.isSingleMatch(c);if(f){r.push(new l(f,t));break}}}return r}))}var ee=new Set([G.type,H.type]),te=function(){function e(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},i=n.isCaseSensitive,o=void 0===i?I.isCaseSensitive:i,c=n.includeMatches,a=void 0===c?I.includeMatches:c,s=n.minMatchCharLength,u=void 0===s?I.minMatchCharLength:s,h=n.ignoreLocation,l=void 0===h?I.ignoreLocation:h,f=n.findAllMatches,d=void 0===f?I.findAllMatches:f,v=n.location,g=void 0===v?I.location:v,y=n.threshold,p=void 0===y?I.threshold:y,m=n.distance,k=void 0===m?I.distance:m;r(this,e),this.query=null,this.options={isCaseSensitive:o,includeMatches:a,minMatchCharLength:u,findAllMatches:d,ignoreLocation:l,location:g,threshold:p,distance:k},this.pattern=o?t:t.toLowerCase(),this.query=Z(this.pattern,this.options)}return o(e,[{key:"searchIn",value:function(e){var t=this.query;if(!t)return{isMatch:!1,score:1};var n=this.options,r=n.includeMatches;e=n.isCaseSensitive?e:e.toLowerCase();for(var i=0,o=[],c=0,a=0,s=t.length;a<s;a+=1){var u=t[a];o.length=0,i=0;for(var h=0,l=u.length;h<l;h+=1){var d=u[h],v=d.search(e),g=v.isMatch,y=v.indices,p=v.score;if(!g){c=0,i=0,o.length=0;break}if(i+=1,c+=p,r){var m=d.constructor.type;ee.has(m)?o=[].concat(f(o),f(y)):o.push(y)}}if(i){var k={isMatch:!0,score:c/i};return r&&(k.indices=o),k}}return{isMatch:!1,score:1}}}],[{key:"condition",value:function(e,t){return t.useExtendedSearch}}]),e}(),ne=[];function re(e,t){for(var n=0,r=ne.length;n<r;n+=1){var i=ne[n];if(i.condition(e,t))return new i(e,t)}return new T(e,t)}var ie="$and",oe="$or",ce="$path",ae="$val",se=function(e){return!(!e[ie]&&!e[oe])},ue=function(e){return!!e[ce]},he=function(e){return!v(e)&&m(e)&&!se(e)},le=function(e){return c({},ie,Object.keys(e).map((function(t){return c({},t,e[t])})))};function fe(e,t){var n=t.ignoreFieldNorm,r=void 0===n?I.ignoreFieldNorm:n;e.forEach((function(e){var t=1;e.matches.forEach((function(e){var n=e.key,i=e.norm,o=e.score,c=n?n.weight:null;t*=Math.pow(0===o&&c?Number.EPSILON:o,(c||1)*(r?1:i))})),e.score=t}))}function de(e,t){var n=e.matches;t.matches=[],k(n)&&n.forEach((function(e){if(k(e.indices)&&e.indices.length){var n={indices:e.indices,value:e.value};e.key&&(n.key=e.key.src),e.idx>-1&&(n.refIndex=e.idx),t.matches.push(n)}}))}function ve(e,t){t.score=e.score}function ge(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},r=n.includeMatches,i=void 0===r?I.includeMatches:r,o=n.includeScore,c=void 0===o?I.includeScore:o,a=[];return i&&a.push(de),c&&a.push(ve),e.map((function(e){var n=e.idx,r={item:t[n],refIndex:n};return a.length&&a.forEach((function(t){t(e,r)})),r}))}var ye=function(){function e(n){var i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},o=arguments.length>2?arguments[2]:void 0;r(this,e),this.options=t(t({},I),i),this.options.useExtendedSearch,this._keyStore=new S(this.options.keys),this.setCollection(n,o)}return o(e,[{key:"setCollection",value:function(e,t){if(this._docs=e,t&&!(t instanceof $))throw new Error("Incorrect 'index' type");this._myIndex=t||F(this.options.keys,this._docs,{getFn:this.options.getFn,fieldNormWeight:this.options.fieldNormWeight})}},{key:"add",value:function(e){k(e)&&(this._docs.push(e),this._myIndex.add(e))}},{key:"remove",value:function(){for(var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:function(){return!1},t=[],n=0,r=this._docs.length;n<r;n+=1){var i=this._docs[n];e(i,n)&&(this.removeAt(n),n-=1,r-=1,t.push(i))}return t}},{key:"removeAt",value:function(e){this._docs.splice(e,1),this._myIndex.removeAt(e)}},{key:"getIndex",value:function(){return this._myIndex}},{key:"search",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=t.limit,r=void 0===n?-1:n,i=this.options,o=i.includeMatches,c=i.includeScore,a=i.shouldSort,s=i.sortFn,u=i.ignoreFieldNorm,h=g(e)?g(this._docs[0])?this._searchStringList(e):this._searchObjectList(e):this._searchLogical(e);return fe(h,{ignoreFieldNorm:u}),a&&h.sort(s),y(r)&&r>-1&&(h=h.slice(0,r)),ge(h,this._docs,{includeMatches:o,includeScore:c})}},{key:"_searchStringList",value:function(e){var t=re(e,this.options),n=this._myIndex.records,r=[];return n.forEach((function(e){var n=e.v,i=e.i,o=e.n;if(k(n)){var c=t.searchIn(n),a=c.isMatch,s=c.score,u=c.indices;a&&r.push({item:n,idx:i,matches:[{score:s,value:n,norm:o,indices:u}]})}})),r}},{key:"_searchLogical",value:function(e){var t=this,n=function(e,t){var n=(arguments.length>2&&void 0!==arguments[2]?arguments[2]:{}).auto,r=void 0===n||n,i=function e(n){var i=Object.keys(n),o=ue(n);if(!o&&i.length>1&&!se(n))return e(le(n));if(he(n)){var c=o?n[ce]:i[0],a=o?n[ae]:n[c];if(!g(a))throw new Error(x(c));var s={keyId:j(c),pattern:a};return r&&(s.searcher=re(a,t)),s}var u={foals:[],operator:i[0]};return i.forEach((function(t){var r=n[t];v(r)&&r.forEach((function(t){u.foals.push(e(t))}))})),u};return se(e)||(e=le(e)),i(e)}(e,this.options),r=function e(n,r,i){if(!n.foals){var o=n.keyId,c=n.searcher,a=t._findMatches({key:t._keyStore.get(o),value:t._myIndex.getValueForItemAtKeyId(r,o),searcher:c});return a&&a.length?[{idx:i,item:r,matches:a}]:[]}for(var s=[],u=0,h=n.foals.length;u<h;u+=1){var l=e(n.foals[u],r,i);if(l.length)s.push.apply(s,f(l));else if(n.operator===ie)return[]}return s},i=this._myIndex.records,o={},c=[];return i.forEach((function(e){var t=e.$,i=e.i;if(k(t)){var a=r(n,t,i);a.length&&(o[i]||(o[i]={idx:i,item:t,matches:[]},c.push(o[i])),a.forEach((function(e){var t,n=e.matches;(t=o[i].matches).push.apply(t,f(n))})))}})),c}},{key:"_searchObjectList",value:function(e){var t=this,n=re(e,this.options),r=this._myIndex,i=r.keys,o=r.records,c=[];return o.forEach((function(e){var r=e.$,o=e.i;if(k(r)){var a=[];i.forEach((function(e,i){a.push.apply(a,f(t._findMatches({key:e,value:r[i],searcher:n})))})),a.length&&c.push({idx:o,item:r,matches:a})}})),c}},{key:"_findMatches",value:function(e){var t=e.key,n=e.value,r=e.searcher;if(!k(n))return[];var i=[];if(v(n))n.forEach((function(e){var n=e.v,o=e.i,c=e.n;if(k(n)){var a=r.searchIn(n),s=a.isMatch,u=a.score,h=a.indices;s&&i.push({score:u,key:t,value:n,idx:o,norm:c,indices:h})}}));else{var o=n.v,c=n.n,a=r.searchIn(o),s=a.isMatch,u=a.score,h=a.indices;s&&i.push({score:u,key:t,value:o,norm:c,indices:h})}return i}}]),e}();return ye.version="6.6.2",ye.createIndex=F,ye.parseIndex=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=t.getFn,r=void 0===n?I.getFn:n,i=t.fieldNormWeight,o=void 0===i?I.fieldNormWeight:i,c=e.keys,a=e.records,s=new $({getFn:r,fieldNormWeight:o});return s.setKeys(c),s.setIndexRecords(a),s},ye.config=I,function(){ne.push.apply(ne,arguments)}(te),ye},"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).Fuse=t();
\ No newline at end of file diff --git a/assets/icons/actions.svg b/assets/icons/actions.svg new file mode 100644 index 0000000..7b36d3d --- /dev/null +++ b/assets/icons/actions.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="M6 36V25.65q0-.85.5-1.85T8 22.35V17.7q0-1.55 1.075-2.625T11.7 14h9.45q1 0 1.7.45T24 15.7q.45-.8 1.15-1.25.7-.45 1.7-.45h9.45q1.55 0 2.625 1.075T40 17.7v4.65q1 .45 1.5 1.45t.5 1.85V36h-1.55v-4H7.55v4Zm18.75-13.9h13.7v-4.4q0-.95-.625-1.55-.625-.6-1.525-.6h-9.55q-.85 0-1.425.625-.575.625-.575 1.525Zm-15.2 0h13.7v-4.4q0-.9-.575-1.525-.575-.625-1.425-.625H11.7q-.9 0-1.525.625-.625.625-.625 1.525Zm-2 8.35h32.9v-4.8q0-.85-.575-1.425-.575-.575-1.425-.575H9.55q-.85 0-1.425.575-.575.575-.575 1.425Zm32.9 0H7.55h32.9Z"/></svg>
\ No newline at end of file diff --git a/assets/icons/dashboard.svg b/assets/icons/dashboard.svg new file mode 100644 index 0000000..984a8b7 --- /dev/null +++ b/assets/icons/dashboard.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="M26.75 18.7V8H40v10.7ZM8 23.75V8h13.25v15.75ZM26.75 40V24.25H40V40ZM8 40V29.3h13.25V40Zm1.55-17.75H19.7V9.55H9.55Zm18.75 16.2h10.15v-12.7H28.3Zm0-21.3h10.15v-7.6H28.3ZM9.55 38.45H19.7v-7.6H9.55Zm10.15-16.2Zm8.6-5.1Zm0 8.6Zm-8.6 5.1Z"/></svg>
\ No newline at end of file diff --git a/assets/icons/debug.svg b/assets/icons/debug.svg new file mode 100644 index 0000000..e147a29 --- /dev/null +++ b/assets/icons/debug.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="M24 40q-2.7 0-5-1.325T15.45 35h-5.7v-1.55h4.95q-.55-1.6-.55-3.275V26.85h-4.4V25.3h4.4q0-1.7-.025-3.425-.025-1.725.625-3.325h-5V17h5.8q.75-1.45 1.975-2.475Q18.75 13.5 20.2 12.9l-3.9-3.85 1-.95 4.4 4.35q1.2-.35 2.375-.35t2.325.35l4.45-4.35 1 .95L28 12.9q1.45.6 2.6 1.65 1.15 1.05 1.95 2.45h5.7v1.55h-5q.7 1.6.65 3.325-.05 1.725-.05 3.425h4.4v1.55h-4.4v3.325q0 1.675-.55 3.275h4.95V35h-5.7q-1.2 2.4-3.5 3.7Q26.75 40 24 40Zm0-1.55q3.45 0 5.875-2.425T32.3 30.15V22q0-3.45-2.425-5.875T24 13.7q-3.45 0-5.875 2.425T15.7 22v8.15q0 3.45 2.425 5.875T24 38.45ZM20.6 31h6.8v-1.55h-6.8Zm0-8.3h6.8v-1.55h-6.8Zm3.4 3.4Z"/></svg>
\ No newline at end of file diff --git a/assets/icons/favicon/about.png b/assets/icons/favicon/about.png Binary files differindex e17841c..a0fccd7 100644 --- a/assets/icons/favicon/about.png +++ b/assets/icons/favicon/about.png diff --git a/assets/icons/favicon/actions.png b/assets/icons/favicon/actions.png Binary files differnew file mode 100644 index 0000000..9638375 --- /dev/null +++ b/assets/icons/favicon/actions.png diff --git a/assets/icons/favicon/add.png b/assets/icons/favicon/add.png Binary files differindex 607ad2d..c7a0553 100644 --- a/assets/icons/favicon/add.png +++ b/assets/icons/favicon/add.png diff --git a/assets/icons/favicon/admin.png b/assets/icons/favicon/admin.png Binary files differindex 7762f56..720bb43 100644 --- a/assets/icons/favicon/admin.png +++ b/assets/icons/favicon/admin.png diff --git a/assets/icons/favicon/bitset.png b/assets/icons/favicon/bitset.png Binary files differindex 5999238..2fb3b08 100644 --- a/assets/icons/favicon/bitset.png +++ b/assets/icons/favicon/bitset.png diff --git a/assets/icons/favicon/compare.png b/assets/icons/favicon/compare.png Binary files differindex b41d42d..b27d92f 100644 --- a/assets/icons/favicon/compare.png +++ b/assets/icons/favicon/compare.png diff --git a/assets/icons/favicon/complete.png b/assets/icons/favicon/complete.png Binary files differindex b548081..c4c3c36 100644 --- a/assets/icons/favicon/complete.png +++ b/assets/icons/favicon/complete.png diff --git a/assets/icons/favicon/dashboard.png b/assets/icons/favicon/dashboard.png Binary files differnew file mode 100644 index 0000000..8725a74 --- /dev/null +++ b/assets/icons/favicon/dashboard.png diff --git a/assets/icons/favicon/debug.png b/assets/icons/favicon/debug.png Binary files differnew file mode 100644 index 0000000..1cc85b0 --- /dev/null +++ b/assets/icons/favicon/debug.png diff --git a/assets/icons/favicon/delete.png b/assets/icons/favicon/delete.png Binary files differindex 8194dd9..d4ccd46 100644 --- a/assets/icons/favicon/delete.png +++ b/assets/icons/favicon/delete.png diff --git a/assets/icons/favicon/disclaimers.png b/assets/icons/favicon/disclaimers.png Binary files differindex ab18631..31a44ff 100644 --- a/assets/icons/favicon/disclaimers.png +++ b/assets/icons/favicon/disclaimers.png diff --git a/assets/icons/favicon/down.png b/assets/icons/favicon/down.png Binary files differindex 305c94a..a90d632 100644 --- a/assets/icons/favicon/down.png +++ b/assets/icons/favicon/down.png diff --git a/assets/icons/favicon/emergency.png b/assets/icons/favicon/emergency.png Binary files differindex 255bdd6..0754554 100644 --- a/assets/icons/favicon/emergency.png +++ b/assets/icons/favicon/emergency.png diff --git a/assets/icons/favicon/form.png b/assets/icons/favicon/form.png Binary files differindex d194644..09a3324 100644 --- a/assets/icons/favicon/form.png +++ b/assets/icons/favicon/form.png diff --git a/assets/icons/favicon/fronting.png b/assets/icons/favicon/fronting.png Binary files differindex b5dd2f1..e4060be 100644 --- a/assets/icons/favicon/fronting.png +++ b/assets/icons/favicon/fronting.png diff --git a/assets/icons/favicon/global.png b/assets/icons/favicon/global.png Binary files differindex f3a2b9a..1953e5f 100644 --- a/assets/icons/favicon/global.png +++ b/assets/icons/favicon/global.png diff --git a/assets/icons/favicon/history.png b/assets/icons/favicon/history.png Binary files differindex 3a6581e..6ffc90a 100644 --- a/assets/icons/favicon/history.png +++ b/assets/icons/favicon/history.png diff --git a/assets/icons/favicon/home.png b/assets/icons/favicon/home.png Binary files differindex d449bda..39eb6f0 100644 --- a/assets/icons/favicon/home.png +++ b/assets/icons/favicon/home.png diff --git a/assets/icons/favicon/login.png b/assets/icons/favicon/login.png Binary files differindex 6cb2e6e..0006e32 100644 --- a/assets/icons/favicon/login.png +++ b/assets/icons/favicon/login.png diff --git a/assets/icons/favicon/logout.png b/assets/icons/favicon/logout.png Binary files differindex 2f4dd99..344788c 100644 --- a/assets/icons/favicon/logout.png +++ b/assets/icons/favicon/logout.png diff --git a/assets/icons/favicon/nicknames.png b/assets/icons/favicon/nicknames.png Binary files differnew file mode 100644 index 0000000..647bbd0 --- /dev/null +++ b/assets/icons/favicon/nicknames.png diff --git a/assets/icons/favicon/none.png b/assets/icons/favicon/none.png Binary files differindex 6c7f2d1..efe2b05 100644 --- a/assets/icons/favicon/none.png +++ b/assets/icons/favicon/none.png diff --git a/assets/icons/favicon/parser.png b/assets/icons/favicon/parser.png Binary files differindex 1c86e71..00a26cf 100644 --- a/assets/icons/favicon/parser.png +++ b/assets/icons/favicon/parser.png diff --git a/assets/icons/favicon/partial.png b/assets/icons/favicon/partial.png Binary files differindex bd95908..58e2f14 100644 --- a/assets/icons/favicon/partial.png +++ b/assets/icons/favicon/partial.png diff --git a/assets/icons/favicon/pleasure.png b/assets/icons/favicon/pleasure.png Binary files differnew file mode 100644 index 0000000..980b1f9 --- /dev/null +++ b/assets/icons/favicon/pleasure.png diff --git a/assets/icons/favicon/prefix.png b/assets/icons/favicon/prefix.png Binary files differindex 4860cf3..a96ee7f 100644 --- a/assets/icons/favicon/prefix.png +++ b/assets/icons/favicon/prefix.png diff --git a/assets/icons/favicon/relations.png b/assets/icons/favicon/relations.png Binary files differindex 01b6cde..9042590 100644 --- a/assets/icons/favicon/relations.png +++ b/assets/icons/favicon/relations.png diff --git a/assets/icons/favicon/right.png b/assets/icons/favicon/right.png Binary files differindex fd8c56d..c9f6b09 100644 --- a/assets/icons/favicon/right.png +++ b/assets/icons/favicon/right.png diff --git a/assets/icons/favicon/rules.png b/assets/icons/favicon/rules.png Binary files differnew file mode 100644 index 0000000..de4491a --- /dev/null +++ b/assets/icons/favicon/rules.png diff --git a/assets/icons/favicon/score.png b/assets/icons/favicon/score.png Binary files differindex ef82f07..6480b7f 100644 --- a/assets/icons/favicon/score.png +++ b/assets/icons/favicon/score.png diff --git a/assets/icons/favicon/shield.png b/assets/icons/favicon/shield.png Binary files differindex f8b5056..ed4e995 100644 --- a/assets/icons/favicon/shield.png +++ b/assets/icons/favicon/shield.png diff --git a/assets/icons/favicon/species.png b/assets/icons/favicon/species.png Binary files differindex f65b1b6..a7ee51f 100644 --- a/assets/icons/favicon/species.png +++ b/assets/icons/favicon/species.png diff --git a/assets/icons/favicon/splitting.png b/assets/icons/favicon/splitting.png Binary files differindex 4756275..50c4e24 100644 --- a/assets/icons/favicon/splitting.png +++ b/assets/icons/favicon/splitting.png diff --git a/assets/icons/favicon/terminology.png b/assets/icons/favicon/terminology.png Binary files differindex 3545fed..4a2ab56 100644 --- a/assets/icons/favicon/terminology.png +++ b/assets/icons/favicon/terminology.png diff --git a/assets/icons/favicon/together.png b/assets/icons/favicon/together.png Binary files differindex 7022be0..e60e4a6 100644 --- a/assets/icons/favicon/together.png +++ b/assets/icons/favicon/together.png diff --git a/assets/icons/favicon/toys.png b/assets/icons/favicon/toys.png Binary files differnew file mode 100644 index 0000000..72fa2b3 --- /dev/null +++ b/assets/icons/favicon/toys.png diff --git a/assets/icons/favicon/travel.png b/assets/icons/favicon/travel.png Binary files differindex fad716a..f320430 100644 --- a/assets/icons/favicon/travel.png +++ b/assets/icons/favicon/travel.png diff --git a/assets/icons/favicon/travelling.png b/assets/icons/favicon/travelling.png Binary files differindex 4c26122..b28b20a 100644 --- a/assets/icons/favicon/travelling.png +++ b/assets/icons/favicon/travelling.png diff --git a/assets/icons/favicon/tree.png b/assets/icons/favicon/tree.png Binary files differindex c530f48..01fdb17 100644 --- a/assets/icons/favicon/tree.png +++ b/assets/icons/favicon/tree.png diff --git a/assets/icons/favicon/up.png b/assets/icons/favicon/up.png Binary files differindex ce1a47b..96ceccc 100644 --- a/assets/icons/favicon/up.png +++ b/assets/icons/favicon/up.png diff --git a/assets/icons/favicon/user.png b/assets/icons/favicon/user.png Binary files differindex c98f156..44f3b0d 100644 --- a/assets/icons/favicon/user.png +++ b/assets/icons/favicon/user.png diff --git a/assets/icons/favicon/visibility-depends.png b/assets/icons/favicon/visibility-depends.png Binary files differindex 521e991..c401f23 100644 --- a/assets/icons/favicon/visibility-depends.png +++ b/assets/icons/favicon/visibility-depends.png diff --git a/assets/icons/favicon/visibility-private.png b/assets/icons/favicon/visibility-private.png Binary files differindex 000bc3e..ba18eeb 100644 --- a/assets/icons/favicon/visibility-private.png +++ b/assets/icons/favicon/visibility-private.png diff --git a/assets/icons/favicon/visibility-public.png b/assets/icons/favicon/visibility-public.png Binary files differindex b15ce73..cd14a0e 100644 --- a/assets/icons/favicon/visibility-public.png +++ b/assets/icons/favicon/visibility-public.png diff --git a/assets/icons/nicknames.svg b/assets/icons/nicknames.svg new file mode 100644 index 0000000..f1eef81 --- /dev/null +++ b/assets/icons/nicknames.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="m17.1 40 3.55-3.55H42V40Zm-9.55-1.55H9.4l22.05-22-1.9-1.9-22 22.05Zm27.2-23.15-4.05-4.05 2.1-2.15q.45-.4 1.1-.4.65 0 1.15.45L36.9 11q.4.45.4 1.1 0 .65-.45 1.1Zm-1.1 1.1L10 40H6v-4l23.6-23.65Zm-3.15-.95-.95-.9 1.9 1.9Z"/></svg>
\ No newline at end of file diff --git a/assets/icons/rules.svg b/assets/icons/rules.svg new file mode 100644 index 0000000..633f0b2 --- /dev/null +++ b/assets/icons/rules.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="M8.25 41v-1.55h22V41Zm11.8-11.35-9.8-9.8 2.5-2.6 9.9 9.8Zm11-11-9.8-9.9 2.6-2.5 9.8 9.8ZM41.5 38.6l-25-25 1.1-1.1 25 25Z"/></svg>
\ No newline at end of file diff --git a/assets/icons/toys.svg b/assets/icons/toys.svg new file mode 100644 index 0000000..3fcc608 --- /dev/null +++ b/assets/icons/toys.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="M24 38q-4.15 0-7.075-2.825T14 28.3h1.55q0 3.45 2.5 5.8 2.5 2.35 5.95 2.35t5.95-2.35q2.5-2.35 2.5-5.8H34q0 4.05-2.925 6.875T24 38Zm-.05-9.35L17.3 22l1.1-1.1 4.75 4.8V11.05h1.55v14.6l4.75-4.75 1.1 1.1Z"/></svg>
\ No newline at end of file diff --git a/includes/backup.php b/includes/backup.php new file mode 100644 index 0000000..18ed6a4 --- /dev/null +++ b/includes/backup.php @@ -0,0 +1,81 @@ +<?php + +$root = array_filter(scandir("data"), function ($i) { + return !str_starts_with($i, "."); +}); +$files = []; +$data = [ + "date" => date('c'), + "files" => [] +]; + +foreach ($root as $file) { + if ($file === "backup.poniesbackup" || $file === "backup.ponieskey" || $file === "encrypted" || str_ends_with($file, ".poniesbackup")) continue; + + if (is_dir("data/$file")) { + foreach (array_filter(scandir("data/$file"), function ($i) { + return !str_starts_with($i, "."); + }) as $dirfile) { + if ($dirfile === "backup.poniesbackup" || $dirfile === "backup.ponieskey" || $dirfile === "encrypted" || str_ends_with($dirfile, ".poniesbackup")) continue; + + $files[] = [ + "dir" => $file, + "file" => $dirfile + ]; + } + } else { + $files[] = [ + "dir" => "", + "file" => $file + ]; + } +} + +foreach ($files as $file) { + $file["mime"] = mime_content_type("data/$file[dir]/$file[file]"); + $file["checksum"] = [ + sha1_file("data/$file[dir]/$file[file]"), + md5_file("data/$file[dir]/$file[file]") + ]; + $file["content"] = base64_encode(file_get_contents("data/$file[dir]/$file[file]")); + + $data["files"][] = $file; +} + +function pkcs7_pad($data, $size) { + $length = $size - strlen($data) % $size; + return $data . str_repeat(chr($length), $length); +} + +if (!file_exists("./data/backup.ponieskey")) { + $key = openssl_random_pseudo_bytes(512); + $iv = openssl_random_pseudo_bytes(16); + file_put_contents("./data/backup.ponieskey", base64_encode(json_encode([ + "iv" => bin2hex($iv), + "key" => bin2hex($key) + ]))); +} else { + $key_raw = json_decode(base64_decode(file_get_contents("./data/backup.ponieskey")), true); + $key = hex2bin($key_raw["key"]); + $iv = hex2bin($key_raw["iv"]); +} + +$payload = json_encode($data); +$encrypted = openssl_encrypt(pkcs7_pad($payload, 16), 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv); + +file_put_contents("./data/backup.poniesbackup", $encrypted); +@mkdir("./data/encrypted"); + +$id = str_replace(":", "-", date('c')); +copy("./data/backup.poniesbackup", "./data/encrypted/" . $id . ".poniesbackup"); + +exec("scp ./data/encrypted/" . $id . ".poniesbackup fedora@bridlewood.equestria.dev:/opt/ponies"); +exec('ssh fedora@bridlewood.equestria.dev bash -c "cd /opt/ponies; ls -tp | grep -v \'/$\' | tail -n +20 | xargs -I {} rm -- {}"'); + +exec("scp ./data/encrypted/" . $id . ".poniesbackup root@canterlot.equestria.dev:/opt/ponies"); +exec('ssh root@canterlot.equestria.dev bash -c "cd /opt/ponies; ls -tp | grep -v \'/$\' | tail -n +20 | xargs -I {} rm -- {}"'); + +copy("./data/encrypted/" . $id . ".poniesbackup", "/opt/ponies/" . $id . ".poniesbackup"); +exec('bash -c "cd /opt/ponies; ls -tp | grep -v \'/$\' | tail -n +20 | xargs -I {} rm -- {}"'); + +unlink("./data/encrypted/" . $id . ".poniesbackup");
\ No newline at end of file diff --git a/includes/banner.php b/includes/banner.php index 6305857..2582b63 100644 --- a/includes/banner.php +++ b/includes/banner.php @@ -322,14 +322,14 @@ function getMemberBannerData(string $id, string $system, bool $french = false) { if ($metadata["host"] ?? false) { if (!$travelling[$member['id']]["travelling"]) { $badges[] = [ - "id" => "host", + "id" => "mcf", "color" => "primary", "html" => ( $french ? - '<span data-bs-toggle="tooltip" data-bs-html="true" title="<b>Hôte</b><br>' . ucfirst(getMemberPronouns($member['pronouns'])["subjective"]) . ' ' . (getMemberPronouns($member['pronouns'])["third"] ? "is" : "are") . ' the one who fronts the most often in ' . getMemberPronouns($member['pronouns'])["possessive_det"] . ' system." class="badge rounded-pill bg-primary">Hôte</span>' + '<span data-bs-toggle="tooltip" data-bs-html="true" title="<b>Fronteuse la plus présente</b><br>' . ucfirst(getMemberPronouns($member['pronouns'])["subjective"]) . ' ' . (getMemberPronouns($member['pronouns'])["third"] ? "is" : "are") . ' the one who fronts the most often in ' . getMemberPronouns($member['pronouns'])["possessive_det"] . ' system." class="badge rounded-pill bg-primary">Fronteuse la plus présente</span>' : - '<span data-bs-toggle="tooltip" data-bs-html="true" title="<b>Host</b><br>' . ucfirst(getMemberPronouns($member['pronouns'])["subjective"]) . ' ' . (getMemberPronouns($member['pronouns'])["third"] ? "is" : "are") . ' the one who fronts the most often in ' . getMemberPronouns($member['pronouns'])["possessive_det"] . ' system." class="badge rounded-pill bg-primary">Host</span>' + '<span data-bs-toggle="tooltip" data-bs-html="true" title="<b>Most common fronter</b><br>' . ucfirst(getMemberPronouns($member['pronouns'])["subjective"]) . ' ' . (getMemberPronouns($member['pronouns'])["third"] ? "is" : "are") . ' the one who fronts the most often in ' . getMemberPronouns($member['pronouns'])["possessive_det"] . ' system." class="badge rounded-pill bg-primary">Most common fronter</span>' ) ]; } @@ -338,7 +338,7 @@ function getMemberBannerData(string $id, string $system, bool $french = false) { if (($metadata["age_spells"] ?? false) && !$french) { $badges[] = [ "id" => "age_spells", - "color" => "primary", + "color" => "#6f42c1", "html" => '<span data-bs-toggle="tooltip" data-bs-html="true" title="<b>Affected by age spells</b><br>' . ucfirst(getMemberPronouns($member['pronouns'])["subjective"]) . ' can feel younger than ' . getMemberPronouns($member['pronouns'])["subjective"] . ' actually ' . (getMemberPronouns($member['pronouns'])["third"] ? "is" : "are") . ' when somepony else casts an age spell on ' . getMemberPronouns($member['pronouns'])["object"] . '." class="badge rounded-pill" style="background-color: #6f42c1;">Affected by age spells</span>' ]; } @@ -357,6 +357,22 @@ function getMemberBannerData(string $id, string $system, bool $french = false) { ]; } + if ($metadata["less_frequent"] ?? false) { + $badges[] = [ + "id" => "nonverbal", + "color" => "#fd7e14", + "html" => '<span data-bs-toggle="tooltip" data-bs-html="true" title="<b>Fronts less often</b><br>' . ucfirst(getMemberPronouns($member['pronouns'])["subjective"]) . ' ' . (getMemberPronouns($member['pronouns'])["third"] ? "front" : "fronts") . ' less often than ' . getMemberPronouns($member['pronouns'])["possessive_det"] . ' headmates, this can due to various reasons." class="badge rounded-pill" style="background-color:#fd7e14;">Fronts less often</span>' + ]; + } + + if ($metadata["nonverbal"] ?? false) { + $badges[] = [ + "id" => "nonverbal", + "color" => "#20c997", + "html" => '<span data-bs-toggle="tooltip" data-bs-html="true" title="<b>Non verbal IRL</b><br>' . ucfirst(getMemberPronouns($member['pronouns'])["subjective"]) . ' ' . (getMemberPronouns($member['pronouns'])["third"] ? "is" : "are") . ' non verbal in real life, although text communication is still possible." class="badge rounded-pill" style="background-color:#20c997;">Non verbal IRL</span>' + ]; + } + if ($member["name"] === "fusion") { $badges[] = [ "id" => "fusion", diff --git a/includes/bitset.php b/includes/bitset.php index 6483669..2e5a645 100644 --- a/includes/bitset.php +++ b/includes/bitset.php @@ -7,6 +7,8 @@ function parseBitset ($bitset) { $median = substr($bin, 10 + 16, 1) !== "0"; $little = bindec(substr($bin, 11 + 16, 2)); $food = bindec(substr($bin, 16, 2)); + $nonverbal = substr($bin, 15, 1) !== "0"; + $lessFrequent = substr($bin, 14, 1) !== "0"; $magic = bindec(substr($bin, 2 + 16, 3)); $sensitivity = bindec(substr($bin, 5 + 16, 3)); $protector = substr($bin, 13 + 16, 1) !== "0"; @@ -63,6 +65,8 @@ function parseBitset ($bitset) { 'sensitivity' => $sensitivity, 'food' => $food, 'plush' => $plush, + 'nonverbal' => $nonverbal, + 'less_frequent' => $lessFrequent, 'age_spells' => $age, 'species' => array_filter([ $species1, diff --git a/includes/emergency.php b/includes/emergency.php index a86e090..5490985 100644 --- a/includes/emergency.php +++ b/includes/emergency.php @@ -1,4 +1,4 @@ -<h2>Emergency Alert +<h2>Emergency alert <details style="display: inline-block;font-size:12px;"> <summary class="text-muted" style="opacity:.5;"></summary> <label><input id="test-mode" type="checkbox"> Test Mode</label> · <label><input id="fake-requests" type="checkbox"> Fake Requests</label> diff --git a/includes/footer.php b/includes/footer.php index 4bf7877..4592b8e 100644 --- a/includes/footer.php +++ b/includes/footer.php @@ -35,8 +35,13 @@ if (!function_exists("timeAgo")) { <hr> <div class="container text-muted"> + <?php + + $refresh = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/refresh.json"), true); + + ?> © <?= date("Y") ?> <a href="https://equestria.horse" target="_blank" class="text-muted">Equestria.dev Developers</a> · <a href="https://git.equestria.dev/equestria.dev/ponies.equestria.horse" target="_blank" class="text-muted">Source Code</a><br> - PluralKit data updated <?= trim(timeAgo(json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/refresh.json"), true)["timestamp"])) ?>, next update in <?php $t = 5 - round((time() - json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/refresh.json"), true)["timestamp"]) / 60); ?><?= $t > 1 ? $t . " minutes" : ($t > 0 ? "a minute" : "a few seconds") ?> + <a href="/-/debug" class="text-muted" style="text-decoration: none;">Data updated <?= trim(timeAgo($refresh["timestamp"])) ?> (<?= date('D j M, G:i:s T', $refresh["timestamp"]) ?>; took <?= round($refresh["duration"] * 1000) ?> ms, <?= count($refresh["restored"]) > 0 ? (count($refresh["restored"]) > 1 ? "reported " . count($refresh["restored"]) . " failures" : "reported 1 failure") : "no failures reported" ?>)</a> <br><br><br> </div> diff --git a/includes/functions.php b/includes/functions.php index 51317ca..db4a8df 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -34,6 +34,26 @@ if (!function_exists("getSystemMember")) { } } +if (!function_exists("getMemberWithoutSystem")) { + function getMemberWithoutSystem(string $id) { + $member = null; + + $members1 = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/ynmuc-members.json"), true); + foreach ($members1 as $m) { + $m["_system"] = "ynmuc"; + if ($m["id"] === $id) $member = $m; + } + + $members2 = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/gdapd-members.json"), true); + foreach ($members2 as $m) { + $m["_system"] = "gdapd"; + if ($m["id"] === $id) $member = $m; + } + + return $member; + } +} + if (!function_exists("showMembersFromList")) { function showMembersFromList(array $list) { foreach ($list as $member) { if ($member['name'] !== "unknown" && $member['name'] !== "fusion") { @@ -167,6 +187,86 @@ if (!function_exists("timeAgo")) { } } +if (!function_exists("timeIn")) { + function timeIn($time): string { + if (!is_numeric($time)) { + $time = strtotime($time); + } + + $periods = ["second", "minute", "hour", "day", "week", "month", "year", "age"]; + $lengths = array("60", "60", "24", "7", "4.35", "12", "100"); + + $now = time(); + + $difference = $time - $now; + if ($difference <= 10 && $difference >= 0) { + return $tense = "now"; + } elseif ($difference > 0) { + $tense = "in"; + } else { + $tense = "ago"; + } + + for ($j = 0; $difference >= $lengths[$j] && $j < count($lengths)-1; $j++) { + $difference /= $lengths[$j]; + } + + $difference = round($difference); + + $period = $periods[$j] . ($difference >1 ? "s" :''); + return "{$tense} {$difference} {$period}"; + } +} + +if (!function_exists("duration")) { + function duration($seconds) { + if ($seconds >= 60) { + if (floor($seconds / 60) >= 60) { + if (floor($seconds / 3600) >= 24) { + $days = floor($seconds / 86400); + return $days . " day" . ($days > 1 ? "s" : ""); + } else { + $hours = floor($seconds / 3600); + return $hours . " hour" . ($hours > 1 ? "s" : ""); + } + } else { + $minutes = floor($seconds / 60); + return $minutes . " minute" . ($minutes > 1 ? "s" : ""); + } + } else { + return $seconds . " seconds"; + } + } +} + +if (!function_exists("relativeDate")) { + function relativeDate($date, $showTime = true) { + if (!is_numeric($date)) $date = strtotime($date); + + if (!$showTime) { + if (date('Y-m-d', $date) === date('Y-m-d')) { + return "today"; + } elseif (date('Y-m-d', $date) === date('Y-m-d', time() + 86400)) { + return "tomorrow"; + } elseif ($date < time() + 518400) { + return date('l', $date); + } else { + return date('D j M', $date); + } + } else { + if (date('Y-m-d', $date) === date('Y-m-d')) { + return "today, <span class='time-adjust'>" . date('H:i', $date) . "</span>"; + } elseif (date('Y-m-d', $date) === date('Y-m-d', time() + 86400)) { + return "tomorrow, <span class='time-adjust'>" . date('H:i', $date) . "</span>"; + } elseif ($date < time() + 518400) { + return date('l', $date) . ", <span class='time-adjust'>" . date('H:i', $date) . "</span>"; + } else { + return date('D j M', $date) . ", <span class='time-adjust'>" . date('H:i', $date) . "</span>"; + } + } + } +} + if (!function_exists("getMemberSystem")) { function getMemberSystem(string $id) { $list = scoreOrderGlobal(); diff --git a/includes/header.php b/includes/header.php index 8ce8389..1f71aa5 100644 --- a/includes/header.php +++ b/includes/header.php @@ -29,6 +29,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $isLogg require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/banner.php"; require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/rainbow.php"; require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/functions.php"; +require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/ical/main.php"; ?> <!doctype html> @@ -230,12 +231,33 @@ require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/functions.php"; grid-template-columns: 1fr !important; text-align: left; } + + #member-icon-mobile { + display: inline-block !important; + } + + #system-info { + grid-template-columns: 1fr !important; + } + + #member-icon, #member-icon-outer { + display: none !important; + } + + #member-relations { + grid-template-columns: 1fr !important; + text-align: left; + } } #page-content a { color: #afd0ff; } + #page-content .btn-outline-light:hover { + color: black !important; + } + #page-content a:hover { opacity: .75; } @@ -616,7 +638,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/functions.php"; } .navbar-collapse.show { - z-index: 999; + z-index: 99999; } @media (max-width: 991px) { @@ -637,6 +659,25 @@ require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/functions.php"; text-align: left; } } + + .linked-card { + opacity: 1 !important; + color: white !important; + text-decoration: none !important; + } + + .linked-card:hover { + opacity: .75 !important; + } + + .linked-card:active { + opacity: .5 !important; + } + + .navbar-brand { + position: relative; + z-index: 9999; + } </style> </head> <body> @@ -712,28 +753,48 @@ require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/functions.php"; <li><hr class="dropdown-divider"></li> <li><h5 class="dropdown-header">Applications</h5></li> + <li><a class="dropdown-item" href="/-/dashboard"> + <img src="/assets/icons/dashboard.svg" class="dropdown-icon" alt="" style="width:24px;vertical-align: middle;"> + <span style="vertical-align: middle;">Dashboard</span> + </a></li> <li><a class="dropdown-item" href="/-/fronting"> <img src="/assets/icons/fronting.svg" class="dropdown-icon" alt="" style="width:24px;vertical-align: middle;"> <span style="vertical-align: middle;">Front planner</span> </a></li> + <!--<li><a class="dropdown-item" href="/-/actions"> + <img src="/assets/icons/actions.svg" class="dropdown-icon" alt="" style="width:24px;vertical-align: middle;"> + <span style="vertical-align: middle;">Actions database</span> + </a></li> + <li><a class="dropdown-item" href="/-/rules"> + <img src="/assets/icons/rules.svg" class="dropdown-icon" alt="" style="width:24px;vertical-align: middle;"> + <span style="vertical-align: middle;">Systems rules</span> + </a></li> + <li><a class="dropdown-item" href="/-/nicknames"> + <img src="/assets/icons/nicknames.svg" class="dropdown-icon" alt="" style="width:24px;vertical-align: middle;"> + <span style="vertical-align: middle;">Relationships nicknames</span> + </a></li> <li><a class="dropdown-item" href="/-/together"> <img src="/assets/icons/together.svg" class="dropdown-icon" alt="" style="width:24px;vertical-align: middle;"> <span style="vertical-align: middle;">Watch Together</span> - </a></li> + </a></li>--> <li><a class="dropdown-item" href="/-/travelling"> <img src="/assets/icons/travel.svg" class="dropdown-icon" alt="" style="width:24px;vertical-align: middle;"> <span style="vertical-align: middle;">System travels manager</span> </a></li> - <li><hr class="dropdown-divider"></li> - - <li><h5 class="dropdown-header">Utilities</h5></li> - <li><a class="dropdown-item" href="/-/splitting"> + <!--<li><a class="dropdown-item" href="/-/splitting"> <img src="/assets/icons/form.svg" class="dropdown-icon" alt="" style="width:24px;vertical-align: middle;"> <span style="vertical-align: middle;">Members by splitting date</span> </a></li> <li><a class="dropdown-item" href="/-/bitset"> <img src="/assets/icons/bitset.svg" class="dropdown-icon" alt="" style="width:24px;vertical-align: middle;"> <span style="vertical-align: middle;">Bitset calculator</span> + </a></li>--> + <li><hr class="dropdown-divider"></li> + + <li><h5 class="dropdown-header">Debugging</h5></li> + <li><a class="dropdown-item" href="/-/debug"> + <img src="/assets/icons/debug.svg" class="dropdown-icon" alt="" style="width:24px;vertical-align: middle;"> + <span style="vertical-align: middle;">Data updater debugging</span> </a></li> <li><a class="dropdown-item" href="/-/score"> <img src="/assets/icons/score.svg" class="dropdown-icon" alt="" style="width:24px;vertical-align: middle;"> diff --git a/includes/ical/LICENSE b/includes/ical/LICENSE new file mode 100644 index 0000000..ce7d41d --- /dev/null +++ b/includes/ical/LICENSE @@ -0,0 +1,24 @@ +Copyright (c) 2014-2022, Roman Ožana +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the <organization> nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/includes/ical/bin/timezones.php b/includes/ical/bin/timezones.php new file mode 100644 index 0000000..3bf3708 --- /dev/null +++ b/includes/ical/bin/timezones.php @@ -0,0 +1,20 @@ +<?php + +/** + * This file generates a map from windows timezones to tz database timezones + * + * @author Clement Wong <cw@clement.hk> + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +$windows_timezones = []; +$windowstimezonexml = new DOMDocument(); +$windowstimezonexml->load('https://raw.githubusercontent.com/unicode-org/cldr/master/common/supplemental/windowsZones.xml'); +$zones = $windowstimezonexml->getElementsByTagName('mapZone'); +foreach ($zones as $zone) { + if ($zone->getAttribute('territory') === '001') { + $windows_timezones[$zone->getAttribute('other')] = $zone->getAttribute('type'); + } +} + +file_put_contents(__DIR__ . '/../src/WindowsTimezones.php', "<?php\n\$windows_timezones = " . var_export($windows_timezones, true) . ';'); + diff --git a/includes/ical/main.php b/includes/ical/main.php new file mode 100644 index 0000000..56d00af --- /dev/null +++ b/includes/ical/main.php @@ -0,0 +1,7 @@ +<?php + +require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/ical/src/EventsList.php"; +require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/ical/src/Freq.php"; +require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/ical/src/IcalParser.php"; +require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/ical/src/Recurrence.php"; +require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/ical/src/WindowsTimezones.php";
\ No newline at end of file diff --git a/includes/ical/src/EventsList.php b/includes/ical/src/EventsList.php new file mode 100644 index 0000000..3325368 --- /dev/null +++ b/includes/ical/src/EventsList.php @@ -0,0 +1,54 @@ +<?php + +namespace om; + +/** + * Copyright (c) 2004-2022 Roman Ožana (https://ozana.cz) + * + * @license BSD-3-Clause + * @author Roman Ožana <roman@ozana.cz> + */ +class EventsList extends \ArrayObject { + + /** + * Return array of Events + * + * @return array + */ + public function getArrayCopy(): array { + return array_values(parent::getArrayCopy()); + } + + /** + * Return sorted EventList (the newest dates are first) + * + * @return $this + */ + public function sorted(): EventsList { + $this->uasort(static function ($a, $b): int { + if ($a['DTSTART'] === $b['DTSTART']) { + return 0; + } + return ($a['DTSTART'] < $b['DTSTART']) ? -1 : 1; + }); + + return $this; + } + + /** + * Return reversed sorted EventList (the oldest dates are first) + * + * @return $this + */ + public function reversed(): EventsList { + $this->uasort(static function ($a, $b): int { + if ($a['DTSTART'] === $b['DTSTART']) { + return 0; + } + return ($a['DTSTART'] > $b['DTSTART']) ? -1 : 1; + }); + + return $this; + } + +}
\ No newline at end of file diff --git a/includes/ical/src/Freq.php b/includes/ical/src/Freq.php new file mode 100644 index 0000000..8557683 --- /dev/null +++ b/includes/ical/src/Freq.php @@ -0,0 +1,633 @@ +<?php + +namespace om; + +use DateTime; +use DateTimeZone; +use Exception; + +/** + * Class taken from https://github.com/coopTilleuls/intouch-iCalendar.git (Freq.php) + * + * @author PC Drew <pc@schoolblocks.com> + */ + +/** + * A class to store Frequency-rules in. Will allow a easy way to find the + * last and next occurrence of the rule. + * + * No - this is so not pretty. But.. ehh.. You do it better, and I will + * gladly accept patches. + * + * Created by trail-and-error on the examples given in the RFC. + * + * TODO: Update to a better way of doing calculating the different options. + * Instead of only keeping track of the best of the current dates found + * it should instead keep a array of all the calculated dates within the + * period. + * This should fix the issues with multi-rule + multi-rule interference, + * and make it possible to implement the SETPOS rule. + * By pushing the next period onto the stack as the last option will + * (hopefully) remove the need for the awful simpleMode + * + * @author Morten Fangel (C) 2008 + * @author Michael Kahn (C) 2013 + * @license http://creativecommons.org/licenses/by-sa/2.5/dk/deed.en_GB CC-BY-SA-DK + */ +class Freq { + + protected array $weekdays = [ + 'MO' => 'monday', + 'TU' => 'tuesday', + 'WE' => 'wednesday', + 'TH' => 'thursday', + 'FR' => 'friday', + 'SA' => 'saturday', + 'SU' => 'sunday', + ]; + protected array $knownRules = [ + 'month', + 'weekno', + 'day', + 'monthday', + 'yearday', + 'hour', + 'minute', + ]; //others : 'setpos', 'second' + + protected array $ruleModifiers = ['wkst']; + protected bool $simpleMode = true; + + protected array $rules = ['freq' => 'yearly', 'interval' => 1]; + protected int $start = 0; + protected string $freq = ''; + + protected array $excluded; //EXDATE + protected array $added; //RDATE + + protected $cache; // getAllOccurrences() + + /** + * Constructs a new Frequency-rule + * + * @param string|array $rule + * @param int $start Unix-timestamp (important : Need to be the start of Event) + * @param array $excluded of int (timestamps), see EXDATE documentation + * @param array $added of int (timestamps), see RDATE documentation + * @throws Exception + */ + public function __construct($rule, int $start, array $excluded = [], array $added = []) { + $this->start = $start; + $this->excluded = []; + + $rules = []; + foreach ($rule as $k => $v) { + $this->rules[strtolower($k)] = $v; + } + + if (isset($this->rules['until']) && is_string($this->rules['until'])) { + $this->rules['until'] = strtotime($this->rules['until']); + } elseif ($this->rules['until'] instanceof DateTime) { + $this->rules['until'] = $this->rules['until']->getTimestamp(); + } + $this->freq = strtolower($this->rules['freq']); + + foreach ($this->knownRules as $rule) { + if (isset($this->rules['by' . $rule])) { + if ($this->isPrerule($rule, $this->freq)) { + $this->simpleMode = false; + } + } + } + + if (!$this->simpleMode) { + if (!(isset($this->rules['byday']) || isset($this->rules['bymonthday']) || isset($this->rules['byyearday']))) { + $this->rules['bymonthday'] = date('d', $this->start); + } + } + + //set until, and cache + if (isset($this->rules['count'])) { + + $cache[$ts] = $ts = $this->start; + for ($n = 1; $n < $this->rules['count']; $n++) { + $ts = $this->findNext($ts); + $cache[$ts] = $ts; + } + $this->rules['until'] = $ts; + + //EXDATE + if (!empty($excluded)) { + foreach ($excluded as $ts) { + unset($cache[$ts]); + } + } + //RDATE + if (!empty($added)) { + $cache = array_unique(array_merge(array_values($cache), $added)); + asort($cache); + } + + $this->cache = array_values($cache); + } + + $this->excluded = $excluded; + $this->added = $added; + } + + private function isPrerule(string $rule, string $freq): bool { + if ($rule === 'year') { + return false; + } + if ($rule === 'month' && $freq === 'yearly') { + return true; + } + if ($rule === 'monthday' && in_array($freq, ['yearly', 'monthly']) && !isset($this->rules['byday'])) { + return true; + } + // TODO: is it faster to do monthday first, and ignore day if monthday exists? - prolly by a factor of 4.. + if ($rule === 'yearday' && $freq === 'yearly') { + return true; + } + if ($rule === 'weekno' && $freq === 'yearly') { + return true; + } + if ($rule === 'day' && in_array($freq, ['yearly', 'monthly', 'weekly'])) { + return true; + } + if ($rule === 'hour' && in_array($freq, ['yearly', 'monthly', 'weekly', 'daily'])) { + return true; + } + if ($rule === 'minute') { + return true; + } + + return false; + } + + /** + * Calculates the next time after the given offset that the rule + * will apply. + * + * The approach to finding the next is as follows: + * First we establish a timeframe to find timestamps in. This is + * between $offset and the end of the period that $offset is in. + * + * We then loop though all the rules (that is a Prerule in the + * current freq.), and finds the smallest timestamp inside the + * timeframe. + * + * If we find something, we check if the date is a valid recurrence + * (with validDate). If it is, we return it. Otherwise we try to + * find a new date inside the same timeframe (but using the new- + * found date as offset) + * + * If no new timestamps were found in the period, we try in the + * next period + * + * @param int $offset + * @return int|bool + * @throws Exception + */ + public function findNext(int $offset) { + if (!empty($this->cache)) { + foreach ($this->cache as $ts) { + if ($ts > $offset) { + return $ts; + } + } + } + + $debug = false; + + //make sure the offset is valid + if ($offset === false || (isset($this->rules['until']) && $offset > $this->rules['until'])) { + if ($debug) printf("STOP: %s\n", date('r', $offset)); + return false; + } + + $found = true; + + //set the timestamp of the offset (ignoring hours and minutes unless we want them to be + //part of the calculations. + if ($debug) printf("O: %s\n", date('r', $offset)); + $hour = (in_array($this->freq, ['hourly', 'minutely']) && $offset > $this->start) ? date('H', $offset) : date( + 'H', $this->start + ); + $minute = (($this->freq === 'minutely' || isset($this->rules['byminute'])) && $offset > $this->start) ? date( + 'i', $offset + ) : date('i', $this->start); + $t = mktime($hour, $minute, date('s', $this->start), date('m', $offset), date('d', $offset), date('Y', $offset)); + if ($debug) printf("START: %s\n", date('r', $t)); + + if ($this->simpleMode) { + if ($offset < $t) { + $ts = $t; + if ($ts && in_array($ts, $this->excluded, true)) { + $ts = $this->findNext($ts); + } + } else { + $ts = $this->findStartingPoint($t, $this->rules['interval'], false); + if (!$this->validDate($ts)) { + $ts = $this->findNext($ts); + } + } + + return $ts; + } + + //EOP needs to have the same TIME as START ($t) + $tO = new DateTime('@' . $t, new DateTimeZone('UTC')); + + $eop = $this->findEndOfPeriod($offset); + $eopO = new DateTime('@' . $eop, new DateTimeZone('UTC')); + $eopO->setTime($tO->format('H'), $tO->format('i'), $tO->format('s')); + $eop = $eopO->getTimestamp(); + unset($eopO, $tO); + + if ($debug) { + echo 'EOP: ' . date('r', $eop) . "\n"; + } + foreach ($this->knownRules as $rule) { + if ($found && isset($this->rules['by' . $rule])) { + if ($this->isPrerule($rule, $this->freq)) { + $subRules = explode(',', $this->rules['by' . $rule]); + $_t = null; + foreach ($subRules as $subRule) { + $imm = call_user_func_array([$this, "ruleBy$rule"], [$subRule, $t]); + if ($imm === false) { + break; + } + if ($debug) { + printf("%s: %s A: %d\n", strtoupper($rule), date('r', $imm), intval($imm > $offset && $imm < $eop)); + } + if ($imm > $offset && $imm <= $eop && ($_t == null || $imm < $_t)) { + $_t = $imm; + } + } + if ($_t !== null) { + $t = $_t; + } else { + $found = $this->validDate($t); + } + } + } + } + + if ($offset < $this->start && $this->start < $t) { + $ts = $this->start; + } elseif ($found && ($t != $offset)) { + if ($this->validDate($t)) { + if ($debug) echo 'OK' . "\n"; + $ts = $t; + } else { + if ($debug) echo 'Invalid' . "\n"; + $ts = $this->findNext($t); + } + } else { + if ($debug) echo 'Not found' . "\n"; + $ts = $this->findNext($this->findStartingPoint($offset, $this->rules['interval'])); + } + if ($ts && in_array($ts, $this->excluded, true)) { + return $this->findNext($ts); + } + + return $ts; + } + + /** + * Finds the starting point for the next rule. It goes $interval + * 'freq' forward in time since the given offset + * + * @param int $offset + * @param int $interval + * @param boolean $truncate + * @return int + */ + private function findStartingPoint(int $offset, int $interval, $truncate = true): int { + $_freq = ($this->freq === 'daily') ? 'day__' : $this->freq; + $t = '+' . $interval . ' ' . substr($_freq, 0, -2) . 's'; + if ($_freq === 'monthly' && $truncate) { + if ($interval > 1) { + $offset = strtotime('+' . ($interval - 1) . ' months ', $offset); // FIXME return type int|false + } + $t = '+' . (date('t', $offset) - date('d', $offset) + 1) . ' days'; + } + + $sp = strtotime($t, $offset); + + if ($truncate) { + $sp = $this->truncateToPeriod($sp, $this->freq); + } + + return $sp; + } + + /** + * Resets the timestamp to the beginning of the + * period specified by freq + * + * Yes - the fall-through is on purpose! + * + * @param int $time + * @param string $freq + * @return int + */ + private function truncateToPeriod(int $time, string $freq): int { + $date = getdate($time); + switch ($freq) { + case 'yearly': + $date['mon'] = 1; + case 'monthly': + $date['mday'] = 1; + case 'daily': + $date['hours'] = 0; + case 'hourly': + $date['minutes'] = 0; + case 'minutely': + $date['seconds'] = 0; + break; + case 'weekly': + if (date('N', $time) == 1) { // FIXME wrong compare, date return string|false + $date['hours'] = 0; + $date['minutes'] = 0; + $date['seconds'] = 0; + } else { + $date = getdate(strtotime('last monday 0:00', $time)); + } + break; + } + return mktime($date['hours'], $date['minutes'], $date['seconds'], $date['mon'], $date['mday'], $date['year']); + } + + private function validDate($t): bool { + if (isset($this->rules['until']) && $t > $this->rules['until']) { + return false; + } + + if (in_array($t, $this->excluded, true)) { + return false; + } + + if (isset($this->rules['bymonth'])) { + $months = explode(',', $this->rules['bymonth']); + if (!in_array(date('m', $t), $months, true)) { + return false; + } + } + if (isset($this->rules['byday'])) { + $days = explode(',', $this->rules['byday']); + foreach ($days as $i => $k) { + $days[$i] = $this->weekdays[preg_replace('/[^A-Z]/', '', $k)]; + } + if (!in_array(strtolower(date('l', $t)), $days, true)) { + return false; + } + } + if (isset($this->rules['byweekno'])) { + $weeks = explode(',', $this->rules['byweekno']); + if (!in_array(date('W', $t), $weeks, true)) { + return false; + } + } + if (isset($this->rules['bymonthday'])) { + $weekdays = explode(',', $this->rules['bymonthday']); + foreach ($weekdays as $i => $k) { + if ($k < 0) { + $weekdays[$i] = date('t', $t) + $k + 1; + } + } + if (!in_array(date('d', $t), $weekdays, true)) { + return false; + } + } + if (isset($this->rules['byhour'])) { + $hours = explode(',', $this->rules['byhour']); + if (!in_array(date('H', $t), $hours, true)) { + return false; + } + } + + return true; + } + + /** + * Finds the earliest timestamp possible outside this period. + * + * @param int $offset + * @return int + */ + public function findEndOfPeriod($offset = 0) { + return $this->findStartingPoint($offset, 1, false); + } + + /** + * Returns the previous (most recent) occurrence of the rule from the + * given offset + * + * @param int $offset + * @return int + * @throws Exception + */ + public function previousOccurrence(int $offset) { + if (!empty($this->cache)) { + $t2 = $this->start; + foreach ($this->cache as $ts) { + if ($ts >= $offset) { + return $t2; + } + $t2 = $ts; + } + } else { + $ts = $this->start; + while (($t2 = $this->findNext($ts)) < $offset) { + if ($t2 == false) { + break; + } + $ts = $t2; + } + } + + return $ts; + } + + /** + * Returns the next occurrence of this rule after the given offset + * + * @param int $offset + * @return int + * @throws Exception + */ + public function nextOccurrence(int $offset) { + if ($offset < $this->start) { + return $this->firstOccurrence(); + } + return $this->findNext($offset); + } + + /** + * Finds the first occurrence of the rule. + * + * @return int timestamp + * @throws Exception + */ + public function firstOccurrence() { + $t = $this->start; + if (in_array($t, $this->excluded)) { + $t = $this->findNext($t); + } + + return $t; + } + + /** + * Finds the absolute last occurrence of the rule from the given offset. + * Builds also the cache, if not set before... + * + * @return int timestamp + * @throws Exception + */ + public function lastOccurrence() { + //build cache if not done + $this->getAllOccurrences(); + //return last timestamp in cache + return end($this->cache); + } + + /** + * Returns all timestamps array(), build the cache if not made before + * + * @return array + * @throws Exception + */ + public function getAllOccurrences() { + if (empty($this->cache)) { + $cache = []; + + //build cache + $next = $this->firstOccurrence(); + while ($next) { + $cache[] = $next; + $next = $this->findNext($next); + } + if (!empty($this->added)) { + $cache = array_unique(array_merge($cache, $this->added)); + asort($cache); + } + $this->cache = $cache; + } + + return $this->cache; + } + + /** + * Applies the BYDAY rule to the given timestamp + * + * @param string $rule + * @param int $t + * @return int + */ + private function ruleByDay(string $rule, int $t): int { + $dir = ($rule[0] === '-') ? -1 : 1; + $dir_t = ($dir === 1) ? 'next' : 'last'; + + $d = $this->weekdays[substr($rule, -2)]; + $s = $dir_t . ' ' . $d . ' ' . date('H:i:s', $t); + + if ($rule == substr($rule, -2)) { + if (date('l', $t) == ucfirst($d)) { + $s = 'today ' . date('H:i:s', $t); + } + + $_t = strtotime($s, $t); + + if ($_t == $t && in_array($this->freq, ['weekly', 'monthly', 'yearly'])) { + // Yes. This is not a great idea.. but hey, it works.. for now + $s = 'next ' . $d . ' ' . date('H:i:s', $t); + $_t = strtotime($s, $_t); + } + + return $_t; + } else { + $_f = $this->freq; + if (isset($this->rules['bymonth']) && $this->freq === 'yearly') { + $this->freq = 'monthly'; + } + if ($dir === -1) { + $_t = $this->findEndOfPeriod($t); + } else { + $_t = $this->truncateToPeriod($t, $this->freq); + } + $this->freq = $_f; + + $c = preg_replace('/[^0-9]/', '', $rule); + $c = ($c == '') ? 1 : $c; + + $n = $_t; + while ($c > 0) { + if ($dir === 1 && $c == 1 && date('l', $t) == ucfirst($d)) { + $s = 'today ' . date('H:i:s', $t); + } + $n = strtotime($s, $n); + $c--; + } + + return $n; + } + } + + private function ruleByMonth($rule, int $t) { + $_t = mktime(date('H', $t), date('i', $t), date('s', $t), $rule, date('d', $t), date('Y', $t)); + if ($t == $_t && isset($this->rules['byday'])) { + // TODO: this should check if one of the by*day's exists, and have a multi-day value + return false; + } else { + return $_t; + } + } + + private function ruleByMonthday($rule, int $t) { + if ($rule < 0) { + $rule = date('t', $t) + $rule + 1; + } + + return mktime(date('H', $t), date('i', $t), date('s', $t), date('m', $t), $rule, date('Y', $t)); + } + + private function ruleByYearday($rule, int $t) { + if ($rule < 0) { + $_t = $this->findEndOfPeriod(); + $d = '-'; + } else { + $_t = $this->truncateToPeriod($t, $this->freq); + $d = '+'; + } + $s = $d . abs($rule - 1) . ' days ' . date('H:i:s', $t); + + return strtotime($s, $_t); + } + + private function ruleByWeekno($rule, int $t) { + if ($rule < 0) { + $_t = $this->findEndOfPeriod(); + $d = '-'; + } else { + $_t = $this->truncateToPeriod($t, $this->freq); + $d = '+'; + } + + $sub = (date('W', $_t) == 1) ? 2 : 1; + $s = $d . abs($rule - $sub) . ' weeks ' . date('H:i:s', $t); + $_t = strtotime($s, $_t); + + return $_t; + } + + private function ruleByHour($rule, int $t) { + return mktime($rule, date('i', $t), date('s', $t), date('m', $t), date('d', $t), date('Y', $t)); + } + + private function ruleByMinute($rule, int $t) { + return mktime(date('h', $t), $rule, date('s', $t), date('m', $t), date('d', $t), date('Y', $t)); + } +} diff --git a/includes/ical/src/IcalParser.php b/includes/ical/src/IcalParser.php new file mode 100644 index 0000000..b4996e4 --- /dev/null +++ b/includes/ical/src/IcalParser.php @@ -0,0 +1,466 @@ +<?php + +namespace om; + +use DateInterval; +use DateTime; +use DateTimeZone; +use Exception; +use InvalidArgumentException; +use RuntimeException; + +/** + * Copyright (c) 2004-2022 Roman Ožana (https://ozana.cz) + * + * @license BSD-3-Clause + * @author Roman Ožana <roman@ozana.cz> + */ +class IcalParser { + + /** @var ?DateTimeZone */ + public ?DateTimeZone $timezone = null; + + /** @var array|null */ + public ?array $data = null; + + /** @var array */ + protected array $counters = []; + + /** @var array */ + private $windowsTimezones; + + public function __construct() { + $this->windowsTimezones = require __DIR__ . '/WindowsTimezones.php'; // load Windows timezones from separate file + } + + /** + * @param string $file + * @param callable|null $callback + * @return array|null + * @throws Exception + */ + public function parseFile(string $file, callable $callback = null): array { + if (!$handle = fopen($file, 'rb')) { + throw new RuntimeException('Can\'t open file' . $file . ' for reading'); + } + fclose($handle); + + return $this->parseString(file_get_contents($file), $callback); + } + + /** + * @param string $string + * @param callable|null $callback + * @param boolean $add if true the parsed string is added to existing data + * @return array|null + * @throws Exception + */ + public function parseString(string $string, callable $callback = null, bool $add = false): ?array { + if ($add === false) { + // delete old data + $this->data = []; + $this->counters = []; + } + + if (!preg_match('/BEGIN:VCALENDAR/', $string)) { + throw new InvalidArgumentException('Invalid ICAL data format'); + } + + $section = 'VCALENDAR'; + + // Replace \r\n with \n + $string = str_replace("\r\n", "\n", $string); + + // Unfold multi-line strings + $string = str_replace("\n ", '', $string); + + foreach (explode("\n", $string) as $row) { + + switch ($row) { + case 'BEGIN:DAYLIGHT': + case 'BEGIN:VALARM': + case 'BEGIN:VTIMEZONE': + case 'BEGIN:VFREEBUSY': + case 'BEGIN:VJOURNAL': + case 'BEGIN:STANDARD': + case 'BEGIN:VTODO': + case 'BEGIN:VEVENT': + $section = substr($row, 6); + $this->counters[$section] = isset($this->counters[$section]) ? $this->counters[$section] + 1 : 0; + continue 2; // while + case 'END:VEVENT': + $section = substr($row, 4); + $currCounter = $this->counters[$section]; + $event = $this->data[$section][$currCounter]; + if (!empty($event['RECURRENCE-ID'])) { + $this->data['_RECURRENCE_IDS'][$event['RECURRENCE-ID']] = $event; + } + + continue 2; // while + case 'END:DAYLIGHT': + case 'END:VALARM': + case 'END:VTIMEZONE': + case 'END:VFREEBUSY': + case 'END:VJOURNAL': + case 'END:STANDARD': + case 'END:VTODO': + continue 2; // while + + case 'END:VCALENDAR': + $veventSection = 'VEVENT'; + if (!empty($this->data[$veventSection])) { + foreach ($this->data[$veventSection] as $currCounter => $event) { + if (!empty($event['RRULE']) || !empty($event['RDATE'])) { + $recurrences = $this->parseRecurrences($event); + if (!empty($recurrences)) { + $this->data[$veventSection][$currCounter]['RECURRENCES'] = $recurrences; + } + + if (!empty($event['UID'])) { + $this->data["_RECURRENCE_COUNTERS_BY_UID"][$event['UID']] = $currCounter; + } + } + } + } + continue 2; // while + } + + [$key, $middle, $value] = $this->parseRow($row); + + if ($callback) { + // call user function for processing line + call_user_func($callback, $row, $key, $middle, $value, $section, $this->counters[$section]); + } else { + if ($section === 'VCALENDAR') { + $this->data[$key] = $value; + } else { + + // use an array since there can be multiple entries for this key. This does not + // break the current implementation--it leaves the original key alone and adds + // a new one specifically for the array of values. + + if ($newKey = $this->isMultipleKey($key)) { + $this->data[$section][$this->counters[$section]][$newKey][] = $value; + } + + // CATEGORIES can be multiple also but there is special case that there are comma separated categories + + if ($this->isMultipleKeyWithCommaSeparation($key)) { + + if (strpos($value, ',') !== false) { + $values = array_map('trim', preg_split('/(?<![^\\\\]\\\\),/', $value)); + } else { + $values = [$value]; + } + + foreach ($values as $value) { + $this->data[$section][$this->counters[$section]][$key][] = $value; + } + + } else { + $this->data[$section][$this->counters[$section]][$key] = $value; + } + + } + + } + } + + return ($callback) ? null : $this->data; + } + + /** + * @param $event + * @return array + * @throws Exception + */ + public function parseRecurrences($event): array { + $recurring = new Recurrence($event['RRULE']); + $exclusions = []; + $additions = []; + + if (!empty($event['EXDATES'])) { + foreach ($event['EXDATES'] as $exDate) { + if (is_array($exDate)) { + foreach ($exDate as $singleExDate) { + $exclusions[] = $singleExDate->getTimestamp(); + } + } else { + $exclusions[] = $exDate->getTimestamp(); + } + } + } + + if (!empty($event['RDATES'])) { + foreach ($event['RDATES'] as $rDate) { + if (is_array($rDate)) { + foreach ($rDate as $singleRDate) { + $additions[] = $singleRDate->getTimestamp(); + } + } else { + $additions[] = $rDate->getTimestamp(); + } + } + } + + $until = $recurring->getUntil(); + if ($until === false) { + //forever... limit to 3 years from now + $end = new DateTime('now'); + $end->add(new DateInterval('P3Y')); // + 3 years + $recurring->setUntil($end); + $until = $recurring->getUntil(); + } + + date_default_timezone_set($event['DTSTART']->getTimezone()->getName()); + $frequency = new Freq($recurring->rrule, $event['DTSTART']->getTimestamp(), $exclusions, $additions); + $recurrenceTimestamps = $frequency->getAllOccurrences(); + $recurrences = []; + foreach ($recurrenceTimestamps as $recurrenceTimestamp) { + $tmp = new DateTime('now', $event['DTSTART']->getTimezone()); + $tmp->setTimestamp($recurrenceTimestamp); + + $recurrenceIDDate = $tmp->format('Ymd'); + $recurrenceIDDateTime = $tmp->format('Ymd\THis'); + if (empty($this->data['_RECURRENCE_IDS'][$recurrenceIDDate]) && + empty($this->data['_RECURRENCE_IDS'][$recurrenceIDDateTime])) { + $gmtCheck = new DateTime('now', new DateTimeZone('UTC')); + $gmtCheck->setTimestamp($recurrenceTimestamp); + $recurrenceIDDateTimeZ = $gmtCheck->format('Ymd\THis\Z'); + if (empty($this->data['_RECURRENCE_IDS'][$recurrenceIDDateTimeZ])) { + $recurrences[] = $tmp; + } + } + } + + return $recurrences; + } + + private function parseRow($row): array { + preg_match('#^([\w-]+);?([\w-]+="[^"]*"|.*?):(.*)$#i', $row, $matches); + + $key = false; + $middle = null; + $value = null; + + if ($matches) { + $key = $matches[1]; + $middle = $matches[2]; + $value = $matches[3]; + $timezone = null; + + if ($key === 'X-WR-TIMEZONE' || $key === 'TZID') { + if (preg_match('#(\w+/\w+)$#i', $value, $matches)) { + $value = $matches[1]; + } + $value = $this->toTimezone($value); + $this->timezone = new DateTimeZone($value); + } + + // have some middle part ? + if ($middle && preg_match_all('#(?<key>[^=;]+)=(?<value>[^;]+)#', $middle, $matches, PREG_SET_ORDER)) { + $middle = []; + foreach ($matches as $match) { + if ($match['key'] === 'TZID') { + $match['value'] = trim($match['value'], "'\""); + $match['value'] = $this->toTimezone($match['value']); + try { + $middle[$match['key']] = $timezone = new DateTimeZone($match['value']); + } catch (Exception $e) { + $middle[$match['key']] = $match['value']; + } + } elseif ($match['key'] === 'ENCODING') { + if ($match['value'] === 'QUOTED-PRINTABLE') { + $value = quoted_printable_decode($value); + } + } + } + } + } + + // process simple dates with timezone + if (in_array($key, ['DTSTAMP', 'LAST-MODIFIED', 'CREATED', 'DTSTART', 'DTEND'], true)) { + try { + $value = new DateTime($value, ($timezone ?: $this->timezone)); + } catch (Exception $e) { + $value = null; + } + } elseif (in_array($key, ['EXDATE', 'RDATE'])) { + $values = []; + foreach (explode(',', $value) as $singleValue) { + try { + $values[] = new DateTime($singleValue, ($timezone ?: $this->timezone)); + } catch (Exception $e) { + // pass + } + } + if (count($values) === 1) { + $value = $values[0]; + } else { + $value = $values; + } + } + + if ($key === 'RRULE' && preg_match_all('#(?<key>[^=;]+)=(?<value>[^;]+)#', $value, $matches, PREG_SET_ORDER)) { + $middle = null; + $value = []; + foreach ($matches as $match) { + if (in_array($match['key'], ['UNTIL'])) { + try { + $value[$match['key']] = new DateTime($match['value'], ($timezone ?: $this->timezone)); + } catch (Exception $e) { + $value[$match['key']] = $match['value']; + } + } else { + $value[$match['key']] = $match['value']; + } + } + } + + //implement 4.3.11 Text ESCAPED-CHAR + $text_properties = [ + 'CALSCALE', 'METHOD', 'PRODID', 'VERSION', 'CATEGORIES', 'CLASS', 'COMMENT', 'DESCRIPTION', + 'LOCATION', 'RESOURCES', 'STATUS', 'SUMMARY', 'TRANSP', 'TZID', 'TZNAME', 'CONTACT', + 'RELATED-TO', 'UID', 'ACTION', 'REQUEST-STATUS', 'URL', + ]; + if (in_array($key, $text_properties, true) || strpos($key, 'X-') === 0) { + if (is_array($value)) { + foreach ($value as &$var) { + $var = strtr($var, ['\\\\' => '\\', '\\N' => "\n", '\\n' => "\n", '\\;' => ';', '\\,' => ',']); + } + } else { + $value = strtr($value, ['\\\\' => '\\', '\\N' => "\n", '\\n' => "\n", '\\;' => ';', '\\,' => ',']); + } + } + + return [$key, $middle, $value]; + } + + /** + * Process timezone and return correct one... + * + * @param string $zone + * @return mixed|null + */ + private function toTimezone(string $zone) { + return $this->windowsTimezones[$zone] ?? $zone; + } + + public function isMultipleKey(string $key): ?string { + return (['ATTACH' => 'ATTACHMENTS', 'EXDATE' => 'EXDATES', 'RDATE' => 'RDATES'])[$key] ?? null; + } + + /** + * @param $key + * @return string|null + */ + public function isMultipleKeyWithCommaSeparation($key): ?string { + return (['X-CATEGORIES' => 'X-CATEGORIES', 'CATEGORIES' => 'CATEGORIES'])[$key] ?? null; + } + + public function getAlarms(): array { + return $this->data['VALARM'] ?? []; + } + + public function getTimezone(): array { + return $this->getTimezones(); + } + + public function getTimezones(): array { + return $this->data['VTIMEZONE'] ?? []; + } + + /** + * Return sorted event list as ArrayObject + * + * @deprecated use IcalParser::getEvents()->sorted() instead + */ + public function getSortedEvents(): \ArrayObject { + return $this->getEvents()->sorted(); + } + + public function getEvents(): EventsList { + $events = new EventsList(); + if (isset($this->data['VEVENT'])) { + foreach ($this->data['VEVENT'] as $iValue) { + $event = $iValue; + + if (empty($event['RECURRENCES'])) { + if (!empty($event['RECURRENCE-ID']) && !empty($event['UID']) && isset($event['SEQUENCE'])) { + $modifiedEventUID = $event['UID']; + $modifiedEventRecurID = $event['RECURRENCE-ID']; + $modifiedEventSeq = (int)$event['SEQUENCE']; + + if (isset($this->data['_RECURRENCE_COUNTERS_BY_UID'][$modifiedEventUID])) { + $counter = $this->data['_RECURRENCE_COUNTERS_BY_UID'][$modifiedEventUID]; + + $originalEvent = $this->data['VEVENT'][$counter]; + if (isset($originalEvent['SEQUENCE'])) { + $originalEventSeq = (int)$originalEvent['SEQUENCE']; + $originalEventFormattedStartDate = $originalEvent['DTSTART']->format('Ymd\THis'); + if ($modifiedEventRecurID === $originalEventFormattedStartDate && $modifiedEventSeq > $originalEventSeq) { + // this modifies the original event + $modifiedEvent = array_replace_recursive($originalEvent, $event); + $this->data['VEVENT'][$counter] = $modifiedEvent; + foreach ($events as $z => $event) { + if ($events[$z]['UID'] === $originalEvent['UID'] && + $events[$z]['SEQUENCE'] === $originalEvent['SEQUENCE']) { + // replace the original event with the modified event + $events[$z] = $modifiedEvent; + break; + } + } + $event = null; // don't add this to the $events[] array again + } elseif (!empty($originalEvent['RECURRENCES'])) { + for ($j = 0; $j < count($originalEvent['RECURRENCES']); $j++) { + $recurDate = $originalEvent['RECURRENCES'][$j]; + $formattedStartDate = $recurDate->format('Ymd\THis'); + if ($formattedStartDate === $modifiedEventRecurID) { + unset($this->data['VEVENT'][$counter]['RECURRENCES'][$j]); + $this->data['VEVENT'][$counter]['RECURRENCES'] = array_values($this->data['VEVENT'][$counter]['RECURRENCES']); + break; + } + } + } + } + } + } + + if (!empty($event)) { + $events->append($event); + } + } else { + $recurrences = $event['RECURRENCES']; + $event['RECURRING'] = true; + $event['DTEND'] = !empty($event['DTEND']) ? $event['DTEND'] : $event['DTSTART']; + $eventInterval = $event['DTSTART']->diff($event['DTEND']); + + $firstEvent = true; + foreach ($recurrences as $j => $recurDate) { + $newEvent = $event; + if (!$firstEvent) { + unset($newEvent['RECURRENCES']); + $newEvent['DTSTART'] = $recurDate; + $newEvent['DTEND'] = clone($recurDate); + $newEvent['DTEND']->add($eventInterval); + } + + $newEvent['RECURRENCE_INSTANCE'] = $j; + $events->append($newEvent); + $firstEvent = false; + } + } + } + } + return $events; + } + + /** + * @return \ArrayObject + * @deprecated use IcalParser::getEvents->reversed(); + */ + public function getReverseSortedEvents(): \ArrayObject { + return $this->getEvents()->reversed(); + } + +} diff --git a/includes/ical/src/Recurrence.php b/includes/ical/src/Recurrence.php new file mode 100644 index 0000000..15f39cd --- /dev/null +++ b/includes/ical/src/Recurrence.php @@ -0,0 +1,234 @@ +<?php + +namespace om; + +use DateTime; +use Exception; + +/** + * Class taken from https://github.com/coopTilleuls/intouch-iCalendar.git (Recurrence.php) + * + * A wrapper for recurrence rules in iCalendar. Parses the given line and puts the + * recurrence rules in the correct field of this object. + * + * See http://tools.ietf.org/html/rfc2445 for more information. Page 39 and onward contains more + * information on the recurrence rules themselves. Page 116 and onward contains + * some great examples which were often used for testing. + * + * @author Steven Oxley + * @author Michael Kahn (C) 2013 + * @license http://creativecommons.org/licenses/by-sa/2.5/dk/deed.en_GB CC-BY-SA-DK + */ +class Recurrence { + + public $rrule; + protected $freq; + protected $until; + protected $count; + protected $interval; + protected $bysecond; + protected $byminute; + protected $byhour; + protected $byday; + protected $bymonthday; + protected $byyearday; + protected $byweekno; + protected $bymonth; + protected $bysetpos; + protected $wkst; + /** + * A list of the properties that can have comma-separated lists for values. + * + * @var array + */ + protected array $listProperties = [ + 'bysecond', 'byminute', 'byhour', 'byday', 'bymonthday', + 'byyearday', 'byweekno', 'bymonth', 'bysetpos', + ]; + + /** + * Creates an recurrence object with a passed in line. Parses the line. + * + * @param array $rrule an om\icalparser row array which will be parsed to get the + * desired information. + */ + public function __construct(array $rrule) { + $this->parseRrule($rrule); + } + + /** + * Parses an 'RRULE' array and sets the member variables of this object. + * Expects a string that looks like this: 'FREQ=WEEKLY;INTERVAL=2;BYDAY=SU,TU,WE' + * + * @param $rrule + */ + protected function parseRrule($rrule): void { + $this->rrule = $rrule; + //loop through the properties in the line and set their associated + //member variables + foreach ($this->rrule as $propertyName => $propertyValue) { + //need the lower-case name for setting the member variable + $propertyName = strtolower($propertyName); + //split up the list of values into an array (if it's a list) + if (in_array($propertyName, $this->listProperties, true)) { + $propertyValue = explode(',', $propertyValue); + } + $this->$propertyName = $propertyValue; + } + } + + /** + * Returns the frequency - corresponds to FREQ in RFC 2445. + * + * @return mixed string if the member has been set, false otherwise + */ + public function getFreq() { + return $this->getMember('freq'); + } + + /** + * Retrieves the desired member variable and returns it (if it's set) + * + * @param string $member name of the member variable + * @return mixed the variable value (if set), false otherwise + */ + protected function getMember(string $member) { + return $this->$member ?? false; + } + + /** + * Returns when the event will go until - corresponds to UNTIL in RFC 2445. + * + * @return mixed string if the member has been set, false otherwise + */ + public function getUntil() { + return $this->getMember('until'); + } + + /** + * Set the $until member + * + * @param mixed $ts timestamp (int) / Valid DateTime format (string) + * @throws Exception + */ + public function setUntil($ts): void { + if ($ts instanceof DateTime) { + $dt = $ts; + } elseif (is_int($ts)) { + $dt = new DateTime('@' . $ts); + } else { + $dt = new DateTime($ts); + } + $this->until = $dt->format('Ymd\THisO'); + $this->rrule['until'] = $this->until; + } + + /** + * Returns the count of the times the event will occur (should only appear if 'until' + * does not appear) - corresponds to COUNT in RFC 2445. + * + * @return mixed string if the member has been set, false otherwise + */ + public function getCount() { + return $this->getMember('count'); + } + + /** + * Returns the interval - corresponds to INTERVAL in RFC 2445. + * + * @return mixed string if the member has been set, false otherwise + */ + public function getInterval() { + return $this->getMember('interval'); + } + + /** + * Returns the bysecond part of the event - corresponds to BYSECOND in RFC 2445. + * + * @return mixed string if the member has been set, false otherwise + */ + public function getBySecond() { + return $this->getMember('bysecond'); + } + + /** + * Returns the byminute information for the event - corresponds to BYMINUTE in RFC 2445. + * + * @return mixed string if the member has been set, false otherwise + */ + public function getByMinute() { + return $this->getMember('byminute'); + } + + /** + * Corresponds to BYHOUR in RFC 2445. + * + * @return mixed string if the member has been set, false otherwise + */ + public function getByHour() { + return $this->getMember('byhour'); + } + + /** + *Corresponds to BYDAY in RFC 2445. + * + * @return mixed string if the member has been set, false otherwise + */ + public function getByDay() { + return $this->getMember('byday'); + } + + /** + * Corresponds to BYMONTHDAY in RFC 2445. + * + * @return mixed string if the member has been set, false otherwise + */ + public function getByMonthDay() { + return $this->getMember('bymonthday'); + } + + /** + * Corresponds to BYYEARDAY in RFC 2445. + * + * @return mixed string if the member has been set, false otherwise + */ + public function getByYearDay() { + return $this->getMember('byyearday'); + } + + /** + * Corresponds to BYWEEKNO in RFC 2445. + * + * @return mixed string if the member has been set, false otherwise + */ + public function getByWeekNo() { + return $this->getMember('byweekno'); + } + + /** + * Corresponds to BYMONTH in RFC 2445. + * + * @return mixed string if the member has been set, false otherwise + */ + public function getByMonth() { + return $this->getMember('bymonth'); + } + + /** + * Corresponds to BYSETPOS in RFC 2445. + * + * @return mixed string if the member has been set, false otherwise + */ + public function getBySetPos() { + return $this->getMember('bysetpos'); + } + + /** + * Corresponds to WKST in RFC 2445. + * + * @return mixed string if the member has been set, false otherwise + */ + public function getWkst() { + return $this->getMember('wkst'); + } +} diff --git a/includes/ical/src/WindowsTimezones.php b/includes/ical/src/WindowsTimezones.php new file mode 100644 index 0000000..9bfa391 --- /dev/null +++ b/includes/ical/src/WindowsTimezones.php @@ -0,0 +1,214 @@ +<?php + +/** + * List of Windows Timezones + */ +return [ + 'Dateline Standard Time' => 'Etc/GMT+12', + '(UTC-12:00) International Date Line West' => 'Etc/GMT+12', + 'UTC-11' => 'Etc/GMT+11', + '(UTC-11:00) Coordinated Universal Time -11' => 'Etc/GMT+11', + 'Hawaiian Standard Time' => 'Pacific/Honolulu', + '(UTC-10:00) Hawaii' => 'Pacific/Honolulu', + 'Alaskan Standard Time' => 'America/Anchorage', + '(UTC-09:00) Alaska' => 'America/Anchorage', + 'Pacific Standard Time (Mexico)' => 'America/Santa_Isabel', + '(UTC-08:00) Baja California' => 'America/Santa_Isabel', + 'Pacific Standard Time' => 'America/Los_Angeles', + 'Pacific Time' => 'America/Los_Angeles', + '(UTC-08:00) Pacific Time (US and Canada)' => 'America/Los_Angeles', + '(UTC-08:00) Pacific Time (US & Canada)' => 'America/Los_Angeles', + 'US Mountain Standard Time' => 'America/Phoenix', + '(UTC-07:00) Arizona' => 'America/Phoenix', + 'Mountain Standard Time (Mexico)' => 'America/Chihuahua', + '(UTC-07:00) Chihuahua, La Paz, Mazatlan' => 'America/Chihuahua', + 'Mountain Standard Time' => 'America/Denver', + 'Mountain Time' => 'America/Denver', + '(UTC-07:00) Mountain Time (US and Canada)' => 'America/Denver', + '(UTC-07:00) Mountain Time (US & Canada)' => 'America/Denver', + 'Central America Standard Time' => 'America/Guatemala', + '(UTC-06:00) Central America' => 'America/Guatemala', + 'Central Standard Time' => 'America/Chicago', + 'Central Time' => 'America/Chicago', + '(UTC-06:00) Central Time (US and Canada)' => 'America/Chicago', + '(UTC-06:00) Central Time (US & Canada)' => 'America/Chicago', + 'Central Standard Time (Mexico)' => 'America/Mexico_City', + '(UTC-06:00) Guadalajara, Mexico City, Monterrey' => 'America/Mexico_City', + 'Canada Central Standard Time' => 'America/Regina', + '(UTC-06:00) Saskatchewan' => 'America/Regina', + 'SA Pacific Standard Time' => 'America/Bogota', + '(UTC-05:00) Bogota, Lima, Quito' => 'America/Bogota', + 'Eastern Standard Time' => 'America/New_York', + 'Eastern Time' => 'America/New_York', + '(UTC-05:00) Eastern Time (US and Canada)' => 'America/New_York', + '(UTC-05:00) Eastern Time (US & Canada)' => 'America/New_York', + 'US Eastern Standard Time' => 'America/Indianapolis', + '(UTC-05:00) Indiana (East)' => 'America/Indianapolis', + 'Venezuela Standard Time' => 'America/Caracas', + '(UTC-04:30) Caracas' => 'America/Caracas', + 'Paraguay Standard Time' => 'America/Asuncion', + '(UTC-04:00) Asuncion' => 'America/Asuncion', + 'Atlantic Standard Time' => 'America/Halifax', + '(UTC-04:00) Atlantic Time (Canada)' => 'America/Halifax', + 'Central Brazilian Standard Time' => 'America/Cuiaba', + '(UTC-04:00) Cuiaba' => 'America/Cuiaba', + 'SA Western Standard Time' => 'America/La_Paz', + '(UTC-04:00) Georgetown, La Paz, Manaus, San Juan' => 'America/La_Paz', + 'Pacific SA Standard Time' => 'America/Santiago', + '(UTC-04:00) Santiago' => 'America/Santiago', + 'Newfoundland Standard Time' => 'America/St_Johns', + '(UTC-03:30) Newfoundland' => 'America/St_Johns', + 'E. South America Standard Time' => 'America/Sao_Paulo', + '(UTC-03:00) Brasilia' => 'America/Sao_Paulo', + 'Argentina Standard Time' => 'America/Buenos_Aires', + '(UTC-03:00) Buenos Aires' => 'America/Buenos_Aires', + 'SA Eastern Standard Time' => 'America/Cayenne', + '(UTC-03:00) Cayenne, Fortaleza' => 'America/Cayenne', + 'Greenland Standard Time' => 'America/Godthab', + '(UTC-03:00) Greenland' => 'America/Godthab', + 'Montevideo Standard Time' => 'America/Montevideo', + '(UTC-03:00) Montevideo' => 'America/Montevideo', + 'Bahia Standard Time' => 'America/Bahia', + 'UTC-02' => 'Etc/GMT+2', + '(UTC-02:00) Coordinated Universal Time -02' => 'Etc/GMT+2', + 'Azores Standard Time' => 'Atlantic/Azores', + '(UTC-01:00) Azores' => 'Atlantic/Azores', + 'Cape Verde Standard Time' => 'Atlantic/Cape_Verde', + '(UTC-01:00) Cabo Verde Is.' => 'Atlantic/Cape_Verde', + 'Morocco Standard Time' => 'Africa/Casablanca', + '(UTC) Casablanca' => 'Africa/Casablanca', + 'UTC' => 'Etc/GMT', + 'Microsoft/Utc' => 'Etc/GMT', + 'GMT Standard Time' => 'Europe/London', + '(UTC) Dublin, Edinburgh, Lisbon, London' => 'Europe/London', + 'Greenwich Standard Time' => 'Atlantic/Reykjavik', + '(UTC) Monrovia, Reykjavik' => 'Atlantic/Reykjavik', + 'W. Europe Standard Time' => 'Europe/Berlin', + '(UTC+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna' => 'Europe/Berlin', + '(UTC+01:00) Amsterdam\, Berlin\, Bern\, Rome\, Stockholm\, Vienna' => 'Europe/Berlin', + '(UTC+01:00) Amsterdam\, Berlin\, Bern\, Rom\, Stockholm\, Wien' => 'Europe/Berlin', + 'Central Europe Standard Time' => 'Europe/Budapest', + '(UTC+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague' => 'Europe/Budapest', + 'Romance Standard Time' => 'Europe/Paris', + '(UTC+01:00) Brussels, Copenhagen, Madrid, Paris' => 'Europe/Paris', + 'Central European Standard Time' => 'Europe/Warsaw', + '(UTC+01:00) Sarajevo, Skopje, Warsaw, Zagreb' => 'Europe/Warsaw', + 'W. Central Africa Standard Time' => 'Africa/Lagos', + '(UTC+01:00) West Central Africa' => 'Africa/Lagos', + 'Namibia Standard Time' => 'Africa/Windhoek', + '(UTC+01:00) Windhoek' => 'Africa/Windhoek', + 'GTB Standard Time' => 'Europe/Bucharest', + '(UTC+02:00) Athens, Bucharest' => 'Europe/Bucharest', + 'Middle East Standard Time' => 'Asia/Beirut', + '(UTC+02:00) Beirut' => 'Asia/Beirut', + 'Egypt Standard Time' => 'Africa/Cairo', + '(UTC+02:00) Cairo' => 'Africa/Cairo', + 'Syria Standard Time' => 'Asia/Damascus', + '(UTC+02:00) Damascus' => 'Asia/Damascus', + 'South Africa Standard Time' => 'Africa/Johannesburg', + '(UTC+02:00) Harare, Pretoria' => 'Africa/Johannesburg', + 'FLE Standard Time' => 'Europe/Kiev', + '(UTC+02:00) Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius' => 'Europe/Kiev', + 'Turkey Standard Time' => 'Europe/Istanbul', + '(UTC+02:00) Istanbul' => 'Europe/Istanbul', + 'Israel Standard Time' => 'Asia/Jerusalem', + '(UTC+02:00) Jerusalem' => 'Asia/Jerusalem', + 'Libya Standard Time' => 'Africa/Tripoli', + 'Jordan Standard Time' => 'Asia/Amman', + '(UTC+02:00) Amman' => 'Asia/Amman', + 'Arabic Standard Time' => 'Asia/Baghdad', + '(UTC+03:00) Baghdad' => 'Asia/Baghdad', + 'Kaliningrad Standard Time' => 'Europe/Kaliningrad', + '(UTC+03:00) Kaliningrad' => 'Europe/Kaliningrad', + 'Arab Standard Time' => 'Asia/Riyadh', + '(UTC+03:00) Kuwait, Riyadh' => 'Asia/Riyadh', + 'E. Africa Standard Time' => 'Africa/Nairobi', + '(UTC+03:00) Nairobi' => 'Africa/Nairobi', + 'Iran Standard Time' => 'Asia/Tehran', + '(UTC+03:30) Tehran' => 'Asia/Tehran', + 'Arabian Standard Time' => 'Asia/Dubai', + '(UTC+04:00) Abu Dhabi, Muscat' => 'Asia/Dubai', + 'Azerbaijan Standard Time' => 'Asia/Baku', + '(UTC+04:00) Baku' => 'Asia/Baku', + 'Russian Standard Time' => 'Europe/Moscow', + '(UTC+04:00) Moscow, St. Petersburg, Volgograd' => 'Europe/Moscow', + 'Mauritius Standard Time' => 'Indian/Mauritius', + '(UTC+04:00) Port Louis' => 'Indian/Mauritius', + 'Georgian Standard Time' => 'Asia/Tbilisi', + '(UTC+04:00) Tbilisi' => 'Asia/Tbilisi', + 'Caucasus Standard Time' => 'Asia/Yerevan', + '(UTC+04:00) Yerevan' => 'Asia/Yerevan', + 'Afghanistan Standard Time' => 'Asia/Kabul', + '(UTC+04:30) Kabul' => 'Asia/Kabul', + 'West Asia Standard Time' => 'Asia/Tashkent', + '(UTC+05:00) Tashkent' => 'Asia/Tashkent', + 'Pakistan Standard Time' => 'Asia/Karachi', + '(UTC+05:00) Islamabad, Karachi' => 'Asia/Karachi', + 'India Standard Time' => 'Asia/Calcutta', + '(UTC+05:30) Chennai, Kolkata, Mumbai, New Delhi' => 'Asia/Calcutta', + 'Sri Lanka Standard Time' => 'Asia/Colombo', + '(UTC+05:30) Sri Jayawardenepura' => 'Asia/Colombo', + 'Nepal Standard Time' => 'Asia/Katmandu', + '(UTC+05:45) Kathmandu' => 'Asia/Katmandu', + 'Central Asia Standard Time' => 'Asia/Almaty', + '(UTC+06:00) Astana' => 'Asia/Almaty', + 'Bangladesh Standard Time' => 'Asia/Dhaka', + '(UTC+06:00) Dhaka' => 'Asia/Dhaka', + 'Ekaterinburg Standard Time' => 'Asia/Yekaterinburg', + '(UTC+06:00) Ekaterinburg' => 'Asia/Yekaterinburg', + 'Myanmar Standard Time' => 'Asia/Rangoon', + '(UTC+06:30) Yangon (Rangoon)' => 'Asia/Rangoon', + 'SE Asia Standard Time' => 'Asia/Bangkok', + '(UTC+07:00) Bangkok, Hanoi, Jakarta' => 'Asia/Bangkok', + 'N. Central Asia Standard Time' => 'Asia/Novosibirsk', + '(UTC+07:00) Novosibirsk' => 'Asia/Novosibirsk', + 'China Standard Time' => 'Asia/Shanghai', + '(UTC+08:00) Beijing, Chongqing, Hong Kong, Urumqi' => 'Asia/Shanghai', + 'North Asia Standard Time' => 'Asia/Krasnoyarsk', + '(UTC+08:00) Krasnoyarsk' => 'Asia/Krasnoyarsk', + 'Singapore Standard Time' => 'Asia/Singapore', + '(UTC+08:00) Kuala Lumpur, Singapore' => 'Asia/Singapore', + 'W. Australia Standard Time' => 'Australia/Perth', + '(UTC+08:00) Perth' => 'Australia/Perth', + 'Taipei Standard Time' => 'Asia/Taipei', + '(UTC+08:00) Taipei' => 'Asia/Taipei', + 'Ulaanbaatar Standard Time' => 'Asia/Ulaanbaatar', + '(UTC+08:00) Ulaanbaatar' => 'Asia/Ulaanbaatar', + 'North Asia East Standard Time' => 'Asia/Irkutsk', + '(UTC+09:00) Irkutsk' => 'Asia/Irkutsk', + 'Tokyo Standard Time' => 'Asia/Tokyo', + '(UTC+09:00) Osaka, Sapporo, Tokyo' => 'Asia/Tokyo', + 'Korea Standard Time' => 'Asia/Seoul', + '(UTC+09:00) Seoul' => 'Asia/Seoul', + 'Cen. Australia Standard Time' => 'Australia/Adelaide', + '(UTC+09:30) Adelaide' => 'Australia/Adelaide', + 'AUS Central Standard Time' => 'Australia/Darwin', + '(UTC+09:30) Darwin' => 'Australia/Darwin', + 'E. Australia Standard Time' => 'Australia/Brisbane', + '(UTC+10:00) Brisbane' => 'Australia/Brisbane', + 'AUS Eastern Standard Time' => 'Australia/Sydney', + '(UTC+10:00) Canberra, Melbourne, Sydney' => 'Australia/Sydney', + 'West Pacific Standard Time' => 'Pacific/Port_Moresby', + '(UTC+10:00) Guam, Port Moresby' => 'Pacific/Port_Moresby', + 'Tasmania Standard Time' => 'Australia/Hobart', + '(UTC+10:00) Hobart' => 'Australia/Hobart', + 'Yakutsk Standard Time' => 'Asia/Yakutsk', + '(UTC+10:00) Yakutsk' => 'Asia/Yakutsk', + 'Central Pacific Standard Time' => 'Pacific/Guadalcanal', + '(UTC+11:00) Solomon Is., New Caledonia' => 'Pacific/Guadalcanal', + 'Vladivostok Standard Time' => 'Asia/Vladivostok', + '(UTC+11:00) Vladivostok' => 'Asia/Vladivostok', + 'New Zealand Standard Time' => 'Pacific/Auckland', + '(UTC+12:00) Auckland, Wellington' => 'Pacific/Auckland', + 'UTC+12' => 'Etc/GMT-12', + '(UTC+12:00) Coordinated Universal Time +12' => 'Etc/GMT-12', + 'Fiji Standard Time' => 'Pacific/Fiji', + '(UTC+12:00) Fiji' => 'Pacific/Fiji', + 'Magadan Standard Time' => 'Asia/Magadan', + '(UTC+12:00) Magadan' => 'Asia/Magadan', + 'Tonga Standard Time' => 'Pacific/Tongatapu', + '(UTC+13:00) Nuku\'alofa' => 'Pacific/Tongatapu', + 'Samoa Standard Time' => 'Pacific/Apia', + '(UTC-11:00)Samoa' => 'Pacific/Apia', + 'W. Europe Standard Time 1' => 'Europe/Berlin', +]; diff --git a/includes/keywords.php b/includes/keywords.php new file mode 100644 index 0000000..4d6afaf --- /dev/null +++ b/includes/keywords.php @@ -0,0 +1,100 @@ +<?php + +function getKeyWords() { + $actions = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/actions.json"), true); + $toys = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/toys.json"), true); + $pages = []; + + foreach ($actions as $action) { + $base = strtolower($action["name"]); + $addKeywords = $action["keywords"]; + $keywords = [ + $base, + ucfirst($base), + ucwords($base) + ]; + + for ($i = 0; $i < strlen($base); $i++) { + $keywords[] = substr($base, 0, $i) . strtoupper(substr($base, $i, 1)) . substr($base, $i + 1, strlen($base) - $i - 1); + + for ($j = 0; $j < strlen($base); $j++) { + $keywords[] = substr($base, 0, $i) . strtoupper(substr($base, $i,$j)) . substr($base, $i + $j, strlen($base) - $i - $j); + } + } + + foreach ($addKeywords as $keyword) { + $keywords[] = $keyword; + $keywords[] = ucfirst($keyword); + $keywords[] = ucwords($keyword); + + for ($i = 0; $i < strlen($keyword); $i++) { + $keywords[] = substr($keyword, 0, $i) . strtoupper(substr($keyword, $i, 1)) . substr($keyword, $i + 1, strlen($keyword) - $i - 1); + + for ($j = 0; $j < strlen($keyword); $j++) { + $keywords[] = substr($keyword, 0, $i) . strtoupper(substr($keyword, $i, $j)) . substr($keyword, $i + $j, strlen($keyword) - $i - $j); + } + } + } + + $pages[$action["id"]] = [ + "keywords" => array_unique($keywords), + "link" => "/-/actions/$action[id]" + ]; + } + + foreach ($toys as $toy) { + $base = strtolower($toy["name"]); + $addKeywords = $toy["keywords"]; + $keywords = [ + $base, + ucfirst($base), + ucwords($base) + ]; + + for ($i = 0; $i < strlen($base); $i++) { + $keywords[] = substr($base, 0, $i) . strtoupper(substr($base, $i, 1)) . substr($base, $i + 1, strlen($base) - $i - 1); + + for ($j = 0; $j < strlen($base); $j++) { + $keywords[] = substr($base, 0, $i) . strtoupper(substr($base, $i, $j)) . substr($base, $i + $j, strlen($base) - $i - $j); + } + } + + foreach ($addKeywords as $keyword) { + $keywords[] = $keyword; + $keywords[] = ucfirst($keyword); + $keywords[] = ucwords($keyword); + + for ($i = 0; $i < strlen($keyword); $i++) { + $keywords[] = substr($keyword, 0, $i) . strtoupper(substr($keyword, $i, 1)) . substr($keyword, $i + 1, strlen($keyword) - $i - 1); + + for ($j = 0; $j < strlen($keyword); $j++) { + $keywords[] = substr($keyword, 0, $i) . strtoupper(substr($keyword, $i, $j)) . substr($keyword, $i + $j, strlen($keyword) - $i - $j); + } + } + } + + $pages[$toy["id"]] = [ + "keywords" => array_unique($keywords), + "link" => "/-/toys/$toy[id]" + ]; + } + + $keywords = []; + foreach ($pages as $page) { + foreach ($page["keywords"] as $keyword) { + $keywords[$keyword] = $page["link"]; + } + } + + return $keywords; +} + +function replaceKeyWords(string $input): string { + $keywords = getKeyWords(); + + foreach ($keywords as $keyword => $url) { + $input = str_replace($keyword, "<a href='$url'>$keyword</a>", $input); + } + + return $input; +}
\ No newline at end of file diff --git a/includes/member.php b/includes/member.php index d027fa7..f8050b0 100644 --- a/includes/member.php +++ b/includes/member.php @@ -165,7 +165,7 @@ if ($memberData["name"] === "fusion") { <li> <b>Score breakdown:</b> <code><?= $score["total"] ?></code> <ul> - <li><b>Host score:</b> <code><?= $score["host"] ?></code></li> + <li><b>Most common fronter score:</b> <code><?= $score["host"] ?></code></li> <li><b>Relationships score:</b> <code><?= $score["relations"] ?></code></li> <li><b>Fictive score:</b> <code><?= $score["fictive"] ?></code></li> <li><b>Median score:</b> <code><?= $score["median"] ?></code></li> diff --git a/includes/planner.php b/includes/planner.php index 72575cd..64d87e4 100644 --- a/includes/planner.php +++ b/includes/planner.php @@ -1,11 +1,20 @@ <?php +require_once $_SERVER["DOCUMENT_ROOT"] . "/includes/travelling.php"; global $travelling; +require_once $_SERVER["DOCUMENT_ROOT"] . "/includes/score.php"; +require_once $_SERVER["DOCUMENT_ROOT"] . "/includes/pronouns.php"; +require_once $_SERVER["DOCUMENT_ROOT"] . "/includes/bitset.php"; +require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $isLoggedIn; global $isUserLoggedIn; +require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/banner.php"; +require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/rainbow.php"; +require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/functions.php"; + $cloudburst = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/ynmuc-planner.json"), true); $raindrops = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/gdapd-planner.json"), true); ?> -<h2>Front Planner</h2> +<h2>Front planner</h2> <table id="planner"> <tbody> <tr class="planner-day"> diff --git a/includes/pleasure.php b/includes/pleasure.php index 1bbfcea..8b4835b 100644 --- a/includes/pleasure.php +++ b/includes/pleasure.php @@ -1,4 +1,4 @@ -<h2>Pleasure Alert +<h2>Pleasure alert <details style="display: inline-block;font-size:12px;"> <summary class="text-muted" style="opacity:.5;"></summary> <label><input id="test-mode" type="checkbox"> Test Mode</label> · <label><input id="fake-requests" type="checkbox"> Fake Requests</label> diff --git a/includes/random.php b/includes/random.php new file mode 100644 index 0000000..b6e7905 --- /dev/null +++ b/includes/random.php @@ -0,0 +1,15 @@ +<?php + +/** + * @throws Exception + */ +function random($length = 13): string { + if (function_exists("random_bytes")) { + $bytes = random_bytes(ceil($length / 2)); + } elseif (function_exists("openssl_random_pseudo_bytes")) { + $bytes = openssl_random_pseudo_bytes(ceil($length / 2)); + } else { + throw new Exception("No cryptographically secure random function available"); + } + return substr(bin2hex($bytes), 0, $length); +}
\ No newline at end of file diff --git a/includes/refresh.php b/includes/refresh.php index 79e3b7f..320af54 100644 --- a/includes/refresh.php +++ b/includes/refresh.php @@ -1,5 +1,6 @@ <?php +$app = json_decode(file_get_contents("./app.json"), true); $start = microtime(true); @mkdir("./data"); @@ -47,10 +48,14 @@ function getSystem(string $id) { getSystem("gdapd"); // Raindrops getSystem("ynmuc"); // Cloudburst +echo("Calendar\n"); +$currentOpStart = microtime(true); +file_put_contents("./data/calendar.ics", file_get_contents($app["calendar"])); +$times["calendar"] = microtime(true) - $currentOpStart; + echo("Downloading images.\n"); if (!file_exists("./data/images")) mkdir("./data/images"); -$currentOpStart = microtime(true); foreach (json_decode(file_get_contents("./data/gdapd-members.json"), true) as $member) { $currentOpStart2 = microtime(true); echo(" " . $member['id'] . "\n"); @@ -68,9 +73,7 @@ foreach (json_decode(file_get_contents("./data/gdapd-members.json"), true) as $m } $times["images-gdapd-" . $member['id']] = microtime(true) - $currentOpStart2; } -$times["images-gdapd"] = microtime(true) - $currentOpStart; -$currentOpStart = microtime(true); foreach (json_decode(file_get_contents("./data/ynmuc-members.json"), true) as $member) { $currentOpStart2 = microtime(true); echo(" " . $member['id'] . "\n"); @@ -88,7 +91,6 @@ foreach (json_decode(file_get_contents("./data/ynmuc-members.json"), true) as $m } $times["images-ynmuc-" . $member['id']] = microtime(true) - $currentOpStart2; } -$times["images-ynmuc"] = microtime(true) - $currentOpStart; $currentOpStart = microtime(true); function isJson($string): bool { @@ -104,11 +106,13 @@ foreach ($files as $file) { } $times["restore"] = microtime(true) - $currentOpStart; -$time = (microtime(true) - $start); +require_once "./backup.php"; + +$time = array_sum($times); echo("Completed in " . $time . " seconds.\n"); file_put_contents("./data/refresh.json", json_encode([ - "timestamp" => time(), + "timestamp" => microtime(true), "duration" => $time, "restored" => $restored, "times" => $times diff --git a/includes/restore.php b/includes/restore.php new file mode 100644 index 0000000..72748ab --- /dev/null +++ b/includes/restore.php @@ -0,0 +1,130 @@ +<?php + +function isJson($string): bool { + json_decode($string); + return (json_last_error() == JSON_ERROR_NONE); +} + +function pkcs7_unpad($data) { + return substr($data, 0, -ord($data[strlen($data) - 1])); +} + +function timeAgo($time): string { + if (!is_numeric($time)) { + $time = strtotime($time); + } + + $periods = ["second", "minute", "hour", "day", "week", "month", "year", "age"]; + $lengths = array("60", "60", "24", "7", "4.35", "12", "100"); + + $now = time(); + + $difference = $now - $time; + if ($difference <= 10 && $difference >= 0) { + return $tense = "now"; + } elseif ($difference > 0) { + $tense = "ago"; + } else { + $tense = "later"; + } + + for ($j = 0; $difference >= $lengths[$j] && $j < count($lengths)-1; $j++) { + $difference /= $lengths[$j]; + } + + $difference = round($difference); + + $period = $periods[$j] . ($difference >1 ? "s" :''); + return "{$difference} {$period} {$tense}"; +} + +if (!isset($_SERVER['argv'][1]) || !isset($_SERVER['argv'][2])) { + echo("Usage: php " . $_SERVER['argv'][0] . " <file> <key>\n"); + die(); +} else { + $file = @file_get_contents($_SERVER['argv'][1]); + $raw = @file_get_contents($_SERVER['argv'][2]); + + if ($file === false) { + echo("Unable to open backup file\n"); + die(); + } + + if ($raw === false) { + echo("Unable to open key file\n"); + die(); + } + + $raw2 = base64_decode($raw); + + if (!isJson($raw2)) { + echo("Key file is corrupt\n"); + die(); + } + + $keydata = json_decode($raw2, true); + + if (!is_array($keydata) || !isset($keydata["iv"]) || !isset($keydata["key"])) { + echo("Key file is invalid\n"); + die(); + } + + $iv = hex2bin($keydata["iv"]); + $key = hex2bin($keydata["key"]); + + $decrypted = openssl_decrypt($file, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv); + + if ($decrypted === false) { + echo("Unable to decrypt backup\n"); + die(); + } + + $unpadded = pkcs7_unpad($decrypted); + + if (!is_string($unpadded)) { + echo("Unable to decrypt backup\n"); + die(); + } + + if (!isJson($unpadded)) { + echo("Backup is corrupt\n"); + die(); + } + + $data = json_decode($unpadded, true); + + if (!is_array($data) || !isset($data["date"]) || !isset($data["files"])) { + echo("Backup is invalid\n"); + die(); + } + + echo(realpath($_SERVER['argv'][1]) . "\n Key: " . $_SERVER['argv'][2] . "\n Date: " . date('r', strtotime($data["date"])) . " (" . timeAgo($data["date"]) . ")" . "\n Contents: " . count($data["files"]) . " files\n"); + + @mkdir("./_restored"); + + $index = 0; + foreach ($data["files"] as $file) { + if ($file["dir"] === "") { + print("[$index] /" . $file["file"] . "\n"); + } else { + print("[$index] /" . $file["dir"] . "/" . $file["file"] . "\n"); + } + + $content = base64_decode($file["content"]); + if (sha1($content) !== $file["checksum"][0]) { + print(" Backed up file is corrupted (SHA1 mismatch)\n Expected: " . $file["checksum"][0] . "\n Got: " . sha1($content) . "\n"); + die("Backup aborted.\n"); + } + if (md5($content) !== $file["checksum"][1]) { + print(" Backed up file is corrupted (MD5 mismatch)\n Expected: " . $file["checksum"][1] . "\n Got: " . md5($content) . "\n"); + die("Backup aborted.\n"); + } + + @mkdir("./_restored/" . $file["dir"], 0777, true); + file_put_contents("./_restored/" . $file["dir"] . "/" . $file["file"], $content); + + $index++; + } + + print("Restored backup to ./_restored; review files before restoring to production\n"); +}
\ No newline at end of file diff --git a/includes/session.php b/includes/session.php index 2157f5f..b440c9c 100644 --- a/includes/session.php +++ b/includes/session.php @@ -11,6 +11,12 @@ if (isset($_COOKIE['PEH2_SESSION_TOKEN'])) { if (file_exists($_SERVER['DOCUMENT_ROOT'] . "/includes/tokens/" . str_replace(".", "", str_replace("/", "", $_COOKIE['PEH2_SESSION_TOKEN'])))) { $_PROFILE = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/tokens/" . str_replace(".", "", str_replace("/", "", $_COOKIE['PEH2_SESSION_TOKEN']))), true); + + if (isset($_GET['invert'])) { + $_PROFILE["login"] = $_PROFILE["login"] === "raindrops" ? "cloudburst" : "raindrops"; + $_PROFILE["name"] = $_PROFILE["name"] === "Raindrops System" ? "Cloudburst System" : "Raindrops System"; + } + $isLoggedIn = true; } else { $isLoggedIn = false; diff --git a/includes/sysbanner.php b/includes/sysbanner.php index 5ffe000..3a2b8c1 100644 --- a/includes/sysbanner.php +++ b/includes/sysbanner.php @@ -18,7 +18,7 @@ $travelling = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includ <h3 style="height:max-content;"><?= $systemCommonName ?></h3> <div style="height:max-content;display:grid;grid-template-columns: repeat(5, 1fr);" id="member-card"> <span> - <b>Host: </b><?php + <b><abbr title="Most common fronter" data-bs-toggle="tooltip">MCF</abbr>: </b><?php $foundHost = false; $hostGoneTravelling = false; diff --git a/includes/system/compare.php b/includes/system/compare.php index f597cf3..9714488 100644 --- a/includes/system/compare.php +++ b/includes/system/compare.php @@ -27,55 +27,55 @@ function getMember(string $id) { <span class="comparison-header-l2">Member</span> <span class="comparison-header-l3">Member</span> <span class="comparison-header-l4">Member</span> - <span class="comparison-header-l5">Mmbr.</span> + <span class="comparison-header-l5"><abbr title="Member" data-bs-toggle="tooltip">Mmbr.</abbr></span> </span> <span class="comparison-header comparison-item"> <span class="comparison-header-l0">Species</span> <span class="comparison-header-l1">Species</span> <span class="comparison-header-l2">Species</span> <span class="comparison-header-l3">Species</span> - <span class="comparison-header-l4">Spec.</span> - <span class="comparison-header-l5">Spec.</span> + <span class="comparison-header-l4"><abbr title="Species" data-bs-toggle="tooltip">Spec.</abbr></span> + <span class="comparison-header-l5"><abbr title="Species" data-bs-toggle="tooltip">Spec.</abbr></span> </span> <span class="comparison-header comparison-item"> <span class="comparison-header-l0">Relations</span> <span class="comparison-header-l1">Relations</span> <span class="comparison-header-l2">Relations</span> <span class="comparison-header-l3">Relations</span> - <span class="comparison-header-l4">Relt.</span> - <span class="comparison-header-l5">Relt.</span> + <span class="comparison-header-l4"><abbr title="Relations" data-bs-toggle="tooltip">Relt.</abbr></span> + <span class="comparison-header-l5"><abbr title="Relations" data-bs-toggle="tooltip">Relt.</abbr></span> </span> <span class="comparison-header comparison-item"> - <span class="comparison-header-l0">Host</span> - <span class="comparison-header-l1">Host</span> - <span class="comparison-header-l2">Host</span> - <span class="comparison-header-l3">Host</span> - <span class="comparison-header-l4">Hst.</span> - <span class="comparison-header-l5">Hst.</span> + <span class="comparison-header-l0"><abbr title="Most common fronter" data-bs-toggle="tooltip">MCF</abbr></span> + <span class="comparison-header-l1"><abbr title="Most common fronter" data-bs-toggle="tooltip">MCF</abbr></span> + <span class="comparison-header-l2"><abbr title="Most common fronter" data-bs-toggle="tooltip">MCF</abbr></span> + <span class="comparison-header-l3"><abbr title="Most common fronter" data-bs-toggle="tooltip">MCF</abbr></span> + <span class="comparison-header-l4"><abbr title="Most common fronter" data-bs-toggle="tooltip">MCF</abbr></span> + <span class="comparison-header-l5"><abbr title="Most common fronter" data-bs-toggle="tooltip">MCF</abbr></span> </span> <span class="comparison-header comparison-item"> <span class="comparison-header-l0">Fictive</span> <span class="comparison-header-l1">Fictive</span> <span class="comparison-header-l2">Fictive</span> <span class="comparison-header-l3">Fictive</span> - <span class="comparison-header-l4">Fic.</span> - <span class="comparison-header-l5">Fic.</span> + <span class="comparison-header-l4"><abbr title="Fictive" data-bs-toggle="tooltip">Fic.</abbr></span> + <span class="comparison-header-l5"><abbr title="Fictive" data-bs-toggle="tooltip">Fic.</abbr></span> </span> <span class="comparison-header comparison-item"> <span class="comparison-header-l0">Little</span> <span class="comparison-header-l1">Little</span> <span class="comparison-header-l2">Little</span> <span class="comparison-header-l3">Little</span> - <span class="comparison-header-l4">Ltl.</span> - <span class="comparison-header-l5">Ltl.</span> + <span class="comparison-header-l4"><abbr title="Little" data-bs-toggle="tooltip">Ltl.</abbr></span> + <span class="comparison-header-l5"><abbr title="Little" data-bs-toggle="tooltip">Ltl.</abbr></span> </span> <span class="comparison-header comparison-item"> <span class="comparison-header-l0">Protector</span> <span class="comparison-header-l1">Protector</span> - <span class="comparison-header-l2">Protect.</span> - <span class="comparison-header-l3">Protect.</span> - <span class="comparison-header-l4">Prt.</span> - <span class="comparison-header-l5">Prt.</span> + <span class="comparison-header-l2"><abbr title="Protector" data-bs-toggle="tooltip">Protect.</abbr></span> + <span class="comparison-header-l3"><abbr title="Protector" data-bs-toggle="tooltip">Protect.</abbr></span> + <span class="comparison-header-l4"><abbr title="Protector" data-bs-toggle="tooltip">Prt.</abbr></span> + <span class="comparison-header-l5"><abbr title="Protector" data-bs-toggle="tooltip">Prt.</abbr></span> </span> <?php foreach (scoreOrder($members, $systemID) as $member): diff --git a/pages/actions.php b/pages/actions.php new file mode 100644 index 0000000..1fa998a --- /dev/null +++ b/pages/actions.php @@ -0,0 +1,1132 @@ +<?php + +require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $isLoggedIn; +if (!$isLoggedIn) header("Location: /-/login") and die(); + +if (isset($_POST['deleteAction'])) { + $data = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/actions.json"), true); + + $selected = null; + $selectedIndex = -1; + $id = $_POST['action']; + + foreach ($data as $index => $item) { + if ($item["id"] === $id) { + $selectedIndex = $index; + $selected = $item; + break; + } + } + + if ($selected === null) { + header("Location: /-/actions"); + die(); + } + + unset($data[$selectedIndex]); + @mkdir($_SERVER['DOCUMENT_ROOT'] . "/includes/data/oldactions"); + file_put_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/oldactions/" . date('c') . ".json", utf8_encode(json_encode($data))); + file_put_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/actions.json", utf8_encode(json_encode($data))); + header("Location: /-/actions/?d&id=" . $id); + die(); +} + +if (isset($_POST['updateAction'])) { + $data = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/actions.json"), true); + + $selected = null; + $selectedIndex = -1; + $id = $_POST['action']; + + foreach ($data as $index => $item) { + if ($item["id"] === $id) { + $selectedIndex = $index; + $selected = $item; + break; + } + } + + if ($selected === null) { + header("Location: /-/actions"); + die(); + } + + if (isset($_POST["consent"])) { + $selected["consent"] = true; + } else { + $selected["consent"] = false; + } + + if (isset($_POST["name"])) $selected["name"] = strip_tags(trim($_POST["name"])); + if (isset($_POST["example"])) $selected["example"] = strip_tags(trim($_POST["example"])); + if (isset($_POST["irl"])) $selected["irl"] = strip_tags(trim($_POST["irl"])); + if (isset($_POST["keywords"])) $selected["keywords"] = array_map(function ($i) { + return trim($i); + }, explode(",", strip_tags(trim($_POST["keywords"])))); + if (isset($_POST["description"])) $selected["description"] = strip_tags(trim($_POST["description"])); + if (isset($_POST["type"])) $selected["type"] = match ($_POST["type"]) { + "0" => "affectionate", + "1" => "sexual", + "2" => "mixed" + }; + + if (isset($_POST["relations"])) { + $ponies = []; + + foreach ($_POST["relations"] as $relation => $d) { + $ponies[] = [ + "members" => explode("-", $relation), + "deprecated" => isset($d["deprecated"]), + "sexual" => isset($d["sexual"]) + ]; + } + + $selected["ponies"] = $ponies; + } + + global $_PROFILE; + if ($_PROFILE['login'] === "raindrops" && isset($_POST["verified"])) { + $selected["verified"] = true; + } else { + unset($selected["verified"]); + } + + if (isset($_POST["untested"])) { + $selected["untested"] = true; + } else { + unset($selected["untested"]); + } + + $data[$selectedIndex] = $selected; + @mkdir($_SERVER['DOCUMENT_ROOT'] . "/includes/data/oldactions"); + file_put_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/oldactions/" . date('c') . ".json", utf8_encode(json_encode($data))); + file_put_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/actions.json", utf8_encode(json_encode($data))); + + header("Location: /-/actions/" . $id); + die(); +} + +if (isset($_POST['createAction'])) { + require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/random.php"; + $data = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/actions.json"), true); + + if (!isset($_POST["name"]) || !isset($_POST["type"])) { + header("Location: /-/actions"); + die(); + } + if (trim($_POST["name"]) === "" || !is_numeric($_POST["type"])) { + header("Location: /-/actions"); + die(); + } + + $type = match ($_POST["type"]) { + "0" => "affectionate", + "1" => "sexual", + "2" => "mixed" + }; + $name = strip_tags(trim($_POST["name"])); + $id = random(); + + $ponies = []; + + if (isset($_POST["relations"])) { + foreach ($_POST["relations"] as $relation => $_) { + $ponies[] = [ + "members" => explode("-", $relation), + "deprecated" => false, + "sexual" => false + ]; + } + } + + if (isset($_POST["consent"])) { + $consent = true; + } else { + $consent = false; + } + + $data[] = [ + "id" => $id, + "name" => $name, + "type" => $type, + "description" => null, + "ponies" => $ponies, + "example" => null, + "irl" => null, + "keywords" => [], + "consent" => $consent + ]; + + @mkdir($_SERVER['DOCUMENT_ROOT'] . "/includes/data/oldactions"); + file_put_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/oldactions/" . date('c') . ".json", utf8_encode(json_encode($data))); + file_put_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/actions.json", utf8_encode(json_encode($data))); + header("Location: /-/actions/" . $id); + die(); +} + +global $pagename; +$parts = explode("/", $pagename); +$data = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/actions.json"), true); +$toys = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/toys.json"), true); + +$selected = null; +$title = "Actions database"; + +if (isset($parts[1])) { + $id = $parts[1]; + + foreach ($data as $item) { + if ($item["id"] === $id) { + $selected = $item; + break; + } + } + + if ($selected === null) { + header("Location: /-/actions/?nf&id=" . $id); + die(); + } else { + $title = $selected["name"] . " · Actions database"; + } +} + +require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/header.php'; +require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/keywords.php'; + +if (!file_exists($_SERVER['DOCUMENT_ROOT'] . "/includes/data/actions.json")) file_put_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/actions.json", "[]"); + +global $_PROFILE; + +?> + +<script src="/assets/editor/fuse.js"></script> + +<br> +<div class="container"> + <div id="<?= isset($parts[1]) ? "page-content" : "" ?>"> + <?php if (isset($_GET['nf'])): ?> + <div class="alert alert-danger alert-dismissible"> + <button onclick='window.history.pushState({"html":null,"pageTitle":document.title},"", "/-/actions");' type="button" class="btn-close" data-bs-dismiss="alert" style="filter: none !important;"></button> + <b>Error: </b> The requested action (<code><?= strip_tags($_GET['id'] ?? "-") ?></code>) was not found, it may have been deleted or has never existed. + </div> + <?php endif; ?> + + <?php if (isset($_GET['d'])): ?> + <div class="alert alert-success alert-dismissible"> + <button onclick='window.history.pushState({"html":null,"pageTitle":document.title},"", "/-/actions");' type="button" class="btn-close" data-bs-dismiss="alert" style="filter: none !important;"></button> + <b>Success: </b> The action with ID <code><?= strip_tags($_GET['id'] ?? "-") ?></code> has been successfully deleted. + </div> + <?php endif; ?> + + <?php if (isset($parts[1])): ?> + + <h2> + <span style="vertical-align: middle;"><?= $selected["name"] ?></span> + <a href="/-/actions" class="small btn btn-outline-light" style="float:right;margin-top:5px;vertical-align:middle;opacity:1 !important; color:white;">Back</a> + </h2> + <p> + <a onclick="event.target.blur();" data-bs-toggle="modal" data-bs-target="#editor" href="#">Edit</a> · + <?php if ($selected["type"] === "affectionate"): ?> + <span style="" class="badge rounded-pill bg-primary">Affectionate</span> + <?php endif; ?> + <?php if ($selected["type"] === "sexual"): ?> + <span style="" class="badge rounded-pill bg-danger">Sexual</span> + <?php endif; ?> + <?php if ($selected["type"] === "mixed"): ?> + <span style="" class="badge rounded-pill bg-primary">Affectionate</span> <span style="" class="badge rounded-pill bg-danger">Sexual</span> + <?php endif; ?> + <?php if (!isset($item["verified"])): ?><span class="badge bg-warning rounded-pill text-black">Unverified</span><?php endif; ?> + <?php if (isset($item["untested"])): ?><span class="badge bg-info rounded-pill">Untested</span><?php endif; ?> + </p> + + <hr> + <?php if ($selected["consent"] && $selected["type"] !== "affectionate"): ?> + <div class="alert alert-danger"> + <b>This action requires consent.</b> Since this action constitues a sexual act, verbal consent from both parties is absolutely required. Both parties must be in a mental state where they are able to consent. Furthermore, if one of the parties involved wishes to stop, the request must be immediately honoured. + </div> + <?php elseif ($selected["consent"]): ?> + <div class="alert alert-warning"> + <b>This action is better with consent.</b> This action is extremely personal to all parties involved, and it is best to ask if they wish to do this. If they ask to stop the action, you must stop immediately and reverse any potential effects. + </div> + <?php endif; ?> + + <?php if (isset($selected["description"]) && trim($selected["description"]) !== ""): ?> + <?= replaceKeyWords(str_replace("\n", "<br>", strip_tags($selected["description"]))); ?> + <?php else: ?> + <p><i>No description provided for this action. Enter edit mode to add a description to this action.</i></p> + <?php endif; ?> + + <?php if (isset($selected["toys"]) && $selected["toys"]): ?> + <hr> + <p><b>Available toys:</b></p> + <div id="toys-list"> + <div class="toys-list-inner"> + <?php + + $init = []; + foreach ($toys as $value) { + $init[$value["name"]] = $value; + } + + ksort($init); + + $sorted = array_values($init); + uasort($sorted, function ($a, $b) { + $uniquePonies1 = []; + + foreach ($a["ponies"] as $ponies) { + foreach ($ponies["members"] as $member) { + $uniquePonies1[$member] = true; + } + } + + $uniquePonies2 = []; + + foreach ($b["ponies"] as $ponies) { + foreach ($ponies["members"] as $member) { + $uniquePonies2[$member] = true; + } + } + + return count($uniquePonies2) - count($uniquePonies1); + }); + + foreach ($sorted as $item): ?> + <a href="/-/toys/<?= $item["id"] ?>" id="action-<?= $item["id"] ?>" style="color:white !important;"> + <div> + <span><?= $item["name"] ?></span> + </div> + </a> + <?php endforeach; ?> + </div> + </div> + <?php endif; ?> + + <hr> + <b>Can be done by:</b> + <?php + + $hasMultipleTypes = false; + $seenType = ""; + + foreach ($selected["ponies"] as $relation) { + if (isset($relation["sexual"]) && $relation["sexual"]) { + $type = "sexual"; + } else { + $type = "affectionate"; + } + + if ($type !== $seenType && $seenType !== "") { + $hasMultipleTypes = true; + } + + if (trim($seenType) === "") { + $seenType = $type; + } + } + + ?> + <?php if ($hasMultipleTypes): ?> + <ul> + <li> + Affectionately: + <ul> + <?php foreach ($selected["ponies"] as $relation): if (!$relation["deprecated"] && (!isset($relation["sexual"]) || !$relation["sexual"])): + + $member1 = getMemberWithoutSystem($relation["members"][0]); + $member2 = getMemberWithoutSystem($relation["members"][1]); + + ?> + <li> + <img style="vertical-align: middle;width:24px;height:24px;" src="/assets/uploads/pt-<?= $member1["name"] ?>.png"> + <img style="vertical-align: middle;width:24px;height:24px;" src="/assets/uploads/pt-<?= $member2["name"] ?>.png"> + + <span style="vertical-align: middle;"><?= getMiniName($member1["display_name"] ?? $member1["name"]) ?> and <?= getMiniName($member2["display_name"] ?? $member2["name"]) ?></span> + </li> + <?php endif; endforeach; ?> + <?php foreach ($selected["ponies"] as $relation): if ($relation["deprecated"] && (!isset($relation["sexual"]) || !$relation["sexual"])): + + $member1 = getMemberWithoutSystem($relation["members"][0]); + $member2 = getMemberWithoutSystem($relation["members"][1]); + + ?> + <li> + <span style="opacity:.5;"> + <img style="vertical-align: middle;width:24px;height:24px;" src="/assets/uploads/pt-<?= $member1["name"] ?>.png"> + <img style="vertical-align: middle;width:24px;height:24px;" src="/assets/uploads/pt-<?= $member2["name"] ?>.png"> + + <span style="vertical-align: middle;"><?= getMiniName($member1["display_name"] ?? $member1["name"]) ?> and <?= getMiniName($member2["display_name"] ?? $member2["name"]) ?></span> + </span> + <span class="badge bg-danger rounded-pill">Deprecated</span> + </li> + <?php endif; endforeach; ?> + </ul> + </li> + <li> + Sexually: + <ul> + <?php foreach ($selected["ponies"] as $relation): if (!$relation["deprecated"] && (isset($relation["sexual"]) && $relation["sexual"])): + + $member1 = getMemberWithoutSystem($relation["members"][0]); + $member2 = getMemberWithoutSystem($relation["members"][1]); + + ?> + <li> + <img style="vertical-align: middle;width:24px;height:24px;" src="/assets/uploads/pt-<?= $member1["name"] ?>.png"> + <img style="vertical-align: middle;width:24px;height:24px;" src="/assets/uploads/pt-<?= $member2["name"] ?>.png"> + + <span style="vertical-align: middle;"><?= getMiniName($member1["display_name"] ?? $member1["name"]) ?> and <?= getMiniName($member2["display_name"] ?? $member2["name"]) ?></span> + </li> + <?php endif; endforeach; ?> + <?php foreach ($selected["ponies"] as $relation): if ($relation["deprecated"] && (isset($relation["sexual"]) && $relation["sexual"])): + + $member1 = getMemberWithoutSystem($relation["members"][0]); + $member2 = getMemberWithoutSystem($relation["members"][1]); + + ?> + <li> + <span style="opacity:.5;"> + <img style="vertical-align: middle;width:24px;height:24px;" src="/assets/uploads/pt-<?= $member1["name"] ?>.png"> + <img style="vertical-align: middle;width:24px;height:24px;" src="/assets/uploads/pt-<?= $member2["name"] ?>.png"> + + <span style="vertical-align: middle;"><?= getMiniName($member1["display_name"] ?? $member1["name"]) ?> and <?= getMiniName($member2["display_name"] ?? $member2["name"]) ?></span> + </span> + <span class="badge bg-danger rounded-pill">Deprecated</span> + </li> + <?php endif; endforeach; ?> + </ul> + </li> + </ul> + <?php else: ?> + <ul> + <?php foreach ($selected["ponies"] as $relation): if (!$relation["deprecated"]): + + $member1 = getMemberWithoutSystem($relation["members"][0]); + $member2 = getMemberWithoutSystem($relation["members"][1]); + + ?> + <li> + <img style="vertical-align: middle;width:24px;height:24px;" src="/assets/uploads/pt-<?= $member1["name"] ?>.png"> + <img style="vertical-align: middle;width:24px;height:24px;" src="/assets/uploads/pt-<?= $member2["name"] ?>.png"> + + <span style="vertical-align: middle;"><?= getMiniName($member1["display_name"] ?? $member1["name"]) ?> and <?= getMiniName($member2["display_name"] ?? $member2["name"]) ?></span> + </li> + <?php endif; endforeach; ?> + <?php foreach ($selected["ponies"] as $relation): if ($relation["deprecated"]): + + $member1 = getMemberWithoutSystem($relation["members"][0]); + $member2 = getMemberWithoutSystem($relation["members"][1]); + + ?> + <li> + <span style="opacity:.5;"> + <img style="vertical-align: middle;width:24px;height:24px;" src="/assets/uploads/pt-<?= $member1["name"] ?>.png"> + <img style="vertical-align: middle;width:24px;height:24px;" src="/assets/uploads/pt-<?= $member2["name"] ?>.png"> + + <span style="vertical-align: middle;"><?= getMiniName($member1["display_name"] ?? $member1["name"]) ?> and <?= getMiniName($member2["display_name"] ?? $member2["name"]) ?></span> + </span> + <span class="badge bg-danger rounded-pill">Deprecated</span> + </li> + <?php endif; endforeach; ?> + </ul> + <?php endif; ?> + + <div style="margin-top:10px;"></div> + + <?php $multipleExamples = count(explode(",", strip_tags($selected["example"]))) > 1; ?> + <b>Example<?= $multipleExamples ? "s" : "" ?>:</b><br> + <?php if (isset($selected["example"]) && trim($selected["example"]) !== ""): ?> + <?php if ($multipleExamples): ?> + <ul> + <?php foreach (explode(",", strip_tags($selected["example"])) as $example): ?> + <li><?= replaceKeyWords(trim($example)) ?></li> + <?php endforeach; ?> + </ul> + <?php else: ?> + <?= replaceKeyWords(strip_tags($selected["example"])) ?> + <?php endif; ?> + <?php else: ?> + <p><i>No example provided for this action. Enter edit mode to add an example to this action.</i></p> + <?php endif; ?> + + <div style="margin-top:10px;"></div> + + <b>Steps to reproduce in real life:</b><br> + <?php if (isset($selected["irl"]) && trim($selected["irl"]) !== ""): ?> + <?= replaceKeyWords(strip_tags($selected["irl"])) ?> + <?php else: ?> + <p><i>This action is not reproducible in real life.</i></p> + <?php endif; ?> + + <hr> + + <h4>Similar actions</h4> + <div class="row"> + <?php + + $names = []; + $currentName = $selected["name"]; + $namesByDistance = []; + + foreach ($data as $action) { + if ($action["name"] !== $currentName) $names[$action["name"]] = [ + "id" => $action["id"], + "type" => $action["type"], + "ponies" => $action["ponies"] + ]; + } + + foreach ($names as $name => $data) { + if ($data["type"] === $selected["type"] || $selected["type"] !== "affectionate") { + $namesByDistance[] = [ + "name" => $name, + "distance" => levenshtein($currentName, $name) + ((int)($data["type"] !== $selected["type"]) * 2), + "id" => $data["id"], + "type" => $data["type"], + "ponies" => $data["ponies"] + ]; + } + } + + uasort($namesByDistance, function ($a, $b) use ($selected) { + return $a["distance"] - $b["distance"]; + }); + + foreach ($namesByDistance as $item) { + echo("<!-- " . $currentName . " <-> " . $item["name"] . " => " . $item["distance"] . " (artif: " . ((int)($item["type"] !== $selected["type"]) * 10) . ") -->"); + } + + $index = 0; + foreach ($namesByDistance as $item): if ($index < 3): + ?> + <div class="col-md-4" style="margin-bottom:20px;"> + <a class="linked-card" href="/-/actions/<?= $item["id"] ?>"><div class="card"> + <div class="card-body"> + <h4 class="card-title"><?= $item["name"] ?></h4> + <p class="card-text"> + <?php + + $uniquePonies = []; + + foreach ($item["ponies"] as $ponies) { + foreach ($ponies["members"] as $member) { + if (isset($uniquePonies[$member]) && !$uniquePonies[$member]) { + $uniquePonies[$member] = false; + } else { + $uniquePonies[$member] = $ponies["deprecated"]; + } + } + } + + foreach ($uniquePonies as $name => $deprecated): if (!$deprecated): ?> + <span data-bs-toggle="tooltip" title="<?= getMemberWithoutSystem($name)["display_name"] ?? getMemberWithoutSystem($name)["display_name"] ?>" style="display: inline-block;"> + <img src="/assets/uploads/pt-<?= getMemberWithoutSystem($name)["name"] ?>.png" style="width:32px;"> + </span> + <?php endif; endforeach; ?> + <?php foreach ($uniquePonies as $name => $deprecated): if ($deprecated): ?> + <span data-bs-toggle="tooltip" data-bs-html="true" title="<i><?= strip_tags(getMemberWithoutSystem($name)["display_name"] ?? getMemberWithoutSystem($name)["name"]) ?></i>" style="opacity:.5;display: inline-block;"> + <img src="/assets/uploads/pt-<?= getMemberWithoutSystem($name)["name"] ?>.png" style="width:32px;"> + </span> + <?php endif; endforeach; ?> + </p> + <?php if ($item["type"] === "affectionate"): ?> + <span style="" class="badge rounded-pill bg-primary">Affectionate</span> + <?php endif; ?> + <?php if ($item["type"] === "sexual"): ?> + <span style="" class="badge rounded-pill bg-danger">Sexual</span> + <?php endif; ?> + <?php if ($item["type"] === "mixed"): ?> + <span style="" class="badge rounded-pill bg-primary">Affectionate</span> <span style="" class="badge rounded-pill bg-danger">Sexual</span> + <?php endif; ?> + </div> + </div></a> + </div> + <?php $index++; endif; endforeach; ?> + </div> + + <div class="modal fade" id="editor"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h4 class="modal-title">Edit action</h4> + <button type="button" class="btn-close" data-bs-dismiss="modal"></button> + </div> + + <div class="modal-body"> + <form method="post" style="display:inline;"> + <input type="text" placeholder="Action title" name="name" class="form-control" style="color:white;background:#111;border-color:#222;margin-bottom:10px;" value="<?= str_replace('"', '"', $selected["name"]) ?>"> + <select name="type" id="editor-type" class="form-select" style='color:white;background-color:#111;border-color:#222;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23ffffff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");margin-bottom:10px;' onchange="changeMixed();"> + <option value="0" <?= $selected["type"] === "affectionate" ? "selected" : "" ?>>Affectionate</option> + <option value="1" <?= $selected["type"] === "sexual" ? "selected" : "" ?>>Sexual</option> + <option value="2" <?= $selected["type"] === "mixed" ? "selected" : "" ?>>Mixed</option> + </select> + + <label style="margin-left:5px;"> + <input <?= ($selected["consent"] ?? false) ? "checked" : "" ?> type="checkbox" name="consent"> + Requires consent + </label><br> + + <label style="margin-left:5px;"> + <input <?= ($selected["verified"] ?? false) ? "checked" : "" ?> <?= $_PROFILE['login'] !== "raindrops" ? "disabled" : "" ?> type="checkbox" name="verified"> + Mark as verified + </label><br> + + <label style="margin-left:5px;"> + <input <?= ($selected["untested"] ?? false) ? "checked" : "" ?> type="checkbox" name="untested"> + Mark as untested + </label> + + <hr> + + <textarea name="description" class="form-control" style="resize: none;color:white;background:#111;border-color:#222;" placeholder="Description"><?= strip_tags($selected["description"] ?? "") ?></textarea> + + <hr> + + <input type="text" placeholder="Keywords (comma-separated)" name="keywords" class="form-control" style="color:white;background:#111;border-color:#222;margin-bottom:10px;" value="<?= str_replace('"', '"', implode(",", $selected["keywords"] ?? [])) ?>"> + <input type="text" placeholder="Action example" name="example" class="form-control" style="color:white;background:#111;border-color:#222;margin-bottom:10px;" value="<?= str_replace('"', '"', $selected["example"] ?? "") ?>"> + <input type="text" placeholder="IRL steps" name="irl" class="form-control" style="color:white;background:#111;border-color:#222;" value="<?= str_replace('"', '"', $selected["irl"] ?? "") ?>"> + + <hr> + + <p>Select the groups of ponies who can do this action:</p> + <?php + + $members = scoreOrderGlobal(); + $relations = []; + + foreach ($members as $member) { + foreach ([ + ...array_map(function ($i) { + $r = [ + "name" => $i + ]; + $r["type"] = "marefriends"; + return $r; + }, $member["_metadata"]["marefriends"] ?? []), + ...array_map(function ($i) { + $r = [ + "name" => $i + ]; + $r["type"] = "sisters"; + return $r; + }, $member["_metadata"]["sisters"] ?? []), + ...array_map(function ($i) { + $r = [ + "name" => $i + ]; + $r["type"] = "caretaking"; + return $r; + }, $member["_metadata"]["caretakers"] ?? []) + ] as $rel) { + $id = $rel["name"]; + $otherMember = getSystemMember(explode("/", $id)[0], explode("/", $id)[1]); + + $parts = [ + $member["id"], + $otherMember["id"] + ]; + + asort($parts); + + $relations[implode("-", $parts)] = [ + "name" => getMiniName($member["display_name"] ?? $member["name"]) . " and " . getMiniName($otherMember["display_name"] ?? $otherMember["name"]), + "type" => $rel["type"], + "images" => [ + file_exists($_SERVER['DOCUMENT_ROOT'] . "/assets/uploads/pt-$member[name].png") ? "/assets/uploads/pt-$member[name].png" : "/assets/uploads/pt.png", + file_exists($_SERVER['DOCUMENT_ROOT'] . "/assets/uploads/pt-$otherMember[name].png") ? "/assets/uploads/pt-$otherMember[name].png" : "/assets/uploads/pt.png", + ] + ]; + } + } + + $inFileRelations = []; + $inFileDeprecations = []; + $inFileSexual = []; + + foreach ($selected["ponies"] as $ponies) { + $inFileRelations[] = $ponies["members"][0] . "-" . $ponies["members"][1]; + + if (isset($ponies["deprecated"]) && $ponies["deprecated"]) $inFileDeprecations[] = $ponies["members"][0] . "-" . $ponies["members"][1]; + if (isset($ponies["sexual"]) && $ponies["sexual"]) $inFileSexual[] = $ponies["members"][0] . "-" . $ponies["members"][1]; + } + + foreach ($relations as $id => $relation): + ?> + <label style="display:block;" class="creator-relation <?= $selected["type"] === "mixed" ? "mixed" : "" ?> <?= in_array($id, $inFileRelations) ? "checked" : "" ?>"> + <input <?= in_array($id, $inFileRelations) ? "checked" : "" ?> name="relations[<?= $id ?>][member]" style="display:none;" type="checkbox" class="checkbox-input"> + + <img style="vertical-align: middle;width:24px;height:24px;" src="<?= $relation["images"][0] ?>"> + <img style="vertical-align: middle;width:24px;height:24px;" src="<?= $relation["images"][1] ?>"> + <span style="vertical-align: middle;"><?= $relation["name"] ?></span> + + <span style="float:right;margin-top:3px;" class="badge rounded-pill bg-<?= match ($relation["type"]) { + "marefriends" => "danger", + "sisters" => "success", + "caretaking" => "primary" + } ?>"><?= match ($relation["type"]) { + "marefriends" => "Marefriends", + "sisters" => "Sisters", + "caretaking" => "Caretaker" + } ?></span> + + <label class="deprecated" style="display:none;margin-left: 20px;margin-top: 5px;"> + <input <?= in_array($id, $inFileDeprecations) ? "checked" : "" ?> name="relations[<?= $id ?>][deprecated]" type="checkbox"> + Mark as deprecated + </label> + <label class="sexual" style="display:none;margin-left: 20px;margin-top: 5px;"> + <input <?= in_array($id, $inFileSexual) ? "checked" : "" ?> name="relations[<?= $id ?>][sexual]" type="checkbox"> + Mark as sexual + </label> + </label> + <?php endforeach; ?> + + <br> + <input type="hidden" name="updateAction"> + <input type="hidden" name="action" value="<?= $selected["id"] ?>"> + <input type="submit" class="btn btn-primary" value="Save"> + </form> + <form method="post" style="display:inline;"> + <input type="hidden" name="deleteAction"> + <input type="hidden" name="action" value="<?= $selected["id"] ?>"> + <input type="submit" class="btn btn-danger" value="Delete"> + </form> + </div> + </div> + </div> + </div> + + <?php else: ?> + + <h2>Actions database</h2> + <p><?= count($data) ?> actions (<?= count(array_filter($data, function ($i) { + return $i["type"] === "affectionate" || $i["type"] === "mixed"; + })) ?> affectionate, <?= count(array_filter($data, function ($i) { + return $i["type"] === "sexual" || $i["type"] === "mixed"; + })) ?> sexual, <?= count(array_filter($data, function ($i) { + return isset($i["untested"]) && $i["untested"]; + })) ?> untested, <?= count(array_filter($data, function ($i) { + return (isset($i["description"]) && trim($i["description"]) === "") || !isset($i["description"]); + })) ?> incomplete)</p> + + <p>TODO: add ponies for all actions (+ keywords)</p> + + <input type="text" placeholder="Search for an action..." class="form-control" style="margin-bottom:15px;color:white;background:#111;border-color:#222;" onchange="search();" onkeydown="search();" onkeyup="search();" id="search"> + + <div id="list"> + <div class="list-group"> + <?php + + $init = []; + foreach ($data as $value) { + $init[$value["name"]] = $value; + } + + ksort($init); + + $sorted = array_values($init); + uasort($sorted, function ($a, $b) { + $uniquePonies1 = []; + + foreach ($a["ponies"] as $ponies) { + foreach ($ponies["members"] as $member) { + $uniquePonies1[$member] = true; + } + } + + $uniquePonies2 = []; + + foreach ($b["ponies"] as $ponies) { + foreach ($ponies["members"] as $member) { + $uniquePonies2[$member] = true; + } + } + + return count($uniquePonies2) - count($uniquePonies1); + }); + + foreach ($sorted as $item): ?> + <a href="/-/actions/<?= $item["id"] ?>" id="action-<?= $item["id"] ?>" style="display:grid;grid-template-columns: 1fr 1fr 0.2fr;" class="list-group-item list-group-item-action action-listing"> + <div> + <span class="<?= trim($item["description"]) === "" ? "text-danger" : "" ?>"><?= $item["name"] ?></span> + <?php if (!isset($item["verified"])): ?><span class="badge bg-warning rounded-pill text-black">Unverified</span><?php endif; ?> + <?php if (isset($item["untested"])): ?><span class="badge bg-info rounded-pill">Untested</span><?php endif; ?> + </div> + <div> + <?php + + $uniquePonies = []; + $longPonyList = false; + + foreach ($item["ponies"] as $ponies) { + foreach ($ponies["members"] as $member) { + if (isset($uniquePonies[$member]) && !$uniquePonies[$member]) { + $uniquePonies[$member] = false; + } else { + $uniquePonies[$member] = $ponies["deprecated"]; + } + } + } + + if (count($uniquePonies) > 6) { + $longPonyList = true; + } + + $index = 1; + foreach ($uniquePonies as $name => $deprecated): if (!$deprecated): ?> + <!-- <?= $index ?> --> + <span title="<?= getMemberWithoutSystem($name)["display_name"] ?? getMemberWithoutSystem($name)["display_name"] ?>" style="display: inline-block;<?= $longPonyList && $index % 2 === 0 ? "position:absolute;margin-left:-12px;z-index:" . (999 - $index) . ";" : "position:relative;z-index:" . (999 - $index) . ";" ?>"> + <img src="/assets/uploads/pt-<?= getMemberWithoutSystem($name)["name"] ?>.png" style="width:24px;"> + </span> + <?php $index++; endif; endforeach; ?> + <?php foreach ($uniquePonies as $name => $deprecated): if ($deprecated): ?> + <span title="<?= strip_tags(getMemberWithoutSystem($name)["display_name"] ?? getMemberWithoutSystem($name)["name"]) ?> (deprecated)" style="opacity:.5;display: inline-block;<?= $longPonyList && $index % 2 === 0 ? "position:absolute;margin-left:-12px;z-index:" . (999 - $index) . ";" : "position:relative;z-index:" . (999 - $index) . ";" ?>"> + <img src="/assets/uploads/pt-<?= getMemberWithoutSystem($name)["name"] ?>.png" style="width:24px;"> + </span> + <?php $index++; endif; endforeach; ?> + </div> + <div style="text-align: right;"> + <?php if ($item["type"] === "affectionate"): ?> + <span style="" class="badge rounded-pill bg-primary">Affectionate</span> + <?php endif; ?> + <?php if ($item["type"] === "sexual"): ?> + <span style="" class="badge rounded-pill bg-danger">Sexual</span> + <?php endif; ?> + <?php if ($item["type"] === "mixed"): ?> + <span style="" class="badge rounded-pill bg-success">Mixed</span> + <?php endif; ?> + </div> + </a> + <?php endforeach; ?> + </div> + </div> + + <div id="search-results" class="list-group"></div> + + <div id="page-content"> + <hr> + <p>Not finding what you are looking for? <a onclick="event.target.blur(); document.getElementById('creator-title').focus();" href="#" data-bs-toggle="modal" data-bs-target="#creator">Create an action.</a></p> + </div> + + <script> + window.actions = JSON.parse(atob(`<?= base64_encode(json_encode(array_values(json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/actions.json"), true)))) ?>`)); + </script> + + <div class="modal fade" id="creator"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h4 class="modal-title">Create a new action</h4> + <button type="button" class="btn-close" data-bs-dismiss="modal"></button> + </div> + + <div class="modal-body"> + <form method="post"> + <input id="creator-title" type="text" placeholder="Action title" name="name" class="form-control" style="color:white;background:#111;border-color:#222;margin-bottom:10px;"> + <select name="type" class="form-select" style='color:white;background-color:#111;border-color:#222;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23ffffff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");margin-bottom:10px;'> + <option value="0" selected>Affectionate</option> + <option value="1">Sexual</option> + <option value="2">Mixed</option> + </select> + + <div class="alert alert-secondary" style="display:none;"> + <p>The following actions might be the same as the one you are trying to add:</p> + <ul> + <li>Action 1</li> + <li>Action 2</li> + <li>Action 3</li> + </ul> + </div> + + <label style="margin-left:5px;"> + <input type="checkbox" name="consent"> + Requires consent + </label> + + <hr> + + <p>Select the groups of ponies who can do this action:</p> + <?php + + $members = scoreOrderGlobal(); + $relations = []; + + foreach ($members as $member) { + foreach ([ + ...array_map(function ($i) { + $r = [ + "name" => $i + ]; + $r["type"] = "marefriends"; + return $r; + }, $member["_metadata"]["marefriends"] ?? []), + ...array_map(function ($i) { + $r = [ + "name" => $i + ]; + $r["type"] = "sisters"; + return $r; + }, $member["_metadata"]["sisters"] ?? []), + ...array_map(function ($i) { + $r = [ + "name" => $i + ]; + $r["type"] = "caretaking"; + return $r; + }, $member["_metadata"]["caretakers"] ?? []) + ] as $rel) { + $id = $rel["name"]; + $otherMember = getSystemMember(explode("/", $id)[0], explode("/", $id)[1]); + + $parts = [ + $member["id"], + $otherMember["id"] + ]; + + asort($parts); + + $relations[implode("-", $parts)] = [ + "name" => getMiniName($member["display_name"] ?? $member["name"]) . " and " . getMiniName($otherMember["display_name"] ?? $otherMember["name"]), + "type" => $rel["type"], + "images" => [ + file_exists($_SERVER['DOCUMENT_ROOT'] . "/assets/uploads/pt-$member[name].png") ? "/assets/uploads/pt-$member[name].png" : "/assets/uploads/pt.png", + file_exists($_SERVER['DOCUMENT_ROOT'] . "/assets/uploads/pt-$otherMember[name].png") ? "/assets/uploads/pt-$otherMember[name].png" : "/assets/uploads/pt.png", + ] + ]; + } + } + + foreach ($relations as $id => $relation): + ?> + <label style="display:block;" class="creator-relation"> + <input name="relations[<?= $id ?>]" style="display:none;" type="checkbox" class="checkbox-input"> + + <img style="vertical-align: middle;width:24px;height:24px;" src="<?= $relation["images"][0] ?>"> + <img style="vertical-align: middle;width:24px;height:24px;" src="<?= $relation["images"][1] ?>"> + <span style="vertical-align: middle;"><?= $relation["name"] ?></span> + + <span style="float:right;margin-top:3px;" class="badge rounded-pill bg-<?= match ($relation["type"]) { + "marefriends" => "danger", + "sisters" => "success", + "caretaking" => "primary" + } ?>"><?= match ($relation["type"]) { + "marefriends" => "Marefriends", + "sisters" => "Sisters", + "caretaking" => "Caretaker" + } ?></span> + </label> + <?php endforeach; ?> + + <p style="margin-top:10px;">You can add additional data (description, example, how to do the action IRL) after creating the action.</p> + <input type="hidden" name="createAction"> + <input type="submit" class="btn btn-primary" value="Create"> + </form> + </div> + </div> + </div> + </div> + + <?php endif; ?> + </div> +</div> + +<!--suppress JSUnresolvedFunction --> +<script> + Array.from(document.getElementsByClassName("checkbox-input")).forEach((el) => { + el.onchange = () => { + let parent = el.parentElement; + + if (el.checked) { + parent.classList.add("checked"); + } else { + parent.classList.remove("checked"); + } + } + }); + + const fuse = new Fuse(window.actions, { + includeScore: true, + keys: [ + { + name: 'name', + weight: 1 + }, + { + name: 'description', + weight: 1 + }, + { + name: 'example', + weight: 0.7 + }, + { + name: 'irl', + weight: 0.5 + } + ] + }) + + function search() { + let query = document.getElementById("search").value; + let results = fuse.search(query).map((i) => { + return { + id: i.item.id, + score: i.score + }; + }); + + let unfiltered = results; + + results = results.filter((i) => { + return i.score < 0.7; + }); + + console.log("Before:", unfiltered, "After:", results); + + document.getElementById("list").style.display = "none"; + document.getElementById("search-results").style.display = "block"; + document.getElementById("search-results").innerHTML = ""; + + for (let result of results) { + document.getElementById("search-results").innerHTML += document.getElementById("action-" + result.id).outerHTML; + } + + console.log(results); + + if (query.trim() === "") { + document.getElementById("list").style.display = "block"; + document.getElementById("search-results").style.display = "none"; + } + } + + function changeMixed() { + let value = document.getElementById("editor-type").value; + console.log(value); + + if (value === "2") { + Array.from(document.getElementsByClassName("creator-relation")).forEach((el) => { + el.classList.add("mixed"); + }); + } else { + Array.from(document.getElementsByClassName("creator-relation")).forEach((el) => { + el.classList.remove("mixed"); + }); + } + } +</script> + +<style> + .modal-header { + border-bottom: 1px solid #353738; + } + + .modal-content { + border: 1px solid rgba(255, 255, 255, .2); + background-color: #111; + } + + .btn-close { + filter: invert(1); + } + + .creator-relation { + border-radius: 10px; + padding: 5px 10px; + opacity: .5; + } + + .creator-relation.checked { + background-color: rgba(255, 255, 255, .1); + opacity: 1; + } + + .creator-relation:hover { + background-color: rgba(255, 255, 255, .1); + } + + .creator-relation.checked:hover { + background-color: rgba(255, 255, 255, .25) !important; + } + + .creator-relation.checked .deprecated { + display: block !important; + } + + .creator-relation.mixed.checked .sexual { + display: block !important; + } + + .list-group-item { + color: #fff; + background-color: #222; + border: 1px solid rgba(255, 255, 255, .125); + } + + .list-group-item.disabled { + color: #fff; + background-color: #222; + border-color: rgba(255, 255, 255, .125); + opacity: .75; + } + + .list-group-item:hover { + background-color: #252525; + color: #ddd; + } + + .list-group-item:active, .list-group-item:focus { + background-color: #272727; + color: #bbb; + } + + @media (max-width: 991px) { + .action-listing { + grid-template-columns: 1fr !important; + text-align: center !important; + } + + .action-listing > * { + margin-bottom: 10px; + text-align: center !important; + } + + .action-listing > *:nth-last-child(1) { + margin-bottom: 0 !important; + } + + .action-listing img { + width: 32px !important; + } + } + + .toys-list-inner { + display: grid; + grid-template-columns: repeat(4, 1fr); + background-color: #111111; + box-sizing: border-box; + grid-gap: 1px; + } + + .toys-list-inner > * { + text-decoration: none !important; + box-shadow: 0 0 0 1px #2f2f2f; + padding: 5px 10px; + } + + .toys-list-inner > *:nth-child(1) { + border-top-left-radius: 5px; + } + + .toys-list-inner > *:nth-child(4) { + border-top-right-radius: 5px; + } + + .toys-list-inner > *:nth-last-child(4) { + border-bottom-left-radius: 5px; + } + + .toys-list-inner > *:nth-last-child(1) { + border-bottom-right-radius: 5px; + } +</style> + +<?php require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/footer.php'; ?> diff --git a/pages/bitset.php b/pages/bitset.php index 11031e2..8c63bc5 100644 --- a/pages/bitset.php +++ b/pages/bitset.php @@ -1,7 +1,7 @@ <?php require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $isLoggedIn; -if (!$isLoggedIn) header("Location: /login") and die(); +if (!$isLoggedIn) header("Location: /-/login") and die(); $title = "Bitset calculator"; require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/header.php'; @@ -10,7 +10,7 @@ $title = "Bitset calculator"; require_once $_SERVER['DOCUMENT_ROOT'] . '/include <br> <div class="container"> <div id="page-content"> - <h2>Bitset Calculator</h2> + <h2>Bitset calculator</h2> </div> <div style="display:grid; grid-template-columns: repeat(48, 1fr);"> <div data-bs-toggle="tooltip" title="Value reserved for future use" id="binary-bit-1" class="font-monospace tooltip-nohelp text-muted" style="text-align: center;cursor: pointer;">0</div> @@ -27,8 +27,8 @@ $title = "Bitset calculator"; require_once $_SERVER['DOCUMENT_ROOT'] . '/include <div data-bs-toggle="tooltip" title="Value reserved for future use" id="binary-bit-12" class="font-monospace tooltip-nohelp text-muted" style="text-align: center;cursor: pointer;">0</div> <div data-bs-toggle="tooltip" title="Value reserved for future use" id="binary-bit-13" class="font-monospace tooltip-nohelp text-muted" style="text-align: center;cursor: pointer;">0</div> <div data-bs-toggle="tooltip" title="Value reserved for future use" id="binary-bit-14" class="font-monospace tooltip-nohelp text-muted" style="text-align: center;cursor: pointer;">0</div> - <div data-bs-toggle="tooltip" title="Value reserved for future use" id="binary-bit-15" class="font-monospace tooltip-nohelp text-muted" style="text-align: center;cursor: pointer;">0</div> - <div data-bs-toggle="tooltip" title="Value reserved for future use" id="binary-bit-16" class="font-monospace tooltip-nohelp text-muted" style="text-align: center;cursor: pointer;">0</div> + <div data-bs-toggle="tooltip" title="Fronts less frequently" id="binary-bit-15" class="font-monospace tooltip-nohelp" style="color: #198754;text-align: center;cursor: pointer;">0</div> + <div data-bs-toggle="tooltip" title="Non verbal in real life" id="binary-bit-16" class="font-monospace tooltip-nohelp" style="color: #20c997;text-align: center;cursor: pointer;">0</div> <div data-bs-toggle="tooltip" title="Eatable food" id="binary-bit-17" class="font-monospace tooltip-nohelp" style="color: #d63384;text-align: center;cursor: pointer;">0</div> <div data-bs-toggle="tooltip" title="Eatable food" id="binary-bit-18" class="font-monospace tooltip-nohelp" style="color: #d63384;text-align: center;cursor: pointer;">0</div> <div data-bs-toggle="tooltip" title="Ability to use magic" id="binary-bit-19" class="font-monospace tooltip-nohelp" style="color: #fd7e14;text-align: center;cursor: pointer;">0</div> @@ -108,6 +108,8 @@ $title = "Bitset calculator"; require_once $_SERVER['DOCUMENT_ROOT'] . '/include let magic = parseInt(binString.substring(2 + 16, 5 + 16), 2); let sensitivity = parseInt(binString.substring(5 + 16, 8 + 16), 2); let ageSpells = binString.substring(31 + 16, 32 + 16) !== "0"; + let nonverbal = binString.substring(15, 16) !== "0"; + let lessFrequent = binString.substring(14, 15) !== "0"; document.getElementById("value-0").value = sharedMemory; document.getElementById("value-1").value = little; @@ -125,6 +127,8 @@ $title = "Bitset calculator"; require_once $_SERVER['DOCUMENT_ROOT'] . '/include document.getElementById("value-13").value = sensitivity; document.getElementById("value-14").value = species3; document.getElementById("value-15").checked = ageSpells; + document.getElementById("value-16").checked = nonverbal; + document.getElementById("value-17").checked = lessFrequent; } } @@ -171,6 +175,8 @@ $title = "Bitset calculator"; require_once $_SERVER['DOCUMENT_ROOT'] . '/include let species2 = binString.substring(21 + 16, 25 + 16); let species3 = binString.substring(25 + 16, 29 + 16); let food = parseInt(binString.substring(16, 2 + 16), 2); + let nonverbal = binString.substring(15, 16) !== "0"; + let lessFrequent = binString.substring(14, 15) !== "0"; let magic = parseInt(binString.substring(2 + 16, 5 + 16), 2); let sensitivity = parseInt(binString.substring(5 + 16, 8 + 16), 2); @@ -190,6 +196,8 @@ $title = "Bitset calculator"; require_once $_SERVER['DOCUMENT_ROOT'] . '/include document.getElementById("value-10").checked = plush; document.getElementById("value-14").value = species3; document.getElementById("value-15").checked = ageSpells; + document.getElementById("value-16").checked = nonverbal; + document.getElementById("value-17").checked = lessFrequent; calculateOutput(); @@ -213,6 +221,8 @@ $title = "Bitset calculator"; require_once $_SERVER['DOCUMENT_ROOT'] . '/include let val13 = document.getElementById("value-13").value; let val14 = document.getElementById("value-14").value; let val15 = document.getElementById("value-15").checked; + let val16 = document.getElementById("value-16").checked; + let val17 = document.getElementById("value-17").checked; let val0bin = parseInt(val0).toString(2); val0bin = val0bin.length === 1 ? "0" + val0bin : val0bin; @@ -241,8 +251,10 @@ $title = "Bitset calculator"; require_once $_SERVER['DOCUMENT_ROOT'] . '/include let val9bin = val9 ? "1" : "0"; let val10bin = val10 ? "1" : "0"; let val15bin = val15 ? "1" : "0"; + let val16bin = val16 ? "1" : "0"; + let val17bin = val17 ? "1" : "0"; - let bin = "0000000000000000" + val11bin + val12bin + val13bin + val0bin + val4bin + val1bin + val5bin + val6bin + val7bin + val8bin + val2bin + val3bin + val14bin + val9bin + val10bin + val15bin; + let bin = "00000000000000" + val17bin + val16bin + val11bin + val12bin + val13bin + val0bin + val4bin + val1bin + val5bin + val6bin + val7bin + val8bin + val2bin + val3bin + val14bin + val9bin + val10bin + val15bin; console.log(bin, parseInt(bin, 2)); @@ -364,7 +376,15 @@ $title = "Bitset calculator"; require_once $_SERVER['DOCUMENT_ROOT'] . '/include <label style="margin-bottom:5px;"> <input type="checkbox" id="value-15" onchange="updateFromSelection();"> Affected by age spells - </label><img alt="" src="/assets/icons/visibility-private.svg" style="filter:invert(1);width:24px;margin-left:5px;margin-top:-5px;" title="This information will remain private" data-bs-toggle="tooltip"> + </label><img alt="" src="/assets/icons/visibility-private.svg" style="filter:invert(1);width:24px;margin-left:5px;margin-top:-5px;" title="This information will remain private" data-bs-toggle="tooltip"><br> + <label style="margin-bottom:5px;"> + <input type="checkbox" id="value-16" onchange="updateFromSelection();"> + Non verbal in real life + </label><img alt="" src="/assets/icons/visibility-public.svg" style="filter:invert(1);width:24px;margin-left:5px;margin-top:-5px;" title="This information will be shown publicly" data-bs-toggle="tooltip"><br> + <label style="margin-bottom:5px;"> + <input type="checkbox" id="value-17" onchange="updateFromSelection();"> + Fronts less frequently + </label><img alt="" src="/assets/icons/visibility-public.svg" style="filter:invert(1);width:24px;margin-left:5px;margin-top:-5px;" title="This information will be shown publicly" data-bs-toggle="tooltip"> </p> </div> diff --git a/pages/dashboard.php b/pages/dashboard.php new file mode 100644 index 0000000..622cd20 --- /dev/null +++ b/pages/dashboard.php @@ -0,0 +1,299 @@ +<?php + +require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $isLoggedIn; +if (!$isLoggedIn) header("Location: /-/login") and die(); + +$title = "Dashboard"; require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/header.php'; global $_PROFILE; +use om\IcalParser; + +$poniesHavingSex = []; + +$actions = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/actions.json"), true); + +foreach ($actions as $action) { + if ($action["type"] !== "sexual") continue; + + foreach ($action["ponies"] as $ponies) { + $id = implode("", $ponies["members"]); + + $member = getMemberWithoutSystem($ponies["members"][0]); + $otherMember = getMemberWithoutSystem($ponies["members"][1]); + + $parts = [ + $member["id"], + $otherMember["id"] + ]; + + asort($parts); + $poniesHavingSex[] = $parts[0]; + $poniesHavingSex[] = $parts[1]; + } +} + +$fronter = array_map(function ($i) { + return $i["id"]; +}, ($_PROFILE["login"] === "raindrops" ? json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/gdapd-fronters.json"), true) : json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/ynmuc-fronters.json"), true))["members"])[0]; + +$viewingPonyHasSex = in_array($fronter, $poniesHavingSex) || isset($_GET['toys']); + +?> + +<br> +<div class="container"> + <div id="page-content"> + <h3 id="date" style="text-align: center;margin-bottom:20px;">Date</h3> + <script> + function _Date() { + let d = new Date(); + let m = d.getMonth(); + let w = d.getDay(); + + switch (m) { + case 0: m = "Jan"; break; + case 1: m = "Feb"; break; + case 2: m = "Mar"; break; + case 3: m = "Apr"; break; + case 4: m = "May"; break; + case 5: m = "Jun"; break; + case 6: m = "Jul"; break; + case 7: m = "Aug"; break; + case 8: m = "Sep"; break; + case 9: m = "Oct"; break; + case 10: m = "Nov"; break; + case 11: m = "Dec"; break; + } + + switch (w) { + case 0: w = "Sun"; break; + case 1: w = "Mon"; break; + case 2: w = "Tue"; break; + case 3: w = "Wed"; break; + case 4: w = "Thu"; break; + case 5: w = "Fri"; break; + case 6: w = "Sat"; break; + } + + function fixed(number) { + if (number < 10) { + return "0" + number; + } else { + return number.toString(); + } + } + + document.getElementById("date").innerText = `${w} ${d.getDate()} ${m} ${d.getHours()}:${fixed(d.getMinutes())}`; + } + + setInterval(_Date); + </script> + + <div class="peh-row" style="<?php if (!$viewingPonyHasSex): ?>grid-template-columns: repeat(6, 1fr) !important;<?php endif; ?>"> + <div class="column"> + <div class="card"> + <a href="/-/actions" class="card-body" style="text-align:center;color: white !important; text-decoration: none !important;"> + <img src="/assets/icons/actions.svg" style="width:32px;height:32px;filter:invert(1);"><br> + <h6 class="app-name">Actions</h6> + </a> + </div> + </div> + <div class="column"> + <div class="card"> + <a href="/-/rules" class="card-body" style="text-align:center;color: white !important; text-decoration: none !important;"> + <img src="/assets/icons/rules.svg" style="width:32px;height:32px;filter:invert(1);"><br> + <h6 class="app-name">Rules</h6> + </a> + </div> + </div> + <div class="column"> + <div class="card"> + <a href="/-/nicknames" class="card-body" style="text-align:center;color: white !important; text-decoration: none !important;"> + <img src="/assets/icons/nicknames.svg" style="width:32px;height:32px;filter:invert(1);"><br> + <h6 class="app-name">Nicknames</h6> + </a> + </div> + </div> + <div class="column"> + <div class="card"> + <a href="/-/together" class="card-body" style="text-align:center;color: white !important; text-decoration: none !important;"> + <img src="/assets/icons/together.svg" style="width:32px;height:32px;filter:invert(1);"><br> + <h6 class="app-name">Watch Together</h6> + </a> + </div> + </div> + <div class="column"> + <div class="card"> + <a href="/-/splitting" class="card-body" style="text-align:center;color: white !important; text-decoration: none !important;"> + <img src="/assets/icons/form.svg" style="width:32px;height:32px;filter:invert(1);"><br> + <h6 class="app-name">Splits</h6> + </a> + </div> + </div> + <div class="column"> + <div class="card"> + <a href="/-/bitset" class="card-body" style="text-align:center;color: white !important; text-decoration: none !important;"> + <img src="/assets/icons/bitset.svg" style="width:32px;height:32px;filter:invert(1);"><br> + <h6 class="app-name">Bitset</h6> + </a> + </div> + </div> + <?php if ($viewingPonyHasSex): ?> + <div class="column"> + <div class="card"> + <a href="/-/toys" class="card-body" style="text-align:center;color: white !important; text-decoration: none !important;"> + <img src="/assets/icons/toys.svg" style="width:32px;height:32px;filter:invert(1);"><br> + <h6 class="app-name">Toys</h6> + </a> + </div> + </div> + <?php endif; ?> + </div> + + <hr> + <h4>Next fronters <span class="small"><a href="/-/fronting">(edit)</a></span></h4> + <?php + + $scheduleCloudburstToday = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/ynmuc-planner.json"), true)[date('Y-m-d')] ?? []; + $scheduleRaindropsToday = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/gdapd-planner.json"), true)[date('Y-m-d')] ?? []; + + $scheduleCloudburstTomorrow = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/ynmuc-planner.json"), true)[date('Y-m-d', time() + 86400)] ?? []; + $scheduleRaindropsTomorrow = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/gdapd-planner.json"), true)[date('Y-m-d', time() + 86400)] ?? []; + + $today1 = $_PROFILE["login"] === "raindrops" ? $scheduleRaindropsToday : $scheduleCloudburstToday; + $today2 = $_PROFILE["login"] === "raindrops" ? $scheduleCloudburstToday : $scheduleRaindropsToday; + $tomorrow1 = $_PROFILE["login"] === "raindrops" ? $scheduleRaindropsTomorrow : $scheduleCloudburstTomorrow; + $tomorrow2 = $_PROFILE["login"] === "raindrops" ? $scheduleCloudburstTomorrow : $scheduleRaindropsTomorrow; + + ?> + + <h5>Fronters today</h5> + <ul> + <?php foreach ($today1 as $index => $id): $member = getSystemMember($_PROFILE["login"] === "raindrops" ? "gdapd" : "ynmuc", $id); ?> + <li> + <img alt="" src="/assets/uploads/pt-<?= $member["name"] ?>.png" style="width:24px; height: 24px; vertical-align: middle;"> <b style="vertical-align: middle;"><?= getMiniName($member["display_name"] ?? $member["name"]) ?></b> + <?php if (isset($today2[$index])): $member2 = getSystemMember($_PROFILE["login"] === "raindrops" ? "ynmuc" : "gdapd", $today2[$index]); ?><span style="vertical-align: middle;">with</span> <img alt="" src="/assets/uploads/pt-<?= $member2["name"] ?>.png" style="width:24px; height: 24px; vertical-align: middle;"> <span style="vertical-align: middle;"><?= getMiniName($member2["display_name"] ?? $member2["name"]) ?></span><?php endif; ?> + </li> + <?php endforeach; ?> + </ul> + + <h5>Fronters tomorrow</h5> + <ul> + <?php foreach ($tomorrow1 as $index => $id): $member = getSystemMember($_PROFILE["login"] === "raindrops" ? "gdapd" : "ynmuc", $id); ?> + <li> + <img alt="" src="/assets/uploads/pt-<?= $member["name"] ?>.png" style="width:24px; height: 24px; vertical-align: middle;"> <b style="vertical-align: middle;"><?= getMiniName($member["display_name"] ?? $member["name"]) ?></b> + <?php if (isset($tomorrow2[$index])): $member2 = getSystemMember($_PROFILE["login"] === "raindrops" ? "ynmuc" : "gdapd", $tomorrow2[$index]); ?><span style="vertical-align: middle;">with</span> <img alt="" src="/assets/uploads/pt-<?= $member2["name"] ?>.png" style="width:24px; height: 24px; vertical-align: middle;"> <span style="vertical-align: middle;"><?= getMiniName($member2["display_name"] ?? $member2["name"]) ?></span><?php endif; ?> + </li> + <?php endforeach; ?> + </ul> + + <hr> + <h4>Next events</h4> + <?php + + $cal = new IcalParser(); + $results = $cal->parseFile($_SERVER['DOCUMENT_ROOT'] . "/includes/data/calendar.ics"); + $events = []; + + foreach ($cal->getEvents()->sorted() as $event) { + $events[] = [ + "id" => $event['UID'], + "date" => [ + "created" => $event['CREATED']->format('c'), + "modified" => $event['LAST-MODIFIED']->format('c'), + "start" => $event['DTSTART']->format('c'), + "end" => $event['DTEND']->format('c'), + "duration" => strtotime($event['DTEND']->format('c')) - strtotime($event['DTSTART']->format('c')), + "full_day" => strtotime($event['DTEND']->format('c')) - strtotime($event['DTSTART']->format('c')) >= 86400, + "days" => ( + strtotime($event['DTEND']->format('c')) - strtotime($event['DTSTART']->format('c')) >= 86400 ? + round((strtotime($event['DTEND']->format('c')) - strtotime($event['DTSTART']->format('c'))) / 86400) : + null + ) + ], + "name" => $event['SUMMARY'], + "description" => $event['DESCRIPTION'], + ]; + } + + $events = array_values(array_filter($events, function ($i) { + return ( + strtotime($i["date"]["end"]) > time() && + strtotime($i["date"]["start"]) < time() + 2629800 + ); + })); + + ?> + <ul> + <?php foreach ($events as $event): ?> + <li> + <?php if ($event["date"]["full_day"]): ?> + <b> + <?php if (strtotime($event["date"]["start"]) < time()): ?> + Started <?= timeAgo($event["date"]["start"]) ?>, ends <?= timeIn($event["date"]["end"]) ?> + <?php else: ?> + <?= ucfirst(relativeDate($event["date"]["start"], false)) ?> + <?php endif; ?> + · + </b> + <?php else: ?> + <b> + <?= ucfirst(relativeDate($event["date"]["start"])) ?> + · + </b> + <?php endif; ?> + for <?= duration($event["date"]["duration"]) ?> + · + <?= $event["name"] ?> + </li> + <?php endforeach; ?> + </ul> + </div> +</div> + +<style> + .peh-row { + display: grid; + grid-template-columns: repeat(7, 1fr); + } + + .column .card { + border-right: none !important; + border-radius: 0 !important; + } + + .column:nth-child(1) .card { + border-bottom-left-radius: 0.25rem !important; + border-top-left-radius: 0.25rem !important; + } + + .column:nth-last-child(1) .card { + border-right: 1px solid rgba(255, 255, 255, .125) !important; + border-bottom-right-radius: 0.25rem !important; + border-top-right-radius: 0.25rem !important; + } + + @media (max-width: 1199px) { + .app-name { + display: none; + } + } + + @media (max-width: 991px) { + .column { + padding-right: calc(var(--bs-gutter-x) * .25); + padding-left: calc(var(--bs-gutter-x) * .25); + } + } + + @media (max-width: 576px) { + .card-body { + padding: 0.5rem 0.5rem !important; + } + + .column { + padding-right: calc(var(--bs-gutter-x) * .175) !important; + padding-left: calc(var(--bs-gutter-x) * .175) !important; + } + } +</style> + +<?php require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/footer.php'; ?> diff --git a/pages/debug.php b/pages/debug.php new file mode 100644 index 0000000..338a896 --- /dev/null +++ b/pages/debug.php @@ -0,0 +1,91 @@ +<?php $title = "Data updater debugging"; require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/header.php'; + +function itemToName(string $item): string { + if ($item === "restore") return "Failure protection"; + if ($item === "backups") return "Encrypted off-site backups"; + if ($item === "calendar") return "Google Calendar integration"; + + if (str_starts_with($item, "system-")) { + $system = explode("-", $item)[2]; + $type = explode("-", $item)[1]; + $systemName = $system === "gdapd" ? "Raindrops System" : "Cloudburst System"; + + return match ($type) { + "general" => "General info about $systemName", + "members" => "Members in the $systemName", + "fronters" => "Current fronter(s) in the $systemName", + "switches" => "Switch history from the $systemName", + default => "$type in $systemName", + }; + } + + if (str_starts_with($item, "images-")) { + $system = explode("-", $item)[1]; + $id = explode("-", $item)[2]; + $systemName = $system === "gdapd" ? "Raindrops" : "Cloudburst"; + $member = getSystemMember($system, $id) ?? [ "name" => $id, "display_name" => $id, "id" => $id ]; + + if ($member["name"] === "unknown") { + return "Unknown (" . $systemName . ")'s images"; + } else { + return getMiniName($member["display_name"] ?? $member["name"]) . "'s images"; + } + } + + return "<code>$item</code>"; +} + +?> + +<br> +<div class="container"> + <?php $data = json_decode(file_get_contents($_SERVER["DOCUMENT_ROOT"] . "/includes/data/refresh.json"), true); ?> + + <div id="page-content"> + <h2>Data updater debugging</h2> + <p>This page provides debugging information to troubleshoot unexpectedly long update times or reported failures.</p> + + <h4>General information</h4> + <ul> + <li><b>Update date:</b> <?php $dt = DateTime::createFromFormat('U.u', $data["timestamp"]); echo($dt->format("l j F Y, G:i:s.u T")); ?></li> + <li><b>Total duration:</b> <?= round($data["duration"] * 1000) ?> ms</li> + <li><b>Longest operation:</b> <?= itemToName(array_search(max(array_values($data["times"])), $data["times"])) ?? "-" ?> (<?= round(max(array_values($data["times"])) * 1000) ?> ms, <?= round((max(array_values($data["times"])) / $data["duration"]) * 100, 2) ?>%)</li> + </ul> + + <h4>Processing times</h4> + <div> + <?php foreach ($data["times"] as $item => $time): ?><span class="element tooltip-nohelp" title="<b><?= itemToName($item) ?></b><br>(<?= round($time * 1000) ?> ms)" data-bs-toggle="tooltip" data-bs-html="true" style="opacity:.75;display:inline-block;background:#<?= substr(md5($item), 0, 6) ?>;height:8px;margin:4px 0;width:<?= ($time / $data["duration"]) * 100 ?>%"></span><?php endforeach; ?> + </div> + <ul> + <?php foreach ($data["times"] as $item => $time): ?> + <li><b><?= itemToName($item) ?>:</b> <?= round($time * 1000) ?> ms</li> + <?php endforeach; ?> + </ul> + + <h4>Reported failures</h4> + <?php if (count($data["restored"]) < 1): ?> + <p><i>The data updater has not reported any update failure in the last run.</i></p> + <?php else: ?> + <p>The following files have failed to update:</p> + <ul> + <?php foreach ($data["restored"] as $item): ?> + <li><code><?= $item ?></code></li> + <?php endforeach; ?> + </ul> + <?php endif; ?> + </div> +</div> + +<style> + .element:hover { + margin: 0 !important; + height: 16px !important; + opacity: 1 !important; + } + + .element { + transition: margin 200ms, height 200ms, opacity 200ms; + } +</style> + +<?php require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/footer.php'; ?> diff --git a/pages/edit.php b/pages/edit.php index 74105e2..ae007be 100644 --- a/pages/edit.php +++ b/pages/edit.php @@ -14,7 +14,7 @@ function getSubsystemByID(string $id) { } require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $isLoggedIn; -if (!$isLoggedIn) header("Location: /login") and die(); +if (!$isLoggedIn) header("Location: /-/login") and die(); if (!isset($_GET['_']) || trim($_GET['_']) === "") header("Location: /?error=Invalid request") and die(); diff --git a/pages/emergency.php b/pages/emergency.php index 0405bca..ba2a35e 100644 --- a/pages/emergency.php +++ b/pages/emergency.php @@ -1,7 +1,7 @@ <?php require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $isLoggedIn; -if (!$isLoggedIn) header("Location: /login") and die(); +if (!$isLoggedIn) header("Location: /-/login") and die(); $emergencyHeader = true; $title = "Emergency alert"; require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/header.php'; diff --git a/pages/fronting.php b/pages/fronting.php index eae0b61..261cf03 100644 --- a/pages/fronting.php +++ b/pages/fronting.php @@ -1,4 +1,4 @@ -<?php require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $isLoggedIn; if (!$isLoggedIn) header("Location: /login") and die(); $title = "Front planner"; require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/header.php'; ?> +<?php require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $isLoggedIn; if (!$isLoggedIn) header("Location: /-/login") and die(); $title = "Front planner"; require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/header.php'; ?> <br> <div class="container"> diff --git a/pages/nicknames.php b/pages/nicknames.php new file mode 100644 index 0000000..050c275 --- /dev/null +++ b/pages/nicknames.php @@ -0,0 +1,117 @@ +<?php + +require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $isLoggedIn; +if (!$isLoggedIn) header("Location: /-/login") and die(); + +$title = "Relations nicknames"; require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/header.php'; + +$members = scoreOrderGlobal(); +$relations = []; + +foreach ($members as $member) { + foreach ([ + ...array_map(function ($i) { + $r = [ + "name" => $i + ]; + $r["type"] = "marefriends"; + return $r; + }, $member["_metadata"]["marefriends"] ?? []), + ...array_map(function ($i) { + $r = [ + "name" => $i + ]; + $r["type"] = "sisters"; + return $r; + }, $member["_metadata"]["sisters"] ?? []), + ...array_map(function ($i) { + $r = [ + "name" => $i + ]; + $r["type"] = "caretaking"; + return $r; + }, $member["_metadata"]["caretakers"] ?? []) + ] as $rel) { + $id = $rel["name"]; + $otherMember = getSystemMember(explode("/", $id)[0], explode("/", $id)[1]); + + $parts = [ + $member["id"], + $otherMember["id"] + ]; + + asort($parts); + + $relations[implode("-", $parts)] = [ + "id" => implode("", $parts), + "name" => getMiniName($member["display_name"] ?? $member["name"]) . " and " . getMiniName($otherMember["display_name"] ?? $otherMember["name"]), + "type" => $rel["type"], + "images" => [ + file_exists($_SERVER['DOCUMENT_ROOT'] . "/assets/uploads/pt-$member[name].png") ? "/assets/uploads/pt-$member[name].png" : "/assets/uploads/pt.png", + file_exists($_SERVER['DOCUMENT_ROOT'] . "/assets/uploads/pt-$otherMember[name].png") ? "/assets/uploads/pt-$otherMember[name].png" : "/assets/uploads/pt.png", + ] + ]; + } +} + +$nicknames = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/nicknames.json"), true); + +?> + +<br> +<div class="container"> + <div id="page-content"> + <h2>Relations nicknames</h2> + <?php foreach ($relations as $relation): ?> + <div class="relation" style="background-color:rgba(255, 255, 255, .1);margin-bottom:10px;padding:10px;border-radius:10px;display:grid;grid-template-columns: 1fr 3fr;"> + <a class="relation-intro" title="<?= $relation["id"] ?>" data-bs-toggle="tooltip" style="background-color:rgba(255, 255, 255, .05);border-right:1px solid rgba(255, 255, 255, .1);margin:-10px;padding:10px;border-top-left-radius:10px;border-bottom-left-radius:10px;color: white;text-decoration: none;"> + <span style="vertical-align: middle;"><img src="<?= $relation["images"][0] ?>" style="width:24px;"><img src="<?= $relation["images"][1] ?>" style="width:24px;"></span> <span style="vertical-align: middle;"><?= $relation["name"] ?></span> + </a> + <div class="relation-item relation-item-marefriends" style="text-align:left;margin-left:10px;padding:0 20px;"> + <?php if (isset($nicknames[$relation["id"]])): ?> + "<?= implode('", "', $nicknames[$relation["id"]]) ?>" + <?php else: ?> + <span class="text-muted">No nickname for this relation</span> + <?php endif; ?> + </div> + </div> + <?php endforeach; ?> + </div> +</div> + +<style> + .relation-intro { + opacity: 1 !important; + } + + @media (max-width: 991px) { + .relation { + grid-template-columns: 1fr !important; + } + + .relation-intro { + text-align: center; + border-bottom-left-radius: 0 !important; + border-top-right-radius: 10px; + border-right: none !important; + border-bottom: 1px solid rgba(255, 255, 255, .1); + } + + .relation-item-marefriends { + margin-top: 20px !important; + text-align: center !important; + } + + .relation-item { + margin-top: 10px; + margin-left: 0 !important; + padding: 10px 0 !important; + } + } + + .relation-item { + text-align: center; + } +</style> + +<?php require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/footer.php'; ?> diff --git a/pages/parser.php b/pages/parser.php index cb07179..b49fd00 100644 --- a/pages/parser.php +++ b/pages/parser.php @@ -3,7 +3,7 @@ <br> <div class="container"> <div id="page-content"> - <h2>Message Parser</h2> + <h2>Message parser</h2> <p>Enter a message here, and we will tell you which system member this message came from.</p> <div style="display:grid;grid-template-columns:1fr 2fr;grid-gap:10px;"> diff --git a/pages/pleasure.php b/pages/pleasure.php index db63f5a..295e597 100644 --- a/pages/pleasure.php +++ b/pages/pleasure.php @@ -1,7 +1,7 @@ <?php require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $isLoggedIn; -if (!$isLoggedIn) header("Location: /login") and die(); +if (!$isLoggedIn) header("Location: /-/login") and die(); $emergencyHeader = true; $title = "Pleasure alert"; require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/header.php'; diff --git a/pages/prefix.php b/pages/prefix.php index 73e9c8a..cba68b2 100644 --- a/pages/prefix.php +++ b/pages/prefix.php @@ -3,7 +3,7 @@ <br> <div class="container"> <div id="page-content"> - <h2>Prefix Generator</h2> + <h2>Prefix generator</h2> <p>This prefix generator will take into account the prefixes from the existing members to try to generate new original prefixes for a potential new member.</p> <div style="display:grid;grid-template-columns:1fr 1fr 1fr;grid-gap:10px;"> diff --git a/pages/rules.php b/pages/rules.php new file mode 100644 index 0000000..b1759dc --- /dev/null +++ b/pages/rules.php @@ -0,0 +1,254 @@ +<?php + +require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $isLoggedIn; +if (!$isLoggedIn) header("Location: /-/login") and die(); + +if (isset($_POST["updateRules"])) { + header("Content-Type: text/plain"); + + if (!isset($_POST['payload'])) { + header("Location: /-/rules"); + die(); + } + + foreach ($_POST['payload'] as $index => $rule) { + if (!isset($rule["name"]) || !isset($rule["content"]) && !is_numeric($index)) { + header("Location: /-/rules"); + die(); + } + + if (trim($rule["name"]) === "") { + unset($_POST["payload"][$index]); + continue; + } + + if (!isset($rule["approved"])) $rule["approved"] = []; + if (isset($rule["approved"][0])) $rule["approved"][0] = true; else $rule["approved"][0] = false; + if (isset($rule["approved"][1])) $rule["approved"][1] = true; else $rule["approved"][1] = false; + + $_POST["payload"][$index] = $rule; + } + + file_put_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/rules.json", utf8_encode(json_encode($_POST["payload"]))); + + header("Location: /-/rules"); + die(); +} + +$title = "Systems rules"; require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/header.php'; + +?> + +<br> +<div class="container"> + <div id="page-content"> + <h2>Systems rules</h2> + <p>Click on a rule to view additional details. <a onclick="event.target.blur();" href="#" data-bs-toggle="modal" data-bs-target="#editor">Edit rules</a></p> + + <?php + + $rules = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/rules.json"), true); + + $protectorCloudburst = array_values(array_filter(scoreOrderGlobal(), function ($i) { + return $i["_system"] === "ynmuc" && $i["_metadata"]["protector"]; + }))[0]; + $protectorRaindrops = array_values(array_filter(scoreOrderGlobal(), function ($i) { + return $i["_system"] === "gdapd" && $i["_metadata"]["protector"]; + }))[0]; + + $pcName = getMiniName($protectorCloudburst["display_name"] ?? $protectorCloudburst["name"]); + $prName = getMiniName($protectorRaindrops["display_name"] ?? $protectorRaindrops["name"]); + + ?> + + <ul class="list-group"> + <?php $index = 1; foreach ($rules as $rule): ?> + <li class="list-group-item rule-outer"> + <details> + <summary class="rule"> + <b><?= $index ?>. <?= strip_tags($rule["name"]) ?></b> + <?php if (in_array(false, $rule["approved"])): ?> + <span class="badge bg-warning text-black rounded-pill">Unapproved</span> + <?php endif; ?> + </summary> + <?php if (in_array(false, $rule["approved"])): ?> + <div style="margin-top:10px;" class="alert alert-warning"> + <b>This rule has not yet been approved.</b> All rules need to be approved by the protectors from both systems. This rule is still missing approval from <?php + + if ($rule["approved"][0] === false) { + if ($rule["approved"][1] === false) { + echo($pcName . " and " . $prName); + } else { + echo($pcName); + } + } else if ($rule["approved"][1] === false) { + echo($prName); + } + + ?>. + </div> + <?php endif; ?> + <div <?= !in_array(false, $rule["approved"]) ? 'style="margin-top:10px;"' : '' ?> class="list-group-item"> + <?= strip_tags($rule["content"]) ?> + </div> + </details> + </li> + <?php $index++; endforeach; ?> + </ul> + </div> +</div> + +<div class="modal fade" id="editor"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h4 class="modal-title">Rules editor</h4> + <button type="button" class="btn-close" data-bs-dismiss="modal"></button> + </div> + + <div class="modal-body"> + <p>Rules with an empty name are automatically deleted. Buttons to add new rules and save the changes are at the bottom of the list.</p> + <hr> + <form method="post"> + <div id="rules-editor"> + <?php $index = 1; foreach ($rules as $rule): ?> + <div <?= $index === 1 ? 'id="default-rule"' : '' ?>> + <p><b <?= $index === 1 ? 'id="default-rule--number"' : '' ?>>Rule #<?= $index ?>:</b></p> + <input <?= $index === 1 ? 'id="default-rule--name"' : '' ?> type="text" placeholder="Rule name" class="form-control" style="margin-bottom:15px;color:white;background:#111;border-color:#222;" name="payload[<?= $index - 1 ?>][name]" value="<?= str_replace('"', """, strip_tags($rule["name"])) ?>"> + + <textarea <?= $index === 1 ? 'id="default-rule--content"' : '' ?> name="payload[<?= $index - 1 ?>][content]" class="form-control" style="resize: none;color:white;background:#111;border-color:#222;" placeholder="Rule details"><?= strip_tags($rule["content"] ?? "") ?></textarea> + + <label style="margin-top:10px;margin-left:5px;"> + <input <?= $index === 1 ? 'id="default-rule--approval-1"' : '' ?> <?= ($rule["approved"][0] ?? false) ? "checked" : "" ?> type="checkbox" name="payload[<?= $index - 1 ?>][approved][0]"> + Approved by <?= $pcName ?> + </label><br> + <label style="margin-left:5px;"> + <input <?= $index === 1 ? 'id="default-rule--approval-2"' : '' ?> <?= ($rule["approved"][1] ?? false) ? "checked" : "" ?> type="checkbox" name="payload[<?= $index - 1 ?>][approved][1]"> + Approved by <?= $prName ?> + </label><br> + + <hr> + </div> + <?php $index++; endforeach; ?> + </div> + <input type="submit" value="Save" class="btn btn-primary"> + <a onclick="editorNewRule();" class="btn btn-secondary">New rule</a> + <input type="hidden" name="updateRules"> + </form> + </div> + </div> + </div> +</div> + +<script> + window.numberOfRules = <?= count($rules) ?>; + + Array.from(document.getElementsByClassName("rule-outer")).forEach((el) => { + let details = el.children[0]; + + el.onclick = () => { + Array.from(document.getElementsByClassName("rule-outer")).forEach((sel) => { + if (el === sel) return; + sel.children[0].open = false; + sel.classList.remove("open"); + }); + + details.open = !details.open; + + if (details.open) { + el.classList.add("open"); + } else { + el.classList.remove("open"); + } + } + }) + + function editorNewRule() { + let id = Math.random(36).toString().split(".")[1]; + + document.getElementById("default-rule").id = "added-" + id + "-1"; + document.getElementById("default-rule--number").id = "added-" + id + "-2"; + document.getElementById("default-rule--name").id = "added-" + id + "-3"; + document.getElementById("default-rule--content").id = "added-" + id + "-4"; + document.getElementById("default-rule--approval-1").id = "added-" + id + "-5"; + document.getElementById("default-rule--approval-2").id = "added-" + id + "-6"; + + let child = document.createElement("div"); + child.id = "temp-" + id; + window.numberOfRules++; + + document.getElementById("rules-editor").appendChild(child); + document.getElementById("temp-" + id).outerHTML = document.getElementById("added-" + id + "-1").outerHTML; + + document.getElementById("added-" + id + "-1").id = "default-rule"; + document.getElementById("added-" + id + "-2").id = "default-rule--number"; + document.getElementById("added-" + id + "-3").id = "default-rule--name"; + document.getElementById("added-" + id + "-4").id = "default-rule--content"; + document.getElementById("added-" + id + "-5").id = "default-rule--approval-1"; + document.getElementById("added-" + id + "-6").id = "default-rule--approval-2"; + + document.getElementById("added-" + id + "-3").name = "payload[" + (numberOfRules - 1) + "][name]"; + document.getElementById("added-" + id + "-4").name = "payload[" + (numberOfRules - 1) + "][content]"; + document.getElementById("added-" + id + "-5").name = "payload[" + (numberOfRules - 1) + "][approved][0]"; + document.getElementById("added-" + id + "-6").name = "payload[" + (numberOfRules - 1) + "][approved][1]"; + + document.getElementById("added-" + id + "-5").checked = false; + document.getElementById("added-" + id + "-6").checked = false; + document.getElementById("added-" + id + "-3").value = ""; + document.getElementById("added-" + id + "-4").value = ""; + document.getElementById("added-" + id + "-2").innerText = "Rule #" + numberOfRules + ":"; + } +</script> + +<style> + .list-group-item { + color: #fff; + background-color: #222; + border: 1px solid rgba(255, 255, 255, .125); + } + + .list-group-item.disabled { + color: #fff; + background-color: #222; + border-color: rgba(255, 255, 255, .125); + opacity: .75; + } + + .rule-outer:hover { + background-color: #252525; + color: #ddd; + } + + .rule-outer:active, .rule-outer:focus { + background-color: #272727; + color: #bbb; + } + + .rule-outer.open { + background-color: #333; + } + + .rule { + list-style: none; + pointer-events: none; + } + + .rule-outer { + cursor: pointer; + } + + .modal-header { + border-bottom: 1px solid #353738; + } + + .modal-content { + border: 1px solid rgba(255, 255, 255, .2); + background-color: #111; + } + + .btn-close { + filter: invert(1); + } +</style> + +<?php require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/footer.php'; ?> diff --git a/pages/score.php b/pages/score.php index 96a9d17..043a695 100644 --- a/pages/score.php +++ b/pages/score.php @@ -1,7 +1,7 @@ <?php require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $isLoggedIn; -if (!$isLoggedIn) header("Location: /login") and die(); +if (!$isLoggedIn) header("Location: /-/login") and die(); $title = "Score system testing"; require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/header.php'; @@ -10,7 +10,7 @@ $title = "Score system testing"; require_once $_SERVER['DOCUMENT_ROOT'] . '/incl <br> <div class="container"> <div id="page-content"> - <h2>Score System Testing</h2> + <h2>Score system testing</h2> <h4>Raindrops System (<code><?php diff --git a/pages/splitting.php b/pages/splitting.php index fab97f1..7b5b1f5 100644 --- a/pages/splitting.php +++ b/pages/splitting.php @@ -1,7 +1,7 @@ <?php require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $isLoggedIn; -if (!$isLoggedIn) header("Location: /login") and die(); +if (!$isLoggedIn) header("Location: /-/login") and die(); $title = "Members by splitting date"; require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/header.php'; diff --git a/pages/together.php b/pages/together.php index 644a6c0..4e39736 100644 --- a/pages/together.php +++ b/pages/together.php @@ -1,7 +1,7 @@ <?php require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $isLoggedIn; -if (!$isLoggedIn) header("Location: /login") and die(); +if (!$isLoggedIn) header("Location: /-/login") and die(); $title = "Watch Together"; require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/header.php'; diff --git a/pages/toys.php b/pages/toys.php new file mode 100644 index 0000000..e4e5def --- /dev/null +++ b/pages/toys.php @@ -0,0 +1,915 @@ +<?php + +require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $isLoggedIn; +if (!$isLoggedIn) header("Location: /-/login") and die(); + +if (isset($_POST['deleteAction'])) { + $data = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/toys.json"), true); + + $selected = null; + $selectedIndex = -1; + $id = $_POST['action']; + + foreach ($data as $index => $item) { + if ($item["id"] === $id) { + $selectedIndex = $index; + $selected = $item; + break; + } + } + + if ($selected === null) { + header("Location: /-/toys"); + die(); + } + + unset($data[$selectedIndex]); + file_put_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/toys.json", utf8_encode(json_encode($data))); + header("Location: /-/toys/?d&id=" . $id); + die(); +} + +if (isset($_POST['updateAction'])) { + $data = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/toys.json"), true); + + $selected = null; + $selectedIndex = -1; + $id = $_POST['action']; + + foreach ($data as $index => $item) { + if ($item["id"] === $id) { + $selectedIndex = $index; + $selected = $item; + break; + } + } + + if ($selected === null) { + header("Location: /-/toys"); + die(); + } + + if (isset($_POST["sexual"])) { + $selected["sexual"] = true; + } else { + $selected["sexual"] = false; + } + + if (isset($_POST["name"])) $selected["name"] = strip_tags(trim($_POST["name"])); + if (isset($_POST["usage"])) $selected["usage"] = strip_tags(trim($_POST["usage"])); + if (isset($_POST["irl"])) $selected["irl"] = strip_tags(trim($_POST["irl"])); + if (isset($_POST["keywords"])) $selected["keywords"] = array_map(function ($i) { + return trim($i); + }, explode(",", strip_tags(trim($_POST["keywords"])))); + if (isset($_POST["description"])) $selected["description"] = strip_tags(trim($_POST["description"])); + if (isset($_POST["water"])) $selected["water"] = match ($_POST["water"]) { + "0" => "out", + "1" => "in", + "2" => "both", + "3" => "playground" + }; + + if (isset($_POST["relations"])) { + $ponies = []; + + foreach ($_POST["relations"] as $relation => $d) { + $ponies[] = [ + "members" => explode("-", $relation), + "deprecated" => isset($d["deprecated"]), + "sexual" => isset($d["sexual"]) + ]; + } + + $selected["ponies"] = $ponies; + } + + global $_PROFILE; + if ($_PROFILE['login'] === "raindrops" && isset($_POST["verified"])) { + $selected["verified"] = true; + } else { + unset($selected["verified"]); + } + + if (isset($_POST["untested"])) { + $selected["untested"] = true; + } else { + unset($selected["untested"]); + } + + $data[$selectedIndex] = $selected; + file_put_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/toys.json", utf8_encode(json_encode($data))); + + header("Location: /-/toys/" . $id); + die(); +} + +if (isset($_POST['createAction'])) { + require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/random.php"; + $data = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/toys.json"), true); + + if (!isset($_POST["name"]) || !isset($_POST["water"])) { + header("Location: /-/toys"); + die(); + } + if (trim($_POST["name"]) === "" || !is_numeric($_POST["water"])) { + header("Location: /-/toys"); + die(); + } + + $water = match ($_POST["water"]) { + "0" => "out", + "1" => "in", + "2" => "both", + "3" => "playground" + }; + $name = strip_tags(trim($_POST["name"])); + $id = random(); + + $ponies = []; + + if (isset($_POST["relations"])) { + foreach ($_POST["relations"] as $relation => $_) { + $ponies[] = [ + "members" => explode("-", $relation), + "deprecated" => false + ]; + } + } + + if (isset($_POST["sexual"])) { + $sexual = true; + } else { + $sexual = false; + } + + $data[] = [ + "id" => $id, + "name" => $name, + "water" => $water, + "description" => null, + "ponies" => $ponies, + "usage" => null, + "irl" => null, + "keywords" => [], + "sexual" => $sexual + ]; + + file_put_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/toys.json", utf8_encode(json_encode($data))); + header("Location: /-/toys/" . $id); + die(); +} + +global $pagename; +$parts = explode("/", $pagename); +$data = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/toys.json"), true); + +$selected = null; +$title = "Toys database"; + +if (isset($parts[1])) { + $id = $parts[1]; + + foreach ($data as $item) { + if ($item["id"] === $id) { + $selected = $item; + break; + } + } + + if ($selected === null) { + header("Location: /-/toys/?nf&id=" . $id); + die(); + } else { + $title = $selected["name"] . " · Toys database"; + } +} + +require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/header.php'; +require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/keywords.php'; + +if (!file_exists($_SERVER['DOCUMENT_ROOT'] . "/includes/data/toys.json")) file_put_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/toys.json", "[]"); + +global $_PROFILE; + +?> + +<script src="/assets/editor/fuse.js"></script> + +<br> +<div class="container"> + <div id="<?= isset($parts[1]) ? "page-content" : "" ?>"> + <?php if (isset($_GET['nf'])): ?> + <div class="alert alert-danger alert-dismissible"> + <button onclick='window.history.pushState({"html":null,"pageTitle":document.title},"", "/-/toys/");' type="button" class="btn-close" data-bs-dismiss="alert" style="filter: none !important;"></button> + <b>Error: </b> The requested toy (<code><?= strip_tags($_GET['id'] ?? "-") ?></code>) was not found, it may have been deleted or has never existed. + </div> + <?php endif; ?> + + <?php if (isset($_GET['d'])): ?> + <div class="alert alert-success alert-dismissible"> + <button onclick='window.history.pushState({"html":null,"pageTitle":document.title},"", "/-/toys");' type="button" class="btn-close" data-bs-dismiss="alert" style="filter: none !important;"></button> + <b>Success: </b> The toy with ID <code><?= strip_tags($_GET['id'] ?? "-") ?></code> has been successfully deleted. + </div> + <?php endif; ?> + + <?php if (isset($parts[1])): ?> + + <h2> + <span style="vertical-align: middle;"><?= $selected["name"] ?></span> + <a href="/-/toys" class="small btn btn-outline-light" style="float:right;margin-top:5px;vertical-align:middle;opacity:1 !important; color:white;">Back</a> + </h2> + <p> + <a onclick="event.target.blur();" data-bs-toggle="modal" data-bs-target="#editor" href="#">Edit</a> · + <?php if ($item["sexual"]): ?> + <span style="" class="badge rounded-pill bg-danger">Sexual</span> + <?php else: ?> + <span style="" class="badge rounded-pill bg-success">Pleasurable</span> + <?php endif; ?> + <?php if (!isset($item["verified"])): ?><span class="badge bg-warning rounded-pill text-black">Unverified</span><?php endif; ?> + <?php if (isset($item["untested"])): ?><span class="badge bg-info rounded-pill">Untested</span><?php endif; ?> + <?php if ($item["water"] === "in"): ?> + <span style="" class="badge rounded-pill bg-primary">Underwater</span> + <?php elseif ($item["water"] === "out"): ?> + <span style="background-color:#d63384;" class="badge rounded-pill">Outside of water</span> + <?php elseif ($item["water"] === "playground"): ?> + <span class="badge rounded-pill" style="background-color:#20c997;">In playground</span> + <?php else: ?> + <span style="" class="badge rounded-pill bg-primary">Underwater</span> + <span style="background-color:#d63384;" class="badge rounded-pill">Outside of water</span> + <?php endif; ?> + </p> + + <?php if (isset($selected["description"]) && trim($selected["description"]) !== ""): ?> + <?= replaceKeyWords(str_replace("\n", "<br>", strip_tags($selected["description"]))); ?> + <?php else: ?> + <p><i>No description provided for this toy. Enter edit mode to add a description to this toy.</i></p> + <?php endif; ?> + + <hr> + <b>Can be used by:</b> + <?php + + $hasMultipleTypes = false; + $seenType = ""; + + foreach ($selected["ponies"] as $relation) { + if (isset($relation["sexual"]) && $relation["sexual"]) { + $type = "sexual"; + } else { + $type = "affectionate"; + } + + if ($type !== $seenType && $seenType !== "") { + $hasMultipleTypes = true; + } + + if (trim($seenType) === "") { + $seenType = $type; + } + } + + ?> + <ul> + <?php foreach ($selected["ponies"] as $relation): if (!$relation["deprecated"]): + + $member1 = getMemberWithoutSystem($relation["members"][0]); + $member2 = getMemberWithoutSystem($relation["members"][1]); + + ?> + <li> + <img style="vertical-align: middle;width:24px;height:24px;" src="/assets/uploads/pt-<?= $member1["name"] ?>.png"> + <img style="vertical-align: middle;width:24px;height:24px;" src="/assets/uploads/pt-<?= $member2["name"] ?>.png"> + + <span style="vertical-align: middle;"><?= getMiniName($member1["display_name"] ?? $member1["name"]) ?> and <?= getMiniName($member2["display_name"] ?? $member2["name"]) ?></span> + </li> + <?php endif; endforeach; ?> + <?php foreach ($selected["ponies"] as $relation): if ($relation["deprecated"]): + + $member1 = getMemberWithoutSystem($relation["members"][0]); + $member2 = getMemberWithoutSystem($relation["members"][1]); + + ?> + <li> + <span style="opacity:.5;"> + <img style="vertical-align: middle;width:24px;height:24px;" src="/assets/uploads/pt-<?= $member1["name"] ?>.png"> + <img style="vertical-align: middle;width:24px;height:24px;" src="/assets/uploads/pt-<?= $member2["name"] ?>.png"> + + <span style="vertical-align: middle;"><?= getMiniName($member1["display_name"] ?? $member1["name"]) ?> and <?= getMiniName($member2["display_name"] ?? $member2["name"]) ?></span> + </span> + <span class="badge bg-danger rounded-pill">Deprecated</span> + </li> + <?php endif; endforeach; ?> + </ul> + + <div style="margin-top:10px;"></div> + + <b>Usage:</b><br> + <?php if (isset($selected["usage"]) && trim($selected["usage"]) !== ""): ?> + <?php + + $lines = explode("\n", strip_tags($selected["usage"])); + + if (count($lines) > 1) echo("<ul>"); + + foreach ($lines as $line) { + if (count($lines) > 1) echo("<li>"); + + $parts = explode(":", $line); + + if (count($parts) > 1 && strlen($parts[0]) < 30) { + $p0 = $parts[0]; array_shift($parts); + echo(replaceKeyWords("<b>" . $p0 . ":</b>" . implode(":", $parts))); + } else { + echo(replaceKeyWords(implode(":", $parts))); + } + + if (count($lines) > 1) echo("</li>"); + } + + if (count($lines) > 1) echo("</ul>"); + + ?> + <?php else: ?> + <p><i>No usage provided for this toy. Enter edit mode to add usage information to this toy.</i></p> + <?php endif; ?> + + <div style="margin-top:10px;"></div> + + <b>Instructions to craft in real life:</b><br> + <?php if (isset($selected["irl"]) && trim($selected["irl"]) !== ""): ?> + <?= replaceKeyWords(strip_tags($selected["irl"])) ?> + <?php else: ?> + <p><i>This toy is not craftable in real life.</i></p> + <?php endif; ?> + + <hr> + + <h4>Similar toys</h4> + <div class="row"> + <?php + + $names = []; + $currentName = $selected["name"]; + $namesByDistance = []; + + foreach ($data as $action) { + if ($action["name"] !== $currentName) $names[$action["name"]] = [ + "id" => $action["id"], + "water" => $action["water"], + "ponies" => $action["ponies"] + ]; + } + + foreach ($names as $name => $data) { + $namesByDistance[] = [ + "name" => $name, + "distance" => levenshtein($currentName, $name) + ((int)($data["type"] !== $selected["type"]) * 2), + "id" => $data["id"], + "water" => $data["water"], + "ponies" => $data["ponies"] + ]; + } + + uasort($namesByDistance, function ($a, $b) use ($selected) { + return $a["distance"] - $b["distance"]; + }); + + foreach ($namesByDistance as $item) { + echo("<!-- " . $currentName . " <-> " . $item["name"] . " => " . $item["distance"] . " (artif: " . ((int)($item["water"] !== $selected["water"]) * 10) . ") -->"); + } + + $index = 0; + foreach ($namesByDistance as $item): if ($index < 3): + ?> + <div class="col-md-4" style="margin-bottom:20px;"> + <a class="linked-card" href="/-/actions/<?= $item["id"] ?>"><div class="card"> + <div class="card-body"> + <h4 class="card-title"><?= $item["name"] ?></h4> + <p class="card-text"> + <?php + + $uniquePonies = []; + + foreach ($item["ponies"] as $ponies) { + foreach ($ponies["members"] as $member) { + if (isset($uniquePonies[$member]) && !$uniquePonies[$member]) { + $uniquePonies[$member] = false; + } else { + $uniquePonies[$member] = $ponies["deprecated"]; + } + } + } + + foreach ($uniquePonies as $name => $deprecated): if (!$deprecated): ?> + <span data-bs-toggle="tooltip" title="<?= getMemberWithoutSystem($name)["display_name"] ?? getMemberWithoutSystem($name)["display_name"] ?>" style="display: inline-block;"> + <img src="/assets/uploads/pt-<?= getMemberWithoutSystem($name)["name"] ?>.png" style="width:32px;"> + </span> + <?php endif; endforeach; ?> + <?php foreach ($uniquePonies as $name => $deprecated): if ($deprecated): ?> + <span data-bs-toggle="tooltip" data-bs-html="true" title="<i><?= strip_tags(getMemberWithoutSystem($name)["display_name"] ?? getMemberWithoutSystem($name)["name"]) ?></i>" style="opacity:.5;display: inline-block;"> + <img src="/assets/uploads/pt-<?= getMemberWithoutSystem($name)["name"] ?>.png" style="width:32px;"> + </span> + <?php endif; endforeach; ?> + </p> + <?php if ($item["sexual"]): ?> + <span style="" class="badge rounded-pill bg-danger">Sexual</span> + <?php else: ?> + <span style="" class="badge rounded-pill bg-success">Pleasurable</span> + <?php endif; ?> + <?php if ($item["water"] === "in"): ?> + <span style="" class="badge rounded-pill bg-primary">Underwater</span> + <?php elseif ($item["water"] === "out"): ?> + <span style="background-color:#d63384;" class="badge rounded-pill">Outside of water</span> + <?php elseif ($item["water"] === "playground"): ?> + <span class="badge rounded-pill" style="background-color:#20c997;">In playground</span> + <?php else: ?> + <span style="" class="badge rounded-pill bg-primary">Underwater</span> + <span style="background-color:#d63384;" class="badge rounded-pill">Outside of water</span> + <?php endif; ?> + </div> + </div></a> + </div> + <?php $index++; endif; endforeach; ?> + </div> + + <div class="modal fade" id="editor"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h4 class="modal-title">Edit toy</h4> + <button type="button" class="btn-close" data-bs-dismiss="modal"></button> + </div> + + <div class="modal-body"> + <form method="post" style="display:inline;"> + <input type="text" placeholder="Toy name" name="name" class="form-control" style="color:white;background:#111;border-color:#222;margin-bottom:10px;" value="<?= str_replace('"', '"', $selected["name"]) ?>"> + <select name="water" class="form-select" style='color:white;background-color:#111;border-color:#222;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23ffffff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");margin-bottom:10px;'> + <option value="0" <?= $selected["water"] === "out" ? "selected" : "" ?>>Usable outside of the water</option> + <option value="1" <?= $selected["water"] === "in" ? "selected" : "" ?>>Usable inside of the water</option> + <option value="2" <?= $selected["water"] === "both" ? "selected" : "" ?>>Usable both inside and outside</option> + <option value="3" <?= $selected["water"] === "playground" ? "selected" : "" ?>>Usable in a playground</option> + </select> + + <label style="margin-left:5px;"> + <input <?= ($selected["sexual"] ?? false) ? "checked" : "" ?> type="checkbox" name="sexual"> + Sexual toy + </label><br> + + <label style="margin-left:5px;"> + <input <?= ($selected["verified"] ?? false) ? "checked" : "" ?> <?= $_PROFILE['login'] !== "raindrops" ? "disabled" : "" ?> type="checkbox" name="verified"> + Mark as verified + </label><br> + + <label style="margin-left:5px;"> + <input <?= ($selected["untested"] ?? false) ? "checked" : "" ?> type="checkbox" name="untested"> + Mark as untested + </label> + + <hr> + + <textarea name="description" class="form-control" style="resize: none;color:white;background:#111;border-color:#222;margin-bottom:10px;" placeholder="Description"><?= strip_tags($selected["description"] ?? "") ?></textarea> + + <textarea placeholder="Toy usage" name="usage" class="form-control" style="resize: none;color:white;background:#111;border-color:#222;margin-bottom:10px;"><?= strip_tags($selected["usage"] ?? "") ?></textarea> + + <hr> + + <input type="text" placeholder="Keywords (comma-separated)" name="keywords" class="form-control" style="color:white;background:#111;border-color:#222;margin-bottom:10px;" value="<?= str_replace('"', '"', implode(",", $selected["keywords"] ?? [])) ?>"> + + <input type="text" placeholder="Instructions to craft IRL" name="irl" class="form-control" style="color:white;background:#111;border-color:#222;" value="<?= str_replace('"', '"', $selected["irl"] ?? "") ?>"> + + <hr> + + <p>Select the groups of ponies who can use this toy:</p> + <?php + + $relations = []; + $actions = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/actions.json"), true); + + foreach ($actions as $action) { + if ($action["type"] !== "sexual") continue; + + foreach ($action["ponies"] as $ponies) { + $id = implode("", $ponies["members"]); + + $member = getMemberWithoutSystem($ponies["members"][0]); + $otherMember = getMemberWithoutSystem($ponies["members"][1]); + + $parts = [ + $member["id"], + $otherMember["id"] + ]; + + asort($parts); + + $relations[implode("-", $parts)] = [ + "name" => getMiniName($member["display_name"] ?? $member["name"]) . " and " . getMiniName($otherMember["display_name"] ?? $otherMember["name"]), + "images" => [ + file_exists($_SERVER['DOCUMENT_ROOT'] . "/assets/uploads/pt-$member[name].png") ? "/assets/uploads/pt-$member[name].png" : "/assets/uploads/pt.png", + file_exists($_SERVER['DOCUMENT_ROOT'] . "/assets/uploads/pt-$otherMember[name].png") ? "/assets/uploads/pt-$otherMember[name].png" : "/assets/uploads/pt.png", + ] + ]; + } + } + + $inFileRelations = []; + $inFileDeprecations = []; + + foreach ($selected["ponies"] as $ponies) { + $inFileRelations[] = $ponies["members"][0] . "-" . $ponies["members"][1]; + + if (isset($ponies["deprecated"]) && $ponies["deprecated"]) $inFileDeprecations[] = $ponies["members"][0] . "-" . $ponies["members"][1]; + } + + foreach ($relations as $id => $relation): + ?> + <label style="display:block;" class="creator-relation <?= $selected["type"] === "mixed" ? "mixed" : "" ?> <?= in_array($id, $inFileRelations) ? "checked" : "" ?>"> + <input <?= in_array($id, $inFileRelations) ? "checked" : "" ?> name="relations[<?= $id ?>][member]" style="display:none;" type="checkbox" class="checkbox-input"> + + <img style="vertical-align: middle;width:24px;height:24px;" src="<?= $relation["images"][0] ?>"> + <img style="vertical-align: middle;width:24px;height:24px;" src="<?= $relation["images"][1] ?>"> + <span style="vertical-align: middle;"><?= $relation["name"] ?></span> + + <label class="deprecated" style="display:none;margin-left: 20px;margin-top: 5px;"> + <input <?= in_array($id, $inFileDeprecations) ? "checked" : "" ?> name="relations[<?= $id ?>][deprecated]" type="checkbox"> + Mark as deprecated + </label> + </label> + <?php endforeach; ?> + + <br> + <input type="hidden" name="updateAction"> + <input type="hidden" name="action" value="<?= $selected["id"] ?>"> + <input type="submit" class="btn btn-primary" value="Save"> + </form> + <form method="post" style="display:inline;"> + <input type="hidden" name="deleteAction"> + <input type="hidden" name="action" value="<?= $selected["id"] ?>"> + <input type="submit" class="btn btn-danger" value="Delete"> + </form> + </div> + </div> + </div> + </div> + + <?php else: ?> + + <h2>Toys database</h2> + <p><?= count($data) ?> toys (<?= count(array_filter($data, function ($i) { + return $i["sexual"] === false; + })) ?> non-sexual, <?= count(array_filter($data, function ($i) { + return $i["sexual"] === true; + })) ?> sexual, <?= count(array_filter($data, function ($i) { + return isset($i["untested"]) && $i["untested"]; + })) ?> untested, <?= count(array_filter($data, function ($i) { + return (isset($i["description"]) && trim($i["description"]) === "") || !isset($i["description"]); + })) ?> incomplete)</p> + + <input type="text" placeholder="Search for a toy..." class="form-control" style="margin-bottom:15px;color:white;background:#111;border-color:#222;" onchange="search();" onkeydown="search();" onkeyup="search();" id="search"> + + <div id="list"> + <div class="list-group"> + <?php + + $init = []; + foreach ($data as $value) { + $init[$value["name"]] = $value; + } + + ksort($init); + + $sorted = array_values($init); + uasort($sorted, function ($a, $b) { + $uniquePonies1 = []; + + foreach ($a["ponies"] as $ponies) { + foreach ($ponies["members"] as $member) { + $uniquePonies1[$member] = true; + } + } + + $uniquePonies2 = []; + + foreach ($b["ponies"] as $ponies) { + foreach ($ponies["members"] as $member) { + $uniquePonies2[$member] = true; + } + } + + return count($uniquePonies2) - count($uniquePonies1); + }); + + foreach ($sorted as $item): ?> + <a href="/-/toys/<?= $item["id"] ?>" id="action-<?= $item["id"] ?>" style="display:grid;grid-template-columns: 1fr 1fr 0.2fr;" class="list-group-item list-group-item-action action-listing"> + <div> + <span class="<?= trim($item["description"]) === "" ? "text-danger" : "" ?>"><?= $item["name"] ?></span> + <?php if (!isset($item["verified"])): ?><span class="badge bg-warning rounded-pill text-black">Unverified</span><?php endif; ?> + <?php if (isset($item["untested"])): ?><span class="badge bg-info rounded-pill">Untested</span><?php endif; ?> + </div> + <div> + <?php + + $uniquePonies = []; + $longPonyList = false; + + foreach ($item["ponies"] as $ponies) { + foreach ($ponies["members"] as $member) { + if (isset($uniquePonies[$member]) && !$uniquePonies[$member]) { + $uniquePonies[$member] = false; + } else { + $uniquePonies[$member] = $ponies["deprecated"]; + } + } + } + + if (count($uniquePonies) > 6) { + $longPonyList = true; + } + + $index = 1; + foreach ($uniquePonies as $name => $deprecated): if (!$deprecated): ?> + <!-- <?= $index ?> --> + <span title="<?= getMemberWithoutSystem($name)["display_name"] ?? getMemberWithoutSystem($name)["display_name"] ?>" style="display: inline-block;<?= $longPonyList && $index % 2 === 0 ? "position:absolute;margin-left:-12px;z-index:" . (999 - $index) . ";" : "position:relative;z-index:" . (999 - $index) . ";" ?>"> + <img src="/assets/uploads/pt-<?= getMemberWithoutSystem($name)["name"] ?>.png" style="width:24px;"> + </span> + <?php $index++; endif; endforeach; ?> + <?php foreach ($uniquePonies as $name => $deprecated): if ($deprecated): ?> + <span title="<?= strip_tags(getMemberWithoutSystem($name)["display_name"] ?? getMemberWithoutSystem($name)["name"]) ?> (deprecated)" style="opacity:.5;display: inline-block;<?= $longPonyList && $index % 2 === 0 ? "position:absolute;margin-left:-12px;z-index:" . (999 - $index) . ";" : "position:relative;z-index:" . (999 - $index) . ";" ?>"> + <img src="/assets/uploads/pt-<?= getMemberWithoutSystem($name)["name"] ?>.png" style="width:24px;"> + </span> + <?php $index++; endif; endforeach; ?> + </div> + <div style="text-align: right;"> + <?php if ($item["sexual"]): ?> + <span style="" class="badge rounded-pill bg-danger">Sexual</span> + <?php else: ?> + <span style="" class="badge rounded-pill bg-success">Pleasurable</span> + <?php endif; ?> + </div> + </a> + <?php endforeach; ?> + </div> + </div> + + <div id="search-results" class="list-group"></div> + + <div id="page-content"> + <hr> + <p>Not finding what you are looking for? <a onclick="event.target.blur(); document.getElementById('creator-title').focus();" href="#" data-bs-toggle="modal" data-bs-target="#creator">Add a toy.</a></p> + </div> + + <script> + window.actions = JSON.parse(atob(`<?= base64_encode(json_encode(array_values(json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/toys.json"), true)))) ?>`)); + </script> + + <div class="modal fade" id="creator"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h4 class="modal-title">Add a new toy</h4> + <button type="button" class="btn-close" data-bs-dismiss="modal"></button> + </div> + + <div class="modal-body"> + <form method="post"> + <input id="creator-title" type="text" placeholder="Toy name" name="name" class="form-control" style="color:white;background:#111;border-color:#222;margin-bottom:10px;"> + <select name="water" class="form-select" style='color:white;background-color:#111;border-color:#222;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23ffffff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");margin-bottom:10px;'> + <option value="0" selected>Usable outside of the water</option> + <option value="1">Usable inside of the water</option> + <option value="2">Usable both inside and outside</option> + <option value="3">Usable in a playground</option> + </select> + + <label style="margin-left:5px;"> + <input type="checkbox" name="sexual"> + Sexual toy + </label> + + <hr> + + <p>Select the groups of ponies who can use this toy:</p> + <?php + + $relations = []; + $actions = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/actions.json"), true); + + foreach ($actions as $action) { + if ($action["type"] !== "sexual") continue; + + foreach ($action["ponies"] as $ponies) { + $id = implode("", $ponies["members"]); + + $member = getMemberWithoutSystem($ponies["members"][0]); + $otherMember = getMemberWithoutSystem($ponies["members"][1]); + + $parts = [ + $member["id"], + $otherMember["id"] + ]; + + asort($parts); + + $relations[implode("-", $parts)] = [ + "name" => getMiniName($member["display_name"] ?? $member["name"]) . " and " . getMiniName($otherMember["display_name"] ?? $otherMember["name"]), + "images" => [ + file_exists($_SERVER['DOCUMENT_ROOT'] . "/assets/uploads/pt-$member[name].png") ? "/assets/uploads/pt-$member[name].png" : "/assets/uploads/pt.png", + file_exists($_SERVER['DOCUMENT_ROOT'] . "/assets/uploads/pt-$otherMember[name].png") ? "/assets/uploads/pt-$otherMember[name].png" : "/assets/uploads/pt.png", + ] + ]; + } + } + + foreach ($relations as $id => $relation): + ?> + <label style="display:block;" class="creator-relation"> + <input name="relations[<?= $id ?>]" style="display:none;" type="checkbox" class="checkbox-input"> + + <img style="vertical-align: middle;width:24px;height:24px;" src="<?= $relation["images"][0] ?>"> + <img style="vertical-align: middle;width:24px;height:24px;" src="<?= $relation["images"][1] ?>"> + <span style="vertical-align: middle;"><?= $relation["name"] ?></span> + </label> + <?php endforeach; ?> + + <p style="margin-top:10px;">You can add additional data (description, how to use) after adding the toy.</p> + <input type="hidden" name="createAction"> + <input type="submit" class="btn btn-primary" value="Add"> + </form> + </div> + </div> + </div> + </div> + + <?php endif; ?> + </div> +</div> + +<!--suppress JSUnresolvedFunction --> +<script> + Array.from(document.getElementsByClassName("checkbox-input")).forEach((el) => { + el.onchange = () => { + let parent = el.parentElement; + + if (el.checked) { + parent.classList.add("checked"); + } else { + parent.classList.remove("checked"); + } + } + }); + + const fuse = new Fuse(window.actions, { + includeScore: true, + keys: [ + { + name: 'name', + weight: 1 + }, + { + name: 'description', + weight: 1 + }, + { + name: 'example', + weight: 0.7 + }, + { + name: 'irl', + weight: 0.5 + } + ] + }) + + function search() { + let query = document.getElementById("search").value; + let results = fuse.search(query).map((i) => { + return { + id: i.item.id, + score: i.score + }; + }); + + let unfiltered = results; + + results = results.filter((i) => { + return i.score < 0.7; + }); + + console.log("Before:", unfiltered, "After:", results); + + document.getElementById("list").style.display = "none"; + document.getElementById("search-results").style.display = "block"; + document.getElementById("search-results").innerHTML = ""; + + for (let result of results) { + document.getElementById("search-results").innerHTML += document.getElementById("action-" + result.id).outerHTML; + } + + console.log(results); + + if (query.trim() === "") { + document.getElementById("list").style.display = "block"; + document.getElementById("search-results").style.display = "none"; + } + } + + function changeMixed() { + let value = document.getElementById("editor-type").value; + console.log(value); + + if (value === "2") { + Array.from(document.getElementsByClassName("creator-relation")).forEach((el) => { + el.classList.add("mixed"); + }); + } else { + Array.from(document.getElementsByClassName("creator-relation")).forEach((el) => { + el.classList.remove("mixed"); + }); + } + } +</script> + +<style> + .modal-header { + border-bottom: 1px solid #353738; + } + + .modal-content { + border: 1px solid rgba(255, 255, 255, .2); + background-color: #111; + } + + .btn-close { + filter: invert(1); + } + + .creator-relation { + border-radius: 10px; + padding: 5px 10px; + opacity: .5; + } + + .creator-relation.checked { + background-color: rgba(255, 255, 255, .1); + opacity: 1; + } + + .creator-relation:hover { + background-color: rgba(255, 255, 255, .1); + } + + .creator-relation.checked:hover { + background-color: rgba(255, 255, 255, .25) !important; + } + + .creator-relation.checked .deprecated { + display: block !important; + } + + .creator-relation.mixed.checked .sexual { + display: block !important; + } + + .list-group-item { + color: #fff; + background-color: #222; + border: 1px solid rgba(255, 255, 255, .125); + } + + .list-group-item.disabled { + color: #fff; + background-color: #222; + border-color: rgba(255, 255, 255, .125); + opacity: .75; + } + + .list-group-item:hover { + background-color: #252525; + color: #ddd; + } + + .list-group-item:active, .list-group-item:focus { + background-color: #272727; + color: #bbb; + } + + @media (max-width: 991px) { + .action-listing { + grid-template-columns: 1fr !important; + text-align: center !important; + } + + .action-listing > * { + margin-bottom: 10px; + text-align: center !important; + } + + .action-listing > *:nth-last-child(1) { + margin-bottom: 0 !important; + } + + .action-listing img { + width: 32px !important; + } + } +</style> + +<?php require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/footer.php'; ?> diff --git a/pages/travelling.php b/pages/travelling.php index affe92e..c40ebab 100644 --- a/pages/travelling.php +++ b/pages/travelling.php @@ -1,7 +1,7 @@ <?php require_once $_SERVER['DOCUMENT_ROOT'] . "/includes/session.php"; global $isLoggedIn; -if (!$isLoggedIn) header("Location: /login") and die(); +if (!$isLoggedIn) header("Location: /-/login") and die(); $travelling = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/data/travelling.json"), true); $app = json_decode(file_get_contents($_SERVER['DOCUMENT_ROOT'] . "/includes/app.json"), true); @@ -116,7 +116,7 @@ global $travelling; <br> <div class="container"> <div> - <h2>System Travelling</h2> + <h2>System travelling</h2> <?php foreach (scoreOrderGlobal() as $member): ?> <div class="relation" style="background-color:rgba(255, 255, 255, .1);margin-bottom:10px;padding:10px;border-radius:10px;display:grid;grid-template-columns: 1fr 2fr max-content;"> <a class="relation-intro" style="background-color:rgba(255, 255, 255, .05);border-right:1px solid rgba(255, 255, 255, .1);margin:-10px;padding:10px;border-top-left-radius:10px;border-bottom-left-radius:10px;color: white;display:flex;align-items:center;text-decoration: none;" href="/<?= $member["name"] ?>"> |