Procházet zdrojové kódy

rev.15 Add smilies management

Visman před 4 roky
rodič
revize
72c250be11

+ 24 - 0
app/Controllers/Routing.php

@@ -540,6 +540,30 @@ class Routing
                 'AdminParser:edit',
                 'AdminParser'
             );
+            $r->add(
+                $r::DUO,
+                '/admin/parser/bbcode',
+                'AdminParserBBCode:view',
+                'AdminBBCode'
+            );
+            $r->add(
+                $r::DUO,
+                '/admin/parser/smilies',
+                'AdminParserSmilies:view',
+                'AdminSmilies'
+            );
+            $r->add(
+                $r::GET,
+                '/admin/parser/smilies/delete/{name}/{token}',
+                'AdminParserSmilies:delete',
+                'AdminSmiliesDelete'
+            );
+            $r->add(
+                $r::PST,
+                '/admin/parser/smilies/upload',
+                'AdminParserSmilies:upload',
+                'AdminSmiliesUpload'
+            );
             $r->add(
                 $r::DUO,
                 '/admin/categories',

+ 1 - 1
app/Core/Parser.php

@@ -49,7 +49,7 @@ class Parser extends Parserus
             $smilies = [];
 
             foreach ($this->c->smilies->list as $cur) {
-                $smilies[$cur['text']] = $this->c->PUBLIC_URL . '/img/sm/' . $cur['image'];
+                $smilies[$cur['sm_code']] = $this->c->PUBLIC_URL . '/img/sm/' . $cur['sm_image'];
             }
 
             $info = $this->c->BBCODE_INFO;

+ 4 - 4
app/Models/Pages/Admin/Install.php

@@ -959,10 +959,10 @@ class Install extends Admin
         // smilies
         $schema = [
             'FIELDS' => [
-                'id'            => ['SERIAL', false],
-                'image'         => ['VARCHAR(40)', false, ''],
-                'text'          => ['VARCHAR(20)', false, ''],
-                'disp_position' => ['TINYINT(4) UNSIGNED', false, 0],
+                'id'          => ['SERIAL', false],
+                'sm_image'    => ['VARCHAR(40)', false, ''],
+                'sm_code'     => ['VARCHAR(20)', false, ''],
+                'sm_position' => ['INT(10) UNSIGNED', false, 0],
             ],
             'PRIMARY KEY' => ['id'],
             'ENGINE' => $this->DBEngine,

+ 17 - 3
app/Models/Pages/Admin/Parser/Edit.php

@@ -61,7 +61,7 @@ class Edit extends Parser
         $this->nameTpl   = 'admin/form';
         $this->form      = $this->formEdit($config);
         $this->titleForm = __('Parser settings head');
-        $this->classForm = 'editparsersettings';
+        $this->classForm = 'parser-settings';
 
         return $this;
     }
@@ -138,6 +138,13 @@ class Edit extends Parser
                     'caption' => __('Quote depth label'),
                     'info'    => __('Quote depth help'),
                 ],
+                'bbcode_management' => [
+                    'type'    => 'btn',
+                    'caption' => null,
+                    'value'   => __('BBCode management'),
+                    'title'   => __('BBCode management'),
+                    'link'    => $this->c->Router->link('AdminBBCode'),
+                ],
             ],
         ];
 
@@ -148,8 +155,8 @@ class Edit extends Parser
                     'type'    => 'radio',
                     'value'   => $config->o_smilies,
                     'values'  => $yn,
-                    'caption' => __('Smilies label'),
-                    'info'    => __('Smilies help'),
+                    'caption' => __('Smilies mess label'),
+                    'info'    => __('Smilies mess help'),
                 ],
                 'o_smilies_sig' => [
                     'type'    => 'radio',
@@ -158,6 +165,13 @@ class Edit extends Parser
                     'caption' => __('Smilies sigs label'),
                     'info'    => __('Smilies sigs help'),
                 ],
+                'smilies_management' => [
+                    'type'    => 'btn',
+                    'caption' => null,
+                    'value'   => __('Smilies management'),
+                    'title'   => __('Smilies management'),
+                    'link'    => $this->c->Router->link('AdminSmilies'),
+                ],
 
             ],
         ];

+ 423 - 0
app/Models/Pages/Admin/Parser/Smilies.php

