From 1d5539b3620ba6e2e07af923afc630c15a838460 Mon Sep 17 00:00:00 2001 From: Sarjuuk Date: Thu, 28 Aug 2025 17:46:57 +0200 Subject: [PATCH] Template/Update (Part 46 - V) * account management rework: Avatar functionality * show avatar at comments (beckported, because no forums) --- endpoints/account/account.php | 25 +++-- endpoints/account/delete-icon.php | 47 +++++++++ endpoints/account/forum-avatar.php | 108 +++++++++++++++++++++ endpoints/account/premium-border.php | 41 ++++++++ endpoints/account/rename-icon.php | 36 +++++++ endpoints/upload/image-complete.php | 88 +++++++++++++++++ endpoints/upload/image-crop.php | 84 ++++++++++++++++ endpoints/user/user.php | 7 +- includes/components/avatarmgr.class.php | 122 ++++++++++++++++++++++++ includes/dbtypes/user.class.php | 27 +++++- includes/user.class.php | 34 ++++--- localization/locale_dede.php | 13 +++ localization/locale_enus.php | 13 +++ localization/locale_eses.php | 13 +++ localization/locale_frfr.php | 13 +++ localization/locale_ruru.php | 13 +++ localization/locale_zhcn.php | 13 +++ setup/tools/clisetup/filegen.us.php | 1 + setup/updates/1758578400_15.sql | 3 + setup/updates/1758578400_16.sql | 2 + static/css/aowow.css | 3 +- static/js/global.js | 92 +++++++++++++++--- template/pages/account.tpl.php | 54 ++++++++--- template/pages/image-crop.tpl.php | 45 +++++++++ 24 files changed, 839 insertions(+), 58 deletions(-) create mode 100644 endpoints/account/delete-icon.php create mode 100644 endpoints/account/forum-avatar.php create mode 100644 endpoints/account/premium-border.php create mode 100644 endpoints/account/rename-icon.php create mode 100644 endpoints/upload/image-complete.php create mode 100644 endpoints/upload/image-crop.php create mode 100644 includes/components/avatarmgr.class.php create mode 100644 setup/updates/1758578400_15.sql create mode 100644 setup/updates/1758578400_16.sql create mode 100644 template/pages/image-crop.tpl.php diff --git a/endpoints/account/account.php b/endpoints/account/account.php index e560a55e..2512d508 100644 --- a/endpoints/account/account.php +++ b/endpoints/account/account.php @@ -14,12 +14,13 @@ class AccountBaseResponse extends TemplateResponse protected array $scripts = [[SC_JS_FILE, 'js/account.js']]; // display status of executed step (forwarding back to this page) - public ?array $generalMessage = null; - public ?array $emailMessage = null; - public ?array $usernameMessage = null; - public ?array $passwordMessage = null; - public ?array $communityMessage = null; - public ?array $avatarMessage = null; + public ?array $generalMessage = null; + public ?array $emailMessage = null; + public ?array $usernameMessage = null; + public ?array $passwordMessage = null; + public ?array $communityMessage = null; + public ?array $avatarMessage = null; + public ?array $premiumborderMessage = null; // form fields public int $modelrace = 0; @@ -36,6 +37,7 @@ class AccountBaseResponse extends TemplateResponse public int $customicon = 0; public array $customicons = []; public bool $premium = false; + public int $reputation = 0; public ?Listview $avatarManager = null; public ?array $bans; @@ -130,7 +132,7 @@ class AccountBaseResponse extends TemplateResponse $this->avMode = $user['avatar']; // status [reviewing, ok, rejected]? (only 2: rejected processed in js) - if (User::isPremium() && ($cuAvatars = DB::Aowow()->select('SELECT `id`, `name`, `current`, `size`, `status`, `when` FROM ?_account_avatars WHERE `userId` = ?d AND `status` > 0', User::$id))) + if (User::isPremium() && ($cuAvatars = DB::Aowow()->select('SELECT `id`, `name`, `current`, `size`, `status`, `when` FROM ?_account_avatars WHERE `userId` = ?d', User::$id))) { array_walk($cuAvatars, function (&$x) { $x['when'] *= 1000; // uploaded timestamp expected as msec for some reason @@ -139,7 +141,7 @@ class AccountBaseResponse extends TemplateResponse }); foreach ($cuAvatars as $a) - if ($a['status'] != 2) + if ($a['status'] != AvatarMgr::STATUS_REJECTED) $this->customicons[$a['id']] = $a['name']; // TODO - replace with array_find in PHP 8.4 @@ -154,6 +156,8 @@ class AccountBaseResponse extends TemplateResponse if (!$this->premium) return; + $this->reputation = User::getReputation(); + // Avatar Manager $this->avatarManager = new Listview([ 'template' => 'avatar', @@ -161,11 +165,12 @@ class AccountBaseResponse extends TemplateResponse 'name' => '$LANG.tab_avatars', 'parent' => 'avatar-manage', 'hideNav' => 1 | 2, // top | bottom - 'data' => $cuAvatars ?? [] + 'data' => $cuAvatars ?? [], + 'note' => Lang::account('avatarSlots', [count($this->customicons), Cfg::get('acc_max_avatar_uploads')]) ]); // Premium Border Selector - // ??? + // solved by js } } diff --git a/endpoints/account/delete-icon.php b/endpoints/account/delete-icon.php new file mode 100644 index 00000000..e70ea64a --- /dev/null +++ b/endpoints/account/delete-icon.php @@ -0,0 +1,47 @@ + ['filter' => FILTER_VALIDATE_INT] + ); + + /* + * response not evaluated + */ + protected function generate() : void + { + if (User::isBanned() || !$this->assertPOST('id')) + return; + + // non-int > error + $selected = DB::Aowow()->selectCell('SELECT `current` FROM ?_account_avatars WHERE `id` = ?d AND `userId` = ?d', $this->_post['id'], User::$id); + if ($selected === null || $selected === false) + return; + + DB::Aowow()->query('DELETE FROM ?_account_avatars WHERE `id` = ?d AND `userId` = ?d', $this->_post['id'], User::$id); + + // if deleted avatar is also currently selected, unset + if ($selected) + DB::Aowow()->query('UPDATE ?_account SET `avatar` = 0 WHERE `id` = ?d', User::$id); + + $path = sprintf('static/uploads/avatars/%d.jpg', $this->_post['id']); + if (!unlink($path)) + trigger_error('AccountDeleteiconResponse - failed to delete file: '.$path, E_USER_ERROR); + } +} + +?> diff --git a/endpoints/account/forum-avatar.php b/endpoints/account/forum-avatar.php new file mode 100644 index 00000000..a123d572 --- /dev/null +++ b/endpoints/account/forum-avatar.php @@ -0,0 +1,108 @@ + ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 0, 'max_range' => 2 ]], + 'wowicon' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' => '/^[[:print:]]+$/' ]], // file name can have \W chars: inv_misc_fork&knife, achievement_dungeon_drak'tharon_heroic + 'customicon' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 1 ]] + ); + // called via ajax + protected array $expectedGET = array( + 'avatar' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 2, 'max_range' => 2]], + 'customicon' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 1 ]] + ); + + private bool $success = false; + + protected function generate() : void + { + if (User::isBanned()) + return; + + $msg = match ($this->_post['avatar'] ?? $this->_get['avatar']) + { + 0 => $this->unset(), // none + 1 => $this->fromIcon(), // wow icon + 2 => $this->fromUpload(!$this->_get['avatar']), // custom icon (premium feature) + default => Lang::main('genericError') + }; + + if ($msg) + $_SESSION['msg'] = ['avatar', $this->success, $msg]; + } + + private function unset() : string + { + $x = DB::Aowow()->query('UPDATE ?_account SET `avatar` = 0 WHERE `id` = ?d', User::$id); + if ($x === null || $x === false) + return Lang::main('genericError'); + + $this->success = true; + + return Lang::account('updateMessage', $x === 0 ? 'avNoChange' : 'avSuccess'); + } + + private function fromIcon() : string + { + if (!$this->assertPOST('wowicon')) + return Lang::main('intError'); + + $icon = strtolower(trim($this->_post['wowicon'])); + + if (!DB::Aowow()->selectCell('SELECT 1 FROM ?_icons WHERE `name` = ?', $icon)) + return Lang::account('updateMessage', 'avNotFound'); + + $x = DB::Aowow()->query('UPDATE ?_account SET `avatar` = 1, `wowicon` = ? WHERE `id` = ?d', strtolower($icon), User::$id); + if ($x === null || $x === false) + return Lang::main('genericError'); + + $this->success = true; + + $msg = Lang::account('updateMessage', $x === 0 ? 'avNoChange' : 'avSuccess'); + if (($qty = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_account WHERE `wowicon` = ?', $icon)) > 1) + $msg .= ' '.Lang::account('updateMessage', 'avNthUser', [$qty]); + else + $msg .= ' '.Lang::account('updateMessage', 'av1stUser'); + + return $msg; + } + + protected function fromUpload(bool $viaPOST) : string + { + if (!User::isPremium()) + return Lang::main('genericError'); + + if (($viaPOST && !$this->assertPOST('customicon')) || (!$viaPOST && !$this->assertGET('customicon'))) + return Lang::main('intError'); + + $customIcon = $this->_post['customicon'] ?? $this->_get['customicon']; + + $x = DB::Aowow()->query('UPDATE ?_account_avatars SET `current` = IF(`id` = ?d, 1, 0) WHERE `userId` = ?d AND `status` <> ?d', $customIcon, User::$id, AvatarMgr::STATUS_REJECTED); + if (!is_int($x)) + return Lang::main('genericError'); + + if (!is_int(DB::Aowow()->query('UPDATE ?_account SET `avatar` = 2 WHERE `id` = ?d', User::$id))) + return Lang::main('intError'); + + $this->success = true; + + return Lang::account('updateMessage', $x === 0 ? 'avNoChange' : 'avSuccess'); + } +} + +?> diff --git a/endpoints/account/premium-border.php b/endpoints/account/premium-border.php new file mode 100644 index 00000000..e1ee43d8 --- /dev/null +++ b/endpoints/account/premium-border.php @@ -0,0 +1,41 @@ + ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 0, 'max_range' => 4]], + ); + + protected function generate() : void + { + if (User::isBanned()) + return; + + if (!$this->assertPOST('avatarborder')) + return; + + $x = DB::Aowow()->query('UPDATE ?_account SET `avatarborder` = ?d WHERE `id` = ?d', $this->_post['avatarborder'], User::$id); + if (!is_int($x)) + $_SESSION['msg'] = ['premiumborder', false, Lang::main('genericError')]; + else if (!$x) + $_SESSION['msg'] = ['premiumborder', true, Lang::account('updateMessage', 'avNoChange')]; + else + $_SESSION['msg'] = ['premiumborder', true, Lang::account('updateMessage', 'avSuccess')]; + } +} + +?> diff --git a/endpoints/account/rename-icon.php b/endpoints/account/rename-icon.php new file mode 100644 index 00000000..46735d01 --- /dev/null +++ b/endpoints/account/rename-icon.php @@ -0,0 +1,36 @@ + ['filter' => FILTER_VALIDATE_INT ], + 'name' => ['filter' => FILTER_VALIDATE_REGEXP, 'options' => ['regexp' =>'/^[a-zA-Z][a-zA-Z0-9 ]{0,19}$/']] + ); + + /* + * response not evaluated + */ + protected function generate() : void + { + if (User::isBanned() || !$this->assertPOST('id', 'name')) + return; + + // regexp same as in account.js + DB::Aowow()->query('UPDATE ?_account_avatars SET `name` = ? WHERE `id` = ?d AND `userId` = ?d', trim($this->_post['name']), $this->_post['id'], User::$id); + } +} + +?> diff --git a/endpoints/upload/image-complete.php b/endpoints/upload/image-complete.php new file mode 100644 index 00000000..10f9ea3b --- /dev/null +++ b/endpoints/upload/image-complete.php @@ -0,0 +1,88 @@ + ['filter' => FILTER_CALLBACK, 'options' => [self::class, 'checkCoords']], + ); + + public string $imgHash; + public int $newId; + + public function __construct(string $pageParam) + { + if (User::isBanned()) + $this->generate404(); + + parent::__construct($pageParam); + + if (!preg_match('/^upload=image-complete&(\d+)\.(\w{16})$/i', $_SERVER['QUERY_STRING'] ?? '', $m, PREG_UNMATCHED_AS_NULL)) + $this->generate404(); + + [, $this->newId, $this->imgHash] = $m; + + if (!$this->imgHash || !$this->newId) + $this->generate404(); + } + + protected function generate() : void + { + if (!$this->handleComplete()) + $_SESSION['msg'] = ['avatar', false, AvatarMgr::$error ?: Lang::main('intError')]; + } + + private function handleComplete() : bool + { + if (!$this->assertPOST('coords')) + return false; + + if (!AvatarMgr::init()) + return false; + + if (!AvatarMgr::loadFile(AvatarMgr::PATH_TEMP, User::$username.'-avatar-'.$this->newId.'-'.$this->imgHash.'_original')) + return false; + + if (!AvatarMgr::cropImg(...$this->_post['coords'])) + return false; + + if (!AvatarMgr::createAtlas($this->newId)) + return false; + + $fSize = filesize(sprintf(AvatarMgr::PATH_AVATARS, $this->newId)); + if (!$fSize) + return false; + + $newId = DB::Aowow()->query('INSERT INTO ?_account_avatars (`id`, `userId`, `name`, `when`, `size`) VALUES (?d, ?d, ?, ?d, ?d)', $this->newId, User::$id, 'Avatar '.$this->newId, time(), $fSize); + if (!is_int($newId)) + { + trigger_error('UploadImagecompleteResponse - avatar query failed', E_USER_ERROR); + return false; + } + + // delete temp files + unlink(sprintf(AvatarMgr::PATH_TEMP, User::$username.'-avatar-'.$this->newId.'-'.$this->imgHash.'_original')); + unlink(sprintf(AvatarMgr::PATH_TEMP, User::$username.'-avatar-'.$this->newId.'-'.$this->imgHash)); + + return true; + } + + protected static function checkCoords(string $val) : ?array + { + if (preg_match('/^[01]\.[0-9]{3}(,[01]\.[0-9]{3}){3}$/', $val)) + return explode(',', $val); + + return null; + } +} + +?> diff --git a/endpoints/upload/image-crop.php b/endpoints/upload/image-crop.php new file mode 100644 index 00000000..ba046dd6 --- /dev/null +++ b/endpoints/upload/image-crop.php @@ -0,0 +1,84 @@ +generateError(); + + parent::__construct($pageParam); + } + + protected function generate() : void + { + if ($err = $this->handleUpload()) + { + $_SESSION['msg'] = ['avatar', false, $err]; + $this->forward('?account#community'); + } + + $this->h1 = Lang::account('avatarSubmit'); + + $fileBase = User::$username.'-avatar-'.$this->nextId.'-'.$this->imgHash; + $dimensions = AvatarMgr::calcImgDimensions(); + + $this->cropper = $dimensions + array( + 'url' => Cfg::get('STATIC_URL').'/uploads/temp/'.$fileBase.'.jpg', + 'parent' => 'av-container', + 'minCrop' => ICON_SIZE_LARGE, // optional; defaults to 150 - min selection size (a square) + 'type' => Type::NPC, // NPC: 15384 [OLDWorld Trigger (DO NOT DELETE)] + 'typeId' => 15384, // = arbitrary image upload + 'constraint' => [1, 1] // [xMult, yMult] - relative size to each other (here: be square) + ); + + parent::generate(); + } + + private function handleUpload() : string + { + if (!AvatarMgr::init()) + return Lang::main('intError'); + + if (!AvatarMgr::validateUpload()) + return AvatarMgr::$error; + + if (!AvatarMgr::loadUpload()) + return Lang::main('intError'); + + $n = DB::Aowow()->selectCell('SELECT COUNT(1) FROM ?_account_avatars WHERE `userId` = ?d', User::$id); + if ($n && $n > Cfg::get('ACC_MAX_AVATAR_UPLOADS')) + return Lang::main('intError'); + + // why is ++(); illegal syntax? WHO KNOWS!? + $this->nextId = (DB::Aowow()->selectCell('SELECT MAX(`id`) FROM ?_account_avatars') ?: 0) + 1; + + if (!AvatarMgr::tempSaveUpload(['avatar', $this->nextId], $this->imgHash)) + return Lang::main('intError'); + + return ''; + } +} + +?> diff --git a/endpoints/user/user.php b/endpoints/user/user.php index b3e8f7b8..c89a6cf2 100644 --- a/endpoints/user/user.php +++ b/endpoints/user/user.php @@ -38,7 +38,7 @@ class UserBaseResponse extends TemplateResponse if (!$pageParam) $this->forwardToSignIn('user'); - if ($user = DB::Aowow()->selectRow('SELECT a.`id`, a.`username`, a.`consecutiveVisits`, a.`userGroups`, a.`avatar`, a.`wowicon`, a.`title`, a.`description`, a.`joinDate`, a.`prevLogin`, IFNULL(SUM(ar.`amount`), 0) AS "sumRep", a.`prevIP`, a.`email` FROM ?_account a LEFT JOIN ?_account_reputation ar ON a.`id` = ar.`userId` WHERE LOWER(a.`username`) = LOWER(?) GROUP BY a.`id`', $pageParam)) + if ($user = DB::Aowow()->selectRow('SELECT a.`id`, a.`username`, a.`consecutiveVisits`, a.`userGroups`, a.`avatar`, a.`avatarborder`, a.`wowicon`, a.`title`, a.`description`, a.`joinDate`, a.`prevLogin`, IFNULL(SUM(ar.`amount`), 0) AS "sumRep", a.`prevIP`, a.`email` FROM ?_account a LEFT JOIN ?_account_reputation ar ON a.`id` = ar.`userId` WHERE LOWER(a.`username`) = LOWER(?) GROUP BY a.`id`', $pageParam)) $this->user = $user; else $this->generateNotFound(Lang::user('notFound', [$pageParam])); @@ -115,12 +115,15 @@ class UserBaseResponse extends TemplateResponse default => '' }; + if (!($this->user['userGroups'] & U_GROUP_PREMIUM)) + $this->user['avatarborder'] = 2; + $this->userIcon = array( // JS: Icon.createUser() $this->user['avatar'], // avatar: 1(iconString), 2(customId) $avatarMore, // avatarMore: iconString or customId IconElement::SIZE_MEDIUM, // size: (always medium) null, // url: (always null) - User::isInGroup(U_GROUP_PREMIUM) ? 0 : 2, // premiumLevel: affixes css class ['-premium', '-gold', '', '-premiumred', '-red'] + $this->user['avatarborder'], // premiumLevel: affixes css class ['-premium', '-gold', '', '-premiumred', '-red'] false, // noBorder: always false '$Icon.getPrivilegeBorder('.$this->user['sumRep'].')' // reputationLevel: calculated in js from passed rep points ); diff --git a/includes/components/avatarmgr.class.php b/includes/components/avatarmgr.class.php new file mode 100644 index 00000000..4df080c8 --- /dev/null +++ b/includes/components/avatarmgr.class.php @@ -0,0 +1,122 @@ + self::MAX_W || $is[1] > self::MAX_H) + self::$error = Lang::account('selectAvatar'); + } + else + self::$error = Lang::account('selectAvatar'); + + if (!self::$error) + return true; + + self::$fileName = ''; + return false; + } + + /* create icon texture atlas + * ****************************** + * * LARGE * MEDIUM * + * * * * + * * * * + * * ************* + * * * SMOL * * + * * * * * + * * ********* * + * ****************************** + * + * as static/uploads/avatars/.jpg + */ + + public static function createAtlas(string $fileName) : bool + { + if (!self::$img) + return false; + + $sizes = [ICON_SIZE_LARGE, ICON_SIZE_MEDIUM, ICON_SIZE_SMALL]; + + $dest = imagecreatetruecolor(ICON_SIZE_LARGE + ICON_SIZE_MEDIUM, ICON_SIZE_LARGE); + $srcW = imagesx(self::$img); + $srcH = imagesx(self::$img); + + $destX = $destY = 0; + foreach ($sizes as $idx => $dim) + { + imagecopyresampled($dest, self::$img, $destX, $destY, 0, 0, $dim, $dim, $srcW, $srcH); + + if ($idx % 2) + $destY += $dim; + else + $destX += $dim; + } + + if (!imagejpeg($dest, sprintf(self::PATH_AVATARS, $fileName), self::JPEG_QUALITY)) + return false; + + self::$img = null; + $dest = null; + return true; + } + + + /*************/ + /* Admin Mgr */ + /*************/ + + // unsure yet how that's supposed to work + // for now pending uploads can be used right away +} + +?> diff --git a/includes/dbtypes/user.class.php b/includes/dbtypes/user.class.php index 1d356760..44f4f83b 100644 --- a/includes/dbtypes/user.class.php +++ b/includes/dbtypes/user.class.php @@ -26,7 +26,7 @@ class UserList extends DBTypeList foreach ($this->iterate() as $userId => $__) { $data[$this->curTpl['username']] = array( - 'border' => 0, // border around avatar (rarityColors) + 'border' => $this->getPremiumborder(), 'roles' => $this->curTpl['userGroups'], 'joined' => date(Util::$dateFormatInternal, $this->curTpl['joinDate']), 'posts' => 0, // forum posts @@ -47,22 +47,39 @@ class UserList extends DBTypeList $data[$this->curTpl['username']]['avatarmore'] = $this->curTpl['wowicon']; break; case 2: - if ($av = DB::Aowow()->selectCell('SELECT `id` FROM ?_account_avatars WHERE `userId` = ?d AND `current` = 1 AND `status` <> 2', $userId)) + if ($this->isPremium()) { - $data[$this->curTpl['username']]['avatar'] = $this->curTpl['avatar']; - $data[$this->curTpl['username']]['avatarmore'] = $av; + if ($av = DB::Aowow()->selectCell('SELECT `id` FROM ?_account_avatars WHERE `userId` = ?d AND `current` = 1 AND `status` <> ?d', $userId, AvatarMgr::STATUS_REJECTED)) + { + $data[$this->curTpl['username']]['avatar'] = $this->curTpl['avatar']; + $data[$this->curTpl['username']]['avatarmore'] = $av; + } } break; } // more optional data // sig: markdown formated string (only used in forum?) - // border: seen as null|1|3 .. changes the border around the avatar (i suspect its meaning changed and got decoupled from premium-status with the introduction of patreon-status) } return [Type::USER => $data]; } + // seen as null|1|3 .. changes the border around the avatar (chosen from account > premium tab?) + // changed at the end of MoP. No longer a jsBool but index to Icon.premiumBorderClasses + private function getPremiumBorder() : int + { + if (!$this->isPremium() || !$this->curTpl['avatar']) + return 2; // 2 is "none" + + return $this->curTpl['avatarborder']; + } + + public function isPremium() : bool + { + return $this->curTpl['userGroups'] & U_GROUP_PREMIUM || $this->curTpl['reputation'] >= Cfg::get('REP_REQ_PREMIUM'); + } + public function getListviewData() : array { return []; } public function renderTooltip() : ?string { return null; } diff --git a/includes/user.class.php b/includes/user.class.php index 5fca60fe..ebf180af 100644 --- a/includes/user.class.php +++ b/includes/user.class.php @@ -24,6 +24,7 @@ class User private static int $reputation = 0; private static string $dataKey = ''; private static int $excludeGroups = 1; + private static int $avatarborder = 2; // 2 is default / reputation colored private static ?LocalProfileList $profiles = null; public static function init() @@ -62,7 +63,7 @@ class User $session = DB::Aowow()->selectRow('SELECT `userId`, `expires` FROM ?_account_sessions WHERE `status` = ?d AND `sessionId` = ?', SESSION_ACTIVE, session_id()); $userData = DB::Aowow()->selectRow( - 'SELECT a.`id`, a.`passHash`, a.`username`, a.`locale`, a.`userGroups`, a.`userPerms`, BIT_OR(ab.`typeMask`) AS "bans", IFNULL(SUM(r.`amount`), 0) AS "reputation", a.`dailyVotes`, a.`excludeGroups`, a.`status`, a.`statusTimer`, a.`email`, a.`debug` + 'SELECT a.`id`, a.`passHash`, a.`username`, a.`locale`, a.`userGroups`, a.`userPerms`, BIT_OR(ab.`typeMask`) AS "bans", IFNULL(SUM(r.`amount`), 0) AS "reputation", a.`dailyVotes`, a.`excludeGroups`, a.`status`, a.`statusTimer`, a.`email`, a.`debug`, a.`avatar`, a.`avatarborder` FROM ?_account a LEFT JOIN ?_account_banned ab ON a.`id` = ab.`userId` AND ab.`end` > UNIX_TIMESTAMP() LEFT JOIN ?_account_reputation r ON a.`id` = r.`userId` @@ -119,6 +120,7 @@ class User self::$status = $userData['status']; self::$debug = $userData['debug']; self::$email = $userData['email']; + self::$avatarborder = $userData['avatarborder']; if (Cfg::get('PROFILER_ENABLE')) { @@ -129,6 +131,18 @@ class User self::$profiles = (new LocalProfileList($conditions)); } + // reset premium options + if (!self::isPremium()) + { + if ($userData['avatar'] == 2) + { + DB::Aowow()->query('UPDATE ?_account SET `avatar` = 1 WHERE `id` = ?d', self::$id); + DB::Aowow()->query('UPDATE ?_account_avatars SET `current` = 0 WHERE `userId` = ?d', self::$id); + } + + // avatar borders + // do not reset, it's just not sent to the browser + } // stuff, that updates on a daily basis goes here (if you keep you session alive indefinitly, the signin-handler doesn't do very much) // - consecutive visits @@ -482,7 +496,7 @@ class User public static function isPremium() : bool { - return self::isInGroup(U_GROUP_PREMIUM) || self::$reputation >= Cfg::get('REP_REQ_PREMIUM'); + return !self::isBanned() && (self::isInGroup(U_GROUP_PREMIUM) || self::$reputation >= Cfg::get('REP_REQ_PREMIUM')); } public static function isLoggedIn() : bool @@ -568,14 +582,14 @@ class User if (self::$debug) $gUser['debug'] = true; // csv id-list output option on listviews - if (self::getPremiumBorder()) - $gUser['settings'] = ['premiumborder' => 1]; + if (self::isPremium()) + { + $gUser['premium'] = 1; + $gUser['settings'] = ['premiumborder' => self::$avatarborder]; + } else $gUser['settings'] = (new \StdClass); // existence is checked in Profiler.js before g_user.excludegroups is applied; should this contain - "defaultModel":{"gender":2,"race":6} ? - if (self::isPremium()) - $gUser['premium'] = 1; - if ($_ = self::getProfilerExclusions()) $gUser = array_merge($gUser, $_); @@ -717,12 +731,6 @@ class User return $data; } - - // not sure what to set .. user selected? - public static function getPremiumBorder() : bool - { - return self::isInGroup(U_GROUP_PREMIUM); - } } ?> diff --git a/localization/locale_dede.php b/localization/locale_dede.php index 23ff084b..c1e1e179 100644 --- a/localization/locale_dede.php +++ b/localization/locale_dede.php @@ -950,6 +950,19 @@ $lang = array( 'newPassDiff' => "Euer neues Kennwort muss sich von eurem alten Kennwort unterscheiden.", // message_newpassdifferent 'newMailDiff' => "Eure neue E-Mail-Adresse muss sich von eurer alten E-Mail-Adresse unterscheiden.", // message_newemaildifferent + // premium avatar manager + 'uploadAvatar' => "Neuen Avatar hochladen", + 'goToManager' => "Zur Avatarverwaltung gehen", + 'manageAvatars' => "Avatare verwalten", + 'avatarSlots' => '%1$d / %2$d Avatarplätze belegt', + 'manageBorders' => "Premium Rahmen verwalten", + 'selectAvatar' => "Wählt einen Avatar zum hochladen.", + 'errTooSmall' => "Euer Avatar muss wenigstens %dpx groß sein.", + 'cropAvatar' => "Ihr könnt Euren Avatar zuschneiden.", + 'avatarSubmit' => "Avatar-Einsendung", + 'reminder' => "Erinnerung", + 'avatarCoC' => "Dass Benutzen von Bildern, die gegen die Regeln verstoßen kann zum Verlust Eures Premium-Status führen.", + // settings 'settings' => "Kontoeinstellungen", 'settingsNote' => "Du kannst einfach die unten stehenden Formulare ausfüllen, um deine Kontodaten zu aktualisieren.", diff --git a/localization/locale_enus.php b/localization/locale_enus.php index 670ec5e9..9ec1ff02 100644 --- a/localization/locale_enus.php +++ b/localization/locale_enus.php @@ -950,6 +950,19 @@ $lang = array( 'newPassDiff' => "Your new password must be different than your previous one.", // message_newpassdifferent 'newMailDiff' => "Your new email address must be different than your previous one.", // message_newemaildifferent + // premium avatar manager + 'uploadAvatar' => "Upload new Avatar", + 'goToManager' => "Go to Avatar Manager", + 'manageAvatars' => "Manage Avatars", + 'avatarSlots' => 'Using %1$d / %2$d avatar slots', + 'manageBorders' => "Manage Premium Borders", + 'selectAvatar' => "Please select the avatar to upload.", + 'errTooSmall' => "Your avatar must be at last %dpx in size.", + 'cropAvatar' => "You may crop your avatar.", + 'avatarSubmit' => "Avatar Submission", + 'reminder' => "Reminder", + 'avatarCoC' => "Using imagery violating out terms of service may result in revocation of your premium privileges.", + // settings 'settings' => "Account Settings", 'settingsNote' => "Simply use the forms below to update your account information.", diff --git a/localization/locale_eses.php b/localization/locale_eses.php index 9e5af257..e281220a 100644 --- a/localization/locale_eses.php +++ b/localization/locale_eses.php @@ -950,6 +950,19 @@ $lang = array( 'newPassDiff' => "Su nueva contraseña tiene que ser diferente a su contraseña anterior.",// message_newpassdifferent 'newMailDiff' => "Su nueva dirección de correo electrónico tiene que ser diferente a tu dirección de correo electrónico anterior.", // message_newemaildifferent + // premium avatar manager + 'uploadAvatar' => "[Upload new Avatar]", + 'goToManager' => "[Go to Avatar Manager]", + 'manageAvatars' => "[Manage Avatars]", + 'avatarSlots' => '[Using %1$d / %2$d avatar slots]', + 'manageBorders' => "[Manage Premium Borders]", + 'selectAvatar' => "[Please select the avatar to upload.]", + 'errTooSmall' => "[Your avatar must be at last %dpx in size.]", + 'cropAvatar' => "[You may crop your avatar.]", + 'avatarSubmit' => "[Avatar Submission]", + 'reminder' => "[Reminder]", + 'avatarCoC' => "[Using imagery violating out terms of service may result in revocation of your premium privileges.]", + // settings 'settings' => "Mi cuenta", 'settingsNote' => "Simplemente usa el siguiente formulario para actualizar la información de tu cuenta.", diff --git a/localization/locale_frfr.php b/localization/locale_frfr.php index cd982692..2d19e8cb 100644 --- a/localization/locale_frfr.php +++ b/localization/locale_frfr.php @@ -950,6 +950,19 @@ $lang = array( 'newPassDiff' => "Votre nouveau mot de passe doit être différent de l'ancien.", // message_newpassdifferent 'newMailDiff' => "Votre nouvelle adresse courriel doit être différente de l'ancienne.", // message_newemaildifferent + // premium avatar manager + 'uploadAvatar' => "[Upload new Avatar]", + 'goToManager' => "[Go to Avatar Manager]", + 'manageAvatars' => "[Manage Avatars]", + 'avatarSlots' => '[Using %1$d / %2$d avatar slots]', + 'manageBorders' => "[Manage Premium Borders]", + 'selectAvatar' => "[Please select the avatar to upload.]", + 'errTooSmall' => "[Your avatar must be at last %dpx in size.]", + 'cropAvatar' => "[You may crop your avatar.]", + 'avatarSubmit' => "[Avatar Submission]", + 'reminder' => "[Reminder]", + 'avatarCoC' => "[Using imagery violating out terms of service may result in revocation of your premium privileges.]", + // settings 'settings' => "Mon compte", 'settingsNote' => "Veuillez utiliser les formulaires ci-dessous pour apporter des changements.", diff --git a/localization/locale_ruru.php b/localization/locale_ruru.php index 5f702169..9238b977 100644 --- a/localization/locale_ruru.php +++ b/localization/locale_ruru.php @@ -950,6 +950,19 @@ $lang = array( 'newPassDiff' => "Прежний и новый пароли не должны совпадать.", // message_newpassdifferent 'newMailDiff' => "Прежний и новый e-mail адреса не должны совпадать.", // message_newemaildifferent + // premium avatar manager + 'uploadAvatar' => "[Upload new Avatar]", + 'goToManager' => "[Go to Avatar Manager]", + 'manageAvatars' => "[Manage Avatars]", + 'avatarSlots' => '[Using %1$d / %2$d avatar slots]', + 'manageBorders' => "[Manage Premium Borders]", + 'selectAvatar' => "[Please select the avatar to upload.]", + 'errTooSmall' => "[Your avatar must be at last %dpx in size.]", + 'cropAvatar' => "[You may crop your avatar.]", + 'avatarSubmit' => "[Avatar Submission]", + 'reminder' => "[Reminder]", + 'avatarCoC' => "[Using imagery violating out terms of service may result in revocation of your premium privileges.]", + // settings 'settings' => "Параметры учетной записи", 'settingsNote' => "Используйте нижеприведённую форму, чтобы обновить информацию о вашей учетной записи.", diff --git a/localization/locale_zhcn.php b/localization/locale_zhcn.php index e184e6dd..c72151a8 100644 --- a/localization/locale_zhcn.php +++ b/localization/locale_zhcn.php @@ -950,6 +950,19 @@ $lang = array( 'newPassDiff' => "你的新密码必须与以前的密码不同。", // message_newpassdifferent 'newMailDiff' => "您的新邮箱地址必须不同于旧地址。", // message_newemaildifferent + // premium avatar manager + 'uploadAvatar' => "[Upload new Avatar]", + 'goToManager' => "[Go to Avatar Manager]", + 'manageAvatars' => "[Manage Avatars]", + 'avatarSlots' => '[Using %1$d / %2$d avatar slots]', + 'manageBorders' => "[Manage Premium Borders]", + 'selectAvatar' => "[Please select the avatar to upload.]", + 'errTooSmall' => "[Your avatar must be at last %dpx in size.]", + 'cropAvatar' => "[You may crop your avatar.]", + 'avatarSubmit' => "[Avatar Submission]", + 'reminder' => "[Reminder]", + 'avatarCoC' => "[Using imagery violating out terms of service may result in revocation of your premium privileges.]", + // settings 'settings' => "账号设置", 'settingsNote' => "使用下列表格就能升级您的账号信息。", diff --git a/setup/tools/clisetup/filegen.us.php b/setup/tools/clisetup/filegen.us.php index 0d7ac502..70188e42 100644 --- a/setup/tools/clisetup/filegen.us.php +++ b/setup/tools/clisetup/filegen.us.php @@ -39,6 +39,7 @@ CLISetup::registerUtility(new class extends UtilityScript 'static/uploads/screenshots/thumb/', 'static/uploads/temp/', 'static/uploads/guide/images/', + 'static/uploads/avatars/' ); public function __construct() diff --git a/setup/updates/1758578400_15.sql b/setup/updates/1758578400_15.sql new file mode 100644 index 00000000..32ee0455 --- /dev/null +++ b/setup/updates/1758578400_15.sql @@ -0,0 +1,3 @@ +DELETE FROM `aowow_config` WHERE `key` = 'acc_max_avatar_uploads'; +INSERT INTO `aowow_config` (`key`, `value`, `default`, `cat`, `flags`, `comment`) VALUES + ('acc_max_avatar_uploads', 10, 10, 3, 129, 'premium users may upload this many avatars'); diff --git a/setup/updates/1758578400_16.sql b/setup/updates/1758578400_16.sql new file mode 100644 index 00000000..4b2f0d43 --- /dev/null +++ b/setup/updates/1758578400_16.sql @@ -0,0 +1,2 @@ +ALTER TABLE `aowow_account` + ADD COLUMN `avatarborder` tinyint unsigned NOT NULL DEFAULT 2 AFTER `avatar`; diff --git a/static/css/aowow.css b/static/css/aowow.css index 9d9f653b..7692cdd9 100644 --- a/static/css/aowow.css +++ b/static/css/aowow.css @@ -1161,7 +1161,8 @@ span.iconblizzard { .iconsmall-premiumred del { background-image:url(../images/Icon/small/border/premiumred.png); } .iconmedium-premiumred del { background-image:url(../images/Icon/medium/border/premiumred.png); } .iconlarge-premiumred del { - background-image:url(../images/logos/special/subscribe/patron-icon.png); + background-image:url(../images/Icon/large/border/premiumred.png); +/* background-image:url(../images/logos/special/subscribe/patron-icon.png); aowow - yeah, no */ height:85px; } diff --git a/static/js/global.js b/static/js/global.js index af34dd52..b389cbd3 100644 --- a/static/js/global.js +++ b/static/js/global.js @@ -1124,6 +1124,25 @@ function g_GetStaffColorFromRoles(roles) { return ''; } +// aowow - stand in for WH.User.getCommentRoleLabel +function g_GetCommentRoleLabel(roles, title) { + if (title) { + return title; + } + + if (roles & U_GROUP_ADMIN) { + return g_user_roles[2]; // LANG.administrator_abbrev + } + else if (roles & U_GROUP_MOD) { + return g_user_roles[4]; // LANG.moderator + } + else if (roles & U_GROUP_PREMIUMISH) { + return LANG.premiumuser; + } + + return null; +}; + function g_formatDate(sp, elapsed, theDate, time, alone) { var today = new Date(); var event_day = new Date(); @@ -13957,6 +13976,49 @@ Listview.templates = { $(div).show(); }, + applyAuthorTitle: function (container, title) + { + if (!title.label) + return; + + let cssClass = ['comment-reply-author-label'].concat(title.classes); + + $WH.ae(container, $WH.ct(' ')); // aowow - LANG.wordspace_punct + + if (title.url) + $WH.ae(container, $WH.ce('a', { className: cssClass.join(' '), href: title.url }, $WH.ct(`<${ title.label }>`))); + else + $WH.ae(container, $WH.ce('span', { className: cssClass.join(' ') }, $WH.ct(`<${ title.label }>`))); + }, + + getAuthorTitle: function (author) + { + let title = { + classes: [], + label: undefined, + url: undefined + }; + + if (g_pageInfo.author === author) { + title.label = LANG.guideAuthor; + return title; + } + + let user = g_users[author]; + if (user) { + // aowow - let roleColor = WH.User.getCommentTitleClass(_.roles, _.tierClass, user); + let roleColor = g_GetStaffColorFromRoles(user.roles); + if (roleColor) { + title.classes.push(roleColor); + } + // aowow - title.label = WH.User.getCommentRoleLabel(user.roles, user.title, user.tierTitle); + title.label = g_GetCommentRoleLabel(user.roles, user.title); + title.url = /* user.tierTitle && !user.title ? '/?premium' : */ ''; // aowow - tierTitle being the premium tier ("Rare|Epic|Legendary Premium User") + } + + return title; + }, + updateReplies: function(comment) { this.updateRepliesCell(comment); @@ -14063,7 +14125,13 @@ Listview.templates = { row.attr('data-replyid', reply.id); row.attr('data-idx', i); - row.find('.reply-text').addClass(g_GetStaffColorFromRoles(reply.roles)); + + // aowow - let cssClass = WH.User.getCommentRoleClass(reply.roles, reply.username); + let cssClass = g_GetStaffColorFromRoles(reply.roles); + if (!['comment-blue', 'comment-green'].includes(cssClass) && owner) { + cssClass = 'comment-green'; // comment-guide-author + } + row.find('.reply-text').addClass(cssClass); var replyWhen = $(''); replyWhen.text(g_formatDate(null, elapsed, creationDate)); @@ -14074,12 +14142,7 @@ Listview.templates = { replyByUserLink.attr('href', '?user=' + reply.username); replyByUserLink.text(reply.username); replyBy.append(replyByUserLink); - - if (owner) - { - $WH.ae(replyBy[0], $WH.ct(' ')); - $WH.ae(replyBy[0], $WH.ce('span', { className: 'comment-reply-author-label' }, $WH.ct('<' + LANG.guideAuthor + '>'))); - } + this.applyAuthorTitle(replyBy[0], this.getAuthorTitle(reply.username)) replyBy.append(' ').append(replyWhen).append(' ').append($WH.sprintf(LANG.lvcomment_patch, g_getPatchVersion(creationDate))); @@ -14170,7 +14233,6 @@ Listview.templates = { updateCommentAuthor: function(comment, container) { var user = g_users[comment.user]; - let owner = g_pageInfo.author === comment.user; var postedOn = new Date(comment.date); var elapsed = (g_serverTime - postedOn) / 1000; @@ -14178,12 +14240,18 @@ Listview.templates = { container.append(LANG.lvcomment_by); container.append($WH.sprintf('$2', comment.user, comment.user)); - if (owner) - { - $WH.ae(container[0], $WH.ct(' ')); - $WH.ae(container[0], $WH.ce('span', { className: 'comment-reply-author-label' }, $WH.ct('<' + LANG.guideAuthor + '>'))); + // aowow - avatar recovered and transplanted from commentsv1 version + if (user != null && user.avatar) { + var icon = Icon.createUser(user.avatar, user.avatarmore, 0, null, (user.roles & U_GROUP_PREMIUM) ? user.border : Icon.STANDARD_BORDER, 0, Icon.getPrivilegeBorder(user.reputation)); + icon.style.marginRight = '3px'; + icon.style.cssFloat = 'left'; + + container.css('lineHeight', '25px'); + container.append(icon); } + // aowow - end recover container.append(g_getReputationPlusAchievementText(user.gold, user.silver, user.copper, user.reputation)); + this.applyAuthorTitle(container[0], this.getAuthorTitle(comment.user)); container.append($WH.sprintf(' $3', comment.id, comment.id, g_formatDate(null, elapsed, postedOn))); container.append(' '); container.append($WH.sprintf(LANG.lvcomment_patch, g_getPatchVersion(postedOn))); diff --git a/template/pages/account.tpl.php b/template/pages/account.tpl.php index fe9b076b..54d67966 100644 --- a/template/pages/account.tpl.php +++ b/template/pages/account.tpl.php @@ -146,9 +146,7 @@ if ($this->bans):
- +
-user::isInGroup(U_GROUP_PREMIUM) && 0): ?> +user::isInGroup(U_GROUP_PREMIUM)): ?> avMode == 2 ? ' checked="checked"' : '');?> />   
@@ -226,7 +224,7 @@ if ($this->bans):
- Go to Avatar Manager +
@@ -255,18 +253,46 @@ if ($this->bans):
  • '.Lang::account('inactive'); ?>
  • '.Lang::account('active'); ?>
-Manage Avatars +

-

Manage Premium Borders

- Todo - - +

+ premiumborderMessage): ?> +
+ +
+
+
+
+
+ +
+
+ + + @@ -280,9 +306,7 @@ if ($this->bans): _.add('', {id: 'premium'}); _.flush(); - +
diff --git a/template/pages/image-crop.tpl.php b/template/pages/image-crop.tpl.php new file mode 100644 index 00000000..59d130f7 --- /dev/null +++ b/template/pages/image-crop.tpl.php @@ -0,0 +1,45 @@ +brick('header'); +?> +
+
+
+ +brick('announcement'); + +$this->brick('pageTemplate'); +?> +
+

h1; ?>

+ + +
+
+ +
+ +
+ +
+ +
+
+ +

+
+ + + +
+
+
+
+ +brick('footer'); ?>