From 0b46d15a2a158d5ba4dc6498fd8cd46a0a8162e4 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Wed, 12 Nov 2014 19:35:03 +0100 Subject: [PATCH] - implemented screenshot handling, inclding * editing after upload * reputation gain for approved uploads * moderator interface (albeit a bit wonky) I still do not speak French, Spanish or Russian, so translations are appreciated --- .htaccess | 2 + includes/ajaxHandler.class.php | 217 ++++++++++++++- includes/community.class.php | 131 ++++++++- includes/defines.php | 2 +- includes/utilities.php | 2 +- index.php | 1 + localization/locale_dede.php | 32 ++- localization/locale_enus.php | 32 ++- localization/locale_eses.php | 32 ++- localization/locale_frfr.php | 32 ++- localization/locale_ruru.php | 32 ++- pages/account.php | 16 +- pages/admin.php | 49 +++- pages/screenshot.php | 301 +++++++++++++++++++++ setup/updates/03_screenshots.sql | 16 ++ static/css/Cropper.css | 129 +++++++++ static/js/Cropper.js | 325 +++++++++++++++++++++++ static/js/screenshot.js | 1 + template/bricks/head.tpl.php | 6 +- template/bricks/infobox.tpl.php | 10 + template/pages/admin/screenshots.tpl.php | 138 ++++++++++ template/pages/screenshot.tpl.php | 119 +++++++++ 22 files changed, 1591 insertions(+), 34 deletions(-) create mode 100644 pages/screenshot.php create mode 100644 setup/updates/03_screenshots.sql create mode 100644 static/css/Cropper.css create mode 100644 static/js/Cropper.js create mode 100644 static/js/screenshot.js create mode 100644 template/pages/admin/screenshots.tpl.php create mode 100644 template/pages/screenshot.tpl.php diff --git a/.htaccess b/.htaccess index 664ca4c6..f9875a4d 100644 --- a/.htaccess +++ b/.htaccess @@ -17,7 +17,9 @@ AddDefaultCharset utf8 CharsetRecodeMultipartForms Off +# 5MB should be enough for the largest screenshots in the land php_value default_charset UTF-8 + php_value upload_max_filesize 5M RewriteEngine on # RewriteBase /~user/localPath/ # enable if the rules do not work, when they should diff --git a/includes/ajaxHandler.class.php b/includes/ajaxHandler.class.php index 557ab3ac..fbc1c5bb 100644 --- a/includes/ajaxHandler.class.php +++ b/includes/ajaxHandler.class.php @@ -640,7 +640,28 @@ class AjaxHandler if (empty($this->get['action']) || !$this->params) return null; - if ($this->params[0] == 'siteconfig') + if ($this->params[0] == 'screenshots') + { + if (!User::isInGroup(U_GROUP_STAFF | U_GROUP_SCREENSHOT)) // comment_mod, handleSSmod, vi_mod ? + return null; + + switch ($this->get['action']) + { + case 'list': // get all => null (optional) + case 'manage': // get: [type => type, typeId => typeId] || [user => username] + case 'editalt': // get: id => ssId; post: alt => caption + case 'approve': // get: id => ssId || ,-separated id-list + case 'sticky': // get: id => ssId || ,-separated id-list + case 'delete': // get: id => ssId || ,-separated id-list + case 'relocate': // get: id => ssId, typeid => typeId (but not type..?) + $fn = 'admin_handleSS'.ucfirst($this->get['action']); + return $this->$fn(); + break; + default: + return null; + } + } + else if ($this->params[0] == 'siteconfig') { if (!User::isInGroup(U_GROUP_DEV | U_GROUP_ADMIN)) return null; @@ -651,7 +672,7 @@ class AjaxHandler if (empty($this->get['id'])) return 'invalid configuration option given'; - if (DB::Aowow()->query('DELETE FROM ?_config WHERE `key` = ? AND (`flags` & ?d) = 0', $this->get['id'], CON_FLAG_PERSISTANT)) + if (DB::Aowow()->query('DELETE FROM ?_config WHERE `key` = ? AND (`flags` & ?d) = 0', $this->get['id'], CON_FLAG_PERSISTENT)) return ''; else return 'option name is either protected or was not found'; @@ -1073,6 +1094,198 @@ class AjaxHandler // hey, still here? you're not a Tauren/Nelf as bear or cat, are you? return DB::Aowow()->selectCell('SELECT IF(?d == 1, IFNULL(displayIdA, displayIdH), IFNULL(displayIdH, displayIdA)) FROM ?_shapeshiftform WHERE id = ?d', Util::sideByRaceMask(1 << ($char['race'] - 1)), $form); } + + // get all => null (optional) + // evaled response .. UNK + private function admin_handleSSList() + { + // ssm_screenshotPages + // ssm_numPagesFound + + $pages = CommunityContent::getScreenshotPagesForManager(isset($this->get['all']), $nPages); + $buff = 'ssm_screenshotPages = '.json_encode($pages, JSON_NUMERIC_CHECK).";\n"; + $buff .= 'ssm_numPagesFound = '.$nPages.';'; + + return $buff; + } + + // get: [type => type, typeId => typeId] || [user => username] + // evaled response .. UNK + private function admin_handleSSManage() + { + $res = []; + + if (!empty($this->get['type']) && intVal($this->get['type']) && !empty($this->get['typeid']) && intVal($this->get['typeid'])) + $res = CommunityContent::getScreenshotsForManager($this->get['type'], $this->get['typeid']); + else if (!empty($this->get['user']) && strlen(urldecode($this->get['user'])) > 2) + if ($uId = DB::Aowow()->selectCell('SELECT id FROM ?_account WHERE displayName = ?', strtolower(urldecode($this->get['user'])))) + $res = CommunityContent::getScreenshotsForManager(0, 0, $uId); + + return 'ssm_screenshotData = '.json_encode($res, JSON_NUMERIC_CHECK); + } + + // get: id => SSid + // resp: '' + private function admin_handleSSEditalt() + { + if (empty($_GET['id']) || empty($this->post['alt'])) + return ''; + + // doesn't need to be htmlEscaped, ths javascript does that + DB::Aowow()->query('UPDATE ?_screenshots SET caption = ? WHERE id = ?d', $this->post['alt'], $_GET['id']); + + return ''; + } + + // get: id => comma-separated SSids + // resp: '' + private function admin_handleSSApprove($override = []) + { + if (empty($_GET['id'])) + return ''; + + $ids = $override ?: array_map('intval', explode(',', $_GET['id'])); + + // create resized and thumb version of screenshot + $resized = [772, 618]; + $thumb = [150, 150]; + $path = 'static/uploads/screenshots/%s/%d.jpg'; + + foreach ($ids as $id) + { + // must not be already approved + if ($_ = DB::Aowow()->selectCell('SELECT uploader FROM ?_screenshots WHERE (status & ?d) = 0 AND id = ?d', CC_FLAG_APPROVED, $id)) + { + // should also error-log + if (!file_exists(sprintf($path, 'pending', $id))) + continue; + + $srcImg = imagecreatefromjpeg(sprintf($path, 'pending', $id)); + $srcW = imagesx($srcImg); + $srcH = imagesy($srcImg); + + // write thumb + $scale = min(1.0, min($thumb[0] / $srcW, $thumb[1] / $srcH)); + $destW = $srcW * $scale; + $destH = $srcH * $scale; + $destImg = imagecreatetruecolor($destW, $destH); + + imagefill($destImg, 0, 0, imagecolorallocate($destImg, 255, 255, 255)); + imagecopyresampled($destImg, $srcImg, 0, 0, 0, 0, $destW, $destH, $srcW, $srcH); + + imagejpeg($destImg, sprintf($path, 'thumb', $id), 100); + + // write resized (only if required) + if ($srcW > $resized[0] || $srcH > $resized[1]) + { + $scale = min(1.0, min($resized[0] / $srcW, $resized[1] / $srcH)); + $destW = $srcW * $scale; + $destH = $srcH * $scale; + $destImg = imagecreatetruecolor($destW, $destH); + + imagefill($destImg, 0, 0, imagecolorallocate($destImg, 255, 255, 255)); + imagecopyresampled($destImg, $srcImg, 0, 0, 0, 0, $destW, $destH, $srcW, $srcH); + + imagejpeg($destImg, sprintf($path, 'resized', $id), 100); + } + + imagedestroy($srcImg); + + // move screenshot from pending to normal + rename(sprintf($path, 'pending', $id), sprintf($path, 'normal', $id)); + + // set as approved in DB and gain rep (once!) + DB::Aowow()->query('UPDATE ?_screenshots SET status = ?d, approvedBy = ?d WHERE id = ?d', CC_FLAG_APPROVED, User::$id, $id); + Util::gainSiteReputation($_, SITEREP_ACTION_UPLOAD, ['id' => $id, 'what' => 1]); + } + } + + return ''; + } + + // get: id => comma-separated SSids + // resp: '' + private function admin_handleSSSticky() + { + if (empty($_GET['id'])) + return ''; + + // this one is a bit strange: as far as i've seen, the only thing a 'sticky' screenshot does is show up in the infobox + // this also means, that only one screenshot per page should be sticky + // so, handle it one by one and the last one affecting one particular type/typId-key gets the cake + $ids = array_map('intval', explode(',', $_GET['id'])); + + foreach ($ids as $id) + { + // reset all others + DB::Aowow()->query('UPDATE ?_screenshots a, ?_screenshots b SET a.status = a.status & ~?d WHERE a.type = b.type AND a.typeId = b.typeId AND a.id <> b.id AND b.id = ?d', CC_FLAG_STICKY, $id); + + // approve this one (if not already) + $this->admin_handleSSApprove([$id]); + + // toggle sticky status + DB::Aowow()->query('UPDATE ?_screenshots SET `status` = IF(`status` & ?d, `status` & ~?d, `status` | ?d) WHERE id = ?d AND `status` & ?d', CC_FLAG_STICKY, CC_FLAG_STICKY, CC_FLAG_STICKY, $id, CC_FLAG_APPROVED); + } + + return ''; + } + + // get: id => comma-separated SSids + // resp: '' + // 2 steps: 1) remove from sight, 2) remove from disk + private function admin_handleSSDelete() + { + if (empty($_GET['id'])) + return ''; + + $path = 'static/uploads/screenshots/%s/%d.jpg'; + $ids = array_map('intval', explode(',', $_GET['id'])); + + foreach ($ids as $id) + { + // irrevocably remove already deleted files + if (DB::Aowow()->selectCell('SELECT 1 FROM ?_screenshots WHERE status & ?d AND id = ?d', CC_FLAG_DELETED, $id)) + { + DB::Aowow()->query('DELETE FROM ?_screenshots WHERE id = ?d', $id); + if (file_exists(sprintf($path, 'pending', $id))) + unlink(sprintf($path, 'pending', $id)); + + continue; + } + + // move pending or normal to pending + if (file_exists(sprintf($path, 'normal', $id))) + rename(sprintf($path, 'normal', $id), sprintf($path, 'pending', $id)); + + // remove resized and thumb + if (file_exists(sprintf($path, 'thumb', $id))) + unlink(sprintf($path, 'thumb', $id)); + + if (file_exists(sprintf($path, 'resized', $id))) + unlink(sprintf($path, 'resized', $id)); + } + + // flag as deleted if not aready + DB::Aowow()->query('UPDATE ?_screenshots SET status = ?d, deletedBy = ?d WHERE id IN (?a)', CC_FLAG_DELETED, User::$id, $ids); + + return ''; + } + + // get: id => ssId, typeid => typeId (but not type..?) + // resp: '' + private function admin_handleSSRelocate() + { + if (empty($this->get['id']) || empty($this->get['typeid'])) + return ''; + + $type = DB::Aowow()->selectCell('SELECT type FROM ?_screenshots WHERE id = ?d', $this->get['id']); + $typeId = (int)$this->get['typeid']; + + if (!(new Util::$typeClasses[$type]([['id', $typeId]]))->error) + DB::Aowow()->query('UPDATE ?_screenshots SET typeId = ?d WHERE id = ?d', $typeId, $this->get['id']); + + return ''; + } } ?> diff --git a/includes/community.class.php b/includes/community.class.php index 7bcadb13..42439b83 100644 --- a/includes/community.class.php +++ b/includes/community.class.php @@ -218,6 +218,128 @@ class CommunityContent return $replies; } + public static function getScreenshotsForManager($type, $typeId, $userId = 0) + { + $screenshots = DB::Aowow()->select(' + SELECT s.id, a.displayName AS user, s.date, s.width, s.height, s.type, s.typeId, s.caption, s.status, s.status AS "flags" + FROM ?_screenshots s, ?_account a + WHERE + s.uploader = a.id AND + { s.type = ?d} + { AND s.typeId = ?d} + { s.uploader = ?d} + LIMIT 100', + $userId ? DBSIMPLE_SKIP : $type, + $userId ? DBSIMPLE_SKIP : $typeId, + $userId ? $userId : DBSIMPLE_SKIP + ); + + $num = []; + foreach ($screenshots as $s) + { + if (empty($num[$s['type']][$s['typeId']])) + $num[$s['type']][$s['typeId']] = 1; + else + $num[$s['type']][$s['typeId']]++; + } + + // format data to meet requirements of the js + foreach ($screenshots as $idx => &$s) + { + $s['date'] = date(Util::$dateFormatInternal, $s['date']); + + $s['name'] = "Screenshot #".$s['id']; // what should we REALLY name it? + + if (isset($screenshots[$idx - 1])) + $s['prev'] = $idx - 1; + + if (isset($screenshots[$idx + 1])) + $s['next'] = $idx + 1; + + // order gives priority for 'status' + if (!($s['flags'] & CC_FLAG_APPROVED)) + { + $s['pending'] = 1; + $s['status'] = 0; + } + else + $s['status'] = 100; + + if ($s['flags'] & CC_FLAG_STICKY) + { + $s['sticky'] = 1; + $s['status'] = 105; + } + + if ($s['flags'] & CC_FLAG_DELETED) + { + $s['deleted'] = 1; + $s['status'] = 999; + } + + // something todo with massSelect .. am i doing this right? + if ($num[$s['type']][$s['typeId']] == 1) + $s['unique'] = 1; + } + + return $screenshots; + } + + public static function getScreenshotPagesForManager($all, &$nFound) + { + // i GUESS .. ss_getALL ? everything : unapproved + $nFound = 0; + $pages = DB::Aowow()->select(' + SELECT s.`type`, s.`typeId`, count(1) AS "count", MIN(s.`date`) AS "date" + FROM ?_screenshots s + {WHERE (s.status & ?d) = 0} + GROUP BY s.`type`, s.`typeId`', + $all ? DBSIMPLE_SKIP : CC_FLAG_APPROVED + ); + + if ($pages) + { + // limit to one actually existing type each + $types = array_intersect(array_unique(array_column($pages, 'type')), array_keys(Util::$typeClasses)); + foreach ($types as $t) + { + $ids = []; + foreach ($pages as $row) + if ($row['type'] == $t) + $ids[] = $row['typeId']; + + if (!$ids) + continue; + + $cnd = [['id', $ids]]; + if ($t == TYPE_WORLDEVENT) // FKIN HOLIDAYS + array_push($cnd, ['holidayId', $ids], 'OR'); + + $tClass = new Util::$typeClasses[$t]($cnd); + foreach ($pages as &$p) + if ($p['type'] == $t) + if ($tClass->getEntry($p['typeId'])) + $p['name'] = $tClass->getField('name', true); + } + + foreach ($pages as &$p) + { + if (empty($p['name'])) + { + Util::addNote(U_GROUP_STAFF | U_GROUP_SCREENSHOT, 'AdminPage::handleScreenshots() - Screenshot linked to nonexistant type/typeId combination '.$p['type'].'/'.$p['typeId']); + unset($p); + } + else + { + $nFound += $p['count']; + $p['date'] = date(Util::$dateFormatInternal, $p['date']); + } + } + } + + return $pages; + } + private static function getComments($type, $typeId) { @@ -300,11 +422,14 @@ class CommunityContent private static function getScreenshots($type, $typeId) { $screenshots = DB::Aowow()->Query(" - SELECT s.id, a.displayName AS user, s.date, s.width, s.height, s.type, s.typeId, s.caption, IF(s.status & 0x4, 1, 0) AS 'sticky' + SELECT s.id, a.displayName AS user, s.date, s.width, s.height, s.type, s.typeId, s.caption, IF(s.status & ?d, 1, 0) AS 'sticky' FROM ?_screenshots s, ?_account a - WHERE s.type = ? AND s.typeId = ? AND s.status & 0x2 AND s.uploader = a.id", + WHERE s.type = ? AND s.typeId = ? AND s.status & ?d AND (s.status & ?d) = 0 AND s.uploader = a.id", + CC_FLAG_STICKY, $type, - $typeId + $typeId, + CC_FLAG_APPROVED, + CC_FLAG_DELETED ); // format data to meet requirements of the js diff --git a/includes/defines.php b/includes/defines.php index 9d4f3c82..77a02974 100644 --- a/includes/defines.php +++ b/includes/defines.php @@ -91,7 +91,7 @@ define('CON_FLAG_TYPE_STRING', 0x08); // define('CON_FLAG_OPT_LIST', 0x10); // single option define('CON_FLAG_BITMASK', 0x20); // multiple options define('CON_FLAG_PHP', 0x40); // applied with ini_set() [restrictions apply!] -define('CON_FLAG_PERSISTANT', 0x80); // can not be deleted +define('CON_FLAG_PERSISTENT', 0x80); // can not be deleted // Auth Result define('AUTH_OK', 0); diff --git a/includes/utilities.php b/includes/utilities.php index d9625aa2..c239d8fd 100644 --- a/includes/utilities.php +++ b/includes/utilities.php @@ -1479,7 +1479,7 @@ class Util $x['sourceB'] = $miscData['voterId']; $x['amount'] = $action == SITEREP_ACTION_UPVOTED ? CFG_REP_REWARD_UPVOTED : CFG_REP_REWARD_DOWNVOTED; break; - case SITEREP_ACTION_UPLOAD: // NYI + case SITEREP_ACTION_UPLOAD: if (empty($miscData['id']) || empty($miscData['what'])) return false; diff --git a/index.php b/index.php index 8848ab7a..b53b8506 100644 --- a/index.php +++ b/index.php @@ -70,6 +70,7 @@ switch ($pageCall) case 'quests': case 'race': case 'races': + case 'screenshot': // prepare uploaded screenshots case 'search': // tool: searches case 'skill': case 'skills': diff --git a/localization/locale_dede.php b/localization/locale_dede.php index 2b1da5df..5be497cf 100644 --- a/localization/locale_dede.php +++ b/localization/locale_dede.php @@ -50,6 +50,7 @@ $lang = array( 'aboutUs' => "Über Aowow", 'and' => " und ", 'or' => " oder ", + 'back' => "Zurück", // filter 'extSearch' => "Erweiterte Suche", @@ -136,9 +137,36 @@ $lang = array( 'dateFmtLong' => "d.m.Y \u\m H:i", // error + 'intError' => "Ein interner Fehler ist aufgetreten.", + 'intError2' => "Ein interner Fehler ist aufgetreten. (%s)", 'genericError' => "Ein Fehler trat auf; aktualisiert die Seite und versucht es nochmal. Wenn der Fehler bestehen bleibt, bitte meldet es bei feedback", # LANG.genericerror 'bannedRating' => "Ihr wurdet davon gesperrt, Kommentare zu bewerten.", # LANG.tooltip_banned_rating - 'tooManyVotes' => "Ihr habt die tägliche Grenze für erlaubte Bewertungen erreicht. Kommt morgen mal wieder!" # LANG.tooltip_too_many_votes + 'tooManyVotes' => "Ihr habt die tägliche Grenze für erlaubte Bewertungen erreicht. Kommt morgen mal wieder!", # LANG.tooltip_too_many_votes + + // screenshots + 'prepError' => "Bei der Aufbereitung eures Screenshots ist ein Fehler aufgetreten", + 'cropHint' => "Schneidet das Bild zu, indem ihr die Auswahl verschiebt.
Bitte beachtet Screenshots: Tipps & Tricks für eine optimale Darstellung.", + 'caption' => "Kurzbeschreibung", + 'originalSize' => "Originalgröße", + 'targetSize' => "Zielgröße", + 'minSize' => "Mindestgröße", + 'displayOn' => "Hochgeladen für: %s[br][%s=%d]", + 'ssEdit' => "Screenshot bearbeiten", + 'ssUpload' => "Screenshot hochladen", + 'ssSubmit' => "Screenshot einsenden", + 'ssErrors' => array( + 'noUpload' => "Die Datei wurde nicht hochgeladen!", + 'maxSize' => "Die Datei überschreitet die max. Größe von %s!", + 'interrupted' => "Der Vorgang wurde unterbrochen!", + 'noFile' => "Es wurde keine Datei empfangen!", + 'noDest' => "Die Seite auf welcher der Screenshot angezeigt werden sollte existiert nicht!", + 'notAllowed' => "Es ist euch nicht erlaubt einen Screenshot hochzuladen!", + 'noImage' => "Die hochgeladene Datei ist kein Bild!", + 'wrongFormat' => "Das Bild muss im PNG oder JPG-Format sein!", + 'load' => "Das Bild konnte nicht geladen werden!", + 'tooSmall' => "Die Abmessungen sind zu klein! (kleiner als %d x %d)", + 'tooLarge' => "Die Abmessungen sind zu groß! (größer als %d x %d)" + ) ), 'game' => array( 'achievement' => "Erfolg", @@ -333,8 +361,6 @@ $lang = array( 'passMismatch' => "Die eingegebenen Kennworte stimmen nicht überein.", 'nameInUse' => "Es existiert bereits ein Konto mit diesem Namen.", 'mailInUse' => "Diese E-Mail-Adresse ist bereits mit einem Konto verbunden.", - 'intError' => "Ein interner Fehler ist aufgetreten.", - 'intError2' => "Ein interner Fehler ist aufgetreten. (%s)", 'isRecovering' => "Dieses Konto wird bereits wiederhergestellt. Folgt den Anweisungen in der Nachricht oder wartet %s bis das Token verfällt.", 'passCheckFail' => "Die Kennwörter stimmen nicht überein.", // message_passwordsdonotmatch 'newPassDiff' => "Euer neues Kennwort muss sich von eurem alten Kennwort unterscheiden." // message_newpassdifferent diff --git a/localization/locale_enus.php b/localization/locale_enus.php index 8b22b969..d52f652c 100644 --- a/localization/locale_enus.php +++ b/localization/locale_enus.php @@ -45,6 +45,7 @@ $lang = array( 'aboutUs' => "About us & contact", 'and' => " and ", 'or' => " or ", + 'back' => "Back", // filter 'extSearch' => "Extended search", @@ -131,9 +132,36 @@ $lang = array( 'dateFmtLong' => "Y/m/d \a\\t H:i", // error + 'intError' => "An internal error has occurred.", + 'intError2' => "An internal error has occurred. (%s)", 'genericError' => "An error has occurred; refresh the page and try again. If the error persists email feedback", # LANG.genericerror 'bannedRating' => "You have been banned from rating comments.", # LANG.tooltip_banned_rating - 'tooManyVotes' => "You have reached the daily voting cap. Come back tomorrow!" # LANG.tooltip_too_many_votes + 'tooManyVotes' => "You have reached the daily voting cap. Come back tomorrow!", # LANG.tooltip_too_many_votes + + // screenshots + 'prepError' => "An error occured preparing your screenshot", + 'cropHint' => "Crop the image by dragging the selection.
Please refer to Screenshots: Tips & Tricks for an optimal layout.", + 'caption' => "Caption", + 'originalSize' => "Original size", + 'targetSize' => "Target size", + 'minSize' => "Minimum size", + 'displayOn' => "Displayed on: %s[br][%s=%d]", + 'ssEdit' => "Edit uploaded screenshot", + 'ssUpload' => "Screenshot Upload", + 'ssSubmit' => "Submit Screenshot", + 'ssErrors' => array( + 'noUpload' => "The file was not uploaded!", + 'maxSize' => "The file exceeds the maximum size of %s!", + 'interrupted' => "The upload process was interrupted!", + 'noFile' => "The file was not received!", + 'noDest' => "The page this screenshot should be displayed on, does not exist!", + 'notAllowed' => "You are not allowed to upload screenshots!", + 'noImage' => "The uploaded file is not an image file!", + 'wrongFormat' => "The image file must be a png or jpg!", + 'load' => "The image file could not be loaded!", + 'tooSmall' => "The image size is too small! (lower than %d x %d)", + 'tooLarge' => "The image size is too large! (greater than %d x %d)" + ) ), 'game' => array( 'achievement' => "achievement", @@ -328,8 +356,6 @@ $lang = array( 'passMismatch' => "The passwords you entered do not match.", 'nameInUse' => "That username is already taken.", 'mailInUse' => "That email is already registered to an account.", - 'intError' => "An internal error occured.", - 'intError2' => "An internal error occured. (%s)", 'isRecovering' => "This account is already recovering. Follow the instructions in your email or wait %s for the token to expire.", 'passCheckFail' => "Passwords do not match.", // message_passwordsdonotmatch 'newPassDiff' => "Your new password must be different than your previous one." // message_newpassdifferent diff --git a/localization/locale_eses.php b/localization/locale_eses.php index a93ac559..e0bf8ef5 100644 --- a/localization/locale_eses.php +++ b/localization/locale_eses.php @@ -50,6 +50,7 @@ $lang = array( 'aboutUs' => "Sobre Aowow", 'and' => " y ", 'or' => " o ", + 'back' => "Arrière", // filter 'extSearch' => "Extender búsqueda", @@ -136,9 +137,36 @@ $lang = array( 'dateFmtLong' => "d/m/Y \a \l\a\s H:i", // error + 'intError' => "[An internal error occured.]", + 'intError2' => "[An internal error occured. (%s)]", 'genericError' => "Ha ocurrido un error; refresca la página e inténtalo de nuevo. Si el error persiste manda un correo a feedback", # LANG.genericerror 'bannedRating' => "Has sido baneado y no podrás valorar comentarios.", # LANG.tooltip_banned_rating - 'tooManyVotes' => "Has alcanzado el límite diario de votos. Vuelve mañana." # LANG.tooltip_too_many_votes + 'tooManyVotes' => "Has alcanzado el límite diario de votos. Vuelve mañana.", # LANG.tooltip_too_many_votes + + // screenshots + 'prepError' => "[An error occured preparing your screenshot]", + 'cropHint' => "[Crop the image by dragging the selection.
Please refer to Screenshots: Tips & Tricks for an optimal layout.]", + 'caption' => "[Caption]", + 'originalSize' => "[Original size]", + 'targetSize' => "[Target size]", + 'minSize' => "[Minimum size]", + 'displayOn' => "[Displayed on: %s[br][%s=%d]]", + 'ssEdit' => "[Edit uploaded screenshot]", + 'ssUpload' => "[Screenshot Upload]", + 'ssSubmit' => "[Submit Screenshot]", + 'ssErrors' => array( + 'noUpload' => "[The file was not uploaded!]", + 'maxSize' => "[The file exceeds the maximum size of %s!]", + 'interrupted' => "[The upload process was interrupted!]", + 'noFile' => "[The file was not received!]", + 'noDest' => "[The page this screenshot should be displayed on, does not exist!]", + 'notAllowed' => "[You are not allowed to upload screenshots!]", + 'noImage' => "[The uploaded file is not an image file!]", + 'wrongFormat' => "[The image file must be a png or jpg!]", + 'load' => "[The image file could not be loaded!]", + 'tooSmall' => "[The image size is too small! (lower than %d x %d)]", + 'tooLarge' => "[The image size is too large! (greater than %d x %d)]" + ) ), 'game' => array( 'achievement' => "logro", @@ -334,8 +362,6 @@ $lang = array( 'passMismatch' => "The passwords you entered do not match.", 'nameInUse' => "That username is already taken.", 'mailInUse' => "That email is already registered to an account.", - 'intError' => "An internal error occured.", - 'intError2' => "An internal error occured. (%s)", 'isRecovering' => "This account is already recovering. Follow the instructions in your email or wait %s for the token to expire.", 'passCheckFail' => "Las contraseñas no son iguales.", // message_passwordsdonotmatch 'newPassDiff' => "Su nueva contraseña tiene que ser diferente a Su contraseña anterior." // message_newpassdifferent diff --git a/localization/locale_frfr.php b/localization/locale_frfr.php index 1cf1f189..89baf887 100644 --- a/localization/locale_frfr.php +++ b/localization/locale_frfr.php @@ -50,6 +50,7 @@ $lang = array( 'aboutUs' => "À propos de Aowow", 'and' => " et ", 'or' => " ou ", + 'back' => "Redro", // filter 'extSearch' => "Recherche avancée", @@ -136,9 +137,36 @@ $lang = array( 'dateFmtLong' => "Y-m-d à H:i", // error + 'intError' => "[An internal error occured.]", + 'intError2' => "[An internal error occured. (%s)]", 'genericError' => "Une erreur est survenue; Actualisez la page et essayez à nouveau. Si l'erreur persiste, envoyez un email à feedback", # LANG.genericerror 'bannedRating' => "Vous avez été banni du score des commentaires.", # LANG.tooltip_banned_rating - 'tooManyVotes' => "Vous avez voté trop souvent aujourd'hui! Revenez demain." # LANG.tooltip_too_many_votes + 'tooManyVotes' => "Vous avez voté trop souvent aujourd'hui! Revenez demain.", # LANG.tooltip_too_many_votes + + // screenshots + 'prepError' => "[An error occured preparing your screenshot]", + 'cropHint' => "[Crop the image by dragging the selection.
Please refer to Screenshots: Tips & Tricks for an optimal layout.]", + 'caption' => "[Caption]", + 'originalSize' => "[Original size]", + 'targetSize' => "[Target size]", + 'minSize' => "[Minimum size]", + 'displayOn' => "[Displayed on: %s[br][%s=%d]]", + 'ssEdit' => "[Edit uploaded screenshot]", + 'ssUpload' => "[Screenshot Upload]", + 'ssSubmit' => "[Submit Screenshot]", + 'ssErrors' => array( + 'noUpload' => "[The file was not uploaded!]", + 'maxSize' => "[The file exceeds the maximum size of %s!]", + 'interrupted' => "[The upload process was interrupted!]", + 'noFile' => "[The file was not received!]", + 'noDest' => "[The page this screenshot should be displayed on, does not exist!]", + 'notAllowed' => "[You are not allowed to upload screenshots!]", + 'noImage' => "[The uploaded file is not an image file!]", + 'wrongFormat' => "[The image file must be a png or jpg!]", + 'load' => "[The image file could not be loaded!]", + 'tooSmall' => "[The image size is too small! (lower than %d x %d)]", + 'tooLarge' => "[The image size is too large! (greater than %d x %d)]" + ) ), 'game' => array( 'achievement' => "haut fait", @@ -333,8 +361,6 @@ $lang = array( 'passMismatch' => "The passwords you entered do not match.", 'nameInUse' => "That username is already taken.", 'mailInUse' => "That email is already registered to an account.", - 'intError' => "An internal error occured.", - 'intError2' => "An internal error occured. (%s)", 'isRecovering' => "This account is already recovering. Follow the instructions in your email or wait %s for the token to expire.", 'passCheckFail' => "Les mots de passe ne correspondent pas.", // message_passwordsdonotmatch 'newPassDiff' => "Votre nouveau mot de passe doit être différent de l'ancien." // message_newpassdifferent diff --git a/localization/locale_ruru.php b/localization/locale_ruru.php index 4e872d6d..a87d98f0 100644 --- a/localization/locale_ruru.php +++ b/localization/locale_ruru.php @@ -50,6 +50,7 @@ $lang = array( 'aboutUs' => "О Aowow", 'and' => " и ", 'or' => " или ", + 'back' => "Назад", // filter 'extSearch' => "Расширенный поиск", @@ -136,9 +137,36 @@ $lang = array( 'dateFmtLong' => "Y-m-d в H:i", // error + 'intError' => "[An internal error occured.]", + 'intError2' => "[An internal error occured. (%s)]", 'genericError' => "Произошла ошибка; обновите страницу и попробуйте снова. Если ситуация повторяется, отправьте сообщение на feedback", # LANG.genericerror 'bannedRating' => "Вам была заблокирована возможность оценивать комментарии.", # LANG.tooltip_banned_rating - 'tooManyVotes' => "Вы сегодня проголосовали слишком много раз! Вы сможете продолжить завтра." # LANG.tooltip_too_many_votes + 'tooManyVotes' => "Вы сегодня проголосовали слишком много раз! Вы сможете продолжить завтра.", # LANG.tooltip_too_many_votes + + // screenshots + 'prepError' => "[An error occured preparing your screenshot]", + 'cropHint' => "[Crop the image by dragging the selection.
Please refer to Screenshots: Tips & Tricks for an optimal layout.]", + 'caption' => "[Caption]", + 'originalSize' => "[Original size]", + 'targetSize' => "[Target size]", + 'minSize' => "[Minimum size]", + 'displayOn' => "[Displayed on: %s[br][%s=%d]]", + 'ssEdit' => "[Edit uploaded screenshot]", + 'ssUpload' => "[Screenshot Upload]", + 'ssSubmit' => "[Submit Screenshot]", + 'ssErrors' => array( + 'noUpload' => "[The file was not uploaded!]", + 'maxSize' => "[The file exceeds the maximum size of %s!]", + 'interrupted' => "[The upload process was interrupted!]", + 'noFile' => "[The file was not received!]", + 'noDest' => "[The page this screenshot should be displayed on, does not exist!]", + 'notAllowed' => "[You are not allowed to upload screenshots!]", + 'noImage' => "[The uploaded file is not an image file!]", + 'wrongFormat' => "[The image file must be a png or jpg!]", + 'load' => "[The image file could not be loaded!]", + 'tooSmall' => "[The image size is too small! (lower than %d x %d)]", + 'tooLarge' => "[The image size is too large! (greater than %d x %d)]" + ) ), 'game' => array( 'achievement' => "достижение", @@ -333,8 +361,6 @@ $lang = array( 'passMismatch' => "The passwords you entered do not match.", 'nameInUse' => "That username is already taken.", 'mailInUse' => "That email is already registered to an account.", - 'intError' => "An internal error occured.", - 'intError2' => "An internal error occured. (%s)", 'isRecovering' => "This account is already recovering. Follow the instructions in your email or wait %s for the token to expire.", 'passCheckFail' => "Пароли не совпадают.", // message_passwordsdonotmatch 'newPassDiff' => "Прежний и новый пароли не должны совпадать." // message_newpassdifferent diff --git a/pages/account.php b/pages/account.php index 07b2369f..739208bd 100644 --- a/pages/account.php +++ b/pages/account.php @@ -323,7 +323,7 @@ Markup.printHtml("description text here", "description-generic", { allow: Markup { case AUTH_OK: if (!User::$ip) - return Lang::$account['intError']; + return Lang::$main['intError']; // reset account status, update expiration DB::Aowow()->query('UPDATE ?_account SET prevIP = IF(curIp = ?, prevIP, curIP), curIP = IF(curIp = ?, curIP, ?), allowExpire = ?d, status = 0, statusTimer = 0, token = "" WHERE user = ?', @@ -354,7 +354,7 @@ Markup.printHtml("description text here", "description-generic", { allow: Markup return sprintf(Lang::$account['loginExceeded'], Util::formatTime(CFG_FAILED_AUTH_EXCLUSION * 1000)); case AUTH_INTERNAL_ERR: User::destroy(); - return Lang::$account['intError']; + return Lang::$main['intError']; default: return; } @@ -391,7 +391,7 @@ Markup.printHtml("description text here", "description-generic", { allow: Markup // check ip if (!User::$ip) - return Lang::$account['intError']; + return Lang::$main['intError']; // limit account creation $ip = DB::Aowow()->selectRow('SELECT ip, count, unbanDate FROM ?_account_bannedips WHERE type = 1 AND ip = ?', User::$ip); @@ -420,7 +420,7 @@ Markup.printHtml("description text here", "description-generic", { allow: Markup $token ); if (!$id) // something went wrong - return Lang::$account['intError']; + return Lang::$main['intError']; else if ($_ = $this->sendMail($email, Lang::$mail['accConfirm'][0], sprintf(Lang::$mail['accConfirm'][1], $token), CFG_ACCOUNT_CREATE_SAVE_DECAY)) { // success:: update ip-bans @@ -460,7 +460,7 @@ Markup.printHtml("description text here", "description-generic", { allow: Markup return Lang::$account['newPassDiff']; if (!DB::Aowow()->query('UPDATE ?_account SET passHash = ?, status = ?d WHERE id = ?d', User::hashcrypt($newPass), ACC_STATUS_OK, $uRow['id'])) - return Lang::$account['intError']; + return Lang::$main['intError']; } private function doRecoverUser($target) @@ -475,7 +475,7 @@ Markup.printHtml("description text here", "description-generic", { allow: Markup private function initRecovery($type, $target, $delay, &$token) { if (!$type) - return Lang::$account['intError']; + return Lang::$main['intError']; // check if already processing if ($_ = DB::Aowow()->selectCell('SELECT statusTimer - UNIX_TIMESTAMP() FROM ?_account WHERE email = ? AND status <> ?d AND statusTimer > UNIX_TIMESTAMP()', $target, ACC_STATUS_OK)) @@ -484,7 +484,7 @@ Markup.printHtml("description text here", "description-generic", { allow: Markup // create new token and write to db $token = Util::createHash(); if (!DB::Aowow()->query('UPDATE ?_account SET token = ?, status = ?d, statusTimer = UNIX_TIMESTAMP() + ?d WHERE email = ?', $token, $type, $delay, $target)) - return Lang::$account['intError']; + return Lang::$main['intError']; } private function sendMail($target, $subj, $msg, $delay = 300) @@ -497,7 +497,7 @@ Markup.printHtml("description text here", "description-generic", { allow: Markup 'X-Mailer: PHP/' . phpversion(); if (!mail($target, $subj, $msg, $header)) - return sprintf(Lang::$account['intError2'], 'send mail'); + return sprintf(Lang::$main['intError2'], 'send mail'); } private function getNext($forHeader = false) diff --git a/pages/admin.php b/pages/admin.php index 689e5ca2..f189ff0e 100644 --- a/pages/admin.php +++ b/pages/admin.php @@ -19,6 +19,14 @@ class AdminPage extends GenericPage { switch ($pageParam) { + case 'screenshots': + $this->reqUGroup = U_GROUP_STAFF | U_GROUP_SCREENSHOT; + $this->generator = 'handleScreenshots'; + $this->tpl = 'admin/screenshots'; + + array_push($this->path, 1, 5); + $this->name = 'Screenshot Manager'; + break; case 'phpinfo': $this->reqUGroup = U_GROUP_ADMIN | U_GROUP_DEV; $this->generator = 'handlePhpInfo'; @@ -439,6 +447,45 @@ class AdminPage extends GenericPage } } + private function handleScreenshots() + { + $this->addJS('screenshot.js'); + $this->addCSS(array( + ['string' => '.layout {margin: 0px 25px; max-width: inherit; min-width: 1200px; }'], + ['string' => '#highlightedRow { background-color: #322C1C; }'] + )); + + $ssGetAll = isset($_GET['all']) && empty($_GET['all']); + $ssPages = []; + $ssData = []; + $nMatches = 0; + + if (!empty($_GET['type']) && !empty($_GET['typeid'])) + { + $ssData = CommunityContent::getScreenshotsForManager(intVal($_GET['type']), intVal($_GET['typeid'])); + $nMatches = count($ssData); + } + else if (!empty($_GET['user'])) + { + $name = urldecode($_GET['user']); + if (strlen($name) > 3) + { + if ($uId = DB::Aowow()->selectCell('SELECT id FROM ?_account WHERE displayName = ?', ucFirst($name))) + { + $ssData = CommunityContent::getScreenshotsForManager(0, 0, $uId); + $nMatches = count($ssData); + } + } + } + else + $ssPages = CommunityContent::getScreenshotPagesForManager($ssGetAll, $nMatches); + + $this->getAll = $ssGetAll; + $this->ssPages = $ssPages; + $this->ssData = $ssData; + $this->ssNFound = $nMatches; // ssm_numPagesFound + } + private function configAddRow($r) { $buff = ''; @@ -487,7 +534,7 @@ class AdminPage extends GenericPage else $buff .= '|'; - if (!($r['flags'] & CON_FLAG_PERSISTANT)) + if (!($r['flags'] & CON_FLAG_PERSISTENT)) $buff .= '|'; $buff .= ''; diff --git a/pages/screenshot.php b/pages/screenshot.php new file mode 100644 index 00000000..20117fb9 --- /dev/null +++ b/pages/screenshot.php @@ -0,0 +1,301 @@ +[_original].jpg + +class ScreenshotPage extends GenericPage +{ + protected $tpl = 'screenshot'; + protected $js = ['Cropper.js']; + protected $css = [['path' => 'Cropper.css']]; + protected $reqAuth = true; + + private $tmpPath = 'static/uploads/temp/'; + private $pendingPath = 'static/uploads/screenshots/pending/'; + private $destination = null; + protected $destType = 0; + protected $destTypeId = 0; + + public function __construct($pageCall, $pageParam) + { + parent::__construct($pageCall, $pageParam); + + $this->name = Lang::$main['ssEdit']; + $this->caption = @$_POST['screenshotcaption']; // do not htmlEscape. It's applied as textnode + + // what are its other uses..? (finalize is custom) + if ($pageParam == 'finalize') + { + if (!$this->handleFinalize()) + $this->error(); + } + else if ($pageParam != 'add') + $this->error(); + + // get screenshot destination + foreach ($_GET as $k => $v) + { + if ($v) // taret delivered as empty type.typeId key + continue; + + $x = explode('_', $k); // . => _ as array key + if (count($x) != 2) + continue; + + // no such type + if (empty(Util::$typeClasses[$x[0]])) + continue; + + $t = Util::$typeClasses[$x[0]]; + $c = [['id', intVal($x[1])]]; + if ($x[0] == TYPE_WORLDEVENT) // ohforfsake.. + $c = array_merge($c, ['OR', ['holidayId', intVal($x[1])]]); + + $this->destination = new $t($c); + + // no such typeId + if ($this->destination->error) + continue; + + $this->destType = intVal($x[0]); + $this->destTypeId = intVal($x[1]); + } + } + + private function handleFinalize() + { + if (empty($_SESSION['ssUpload'])) + return false; + + // as its saved in session it should be valid + $file = $_SESSION['ssUpload']['file']; + + // check tmp file + $fullPath = $this->tmpPath.$file.'_original.jpg'; + if (!file_exists($fullPath)) + return false; + + // check post data + if (empty($_POST) || empty($_POST['selection'])) + return false; + + $dims = explode(',', $_POST['selection']); + if (count($dims) != 4) + return false; + + Util::checkNumeric($dims); + + // actually crop the image + $srcImg = imagecreatefromjpeg($fullPath); + + $x = (int)(imagesx($srcImg) * $dims[0]); + $y = (int)(imagesy($srcImg) * $dims[1]); + $w = (int)(imagesx($srcImg) * $dims[2]); + $h = (int)(imagesy($srcImg) * $dims[3]); + + $destImg = imagecreatetruecolor($w, $h); + + imagefill($destImg, 0, 0, imagecolorallocate($destImg, 255, 255, 255)); + imagecopy($destImg, $srcImg, 0, 0, $x, $y, $w, $h); + imagedestroy($srcImg); + + // write to db + $newId = DB::Aowow()->query( + 'INSERT INTO ?_screenshots (type, typeId, uploader, date, width, height, caption) VALUES (?d, ?d, ?d, UNIX_TIMESTAMP(), ?d, ?d, ?)', + $_SESSION['ssUpload']['type'], $_SESSION['ssUpload']['typeId'], + User::$id, + $w, $h, + $this->caption + ); + + // write to file + if (is_int($newId)) // 0 is valid, NULL or FALSE is not + imagejpeg($destImg, $this->pendingPath.$newId.'.jpg', 100); + + unset($_SESSION['ssUpload']); + header('Location: ?user='.User::$displayName.'#screenshots'); + } + + protected function generateContent() + { + $maxW = 488; + $maxH = 325; + $minCrop = CFG_SCREENSHOT_MIN_SIZE; + + if ($minCrop <= 0) + { + Util::addNote(U_GROUP_DEV | U_GROUP_ADMIN, 'ScreenshotPage::generateContent() - config error: dimensions for uploaded screenshots egual or less than zero. Value forced to 200'); + $minCrop = 200; + } + + if (!$this->destType) + { + $this->error = Lang::$main['ssErrors']['noDest']; + return; + } + + if (User::$banStatus & ACC_BAN_SCREENSHOT) + { + $this->error = Lang::$main['ssErrors']['notAllowed']; + return; + } + + if ($_ = $this->validateScreenshot($isPNG)) + { + $this->error = $_; + return; + } + + $im = $isPNG ? $this->loadFromPNG() : $this->loadFromJPG(); + if (!$im) + { + $this->error = Lang::$main['ssErrors']['load']; + return; + } + + $name = User::$displayName.'-'.$this->destType.'-'.$this->destTypeId.'-'.Util::createHash(16); + $oSize = $rSize = [imagesx($im), imagesy($im)]; + $rel = $oSize[0] / $oSize[1]; + + // not sure if this is the best way + $_SESSION['ssUpload'] = array( + 'file' => $name, + 'type' => $this->destType, + 'typeId' => $this->destTypeId + ); + + // check for oversize and refit to crop-screen + if ($rel >= 1.5 && $oSize[0] > $maxW) + $rSize = [$maxW, $maxW / $rel]; + else if ($rel < 1.5 && $oSize[1] > $maxH) + $rSize = [$maxH * $rel, $maxH]; + + $this->writeImage($im, $oSize, $name.'_original'); // use this image for work + $this->writeImage($im, $rSize, $name); // use this image to display + + // r: resized; o: original + // r: x <= 488 && y <= 325 while x proportional to y + // mincrop is optional and specifies the minimum resulting image size + $this->cropper = [ + 'url' => $this->tmpPath.$name.'.jpg', + 'parent' => 'ss-container', + 'oWidth' => $oSize[0], + 'rWidth' => $rSize[0], + 'oHeight' => $oSize[1], + 'rHeight' => $rSize[1], + ]; + + $infobox = []; + + // target + $infobox[] = sprintf(Lang::$main['displayOn'], Util::ucFirst(Lang::$game[Util::$typeStrings[$this->destType]]), Util::$typeStrings[$this->destType], $this->destTypeId); + $this->extendGlobalIds($this->destType, $this->destTypeId); + + // dimensions + $infobox[] = Lang::$main['originalSize'].Lang::$main['colon'].$oSize[0].' x '.$oSize[1]; + $infobox[] = Lang::$main['targetSize'].Lang::$main['colon'].'[span id=qf-newSize][/span]'; + + // minimum dimensions + if (!User::isInGroup(U_GROUP_STAFF)) + { + $infobox[] = Lang::$main['minSize'].Lang::$main['colon'].$minCrop.' x '.$minCrop; + $this->cropper['minCrop'] = $minCrop; + } + + $this->infobox = '[ul][li]'.implode('[/li][li]', $infobox).'[/li][/ul]'; + } + + private function loadFromPNG() + { + $image = imagecreatefrompng($_FILES['screenshotfile']['tmp_name']); + $bg = imagecreatetruecolor(imagesx($image), imagesy($image)); + + imagefill($bg, 0, 0, imagecolorallocate($bg, 255, 255, 255)); + imagealphablending($bg, true); + imagecopy($bg, $image, 0, 0, 0, 0, imagesx($image), imagesy($image)); + imagedestroy($image); + + return $bg; + } + + private function loadFromJPG() + { + return imagecreatefromjpeg($_FILES['screenshotfile']['tmp_name']); + } + + private function writeImage($im, $dims, $file) + { + if ($res = imagecreatetruecolor($dims[0], $dims[1])) + if (imagecopyresampled($res, $im, 0, 0, 0, 0, $dims[0], $dims[1], imagesx($im), imagesy($im))) + if (imagejpeg($res, $this->tmpPath.$file.'.jpg', 100)) + return true; + + return false; + } + + private function validateScreenshot(&$isPNG = false) + { + // no upload happened or some error occured + if (!$_FILES || empty($_FILES['screenshotfile'])) + return Lang::$main['ssErrors']['noUpload']; + + switch ($_FILES['screenshotfile']['error']) + { + case 1: + return sprintf(Lang::$main['ssErrors']['maxSize'], ini_get('upload_max_filesize'));; + case 3: + return Lang::$main['ssErrors']['interrupted']; + case 4: + return Lang::$main['ssErrors']['noFile']; + case 6: + Util::addNote(U_GROUP_ADMIN, 'ScreenshotPage::validateScreenshot() - temporary upload directory is not set'); + return Lang::$main['intError']; + case 7: + Util::addNote(U_GROUP_ADMIN, 'ScreenshotPage::validateScreenshot() - could not write temporary file to disk'); + return Lang::$main['genericError']; + } + + // points to invalid file (hack attempt) + if (!is_uploaded_file($_FILES['screenshotfile']['tmp_name'])) + { + Util::addNote(U_GROUP_ADMIN, 'ScreenshotPage::validateScreenshot() - uploaded file not in upload directory'); + return Lang::$main['genericError']; + } + + // invalid file + $is = getimagesize($_FILES['screenshotfile']['tmp_name']); + if (!$is || empty($is['mime'])) + return Lang::$main['ssErrors']['notImage']; + + // allow jpeg, png + switch ($is['mime']) + { + case 'image/png': + $isPNG = true; + case 'image/jpg': + case 'image/jpeg': + break; + default: + return Lang::$main['ssErrors']['wrongFormat']; + } + + // size-missmatch: 4k UHD upper limit; 150px lower limit + if ($is[0] < 150 || $is[1] < 150) + return sprintf(Lang::$main['ssErrors']['tooSmall'], 150, 150); + + if ($is[0] > 3840 || $is[1] > 2160) + return sprintf(Lang::$main['ssErrors']['tooLarge'], 150, 150); + + return null; + } + + protected function generatePath() { } + protected function generateTitle() + { + array_unshift($this->title, Lang::$main['ssUpload']); + } +} + +?> diff --git a/setup/updates/03_screenshots.sql b/setup/updates/03_screenshots.sql new file mode 100644 index 00000000..179ca830 --- /dev/null +++ b/setup/updates/03_screenshots.sql @@ -0,0 +1,16 @@ +DROP TABLE IF EXISTS `aowow_screenshots`; +CREATE TABLE IF NOT EXISTS `aowow_screenshots` ( + `id` int(16) unsigned NOT NULL AUTO_INCREMENT, + `type` tinyint(4) unsigned NOT NULL, + `typeId` mediumint(9) NOT NULL, + `uploader` int(16) unsigned NOT NULL, + `date` int(32) unsigned NOT NULL, + `width` smallint(5) unsigned NOT NULL, + `height` smallint(5) unsigned NOT NULL, + `caption` varchar(250) DEFAULT NULL, + `status` tinyint(3) unsigned NOT NULL COMMENT 'see defines.php - CC_FLAG_*', + `approvedBy` int(16) unsigned DEFAULT NULL, + `deletedBy` int(16) unsigned DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `type` (`type`,`typeId`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; diff --git a/static/css/Cropper.css b/static/css/Cropper.css new file mode 100644 index 00000000..6ae9c313 --- /dev/null +++ b/static/css/Cropper.css @@ -0,0 +1,129 @@ +.cropper { + width:488px; + height:325px; + border:3px solid #404040; + text-align:center; + background:black; +} + +.canvas { + position:relative; + margin:0 auto; +} + +.selection { + position:absolute; + cursor:move; +} + +.selection .opac { + position:absolute; + left:0; + top:0; + width:100%; + height:100%; + background-color:#369; + filter:alpha(opacity=40); + opacity:.4; +} + +.selection div { + position:absolute; + font-size:1px; +} + +.selection .grabw { + left:-4px; + top:0; + width:9px; + height:100%; + cursor:w-resize; +} + +.selection .grabe { + right:-4px; + top:0; + width:9px; + height:100%; + cursor:w-resize; +} + +.selection .grabn { + left:0; + top:-4px; + width:100%; + height:9px; + cursor:n-resize; +} + +.selection .grabs { + left:0; + bottom:-4px; + width:100%; + height:9px; + cursor:n-resize; +} + +.selection .grabnw { + left:-3px; + top:-3px; + width:10px; + height:10px; + cursor:nw-resize; +} + +.selection .grabne { + right:-3px; + top:-3px; + width:10px; + height:10px; + cursor:ne-resize; +} + +.selection .grabsw { + left:-3px; + bottom:-3px; + width:10px; + height:10px; + cursor:ne-resize; +} + +.selection .grabse { + right:-3px; + bottom:-3px; + width:10px; + height:10px; + cursor:nw-resize; +} + +.selection .hborder { + left:0; + top:0; + width:100%; + height:1px; + background:url(//wowimg.zamimg.com/images/ui/misc/selection-h.gif) repeat-x top; +} + +.selection .hborder2 { + left:0; + bottom:0; + width:100%; + height:1px; + background:url(//wowimg.zamimg.com/images/ui/misc/selection-h.gif) repeat-x bottom; +} + +.selection .vborder { + left:0; + top:0; + width:1px; + height:100%; + background:url(//wowimg.zamimg.com/images/ui/misc/selection-v.gif) repeat-y left; +} + +.selection .vborder2 { + right:0; + top:0; + width:1px; + height:100%; + background:url(//wowimg.zamimg.com/images/ui/misc/selection-v.gif) repeat-y right; +} diff --git a/static/js/Cropper.js b/static/js/Cropper.js new file mode 100644 index 00000000..2e853483 --- /dev/null +++ b/static/js/Cropper.js @@ -0,0 +1,325 @@ +function Cropper(opts) { + var + canvas, + div, + _; + + $WH.cO(this, opts); + + if (this.parent) { + this.parent = $WH.ge(this.parent); + } + else { + return; + } + + this.locked = 0; + this.selection = { node: $WH.ce('div') }; + this.canvas = { node: $WH.ce('div') }; + + if (this.minCrop == null) { + this.minCrop = [150, 150]; + } + else if (!isNaN(this.minCrop)) { + this.minCrop = [(this.minCrop || 150), (this.minCrop || 150)]; + } + + this.minCrop[0] = Math.ceil(this.minCrop[0] * this.rWidth / this.oWidth); + this.minCrop[1] = Math.ceil(this.minCrop[1] * this.rHeight / this.oHeight); + + canvas = this.container = $WH.ce('div'); + canvas.className = 'cropper'; + canvas = this.canvas.node; + canvas.className = 'canvas'; + + if (this.rHeight < 325 && !this.blog) { + canvas.style.marginTop = Math.floor((325 - this.rHeight) / 2) + 'px' + } + + canvas.style.width = this.rWidth + 'px'; + canvas.style.height = this.rHeight + 'px'; + canvas.style.backgroundImage = 'url(' + this.url + ')'; + $WH.ns(canvas); + + var + f = (this.minCrop[0] > this.minCrop[1] ? 0 : 1), + e = (this.oWidth > this.oHeight ? 0 : 1), + h = [this.oWidth, this.oHeight], + g = this.minCrop[f] / this.minCrop[1 - f], + l = []; + + if (Math.floor(h[e] / 2) >= this.minCrop[e]) { + l[f] = 100; + } + else { + l[f] = Math.ceil(this.minCrop[f] / h[f] * 100 * 1000) / 1000; + } + + l[1 - f] = l[f] / g; + if ((this.type == 1 && this.typeId == 15384) && g_user.roles & (U_GROUP_MODERATOR | U_GROUP_EDITOR)) { + this.minCrop[0] = 1; + this.minCrop[1] = 1; + l = [100, 100]; + } + + canvas = this.selection.node; + canvas.className = 'selection'; + canvas.style.width = l[0] + '%'; + canvas.style.height = l[1] + '%'; + canvas.style.left = (100 - l[0]) / 2 + '%'; + canvas.style.top = (100 - l[1]) / 2 + '%'; + + div = $WH.ce('div'); + div.className = 'opac'; + div.onmousedown = Cropper.lock.bind(this, 5); + canvas.appendChild(div); + + _ = ['hborder', 'hborder2', 'vborder', 'vborder2']; + for (var i = 0, len = _.length; i < len; ++i) { + div = $WH.ce('div'); + div.className = _[i]; + canvas.appendChild(div); + } + + _ = [['w', 4], ['e', 6], ['n', 8], ['s', 2], ['nw', 7], ['ne', 9], ['sw', 1], ['se', 3]]; + for (var i = 0, len = _.length; i < len; ++i) { + div = $WH.ce('div'); + div.className = 'grab' + _[i][0]; + div.onmousedown = Cropper.lock.bind(this, _[i][1]); + canvas.appendChild(div); + } + + this.canvas.node.appendChild(this.selection.node); + this.container.appendChild(this.canvas.node); + this.parent.appendChild(this.container); + + $WH.aE(document, 'mousedown', Cropper.mouseDown.bind(this)); + $WH.aE(document, 'mouseup', Cropper.mouseUp.bind(this)); + $WH.aE(document, 'mousemove', Cropper.mouseMove.bind(this)); +} + +Cropper.prototype = { + refreshCoords: function () { + this.selection.coords = $WH.ac(this.selection.node); + + this.selection.size = [this.selection.node.offsetWidth, this.selection.node.offsetHeight]; + + this.canvas.coords = $WH.ac(this.canvas.node); + }, + + getCoords: function () { + this.refreshCoords(); + + var + left = this.selection.coords[0] - this.canvas.coords[0], + top = this.selection.coords[1] - this.canvas.coords[1], + width = this.selection.size[0], + height = this.selection.size[1]; + + var + w = this.rWidth, + h = this.rHeight; + + return [ + (left / w).toFixed(3), + (top / h).toFixed(3), + (width / w).toFixed(3), + (height / h).toFixed(3) + ].join(','); + }, + + moveSelection: function (left, top, width, height) { + this.selection.node.style.left = left + 'px'; + this.selection.node.style.top = top + 'px'; + this.selection.node.style.width = width + 'px'; + this.selection.node.style.height = height + 'px'; + }, + + selectAll: function () { + this.moveSelection(0, 0, this.rWidth, this.rHeight); + } +}; + +Cropper.lock = function (locked) { + this.locked = locked; + + return false; +}; + +Cropper.mouseDown = function (ev) { + ev = $WH.$E(ev); + + this.drag = 1; + this.anchorX = ev.clientX; + this.anchorY = ev.clientY; + this.refreshCoords(); +}; + +Cropper.mouseUp = function (ev) { + ev = $WH.$E(ev); + + this.drag = this.locked = 0; +}; + +Cropper.mouseMove = function (ev) { + if (this.drag && this.locked) { + ev = $WH.$E(ev); + + var + left = this.selection.coords[0] - this.canvas.coords[0], + top = this.selection.coords[1] - this.canvas.coords[1], + width = left + this.selection.size[0], + height = top + this.selection.size[1]; + + var + x = (ev.clientX - this.anchorX), + y = (ev.clientY - this.anchorY); + + var locks = null; + + if (this.locked == 5) { + left += x; + width += x; + top += y; + height += y; + } + else { + if (this.locked == 1 || this.locked == 4 || this.locked == 7) { + left += Math.max(x, -left); + left = Math.min(left, this.rWidth); + + if (this.locked == 4) { + locks = 'x'; + } + else { + locks = 'xy'; + } + + if (Math.abs(left - width) < this.minCrop[0]) { + if (left > width) { + left = width + this.minCrop[0]; + } + else { + left = width - this.minCrop[0]; + } + } + } + else if (this.locked == 3 || this.locked == 6 || this.locked == 9) { + width += Math.min(x, this.rWidth - width); + width = Math.max(width, 0); + + if (this.locked == 6) { + locks = 'x'; + } + else { + locks = 'xy'; + } + + if (Math.abs(left - width) < this.minCrop[0]) { + if (width > left) { + width = left + this.minCrop[0]; + } + else { + width = left - this.minCrop[0]; + } + } + } + + if (this.locked == 1 || this.locked == 2 || this.locked == 3) { + height += Math.min(y, this.rHeight - height); + height = Math.max(height, 0); + + if (this.locked == 2) { + locks = 'y'; + } + else { + locks = 'xy'; + } + + if (Math.abs(top - height) < this.minCrop[1]) { + if (height > top) { + height = top + this.minCrop[1]; + } + else { + height = top - this.minCrop[1]; + } + } + } + else if (this.locked == 7 || this.locked == 8 || this.locked == 9) { + top += Math.max(y, -top); + top = Math.min(top, this.rHeight); + + if (this.locked == 8) { + locks = 'y'; + } + else { + locks = 'xy'; + } + + if (Math.abs(top - height) < this.minCrop[1]) { + if (top > height) { + top = height + this.minCrop[1]; + } + else { + top = height - this.minCrop[1]; + } + } + } + } + + if (left > width) { + var _ = left; + left = width; + width = _; + } + + if (top > height) { + var _ = top; + top = height; + height = _; + } + + if (this.constraint) { + var + absX = Math.abs(x), + absY = Math.abs(y); + + if (locks == 'x' || (locks == 'xy' && absX > absY)) { + var b = width - left; + var k = this.constraint[1] / this.constraint[0]; + var p = b * k; + + height = top + p; + } + else if (locks == 'y' || (locks == 'xy' && absX < absY)) { + var p = height - top; + var k = this.constraint[0] / this.constraint[1]; + var b = p * k; + + width = left + b; + } + } + + if (left < 0) { + width -= left; + left = 0; + } + + if (top < 0) { + height -= top; + top = 0; + } + + if (width > this.rWidth) { + left -= (width - this.rWidth); + width = this.rWidth; + } + + if (height > this.rHeight) { + top -= (height - this.rHeight); + height = this.rHeight; + } + + this.moveSelection(left, top, width - left, height - top); + } +}; \ No newline at end of file diff --git a/static/js/screenshot.js b/static/js/screenshot.js new file mode 100644 index 00000000..8515855a --- /dev/null +++ b/static/js/screenshot.js @@ -0,0 +1 @@ +var ss_managedRow = null; var ss_getAll = false; // never changed (maybe with ?admin=screenshots&all) var ssm_ViewedRow = null; var ssm_screenshotData = []; var ssm_screenshotPages = []; var ssm_numPagesFound = 0; var ssm_numPages = 0; // never accessed var ssm_numPending = 0; var ssm_statuses = { 0 : 'Pending', 999: 'Deleted', 100: 'Approved', 105: 'Sticky' }; function makePipe() { var sp = $WH.ce('span'); $WH.ae(sp, $WH.ct(' ')); var sm = $WH.ce('small'); sm.className = 'q0'; $WH.ae(sm, $WH.ct('|')); $WH.ae(sp, sm); $WH.ae(sp, $WH.ct(' ')); return sp; } function ss_OnResize() { var _ = Math.max(100, Math.min($WH.g_getWindowSize().h - 50, 700)); $WH.ge('menu-container').style.height = $WH.ge('pages-container').style.height = _ + 'px'; $WH.ge('data-container').style.height = _ + 'px'; } $WH.aE(window, 'resize', ss_OnResize); function ss_Refresh(openNext, type, typeId) { new Ajax('?admin=screenshots&action=list' + (ss_getAll ? '&all' : ''), { method: 'get', onSuccess: function (xhr) { eval(xhr.responseText); if (ssm_screenshotPages.length > 0) { $WH.ge('show-all-pages').innerHTML = ' – Show All (' + ssm_numPagesFound + ')'; ssm_UpdatePages(); if (openNext) { ss_Manage($WH.ge('pages-container').firstChild.firstChild, ssm_screenshotPages[0].type, ssm_screenshotPages[0].typeId, true); } else if (type && typeId) { ss_Manage(null, type, typeId, true); } } else { $WH.ee($WH.ge('show-all-pages')); $WH.ge('pages-container').innerHTML = 'NO SCREENZSHOT NEEDS 2 BE APPRVED NOW KTHX. :)'; if (type && typeId) { ss_Manage(null, type, typeId, true); } } } }); } function ss_Manage(_this, type, typeId, openNext) { new Ajax('?admin=screenshots&action=manage&type=' + type + '&typeid=' + typeId, { method: 'get', onSuccess: function (xhr) { eval(xhr.responseText); ssm_numPending = 0; for (var i in ssm_screenshotData) { if (ssm_screenshotData[i].pending) { ssm_numPending++; } } var nRows = ssm_screenshotData.length; $WH.ge('screenshotTotal').innerHTML = nRows + ' total' + (nRows == 100 ? ' (limit reached)' : ''); ssm_UpdateList(openNext); ssm_UpdateMassLinks(); if (ss_managedRow != null) { ss_ColorizeRow('transparent'); } ss_managedRow = _this; if (ss_managedRow != null) { ss_ColorizeRow('#282828'); } } }); } function ss_ManageUser() { var username = $WH.ge('usermanage'); username.value = $WH.trim(username.value); if (username.value.length < 4) { alert('Username must be at least 4 characters long.'); username.focus(); return false; } if (username.value.match(/[^a-z0-9]/i) != null) { alert('Username can only contain letters and numbers.'); username.focus(); return false; } new Ajax('?admin=screenshots&action=manage&user=' + username.value, { method: 'get', onSuccess: function (xhr) { eval(xhr.responseText); var nRows = ssm_screenshotData.length; $WH.ge('screenshotTotal').innerHTML = nRows + ' total' + (nRows == 100 ? ' (limit reached)' : ''); ssm_UpdateList(); ssm_UpdateMassLinks(); if (ss_managedRow != null) { ss_ColorizeRow('transparent'); } } }); return true; } function ss_ColorizeRow(color) { for (var i = 0; i < ss_managedRow.childNodes.length; ++i) { ss_managedRow.childNodes[i].style.backgroundColor = color; } } function ssm_GetScreenshot(id) { for (var i in ssm_screenshotData) { if (ssm_screenshotData[i].id == id) { return ssm_screenshotData[i]; } } return null; } function ssm_View(row, id) { if (ssm_ViewedRow != null) { ssm_ColorizeRow('transparent'); } ssm_ViewedRow = row; ssm_ColorizeRow('#282828'); var screenshot = ssm_GetScreenshot(id); if (screenshot != null) { ScreenshotManager.show(screenshot); } } function ssm_ColorizeRow(color) { for (var i = 0; i < ssm_ViewedRow.childNodes.length; ++i) { ssm_ViewedRow.childNodes[i].style.backgroundColor = color; } } function ssm_ConfirmMassApprove() { ajaxAnchor(this); // sarjuuk custom - there has to be something in place or we are manually using a script for ajax return false; // return true; } function ssm_ConfirmMassDelete() { if (confirm('Delete selected screenshot(s)?')) // sarjuuk custom - see above ajaxAnchor(this); return false; // return confirm('Delete selected screenshot(s)?'); } function ssm_ConfirmMassSticky() { if (confirm('Sticky selected screenshot(s)?')) // sarjuuk custom - see above ajaxAnchor(this); return false; // return confirm('Sticky selected screenshot(s)?'); } function ssm_UpdatePages(UNUSED) { var pc = $WH.ge('pages-container'); $WH.ee(pc); var tbl = $WH.ce('table'); tbl.className = 'grid'; tbl.style.width = '400px'; var tr = $WH.ce('tr'); var th = $WH.ce('th'); $WH.ae(th, $WH.ct('Page')); $WH.ae(tr, th); th = $WH.ce('th'); $WH.ae(th, $WH.ct('Submitted')); $WH.ae(tr, th); th = $WH.ce('th'); th.align = 'right'; $WH.ae(th, $WH.ct('#')); $WH.ae(tr, th); $WH.ae(tbl, tr); var now = new Date(); for (var i in ssm_screenshotPages) { var ssPage = ssm_screenshotPages[i]; tr = $WH.ce('tr'); tr.onclick = ss_Manage.bind(tr, tr, ssPage.type, ssPage.typeId, true, i); var td = $WH.ce('td'); var a = $WH.ce('a'); a.href = '?' + g_types[ssPage.type] + '=' + ssPage.typeId; a.target = '_blank'; $WH.ae(a, $WH.ct(ssPage.name)); $WH.ae(td, a); $WH.ae(tr, td); td = $WH.ce('td'); var elapsed = new Date(ssPage.date); $WH.ae(td, $WH.ct(g_formatTimeElapsed((now.getTime() - elapsed.getTime()) / 1000) + ' ago')); $WH.ae(tr, td); td = $WH.ce('td'); td.align = 'right'; $WH.ae(td, $WH.ct(ssPage.count)); $WH.ae(tr, td); $WH.ae(tbl, tr); } $WH.ae(pc, tbl); } function ssm_UpdateList(openNext) { var tsl = $WH.ge('theScreenshotsList'); var tBody = false; var i = 1; while (tsl.childNodes.length > i) { if (tsl.childNodes[i].nodeName == 'TR' && tBody) { $WH.de(tsl.childNodes[i]); } else if (tsl.childNodes[i].nodeName == 'TR') { tBody = true; } else { i++; } } var now = new Date(); var ssId = 0; for (var i in ssm_screenshotData) { var screenshot = ssm_screenshotData[i]; var tr = $WH.ce('tr'); if (ssId == 0 && screenshot.pending) { ssId = screenshot.id; tr.id = 'highlightedRow'; } var td = $WH.ce('td'); td.align = 'center'; var a = $WH.ce('a'); a.href = g_staticUrl + '/uploads/screenshots/' + (screenshot.status != 999 && !screenshot.pending ? 'normal' : 'pending') + '/' + screenshot.id + '.jpg'; a.target = '_blank'; a.onclick = function (id, e) { $WH.sp(e); (ssm_View.bind(this, id))(); return false; }.bind(tr, screenshot.id); var img = $WH.ce('img'); img.src = g_staticUrl + '/uploads/screenshots/' + (screenshot.status != 999 && !screenshot.pending ? 'thumb' : 'pending') + '/' + screenshot.id + '.jpg'; img.height = 50; $WH.ae(a, img); $WH.ae(td, a); $WH.ae(tr, td); td = $WH.ce('td'); if (screenshot.status != 999 && !screenshot.pending) { var a = $WH.ce('a'); a.href = '?' + g_types[screenshot.type] + '=' + screenshot.typeId + '#screenshots:id=' + screenshot.id; a.target = '_blank'; a.onclick = function (e) { $WH.sp(e); }; $WH.ae(a, $WH.ct(screenshot.id)); $WH.ae(td, a); } else { $WH.ae(td, $WH.ct(screenshot.id)); } $WH.ae(tr, td); td = $WH.ce('td'); td.id = 'alt-' + screenshot.id; var sp = $WH.ce('span'); sp.style.paddingRight = '8px'; if (screenshot.caption) { var sp2 = $WH.ce('span'); sp2.className = 'q2'; var b = $WH.ce('b'); $WH.ae(b, $WH.ct(screenshot.caption)); $WH.ae(sp2, b); $WH.ae(sp, sp2); } else { var it = $WH.ce('i'); it.className = 'q0'; $WH.ae(it, $WH.ct('NULL')); $WH.ae(sp, it); } $WH.ae(td, sp); sp = $WH.ce('span'); sp.style.whiteSpace = 'nowrap'; var a = $WH.ce('a'); a.href = 'javascript:;'; a.onclick = function (id, e) { $WH.sp(e); (ssm_ShowEdit.bind(this, id))() }.bind(a, screenshot); $WH.ae(a, $WH.ct('Edit')); $WH.ae(sp, a); $WH.ae(sp, makePipe()); a = $WH.ce('a'); a.href = 'javascript:;'; a.onclick = function (id, e) { $WH.sp(e); (ssm_Clear.bind(this, id))() }.bind(a, screenshot); $WH.ae(a, $WH.ct('Clear')); $WH.ae(sp, a); $WH.ae(td, sp); $WH.ae(tr, td); td = $WH.ce('td'); var elapsed = new Date(screenshot.date); $WH.ae(td, $WH.ct(g_formatTimeElapsed((now.getTime() - elapsed.getTime()) / 1000) + ' ago')); $WH.ae(tr, td); td = $WH.ce('td'); a = $WH.ce('a'); a.href = '?user=' + screenshot.user; a.target = '_blank'; a.onclick = function (e) { $WH.sp(e); }; $WH.ae(a, $WH.ct(screenshot.user)); $WH.ae(td, a); $WH.ae(tr, td); td = $WH.ce('td'); $WH.ae(td, $WH.ct(ssm_statuses[screenshot.status])); $WH.ae(tr, td); td = $WH.ce('td'); var cb = $WH.ce('input'); cb.type = 'checkbox'; cb.value = screenshot.id; cb.onclick = function (e) { $WH.sp(e); (ssm_UpdateMassLinks.bind(this))(); }.bind(cb); $WH.ae(td, cb); $WH.ae(td, $WH.ct(' ')); if (screenshot.status != 999) { tr.onclick = function (id) { ssm_View(this, id); return false; }.bind(tr, screenshot.id); if (screenshot.id == ssId && openNext) { ssm_View(tr, screenshot.id); } if (screenshot.pending) { a = $WH.ce('a'); a.href = 'javascript:;'; a.onclick = function (e) { $WH.sp(e); (ssm_Approve.bind(this, false))() }.bind(screenshot); $WH.ae(a, $WH.ct('Approve')); $WH.ae(td, a); } else { $WH.ae(td, $WH.ct('Approve')); } $WH.ae(td, makePipe()); if (screenshot.status != 105) { a = $WH.ce('a'); a.href = 'javascript:;'; a.onclick = function (e) { $WH.sp(e); (ssm_Sticky.bind(this, false))(); }.bind(screenshot); $WH.ae(a, $WH.ct('Make sticky')); $WH.ae(td, a); } else { $WH.ae(td, $WH.ct('Make sticky')); } $WH.ae(td, makePipe()); a = $WH.ce('a'); a.href = 'javascript:;'; a.onclick = function (e) { $WH.sp(e); (ssm_Delete.bind(this, false))(); }.bind(screenshot); $WH.ae(a, $WH.ct('Delete')); $WH.ae(td, a); $WH.ae(td, makePipe()); a = $WH.ce('a'); a.href = 'javascript:;'; a.onclick = function (e) { $WH.sp(e); var id = prompt('Enter the ID to move this screenshot to:'); (ssm_Relocate.bind(this, id))(); }.bind(screenshot); $WH.ae(a, $WH.ct('Relocate')); $WH.ae(td, a); } $WH.ae(tr, td); $WH.ae(tsl, tr); } } function ssm_UpdateMassLinks() { var buff = ''; var i = 0; var tSL = $WH.ge('theScreenshotsList'); var inp = $WH.gE(tSL, 'input'); $WH.array_walk(inp, function (x) { if (x.checked) { buff += x.value + ','; ++i; } }); buff = $WH.rtrim(buff, ','); var selCnt = $WH.ge('withselected'); if (i > 0) { selCnt.style.display = ''; $WH.gE(selCnt, 'b')[0].firstChild.nodeValue = '(' + i + ')'; var c = $WH.ge('massapprove'); var b = $WH.ge('massdelete'); var a = $WH.ge('masssticky'); c.href = '?admin=screenshots&action=approve&id=' + buff; c.onclick = ssm_ConfirmMassApprove; b.href = '?admin=screenshots&action=delete&id=' + buff; b.onclick = ssm_ConfirmMassDelete; a.href = '?admin=screenshots&action=sticky&id=' + buff; a.onclick = ssm_ConfirmMassSticky; } else { selCnt.style.display = 'none'; } } function ssm_MassSelect(action) { var tSL = $WH.ge('theScreenshotsList'); var inp = $WH.gE(tSL, 'input'); switch (parseInt(action)) { case 1: $WH.array_walk(inp, function (x) { x.checked = true }); break; case 0: $WH.array_walk(inp, function (x) { x.checked = false }); break; case -1: $WH.array_walk(inp, function (x) { x.checked = !x.checked }); break; case 2: $WH.array_walk(inp, function (x) { x.checked = ssm_GetScreenshot(x.value).status == 0 }); break; case 5: $WH.array_walk(inp, function (x) { x.checked = ssm_GetScreenshot(x.value).unique == 1 && ssm_GetScreenshot(x.value).status == 0 }); break; case 3: $WH.array_walk(inp, function (x) { x.checked = ssm_GetScreenshot(x.value).status == 100 }); break; case 4: $WH.array_walk(inp, function (x) { x.checked = ssm_GetScreenshot(x.value).status == 105 }); break; default: return; } ssm_UpdateMassLinks(); } function ssm_ShowEdit(screenshot, isAlt) { var node; if (isAlt) { node = $WH.ge('alt2-' + screenshot.id) } else { node = $WH.ge('alt-' + screenshot.id) } var sp = $WH.gE(node, 'span')[0]; var div = $WH.ce('div'); div.style.whiteSpace = 'nowrap'; var iCaption = $WH.ce('input'); iCaption.type = 'text'; iCaption.value = screenshot.caption; iCaption.maxLength = 200; iCaption.size = 35; iCaption.onclick = function (e) { $WH.sp(e); } // sarjuuk - custom to inhibit screenshot popup, when clicking into input element div.appendChild(iCaption); var btn = $WH.ce('input'); btn.type = 'button'; btn.value = 'Update'; btn.onclick = function (i, j, k) { if (!j) { $WH.sp(k); } (ssm_Edit.bind(this, i, j))(); }.bind(btn, screenshot, isAlt); div.appendChild(btn); var c = $WH.ce('span'); c.appendChild($WH.ct(' ')); div.appendChild(c); btn = $WH.ce('input'); btn.type = 'button'; btn.value = 'Cancel'; btn.onclick = function (i, j, k) { if (!j) { $WH.sp(k); } (ssm_CancelEdit.bind(this, i, j))(); }.bind(btn, screenshot, isAlt); div.appendChild(btn); sp.style.display = 'none'; sp.nextSibling.style.display = 'none'; node.insertBefore(div, sp); iCaption.focus() } function ssm_CancelEdit(screenshot, isAlt) { var node; if (isAlt) { node = $WH.ge('alt2-' + screenshot.id); } else { node = $WH.ge('alt-' + screenshot.id); } var sp = $WH.gE(node, 'span')[1]; sp.style.display = ''; sp.nextSibling.style.display = ''; node.removeChild(node.firstChild); } function ssm_Edit(screenshot, isAlt) { var node; if (isAlt) { node = $WH.ge('alt2-' + screenshot.id); } else { node = $WH.ge('alt-' + screenshot.id); } var desc = node.firstChild.childNodes; if (desc[0].value == screenshot.caption) { ssm_CancelEdit(screenshot, isAlt); return } screenshot.caption = desc[0].value; ssm_CancelEdit(screenshot, isAlt); node = node.firstChild; while (node.childNodes.length > 0) { node.removeChild(node.firstChild); } $WH.ae(node, $WH.ct(screenshot.caption)); new Ajax('?admin=screenshots&action=editalt&id=' + screenshot.id, { method: 'POST', params: 'alt=' + $WH.urlencode(screenshot.caption) }) } function ssm_Clear(screenshot, isAlt) { var node; if (isAlt) { node = $WH.ge('alt2-' + screenshot.id); } else { node = $WH.ge('alt-' + screenshot.id); } var sp = $WH.gE(node, 'span'); var a = $WH.gE(sp[1], 'a'); sp = sp[0]; if (screenshot.caption == '') { return; } screenshot.caption = ''; sp.innerHTML = 'NULL'; new Ajax('?admin=screenshots&action=editalt&id=' + screenshot.id, { method: 'POST', params: 'alt=' + $WH.urlencode('') }) } function ssm_Approve(openNext) { var _self = this; new Ajax('?admin=screenshots&action=approve&id=' + _self.id, { method: 'get', onSuccess: function (x) { Lightbox.hide(); if (ssm_numPending == 1 && _self.pending) { ss_Refresh(true); } else { ss_Refresh(); ss_Manage(ss_managedRow, _self.type, _self.typeId, openNext, 0); } } }) } function ssm_Sticky(openNext) { var _self = this; new Ajax('?admin=screenshots&action=sticky&id=' + _self.id, { method: 'get', onSuccess: function (x) { Lightbox.hide(); if (ssm_numPending == 1 && _self.pending) { ss_Refresh(true); } else { ss_Refresh(); ss_Manage(ss_managedRow, _self.type, _self.typeId, openNext, 0); } } }) } function ssm_Delete(openNext) { var _self = this; new Ajax('?admin=screenshots&action=delete&id=' + _self.id, { method: 'get', onSuccess: function (x) { Lightbox.hide(); if (ssm_numPending == 1 && _self.pending) { ss_Refresh(true); } else { ss_Refresh(); ss_Manage(ss_managedRow, _self.type, _self.typeId, openNext, 0); } } }); } function ssm_Relocate(typeId) { var _self = this; new Ajax('?admin=screenshots&action=relocate&id=' + _self.id + '&typeid=' + typeId, { method: 'get', onSuccess: function (x) { ss_Refresh(); ss_Manage(ss_managedRow, _self.type, typeId); } }); } var ScreenshotManager = new function () { var screenshot, pos, imgWidth, imgHeight, scale, desiredScale, container, screen, imgDiv, aPrev, aNext, aCover, aOriginal, divFrom, divCaption, __div, h2Name, u, aEdit, aClear, spApprove, aApprove, aMakeSticky, aDelete, loadingImage, lightboxComponents; function computeDimensions(captionExtraHeight) { var availHeight = Math.max(50, Math.min(618, $WH.g_getWindowSize().h - 122 - captionExtraHeight)); if (screenshot.id) { desiredScale = Math.min(772 / screenshot.width, 618 / screenshot.height); scale = Math.min(772 / screenshot.width, availHeight / screenshot.height); } else { desiredScale = scale = 1; } if (desiredScale > 1) { desiredScale = 1; } if (scale > 1) { scale = 1; } imgWidth = Math.round(scale * screenshot.width); imgHeight = Math.round(scale * screenshot.height); var lbWidth = Math.max(480, imgWidth); Lightbox.setSize(lbWidth + 20, imgHeight + 116 + captionExtraHeight); if (captionExtraHeight) { imgDiv.firstChild.width = imgWidth; imgDiv.firstChild.height = imgHeight; } } function render(resizing) { if (resizing && (scale == desiredScale) && $WH.g_getWindowSize().h > container.offsetHeight) { return; } container.style.visibility = 'hidden'; var resized = (screenshot.width > 772 || screenshot.height > 618); computeDimensions(0); var url = g_staticUrl + '/uploads/screenshots/' + (screenshot.pending ? 'pending' : 'normal') + '/' + screenshot.id + '.jpg'; var html = ''; } divCaption.innerHTML = html; } else { divCaption.innerHTML = 'NULL'; } __div.id = 'alt2-' + screenshot.id; aEdit.onclick = ssm_ShowEdit.bind(aEdit, screenshot, true); aClear.onclick = ssm_Clear.bind(aClear, screenshot, true); if (screenshot.next !== undefined) { aPrev.style.display = aNext.style.display = ''; aCover.style.display = 'none'; } else { aPrev.style.display = aNext.style.display = 'none'; aCover.style.display = ''; } } Lightbox.reveal(); if (divCaption.offsetHeight > 18) { computeDimensions(divCaption.offsetHeight - 18); } container.style.visibility = 'visible'; } function nextScreenshot() { if (screenshot.next !== undefined) { screenshot = ssm_screenshotData[screenshot.next]; } onRender(); } function prevScreenshot() { if (screenshot.prev !== undefined) { screenshot = ssm_screenshotData[screenshot.prev]; } onRender(); } function onResize() { render(1); } function onHide() { aApprove.onclick = aMakeSticky.onclick = aDelete.onclick = null; cancelImageLoading(); } function onShow(dest, first, opt) { screenshot = opt; container = dest; if (first) { dest.className = 'screenshotviewer'; screen = $WH.ce('div'); screen.className = 'screenshotviewer-screen'; aPrev = $WH.ce('a'); aNext = $WH.ce('a'); aPrev.className = 'screenshotviewer-prev'; aNext.className = 'screenshotviewer-next'; aPrev.href = 'javascript:;'; aNext.href = 'javascript:;'; var foo = $WH.ce('span'); $WH.ae(foo, $WH.ce('b')); $WH.ae(aPrev, foo); var foo = $WH.ce('span'); $WH.ae(foo, $WH.ce('b')); $WH.ae(aNext, foo); aPrev.onclick = prevScreenshot; aNext.onclick = nextScreenshot; aCover = $WH.ce('a'); aCover.className = 'screenshotviewer-cover'; aCover.href = 'javascript:;'; aCover.onclick = Lightbox.hide; var foo = $WH.ce('span'); $WH.ae(foo, $WH.ce('b')); $WH.ae(aCover, foo); $WH.ae(screen, aPrev); $WH.ae(screen, aNext); $WH.ae(screen, aCover); var _div = $WH.ce('div'); _div.className = 'text'; h2Name = $WH.ce('h2'); h2Name.className = 'first'; $WH.ae(h2Name, $WH.ct(screenshot.name)); $WH.ae(_div, h2Name); $WH.ae(dest, _div); imgDiv = $WH.ce('div'); $WH.ae(screen, imgDiv); $WH.ae(dest, screen); var _div = $WH.ce('div'); _div.style.paddingTop = '6px'; _div.style.cssFloat = _div.style.styleFloat = 'right'; _div.className = 'bigger-links'; aApprove = $WH.ce('a'); aApprove.href = 'javascript:;'; $WH.ae(aApprove, $WH.ct('Approve')); $WH.ae(_div, aApprove); spApprove = $WH.ce('span'); spApprove.style.display = 'none'; $WH.ae(spApprove, $WH.ct('Approve')); $WH.ae(_div, spApprove); $WH.ae(_div, makePipe()); aMakeSticky = $WH.ce('a'); aMakeSticky.href = 'javascript:;'; $WH.ae(aMakeSticky, $WH.ct('Make sticky')); $WH.ae(_div, aMakeSticky); $WH.ae(_div, makePipe()); aDelete = $WH.ce('a'); aDelete.href = 'javascript:;'; $WH.ae(aDelete, $WH.ct('Delete')); $WH.ae(_div, aDelete); u = _div; $WH.ae(dest, _div); divFrom = $WH.ce('div'); divFrom.className = 'screenshotviewer-from'; var sp = $WH.ce('span'); $WH.ae(sp, $WH.ct(LANG.lvscreenshot_from)); $WH.ae(sp, $WH.ce('a')); $WH.ae(sp, $WH.ct(' ')); $WH.ae(sp, $WH.ce('span')); $WH.ae(divFrom, sp); $WH.ae(dest, divFrom); _div = $WH.ce('div'); _div.className = 'clear'; $WH.ae(dest, _div); var aClose = $WH.ce('a'); aClose.className = 'screenshotviewer-close'; aClose.href = 'javascript:;'; aClose.onclick = Lightbox.hide; $WH.ae(aClose, $WH.ce('span')); $WH.ae(dest, aClose); aOriginal = $WH.ce('a'); aOriginal.className = 'screenshotviewer-original'; aOriginal.href = 'javascript:;'; aOriginal.target = '_blank'; $WH.ae(aOriginal, $WH.ce('span')); $WH.ae(dest, aOriginal); __div = $WH.ce('div'); divCaption = $WH.ce('span'); divCaption.style.paddingRight = '8px'; $WH.ae(__div, divCaption); var sp = $WH.ce('span'); sp.style.whiteSpace = 'nowrap'; aEdit = $WH.ce('a'); aEdit.href = 'javascript:;'; $WH.ae(aEdit, $WH.ct('Edit')); $WH.ae(sp, aEdit); $WH.ae(sp, makePipe()); aClear = $WH.ce('a'); aClear.href = 'javascript:;'; $WH.ae(aClear, $WH.ct('Clear')); $WH.ae(sp, aClear); $WH.ae(__div, sp); $WH.ae(dest, __div); _div = $WH.ce('div'); _div.className = 'clear'; $WH.ae(dest, _div); } else { $WH.ee(h2Name); $WH.ae(h2Name, $WH.ct(screenshot.name)); } onRender(); } function onRender() { if (screenshot.pending) { aApprove.onclick = ssm_Approve.bind(screenshot, true); aMakeSticky.onclick = ssm_Sticky.bind(screenshot, true); aDelete.onclick = ssm_Delete.bind(screenshot, true); } else { aMakeSticky.onclick = ssm_Sticky.bind(screenshot, true); aDelete.onclick = ssm_Delete.bind(screenshot, true); } aApprove.style.display = screenshot.pending ? '' : 'none'; spApprove.style.display = screenshot.pending ? 'none' : ''; if (!screenshot.width || !screenshot.height) { if (loadingImage) { loadingImage.onload = null; loadingImage.onerror = null; } else { container.className = ''; lightboxComponents = []; while (container.firstChild) { lightboxComponents.push(container.firstChild); $WH.de(container.firstChild); } } var lightboxTimer = setTimeout(function () { screenshot.width = 126; screenshot.height = 22; computeDimensions(0); screenshot.width = null; screenshot.height = null; var div = $WH.ce('div'); div.style.margin = '0 auto'; div.style.width = '126px'; var img = $WH.ce('img'); img.src = g_staticUrl + '/images/ui/misc/progress-anim.gif'; img.width = 126; img.height = 22; $WH.ae(div, img); $WH.ae(container, div); Lightbox.reveal(); container.style.visiblity = 'visible'; }, 150); loadingImage = new Image(); loadingImage.onload = (function (screen, timer) { clearTimeout(timer); screen.width = this.width; screen.height = this.height; loadingImage = null; restoreLightbox(); render(); }).bind(loadingImage, screenshot, lightboxTimer); loadingImage.onerror = (function (timer) { clearTimeout(timer); loadingImage = null; Lightbox.hide(); restoreLightbox(); }).bind(loadingImage, lightboxTimer); loadingImage.src = (screenshot.url ? screenshot.url : g_staticUrl + '/uploads/screenshots/' + (screenshot.pending ? 'pending' : 'normal') + '/' + screenshot.id + '.jpg'); } else { render(); } } function cancelImageLoading() { if (!loadingImage) { return; } loadingImage.onload = null; loadingImage.onerror = null; loadingImage = null; restoreLightbox(); } function restoreLightbox() { if (!lightboxComponents) { return; } $WH.ee(container); container.className = 'screenshotviewer'; for (var i = 0; i < lightboxComponents.length; ++i) { $WH.ae(container, lightboxComponents[i]); } lightboxComponents = null; } this.show = function (opt) { Lightbox.show('screenshotmanager', { onShow: onShow, onHide: onHide, onResize: onResize }, opt); } }; \ No newline at end of file diff --git a/template/bricks/head.tpl.php b/template/bricks/head.tpl.php index fcab4eb3..d29f2094 100644 --- a/template/bricks/head.tpl.php +++ b/template/bricks/head.tpl.php @@ -1,6 +1,6 @@ <?php echo htmlentities(implode(' - ', $this->title)); ?> - + @@ -8,7 +8,7 @@ \n"; endif; @@ -40,7 +40,7 @@ endif; \n"; endif; diff --git a/template/bricks/infobox.tpl.php b/template/bricks/infobox.tpl.php index 919bcdf2..e4ff0f5c 100644 --- a/template/bricks/infobox.tpl.php +++ b/template/bricks/infobox.tpl.php @@ -36,12 +36,22 @@ if (!empty($this->type) && !empty($this->typeId)): ?>
+community['vi'])): +?>
+ +community['vi'])): +?> \n"; endif; diff --git a/template/pages/admin/screenshots.tpl.php b/template/pages/admin/screenshots.tpl.php new file mode 100644 index 00000000..64125a93 --- /dev/null +++ b/template/pages/admin/screenshots.tpl.php @@ -0,0 +1,138 @@ +brick('header'); ?> + +
+
+
+ +brick('announcement'); + +$this->brick('pageTemplate'); +?> +
+

name; ?>

+ + + + + + + + + + + + + +
User: » Search by User
Page: + + #» Search by Page
+
+ + + + + + + +
Menu
PagesScreenshots:
+ + + + + + + +
ScreenshotIdCaptionDateUploaderStatusOptions
+ + +
+
+
+ +brick('footer'); ?> diff --git a/template/pages/screenshot.tpl.php b/template/pages/screenshot.tpl.php new file mode 100644 index 00000000..78435cc1 --- /dev/null +++ b/template/pages/screenshot.tpl.php @@ -0,0 +1,119 @@ +brick('header'); ?> + +
+
+
+ +brick('announcement'); + +$this->brick('pageTemplate'); + +$this->brick('infobox'); + +if (isset($this->error)): +?> +
+ +
+

+
error; ?>
+ +
+

name; ?>

+ +mode == 'add'): +?> +
+
+ +
+
+
+ +
text counter ph
+
+ + +
+
+ + +
+ +
+
+
+ +brick('footer'); ?>