@@ -0,0 +1,423 @@
+<?php
+
+namespace ForkBB\Models\Pages\Admin\Parser;
+
+use ForkBB\Core\Image;
+use ForkBB\Core\Validator;
+use ForkBB\Models\Page;
+use ForkBB\Models\Pages\Admin\Parser;
+use ForkBB\Models\Config\Model as Config;
+use function \ForkBB\__;
+
+class Smilies extends Parser
+{
+    /**
+     * Паттерн для имени изображения
+     * @var string
+     */
+    protected $pattern = '%^[a-z0-9-_]+\.(?:gif|jpe?g|png|webp)$%isD';
+
+    /**
+     * Паттерн для доступных к загрузке типов файлов
+     * @var string
+     */
+    protected $accept = 'image/*';
+
+    /**
+    * Заполняет список файлов из каталога смайлов
+    */
+    protected function calcImages(): void
+    {
+        $dir    = $this->c->DIR_PUBLIC . '/img/sm/';
+        $result = [];
+
+        if (
+            \is_dir($dir)
+            && false !== ($dh = \opendir($dir))
+        ) {
+            while (false !== ($entry = \readdir($dh))) {
+                if (
+                    \preg_match($this->pattern, $entry)
+                    && \is_file($dir . $entry)
+                ) {
+                    $result[] = $entry;
+                }
+            }
+            \closedir($dh);
+            \sort($result, \SORT_NATURAL);
+        }
+
+        $this->imageList = $result;
+    }
+
+    /**
+     * Управление смайлами
+     */
+    public function view(array $args, string $method): Page
+    {
+        $this->calcImages();
+
+        if ('POST' === $method) {
+            $imageStr = \implode(',', $this->imageList);
+            $v        = $this->c->Validator->reset()
+                ->addValidators([
+                ])->addRules([
+                    'token'                 => 'token:AdminSmilies',
+                    'smilies.*.sm_code'     => 'required|string:trim|max:20',
+                    'smilies.*.sm_position' => 'required|integer|min:0|max:9999',
+                    'smilies.*.sm_image'    => 'required|string|max:40|in:' . $imageStr,
+                    'new_sm_code'           => 'string:trim|max:20',
+                    'new_sm_position'       => 'integer|min:0|max:9999',
+                    'new_sm_image'          => 'string|max:40|in:' . $imageStr,
+                ])->addAliases([
+                    'smilies.*.sm_code'     => 'Smiley code label',
+                    'smilies.*.sm_position' => 'Position label',
+                    'smilies.*.sm_image'    => 'Name label',
+                    'new_sm_code'           => 'Smiley code label',
+                    'new_sm_position'       => 'Position label',
+                    'new_sm_image'          => 'Name label',
+                ])->addArguments([
+                ])->addMessages([
+                ]);
+
+            $valid = $v->validation($_POST);
+            $data  = $v->getData();
+
+            if ($valid) {
+                $old = $this->c->smilies->list;
+                $new = $v->smilies;
+
+                foreach ($new as $id => $cur) {
+                    if (isset($old[$id]) && $cur != $old[$id]) {
+                        $this->c->smilies->update($id, $cur);
+                    }
+                }
+
+                if ('' != $v->new_sm_code) {
+                    $this->c->smilies->insert([
+                        'sm_code'     => $v->new_sm_code,
+                        'sm_position' => $v->new_sm_position,
+                        'sm_image'    => $v->new_sm_image,
+                    ]);
+                }
+
+                $this->c->Cache->delete('smilies');
+
+                return $this->c->Redirect->page('AdminSmilies')->message('Smilies updated redirect');
+            }
+
+            $this->fIswev = $v->getErrors();
+        } else {
+            $data = [];
+        }
+
+        $this->nameTpl         = 'admin/smilies';
+        $this->aCrumbs[]       = [
+            $this->c->Router->link('AdminSmilies'),
+            __('Smilies management'),
+        ];
+        $this->formSmilies     = $this->formSmilies($data);
+        $this->formImages      = $this->formImages();
+        $this->formUploadImage = $this->formUploadImage();
+
+        return $this;
+    }
+
+    /**
+     * Формирует данные для формы смайлов
+     */
+    protected function formSmilies(array $data): array
+    {
+        $form = [
+            'action' => $this->c->Router->link('AdminSmilies'),
+            'hidden' => [
+                'token' => $this->c->Csrf->create('AdminSmilies'),
+            ],
+            'sets' => [
+                'smilies-legend' => [
+                    'class'  => 'smilies-legend',
+                    'legend' => __('Smilies list subhead'),
+                    'fields' => [],
+                ],
+            ],
+            'btns'   => [
+                'save' => [
+                    'type'      => 'submit',
+                    'value'     => __('Save changes'),
+//                    'accesskey' => 's',
+                ],
+            ],
+        ];
+
+        $imageList = \array_combine($this->imageList, $this->imageList);
+
+        $i   = 1;
+        $max = 0;
+        foreach ($this->c->smilies->list as $id => $cur) {
+            $fields = [];
+            $max    = \max($max, $cur['sm_position']);
+
+            $fields["smilies[{$id}][sm_code]"] = [
+                'class'     => ['code', 'smile'],
+                'type'      => 'text',
+                'maxlength' => 20,
+                'value'     => $data['smilies'][$id]['sm_code'] ?? $cur['sm_code'],
+                'caption'   => __('Smiley code label'),
+                'required'  => true,
+            ];
+            $fields["smilies[{$id}][sm_position]"] = [
+                'class'     => ['position', 'smile'],
+                'type'      => 'number',
+                'min'       => 0,
+                'max'       => 255,
+                'value'     => $data['smilies'][$id]['sm_position'] ?? $cur['sm_position'],
+                'caption'   => __('Position label'),
+                'required'  => true,
+            ];
+            $fields["smilies[{$id}][sm_image]"] = [
+                'class'     => ['image', 'smile'],
+                'type'     => 'select',
+                'options'  => $imageList,
+                'value'    => $data['smilies'][$id]['sm_image'] ?? $cur['sm_image'],
+                'caption'  => __('Name label'),
+                'required' => true,
+            ];
+            $fields["smile{$id}-pic"] = [
+                'class'     => ['pic', 'smile'],
+                'type'      => 'str',
+                'value'     => __('<img src="%1$s" alt="%2$s">', $this->c->PUBLIC_URL . '/img/sm/' . $cur['sm_image'], $cur['sm_image']),
+                'caption'   => __('Picture label'),
+                'html'      => true,
+            ];
+            $fields["smile{$id}-del"] = [
+                'class'     => ['delete', 'smile'],
+                'type'      => 'btn',
+                'value'     => '❌',
+                'caption'   => __('Delete'),
+                'title'     => __('Delete'),
+                'link'      => $this->c->Router->link(
+                    'AdminSmiliesDelete',
+                    [
+                        'name'  => $id,
+                        'token' => $this->c->Csrf->create(
+                            'AdminSmiliesDelete',
+                            [
+                                'name'  => $id,
+                            ]
+                        ),
+                    ]
+                ),
+            ];
+
+            $form['sets']["smile{$id}"] = [
+                'class'  => 'smile',
+                'legend' => __('Smiley number %s', $i),
+                'fields' => $fields,
+            ];
+
+            ++$i;
+        }
+
+        $form['sets']['new-smile'] = [
+            'class'  => 'new-smile',
+            'legend' => __('New smile subhead'),
+            'fields' => [
+                'new_sm_code' => [
+                    'class'     => ['code', 'new-smile'],
+                    'type'      => 'text',
+                    'maxlength' => 20,
+                    'value'     => $data['new_sm_code'] ?? '',
+                    'caption'   => __('Smiley code label'),
+                ],
+                'new_sm_position' => [
+                    'class'     => ['position', 'new-smile'],
+                    'type'      => 'number',
+                    'min'       => 0,
+                    'max'       => 255,
+                    'value'     => $data['new_sm_position'] ?? $max + 1,
+                    'caption'   => __('Position label'),
+                ],
+                'new_sm_image' => [
+                    'class'     => ['image', 'new-smile'],
+                    'type'     => 'select',
+                    'options'  => $imageList,
+                    'value'    => $data['new_image'] ?? null,
+                    'caption'  => __('Name label'),
+                ],
+            ],
+        ];
+
+
+        return $form;
+    }
+
+    /**
+     * Формирует данные для формы изображений
+     */
+    protected function formImages(): array
+    {
+        $form = [
+            'sets' => [
+                'image-legend' => [
+                    'class'  => 'image-legend',
+                    'legend' => __('Available images subhead'),
+                    'fields' => [],
+                ],
+            ],
+        ];
+
+        foreach ($this->imageList as $key => $name) {
+            $fields = [];
+            $fields["image{$key}-pic"] = [
+                'class'     => ['pic', 'image'],
+                'type'      => 'str',
+                'value'     => __('<img src="%1$s" alt="%2$s">', $this->c->PUBLIC_URL . '/img/sm/' . $name, $name),
+                'caption'   => __('Picture label'),
+                'html'      => true,
+            ];
+            $fields["image{$key}-name"] = [
+                'class'   => ['name', 'image'],
+                'type'    => 'str',
+                'value'   => $name,
+                'caption' => __('Name label'),
+            ];
+            $fields["image{$key}-del"] = [
+                'class'   => ['delete', 'image'],
+                'type'    => 'link',
+                'value'   => '❌',
+                'caption' => __('Delete'),
+                'title'   => __('Delete'),
+                'href'    => $this->c->Router->link(
+                    'AdminSmiliesDelete',
+                    [
+                        'name'  => $name,
+                        'token' => $this->c->Csrf->create(
+                            'AdminSmiliesDelete',
+                            [
+                                'name'  => $name,
+                            ]
+                        ),
+                    ]
+                ),
+            ];
+
+            $form['sets']["image{$key}"] = [
+                'class'  => 'image',
+                'legend' => $name,
+                'fields' => $fields,
+            ];
+        }
+
+        return $form;
+    }
+
+    /**
+     * Формирует данные для формы загрузки картинки
+     */
+    protected function formUploadImage(): array
+    {
+        $form = [
+            'action'  => $this->c->Router->link('AdminSmiliesUpload'),
+            'enctype' => 'multipart/form-data',
+            'hidden'  => [
+                'token'         => $this->c->Csrf->create('AdminSmiliesUpload'),
+                'MAX_FILE_SIZE' => $this->c->Files->maxImgSize(),
+            ],
+            'sets'    => [
+                'upload' => [
+                    'class'  => 'upload_smile',
+                    'legend' => __('Upload image subhead'),
+                    'fields' => [
+                        'upload_image' => [
+                            'type'    => 'file',
+                            'caption' => __('Upload image label'),
+                            'info'    => __('Upload image info'),
+                            'accept'  => $this->accept,
+                        ],
+                    ],
+
+                ],
+            ],
+            'btns'    => [
+                'upload' => [
+                    'type'      => 'submit',
+                    'value'     => __('Upload'),
+//                    'accesskey' => 's',
+                ],
+            ],
+        ];
+
+        return $form;
+    }
+
+    /**
+     * Удаляет смайл или изображение
+     */
+    public function delete(array $args, string $method): Page
+    {
+        if (! $this->c->Csrf->verify($args['token'], 'AdminSmiliesDelete', $args)) {
+            return $this->c->Message->message('Bad token');
+        }
+
+        if (
+            \is_numeric($args['name'])
+            && \is_int(0 + $args['name'])
+        ) {
+            $this->c->smilies->delete((int) $args['name']);
+
+            $message = 'Smile deleted redirect';
+        } elseif (\preg_match($this->pattern, $args['name'])) {
+            $file = $this->c->DIR_PUBLIC . '/img/sm/' . $args['name'];
+
+            if (
+                \is_file($file)
+                && @\unlink($file)
+            ) {
+                $message = __('File %s deleted redirect', $args['name']);
+            } else {
+                $message = __('File %s not deleted redirect', $args['name']);
+            }
+        } else {
+            return $this->c->Message->message('Bad request');
+        }
+
+        return $this->c->Redirect->page('AdminSmilies')->message($message);
+    }
+
+    /**
+     * Загружает изображение
+     */
+    public function upload(array $args, string $method): Page
+    {
+        $v = $this->c->Validator->reset()
+            ->addValidators([
+            ])->addRules([
+                'token'        => 'token:AdminSmiliesUpload',
+                'upload_image' => "required|image|max:{$this->c->Files->maxImgSize('K')}",
+            ])->addAliases([
+                'upload_image' => 'Upload image label',
+            ])->addArguments([
+            ])->addMessages([
+            ]);
+
+        if (
+            $v->validation($_FILES + $_POST)
+            && $v->upload_image instanceof Image
+        ) {
+            if (
+                $v->upload_image
+                    ->rename(true)
+                    ->rewrite(false)
+                    ->toFile($this->c->DIR_PUBLIC . '/img/sm/*.(jpg|png|gif)')
+            ) {
+                return $this->c->Redirect->page('AdminSmilies')->message('Image uploaded redirect');
+            } else {
+                return $this->c->Message->message($v->upload_image->error());
+            }
+        }
+
+        $this->fIswev = $v->getErrors();
+
+        return $this->view([], 'GET');
+    }
+}

