Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
695 changes: 21 additions & 674 deletions LICENSE

Large diffs are not rendered by default.

11 changes: 7 additions & 4 deletions manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@
"version": "200.1.0",
"status": "dev",
"dle": [
"20.0"
"20.0",
"21.0"
],
"php": [
"8.3"
],
"mhadmin": "200.4.0",
"name": "Название проекта",
"description": "Описание проекта"
"mhadmin": "200.4.1",
"name": "Пользовательские списки",
"description": "UserLists — свои и общие списки новостей для DLE 21 / DevCraft Admin ≥ 200.4.1",
"code": "user_lists",
"mod": "user_lists"
}
72 changes: 63 additions & 9 deletions readme.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,65 @@
Просто шаблон для репозиториев
# Пользовательские списки (UserLists)

1. Обновить значения в `manifest.json`
2. Удалить `crowdin.yml`, если нет файлов локализации
3. Обновить данные в `install_archive.(sh|bat)` либо удалить эти файлы
4. Очередь Dependabot (`.github/workflows/dependabot-queue.yml`):
- Settings → General → Allow auto-merge (по желанию)
- Опциональный секрет `QUEUE_GITHUB_TOKEN` (PAT) для обхода CODEOWNERS / rulesets
- В `.github/dependabot.yml` поменять reviewers (`MaximHarder` → владелец репозитория)
- CI в `dependabot-ci-automerge.yml` подогнать под стек (без `package.json` — только минимальный gate)
Сателлит [DevCraft Admin](https://readme.devcraft.club/dev/dle/devcraft_admin/getting_started) для DataLife Engine: люди собирают новости в **свои списки**, администратор задаёт **общие имена** («В планах», «Просмотрено»). Штатное «Избранное» DLE модуль не трогает.

**Версия:** 200.1.0

## Документация на сайте

| Страница | Адрес |
| -------- | ----- |
| Начало работы | https://readme.devcraft.club/dev/dle/user_lists/200.1.0/getting_started |
| Установка | https://readme.devcraft.club/dev/dle/user_lists/200.1.0/install |
| Подключение в теме | https://readme.devcraft.club/dev/dle/user_lists/200.1.0/guides/theme |
| Общая установка плагинов | https://readme.devcraft.club/instructions/install_instructions |
| Публичные стили и скрипты | https://readme.devcraft.club/dev/dle/devcraft_admin/200.4.1/guides/public_assets |

## Требования

| Компонент | Минимум |
| --------- | ------- |
| DataLife Engine | **≥ 21.0** |
| PHP | **≥ 8.3** |
| DevCraft Admin | **≥ 200.4.1** |

Сначала установите и включите DevCraft Admin, затем **UserLists**.

## Установка

Как собрать архив или поставить zip — [Установка плагинов](https://readme.devcraft.club/instructions/install_instructions). В комплекте: `install_archive.sh` / `install_archive.bat`, затем **Панель управления → Плагины**. Нужен `install.xml` в корне архива (`needplugin` = DevCraft Admin). Отдельный SQL-файл регистрации не используется.

После включения откройте `?mod=user_lists`. Таблицы `dc_user_lists*` появятся при первом заходе. Затем права групп и вставки в тему.

Библиотеки PHP обычно ставятся сами (скрипт установки или раздел Composer в админке). Вручную — только если автоматом не вышло: [Composer](https://readme.devcraft.club/instructions/composer).

## Вставки в тему

Стили и скрипты оболочки — не через include. В `main.tpl`: `{devcraft-header}` в `<head>` и `{devcraft-scripts}` перед `</body>`.

Кнопка и окно на новости:

```smarty
{include file="devcraft/src/modules/UserLists/Controller/show_user_lists.php?news_id={news-id}&focus=button"}
{include file="devcraft/src/modules/UserLists/Controller/show_user_lists.php?news_id={news-id}&focus=modal"}
```

Страницы списков:

```smarty
{include file="devcraft/src/modules/UserLists/Controller/show_user_lists_page.php?focus=mine"}
{include file="devcraft/src/modules/UserLists/Controller/show_user_lists_page.php?focus=catalog"}
{include file="devcraft/src/modules/UserLists/Controller/show_user_lists_page.php?focus=proposals"}
{include file="devcraft/src/modules/UserLists/Controller/show_user_lists_page.php?focus=view&list_id=1"}
```

Пути `engine/modules/devcraft/user_lists*.php` больше не входят в пакет.

Разметка: `templates/{skin}/devcraft/user_lists/` (`button.tpl`, `modal.tpl`, `page.tpl`, `view.tpl`, `proposals.tpl`). Нет каталога в своей теме — скопируйте из `Default`.

Запросы: `devcraft/ajax.php` (`mod=user_lists`). Отдельный `engine/ajax/user_lists.php` не создаём.

Код модуля: `UserLists`. Код плагина в DLE: `user_lists`. Точка входа админки: `engine/inc/user_lists.php`.

## Лицензия

MIT — см. [LICENSE](LICENSE).
9 changes: 9 additions & 0 deletions upload/devcraft/config/user_lists.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"guest_can_view_public": false,
"button_label": "В списки",
"count_web": 20,
"count_admin": 50,
"bad_words": "",
"name_min": 2,
"name_max": 100
}
46 changes: 46 additions & 0 deletions upload/devcraft/src/modules/UserLists/Ajax/CreateListHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

declare(strict_types=1);

namespace DevCraft\Modules\UserLists\Ajax;

use DevCraft\Core\Http\AjaxRequest;
use DevCraft\Core\Http\JsonResponse;
use DevCraft\Core\Interfaces\ResponseInterface;
use DevCraft\Core\Interfaces\AjaxHandlerInterface;
use DevCraft\Modules\UserLists\Models\UserList;
use DevCraft\Modules\UserLists\Services\ListService;
use Throwable;

/**
* Быстрое создание своего списка (из модалки).
*/
final class CreateListHandler implements AjaxHandlerInterface {

public function handle(AjaxRequest $request): ResponseInterface {
global $member_id;

$userId = (int) ($member_id['user_id'] ?? 0);

if($userId <= 0) {
return JsonResponse::fail(__('Ошибка'), __('Требуется авторизация'), 'auth', 403);
}

try {
$list = (new ListService())->createUserList(
$userId,
(string) ($request->data['name'] ?? ''),
(string) ($request->data['visibility'] ?? UserList::VIS_PRIVATE),
(string) ($request->data['description'] ?? ''),
);

return JsonResponse::toast(__('Список создан'), [
'id' => $list->id(),
'name' => $list->name,
]);
} catch(Throwable $e) {
return JsonResponse::fail(__('Ошибка'), $e->getMessage(), 'error', 400);
}
}

}
41 changes: 41 additions & 0 deletions upload/devcraft/src/modules/UserLists/Ajax/DeleteListHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace DevCraft\Modules\UserLists\Ajax;

use DevCraft\Core\Http\AjaxRequest;
use DevCraft\Core\Http\JsonResponse;
use DevCraft\Core\Interfaces\ResponseInterface;
use DevCraft\Core\Interfaces\AjaxHandlerInterface;
use DevCraft\Modules\UserLists\Services\ListService;
use Throwable;

/**
* Удаление списка.
*/
final class DeleteListHandler implements AjaxHandlerInterface {

public function handle(AjaxRequest $request): ResponseInterface {
global $member_id;

$actorId = (int) ($member_id['user_id'] ?? 0);
$asAdmin = !empty($request->data['_admin']) || ($request->controller ?? '') === 'admin';
$service = new ListService();
$id = (int) ($request->data['id'] ?? 0);
$list = $service->listsRepo()->findOneById($id);

if($list === null) {
return JsonResponse::fail(__('Ошибка'), __('Список не найден'), 'not_found', 404);
}

try {
$service->deleteList($list, $actorId, $asAdmin);

return JsonResponse::toast(__('Удалено'), ['id' => $id]);
} catch(Throwable $e) {
return JsonResponse::fail(__('Ошибка'), $e->getMessage(), 'error', 400);
}
}

}
52 changes: 52 additions & 0 deletions upload/devcraft/src/modules/UserLists/Ajax/ModalListsHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

declare(strict_types=1);

namespace DevCraft\Modules\UserLists\Ajax;

use DevCraft\Core\Http\AjaxRequest;
use DevCraft\Core\Http\JsonResponse;
use DevCraft\Core\Interfaces\ResponseInterface;
use DevCraft\Core\Interfaces\AjaxHandlerInterface;
use DevCraft\Modules\UserLists\Services\ListService;
use DevCraft\Modules\UserLists\Services\PermissionService;
use DevCraft\Modules\UserLists\Services\SeedService;
use Throwable;

/**
* Данные модалки списков для новости.
*/
final class ModalListsHandler implements AjaxHandlerInterface {

public function handle(AjaxRequest $request): ResponseInterface {
global $member_id;

$userId = (int) ($member_id['user_id'] ?? 0);
$newsId = (int) ($request->data['news_id'] ?? 0);

if($userId <= 0) {
return JsonResponse::fail(__('Ошибка'), __('Требуется авторизация'), 'auth', 403);
}

if(!(new PermissionService())->isEnabled($userId)) {
return JsonResponse::fail(__('Ошибка'), __('Модуль недоступен'), 'auth', 403);
}

if($newsId <= 0) {
return JsonResponse::fail(__('Ошибка'), __('Не указана новость'), 'validation', 422);
}

try {
$service = new ListService();
(new SeedService())->ensureDefaults($service);

return JsonResponse::ok([
'lists' => $service->modalPayload($userId, $newsId),
'news_id' => $newsId,
]);
} catch(Throwable $e) {
return JsonResponse::fail(__('Ошибка'), $e->getMessage(), 'error', 400);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

namespace DevCraft\Modules\UserLists\Ajax;

use DevCraft\Core\Http\AjaxRequest;
use DevCraft\Core\Http\JsonResponse;
use DevCraft\Core\Interfaces\ResponseInterface;
use DevCraft\Core\Interfaces\AjaxHandlerInterface;
use DevCraft\Modules\UserLists\Services\ListService;
use Throwable;

/**
* Одобрение / отклонение предложения.
*/
final class ModerateSuggestionHandler implements AjaxHandlerInterface {

public function handle(AjaxRequest $request): ResponseInterface {
global $member_id;

$userId = (int) ($member_id['user_id'] ?? 0);
$itemId = (int) ($request->data['item_id'] ?? 0);
$approve = !empty($request->data['approve']);

if($userId <= 0) {
return JsonResponse::fail(__('Ошибка'), __('Требуется авторизация'), 'auth', 403);
}

try {
(new ListService())->moderateSuggestion($itemId, $userId, $approve);

return JsonResponse::toast($approve ? __('Одобрено') : __('Отклонено'));
} catch(Throwable $e) {
return JsonResponse::fail(__('Ошибка'), $e->getMessage(), 'error', 400);
}
}

}
89 changes: 89 additions & 0 deletions upload/devcraft/src/modules/UserLists/Ajax/PermissionsHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php

declare(strict_types=1);

namespace DevCraft\Modules\UserLists\Ajax;

use DevCraft\Core\Application;
use DevCraft\Core\Http\AjaxRequest;
use DevCraft\Core\Http\JsonResponse;
use DevCraft\Core\Interfaces\ResponseInterface;
use DevCraft\Core\Interfaces\AjaxHandlerInterface;
use DevCraft\Modules\UserLists\Models\GroupPermission;
use DevCraft\Modules\UserLists\Repositories\GroupPermissionRepository;
use DevCraft\Modules\UserLists\Services\PermissionService;

/**
* Сохранение прав групп UserLists (все группы одним запросом).
*/
final class PermissionsHandler implements AjaxHandlerInterface {

public function handle(AjaxRequest $request): ResponseInterface {
/** @var GroupPermissionRepository $repo */
$repo = Application::instance()->database()->repository(GroupPermission::class);

$batch = $request->data['groups'] ?? null;

if(is_array($batch) && $batch !== []) {
$saved = 0;

foreach($batch as $groupId => $raw) {
$gid = (int) $groupId;

if($gid <= 0 || !is_array($raw)) {
continue;
}

$repo->upsert($gid, $this->normalizeFlags($raw));
$saved++;
}

if($saved === 0) {
return JsonResponse::fail(__('Ошибка'), __('Нет групп для сохранения'), 'validation', 422);
}

return JsonResponse::toast(__('Сохранено для {n} групп', ['{n}' => (string) $saved]), [
'saved' => $saved,
]);
}

$groupId = (int) ($request->data['group_id'] ?? 0);

if($groupId <= 0) {
return JsonResponse::fail(__('Ошибка'), __('Не указана группа'), 'validation', 422);
}

$raw = $request->data['flags'] ?? [];

if(!is_array($raw)) {
$raw = [];
}

$repo->upsert($groupId, $this->normalizeFlags($raw));

return JsonResponse::toast(__('Сохранено'), ['group_id' => $groupId, 'saved' => 1]);
}

/**
* @param array<string, mixed> $raw
*
* @return array<string, mixed>
*/
private function normalizeFlags(array $raw): array {
$values = [];

foreach(PermissionService::defs() as $def) {
$id = $def['id'];
$type = $def['type'] ?? 'bool';

if($type === 'int') {
$values[$id] = max(0, (int) ($raw[$id] ?? ($def['default'] ?? 0)));
} else {
$values[$id] = !empty($raw[$id]);
}
}

return $values;
}

}
Loading
Loading