Initial import.
1
.gitattributes
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
* text=auto
|
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
/.debug
|
||||
/public/ss
|
||||
/uploads
|
||||
/config.php
|
46
cron.php
Normal file
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
namespace YTKNS;
|
||||
|
||||
if(!defined('YTKNS_SEM_NAME'))
|
||||
define('YTKNS_SEM_NAME', 'b');
|
||||
if(!defined('YTKNS_SFM_PATH'))
|
||||
define('YTKNS_SFM_PATH', sys_get_temp_dir() . DIRECTORY_SEPARATOR . '{04884071-d334-4388-947a-4fd8c4d4f4ea}');
|
||||
|
||||
if(!is_file(YTKNS_SFM_PATH))
|
||||
touch(YTKNS_SFM_PATH);
|
||||
|
||||
$ftok = ftok(YTKNS_SFM_PATH, YTKNS_SEM_NAME);
|
||||
$semaphore = sem_get($ftok, 1);
|
||||
if(!sem_acquire($semaphore))
|
||||
die('Failed to acquire semaphore.' . PHP_EOL);
|
||||
|
||||
require_once __DIR__ . '/startup.php';
|
||||
|
||||
// Destroy old sessions
|
||||
UserSession::purge();
|
||||
|
||||
// Resynchronise use counts
|
||||
Upload::resync(EFFECT_UPLOADS);
|
||||
|
||||
// Destroy orphaned uploads
|
||||
Upload::purgeOrphans();
|
||||
|
||||
// Get task queue
|
||||
$taskQueue = ZoneTask::queue();
|
||||
|
||||
// Plow through tasks
|
||||
// TODO: make task functions modular
|
||||
while($task = array_shift($taskQueue)) {
|
||||
if(!isset($zoneInfo) || $zoneInfo->getId() !== $task->getZoneId())
|
||||
$zoneInfo = $task->getZone();
|
||||
|
||||
switch($task->getName()) {
|
||||
case 'screenshot':
|
||||
$zoneInfo->takeScreenshot();
|
||||
break;
|
||||
}
|
||||
|
||||
$task->delete();
|
||||
}
|
||||
|
||||
sem_release($semaphore);
|
BIN
public/assets/accept.png
Normal file
After Width: | Height: | Size: 781 B |
BIN
public/assets/add.png
Normal file
After Width: | Height: | Size: 733 B |
BIN
public/assets/arrow_down.png
Normal file
After Width: | Height: | Size: 379 B |
BIN
public/assets/arrow_refresh.png
Normal file
After Width: | Height: | Size: 685 B |
BIN
public/assets/arrow_rotate_clockwise.png
Normal file
After Width: | Height: | Size: 602 B |
BIN
public/assets/arrow_up.png
Normal file
After Width: | Height: | Size: 372 B |
BIN
public/assets/bin_closed.png
Normal file
After Width: | Height: | Size: 363 B |
BIN
public/assets/bomb.png
Normal file
After Width: | Height: | Size: 793 B |
BIN
public/assets/cancel.png
Normal file
After Width: | Height: | Size: 587 B |
BIN
public/assets/delete.png
Normal file
After Width: | Height: | Size: 715 B |
BIN
public/assets/drive_add.png
Normal file
After Width: | Height: | Size: 623 B |
554
public/assets/editor.css
Normal file
|
@ -0,0 +1,554 @@
|
|||
.ye {
|
||||
display: flex;
|
||||
min-height: 500px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
margin: -1px;
|
||||
}
|
||||
|
||||
.ye-sidebar {
|
||||
flex: 0 0 auto;
|
||||
min-width: 250px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ye-sidebar-buttons {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
background-color: #eee;
|
||||
border-bottom: 1px solid #ddd;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
height: 28px;
|
||||
}
|
||||
.ye-sidebar-buttons-separator {
|
||||
width: 1px;
|
||||
height: 22px;
|
||||
margin: 1px;
|
||||
background-color: #ddd;
|
||||
}
|
||||
.ye-sidebar-buttons-button {
|
||||
margin: 1px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
cursor: pointer;
|
||||
background-size: 16px 16px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
background-image: url('page_white.png');
|
||||
}
|
||||
.ye-sidebar-buttons-button:hover,
|
||||
.ye-sidebar-buttons-button:focus {
|
||||
border: 1px solid #7ca1cd;
|
||||
background-color: #deecfd;
|
||||
}
|
||||
.ye-sidebar-buttons-button:active {
|
||||
background-color: #f1f7fe;
|
||||
}
|
||||
.ye-sidebar-buttons-button--save { background-image: url('accept.png'); }
|
||||
.ye-sidebar-buttons-button--cancel { background-image: url('cancel.png'); }
|
||||
.ye-sidebar-buttons-button--add { background-image: url('add.png'); }
|
||||
.ye-sidebar-buttons-button--preview { background-image: url('page_white_magnify.png'); }
|
||||
.ye-sidebar-buttons-button--edit { background-image: url('page_white_edit.png'); }
|
||||
.ye-sidebar-buttons-button--live { background-image: url('page_white_link.png'); }
|
||||
.ye-sidebar-buttons-button--reset { background-image: url('arrow_refresh.png'); }
|
||||
|
||||
.ye-sidebar-effects {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.ye-sidebar-effects-empty {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.ye-sidebar-effects-effect {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding: 5px;
|
||||
}
|
||||
.ye-sidebar-effects-effect-name {
|
||||
padding: 0 5px;
|
||||
}
|
||||
.ye-sidebar-effects-effect-actions {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
.ye-sidebar-effects-effect-actions-action {
|
||||
margin: 1px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
background-size: 16px 16px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
background-image: url('page_white.png');
|
||||
}
|
||||
.ye-sidebar-effects-effect-actions-action--edit { background-image: url('pencil.png'); }
|
||||
.ye-sidebar-effects-effect-actions-action--delete { background-image: url('delete.png'); }
|
||||
|
||||
.ye-main {
|
||||
flex: 1 1 auto;
|
||||
border-left: 1px solid #ddd;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ye-main-title {
|
||||
flex: 0 0 auto;
|
||||
border-left: 1px solid #ddd;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
background-color: #eee;
|
||||
border-bottom: 1px solid #ddd;
|
||||
height: 28px;
|
||||
font-size: 1.2em;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
.ye-main-container {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.ye-main-welcome {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ye-main-effect-delete-confirm {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.ye-main-effect-delete-confirm-text {
|
||||
font-size: 1.2em;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
.ye-main-effect-delete-confirm-actions {
|
||||
display: flex;
|
||||
margin: 5px;
|
||||
}
|
||||
.ye-main-effect-delete-confirm-actions-action {
|
||||
cursor: pointer;
|
||||
margin: 0 5px;
|
||||
padding: 5px 10px;
|
||||
font-size: 1.1em;
|
||||
border-radius: 5px;
|
||||
min-width: 100px;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
transition: background-color .1s;
|
||||
}
|
||||
.ye-main-effect-delete-confirm-actions-action--accept { background-color: #28a745; }
|
||||
.ye-main-effect-delete-confirm-actions-action--accept:hover,
|
||||
.ye-main-effect-delete-confirm-actions-action--accept:focus { background-color: #218838; }
|
||||
.ye-main-effect-delete-confirm-actions-action--accept:active { background-color: #1e7e34; }
|
||||
.ye-main-effect-delete-confirm-actions-action--deny { background-color: #dc3545; }
|
||||
.ye-main-effect-delete-confirm-actions-action--deny:hover,
|
||||
.ye-main-effect-delete-confirm-actions-action--deny:focus { background-color: #c82333; }
|
||||
.ye-main-effect-delete-confirm-actions-action--deny:active { background-color: #bc2130; }
|
||||
|
||||
.ye-main-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.ye-main-details-fields {
|
||||
width: 100%;
|
||||
}
|
||||
.ye-main-details-fields-field {
|
||||
display: flex;
|
||||
margin: 5px 0;
|
||||
}
|
||||
.ye-main-details-fields-field-name {
|
||||
min-width: 100px;
|
||||
}
|
||||
.ye-main-details-fields-field-wrap {
|
||||
border: 1px solid #000;
|
||||
min-width: 300px;
|
||||
height: 22px;
|
||||
display: flex;
|
||||
padding: 0 2px;
|
||||
background-color: #eee;
|
||||
color: #444;
|
||||
}
|
||||
.ye-main-details-fields-field-input {
|
||||
border-width: 0;
|
||||
flex-grow: 1;
|
||||
font: 12px/20px Tahoma, Geneva, 'Dejavu Sans', Arial, Helvetica, sans-serif;
|
||||
background-color: #eee;
|
||||
color: #000;
|
||||
}
|
||||
.ye-main-details-fields-field--readonly .ye-main-details-fields-field-wrap,
|
||||
.ye-main-details-fields-field--readonly .ye-main-details-fields-field-input {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.ye-applet-effects-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.ye-applet-effects-list-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 5px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
.ye-applet-effects-list-item--used {
|
||||
opacity: .5;
|
||||
}
|
||||
.ye-applet-effects-list-item-name {
|
||||
padding: 1px 5px;
|
||||
}
|
||||
.ye-applet-effects-list-item-actions {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
.ye-applet-effects-list-item-actions-action {
|
||||
margin: 1px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
background-size: 16px 16px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
background-image: url('page_white.png');
|
||||
}
|
||||
.ye-applet-effects-list-item-actions-action--add { background-image: url('add.png'); }
|
||||
|
||||
.ye-applet-uploads {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.ye-applet-uploads-progress {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
}
|
||||
.ye-applet-uploads-progress-bar {
|
||||
height: 24px;
|
||||
width: 100%;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.ye-applet-uploads-cancel {
|
||||
margin: 5px;
|
||||
margin-top: 0;
|
||||
font-size: 1.2em;
|
||||
padding: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.ye-applet-uploads-dropzone {
|
||||
flex: 1 1 auto;
|
||||
min-height: 200px;
|
||||
padding: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.ye-applet-uploads-dropzone-inner {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #ccc;
|
||||
background-image: radial-gradient(circle at center, #eee, #bbb);
|
||||
border: 5px dashed #888;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 1.5em;
|
||||
line-height: 1.5em;
|
||||
transition: border-color .2s;
|
||||
}
|
||||
.ye-applet-uploads-dropzone--active .ye-applet-uploads-dropzone-inner {
|
||||
border-color: #444;
|
||||
}
|
||||
.ye-applet-uploads-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ye-applet-editor {
|
||||
}
|
||||
.ye-applet-editor--fill {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.ye-applet-editor-empty {
|
||||
font-size: 1.2em;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
.ye-applet-editor-properties {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.ye-applet-editor-properties-property {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
.ye-applet-editor-properties-property-title {
|
||||
font-size: 1.1em;
|
||||
line-height: 1.5em;
|
||||
padding: 2px 5px;
|
||||
}
|
||||
.ye-applet-editor-properties-property-wrap {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.ye-applet-editor-properties-property-none {
|
||||
font-size: .9em;
|
||||
padding: 2px 10px;
|
||||
}
|
||||
.ye-applet-editor-properties-property-reset {
|
||||
flex: 0 0 auto;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin: 4px;
|
||||
background-image: url('bomb.png');
|
||||
background-size: 16px 16px;
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
cursor: pointer;
|
||||
}
|
||||
.ye-applet-editor-properties-property-bool {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
.ye-applet-editor-properties-property-bool-toggle {
|
||||
margin: 7px;
|
||||
}
|
||||
.ye-applet-editor-properties-property-int {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
.ye-applet-editor-properties-property-int-input {
|
||||
margin: 2px 5px;
|
||||
padding: 2px;
|
||||
border: 1px solid #aaa;
|
||||
}
|
||||
.ye-applet-editor-properties-property-int-input:focus {
|
||||
border-color: #f78f2e;
|
||||
}
|
||||
.ye-applet-editor-properties-property-upload {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.ye-applet-editor-properties-property-upload-id {
|
||||
font-size: .9em;
|
||||
padding: 2px 10px;
|
||||
}
|
||||
.ye-applet-editor-properties-property-upload-select {
|
||||
flex: 0 0 auto;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin: 4px;
|
||||
background-image: url('drive_add.png');
|
||||
background-size: 16px 16px;
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
cursor: pointer;
|
||||
}
|
||||
.ye-applet-editor-properties-property-select {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
.ye-applet-editor-properties-property-select-input {
|
||||
margin: 5px;
|
||||
padding: 2px;
|
||||
border: 1px solid #aaa;
|
||||
}
|
||||
.ye-applet-editor-properties-property-select-input:focus {
|
||||
border-color: #f78f2e;
|
||||
}
|
||||
.ye-applet-editor-properties-property-colour {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
.ye-applet-editor-properties-property-colour-input {
|
||||
margin: 5px;
|
||||
padding: 2px;
|
||||
border: 1px solid #aaa;
|
||||
}
|
||||
.ye-applet-editor-properties-property-colour-input:focus {
|
||||
border-color: #f78f2e;
|
||||
}
|
||||
.ye-applet-editor-properties-property-string {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
.ye-applet-editor-properties-property-string-input {
|
||||
margin: 5px;
|
||||
padding: 2px;
|
||||
border: 1px solid #aaa;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.ye-applet-editor-properties-property-string-input:focus {
|
||||
border-color: #f78f2e;
|
||||
}
|
||||
.ye-applet-editor-properties-property-gradient {
|
||||
flex-grow: 1;
|
||||
}
|
||||
.ye-applet-editor-properties-property-gradient-preview {
|
||||
margin: 0 5px;
|
||||
height: 50px;
|
||||
border: 1px solid #aaa;
|
||||
}
|
||||
.ye-applet-editor-properties-property-gradient-direction {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.ye-applet-editor-properties-property-gradient-direction-circle {
|
||||
margin: 6px;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 100%;
|
||||
border: 1px solid #8c8c8c;
|
||||
padding: 1px;
|
||||
background: #fefefe linear-gradient(0deg, #fdfdfd, #d9d9d9);
|
||||
box-shadow: inset 0 0 0 1px #fff;
|
||||
}
|
||||
.ye-applet-editor-properties-property-gradient-direction-circle:active {
|
||||
border-color: #f78f2e;
|
||||
}
|
||||
.ye-applet-editor-properties-property-gradient-direction-circle-value {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
pointer-events: none;
|
||||
}
|
||||
.ye-applet-editor-properties-property-gradient-direction-circle-indicator {
|
||||
background: #999;
|
||||
width: 2px;
|
||||
height: 50%;
|
||||
border-radius: 1px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.ye-applet-editor-properties-property-gradient-direction-input {
|
||||
margin: 5px;
|
||||
margin-left: 0;
|
||||
padding: 2px;
|
||||
border: 1px solid #aaa;
|
||||
width: 60px;
|
||||
}
|
||||
.ye-applet-editor-properties-property-gradient-direction-input:focus {
|
||||
border-color: #f78f2e;
|
||||
}
|
||||
.ye-applet-editor-properties-property-gradient-points-add {
|
||||
margin: 1px;
|
||||
margin-left: auto;
|
||||
border: 1px solid #707070;
|
||||
border-radius: 2px;
|
||||
background: linear-gradient(180deg, #fcfcfc 0%, #ebebeb 50%, #dbdbdb 50%) #ebebeb;
|
||||
cursor: pointer;
|
||||
padding: 0 5px;
|
||||
font: 12px/20px Tahoma, Geneva, 'Dejavu Sans', Arial, Helvetica, sans-serif;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
transition: border-color .2s;
|
||||
}
|
||||
.ye-applet-editor-properties-property-gradient-points-add:hover,
|
||||
.ye-applet-editor-properties-property-gradient-points-add:focus {
|
||||
border-color: #f78f2e;
|
||||
}
|
||||
.ye-applet-editor-properties-property-gradient-points-add:active {
|
||||
background: linear-gradient(0deg, #fcfcfc 0%, #ebebeb 50%, #dbdbdb 50%) #ebebeb;
|
||||
}
|
||||
.ye-applet-editor-properties-property-gradient-points-add-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-image: url('add.png');
|
||||
background-size: 16px 16px;
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.ye-applet-editor-properties-property-gradient-points {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 400px;
|
||||
overflow: auto;
|
||||
}
|
||||
.ye-applet-editor-properties-property-gradient-points-point {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.ye-applet-editor-properties-property-gradient-points-point-actions {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
.ye-applet-editor-properties-property-gradient-points-point-actions-action {
|
||||
margin: 1px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
background-size: 16px 16px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
background-image: url('page_white.png');
|
||||
}
|
||||
.ye-applet-editor-properties-property-gradient-points-point-actions-action--delete { background-image: url('delete.png'); }
|
||||
.ye-applet-editor-properties-property-gradient-points-point-colour {
|
||||
margin: 2px 5px;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
width: 41px;
|
||||
height: 21px;
|
||||
border: 1px solid #aaa;
|
||||
box-shadow: inset 0 0 0 1px #fff;
|
||||
}
|
||||
.ye-applet-editor-properties-property-gradient-points-point-colour:active,
|
||||
.ye-applet-editor-properties-property-gradient-points-point-colour:focus-within {
|
||||
border-color: #f78f2e;
|
||||
}
|
||||
.ye-applet-editor-properties-property-gradient-points-point-colour-value {
|
||||
position: relative;
|
||||
top: -500px;
|
||||
}
|
||||
.ye-applet-editor-properties-property-gradient-points-point-offset-range {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.ye-applet-editor-properties-property-gradient-points-point-offset-numeric {
|
||||
margin: 5px;
|
||||
padding: 2px;
|
||||
border: 1px solid #aaa;
|
||||
width: 60px;
|
||||
}
|
||||
.ye-applet-editor-properties-property-gradient-points-point-offset-numeric:focus {
|
||||
border-color: #f78f2e;
|
||||
}
|
1081
public/assets/editor.js
Normal file
BIN
public/assets/link.png
Normal file
After Width: | Height: | Size: 343 B |
BIN
public/assets/no-screenshot.jpg
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
public/assets/page_white.png
Normal file
After Width: | Height: | Size: 294 B |
BIN
public/assets/page_white_edit.png
Normal file
After Width: | Height: | Size: 618 B |
BIN
public/assets/page_white_link.png
Normal file
After Width: | Height: | Size: 614 B |
BIN
public/assets/page_white_magnify.png
Normal file
After Width: | Height: | Size: 554 B |
BIN
public/assets/pencil.png
Normal file
After Width: | Height: | Size: 450 B |
68
public/assets/shared.css
Normal file
|
@ -0,0 +1,68 @@
|
|||
* {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
outline-style: none !important;
|
||||
}
|
||||
|
||||
@keyframes SharedAnimation_Spin360 {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#container {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.NewCreatePageEffect_Main {
|
||||
font-family: sans-serif;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ZoomText {
|
||||
font-family: sans-serif;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 810px;
|
||||
z-index: 10;
|
||||
text-align: center;
|
||||
}
|
||||
.ZoomText_Child {
|
||||
position: absolute;
|
||||
width: 810px;
|
||||
}
|
||||
|
||||
.BackgroundImage {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
}
|
||||
.BackgroundImage.Cover {
|
||||
width: 10000px;
|
||||
height: 10000px;
|
||||
top: -5000px;
|
||||
left: -5000px;
|
||||
}
|
||||
|
||||
.ForegroundImage {
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
64
public/assets/shared.js
Normal file
|
@ -0,0 +1,64 @@
|
|||
var _paq = window._paq || [];
|
||||
_paq.push(['disableCookies']);
|
||||
_paq.push(['trackPageView']);
|
||||
_paq.push(['enableLinkTracking']);
|
||||
_paq.push(['enableHeartBeatTimer']);
|
||||
(function() {
|
||||
_paq.push(['setTrackerUrl', '//uiharu.railgun.sh/mtm']);
|
||||
_paq.push(['setSiteId', 'zlwmGOjMBk5J']);
|
||||
var g = document.createElement('script');
|
||||
g.type = 'text/javascript'; g.async = true;
|
||||
g.defer = true; g.src = '//uiharu.railgun.sh/mtm.js';
|
||||
document.head.appendChild(g);
|
||||
})();
|
||||
|
||||
function synchroniseBackgroundWithAudio(bgi, target) {
|
||||
var cnt = document.getElementById(target || 'BackgroundImage') || document.getElementById('container'),
|
||||
bga = document.getElementById('BackgroundAudio'),
|
||||
rdy = false,
|
||||
bgs = [];
|
||||
|
||||
if(!cnt)
|
||||
return;
|
||||
|
||||
var computed = getComputedStyle(cnt);
|
||||
|
||||
if(computed.backgroundImage && target !== 'ForegroundImage')
|
||||
bgs = computed.backgroundImage.split(',');
|
||||
|
||||
if(!bga) {
|
||||
bgs.splice(0, 0, 'url(' + bgi + ')');
|
||||
cnt.style.backgroundImage = bgs.join(',');
|
||||
return;
|
||||
}
|
||||
|
||||
var onReady = function(url) {
|
||||
bgs.splice(0, 0, 'url(' + url + ')');
|
||||
cnt.style.backgroundImage = bgs.join(',');
|
||||
bga.play();
|
||||
};
|
||||
|
||||
bga.addEventListener('canplay', function() {
|
||||
rdy = true;
|
||||
});
|
||||
|
||||
bga.pause();
|
||||
bga.currentTime = 0;
|
||||
|
||||
var xhr = new XMLHttpRequest;
|
||||
xhr.responseType = 'blob';
|
||||
xhr.onreadystatechange = function() {
|
||||
if(xhr.readyState !== 4)
|
||||
return;
|
||||
bgi = URL.createObjectURL(xhr.response);
|
||||
|
||||
if(rdy)
|
||||
onReady(bgi);
|
||||
else
|
||||
bga.addEventListener('canplay', function() {
|
||||
onReady(bgi);
|
||||
});
|
||||
};
|
||||
xhr.open('GET', bgi);
|
||||
xhr.send();
|
||||
}
|
BIN
public/assets/spinner.gif
Normal file
After Width: | Height: | Size: 3.1 KiB |
574
public/assets/style.css
Normal file
|
@ -0,0 +1,574 @@
|
|||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
outline-style: none !important;
|
||||
}
|
||||
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font: 12px/20px Tahoma, Geneva, 'Dejavu Sans', Arial, Helvetica, sans-serif;
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
|
||||
.hidden,
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
visibility: hidden !important;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #922;
|
||||
color: #fff;
|
||||
font-size: 1.2em;
|
||||
line-height: 1.5em;
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
background-image: linear-gradient(0deg, #922, #f22);
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, .5);
|
||||
box-shadow: 0 1px 2px #000;
|
||||
}
|
||||
|
||||
.noscript {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 1px 4px #000;
|
||||
}
|
||||
|
||||
@media(min-width: 1000px) {
|
||||
body { padding-top: 1px; }
|
||||
.wrapper { margin: 3px auto 10px; }
|
||||
}
|
||||
|
||||
.footer {
|
||||
font-size: 10px;
|
||||
padding: 0 5px;
|
||||
text-align: right;
|
||||
color: #888;
|
||||
}
|
||||
.footer a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
.footer a:hover,
|
||||
.footer a:focus,
|
||||
.footer a:active {
|
||||
color: #222;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.header {}
|
||||
.header-title {
|
||||
padding-top: 10%;
|
||||
background: url('ytkns.png') center / cover;
|
||||
display: block;
|
||||
}
|
||||
.header-usermenu {
|
||||
border: 1px solid #000;
|
||||
background: linear-gradient(180deg, #545454 0%, #1a1a1a 50%, #000 50%) #545454;
|
||||
}
|
||||
.header-navigation {
|
||||
border: 1px solid #000;
|
||||
background: linear-gradient(180deg, #5a5a5a 0%, #272727 50%, #111 50%) #5a5a5a;
|
||||
font-size: 1.4em;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
.header-navigation-item,
|
||||
.header-usermenu-item {
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
padding: 2px 10px;
|
||||
display: inline-block;
|
||||
transition: background-color .1s;
|
||||
}
|
||||
.header-navigation-item:hover,
|
||||
.header-navigation-item:active,
|
||||
.header-usermenu-item:hover,
|
||||
.header-usermenu-item:active {
|
||||
background-color: rgba(255, 255, 255, .1);
|
||||
}
|
||||
.header-navigation-item:active,
|
||||
.header-usermenu-item:active {
|
||||
background-color: rgba(0, 0, 0, .5);
|
||||
}
|
||||
|
||||
.content {
|
||||
display: block;
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
.information {
|
||||
margin: 5px;
|
||||
display: block;
|
||||
border: 1px solid #000;
|
||||
padding: 2px 5px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.information-title {
|
||||
border: 1px solid #000;
|
||||
border-radius: 10px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
padding: 8px 5px;
|
||||
}
|
||||
.page-subtitle {
|
||||
padding: 2px 5px;
|
||||
}
|
||||
.page-paragraph {
|
||||
margin: 1px 5px;
|
||||
}
|
||||
|
||||
.zone-creation-form {
|
||||
margin: 10px auto;
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
}
|
||||
.zone-creation-input {
|
||||
display: flex;
|
||||
margin: 5px 0;
|
||||
}
|
||||
.zone-creation-label {
|
||||
min-width: 100px;
|
||||
}
|
||||
.zone-creation-wrap {
|
||||
border: 1px solid #000;
|
||||
min-width: 300px;
|
||||
height: 22px;
|
||||
display: flex;
|
||||
padding: 0 2px;
|
||||
background-color: #eee;
|
||||
color: #444;
|
||||
}
|
||||
.zone-creation-value {
|
||||
border-width: 0;
|
||||
flex-grow: 1;
|
||||
font: 12px/20px Tahoma, Geneva, 'Dejavu Sans', Arial, Helvetica, sans-serif;
|
||||
background-color: #eee;
|
||||
color: #000;
|
||||
}
|
||||
.zone-creation-actions {
|
||||
text-align: center;
|
||||
}
|
||||
.zone-creation-action {
|
||||
border: 1px solid #707070;
|
||||
border-radius: 2px;
|
||||
background: linear-gradient(180deg, #fcfcfc 0%, #ebebeb 50%, #dbdbdb 50%) #ebebeb;
|
||||
cursor: pointer;
|
||||
padding: 2px 10px;
|
||||
margin: 1px;
|
||||
font: 20px/25px Tahoma, Geneva, 'Dejavu Sans', Arial, Helvetica, sans-serif;
|
||||
display: inline-block;
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
transition: border-color .2s;
|
||||
}
|
||||
.zone-creation-action:hover,
|
||||
.zone-creation-action:focus {
|
||||
border-color: #3c7fb1;
|
||||
}
|
||||
.zone-creation-action:active {
|
||||
background: linear-gradient(0deg, #fcfcfc 0%, #ebebeb 50%, #dbdbdb 50%) #ebebeb;
|
||||
}
|
||||
.zone-creation-paragraph {
|
||||
text-align: center;
|
||||
padding: 5px 10px;
|
||||
font-size: 1.2em;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
.loading-editor {
|
||||
max-width: 400px;
|
||||
margin: 10px auto;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.loading-editor-image {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background-image: url('spinner.gif');
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.loading-editor-text {
|
||||
font-size: 1.5em;
|
||||
line-height: 1.5em;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.auth {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 300px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.auth-field {
|
||||
margin: 5px 0;
|
||||
}
|
||||
.auth-field-label {
|
||||
font-size: 1.2em;
|
||||
line-height: 1.5em;
|
||||
padding: 2px 5px;
|
||||
}
|
||||
.auth-field-value {
|
||||
display: flex;
|
||||
}
|
||||
.auth-field-value-input {
|
||||
width: 100%;
|
||||
padding: 2px;
|
||||
border: 1px solid #aaa;
|
||||
}
|
||||
.auth-field-value-input:focus {
|
||||
border-color: #f78f2e;
|
||||
}
|
||||
.auth-option {
|
||||
font-size: .9em;
|
||||
line-height: 1.5em;
|
||||
padding: 5px 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.auth-option-input {
|
||||
margin: 0 5px;
|
||||
}
|
||||
.auth-buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 5px 0;
|
||||
}
|
||||
.auth-fid .auth-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
.auth-buttons-button {
|
||||
padding: 2px 5px;
|
||||
min-width: 75px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.auth-fid .auth-buttons-button {
|
||||
width: 100%;
|
||||
border: 1px solid #707070;
|
||||
border-radius: 2px;
|
||||
background-image: linear-gradient(180deg, #fcfcfc 0%, #ebebeb 50%, #dbdbdb 50%);
|
||||
background-color: #ebebeb;
|
||||
cursor: pointer;
|
||||
padding: 2px 10px;
|
||||
margin: 1px;
|
||||
font: 12px/20px Tahoma, Geneva, 'Dejavu Sans', Arial, Helvetica, sans-serif;
|
||||
display: inline-block;
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
transition: border-color .2s;
|
||||
}
|
||||
.auth-fid .auth-buttons-button:hover,
|
||||
.auth-fid .auth-buttons-button:focus {
|
||||
border-color: #3c7fb1;
|
||||
}
|
||||
.auth-fid .auth-buttons-button:active {
|
||||
background-image: linear-gradient(0deg, #fcfcfc 0%, #ebebeb 50%, #dbdbdb 50%);
|
||||
}
|
||||
.auth-fid .auth-buttons-button-fid {
|
||||
font-size: 16px;
|
||||
line-height: 30px;
|
||||
background-image: linear-gradient(180deg, #9475b2 0%, #9475b2 50%, #8559a5 50%);
|
||||
color: #fff;
|
||||
}
|
||||
.auth-fid .auth-buttons-button-fid:hover,
|
||||
.auth-fid .auth-buttons-button-fid:focus {
|
||||
border-color: #9475b2;
|
||||
}
|
||||
.auth-fid .auth-buttons-button-fid:active {
|
||||
background-image: linear-gradient(0deg, #9475b2 0%, #9475b2 50%, #8559a5 50%);
|
||||
}
|
||||
|
||||
.invites {}
|
||||
.invites .error {
|
||||
margin: 5px;
|
||||
}
|
||||
.invites-create {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 5px;
|
||||
}
|
||||
.invites-create-button {
|
||||
padding: 2px 10px;
|
||||
min-width: 75px;
|
||||
cursor: pointer;
|
||||
font-size: 1.2em;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
.invites-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 900px;
|
||||
width: 100%;
|
||||
margin: 10px auto;
|
||||
}
|
||||
.invites-list-item {
|
||||
display: flex;
|
||||
margin-top: 1px;
|
||||
padding-bottom: 1px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.invites-list-item:not(:last-child) {
|
||||
border-bottom: 1px solid #000;
|
||||
}
|
||||
.invites-list-item:nth-child(even) {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
.invites-list-item-created,
|
||||
.invites-list-item-used,
|
||||
.invites-list-item-user {
|
||||
max-width: 200px;
|
||||
width: 100%;
|
||||
}
|
||||
.invites-list-item-token {
|
||||
max-width: 300px;
|
||||
width: 100%;
|
||||
}
|
||||
.invites-list-item-token code {
|
||||
display: block;
|
||||
color: #000;
|
||||
background: #000;
|
||||
font-size: 1.1em;
|
||||
text-align: center;
|
||||
border-radius: 10px;
|
||||
padding: 0 10px;
|
||||
cursor: pointer;
|
||||
transition: background .5s;
|
||||
}
|
||||
.invites-list-item-token code:hover,
|
||||
.invites-list-item-token code:focus {
|
||||
color: #fff;
|
||||
}
|
||||
.invites-list-item-token code:active {
|
||||
background: #fff;
|
||||
transition: background .1s;
|
||||
}
|
||||
|
||||
.nozone {
|
||||
text-align: center;
|
||||
}
|
||||
.nozone h1 {
|
||||
margin: 20px;
|
||||
}
|
||||
.nozone p {
|
||||
font-size: 1.2em;
|
||||
margin: 10px;
|
||||
}
|
||||
.nozone a {
|
||||
color: #000;
|
||||
transition: text-shadow .2s, color .2s;
|
||||
}
|
||||
.nozone a:hover,
|
||||
.nozone a:focus {
|
||||
text-decoration: none;
|
||||
color: #22f;
|
||||
text-shadow: 0 0 10px #22f;
|
||||
}
|
||||
.nozone a:active {
|
||||
color: #f22;
|
||||
text-shadow: 0 0 10px #f22;
|
||||
}
|
||||
|
||||
.my-zones-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.my-zones-list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 5px;
|
||||
margin: 0 5px;
|
||||
background-color: #fff;
|
||||
}
|
||||
.my-zones-list-item:not(:first-child) {
|
||||
border-top: 1px solid #000;
|
||||
}
|
||||
.my-zones-list-item-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
word-wrap: normal;
|
||||
word-break: break-word;
|
||||
}
|
||||
.my-zones-list-item-info-name {
|
||||
font-size: 1.4em;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
.my-zones-list-item-info-name a {
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
}
|
||||
.my-zones-list-item-info-name a:hover,
|
||||
.my-zones-list-item-info-name a:focus,
|
||||
.my-zones-list-item-info-name a:active {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.my-zones-list-item-info-title {
|
||||
margin-right: 5px;
|
||||
}
|
||||
.my-zones-list-item-actions {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.my-zones-list-item-actions-action {
|
||||
margin: 1px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
cursor: pointer;
|
||||
border-radius: 100%;
|
||||
background-size: 16px 16px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
background-image: url('page_white.png');
|
||||
background-color: #fff;
|
||||
transition: background-color .2s;
|
||||
}
|
||||
.my-zones-list-item-actions-action:hover,
|
||||
.my-zones-list-item-actions-action:focus {
|
||||
background-color: #eee;
|
||||
}
|
||||
.my-zones-list-item-actions-action:active {
|
||||
background-color: #ddd;
|
||||
}
|
||||
.my-zones-list-item-actions-action--view { background-image: url('link.png'); }
|
||||
.my-zones-list-item-actions-action--edit { background-image: url('pencil.png'); }
|
||||
.my-zones-list-item-actions-action--delete { background-image: url('bin_closed.png'); background-position: 2px center; }
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.pagination-page {
|
||||
flex: 0 0 auto;
|
||||
min-width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
}
|
||||
.pagination-page:not(:first-child) {
|
||||
border-left: 1px solid #000;
|
||||
}
|
||||
.pagination-page-current {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.zones-sorts {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 2px 5px;
|
||||
}
|
||||
.zones-sorts-sort {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 5px;
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
}
|
||||
.zones-sorts-sort:hover,
|
||||
.zones-sorts-sort:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.zones-sorts-sort img {
|
||||
margin-left: 4px;
|
||||
}
|
||||
.zones-list {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.zones-list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
width: 240px;
|
||||
margin: 4px;
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
box-shadow: 0 1px 3px #888;
|
||||
background-color: #fff;
|
||||
}
|
||||
.zones-list-item a {
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
}
|
||||
.zones-list-item a:hover,
|
||||
.zones-list-item a:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.zones-list-item-screenshot {
|
||||
border: 1px solid #000;
|
||||
display: block;
|
||||
margin-top: 18px;
|
||||
}
|
||||
.zones-list-item-screenshot-image {
|
||||
width: 200px;
|
||||
height: 150px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.zones-list-item-info {
|
||||
margin: 10px 5px;
|
||||
text-align: center;
|
||||
word-wrap: normal;
|
||||
word-break: break-word;
|
||||
}
|
||||
.zones-list-item-info-name {
|
||||
font-size: 1.4em;
|
||||
line-height: 1.2em;
|
||||
margin: 2px 0;
|
||||
}
|
||||
.zones-list-item-info-title {
|
||||
font-size: 1.1em;
|
||||
line-height: 1.2em;
|
||||
margin: 2px 0;
|
||||
}
|
||||
.zones-list-item-info-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex: 0 0 auto;
|
||||
margin: 2px;
|
||||
}
|
||||
.zones-list-item-info-actions-action {
|
||||
margin: 1px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
cursor: pointer;
|
||||
border-radius: 100%;
|
||||
background-size: 16px 16px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
background-image: url('page_white.png');
|
||||
background-color: #fff;
|
||||
transition: background-color .2s;
|
||||
}
|
||||
.zones-list-item-info-actions-action:hover,
|
||||
.zones-list-item-info-actions-action:focus {
|
||||
background-color: #eee;
|
||||
}
|
||||
.zones-list-item-info-actions-action:active {
|
||||
background-color: #ddd;
|
||||
}
|
||||
.zones-list-item-info-actions-action--view { background-image: url('link.png'); }
|
||||
.zones-list-item-info-actions-action--edit { background-image: url('pencil.png'); }
|
||||
.zones-list-item-info-actions-action--delete { background-image: url('bin_closed.png'); background-position: 2px center; }
|
BIN
public/assets/ytkns.png
Normal file
After Width: | Height: | Size: 158 KiB |
1071
public/index.php
Normal file
27
src/Colour.php
Normal file
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
namespace YTKNS;
|
||||
|
||||
final class Colour {
|
||||
private int $raw = 0;
|
||||
|
||||
public function __construct(int $raw = 0) {
|
||||
$this->setRaw($raw);
|
||||
}
|
||||
|
||||
public static function create($value): ?Colour {
|
||||
$colour = is_int($value) ? $value : (is_string($value) && ctype_digit($value) ? (int)$value : -1);
|
||||
return $colour < 0 || $colour > 0xFFFFFF ? null : new static($colour);
|
||||
}
|
||||
|
||||
public function getRaw(): int {
|
||||
return $this->raw;
|
||||
}
|
||||
public function setRaw(int $raw): self {
|
||||
$this->raw = $raw & 0xFFFFFF;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getHex(): string {
|
||||
return str_pad(dechex($this->raw), 6, '0', STR_PAD_LEFT);
|
||||
}
|
||||
}
|
56
src/Config.php
Normal file
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
namespace YTKNS;
|
||||
|
||||
final class Config {
|
||||
public const TYPE_ANY = '';
|
||||
public const TYPE_STR = 'string';
|
||||
public const TYPE_INT = 'integer';
|
||||
public const TYPE_BOOL = 'boolean';
|
||||
public const TYPE_ARR = 'array';
|
||||
|
||||
public const DEFAULTS = [
|
||||
self::TYPE_ANY => null,
|
||||
self::TYPE_STR => '',
|
||||
self::TYPE_INT => 0,
|
||||
self::TYPE_BOOL => false,
|
||||
self::TYPE_ARR => [],
|
||||
];
|
||||
|
||||
private static array $config = [];
|
||||
|
||||
public static function init(): void {
|
||||
$raw = DB::prepare('SELECT `config_key`, `config_value` FROM `ytkns_config`');
|
||||
$raw->execute();
|
||||
$raw = $raw->fetchAll();
|
||||
|
||||
foreach($raw as $entry)
|
||||
self::$config[$entry['config_key']] = unserialize($entry['config_value']);
|
||||
}
|
||||
|
||||
public static function get(string $key, string $type = self::TYPE_ANY, $default = null) {
|
||||
$value = self::$config[$key] ?? null;
|
||||
|
||||
if($type !== self::TYPE_ANY && gettype($value) !== $type)
|
||||
$value = null;
|
||||
|
||||
return $value ?? $default ?? self::DEFAULTS[$type];
|
||||
}
|
||||
|
||||
public static function set(string $key, $value, bool $soft = false): void {
|
||||
self::$config[$key] = $value;
|
||||
|
||||
if(!$soft) {
|
||||
$value = serialize($value);
|
||||
|
||||
$save = DB::prepare('REPLACE INTO `ytkns_config` (`config_key`, `config_value`) VALUES (:key, :value)');
|
||||
$save->bindValue('key', $key);
|
||||
$save->bindValue('value', $value);
|
||||
$save->execute();
|
||||
}
|
||||
}
|
||||
|
||||
public static function setDefault(string $key, $value, bool $soft = false): void {
|
||||
if($value !== null && self::get($key) === null)
|
||||
self::set($key, $value, $soft);
|
||||
}
|
||||
}
|
32
src/DB.php
Normal file
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
namespace YTKNS;
|
||||
|
||||
use PDO;
|
||||
use PDOStatement;
|
||||
use PDOException;
|
||||
|
||||
final class DB {
|
||||
public const FLAGS = [
|
||||
PDO::ATTR_CASE => PDO::CASE_NATURAL,
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL,
|
||||
PDO::ATTR_STRINGIFY_FETCHES => false,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::MYSQL_ATTR_INIT_COMMAND => "
|
||||
SET SESSION
|
||||
sql_mode = 'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION',
|
||||
time_zone = '+00:00';
|
||||
",
|
||||
];
|
||||
|
||||
public static $instance = null;
|
||||
|
||||
public static function init(...$args) {
|
||||
self::$instance = new PDO(...$args);
|
||||
}
|
||||
|
||||
public static function __callStatic(string $name, array $args) {
|
||||
return self::$instance->{$name}(...$args);
|
||||
}
|
||||
}
|
137
src/Effects/BackgroundAudioEffect.php
Normal file
|
@ -0,0 +1,137 @@
|
|||
<?php
|
||||
namespace YTKNS\Effects;
|
||||
|
||||
use Exception;
|
||||
use YTKNS\HtmlTag;
|
||||
use YTKNS\PageBuilder;
|
||||
use YTKNS\PageEffectInterface;
|
||||
use YTKNS\PageEffectException;
|
||||
use YTKNS\Upload;
|
||||
use YTKNS\UploadNotFoundException;
|
||||
|
||||
class BackgroundAudioEffect implements PageEffectInterface {
|
||||
private $oggUpload = null;
|
||||
private $mp3Upload = null;
|
||||
private bool $autoPlay = true;
|
||||
private bool $loop = true;
|
||||
|
||||
private const OGG_MIME = [
|
||||
'audio/ogg',
|
||||
'application/ogg',
|
||||
];
|
||||
|
||||
private const MP3_MIME = [
|
||||
'audio/mpeg',
|
||||
'application/x-font-gdos',
|
||||
];
|
||||
|
||||
public function getEffectName(): string {
|
||||
return 'Background Audio';
|
||||
}
|
||||
|
||||
public function getEffectProperties(): array {
|
||||
return [
|
||||
[
|
||||
'name' => 'ogg',
|
||||
'title' => 'Ogg Audio Source',
|
||||
'type' => [
|
||||
'name' => 'upload',
|
||||
'allowed' => self::OGG_MIME,
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => 'mp3',
|
||||
'title' => 'MP3 Audio Source',
|
||||
'type' => [
|
||||
'name' => 'upload',
|
||||
'allowed' => self::MP3_MIME,
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => 'autoplay',
|
||||
'title' => 'Auto Play',
|
||||
'default' => true,
|
||||
'type' => [
|
||||
'name' => 'bool',
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => 'loop',
|
||||
'title' => 'Loop',
|
||||
'default' => true,
|
||||
'type' => [
|
||||
'name' => 'bool',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function setEffectParams(array $vars, bool $quiet = false): void {
|
||||
try {
|
||||
if(isset($vars['ogg']) && is_string($vars['ogg'])) {
|
||||
try {
|
||||
$this->oggUpload = Upload::byId($vars['ogg']);
|
||||
} catch(Exception $ex) {
|
||||
if(!$quiet)
|
||||
throw $ex;
|
||||
}
|
||||
|
||||
if(!$quiet && !in_array($this->oggUpload->getType(), self::OGG_MIME))
|
||||
throw new PageEffectException('Ogg source upload was of invalid type.');
|
||||
}
|
||||
} catch(UploadNotFoundException $ex) {
|
||||
$noOGG = true;
|
||||
}
|
||||
|
||||
try {
|
||||
if(isset($vars['mp3']) && is_string($vars['mp3'])) {
|
||||
try {
|
||||
$this->mp3Upload = Upload::byId($vars['mp3']);
|
||||
} catch(Exception $ex) {
|
||||
if(!$quiet)
|
||||
throw $ex;
|
||||
}
|
||||
|
||||
if(!$quiet && !in_array($this->mp3Upload->getType(), self::MP3_MIME))
|
||||
throw new PageEffectException('MP3 source upload was of invalid type.');
|
||||
}
|
||||
} catch(UploadNotFoundException $ex) {
|
||||
$noMP3 = true;
|
||||
}
|
||||
|
||||
if(!empty($noMP3) && !empty($noOGG))
|
||||
throw new PageEffectException('No audio source uploaded.');
|
||||
|
||||
if(isset($vars['autoplay']))
|
||||
$this->autoPlay = is_bool($vars['autoplay']) ? $vars['autoplay'] : (is_string($vars['autoplay']) ? boolval($vars['autoplay']) : false);
|
||||
|
||||
if(isset($vars['loop']))
|
||||
$this->loop = is_bool($vars['loop']) ? $vars['loop'] : (is_string($vars['loop']) ? boolval($vars['loop']) : false);
|
||||
}
|
||||
|
||||
public function getEffectParams(): array {
|
||||
return [
|
||||
'ogg' => empty($this->oggUpload) ? null : $this->oggUpload->getId(),
|
||||
'mp3' => empty($this->mp3Upload) ? null : $this->mp3Upload->getId(),
|
||||
'autoplay' => $this->autoPlay,
|
||||
'loop' => $this->loop,
|
||||
];
|
||||
}
|
||||
|
||||
public function applyEffect(PageBuilder $builder): void {
|
||||
$audioTag = new HtmlTag('audio');
|
||||
$audioTag->setAttribute('id', 'BackgroundAudio');
|
||||
|
||||
if($this->autoPlay)
|
||||
$audioTag->setAttribute('autoplay', 'autoplay');
|
||||
if($this->loop)
|
||||
$audioTag->setAttribute('loop', 'loop');
|
||||
|
||||
if(!empty($this->oggUpload))
|
||||
$audioTag->appendChild(new HtmlTag('source', ['type' => 'audio/ogg', 'src' => $this->oggUpload->getUrl()], true));
|
||||
if(!empty($this->mp3Upload))
|
||||
$audioTag->appendChild(new HtmlTag('source', ['type' => 'audio/mpeg', 'src' => $this->mp3Upload->getUrl()], true));
|
||||
|
||||
$builder->getBody()->appendChild($audioTag);
|
||||
}
|
||||
}
|
401
src/Effects/BackgroundImageEffect.php
Normal file
|
@ -0,0 +1,401 @@
|
|||
<?php
|
||||
namespace YTKNS\Effects;
|
||||
|
||||
use Exception;
|
||||
use YTKNS\Colour;
|
||||
use YTKNS\Gradient;
|
||||
use YTKNS\HtmlTag;
|
||||
use YTKNS\PageBuilder;
|
||||
use YTKNS\PageEffectInterface;
|
||||
use YTKNS\PageEffectException;
|
||||
use YTKNS\Upload;
|
||||
use YTKNS\UploadNotFoundException;
|
||||
|
||||
class BackgroundImageEffect implements PageEffectInterface {
|
||||
private const IMG_MIME = [
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'image/gif',
|
||||
];
|
||||
private const ATTACH_OPTS = [
|
||||
'scroll' => 'Relative to browser',
|
||||
'fixed' => 'Fixed to browser',
|
||||
'local' => 'Local to browser',
|
||||
];
|
||||
private const POSITION_OPTS = [
|
||||
'top left' => 'Top left',
|
||||
'top center' => 'Top center',
|
||||
'top right' => 'Top right',
|
||||
'center left' => 'Center left',
|
||||
'center center' => 'Center',
|
||||
'center right' => 'Center right',
|
||||
'bottom left' => 'Bottom left',
|
||||
'bottom center' => 'Bottom center',
|
||||
'bottom right' => 'Bottom right',
|
||||
];
|
||||
private const REPEAT_OPTS = [
|
||||
'repeat' => 'Repeat',
|
||||
'repeat-x' => 'Repeat horizontally',
|
||||
'repeat-y' => 'Repeat vertically',
|
||||
'space' => 'Repeat without clipping',
|
||||
'round' => 'Repeat with stretching',
|
||||
'no-repeat' => 'Don\'t repeat',
|
||||
];
|
||||
private const SIZE_OPTS = [
|
||||
'auto auto' => 'Determine automatically',
|
||||
'cover' => 'Cover entire screen',
|
||||
'contain' => 'Contain within screen',
|
||||
];
|
||||
private const SLIDE_DIRS = [
|
||||
'tl' => 'Top left',
|
||||
'tc' => 'Top',
|
||||
'tr' => 'Top right',
|
||||
'cl' => 'Left',
|
||||
'cr' => 'Right',
|
||||
'bl' => 'Bottom left',
|
||||
'bc' => 'Bottom',
|
||||
'br' => 'Bottom right',
|
||||
];
|
||||
private const SLIDE_MIN = 0.01;
|
||||
private const SLIDE_MAX = 1000;
|
||||
private const SPIN_DIRS = [
|
||||
'cw' => 'Clockwise',
|
||||
'ccw' => 'Counterclockwise',
|
||||
];
|
||||
private const SPIN_MIN = 0.01;
|
||||
private const SPIN_MAX = 1000;
|
||||
|
||||
private $imageUpload = null;
|
||||
private $slide = false;
|
||||
private $slideSpeed = 1;
|
||||
private $slideDirection = 'br';
|
||||
private $spin = false;
|
||||
private $spinSpeed = 1;
|
||||
private $spinDirection = 'cw';
|
||||
private $attachment = null;
|
||||
private $colour = null;
|
||||
private $position = null;
|
||||
private $repeat = null;
|
||||
private $size = null;
|
||||
private $syncWithAudio = false;
|
||||
private $gradient = null;
|
||||
private $gradientAnimate = false;
|
||||
|
||||
public function getEffectName(): string {
|
||||
return 'Background Image';
|
||||
}
|
||||
|
||||
public function getEffectProperties(): array {
|
||||
return [
|
||||
[
|
||||
'name' => 'img',
|
||||
'title' => 'Image',
|
||||
'type' => [
|
||||
'name' => 'upload',
|
||||
'allowed' => self::IMG_MIME,
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => 'sld',
|
||||
'title' => 'Slide Animation',
|
||||
'default' => false,
|
||||
'type' => [
|
||||
'name' => 'bool',
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => 'sldspd',
|
||||
'title' => 'Slide Animation Period',
|
||||
'default' => 1,
|
||||
'type' => [
|
||||
'name' => 'float',
|
||||
'min' => self::SLIDE_MIN,
|
||||
'max' => self::SLIDE_MAX,
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => 'slddir',
|
||||
'title' => 'Slide Animation Direction',
|
||||
'default' => 'br',
|
||||
'type' => [
|
||||
'name' => 'select',
|
||||
'options' => self::SLIDE_DIRS,
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => 'spin',
|
||||
'title' => 'Spin Animation',
|
||||
'default' => false,
|
||||
'type' => [
|
||||
'name' => 'bool',
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => 'spnspd',
|
||||
'title' => 'Spin Animation Period',
|
||||
'default' => 1,
|
||||
'type' => [
|
||||
'name' => 'float',
|
||||
'min' => self::SPIN_MIN,
|
||||
'max' => self::SPIN_MAX,
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => 'spndir',
|
||||
'title' => 'Spin Animation Direction',
|
||||
'default' => 'br',
|
||||
'type' => [
|
||||
'name' => 'select',
|
||||
'options' => self::SPIN_DIRS,
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => 'attach',
|
||||
'title' => 'Attachment',
|
||||
'type' => [
|
||||
'name' => 'select',
|
||||
'options' => self::ATTACH_OPTS,
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => 'col',
|
||||
'title' => 'Colour',
|
||||
'type' => [
|
||||
'name' => 'colour',
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => 'pos',
|
||||
'title' => 'Position',
|
||||
'type' => [
|
||||
'name' => 'select',
|
||||
'options' => self::POSITION_OPTS,
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => 'rep',
|
||||
'title' => 'Repeat',
|
||||
'type' => [
|
||||
'name' => 'select',
|
||||
'options' => self::REPEAT_OPTS,
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => 'size',
|
||||
'title' => 'Size',
|
||||
'type' => [
|
||||
'name' => 'select',
|
||||
'options' => self::SIZE_OPTS,
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => 'sync',
|
||||
'title' => 'Synchronise with Background Audio',
|
||||
'default' => false,
|
||||
'type' => [
|
||||
'name' => 'bool',
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => 'grad',
|
||||
'title' => 'Gradient',
|
||||
'default' => [],
|
||||
'type' => [
|
||||
'name' => 'gradient',
|
||||
],
|
||||
],
|
||||
/*[
|
||||
'name' => 'grdanm',
|
||||
'title' => 'Apply animations to gradient',
|
||||
'default' => false,
|
||||
'type' => [
|
||||
'name' => 'bool',
|
||||
],
|
||||
],*/
|
||||
];
|
||||
}
|
||||
|
||||
public function setEffectParams(array $vars, bool $quiet = false): void {
|
||||
try {
|
||||
if(isset($vars['img']) && is_string($vars['img'])) {
|
||||
try {
|
||||
$this->imageUpload = Upload::byId($vars['img']);
|
||||
} catch(Exception $ex) {
|
||||
if(!$quiet)
|
||||
throw $ex;
|
||||
}
|
||||
|
||||
if(!$quiet && !in_array($this->imageUpload->getType(), self::IMG_MIME))
|
||||
throw new PageEffectException('Image upload was of invalid type.');
|
||||
}
|
||||
} catch(UploadNotFoundException $ex) {
|
||||
$this->imageUpload = null;
|
||||
}
|
||||
|
||||
if(isset($vars['sld']))
|
||||
$this->slide = is_bool($vars['sld']) ? $vars['sld'] : (is_string($vars['sld']) ? boolval($vars['sld']) : false);
|
||||
if(isset($vars['spin']))
|
||||
$this->spin = is_bool($vars['spin']) ? $vars['spin'] : (is_string($vars['spin']) ? boolval($vars['spin']) : false);
|
||||
if(isset($vars['sync']))
|
||||
$this->syncWithAudio = is_bool($vars['sync']) ? $vars['sync'] : (is_string($vars['sync']) ? boolval($vars['sync']) : false);
|
||||
if(isset($vars['grdanm']))
|
||||
$this->gradientAnimate = is_bool($vars['grdanm']) ? $vars['grdanm'] : (is_string($vars['grdanm']) ? boolval($vars['grdanm']) : false);
|
||||
|
||||
if(isset($vars['sldspd'])) {
|
||||
$this->slideSpeed = is_int($vars['sldspd']) || is_float($vars['sldspd']) ? $vars['sldspd'] : (is_string($vars['sldspd']) ? floatval($vars['sldspd']) : 1);
|
||||
|
||||
if(!$quiet && ($this->slideSpeed < self::SLIDE_MIN || $this->slideSpeed > self::SLIDE_MAX))
|
||||
throw new PageEffectException(sprintf('Slide speed may not be less than %d or more than %d', self::SLIDE_MIN, self::SLIDE_MAX));
|
||||
}
|
||||
if(isset($vars['spnspd'])) {
|
||||
$this->spinSpeed = is_int($vars['spnspd']) || is_float($vars['spnspd']) ? $vars['spnspd'] : (is_string($vars['spnspd']) ? floatval($vars['spnspd']) : 1);
|
||||
|
||||
if(!$quiet && ($this->spinSpeed < self::SPIN_MIN || $this->spinSpeed > self::SPIN_MAX))
|
||||
throw new PageEffectException(sprintf('Spin speed may not be less than %d or more than %d', self::SPIN_MIN, self::SPIN_MAX));
|
||||
}
|
||||
|
||||
if(isset($vars['slddir']) && is_string($vars['slddir']) && array_key_exists($vars['slddir'], self::SLIDE_DIRS))
|
||||
$this->slideDirection = $vars['slddir'];
|
||||
if(isset($vars['spndir']) && is_string($vars['spndir']) && array_key_exists($vars['spndir'], self::SPIN_DIRS))
|
||||
$this->spinDirection = $vars['spndir'];
|
||||
|
||||
if(isset($vars['col']))
|
||||
$this->colour = Colour::create($vars['col']);
|
||||
|
||||
try {
|
||||
if(isset($vars['grad']))
|
||||
$this->gradient = Gradient::fromArray($vars['grad']);
|
||||
} catch(Exception $ex) {
|
||||
if(!$quiet)
|
||||
throw $ex;
|
||||
}
|
||||
|
||||
if(isset($vars['attach']) && is_string($vars['attach']) && array_key_exists($vars['attach'], self::ATTACH_OPTS))
|
||||
$this->attachment = $vars['attach'];
|
||||
if(isset($vars['pos']) && is_string($vars['pos']) && array_key_exists($vars['pos'], self::POSITION_OPTS))
|
||||
$this->position = $vars['pos'];
|
||||
if(isset($vars['rep']) && is_string($vars['rep']) && array_key_exists($vars['rep'], self::REPEAT_OPTS))
|
||||
$this->repeat = $vars['rep'];
|
||||
if(isset($vars['size']) && is_string($vars['size']) && array_key_exists($vars['size'], self::SIZE_OPTS))
|
||||
$this->size = $vars['size'];
|
||||
}
|
||||
|
||||
public function getEffectParams(): array {
|
||||
return [
|
||||
'img' => empty($this->imageUpload) ? null : $this->imageUpload->getId(),
|
||||
'sld' => $this->slide,
|
||||
'sldspd' => $this->slideSpeed,
|
||||
'slddir' => $this->slideDirection,
|
||||
'spin' => $this->spin,
|
||||
'spnspd' => $this->spinSpeed,
|
||||
'spndir' => $this->spinDirection,
|
||||
'attach' => $this->attachment,
|
||||
'col' => empty($this->colour) ? 0 : $this->colour->getRaw(),
|
||||
'pos' => $this->position,
|
||||
'rep' => $this->repeat,
|
||||
'size' => $this->size,
|
||||
'sync' => $this->syncWithAudio,
|
||||
'grad' => empty($this->gradient) ? null : $this->gradient->getArray(),
|
||||
'grdanm' => $this->gradientAnimate,
|
||||
];
|
||||
}
|
||||
|
||||
public function applyEffect(PageBuilder $builder): void {
|
||||
$head = $builder->getHead();
|
||||
$body = $builder->getContainer();
|
||||
$bgTarget = $body;
|
||||
$styleText = '';
|
||||
|
||||
if(empty($_GET['preview']) && !$this->gradientAnimate) {
|
||||
$bgTargetClasses = ['BackgroundImage'];
|
||||
if($this->spin && $this->slide)
|
||||
$bgTargetClasses[] = 'Cover';
|
||||
|
||||
$bgTarget = $body->appendChild(new HtmlTag('div', ['class' => implode(' ', $bgTargetClasses), 'id' => 'BackgroundImage']));
|
||||
}
|
||||
|
||||
if(empty($_GET['preview']) && !empty($this->imageUpload)) {
|
||||
if($this->slide) {
|
||||
$imageSize = getimagesize($this->imageUpload->getPath());
|
||||
$imageWidth = $imageSize[0];
|
||||
$imageHeight = $imageSize[1];
|
||||
|
||||
$animSX = $this->slideDirection[1] === 'l' ? $imageWidth : 0;
|
||||
$animSY = $this->slideDirection[0] === 't' ? $imageHeight : 0;
|
||||
$animEX = $this->slideDirection[1] === 'r' ? $imageWidth : 0;
|
||||
$animEY = $this->slideDirection[0] === 'b' ? $imageHeight : 0;
|
||||
|
||||
if($imageSize !== false) {
|
||||
$slideAnim = '_' . bin2hex(random_bytes(8));
|
||||
$styleText .= sprintf(
|
||||
'@keyframes %s { 0%% { background-position: %dpx %dpx; } 100%% { background-position: %dpx %dpx; } } ',
|
||||
$slideAnim, $animSX, $animSY, $animEX, $animEY
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$styleBgColour = isset($this->colour)
|
||||
? sprintf(' background-color: #%s;', $this->colour->getHex())
|
||||
: ' background-color: #000;';
|
||||
|
||||
$animation = [];
|
||||
$backgroundImage = [];
|
||||
|
||||
if($this->spin)
|
||||
$animation[] = sprintf('SharedAnimation_Spin360 infinite linear %s %Fs', $this->spinDirection === 'cw' ? 'normal' : 'reverse', $this->spinSpeed);
|
||||
if(isset($slideAnim))
|
||||
$animation[] = sprintf('%s infinite linear %Fs', $slideAnim, $this->slideSpeed);
|
||||
|
||||
$syncWithAudio = empty($_GET['preview']) && $this->syncWithAudio;
|
||||
|
||||
if(!empty($this->imageUpload) && !$syncWithAudio)
|
||||
$backgroundImage[] = sprintf('url(\'%s\')', $this->imageUpload->getUrl());
|
||||
|
||||
if($bgTarget !== $body) {
|
||||
$styleText .= '#container {';
|
||||
if(!empty($this->gradient))
|
||||
$styleText .= 'background-image: ' . $this->gradient->getCSS() . ';';
|
||||
$styleText .= $styleBgColour;
|
||||
$styleText .= '}';
|
||||
}
|
||||
|
||||
$styleText .= '#' . ($bgTarget->getAttribute('id')) . ' {';
|
||||
|
||||
if($bgTarget === $body) {
|
||||
if(!empty($this->gradient))
|
||||
$backgroundImage[] = $this->gradient->getCSS();
|
||||
|
||||
$styleText .= $styleBgColour;
|
||||
}
|
||||
|
||||
if(!empty($backgroundImage))
|
||||
$styleText .= sprintf(' background-image: %s;', implode(', ', $backgroundImage));
|
||||
|
||||
if(!empty($this->attachment))
|
||||
$styleText .= sprintf(' background-attachment: %s;', $this->attachment);
|
||||
if(!empty($this->position))
|
||||
$styleText .= sprintf(' background-position: %s;', $this->position);
|
||||
if(!empty($this->repeat))
|
||||
$styleText .= sprintf(' background-repeat: %s;', $this->repeat);
|
||||
if(!empty($this->size))
|
||||
$styleText .= sprintf(' background-size: %s;', $this->size);
|
||||
if(!empty($animation))
|
||||
$styleText .= sprintf(' animation: %s;', implode(', ', $animation));
|
||||
|
||||
$styleText .= ' }';
|
||||
|
||||
$styleTag = new HtmlTag('style', ['type' => 'text/css']);
|
||||
$styleTag->setTextContent($styleText);
|
||||
$head->appendChild($styleTag);
|
||||
|
||||
if(!empty($this->imageUpload) && $syncWithAudio) {
|
||||
$scriptText = 'window.addEventListener(\'DOMContentLoaded\', function() {';
|
||||
$scriptText .= 'synchroniseBackgroundWithAudio(\'' . $this->imageUpload->getUrl() . '\');';
|
||||
$scriptText .= '});';
|
||||
$scriptTag = new HtmlTag('script', ['type' => 'text/javascript']);
|
||||
$scriptTag->setTextContent($scriptText);
|
||||
$head->appendChild($scriptTag);
|
||||
}
|
||||
}
|
||||
}
|
153
src/Effects/ForegroundImageEffect.php
Normal file
|
@ -0,0 +1,153 @@
|
|||
<?php
|
||||
namespace YTKNS\Effects;
|
||||
|
||||
use Exception;
|
||||
use YTKNS\HtmlTag;
|
||||
use YTKNS\HtmlText;
|
||||
use YTKNS\PageBuilder;
|
||||
use YTKNS\PageEffectInterface;
|
||||
use YTKNS\Upload;
|
||||
use YTKNS\UploadNotFoundException;
|
||||
|
||||
class ForegroundImageEffect implements PageEffectInterface {
|
||||
private const IMG_MIME = [
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'image/gif',
|
||||
];
|
||||
private const SPIN_DIRS = [
|
||||
'cw' => 'Clockwise',
|
||||
'ccw' => 'Counterclockwise',
|
||||
];
|
||||
private const SPIN_MIN = 0.01;
|
||||
private const SPIN_MAX = 1000;
|
||||
|
||||
private $imageUpload = null;
|
||||
private $spin = false;
|
||||
private $spinSpeed = 1;
|
||||
private $spinDirection = 'cw';
|
||||
private $syncWithAudio = false;
|
||||
|
||||
public function getEffectName(): string {
|
||||
return 'Foreground Image';
|
||||
}
|
||||
|
||||
public function getEffectProperties(): array {
|
||||
return [
|
||||
[
|
||||
'name' => 'img',
|
||||
'title' => 'Image',
|
||||
'type' => [
|
||||
'name' => 'upload',
|
||||
'allowed' => self::IMG_MIME,
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => 'spin',
|
||||
'title' => 'Spin Animation',
|
||||
'default' => false,
|
||||
'type' => [
|
||||
'name' => 'bool',
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => 'spnspd',
|
||||
'title' => 'Spin Animation Period',
|
||||
'default' => 1,
|
||||
'type' => [
|
||||
'name' => 'float',
|
||||
'min' => self::SPIN_MIN,
|
||||
'max' => self::SPIN_MAX,
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => 'spndir',
|
||||
'title' => 'Spin Animation Direction',
|
||||
'default' => 'br',
|
||||
'type' => [
|
||||
'name' => 'select',
|
||||
'options' => self::SPIN_DIRS,
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => 'sync',
|
||||
'title' => 'Synchronise with Background Audio',
|
||||
'default' => false,
|
||||
'type' => [
|
||||
'name' => 'bool',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function setEffectParams(array $vars, bool $quiet = false): void {
|
||||
try {
|
||||
if(isset($vars['img']) && is_string($vars['img'])) {
|
||||
try {
|
||||
$this->imageUpload = Upload::byId($vars['img']);
|
||||
} catch(Exception $ex) {
|
||||
if(!$quiet)
|
||||
throw $ex;
|
||||
}
|
||||
|
||||
if(!$quiet && !in_array($this->imageUpload->getType(), self::IMG_MIME))
|
||||
throw new PageEffectException('Image upload was of invalid type.');
|
||||
}
|
||||
} catch(UploadNotFoundException $ex) {
|
||||
$this->imageUpload = null;
|
||||
}
|
||||
|
||||
if(isset($vars['spin']))
|
||||
$this->spin = is_bool($vars['spin']) ? $vars['spin'] : (is_string($vars['spin']) ? boolval($vars['spin']) : false);
|
||||
if(isset($vars['sync']))
|
||||
$this->syncWithAudio = is_bool($vars['sync']) ? $vars['sync'] : (is_string($vars['sync']) ? boolval($vars['sync']) : false);
|
||||
|
||||
if(isset($vars['spnspd'])) {
|
||||
$this->spinSpeed = is_int($vars['spnspd']) || is_float($vars['spnspd']) ? $vars['spnspd'] : (is_string($vars['spnspd']) ? floatval($vars['spnspd']) : 1);
|
||||
|
||||
if(!$quiet && ($this->spinSpeed < self::SPIN_MIN || $this->spinSpeed > self::SPIN_MAX))
|
||||
throw new PageEffectException(sprintf('Spin speed may not be less than %d or more than %d', self::SPIN_MIN, self::SPIN_MAX));
|
||||
}
|
||||
|
||||
if(isset($vars['spndir']) && is_string($vars['spndir']) && array_key_exists($vars['spndir'], self::SPIN_DIRS))
|
||||
$this->spinDirection = $vars['spndir'];
|
||||
}
|
||||
|
||||
public function getEffectParams(): array {
|
||||
return [
|
||||
'img' => empty($this->imageUpload) ? null : $this->imageUpload->getId(),
|
||||
'spin' => $this->spin,
|
||||
'spnspd' => $this->spinSpeed,
|
||||
'spndir' => $this->spinDirection,
|
||||
'sync' => $this->syncWithAudio,
|
||||
];
|
||||
}
|
||||
|
||||
public function applyEffect(PageBuilder $builder): void {
|
||||
$element = $builder->getContainer()->appendChild(new HtmlTag('div', ['class' => 'ForegroundImage']));
|
||||
$imageTarget = $element->appendChild(new HtmlTag('div', ['class' => 'ForegroundImage_Image', 'id' => 'ForegroundImage']));
|
||||
|
||||
if(!empty($this->imageUpload)) {
|
||||
$imageSize = getimagesize($this->imageUpload->getPath());
|
||||
|
||||
if($imageSize !== false) {
|
||||
$styleText = sprintf('width: %dpx; height: %dpx', $imageSize[0], $imageSize[1]);
|
||||
|
||||
if(!empty($_GET['preview']) || !$this->syncWithAudio)
|
||||
$styleText .= sprintf(';background-image:url(\'%s\')', $this->imageUpload->getUrl());
|
||||
|
||||
if(empty($_GET['preview']) && $this->spin)
|
||||
$styleText .= sprintf(';animation: SharedAnimation_Spin360 infinite linear %s %Fs', $this->spinDirection === 'cw' ? 'normal' : 'reverse', $this->spinSpeed);
|
||||
|
||||
$imageTarget->setAttribute('style', $styleText);
|
||||
|
||||
$scriptText = 'window.addEventListener(\'DOMContentLoaded\', function() {';
|
||||
$scriptText .= 'synchroniseBackgroundWithAudio(\'' . $this->imageUpload->getUrl() . '\', \'ForegroundImage\');';
|
||||
$scriptText .= '});';
|
||||
$scriptTag = new HtmlTag('script', ['type' => 'text/javascript']);
|
||||
$scriptTag->setTextContent($scriptText);
|
||||
$builder->getHead()->appendChild($scriptTag);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
45
src/Effects/NewlyCreatedPageEffect.php
Normal file
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
namespace YTKNS\Effects;
|
||||
|
||||
use YTKNS\HtmlTag;
|
||||
use YTKNS\HtmlText;
|
||||
use YTKNS\PageBuilder;
|
||||
use YTKNS\PageEffectInterface;
|
||||
|
||||
class NewlyCreatedPageEffect implements PageEffectInterface {
|
||||
private $headerText = null;
|
||||
private $subText = null;
|
||||
|
||||
public function getEffectName(): string {
|
||||
return 'Newly Created Page Notice';
|
||||
}
|
||||
|
||||
public function getEffectProperties(): array {
|
||||
return [];
|
||||
}
|
||||
|
||||
public function setEffectParams(array $vars, bool $quiet = false): void {
|
||||
if(!empty($vars['h']))
|
||||
$this->headerText = $vars['h'];
|
||||
if(!empty($vars['s']))
|
||||
$this->subText = $vars['s'];
|
||||
}
|
||||
|
||||
public function getEffectParams(): array {
|
||||
$vars = [];
|
||||
|
||||
if(!empty($this->headerText))
|
||||
$vars['h'] = $this->headerText;
|
||||
if(!empty($this->subText))
|
||||
$vars['s'] = $this->subText;
|
||||
|
||||
return $vars;
|
||||
}
|
||||
|
||||
public function applyEffect(PageBuilder $builder): void {
|
||||
$builder->getContainer()->appendChild(new HtmlTag('div', ['class' => 'NewCreatePageEffect_Main'], [
|
||||
new HtmlTag('h1', [], [new HtmlText($this->headerText ?? 'This page is still empty')]),
|
||||
new HtmlTag('p', [], [new HtmlText($this->subText ?? 'Please come back later')]),
|
||||
]));
|
||||
}
|
||||
}
|
57
src/Effects/ZoomTextEffect.php
Normal file
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
namespace YTKNS\Effects;
|
||||
|
||||
use YTKNS\HtmlTag;
|
||||
use YTKNS\HtmlText;
|
||||
use YTKNS\PageBuilder;
|
||||
use YTKNS\PageEffectInterface;
|
||||
use YTKNS\PageEffectException;
|
||||
|
||||
class ZoomTextEffect implements PageEffectInterface {
|
||||
private const TEXT_MIN = 1;
|
||||
private const TEXT_MAX = 1000;
|
||||
|
||||
private $text = '';
|
||||
|
||||
public function getEffectName(): string {
|
||||
return 'Zoom Text';
|
||||
}
|
||||
|
||||
public function getEffectProperties(): array {
|
||||
return [
|
||||
[
|
||||
'name' => 'txt',
|
||||
'title' => 'Text',
|
||||
'type' => [
|
||||
'name' => 'string',
|
||||
'min' => self::TEXT_MIN,
|
||||
'max' => self::TEXT_MAX,
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function setEffectParams(array $vars, bool $quiet = false): void {
|
||||
if(isset($vars['txt']) && is_string($vars['txt'])) {
|
||||
if(!$quiet && (mb_strlen($vars['txt']) < self::TEXT_MIN || mb_strlen($vars['txt']) > self::TEXT_MAX))
|
||||
throw new PageEffectException('Your text is too long or too short.');
|
||||
$this->text = $vars['txt'];
|
||||
}
|
||||
}
|
||||
|
||||
public function getEffectParams(): array {
|
||||
return [
|
||||
'txt' => $this->text,
|
||||
];
|
||||
}
|
||||
|
||||
public function applyEffect(PageBuilder $builder): void {
|
||||
$tags = [];
|
||||
|
||||
for($i = 1; $i <= 50; $i++) {
|
||||
$tags[] = new HtmlTag('div', ['class' => 'ZoomText_Child', 'style' => sprintf('font-size: %1$dpt; top: %1$dpx; left: %2$dpx; color: rgb(%3$d, %3$d, %3$d);', $i * 2, $i, $i === 50 ? 0 : (4 * $i))], [new HtmlText($this->text)]);
|
||||
}
|
||||
|
||||
$builder->getContainer()->appendChild(new HtmlTag('div', ['class' => 'ZoomText'], $tags));
|
||||
}
|
||||
}
|
87
src/Gradient.php
Normal file
|
@ -0,0 +1,87 @@
|
|||
<?php
|
||||
namespace YTKNS;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
final class Gradient {
|
||||
private int $direction = 0;
|
||||
private array $points = [];
|
||||
|
||||
public function getArray(): array {
|
||||
$arr = [
|
||||
'd' => $this->getDirection(),
|
||||
'p' => [],
|
||||
];
|
||||
|
||||
foreach($this->getPoints() as $point)
|
||||
$arr['p'][] = $point->getArray();
|
||||
|
||||
return $arr;
|
||||
}
|
||||
|
||||
public static function fromArray($array): ?self {
|
||||
$gradient = new static;
|
||||
|
||||
if(!empty($array)) {
|
||||
if(is_string($array))
|
||||
$array = json_decode($array);
|
||||
if(is_object($array))
|
||||
$array = (array)$array;
|
||||
if(is_array($array)) {
|
||||
if(isset($array['d']) && is_int($array['d']))
|
||||
$gradient->setDirection($array['d']);
|
||||
|
||||
if(isset($array['p']) && is_array($array['p'])) {
|
||||
$gradient->resetPoints();
|
||||
|
||||
foreach($array['p'] as $point)
|
||||
$gradient->addPoint(GradientPoint::fromArray($point));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(count($gradient->points) < 2)
|
||||
return null;
|
||||
|
||||
return $gradient;
|
||||
}
|
||||
|
||||
public function getCSS(): string {
|
||||
$points = [];
|
||||
|
||||
foreach($this->points as $point)
|
||||
$points[] = $point->getCSS();
|
||||
|
||||
return sprintf('linear-gradient(%ddeg, %s)', $this->getDirection(), implode(', ', $points));
|
||||
}
|
||||
|
||||
public function getDirection(): int {
|
||||
return $this->direction;
|
||||
}
|
||||
public function setDirection(int $dir): self {
|
||||
if($dir < 0 || $dir > 359)
|
||||
throw new InvalidArgumentException('dir must be between 0 and 359.');
|
||||
$this->direction = $dir;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPoints(): array {
|
||||
return $this->points;
|
||||
}
|
||||
|
||||
public function addPoint(GradientPoint $point): self {
|
||||
if(!in_array($point, $this->points))
|
||||
$this->points[] = $point;
|
||||
return $this;
|
||||
}
|
||||
public function removePoint(GradientPoint $point): self {
|
||||
$index = array_search($point, $this->points);
|
||||
if($index !== null)
|
||||
unset($this->points[$index]);
|
||||
return $this;
|
||||
}
|
||||
public function resetPoints(): self {
|
||||
$this->points = [];
|
||||
return $this;
|
||||
}
|
||||
}
|
65
src/GradientPoint.php
Normal file
|
@ -0,0 +1,65 @@
|
|||
<?php
|
||||
namespace YTKNS;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
final class GradientPoint {
|
||||
private int $offset = 0;
|
||||
private $colour = null;
|
||||
|
||||
public function __construct(int $offset = 0, \Colour $colour = null) {
|
||||
$this->setOffset($offset);
|
||||
if($colour !== null)
|
||||
$this->setColour($colour);
|
||||
}
|
||||
|
||||
public function getArray(): array {
|
||||
return [
|
||||
'o' => $this->getOffset(),
|
||||
'c' => $this->getColour()->getRaw(),
|
||||
];
|
||||
}
|
||||
|
||||
public static function fromArray($array): self {
|
||||
$point = new static;
|
||||
|
||||
if(!empty($array)) {
|
||||
if(is_string($array))
|
||||
$array = json_decode($array);
|
||||
if(is_object($array))
|
||||
$array = (array)$array;
|
||||
if(is_array($array)) {
|
||||
if(isset($array['o']) && is_int($array['o']))
|
||||
$point->setOffset($array['o']);
|
||||
if(isset($array['c']))
|
||||
$point->setColour(Colour::create($array['c']));
|
||||
}
|
||||
}
|
||||
|
||||
return $point;
|
||||
}
|
||||
|
||||
public function getOffset(): int {
|
||||
return $this->offset;
|
||||
}
|
||||
public function setOffset(int $offset): self {
|
||||
if($offset < 0 || $offset > 100)
|
||||
throw new InvalidArgumentException('offset must be between 0 and 100');
|
||||
$this->offset = $offset;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getColour(): Colour {
|
||||
if($this->colour === null)
|
||||
$this->colour = new Colour;
|
||||
return $this->colour;
|
||||
}
|
||||
public function setColour(Colour $colour): self {
|
||||
$this->colour = $colour;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCSS(): string {
|
||||
return sprintf('#%s %d%%', $this->getColour()->getHex(), $this->getOffset());
|
||||
}
|
||||
}
|
126
src/HtmlTag.php
Normal file
|
@ -0,0 +1,126 @@
|
|||
<?php
|
||||
namespace YTKNS;
|
||||
|
||||
class HtmlTag implements HtmlTypeInterface {
|
||||
private string $tagName = 'div';
|
||||
private array $attributes = [];
|
||||
private array $children = [];
|
||||
private bool $selfClosing = false;
|
||||
|
||||
public function __construct(string $tagName, array $attributes = [], $children = null) {
|
||||
$this->tagName = $tagName;
|
||||
|
||||
foreach($attributes as $key => $val)
|
||||
$this->setAttribute($key, $val);
|
||||
|
||||
if(is_bool($children))
|
||||
$this->selfClosing = $children;
|
||||
elseif(is_array($children)) {
|
||||
foreach($children as $child)
|
||||
if($child !== null && $child instanceof HtmlTypeInterface)
|
||||
$this->appendChild($child);
|
||||
}
|
||||
}
|
||||
|
||||
public function getTagName(): string {
|
||||
return $this->tagName;
|
||||
}
|
||||
|
||||
public function getAttribute(string $name): ?string {
|
||||
return $this->attributes[$name] ?? null;
|
||||
}
|
||||
public function setAttribute(string $name, $value): void {
|
||||
if($value === null) {
|
||||
$this->removeAttribute($name);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->attributes[$name] = $value;
|
||||
}
|
||||
public function removeAttribute(string $name): void {
|
||||
unset($this->attributes[$name]);
|
||||
}
|
||||
|
||||
public function appendChild(HtmlTypeInterface $child): HtmlTypeInterface {
|
||||
return $this->children[] = $child;
|
||||
}
|
||||
public function removeChild(HtmlTypeInterface $target): void {
|
||||
$remove = [];
|
||||
|
||||
foreach($this->children as $child)
|
||||
if($child === $target)
|
||||
$remove[] = $child;
|
||||
|
||||
$this->children = array_diff($this->children, $remove);
|
||||
}
|
||||
|
||||
public function setTextContent(string $textContent): void {
|
||||
$this->children = [new HtmlText($textContent)];
|
||||
}
|
||||
|
||||
public function getElementsByTagName(string $tagName): array {
|
||||
$tagName = mb_strtolower($tagName);
|
||||
$elements = [];
|
||||
|
||||
foreach($this->children as $child) {
|
||||
if($child instanceof HtmlTag) {
|
||||
$elements = array_merge($elements, $child->getElementsByTagName($tagName));
|
||||
|
||||
if(mb_strtolower($child->getTagName()) === $tagName)
|
||||
$elements[] = $child;
|
||||
}
|
||||
}
|
||||
|
||||
return $elements;
|
||||
}
|
||||
public function getElementsByClassName(string $className): array {
|
||||
$elements = [];
|
||||
|
||||
foreach($this->children as $child) {
|
||||
if($child instanceof HtmlTag) {
|
||||
$elements = array_merge($elements, $child->getElementsByClassName($className));
|
||||
|
||||
$classList = explode(' ', $child->getAttribute('class') ?? '');
|
||||
if(in_array($className, $classList))
|
||||
$elements[] = $child;
|
||||
}
|
||||
}
|
||||
|
||||
return $elements;
|
||||
}
|
||||
public function getElementById(string $idString): ?HtmlTag {
|
||||
foreach($this->children as $child) {
|
||||
if($child instanceof HtmlTag) {
|
||||
if($child->getAttribute('id') == $idString)
|
||||
return $child;
|
||||
|
||||
$element = $child->getElementById($idString);
|
||||
|
||||
if($element !== null)
|
||||
return $element;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function asHTML(): string {
|
||||
$attrs = '';
|
||||
$children = '';
|
||||
|
||||
foreach($this->attributes as $key => $val) {
|
||||
$attrs .= sprintf(' %s', $key);
|
||||
|
||||
if($key !== $val)
|
||||
$attrs .= sprintf('="%s"', htmlspecialchars($val));
|
||||
}
|
||||
|
||||
if($this->selfClosing)
|
||||
return sprintf('<%s%s/>', $this->getTagName(), $attrs);
|
||||
|
||||
foreach($this->children as $child)
|
||||
$children .= $child->asHTML();
|
||||
|
||||
return sprintf('<%1$s%2$s>%3$s</%1$s>', $this->getTagName(), $attrs, $children);
|
||||
}
|
||||
}
|
14
src/HtmlText.php
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
namespace YTKNS;
|
||||
|
||||
class HtmlText implements HtmlTypeInterface {
|
||||
private string $text = '';
|
||||
|
||||
public function __construct(string $text) {
|
||||
$this->text = $text;
|
||||
}
|
||||
|
||||
public function asHTML(): string {
|
||||
return htmlspecialchars($this->text);
|
||||
}
|
||||
}
|
6
src/HtmlTypeInterface.php
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?php
|
||||
namespace YTKNS;
|
||||
|
||||
interface HtmlTypeInterface {
|
||||
public function asHTML(): string;
|
||||
}
|
61
src/PageBuilder.php
Normal file
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
namespace YTKNS;
|
||||
|
||||
final class PageBuilder {
|
||||
private $tagHtml;
|
||||
private $tagHead;
|
||||
private $tagBody;
|
||||
private $tagTitle;
|
||||
private $tagContainer;
|
||||
|
||||
public function __construct(string $pageTitle) {
|
||||
$sharedCss = '//' . Config::get('domain.main') . '/assets/shared.css?v=' . hash_file('md5', YTKNS_PUB . '/assets/shared.css');
|
||||
$sharedJs = '//' . Config::get('domain.main') . '/assets/shared.js?v=' . hash_file('md5', YTKNS_PUB . '/assets/shared.js');
|
||||
|
||||
$this->tagHtml = new HtmlTag('html');
|
||||
$this->tagHtml->appendChild($this->tagHead = new HtmlTag('head'));
|
||||
$this->tagHtml->appendChild($this->tagBody = new HtmlTag('body'));
|
||||
$this->tagHead->appendChild(new HtmlTag('meta', ['charset' => 'utf-8'], true));
|
||||
$this->tagHead->appendChild(new HtmlTag('meta', ['name' => 'viewport', 'content' => 'width=device-width, initial-scale=1'], true));
|
||||
$this->tagHead->appendChild($this->tagTitle = new HtmlTag('title'));
|
||||
$this->tagHead->appendChild(new HtmlTag('link', ['href' => $sharedCss, 'type' => 'text/css', 'rel' => 'stylesheet'], true));
|
||||
$this->tagHead->appendChild(new HtmlTag('script', ['charset' => 'utf-8', 'src' => $sharedJs]));
|
||||
$this->tagBody->appendChild($this->tagContainer = new HtmlTag('div', ['id' => 'container']));
|
||||
$this->setTitle($pageTitle);
|
||||
}
|
||||
|
||||
public function getHtml(): HtmlTag {
|
||||
return $this->tagHtml;
|
||||
}
|
||||
public function getHead(): HtmlTag {
|
||||
return $this->tagHead;
|
||||
}
|
||||
public function getBody(): HtmlTag {
|
||||
return $this->tagBody;
|
||||
}
|
||||
public function getContainer(): HtmlTag {
|
||||
return $this->tagContainer;
|
||||
}
|
||||
|
||||
public function setTitle(string $title): void {
|
||||
$this->tagTitle->setTextContent($title);
|
||||
}
|
||||
|
||||
public function getElementsByTagName(string $tagName): array {
|
||||
return $this->tagHtml->getElementsByTagName($tagName);
|
||||
}
|
||||
public function getElementsByClassName(string $className): array {
|
||||
return $this->tagHtml->getElementsByClassName($className);
|
||||
}
|
||||
public function getElementById(string $idString): ?HtmlTag {
|
||||
return $this->tagHtml->getElementById($idString);
|
||||
}
|
||||
|
||||
public function serialise(): string {
|
||||
return '<!doctype html>' . $this->tagHtml->asHTML();
|
||||
}
|
||||
|
||||
public function __toString(): string {
|
||||
return $this->serialise();
|
||||
}
|
||||
}
|
14
src/PageEffectInterface.php
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
namespace YTKNS;
|
||||
|
||||
use Exception;
|
||||
|
||||
class PageEffectException extends Exception {};
|
||||
|
||||
interface PageEffectInterface {
|
||||
public function setEffectParams(array $vars, bool $quiet = false): void;
|
||||
public function getEffectParams(): array;
|
||||
public function getEffectName(): string;
|
||||
public function getEffectProperties(): array;
|
||||
public function applyEffect(PageBuilder $builder): void;
|
||||
}
|
40
src/Template.php
Normal file
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
namespace YTKNS;
|
||||
|
||||
final class Template {
|
||||
private static array $vars = [];
|
||||
|
||||
public static function setVar(string $name, $value) {
|
||||
return $vars[$name] = $value;
|
||||
}
|
||||
|
||||
public static function prefixArray(array $input): array {
|
||||
$output = [];
|
||||
|
||||
foreach($input as $key => $val)
|
||||
$output[':' . $key] = $val;
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
public static function renderRaw(string $path, array $vars = []): string {
|
||||
$vars = self::prefixArray(array_merge(self::$vars, $vars));
|
||||
$tpl = file_get_contents(YTKNS_TPL . '/' . $path . '.html');
|
||||
$tpl = strtr($tpl, $vars);
|
||||
return $tpl;
|
||||
}
|
||||
|
||||
public static function render(string $path, array $vars = []): void {
|
||||
echo self::renderRaw($path, $vars);
|
||||
}
|
||||
|
||||
public static function renderSet(string $path, array $varSets = []): string {
|
||||
$html = '';
|
||||
$tpl = file_get_contents(YTKNS_TPL . '/' . $path . '.html');
|
||||
|
||||
foreach($varSets as $vars)
|
||||
$html .= strtr($tpl, self::prefixArray($vars));
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
276
src/Upload.php
Normal file
|
@ -0,0 +1,276 @@
|
|||
<?php
|
||||
namespace YTKNS;
|
||||
|
||||
use Exception;
|
||||
|
||||
class UploadNotFoundException extends Exception {};
|
||||
class UploadCreationFailedException extends Exception {};
|
||||
|
||||
final class Upload {
|
||||
private const ID_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
|
||||
|
||||
public function getId(): string {
|
||||
return $this->upload_id;
|
||||
}
|
||||
|
||||
public function getPath(): string {
|
||||
return YTKNS_UPLOADS . '/' . $this->getId();
|
||||
}
|
||||
public function getUrl(): string {
|
||||
return 'https://' . Config::get('domain.main') . '/uploads/' . $this->getId();
|
||||
}
|
||||
|
||||
public function getUserId(): int {
|
||||
return $this->user_id;
|
||||
}
|
||||
public function setUserId(int $userId): void {
|
||||
$this->user_id = $userId;
|
||||
}
|
||||
|
||||
public function getType(): string {
|
||||
return $this->upload_type ?? 'text/plain';
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
return $this->upload_name ?? '';
|
||||
}
|
||||
|
||||
public function getHash(): string {
|
||||
return $this->upload_hash ?? str_pad('', 64, '0');
|
||||
}
|
||||
|
||||
public function getUser(): User {
|
||||
return User::byId($this->getUserId());
|
||||
}
|
||||
|
||||
public function getUseCount(): int {
|
||||
return $this->upload_use_count ?? 0;
|
||||
}
|
||||
|
||||
public function getCreated(): int {
|
||||
return $this->upload_created;
|
||||
}
|
||||
|
||||
public function getlastUsed(): ?int {
|
||||
return $this->upload_last_used;
|
||||
}
|
||||
|
||||
public function getDeleted(): ?int {
|
||||
return $this->upload_deleted;
|
||||
}
|
||||
|
||||
public function getDMCA(): ?int {
|
||||
return $this->upload_dmca;
|
||||
}
|
||||
|
||||
public function delete(bool $hard): void {
|
||||
if($hard) {
|
||||
if(is_file($this->getPath()))
|
||||
unlink($this->getPath());
|
||||
|
||||
if($this->getDMCA() < 1) {
|
||||
$delete = DB::prepare('
|
||||
DELETE FROM `ytkns_uploads`
|
||||
WHERE `upload_id` = :id
|
||||
');
|
||||
$delete->bindValue('id', $this->getId());
|
||||
$delete->execute();
|
||||
}
|
||||
} else {
|
||||
$delete = DB::prepare('
|
||||
UPDATE `ytkns_uploads`
|
||||
SET `upload_deleted` = NOW()
|
||||
WHERE `upload_id` = :id
|
||||
');
|
||||
$delete->bindValue('id', $this->getId());
|
||||
$delete->execute();
|
||||
}
|
||||
}
|
||||
|
||||
public function toJson(bool $asString = false) {
|
||||
$uploadInfo = [
|
||||
'id' => $this->getId(),
|
||||
'name' => $this->getName(),
|
||||
'type' => $this->getType(),
|
||||
'user' => $this->getUserId(),
|
||||
'uses' => $this->getUseCount(),
|
||||
'hash' => $this->getHash(),
|
||||
'created' => $this->getCreated(),
|
||||
'last_used' => $this->getLastUsed(),
|
||||
'deleted' => $this->getDeleted(),
|
||||
'dmca' => $this->getDMCA(),
|
||||
];
|
||||
|
||||
if($asString)
|
||||
$uploadInfo = json_encode($uploadInfo);
|
||||
|
||||
return $uploadInfo;
|
||||
}
|
||||
|
||||
public static function generateId(int $length = 16): string {
|
||||
$token = random_bytes($length);
|
||||
$chars = strlen(self::ID_CHARS);
|
||||
|
||||
for($i = 0; $i < $length; $i++)
|
||||
$token[$i] = self::ID_CHARS[ord($token[$i]) % $chars];
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
public static function create(User $user, string $fileName, string $fileType, string $fileHash): self {
|
||||
$id = self::generateId();
|
||||
$create = DB::prepare('
|
||||
INSERT INTO `ytkns_uploads` (
|
||||
`upload_id`, `user_id`, `upload_ip`, `upload_name`, `upload_type`, `upload_hash`
|
||||
) VALUES (
|
||||
:id, :user, INET6_ATON(:ip), :name, :type, UNHEX(:hash)
|
||||
)
|
||||
');
|
||||
$create->bindValue('id', $id);
|
||||
$create->bindValue('user', $user->getId());
|
||||
$create->bindValue('ip', $_SERVER['REMOTE_ADDR']);
|
||||
$create->bindValue('name', $fileName);
|
||||
$create->bindValue('type', $fileType);
|
||||
$create->bindValue('hash', $fileHash);
|
||||
$create->execute();
|
||||
|
||||
try {
|
||||
return self::byId($id);
|
||||
} catch(UploadNotFoundException $ex) {
|
||||
throw new UploadCreationFailedException;
|
||||
}
|
||||
}
|
||||
|
||||
public static function byId(string $id): self {
|
||||
$getUpload = DB::prepare('
|
||||
SELECT `upload_id`, `user_id`, `upload_use_count`, `upload_name`, `upload_type`,
|
||||
UNIX_TIMESTAMP(`upload_created`) AS `upload_created`,
|
||||
UNIX_TIMESTAMP(`upload_last_used`) AS `upload_last_used`,
|
||||
UNIX_TIMESTAMP(`upload_deleted`) AS `upload_deleted`,
|
||||
UNIX_TIMESTAMP(`upload_dmca`) AS `upload_dmca`,
|
||||
INET6_NTOA(`upload_ip`) AS `upload_ip`,
|
||||
LOWER(HEX(`upload_hash`)) AS `upload_hash`
|
||||
FROM `ytkns_uploads`
|
||||
WHERE `upload_id` = :id
|
||||
AND `upload_deleted` IS NULL
|
||||
');
|
||||
$getUpload->bindValue('id', $id);
|
||||
$upload = $getUpload->execute() ? $getUpload->fetchObject(self::class) : false;
|
||||
|
||||
if(!$upload)
|
||||
throw new UploadNotFoundException;
|
||||
|
||||
return $upload;
|
||||
}
|
||||
|
||||
public static function byHash(string $hash): ?self {
|
||||
$getUpload = DB::prepare('
|
||||
SELECT `upload_id`, `user_id`, `upload_use_count`, `upload_name`, `upload_type`,
|
||||
UNIX_TIMESTAMP(`upload_created`) AS `upload_created`,
|
||||
UNIX_TIMESTAMP(`upload_last_used`) AS `upload_last_used`,
|
||||
UNIX_TIMESTAMP(`upload_deleted`) AS `upload_deleted`,
|
||||
UNIX_TIMESTAMP(`upload_dmca`) AS `upload_dmca`,
|
||||
INET6_NTOA(`upload_ip`) AS `upload_ip`,
|
||||
LOWER(HEX(`upload_hash`)) AS `upload_hash`
|
||||
FROM `ytkns_uploads`
|
||||
WHERE `upload_hash` = UNHEX(:hash)
|
||||
AND `upload_deleted` IS NULL
|
||||
');
|
||||
$getUpload->bindValue('hash', $hash);
|
||||
$upload = $getUpload->execute() ? $getUpload->fetchObject(self::class) : false;
|
||||
return $upload ? $upload : null;
|
||||
}
|
||||
|
||||
public static function deleted(): array {
|
||||
$getDeleted = DB::prepare('
|
||||
SELECT `upload_id`, `user_id`, `upload_use_count`, `upload_name`, `upload_type`,
|
||||
UNIX_TIMESTAMP(`upload_created`) AS `upload_created`,
|
||||
UNIX_TIMESTAMP(`upload_last_used`) AS `upload_last_used`,
|
||||
UNIX_TIMESTAMP(`upload_deleted`) AS `upload_deleted`,
|
||||
UNIX_TIMESTAMP(`upload_dmca`) AS `upload_dmca`,
|
||||
INET6_NTOA(`upload_ip`) AS `upload_ip`,
|
||||
LOWER(HEX(`upload_hash`)) AS `upload_hash`
|
||||
FROM `ytkns_uploads`
|
||||
WHERE `upload_deleted` IS NOT NULL
|
||||
OR `upload_dmca` IS NOT NULL
|
||||
');
|
||||
if(!$getDeleted->execute())
|
||||
return [];
|
||||
|
||||
$deleted = [];
|
||||
|
||||
while($upload = $getDeleted->fetchObject(self::class))
|
||||
$deleted[] = $upload;
|
||||
|
||||
return $deleted;
|
||||
}
|
||||
|
||||
public static function purgeOrphans(): void {
|
||||
DB::exec('
|
||||
UPDATE `ytkns_uploads`
|
||||
SET `upload_deleted` = NOW()
|
||||
WHERE `upload_use_count` < 1
|
||||
AND (`upload_created` + INTERVAL 1 DAY) < NOW()
|
||||
AND `upload_dmca` IS NULL
|
||||
');
|
||||
|
||||
$orphans = self::deleted();
|
||||
|
||||
foreach($orphans as $orphan)
|
||||
$orphan->delete(true);
|
||||
}
|
||||
|
||||
public static function resync(array $uploadFields): void {
|
||||
if(empty($uploadFields))
|
||||
return;
|
||||
|
||||
$effectNames = array_keys($uploadFields);
|
||||
$fetchEffectsWhereIn = range(0, count($effectNames) - 1);
|
||||
array_walk($fetchEffectsWhereIn, function(&$i, $k, $v) { $i = $v . $i; }, ':field_');
|
||||
|
||||
$fetchEffects = DB::prepare(sprintf('
|
||||
SELECT `effect_name`, `effect_params`
|
||||
FROM `ytkns_zones_effects`
|
||||
WHERE `effect_name` IN (%s)
|
||||
', implode(', ', $fetchEffectsWhereIn)));
|
||||
|
||||
foreach($fetchEffectsWhereIn as $index => $name)
|
||||
$fetchEffects->bindValue($name, $effectNames[$index]);
|
||||
|
||||
$effectsWithUploads = $fetchEffects->execute() ? $fetchEffects->fetchAll(\PDO::FETCH_OBJ) : false;
|
||||
$effectsWithUploads = $effectsWithUploads ? $effectsWithUploads : [];
|
||||
|
||||
$uploadUseCounts = [];
|
||||
|
||||
foreach($effectsWithUploads as $effectWithUploads) {
|
||||
if(!array_key_exists($effectWithUploads->effect_name, $uploadFields))
|
||||
continue;
|
||||
|
||||
$effectParams = json_decode($effectWithUploads->effect_params, true);
|
||||
|
||||
foreach($uploadFields[$effectWithUploads->effect_name] as $uploadField) {
|
||||
if(empty($uploadId = $effectParams[$uploadField]))
|
||||
continue;
|
||||
|
||||
if(!isset($uploadUseCounts[$uploadId]))
|
||||
$uploadUseCounts[$uploadId] = 1;
|
||||
else
|
||||
$uploadUseCounts[$uploadId] += 1;
|
||||
}
|
||||
}
|
||||
|
||||
$updateUploadUseCount = DB::prepare('
|
||||
UPDATE `ytkns_uploads`
|
||||
SET `upload_use_count` = :count
|
||||
WHERE `upload_id` = :id
|
||||
');
|
||||
|
||||
DB::exec('UPDATE `ytkns_uploads` SET `upload_use_count` = 0');
|
||||
|
||||
foreach($uploadUseCounts as $uploadId => $uploadUseCount) {
|
||||
$updateUploadUseCount->bindValue('id', $uploadId);
|
||||
$updateUploadUseCount->bindValue('count', $uploadUseCount);
|
||||
$updateUploadUseCount->execute();
|
||||
}
|
||||
}
|
||||
}
|
123
src/User.php
Normal file
|
@ -0,0 +1,123 @@
|
|||
<?php
|
||||
namespace YTKNS;
|
||||
|
||||
use Exception;
|
||||
|
||||
class UserNotFoundException extends Exception {}
|
||||
class UserCreationFailedException extends Exception {}
|
||||
class UserCreationInvalidNameException extends Exception {}
|
||||
class UserCreationInvalidPasswordException extends Exception {}
|
||||
class UserCreationInvalidMailException extends Exception {}
|
||||
|
||||
class User {
|
||||
public function __construct() {
|
||||
}
|
||||
|
||||
public function getId(): int {
|
||||
return $this->user_id ?? 0;
|
||||
}
|
||||
private function setId(int $userId): void {
|
||||
$this->user_id = $userId;
|
||||
}
|
||||
|
||||
public function getUsername(): string {
|
||||
return $this->username ?? '';
|
||||
}
|
||||
|
||||
public static function byId(int $userId): self {
|
||||
$getUser = DB::prepare('
|
||||
SELECT `user_id`, `username`, `email`, `password`, `user_created`
|
||||
FROM `ytkns_users`
|
||||
WHERE `user_id` = :user
|
||||
');
|
||||
$getUser->bindValue('user', $userId);
|
||||
$getUser->execute();
|
||||
$user = $getUser->fetchObject(self::class);
|
||||
|
||||
if($user === false)
|
||||
throw new UserNotFoundException;
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
public static function forLogin(string $usernameOrEmail): self {
|
||||
$getUser = DB::prepare('
|
||||
SELECT `user_id`, `username`, `email`, `password`, `user_created`
|
||||
FROM `ytkns_users`
|
||||
WHERE LOWER(`username`) = LOWER(:username)
|
||||
OR LOWER(`email`) = LOWER(:email)
|
||||
');
|
||||
$getUser->bindValue('username', $usernameOrEmail);
|
||||
$getUser->bindValue('email', $usernameOrEmail);
|
||||
$getUser->execute();
|
||||
$user = $getUser->fetchObject(self::class);
|
||||
|
||||
if($user === false)
|
||||
throw new UserNotFoundException;
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
public static function forProfile(string $username): self {
|
||||
$getUser = DB::prepare('
|
||||
SELECT `user_id`, `username`, `email`, `password`, `user_created`
|
||||
FROM `ytkns_users`
|
||||
WHERE LOWER(`username`) = LOWER(:username)
|
||||
');
|
||||
$getUser->bindValue('username', $username);
|
||||
$getUser->execute();
|
||||
$user = $getUser->fetchObject(self::class);
|
||||
|
||||
if($user === false)
|
||||
throw new UserNotFoundException;
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
public static function validatePassword(string $password): bool {
|
||||
$chars = [];
|
||||
$length = mb_strlen($password);
|
||||
|
||||
for($i = 0; $i < $length; $i++) {
|
||||
$current = mb_substr($password, $i, 1);
|
||||
|
||||
if(!in_array($current, $chars, true))
|
||||
$chars[] = $current;
|
||||
}
|
||||
|
||||
return count($chars) >= 6;
|
||||
}
|
||||
|
||||
public static function hashPassword(string $password): string {
|
||||
return password_hash($password, PASSWORD_ARGON2ID);
|
||||
}
|
||||
|
||||
public static function create(string $userName, string $password, string $email): self {
|
||||
if(!preg_match('#^([a-zA-Z0-9-_]{1,20})$#', $userName))
|
||||
throw new UserCreationInvalidNameException;
|
||||
if(!filter_var($email, FILTER_VALIDATE_EMAIL))
|
||||
throw new UserCreationInvalidMailException;
|
||||
if(!self::validatePassword($password))
|
||||
throw new UserCreationInvalidPasswordException;
|
||||
|
||||
$password = self::hashPassword($password);
|
||||
|
||||
$createUser = DB::prepare('
|
||||
INSERT INTO `ytkns_users` (
|
||||
`username`, `email`, `password`
|
||||
) VALUES (
|
||||
:username, :email, :password
|
||||
)
|
||||
');
|
||||
$createUser->bindValue('username', $userName);
|
||||
$createUser->bindValue('email', $email);
|
||||
$createUser->bindValue('password', $password);
|
||||
$userId = $createUser->execute() ? (int)DB::lastInsertId() : 0;
|
||||
|
||||
try {
|
||||
return self::byId($userId);
|
||||
} catch(UserNotFoundException $ex) {
|
||||
throw new UserCreationFailedException;
|
||||
}
|
||||
}
|
||||
}
|
122
src/UserInvite.php
Normal file
|
@ -0,0 +1,122 @@
|
|||
<?php
|
||||
namespace YTKNS;
|
||||
|
||||
use Exception;
|
||||
|
||||
class UserInviteNotFoundException extends Exception {}
|
||||
class UserInviteCreationFailedException extends Exception {}
|
||||
|
||||
final class UserInvite {
|
||||
private const TOKEN_LENGTH = 16;
|
||||
|
||||
public function getToken(): string {
|
||||
return $this->invite_token;
|
||||
}
|
||||
|
||||
public function getCreatorId(): int {
|
||||
return $this->created_by ?? 0;
|
||||
}
|
||||
public function getCreator(): User {
|
||||
return User::byId($this->getCreatedById());
|
||||
}
|
||||
|
||||
public function getUserId(): ?int {
|
||||
return $this->used_by ?? null;
|
||||
}
|
||||
public function getUser(): ?User {
|
||||
$userId = $this->getUserId();
|
||||
|
||||
if($userId === null)
|
||||
return null;
|
||||
|
||||
try {
|
||||
return User::byId($this->getUserId());
|
||||
} catch(UserNotFoundException $ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function getCreated(): int {
|
||||
return $this->invite_created ?? 0;
|
||||
}
|
||||
|
||||
public function getUsed(): ?int {
|
||||
return $this->invite_used ?? null;
|
||||
}
|
||||
|
||||
public function isUsed(): bool {
|
||||
return !empty($this->used_by) || !empty($this->invite_used);
|
||||
}
|
||||
|
||||
public function markUsed(User $user): void {
|
||||
$markUsed = DB::prepare('
|
||||
UPDATE `ytkns_users_invites`
|
||||
SET `used_by` = :user
|
||||
WHERE `invite_token` = UNHEX(:token)
|
||||
');
|
||||
$markUsed->bindValue('user', $user->getId());
|
||||
$markUsed->bindValue('token', $this->getToken());
|
||||
$markUsed->execute();
|
||||
}
|
||||
|
||||
public static function fromCreator(User $creator): array {
|
||||
$getInvites = DB::prepare('
|
||||
SELECT `created_by`, `used_by`,
|
||||
LOWER(HEX(`invite_token`)) AS `invite_token`,
|
||||
UNIX_TIMESTAMP(`invite_created`) AS `invite_created`,
|
||||
UNIX_TIMESTAMP(`invite_used`) AS `invite_used`
|
||||
FROM `ytkns_users_invites`
|
||||
WHERE `created_by` = :creator
|
||||
');
|
||||
$getInvites->bindValue('creator', $creator->getId());
|
||||
$getInvites->execute();
|
||||
$invites = [];
|
||||
|
||||
while($invite = $getInvites->fetchObject(self::class))
|
||||
$invites[] = $invite;
|
||||
|
||||
return $invites;
|
||||
}
|
||||
|
||||
public static function byToken(string $token, bool $isHex = true): self {
|
||||
$getInvite = DB::prepare(sprintf('
|
||||
SELECT `created_by`, `used_by`,
|
||||
LOWER(HEX(`invite_token`)) AS `invite_token`,
|
||||
UNIX_TIMESTAMP(`invite_created`) AS `invite_created`,
|
||||
UNIX_TIMESTAMP(`invite_used`) AS `invite_used`
|
||||
FROM `ytkns_users_invites`
|
||||
WHERE `invite_token` = %s(:token)
|
||||
', $isHex ? 'UNHEX' : ''));
|
||||
$getInvite->bindValue('token', $token);
|
||||
$invite = $getInvite->execute() ? $getInvite->fetchObject(self::class) : false;
|
||||
|
||||
if(!$invite)
|
||||
throw new UserInviteNotFoundException;
|
||||
|
||||
return $invite;
|
||||
}
|
||||
|
||||
public static function generateToken(): string {
|
||||
return bin2hex(random_bytes(self::TOKEN_LENGTH));
|
||||
}
|
||||
|
||||
public static function create(User $creator): self {
|
||||
$inviteToken = self::generateToken();
|
||||
$insertInvite = DB::prepare('
|
||||
INSERT INTO `ytkns_users_invites` (
|
||||
`invite_token`, `created_by`
|
||||
) VALUES (
|
||||
UNHEX(:token), :creator
|
||||
)
|
||||
');
|
||||
$insertInvite->bindValue('token', $inviteToken);
|
||||
$insertInvite->bindValue('creator', $creator->getId());
|
||||
$insertInvite->execute();
|
||||
|
||||
try {
|
||||
return self::byToken($inviteToken);
|
||||
} catch(UserInviteNotFoundException $ex) {
|
||||
throw new UserInviteCreationFailedException;
|
||||
}
|
||||
}
|
||||
}
|
149
src/UserSession.php
Normal file
|
@ -0,0 +1,149 @@
|
|||
<?php
|
||||
namespace YTKNS;
|
||||
|
||||
use Exception;
|
||||
|
||||
final class UserSessionNotFoundException extends Exception {};
|
||||
final class UserSessionCreatedFailedException extends Exception {};
|
||||
|
||||
class UserSession {
|
||||
private const TOKEN_CHARS = 'abcdefghijklmnopqrstuvwxyz-0123456789_ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
private static $instance = null;
|
||||
|
||||
public static function instance(): ?self {
|
||||
return self::$instance;
|
||||
}
|
||||
public function setInstance(): void {
|
||||
self::$instance = $this;
|
||||
}
|
||||
public static function hasInstance(): bool {
|
||||
return !empty(self::$instance->session_token);
|
||||
}
|
||||
public static function unsetInstance(): void {
|
||||
self::$instance = null;
|
||||
}
|
||||
|
||||
public function __construct() {
|
||||
}
|
||||
|
||||
public function getUserId(): int {
|
||||
return $this->user_id ?? 0;
|
||||
}
|
||||
public function getUser(): User {
|
||||
return User::byId($this->getUserId());
|
||||
}
|
||||
|
||||
public function getToken(): string {
|
||||
return $this->session_token;
|
||||
}
|
||||
public function getSmallToken(int $rounds = 5, int $length = 8, int $offset = 0): string {
|
||||
$token = $this->getToken();
|
||||
$tokenLength = strlen($token) - $length;
|
||||
|
||||
for($i = 0; $i < $rounds; $i++)
|
||||
$offset = ord($token[$offset]) % $tokenLength;
|
||||
|
||||
return str_rot13(substr($token, $offset, $length));
|
||||
}
|
||||
|
||||
public function getCreated(): int {
|
||||
return $this->session_created ?? 0;
|
||||
}
|
||||
|
||||
public function getExpires(): int {
|
||||
return $this->session_expires ?? 0;
|
||||
}
|
||||
|
||||
public function getBump(): bool {
|
||||
return $this->session_bump ?? false;
|
||||
}
|
||||
|
||||
public function getFirstIp(): string {
|
||||
return $this->session_ip_first ?? '';
|
||||
}
|
||||
|
||||
public function getLastIp(): ?string {
|
||||
return $this->session_ip_last ?? null;
|
||||
}
|
||||
|
||||
public function update(): void {
|
||||
$update = DB::prepare('
|
||||
UPDATE `ytkns_users_sessions`
|
||||
SET `session_expires` = IF(:bump, NOW() + INTERVAL 1 MONTH, `session_expires`),
|
||||
`session_ip_last` = INET6_ATON(:ip),
|
||||
`session_used` = NOW()
|
||||
WHERE `session_token` = :token
|
||||
');
|
||||
$update->bindValue('bump', $this->getBump() ? 1 : 0);
|
||||
$update->bindValue('ip', $_SERVER['REMOTE_ADDR']);
|
||||
$update->bindValue('token', $this->getToken());
|
||||
$update->execute();
|
||||
}
|
||||
|
||||
public function destroy(): void {
|
||||
$destroy = DB::prepare('
|
||||
DELETE FROM `ytkns_users_sessions`
|
||||
WHERE `session_token` = :token
|
||||
');
|
||||
$destroy->bindValue('token', $this->getToken());
|
||||
$destroy->execute();
|
||||
}
|
||||
|
||||
public static function generateToken(int $length = 64): string {
|
||||
$token = random_bytes($length);
|
||||
$chars = strlen(self::TOKEN_CHARS);
|
||||
|
||||
for($i = 0; $i < $length; $i++)
|
||||
$token[$i] = self::TOKEN_CHARS[ord($token[$i]) % $chars];
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
public static function create(User $user, bool $bump = false): self {
|
||||
$token = self::generateToken();
|
||||
$create = DB::prepare('
|
||||
INSERT INTO `ytkns_users_sessions` (
|
||||
`user_id`, `session_token`, `session_ip_first`, `session_bump`
|
||||
) VALUES (
|
||||
:user, :token, INET6_ATON(:ip), :bump
|
||||
)
|
||||
');
|
||||
$create->bindValue('user', $user->getId());
|
||||
$create->bindValue('token', $token);
|
||||
$create->bindValue('ip', $_SERVER['REMOTE_ADDR']);
|
||||
$create->bindValue('bump', $bump ? 1 : 0);
|
||||
$create->execute();
|
||||
|
||||
try {
|
||||
return self::byToken($token);
|
||||
} catch(UserSessionNotFoundException $ex) {
|
||||
throw new UserSessionCreatedFailedException;
|
||||
}
|
||||
}
|
||||
|
||||
public static function byToken(string $token): self {
|
||||
$getSession = DB::prepare('
|
||||
SELECT `user_id`, `session_token`, `session_bump`,
|
||||
UNIX_TIMESTAMP(`session_created`) AS `session_created`,
|
||||
UNIX_TIMESTAMP(`session_expires`) AS `session_expires`,
|
||||
INET6_NTOA(`session_ip_first`) AS `session_ip_first`,
|
||||
INET6_NTOA(`session_ip_last`) AS `session_ip_last`
|
||||
FROM `ytkns_users_sessions`
|
||||
WHERE `session_token` = :token
|
||||
');
|
||||
$getSession->bindValue('token', $token);
|
||||
$session = $getSession->execute() ? $getSession->fetchObject(self::class) : false;
|
||||
|
||||
if(!$session)
|
||||
throw new UserSessionNotFoundException;
|
||||
|
||||
return $session;
|
||||
}
|
||||
|
||||
public static function purge(): void {
|
||||
DB::exec('
|
||||
DELETE FROM `ytkns_users_sessions`
|
||||
WHERE `session_expires` <= NOW()
|
||||
');
|
||||
}
|
||||
}
|
361
src/Zone.php
Normal file
|
@ -0,0 +1,361 @@
|
|||
<?php
|
||||
namespace YTKNS;
|
||||
|
||||
use Exception;
|
||||
|
||||
class ZoneNotFoundException extends Exception {};
|
||||
class ZoneCreationFailedException extends Exception {};
|
||||
class ZoneInvalidIdException extends Exception {};
|
||||
class ZoneInvalidNameException extends Exception {};
|
||||
class ZoneInvalidTitleException extends Exception {};
|
||||
|
||||
final class Zone {
|
||||
private $effects = null;
|
||||
private $passiveMode = false;
|
||||
|
||||
public function __construct() {
|
||||
}
|
||||
|
||||
public function setPassiveMode(): void {
|
||||
$this->passiveMode = true;
|
||||
|
||||
if($this->effects === null)
|
||||
$this->effects = [];
|
||||
}
|
||||
|
||||
public function getId(): int {
|
||||
return $this->zone_id ?? 0;
|
||||
}
|
||||
public function hasId(): bool {
|
||||
return isset($this->zone_id) && $this->zone_id > 0;
|
||||
}
|
||||
private function setId(int $id): void {
|
||||
if($id < 1)
|
||||
throw new ZoneInvalidIdException;
|
||||
|
||||
$this->zone_id = $id;
|
||||
}
|
||||
|
||||
public function getUserId(): int {
|
||||
return $this->user_id ?? 0;
|
||||
}
|
||||
public function setUserId(int $userId): void {
|
||||
$this->user_id = $userId;
|
||||
}
|
||||
|
||||
private $userObj = null;
|
||||
public function getUser(): User {
|
||||
if($this->userObj === null)
|
||||
$this->userObj = User::byId($this->getUserId());
|
||||
|
||||
return $this->userObj;
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
return $this->zone_name;
|
||||
}
|
||||
public function setName(string $name): void {
|
||||
if(!self::validName($name))
|
||||
throw new ZoneInvalidNameException;
|
||||
|
||||
$this->zone_name = $name;
|
||||
}
|
||||
|
||||
public function getUrl(): string {
|
||||
return 'https://' . sprintf(Config::get('domain.zone'), $this->getName());
|
||||
}
|
||||
public function getUrlForPreview(): string {
|
||||
return $this->getUrl() . '?preview=1';
|
||||
}
|
||||
|
||||
public function getScreenshotPath(): string {
|
||||
return YTKNS_SCREENSHOTS . '/' . $this->getName() . '.jpg';
|
||||
}
|
||||
public function getScreenshotUrl(): string {
|
||||
return 'https://' . Config::get('domain.main') . '/ss/' . $this->getName() . '.jpg';
|
||||
}
|
||||
public function takeScreenshot(): void {
|
||||
$path = escapeshellarg($this->getScreenshotPath());
|
||||
$url = escapeshellarg($this->getUrlForPreview());
|
||||
system(sprintf('/usr/bin/firefox --window-size=800,600 --screenshot %s %s', $path, $url));
|
||||
//system(sprintf('/usr/bin/convert %1$s 200x150 %1$s', $path));
|
||||
|
||||
}
|
||||
public function removeScreenshot(): void {
|
||||
$path = $this->getScreenshotPath();
|
||||
|
||||
if(is_file($path))
|
||||
unlink($path);
|
||||
}
|
||||
|
||||
public function getTitle(): string {
|
||||
return $this->zone_title;
|
||||
}
|
||||
public function setTitle(string $title): void {
|
||||
if(strlen($title) > 255)
|
||||
throw new ZoneInvalidTitleException;
|
||||
|
||||
$this->zone_title = $title;
|
||||
}
|
||||
|
||||
public function getViews(): int {
|
||||
return $this->zone_views ?? 0;
|
||||
}
|
||||
|
||||
public function incrementViews(): void {
|
||||
$updateViews = DB::prepare('
|
||||
UPDATE `ytkns_zones`
|
||||
SET `zone_views` = `zone_views` + 1
|
||||
WHERE `zone_id` = :zone
|
||||
');
|
||||
$updateViews->bindValue('zone', $this->getId());
|
||||
if($updateViews->execute())
|
||||
$this->zone_views = $this->getViews() + 1;
|
||||
}
|
||||
|
||||
public function getEffects(): array {
|
||||
if(!is_array($this->effects))
|
||||
$this->effects = $this->hasId() ? ZoneEffect::byZone($this) : [];
|
||||
|
||||
return $this->effects;
|
||||
}
|
||||
|
||||
public function getPageBuilder(bool $quiet = false): PageBuilder {
|
||||
$pageBuilder = new PageBuilder($this->getTitle());
|
||||
|
||||
$effects = $this->getEffects();
|
||||
|
||||
foreach($effects as $effect)
|
||||
$effect->applyEffect($pageBuilder, $quiet);
|
||||
|
||||
return $pageBuilder;
|
||||
}
|
||||
|
||||
public function addEffect(PageEffectInterface $effect): void {
|
||||
if(is_array($this->effects))
|
||||
$this->effects[] = $effect;
|
||||
|
||||
if(!$this->passiveMode)
|
||||
$this->addEffectDatabase($effect);
|
||||
}
|
||||
private function addEffectDatabase(PageEffectInterface $effect): void {
|
||||
if(!$this->hasId())
|
||||
return;
|
||||
|
||||
$insert = DB::prepare('
|
||||
REPLACE INTO `ytkns_zones_effects` (
|
||||
`zone_id`, `effect_name`, `effect_params`
|
||||
) VALUES (
|
||||
:zone, :name, :params
|
||||
)
|
||||
');
|
||||
$insert->bindValue('zone', $this->getId());
|
||||
$insert->bindValue('name', substr(get_class($effect), 14, -6));
|
||||
$insert->bindValue('params', json_encode($effect->getEffectParams()));
|
||||
$insert->execute();
|
||||
}
|
||||
|
||||
public function removeEffects(): void {
|
||||
$this->effects = [];
|
||||
|
||||
if(!$this->passiveMode)
|
||||
$this->removeEffectsDatabase();
|
||||
}
|
||||
private function removeEffectsDatabase(): void {
|
||||
if(!$this->hasId())
|
||||
return;
|
||||
|
||||
$removeEffects = DB::prepare('
|
||||
DELETE FROM `ytkns_zones_effects`
|
||||
WHERE `zone_id` = :zone
|
||||
');
|
||||
$removeEffects->bindValue('zone', $this->getId());
|
||||
$removeEffects->execute();
|
||||
}
|
||||
|
||||
public static function byId(string $id): self {
|
||||
$getZone = DB::prepare('
|
||||
SELECT `zone_id`, `user_id`, `zone_name`, `zone_title`, `zone_views`,
|
||||
UNIX_TIMESTAMP(`zone_created`) AS `zone_created`,
|
||||
UNIX_TIMESTAMP(`zone_updated`) AS `zone_updated`
|
||||
FROM `ytkns_zones`
|
||||
WHERE `zone_id` = :zone
|
||||
');
|
||||
$getZone->bindValue('zone', $id);
|
||||
$zone = $getZone->execute() ? $getZone->fetchObject(self::class) : false;
|
||||
|
||||
if(!$zone)
|
||||
throw new ZoneNotFoundException;
|
||||
|
||||
return $zone;
|
||||
}
|
||||
|
||||
public static function byName(string $name): self {
|
||||
$getZone = DB::prepare('
|
||||
SELECT `zone_id`, `user_id`, `zone_name`, `zone_title`, `zone_views`,
|
||||
UNIX_TIMESTAMP(`zone_created`) AS `zone_created`,
|
||||
UNIX_TIMESTAMP(`zone_updated`) AS `zone_updated`
|
||||
FROM `ytkns_zones`
|
||||
WHERE `zone_name` = :name
|
||||
');
|
||||
$getZone->bindValue('name', $name);
|
||||
$zone = $getZone->execute() ? $getZone->fetchObject(self::class) : false;
|
||||
|
||||
if(!$zone)
|
||||
throw new ZoneNotFoundException;
|
||||
|
||||
return $zone;
|
||||
}
|
||||
|
||||
public static function byUser(User $user, ?string $orderBy = null, bool $ascending = true, int $take = 0, int $offset = 0): array {
|
||||
$getZonesQuery = '
|
||||
SELECT `zone_id`, `user_id`, `zone_name`, `zone_title`, `zone_views`,
|
||||
UNIX_TIMESTAMP(`zone_created`) AS `zone_created`,
|
||||
UNIX_TIMESTAMP(`zone_updated`) AS `zone_updated`
|
||||
FROM `ytkns_zones`
|
||||
WHERE `user_id` = :user
|
||||
';
|
||||
|
||||
if($orderBy !== null)
|
||||
$getZonesQuery .= sprintf('ORDER BY `%s` %s', $orderBy, $ascending ? 'ASC' : 'DESC');
|
||||
if($take > 0)
|
||||
$getZonesQuery .= sprintf(' LIMIT %d OFFSET %d', $take, $offset);
|
||||
|
||||
$getZones = DB::prepare($getZonesQuery);
|
||||
$getZones->bindValue('user', $user->getId());
|
||||
$getZones->execute();
|
||||
$zones = [];
|
||||
|
||||
while($zone = $getZones->fetchObject(self::class))
|
||||
$zones[] = $zone;
|
||||
|
||||
return $zones;
|
||||
}
|
||||
|
||||
public static function all(?string $orderBy = null, bool $ascending = true, int $take = 0, int $offset = 0): array {
|
||||
$getZonesQuery = '
|
||||
SELECT `zone_id`, `user_id`, `zone_name`, `zone_title`, `zone_views`,
|
||||
UNIX_TIMESTAMP(`zone_created`) AS `zone_created`,
|
||||
UNIX_TIMESTAMP(`zone_updated`) AS `zone_updated`
|
||||
FROM `ytkns_zones`
|
||||
';
|
||||
|
||||
if($orderBy !== null)
|
||||
$getZonesQuery .= sprintf(' ORDER BY `%s` %s', $orderBy, $ascending ? 'ASC' : 'DESC');
|
||||
if($take > 0)
|
||||
$getZonesQuery .= sprintf(' LIMIT %d OFFSET %d', $take, $offset);
|
||||
|
||||
$getZones = DB::prepare($getZonesQuery);
|
||||
$getZones->execute();
|
||||
$zones = [];
|
||||
|
||||
while($zone = $getZones->fetchObject(self::class))
|
||||
$zones[] = $zone;
|
||||
|
||||
return $zones;
|
||||
}
|
||||
|
||||
public static function count(): int {
|
||||
$getZoneCount = DB::prepare('
|
||||
SELECT COUNT(`zone_id`)
|
||||
FROM `ytkns_zones`
|
||||
');
|
||||
return (int)($getZoneCount->execute() ? $getZoneCount->fetchColumn() : 0);
|
||||
}
|
||||
|
||||
public static function create(User $user, string $name, string $title): self {
|
||||
$create = DB::prepare('
|
||||
INSERT INTO `ytkns_zones` (
|
||||
`user_id`, `zone_name`, `zone_title`
|
||||
) VALUES (
|
||||
:user, LOWER(:name), :title
|
||||
)
|
||||
');
|
||||
$create->bindValue('user', $user->getId());
|
||||
$create->bindValue('name', $name);
|
||||
$create->bindValue('title', $title);
|
||||
$create->execute();
|
||||
|
||||
try {
|
||||
return self::byName($name);
|
||||
} catch(ZoneNotFoundException $ex) {
|
||||
throw new ZoneCreationFailedException;
|
||||
}
|
||||
}
|
||||
|
||||
public function update(array $save = ['user_id', 'zone_name', 'zone_title']): void {
|
||||
if(!$this->hasId())
|
||||
return;
|
||||
|
||||
$set = [
|
||||
'`zone_updated` = NOW()',
|
||||
];
|
||||
|
||||
foreach($save as $param)
|
||||
$set[] = sprintf('`%1$s` = :%1$s', $param);
|
||||
|
||||
$update = DB::prepare(sprintf('UPDATE `ytkns_zones` SET %s WHERE `zone_id` = :zone', implode(', ', $set)));
|
||||
|
||||
if(in_array('user_id', $save))
|
||||
$update->bindValue('user_id', $this->getUserId());
|
||||
if(in_array('zone_name', $save))
|
||||
$update->bindValue('zone_name', $this->getName());
|
||||
if(in_array('zone_title', $save))
|
||||
$update->bindValue('zone_title', $this->getTitle());
|
||||
|
||||
$update->bindValue('zone', $this->getId());
|
||||
$update->execute();
|
||||
|
||||
if($this->passiveMode) {
|
||||
$effects = $this->getEffects();
|
||||
$this->removeEffectsDatabase();
|
||||
|
||||
foreach($this->getEffects() as $effectInfo)
|
||||
$this->addEffectDatabase($effectInfo);
|
||||
}
|
||||
}
|
||||
|
||||
public static function exists(string $name): bool {
|
||||
$check = DB::prepare('
|
||||
SELECT COUNT(`zone_name`) > 0
|
||||
FROM `ytkns_zones`
|
||||
WHERE `zone_name` = :name
|
||||
');
|
||||
$check->bindValue('name', $name);
|
||||
return $check->execute() ? (bool)$check->fetchColumn() : true;
|
||||
}
|
||||
|
||||
public function delete(): void {
|
||||
$delete = DB::prepare('
|
||||
DELETE FROM `ytkns_zones`
|
||||
WHERE `zone_id` = :zone
|
||||
');
|
||||
$delete->bindValue('zone', $this->getId());
|
||||
$delete->execute();
|
||||
}
|
||||
|
||||
public static function validName(string $name): bool {
|
||||
return preg_match('#^([A-Za-z0-9]{1,50})$#', $name);
|
||||
}
|
||||
|
||||
public function toJson(bool $asString = false) {
|
||||
$zoneInfo = [
|
||||
'id' => $this->getId(),
|
||||
'name' => $this->getName(),
|
||||
'title' => $this->getTitle(),
|
||||
'effects' => [],
|
||||
];
|
||||
|
||||
foreach($this->getEffects() as $effect)
|
||||
$zoneInfo['effects'][] = $effect->toJson();
|
||||
|
||||
if($asString)
|
||||
$zoneInfo = json_encode($zoneInfo);
|
||||
|
||||
return $zoneInfo;
|
||||
}
|
||||
|
||||
public function queueTask(string $task, ...$params): void {
|
||||
ZoneTask::enqueue($this, $task, $params);
|
||||
}
|
||||
}
|
102
src/ZoneEffect.php
Normal file
|
@ -0,0 +1,102 @@
|
|||
<?php
|
||||
namespace YTKNS;
|
||||
|
||||
use Exception;
|
||||
|
||||
class ZoneEffectClassNotFoundException extends Exception {};
|
||||
|
||||
final class ZoneEffect {
|
||||
private const CLASS_NAME = '\\YTKNS\\Effects\\%sEffect';
|
||||
|
||||
public function __construct() {
|
||||
}
|
||||
|
||||
public function getZoneId(): int {
|
||||
return $this->zone_id ?? 0;
|
||||
}
|
||||
public function getZone(): Zone {
|
||||
return Zone::byId($this->getZoneId());
|
||||
}
|
||||
public function setZoneId(Zone $zone): void {
|
||||
$this->zone_id = $zone->getId();
|
||||
}
|
||||
|
||||
public function getEffectName(): string {
|
||||
return $this->effect_name;
|
||||
}
|
||||
public function setEffectName(string $name): void {
|
||||
$this->effect_name = $name;
|
||||
}
|
||||
|
||||
public function getEffectParams(): array {
|
||||
if(!isset($this->effect_params) || $this->effect_params === null)
|
||||
return [];
|
||||
|
||||
if(is_string($this->effect_params))
|
||||
$this->effect_params = json_decode($this->effect_params, true);
|
||||
|
||||
return $this->effect_params;
|
||||
}
|
||||
public function setEffectParams(array $params): void {
|
||||
$this->effect_params = $params;
|
||||
}
|
||||
|
||||
public static function effectClassName(string $name): string {
|
||||
return sprintf(self::CLASS_NAME, $name);
|
||||
}
|
||||
public function getEffectClassName(): string {
|
||||
return self::effectClassName($this->getEffectName());
|
||||
}
|
||||
public static function effectClass(string $name) {
|
||||
$className = self::effectClassName($name);
|
||||
|
||||
if(!class_exists($className))
|
||||
throw new ZoneEffectClassNotFoundException($name);
|
||||
|
||||
return new $className;
|
||||
}
|
||||
public function getEffectClass(bool $quiet = false) {
|
||||
try {
|
||||
$effect = self::effectClass($this->getEffectName());
|
||||
$effect->setEffectParams($this->getEffectParams(), $quiet);
|
||||
return $effect;
|
||||
} catch(ZoneEffectClassNotFoundException $ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
public function applyEffect(PageBuilder $builder, bool $quiet = false): void {
|
||||
$effect = $this->getEffectClass($quiet);
|
||||
|
||||
if($effect !== null)
|
||||
$effect->applyEffect($builder);
|
||||
}
|
||||
|
||||
public static function byZone(Zone $zone): array {
|
||||
$getEffects = DB::prepare('
|
||||
SELECT `zone_id`, `effect_name`, `effect_params`
|
||||
FROM `ytkns_zones_effects`
|
||||
WHERE `zone_id` = :zone
|
||||
');
|
||||
$getEffects->bindValue('zone', $zone->getId());
|
||||
if(!$getEffects->execute())
|
||||
return [];
|
||||
|
||||
$effects = [];
|
||||
while($effect = $getEffects->fetchObject(self::class))
|
||||
$effects[] = $effect;
|
||||
|
||||
return $effects;
|
||||
}
|
||||
|
||||
public function toJson(bool $asString = false) {
|
||||
$effectInfo = [
|
||||
'type' => $this->getEffectName(),
|
||||
'values' => $this->getEffectParams(),
|
||||
];
|
||||
|
||||
if($asString)
|
||||
$effectInfo = json_encode($effectInfo);
|
||||
|
||||
return $effectInfo;
|
||||
}
|
||||
}
|
46
src/ZoneRedirect.php
Normal file
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
namespace YTKNS;
|
||||
|
||||
final class ZoneRedirect {
|
||||
public function __construct() {
|
||||
}
|
||||
|
||||
public static function exists(string $subdomain): bool {
|
||||
$check = DB::prepare('
|
||||
SELECT COUNT(`redirect_name`) > 0
|
||||
FROM `ytkns_redirects`
|
||||
WHERE `redirect_name` = :subdomain
|
||||
');
|
||||
$check->bindValue('subdomain', $subdomain);
|
||||
return $check->execute() ? (bool)$check->fetchColumn() : true;
|
||||
}
|
||||
|
||||
public static function find(string $subdomain): ?ZoneRedirect {
|
||||
$find = DB::prepare('
|
||||
SELECT `redirect_name`, `redirect_target`
|
||||
FROM `ytkns_redirects`
|
||||
WHERE `redirect_name` = :subdomain
|
||||
');
|
||||
$find->bindValue('subdomain', $subdomain);
|
||||
$redirect = $find->execute() ? $find->fetchObject(self::class) : false;
|
||||
return $redirect ? $redirect : null;
|
||||
}
|
||||
|
||||
public static function create(string $subdomain, string $target): void {
|
||||
$create = DB::prepare('
|
||||
REPLACE INTO `ytkns_redirects` (
|
||||
`redirect_name`, `redirect_target`
|
||||
) VALUES (
|
||||
:name, :target
|
||||
)
|
||||
');
|
||||
$create->bindValue('name', $subdomain);
|
||||
$create->bindValue('target', $target);
|
||||
$create->execute();
|
||||
}
|
||||
|
||||
public function execute(): void {
|
||||
http_response_code(301);
|
||||
header(sprintf('Location: %s', $this->redirect_target));
|
||||
}
|
||||
}
|
60
src/ZoneTask.php
Normal file
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
namespace YTKNS;
|
||||
|
||||
final class ZoneTask {
|
||||
public function getZoneId(): int {
|
||||
return $this->zone_id ?? 0;
|
||||
}
|
||||
public function getZone(): Zone {
|
||||
return Zone::byId($this->getZoneId());
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
return $this->task_name ?? '';
|
||||
}
|
||||
|
||||
public function getParams(): array {
|
||||
if(empty($this->task_params))
|
||||
return [];
|
||||
return unserialize($this->task_params);
|
||||
}
|
||||
|
||||
public function delete(): void {
|
||||
$deleteTask = DB::prepare('
|
||||
DELETE FROM `ytkns_zones_tasks`
|
||||
WHERE `zone_id` = :zone
|
||||
AND `task_name` = :task
|
||||
');
|
||||
$deleteTask->bindValue('zone', $this->getZoneId());
|
||||
$deleteTask->bindValue('task', $this->getName());
|
||||
$deleteTask->execute();
|
||||
}
|
||||
|
||||
public static function queue(): array {
|
||||
$getTasks = DB::prepare('
|
||||
SELECT `zone_id`, `task_name`, `task_params`
|
||||
FROM `ytkns_zones_tasks`
|
||||
');
|
||||
$getTasks->execute();
|
||||
$tasks = [];
|
||||
|
||||
while($task = $getTasks->fetchObject(self::class))
|
||||
$tasks[] = $task;
|
||||
|
||||
return $tasks;
|
||||
}
|
||||
|
||||
public static function enqueue(Zone $zone, string $name, array $params = []): void {
|
||||
$enqueue = DB::prepare('
|
||||
REPLACE INTO `ytkns_zones_tasks` (
|
||||
`zone_id`, `task_name`, `task_params`
|
||||
) VALUES (
|
||||
:zone, :task, :params
|
||||
)
|
||||
');
|
||||
$enqueue->bindValue('zone', $zone->getId());
|
||||
$enqueue->bindValue('task', $name);
|
||||
$enqueue->bindValue('params', serialize(array_values($params)));
|
||||
$enqueue->execute();
|
||||
}
|
||||
}
|
44
src/ZoneView.php
Normal file
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
namespace YTKNS;
|
||||
|
||||
final class ZoneView {
|
||||
private const THRESHOLD = 60 * 60 * 24;
|
||||
|
||||
public function getTime(): int {
|
||||
return $this->view_time ?? 0;
|
||||
}
|
||||
|
||||
public static function byZoneAddress(Zone $zone, string $ipAddress): ?self {
|
||||
$getZoneView = DB::prepare('
|
||||
SELECT `zone_id`,
|
||||
UNIX_TIMESTAMP(`view_time`) AS `view_time`,
|
||||
INET6_NTOA(`view_address`) AS `view_address`
|
||||
FROM `ytkns_zones_views`
|
||||
WHERE `zone_id` = :zone
|
||||
AND `view_address` = INET6_ATON(:ip)
|
||||
');
|
||||
$getZoneView->bindValue('zone', $zone->getId());
|
||||
$getZoneView->bindValue('ip', $ipAddress);
|
||||
$zoneView = $getZoneView->execute() ? $getZoneView->fetchObject(self::class) : false;
|
||||
return $zoneView ? $zoneView : null;
|
||||
}
|
||||
|
||||
public static function increment(Zone $zone, string $ipAddress): void {
|
||||
$zoneView = self::byZoneAddress($zone, $ipAddress);
|
||||
|
||||
if($zoneView !== null && ($zoneView->getTime() + self::THRESHOLD) > time())
|
||||
return;
|
||||
|
||||
$updateZoneView = DB::prepare('
|
||||
REPLACE INTO `ytkns_zones_views` (
|
||||
`view_address`, `zone_id`, `view_time`
|
||||
) VALUES (
|
||||
INET6_ATON(:ip), :zone, NOW()
|
||||
)
|
||||
');
|
||||
$updateZoneView->bindValue('ip', $ipAddress);
|
||||
$updateZoneView->bindValue('zone', $zone->getId());
|
||||
if($updateZoneView->execute())
|
||||
$zone->incrementViews();
|
||||
}
|
||||
}
|
74
startup.php
Normal file
|
@ -0,0 +1,74 @@
|
|||
<?php
|
||||
namespace YTKNS;
|
||||
|
||||
define('YTKNS_STARTUP', microtime(true));
|
||||
define('YTKNS_ROOT', __DIR__);
|
||||
define('YTKNS_SRC', YTKNS_ROOT . '/src');
|
||||
define('YTKNS_TPL', YTKNS_ROOT . '/templates');
|
||||
define('YTKNS_UPLOADS', YTKNS_ROOT . '/uploads');
|
||||
define('YTKNS_PUB', YTKNS_ROOT . '/public');
|
||||
define('YTKNS_SCREENSHOTS', YTKNS_PUB . '/ss');
|
||||
define('YTKNS_DEBUG', is_file(YTKNS_ROOT . '/.debug'));
|
||||
|
||||
require_once __DIR__ . '/config.php';
|
||||
|
||||
define('ALLOWED_UPLOADS', [
|
||||
'audio/mpeg', 'application/x-font-gdos', 'audio/ogg', 'application/ogg',
|
||||
'image/jpeg', 'image/png', 'image/gif',
|
||||
]);
|
||||
define('SITE_EFFECTS', [
|
||||
\YTKNS\Effects\BackgroundAudioEffect::class,
|
||||
\YTKNS\Effects\BackgroundImageEffect::class,
|
||||
\YTKNS\Effects\ForegroundImageEffect::class,
|
||||
\YTKNS\Effects\ZoomTextEffect::class,
|
||||
\YTKNS\Effects\NewlyCreatedPageEffect::class,
|
||||
]);
|
||||
define('EFFECT_UPLOADS', [
|
||||
'BackgroundAudio' => ['ogg', 'mp3'],
|
||||
'BackgroundImage' => ['img'],
|
||||
'ForegroundImage' => ['img'],
|
||||
]);
|
||||
|
||||
error_reporting(YTKNS_DEBUG ? -1 : 0);
|
||||
ini_set('display_errors', YTKNS_DEBUG ? 'On' : 'Off');
|
||||
|
||||
mb_internal_encoding('utf-8');
|
||||
date_default_timezone_set('utc');
|
||||
|
||||
// Display exception report
|
||||
set_exception_handler(function(\Throwable $ex) {
|
||||
http_response_code(500);
|
||||
|
||||
$out = file_get_contents(YTKNS_TPL . (YTKNS_DEBUG ? '/debug/index.html' : '/errors/500.html'));
|
||||
echo strtr($out, [
|
||||
':raw_ex' => (string)$ex,
|
||||
':type' => get_class($ex),
|
||||
':msg' => $ex->getMessage(),
|
||||
]);
|
||||
|
||||
exit;
|
||||
});
|
||||
|
||||
// Turn uncaught errors into uncaught exceptions.
|
||||
set_error_handler(function(int $errno, string $errstr, string $errfile, int $errline) {
|
||||
throw new \ErrorException($errstr, 0, $errno, $errfile, $errline);
|
||||
return true;
|
||||
}, -1);
|
||||
|
||||
// Register class autoloader
|
||||
spl_autoload_register(function(string $className) {
|
||||
if(substr($className, 0, 6) !== 'YTKNS\\')
|
||||
return;
|
||||
|
||||
$classPath = YTKNS_SRC . str_replace('\\', '/', substr($className, 5)) . '.php';
|
||||
|
||||
if(is_file($classPath))
|
||||
require_once $classPath;
|
||||
});
|
||||
|
||||
DB::init(PDO_DSN, PDO_USER, PDO_PASS, DB::FLAGS);
|
||||
|
||||
Config::init();
|
||||
Config::setDefault('user.invite_only', true);
|
||||
Config::setDefault('domain.main', 'ytkns.com');
|
||||
Config::setDefault('domain.zone', '%s.ytkns.com');
|
8
templates/auth/field.html
Normal file
|
@ -0,0 +1,8 @@
|
|||
<label class="auth-field">
|
||||
<div class="auth-field-label">
|
||||
:field_title
|
||||
</div>
|
||||
<div class="auth-field-value">
|
||||
<input type=":field_type" name=":field_name" class="auth-field-value-input" value=":field_value"/>
|
||||
</div>
|
||||
</label>
|
11
templates/auth/login.html
Normal file
|
@ -0,0 +1,11 @@
|
|||
<h1 class="page-title">Log in</h1>
|
||||
<form action="/auth/login" method="post" class="auth">
|
||||
:auth_error
|
||||
:auth_fields
|
||||
<label class="auth-option">
|
||||
<input type="checkbox" name="remember" class="auth-option-input" :auth_remember/> Stay logged in
|
||||
</label>
|
||||
<div class="auth-buttons">
|
||||
<input type="submit" value="Log in" class="auth-buttons-button"/>
|
||||
</div>
|
||||
</form>
|
17
templates/auth/login2.html
Normal file
|
@ -0,0 +1,17 @@
|
|||
<h1 class="page-title">Log in</h1>
|
||||
<form action="/auth/fid" method="post" class="auth auth-fid">
|
||||
:auth_error
|
||||
<div class="auth-buttons">
|
||||
<input type="submit" value="Log in with Flashii ID" class="auth-buttons-button auth-buttons-button-fid"/>
|
||||
<input type="button" value="Log in with YTKNS details" class="auth-buttons-button" onclick="document.getElementById('_login').classList.remove('hidden');this.classList.add('hidden');" />
|
||||
</div>
|
||||
</form>
|
||||
<form action="/auth/login" method="post" class="auth hidden" id="_login">
|
||||
:auth_fields
|
||||
<label class="auth-option">
|
||||
<input type="checkbox" name="remember" class="auth-option-input" :auth_remember/> Stay logged in
|
||||
</label>
|
||||
<div class="auth-buttons">
|
||||
<input type="submit" value="Log in" class="auth-buttons-button"/>
|
||||
</div>
|
||||
</form>
|
8
templates/auth/register.html
Normal file
|
@ -0,0 +1,8 @@
|
|||
<h1 class="page-title">Register</h1>
|
||||
<form action="/auth/register" method="post" class="auth">
|
||||
:auth_error
|
||||
:auth_fields
|
||||
<div class="auth-buttons">
|
||||
<input type="submit" value="Create account" class="auth-buttons-button"/>
|
||||
</div>
|
||||
</form>
|
0
templates/debug/frame.html
Normal file
15
templates/debug/index.html
Normal file
|
@ -0,0 +1,15 @@
|
|||
<!doctype html>
|
||||
<!--
|
||||
:raw_ex
|
||||
-->
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<title>:type - :msg</title>
|
||||
</head>
|
||||
<body>
|
||||
<pre>
|
||||
:raw_ex
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
3
templates/error.html
Normal file
|
@ -0,0 +1,3 @@
|
|||
<div class="error">
|
||||
:error_text
|
||||
</div>
|
11
templates/errors/500.html
Normal file
|
@ -0,0 +1,11 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<title>Error 500</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>ytkns is fucking dead</h1>
|
||||
<p>:msg</p>
|
||||
</body>
|
||||
</html>
|
9
templates/footer.html
Normal file
|
@ -0,0 +1,9 @@
|
|||
</div>
|
||||
<div class="footer" id="ytkns-footer">
|
||||
YTKNS by <a href="//flash.moe">Flashwave</a> 2019-:footer_year |
|
||||
<a href="#" onclick="alert(['e&', '.m&o', 'h', 's', 'la', '#&f', 's', 't&kn', 'y', '+&&', 'e', '&m'].reverse().join('&').replace(/&/g, '').replace('#', '@'));">Contact</a> |
|
||||
Loaded in :footer_took seconds
|
||||
</div>
|
||||
</div>:scripts
|
||||
</body>
|
||||
</html>
|
34
templates/header.html
Normal file
|
@ -0,0 +1,34 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>:title</title>
|
||||
<link href="/assets/style.css" type="text/css" rel="stylesheet"/>
|
||||
<script type="text/javascript">
|
||||
var _paq = window._paq || [];
|
||||
_paq.push(['disableCookies']);
|
||||
_paq.push(['trackPageView']);
|
||||
_paq.push(['enableLinkTracking']);
|
||||
(function() {
|
||||
_paq.push(['setTrackerUrl', '//uiharu.railgun.sh/mtm']);
|
||||
_paq.push(['setSiteId', 'zlwmGOjMBk5J']);
|
||||
var g = document.createElement('script');
|
||||
g.type = 'text/javascript'; g.async = true;
|
||||
g.defer = true; g.src = '//uiharu.railgun.sh/mtm.js';
|
||||
document.head.appendChild(g);
|
||||
})();
|
||||
</script>:head
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<div class="header">
|
||||
<div class="header-usermenu">
|
||||
:menu_user
|
||||
</div>
|
||||
<a href="/" class="header-title"></a>
|
||||
<div class="header-navigation">
|
||||
:menu_site
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
2
templates/home.html
Normal file
|
@ -0,0 +1,2 @@
|
|||
<p>this site has nothing in particular to do with splatoon lol, it's just a ytmnd clone</p>
|
||||
<p>for the best experience, enable autoplay for this domain</p>
|
1
templates/information-redirect.html
Normal file
|
@ -0,0 +1 @@
|
|||
<br/><a href=":info_redirect">Click here if nothing happens.</a>
|
6
templates/information.html
Normal file
|
@ -0,0 +1,6 @@
|
|||
<fieldset class="information">
|
||||
<legend class="information-title">:info_title</legend>
|
||||
<div class="information-content">
|
||||
:info_content
|
||||
</div>
|
||||
</fieldset>
|
1
templates/menu-site-item.html
Normal file
|
@ -0,0 +1 @@
|
|||
<a href=":link" class="header-navigation-item">:text</a>
|
1
templates/menu-user-item.html
Normal file
|
@ -0,0 +1 @@
|
|||
<a href=":link" class="header-usermenu-item">:text</a>
|
3
templates/profile/index.html
Normal file
|
@ -0,0 +1,3 @@
|
|||
<h1 class="page-title">@:profile_username</h1>
|
||||
<h2 class="page-subtitle">Zones</h2>
|
||||
:profile_zones
|
2
templates/profile/notfound.html
Normal file
|
@ -0,0 +1,2 @@
|
|||
<h1 class="page-title">User not found!</h1>
|
||||
<p class="page-paragraph">No user with this username exists.</p>
|
16
templates/profile/zone-item.html
Normal file
|
@ -0,0 +1,16 @@
|
|||
<div class="zones-list-item">
|
||||
<a class="zones-list-item-screenshot" href=":zone_url" target="_blank">
|
||||
<img src=":zone_screenshot" alt=":zone_name" class="zones-list-item-screenshot-image"/>
|
||||
</a>
|
||||
<div class="zones-list-item-info">
|
||||
<div class="zones-list-item-info-name">
|
||||
<a href=":zone_url" target="_blank">:zone_name</a>
|
||||
</div>
|
||||
<div class="zones-list-item-info-title">
|
||||
<a href=":zone_url" target="_blank">:zone_title</a>
|
||||
</div>
|
||||
<div class="zones-list-item-info-misc">
|
||||
:zone_views views
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
3
templates/profile/zone-list.html
Normal file
|
@ -0,0 +1,3 @@
|
|||
<div class="zones-list">
|
||||
:zone_items
|
||||
</div>
|
1
templates/profile/zone-none.html
Normal file
|
@ -0,0 +1 @@
|
|||
<p class="page-paragraph">This user has no zones yet.</p>
|
1
templates/settings/index.html
Normal file
|
@ -0,0 +1 @@
|
|||
<h1 class="page-title">Settings</h1>
|
4
templates/settings/invites-create.html
Normal file
|
@ -0,0 +1,4 @@
|
|||
<form method="post" action="/settings/invites" class="invites-create">
|
||||
<input type="hidden" name="invite_token" value=":create_token"/>
|
||||
<input type="submit" value="Create Invite" class="invites-create-button"/>
|
||||
</form>
|
14
templates/settings/invites-item.html
Normal file
|
@ -0,0 +1,14 @@
|
|||
<div class="invites-list-item">
|
||||
<div class="invites-list-item-created">
|
||||
:invite_created
|
||||
</div>
|
||||
<div class="invites-list-item-used">
|
||||
:invite_used
|
||||
</div>
|
||||
<div class="invites-list-item-user">
|
||||
:invite_user
|
||||
</div>
|
||||
<div class="invites-list-item-token">
|
||||
<code onclick="navigator.clipboard.writeText('https://ytkns.com/auth/register?inv=:invite_token');" title="Click to copy registration link with invite">:invite_token</code>
|
||||
</div>
|
||||
</div>
|
22
templates/settings/invites.html
Normal file
|
@ -0,0 +1,22 @@
|
|||
<h1 class="page-title">Invites</h1>
|
||||
<div class="invites">
|
||||
:invite_error
|
||||
:invite_create
|
||||
<div class="invites-list">
|
||||
<div class="invites-list-item">
|
||||
<div class="invites-list-item-created">
|
||||
Created on
|
||||
</div>
|
||||
<div class="invites-list-item-used">
|
||||
Used on
|
||||
</div>
|
||||
<div class="invites-list-item-user">
|
||||
Used by
|
||||
</div>
|
||||
<div class="invites-list-item-token">
|
||||
Invitation
|
||||
</div>
|
||||
</div>
|
||||
:invite_list
|
||||
</div>
|
||||
</div>
|
20
templates/zones/create.html
Normal file
|
@ -0,0 +1,20 @@
|
|||
<h1 class="page-title">Create a Zone</h1>
|
||||
<form action=":create_action" method="post" class="zone-creation-form">
|
||||
:create_error
|
||||
<input type="hidden" name="zone_token" value=":create_token"/>
|
||||
<label class="zone-creation-input">
|
||||
<div class="zone-creation-label">Subdomain:</div>
|
||||
<div class="zone-creation-wrap">
|
||||
<input type="text" maxlength="50" name="zone_subdomain" value=":create_subdomain" class="zone-creation-value"/>.:create_domain
|
||||
</div>
|
||||
</label>
|
||||
<label class="zone-creation-input">
|
||||
<div class="zone-creation-label">Title:</div>
|
||||
<div class="zone-creation-wrap">
|
||||
<input type="text" maxlength="255" name="zone_title" value=":create_title" class="zone-creation-value"/>
|
||||
</div>
|
||||
</label>
|
||||
<div class="zone-creation-actions">
|
||||
<input type="submit" value="Create This Zone" class="zone-creation-action"/>
|
||||
</div>
|
||||
</form>
|
8
templates/zones/delete.html
Normal file
|
@ -0,0 +1,8 @@
|
|||
<h1 class="page-title">Deleting Zone - :delete_zone_name</h1>
|
||||
<form method="post" action="/zones/:delete_zone_id/delete" class="zone-creation-form">
|
||||
<input type="hidden" name="zone_token" value=":delete_zone_token"/>
|
||||
<div class="zone-creation-actions">
|
||||
<input type="submit" value="Yes, delete this Zone" class="zone-creation-action"/>
|
||||
<a href="/zones?f=my" class="zone-creation-action">No, bring me back to the Zone list</a>
|
||||
</div>
|
||||
</form>
|
25
templates/zones/edit.html
Normal file
|
@ -0,0 +1,25 @@
|
|||
<noscript>
|
||||
<div class="noscript error">
|
||||
The editor requires Javascript. Please enable it or use a compatible browser.
|
||||
</div>
|
||||
</noscript>
|
||||
<div id="editor">
|
||||
<div class="loading-editor">
|
||||
<div class="loading-editor-image"></div>
|
||||
<div class="loading-editor-text">
|
||||
Loading editor...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
window.onload = function() {
|
||||
document.getElementById('ytkns-footer').appendChild(document.createTextNode('| JS: :edit_js_ver | CSS: :edit_css_ver'));
|
||||
|
||||
ytknsEditorMain(
|
||||
document.getElementById('editor'),
|
||||
:edit_id,
|
||||
':edit_token',
|
||||
':upload_token'
|
||||
);
|
||||
};
|
||||
</script>
|
16
templates/zones/item.html
Normal file
|
@ -0,0 +1,16 @@
|
|||
<div class="zones-list-item">
|
||||
<a class="zones-list-item-screenshot" href=":zone_url" target="_blank">
|
||||
<img src=":zone_screenshot" alt=":zone_name" class="zones-list-item-screenshot-image"/>
|
||||
</a>
|
||||
<div class="zones-list-item-info">
|
||||
<div class="zones-list-item-info-name">
|
||||
<a href=":zone_url" target="_blank">:zone_name</a>
|
||||
</div>
|
||||
<div class="zones-list-item-info-title">
|
||||
<a href=":zone_url" target="_blank">:zone_title</a>
|
||||
</div>
|
||||
<div class="zones-list-item-info-misc">
|
||||
:zone_views views - <a href=":zone_user_url">@:zone_user_name</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
7
templates/zones/list.html
Normal file
|
@ -0,0 +1,7 @@
|
|||
<h1 class="page-title">:zone_list_title</h1>
|
||||
<div class="zones-sorts">
|
||||
:zone_sortings
|
||||
</div>
|
||||
<div class="zones-list">
|
||||
:zone_list
|
||||
</div>
|
33
templates/zones/my-item.html
Normal file
|
@ -0,0 +1,33 @@
|
|||
<!--div class="my-zones-list-item" id="z:zone_id">
|
||||
<div class="my-zones-list-item-info">
|
||||
<div class="my-zones-list-item-info-name">
|
||||
<a href=":zone_view_url" target="_blank">:zone_name</a>
|
||||
</div>
|
||||
<div class="my-zones-list-item-info-title">
|
||||
:zone_views views - :zone_title
|
||||
</div>
|
||||
</div>
|
||||
<div class="my-zones-list-item-actions">
|
||||
<a href=":zone_view_url" title="View Zone" class="my-zones-list-item-actions-action my-zones-list-item-actions-action--view" target="_blank"></a>
|
||||
<a href=":zone_edit_url" title="Edit Zone" class="my-zones-list-item-actions-action my-zones-list-item-actions-action--edit"></a>
|
||||
<a href=":zone_delete_url" title="Delete Zone" class="my-zones-list-item-actions-action my-zones-list-item-actions-action--delete"></a>
|
||||
</div>
|
||||
</div-->
|
||||
<div class="zones-list-item">
|
||||
<a class="zones-list-item-screenshot" href=":zone_url" target="_blank">
|
||||
<img src=":zone_screenshot" alt=":zone_name" class="zones-list-item-screenshot-image"/>
|
||||
</a>
|
||||
<div class="zones-list-item-info">
|
||||
<div class="zones-list-item-info-name">
|
||||
<a href=":zone_url" target="_blank">:zone_name</a>
|
||||
</div>
|
||||
<div class="zones-list-item-info-title">
|
||||
<a href=":zone_url" target="_blank">:zone_views views - :zone_title</a>
|
||||
</div>
|
||||
<div class="zones-list-item-info-actions">
|
||||
<a href=":zone_url" title="View Zone" class="zones-list-item-info-actions-action zones-list-item-info-actions-action--view" target="_blank"></a>
|
||||
<a href=":zone_edit_url" title="Edit Zone" class="zones-list-item-info-actions-action zones-list-item-info-actions-action--edit"></a>
|
||||
<a href=":zone_delete_url" title="Delete Zone" class="zones-list-item-info-actions-action zones-list-item-info-actions-action--delete"></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
4
templates/zones/my-list.html
Normal file
|
@ -0,0 +1,4 @@
|
|||
<h1 class="page-title">My Zones</h1>
|
||||
<div class="my-zones-list">
|
||||
:zone_list
|
||||
</div>
|
9
templates/zones/my-none.html
Normal file
|
@ -0,0 +1,9 @@
|
|||
<h1 class="page-title">My Zones</h1>
|
||||
<div class="zone-creation-form">
|
||||
<div class="zone-creation-paragraph">
|
||||
You have no zones yet.
|
||||
</div>
|
||||
<div class="zone-creation-actions">
|
||||
<a href="/zones/create" class="zone-creation-action">Create a Zone!</a>
|
||||
</div>
|
||||
</div>
|
4
templates/zones/none.html
Normal file
|
@ -0,0 +1,4 @@
|
|||
<div class="nozone">
|
||||
<h1>This zone doesn't exist!</h1>
|
||||
<p>Would you like to <a href=":zone_create_url">create it</a>?</p>
|
||||
</div>
|