+ 42 - 1
app/Models/Pages/Admin/Update.php

@@ -17,7 +17,7 @@ class Update extends Admin
 {
     const PHP_MIN = '7.3.0';
 
-    const LATEST_REV_WITH_DB_CHANGES = 9;
+    const LATEST_REV_WITH_DB_CHANGES = 15;
 
     const LOCK_NAME = 'lock_update';
     const LOCk_TTL  = 1800;
@@ -693,4 +693,45 @@ class Update extends Admin
 
         return null;
     }
+
+    /**
+     * rev.14 to rev.15
+     */
+    protected function stageNumber14(array $args): ?int
+    {
+        $coreConfig = new CoreConfig($this->c->DIR_CONFIG . '/' . self::CONFIG_FILE);
+
+        $result = $coreConfig->delete(
+            'multiple=>SmileyListModelLoad',
+        );
+        $coreConfig->add(
+            'shared=>SmileyListModelLoad',
+            '\\ForkBB\\Models\\SmileyList\\Load::class'
+        );
+
+        $coreConfig->add(
+            'shared=>SmileyListModelUpdate',
+            '\\ForkBB\\Models\\SmileyList\\Update::class'
+        );
+
+        $coreConfig->add(
+            'shared=>SmileyListModelInsert',
+            '\\ForkBB\\Models\\SmileyList\\Insert::class'
+        );
+
+        $coreConfig->add(
+            'shared=>SmileyListModelDelete',
+            '\\ForkBB\\Models\\SmileyList\\Delete::class'
+        );
+
+        $coreConfig->save();
+
+        $this->c->DB->renameField('smilies', 'image', 'sm_image');
+        $this->c->DB->renameField('smilies', 'text', 'sm_code');
+        $this->c->DB->renameField('smilies', 'disp_position', 'sm_position');
+
+        $this->c->DB->alterField('smilies', 'sm_position', 'INT(10) UNSIGNED', false, 0);
+
+        return null;
+    }
 }

+ 29 - 0
app/Models/SmileyList/Delete.php

@@ -0,0 +1,29 @@
+<?php
+
+namespace ForkBB\Models\SmileyList;
+
+use ForkBB\Models\Method;
+use ForkBB\Models\SmileyList\Model as SmileyList;
+
+class Delete extends Method
+{
+    /**
+     * Удаляет смайл из БД
+     * Удаляет кеш смайлов
+     */
+    public function delete(int $id): SmileyList
+    {
+        $vars = [
+            ':id' => $id,
+        ];
+        $query = 'DELETE
+            FROM ::smilies
+            WHERE id=?i:id';
+
+        $this->c->DB->exec($query, $vars);
+
+        $this->c->Cache->delete('smilies');
+
+        return $this->model;
+    }
+}

+ 31 - 0
app/Models/SmileyList/Insert.php

@@ -0,0 +1,31 @@
+<?php
+
+namespace ForkBB\Models\SmileyList;
+
+use ForkBB\Models\Method;
+use InvalidArgumentException;
+
+class Insert extends Method
+{
+    /**
+     * Создает запись в БД для смайла
+     */
+    public function insert(array $data): int
+    {
+        if (
+            isset($data['id'])
+            || ! isset($data['sm_code'], $data['sm_position'], $data['sm_image'])
+            || '' == $data['sm_code']
+            || '' == $data['sm_image']
+        ) {
+            throw new InvalidArgumentException('Expected an array with a smile description');
+        }
+
+        $query = 'INSERT INTO ::smilies (sm_code, sm_position, sm_image)
+            VALUES (?s:sm_code, ?i:sm_position, ?s:sm_image)';
+
+        $this->c->DB->exec($query, $data);
+
+        return (int) $this->c->DB->lastInsertId();
+    }
+}

+ 3 - 3
app/Models/SmileyList/Load.php

@@ -16,9 +16,9 @@ class Load extends Method
      */
     public function load(): SmileyList
     {
-        $query = 'SELECT sm.id, sm.text, sm.image, sm.disp_position
-            FROM ::smilies AS sm
-            ORDER BY sm.disp_position';
+        $query = 'SELECT id, sm_code, sm_image, sm_position
+            FROM ::smilies
+            ORDER BY sm_position';
 
         $list = $this->c->DB->query($query)->fetchAll(PDO::FETCH_UNIQUE);
 

+ 35 - 0
app/Models/SmileyList/Update.php

@@ -0,0 +1,35 @@
+<?php
+
+namespace ForkBB\Models\SmileyList;
+
+use ForkBB\Models\Method;
+use ForkBB\Models\SmileyList\Model as SmileyList;
+use InvalidArgumentException;
+
+class Update extends Method
+{
+    /**
+     * Обновляет запись в БД для смайла
+     */
+    public function update(int $id, array $data): SmileyList
+    {
+        if (
+            isset($data['id'])
+            || ! isset($data['sm_code'], $data['sm_position'], $data['sm_image'])
+            || '' == $data['sm_code']
+            || '' == $data['sm_image']
+        ) {
+            throw new InvalidArgumentException('Expected an array with a smile description');
+        }
+
+        $data[':id'] = $id;
+
+        $query = 'UPDATE ::smilies
+            SET sm_code=?s:sm_code, sm_position=?i:sm_position, sm_image=?s:sm_image
+            WHERE id=?i:id';
+
+        $this->c->DB->exec($query, $data);
+
+        return $this->model;
+    }
+}

+ 1 - 1
app/bootstrap.php

@@ -42,7 +42,7 @@ if (
 }
 $c->PUBLIC_URL = $c->BASE_URL . $forkPublicPrefix;
 
-$c->FORK_REVISION = 14;
+$c->FORK_REVISION = 15;
 $c->START         = $forkStart;
 $c->DIR_APP       = __DIR__;
 $c->DIR_PUBLIC    = $forkPublic;

+ 5 - 1
app/config/main.dist.php

@@ -140,6 +140,11 @@ return [
         'SubscriptionModelSend' => \ForkBB\Models\Subscription\Send::class,
 
         'BanListModelIsBanned' => \ForkBB\Models\BanList\IsBanned::class,
+
+        'SmileyListModelLoad'   => \ForkBB\Models\SmileyList\Load::class,
+        'SmileyListModelUpdate' => \ForkBB\Models\SmileyList\Update::class,
+        'SmileyListModelInsert' => \ForkBB\Models\SmileyList\Insert::class,
+        'SmileyListModelDelete' => \ForkBB\Models\SmileyList\Delete::class,
     ],
     'multiple'  => [
         'CtrlPrimary' => \ForkBB\Controllers\Primary::class,
@@ -221,7 +226,6 @@ return [
         'AdminListModel' => \ForkBB\Models\AdminList\Model::class,
 
         'SmileyListModel'     => \ForkBB\Models\SmileyList\Model::class,
-        'SmileyListModelLoad' => \ForkBB\Models\SmileyList\Load::class,
 
         'DBMapModel'          => \ForkBB\Models\DBMap\Model::class,
 

+ 71 - 2
app/lang/en/admin_parser.po

@@ -48,10 +48,10 @@ msgstr "Allow the BBCode [img][/img] tag in user signatures (not recommended)."
 msgid "Smilies subhead"
 msgstr "Smilies"
 
-msgid "Smilies label"
+msgid "Smilies mess label"
 msgstr "Smilies in posts"
 
-msgid "Smilies help"
+msgid "Smilies mess help"
 msgstr "Convert smilies to small graphic icons."
 
 msgid "Smilies sigs label"
@@ -71,3 +71,72 @@ msgstr "Maximum [quote] depth"
 
 msgid "Quote depth help"
 msgstr "The maximum times a [quote] tag can go inside other [quote] tags, any tags deeper than this will be discarded."
+
+msgid "BBCode management"
+msgstr "BBCode management"
+
+msgid "Smilies management"
+msgstr "Smilies management"
+
+msgid "Smilies head"
+msgstr "Smilies"
+
+msgid "Available images head"
+msgstr "Available images"
+
+msgid "Available images subhead"
+msgstr "Available images"
+
+msgid "Upload image head"
+msgstr "Upload image"
+
+msgid "Upload image subhead"
+msgstr "Upload"
+
+msgid "Picture label"
+msgstr "Picture"
+
+msgid "<img src=\"%1$s\" alt=\"%2$s\">"
+msgstr "<img src=\"%1$s\" alt=\"%2$s\">"
+
+msgid "Name label"
+msgstr "File name"
+
+msgid "File %s deleted redirect"
+msgstr "File '%s' deleted."
+
+msgid "File %s not deleted redirect"
+msgstr "File '%s' not deleted."
+
+msgid "Upload"
+msgstr "Upload"
+
+msgid "Upload image label"
+msgstr "New image"
+
+msgid "Upload image info"
+msgstr "Select an image file to upload."
+
+msgid "Image uploaded redirect"
+msgstr "Image uploaded."
+
+msgid "Smilies list subhead"
+msgstr "Smilies list"
+
+msgid "Smiley number %s"
+msgstr "Smile #%s"
+
+msgid "Smiley code label"
+msgstr "Code"
+
+msgid "Position label"
+msgstr "Position"
+
+msgid "New smile subhead"
+msgstr "New smile"
+
+msgid "Smilies updated redirect"
+msgstr "Smilies updated."
+
+msgid "Smile deleted redirect"
+msgstr "Smile deleted."

+ 71 - 2
app/lang/ru/admin_parser.po

@@ -48,10 +48,10 @@ msgstr "Разрешить тег [img][/img] в подписях пользов
 msgid "Smilies subhead"
 msgstr "Смайлы"
 
-msgid "Smilies label"
+msgid "Smilies mess label"
 msgstr "Смайлы в сообщениях"
 
-msgid "Smilies help"
+msgid "Smilies mess help"
 msgstr "Преобразовывать смайлы из сообщений в графические иконки."
 
 msgid "Smilies sigs label"
@@ -71,3 +71,72 @@ msgstr "Максимальная глубина [quote]"
 
 msgid "Quote depth help"
 msgstr "Сколько раз тег [quote] может вкладываться в другие [quote], все теги свыше указанного порога будут игнорироваться."
+
+msgid "BBCode management"
+msgstr "Управление BB-кодами"
+
+msgid "Smilies management"
+msgstr "Управление смайлами"
+
+msgid "Smilies head"
+msgstr "Смайлы"
+
+msgid "Available images head"
+msgstr "Доступные изображения"
+
+msgid "Available images subhead"
+msgstr "Доступные изображения"
+
+msgid "Upload image head"
+msgstr "Загрузка изображения"
+
+msgid "Upload image subhead"
+msgstr "Загрузка"
+
+msgid "Picture label"
+msgstr "Изображение"
+
+msgid "<img src=\"%1$s\" alt=\"%2$s\">"
+msgstr "<img src=\"%1$s\" alt=\"%2$s\">"
+
+msgid "Name label"
+msgstr "Имя файла"
+
+msgid "File %s deleted redirect"
+msgstr "Файл '%s' удален."
+
+msgid "File %s not deleted redirect"
+msgstr "Файл '%s' не получилось удалить."
+
+msgid "Upload"
+msgstr "Загрузить"
+
+msgid "Upload image label"
+msgstr "Новое изображение"
+
+msgid "Upload image info"
+msgstr "Выберите файл изображения для загрузки."
+
+msgid "Image uploaded redirect"
+msgstr "Изображение загружено."
+
+msgid "Smilies list subhead"
+msgstr "Список смайлов"
+
+msgid "Smiley number %s"
+msgstr "Смайл #%s"
+
+msgid "Smiley code label"
+msgstr "Код"
+
+msgid "Position label"
+msgstr "Позиция"
+
+msgid "New smile subhead"
+msgstr "Новый смайл"
+
+msgid "Smilies updated redirect"
+msgstr "Смайлы обновлены."
+
+msgid "Smile deleted redirect"
+msgstr "Смайл удален."

+ 25 - 0
app/templates/admin/smilies.forkbb.php

@@ -0,0 +1,25 @@
+@extends ('layouts/admin')
+@if ($form = $p->formSmilies)
+      <section class="f-admin f-smilies-form">
+        <h2>{!! __('Smilies head') !!}</h2>
+        <div class="f-fdiv">
+    @include ('layouts/form')
+        </div>
+      </section>
+@endif
+@if ($form = $p->formImages)
+      <section class="f-admin f-images-list">
+        <h2>{!! __('Available images head') !!}</h2>
+        <div class="f-fdiv">
+    @include ('layouts/form')
+        </div>
+      </section>
+@endif
+@if ($form = $p->formUploadImage)
+      <section class="f-admin f-image-upload-form">
+        <h2>{!! __('Upload image head') !!}</h2>
+        <div class="f-fdiv">
+    @include ('layouts/form')
+        </div>
+      </section>
+@endif

+ 128 - 1
public/style/ForkBB/style.css

@@ -2079,7 +2079,6 @@ body,
 #fork .f-fs-forum {
   display: flex;
   align-items: stretch;
-  /*min-width: 0;*/
 }
 
 #fork .f-fs-forum > legend {
@@ -2922,3 +2921,131 @@ body,
     padding-bottom: 0;
   }
 }
+
+/****************************************/
+/* Админка/Парсер                       */
+/****************************************/
+#fork .f-smilies-form .f-fs-new-smile,
+#fork .f-images-list .f-fdiv {
+  display: flex;
+  flex-wrap: wrap;
+}
+
+#fork .f-images-list .f-fs-image {
+  display: flex;
+  align-items: center;
+  border: 0.0625rem dotted #AA7939;
+  margin: 0.3125rem;
+}
+
+#fork #id-fs-image-legend {
+  width: 100%;
+}
+
+#fork .f-images-list .f-field-image {
+  border: 0;
+  margin: 0;
+}
+
+#fork .f-smilies-form .f-fs-smile > legend,
+#fork .f-smilies-form .f-fs-new-smile > legend {
+  border-top: 0.0625rem dotted #AA7939;
+}
+
+#fork .f-images-list .f-fs-image > legend,
+#fork .f-images-list .f-field-image > dt {
+  display: none;
+}
+
+#fork .f-images-list .f-field-image > dd {
+  width: 100%;
+}
+
+#fork .f-field-pic img {
+  max-width: 100%;
+  min-width: 1rem;
+  height: auto;
+}
+
+#fork .f-images-list .f-field-name {
+  max-width: 80%;
+}
+
+#fork .f-smilies-form .f-field-new-smile {
+  display: flex;
+  flex-direction: column;
+  width: 33.33%;
+}
+
+#fork .f-smilies-form .f-field-new-smile .f-child1 {
+  font-weight: normal;
+}
+
+#fork .f-smilies-form .f-field-new-smile > dt {
+  width: 100%;
+  white-space: nowrap;
+  overflow: hidden;
+}
+
+#fork .f-smilies-form .f-field-new-smile > dd {
+  width: 100%;
+}
+
+@media screen and (min-width: 35rem) {
+  #fork .f-smilies-form .f-fs-smile {
+    display: flex;
+  }
+
+  #fork .f-smilies-form .f-field-smile {
+    display: flex;
+    flex-direction: column;
+    /* justify-content: space-between; */
+  }
+
+  #fork .f-smilies-form .f-fs-smile > legend,
+  #fork .f-smilies-form .f-field-smile > dt {
+    display: none;
+  }
+
+  #fork .f-fs-smilies-legend + .f-fs-smile .f-field-smile > dt {
+    display: block;
+    width: 100%;
+    white-space: nowrap;
+    overflow: hidden;
+  }
+
+  #fork .f-smilies-form .f-field-smile .f-child1 {
+    font-weight: normal;
+  }
+
+
+  #fork .f-smilies-form .f-field-smile > dd {
+    width: 100%;
+    flex-grow: 1;
+  }
+
+  #fork .f-field-smile.f-field-code {
+    width: 10rem;
+  }
+
+  #fork .f-field-smile.f-field-position {
+    width: 5rem;
+  }
+
+  #fork .f-field-smile.f-field-image {
+    width: 12rem;
+  }
+
+  #fork .f-field-smile.f-field-pic {
+    width: calc(100% - 30rem);
+    text-align: center;
+  }
+
+  #fork .f-field-smile.f-field-delete {
+    width: 3rem;
+  }
+
+  #fork .f-smilies-form .f-fs-new-smile > legend {
+    border-top: 0;
+  }
+}