Start small MVC changes
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
// controllers/AuthController.php
|
||||
require_once 'controllers/BaseController.php';
|
||||
require_once 'models/User.php';
|
||||
|
||||
class AuthController extends BaseController {
|
||||
|
||||
public function login() {
|
||||
// Если пользователь уже авторизован, перенаправляем на dashboard
|
||||
if (is_logged_in()) {
|
||||
$this->redirect('/dashboard');
|
||||
}
|
||||
|
||||
$error = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
|
||||
$error = "Ошибка безопасности";
|
||||
} else {
|
||||
$username = trim($_POST['username'] ?? '');
|
||||
$password = $_POST['password'] ?? '';
|
||||
|
||||
if (empty($username) || empty($password)) {
|
||||
$error = 'Пожалуйста, введите имя пользователя и пароль';
|
||||
} else {
|
||||
$userModel = new User($this->pdo);
|
||||
$user = $userModel->findByUsername($username);
|
||||
|
||||
if ($user && $userModel->verifyPassword($password, $user['password_hash'])) {
|
||||
if (!$user['is_active']) {
|
||||
$error = 'Ваш аккаунт деактивирован или ожидает активации администратором.';
|
||||
} else {
|
||||
// Успешный вход
|
||||
session_regenerate_id(true);
|
||||
$_SESSION['user_id'] = $user['id'];
|
||||
$_SESSION['username'] = $user['username'];
|
||||
$_SESSION['display_name'] = $user['display_name'] ?: $user['username'];
|
||||
$_SESSION['avatar'] = $user['avatar'] ?? null;
|
||||
|
||||
// Обновляем время последнего входа
|
||||
$userModel->updateLastLogin($user['id']);
|
||||
|
||||
$_SESSION['success'] = 'Добро пожаловать, ' . e($user['display_name'] ?: $user['username']) . '!';
|
||||
$this->redirect('/dashboard');
|
||||
}
|
||||
} else {
|
||||
$error = 'Неверное имя пользователя или пароль';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->render('auth/login', [
|
||||
'error' => $error,
|
||||
'page_title' => 'Вход в систему'
|
||||
]);
|
||||
}
|
||||
|
||||
public function logout() {
|
||||
// Очищаем все данные сессии
|
||||
$_SESSION = [];
|
||||
|
||||
if (ini_get("session.use_cookies")) {
|
||||
$params = session_get_cookie_params();
|
||||
setcookie(session_name(), '', time() - 42000,
|
||||
$params["path"], $params["domain"],
|
||||
$params["secure"], $params["httponly"]
|
||||
);
|
||||
}
|
||||
|
||||
session_destroy();
|
||||
$this->redirect('/login');
|
||||
}
|
||||
|
||||
public function register() {
|
||||
// Если пользователь уже авторизован, перенаправляем на dashboard
|
||||
if (is_logged_in()) {
|
||||
$this->redirect('/dashboard');
|
||||
}
|
||||
|
||||
$error = '';
|
||||
$success = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
|
||||
$error = "Ошибка безопасности";
|
||||
} else {
|
||||
$username = trim($_POST['username'] ?? '');
|
||||
$password = $_POST['password'] ?? '';
|
||||
$password_confirm = $_POST['password_confirm'] ?? '';
|
||||
$email = trim($_POST['email'] ?? '');
|
||||
$display_name = trim($_POST['display_name'] ?? '');
|
||||
|
||||
// Валидация
|
||||
if (empty($username) || empty($password)) {
|
||||
$error = 'Имя пользователя и пароль обязательны';
|
||||
} elseif ($password !== $password_confirm) {
|
||||
$error = 'Пароли не совпадают';
|
||||
} elseif (strlen($password) < 6) {
|
||||
$error = 'Пароль должен быть не менее 6 символов';
|
||||
} else {
|
||||
$userModel = new User($this->pdo);
|
||||
|
||||
// Проверяем, не занят ли username
|
||||
if ($userModel->findByUsername($username)) {
|
||||
$error = 'Имя пользователя уже занято';
|
||||
} elseif ($email && $userModel->findByEmail($email)) {
|
||||
$error = 'Email уже используется';
|
||||
} else {
|
||||
$data = [
|
||||
'username' => $username,
|
||||
'password' => $password,
|
||||
'email' => $email ?: null,
|
||||
'display_name' => $display_name ?: $username,
|
||||
'is_active' => 1 // Авто-активация для простоты
|
||||
];
|
||||
|
||||
if ($userModel->create($data)) {
|
||||
$success = 'Регистрация успешна! Теперь вы можете войти в систему.';
|
||||
// Можно автоматически войти после регистрации
|
||||
// $this->redirect('/login');
|
||||
} else {
|
||||
$error = 'Ошибка при создании аккаунта';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->render('auth/register', [
|
||||
'error' => $error,
|
||||
'success' => $success,
|
||||
'page_title' => 'Регистрация'
|
||||
]);
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
// controllers/BaseController.php
|
||||
class BaseController {
|
||||
protected $pdo;
|
||||
|
||||
public function __construct() {
|
||||
global $pdo;
|
||||
$this->pdo = $pdo;
|
||||
}
|
||||
|
||||
protected function render($view, $data = []) {
|
||||
extract($data);
|
||||
include "views/$view.php";
|
||||
}
|
||||
|
||||
protected function redirect($url) {
|
||||
header("Location: " . SITE_URL . $url);
|
||||
exit;
|
||||
}
|
||||
|
||||
protected function requireLogin() {
|
||||
if (!is_logged_in()) {
|
||||
$this->redirect('/login');
|
||||
}
|
||||
}
|
||||
|
||||
protected function jsonResponse($data) {
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($data);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,268 @@
|
||||
<?php
|
||||
// controllers/BookController.php
|
||||
require_once 'controllers/BaseController.php';
|
||||
require_once 'models/Book.php';
|
||||
require_once 'models/Chapter.php';
|
||||
require_once 'models/Series.php';
|
||||
|
||||
class BookController extends BaseController {
|
||||
public function index() {
|
||||
$this->requireLogin();
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$bookModel = new Book($this->pdo);
|
||||
$books = $bookModel->findByUser($user_id);
|
||||
$this->render('books/index', [
|
||||
'books' => $books,
|
||||
'page_title' => 'Мои книги'
|
||||
]);
|
||||
}
|
||||
|
||||
public function create() {
|
||||
$this->requireLogin();
|
||||
$seriesModel = new Series($this->pdo);
|
||||
$series = $seriesModel->findByUser($_SESSION['user_id']);
|
||||
|
||||
// Возвращаем типы редакторов для выбора
|
||||
$editor_types = [
|
||||
'markdown' => 'Markdown редактор',
|
||||
'html' => 'HTML редактор (TinyMCE)'
|
||||
];
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
|
||||
$_SESSION['error'] = "Ошибка безопасности";
|
||||
$this->redirect('/books/create');
|
||||
}
|
||||
|
||||
$title = trim($_POST['title'] ?? '');
|
||||
if (empty($title)) {
|
||||
$_SESSION['error'] = "Название книги обязательно";
|
||||
$this->redirect('/books/create');
|
||||
}
|
||||
|
||||
$bookModel = new Book($this->pdo);
|
||||
$data = [
|
||||
'title' => $title,
|
||||
'description' => trim($_POST['description'] ?? ''),
|
||||
'genre' => trim($_POST['genre'] ?? ''),
|
||||
'user_id' => $_SESSION['user_id'],
|
||||
'editor_type' => $_POST['editor_type'] ?? 'markdown',
|
||||
'series_id' => !empty($_POST['series_id']) ? (int)$_POST['series_id'] : null,
|
||||
'sort_order_in_series' => !empty($_POST['sort_order_in_series']) ? (int)$_POST['sort_order_in_series'] : null,
|
||||
'published' => isset($_POST['published']) ? 1 : 0
|
||||
];
|
||||
|
||||
if ($bookModel->create($data)) {
|
||||
$_SESSION['success'] = "Книга успешно создана";
|
||||
$new_book_id = $this->pdo->lastInsertId();
|
||||
$this->redirect("/books/{$new_book_id}/edit");
|
||||
} else {
|
||||
$_SESSION['error'] = "Ошибка при создании книги";
|
||||
}
|
||||
}
|
||||
|
||||
$this->render('books/create', [
|
||||
'series' => $series,
|
||||
'editor_types' => $editor_types,
|
||||
'selected_editor' => 'markdown', // по умолчанию
|
||||
'page_title' => 'Создание новой книги'
|
||||
]);
|
||||
}
|
||||
|
||||
public function edit($id) {
|
||||
$this->requireLogin();
|
||||
$bookModel = new Book($this->pdo);
|
||||
$book = $bookModel->findById($id);
|
||||
|
||||
if (!$book || $book['user_id'] != $_SESSION['user_id']) {
|
||||
$_SESSION['error'] = "Книга не найдена или у вас нет доступа";
|
||||
$this->redirect('/books');
|
||||
}
|
||||
|
||||
$seriesModel = new Series($this->pdo);
|
||||
$series = $seriesModel->findByUser($_SESSION['user_id']);
|
||||
|
||||
// Типы редакторов для выбора
|
||||
$editor_types = [
|
||||
'markdown' => 'Markdown редактор',
|
||||
'html' => 'HTML редактор (TinyMCE)'
|
||||
];
|
||||
|
||||
$error = '';
|
||||
$cover_error = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
|
||||
$error = "Ошибка безопасности";
|
||||
} else {
|
||||
$title = trim($_POST['title'] ?? '');
|
||||
if (empty($title)) {
|
||||
$error = "Название книги обязательно";
|
||||
} else {
|
||||
$old_editor_type = $book['editor_type'];
|
||||
$new_editor_type = $_POST['editor_type'] ?? 'markdown';
|
||||
$editor_changed = ($old_editor_type !== $new_editor_type);
|
||||
|
||||
$data = [
|
||||
'title' => $title,
|
||||
'description' => trim($_POST['description'] ?? ''),
|
||||
'genre' => trim($_POST['genre'] ?? ''),
|
||||
'user_id' => $_SESSION['user_id'],
|
||||
'editor_type' => $new_editor_type,
|
||||
'series_id' => !empty($_POST['series_id']) ? (int)$_POST['series_id'] : null,
|
||||
'sort_order_in_series' => !empty($_POST['sort_order_in_series']) ? (int)$_POST['sort_order_in_series'] : null,
|
||||
'published' => isset($_POST['published']) ? 1 : 0
|
||||
];
|
||||
|
||||
// Обработка смены редактора (прежде чем обновлять книгу)
|
||||
if ($editor_changed) {
|
||||
$conversion_success = $bookModel->convertChaptersContent($id, $old_editor_type, $new_editor_type);
|
||||
if (!$conversion_success) {
|
||||
$_SESSION['warning'] = "Внимание: не удалось автоматически сконвертировать содержание всех глав.";
|
||||
}
|
||||
}
|
||||
|
||||
// Обработка обложки
|
||||
if (isset($_FILES['cover_image']) && $_FILES['cover_image']['error'] === UPLOAD_ERR_OK) {
|
||||
$cover_result = handleCoverUpload($_FILES['cover_image'], $id);
|
||||
if ($cover_result['success']) {
|
||||
$bookModel->updateCover($id, $cover_result['filename']);
|
||||
} else {
|
||||
$cover_error = $cover_result['error'];
|
||||
}
|
||||
}
|
||||
|
||||
// Удаление обложки
|
||||
if (isset($_POST['delete_cover']) && $_POST['delete_cover'] == '1') {
|
||||
$bookModel->deleteCover($id);
|
||||
}
|
||||
|
||||
// Обновление книги
|
||||
$success = $bookModel->update($id, $data);
|
||||
|
||||
if ($success) {
|
||||
$success_message = "Книга успешно обновлена";
|
||||
if ($editor_changed) {
|
||||
$success_message .= ". Содержание глав сконвертировано в новый формат.";
|
||||
}
|
||||
$_SESSION['success'] = $success_message;
|
||||
$this->redirect("/books/{$id}/edit");
|
||||
} else {
|
||||
$error = "Ошибка при обновлении книги";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Получаем статистику по главам для отображения в шаблоне
|
||||
$chapterModel = new Chapter($this->pdo);
|
||||
$chapters = $chapterModel->findByBook($id);
|
||||
|
||||
$this->render('books/edit', [
|
||||
'book' => $book,
|
||||
'series' => $series,
|
||||
'chapters' => $chapters,
|
||||
'editor_types' => $editor_types,
|
||||
'error' => $error,
|
||||
'cover_error' => $cover_error,
|
||||
'page_title' => 'Редактирование книги'
|
||||
]);
|
||||
}
|
||||
|
||||
public function delete($id) {
|
||||
$this->requireLogin();
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
$_SESSION['error'] = "Неверный метод запроса";
|
||||
$this->redirect('/books');
|
||||
}
|
||||
if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
|
||||
$_SESSION['error'] = "Ошибка безопасности";
|
||||
$this->redirect('/books');
|
||||
}
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$bookModel = new Book($this->pdo);
|
||||
if (!$bookModel->userOwnsBook($id, $user_id)) {
|
||||
$_SESSION['error'] = "У вас нет доступа к этой книге";
|
||||
$this->redirect('/books');
|
||||
}
|
||||
if ($bookModel->delete($id, $user_id)) {
|
||||
$_SESSION['success'] = "Книга успешно удалена";
|
||||
} else {
|
||||
$_SESSION['error'] = "Ошибка при удалении книги";
|
||||
}
|
||||
$this->redirect('/books');
|
||||
}
|
||||
|
||||
public function viewPublic($share_token) {
|
||||
$bookModel = new Book($this->pdo);
|
||||
$book = $bookModel->findByShareToken($share_token);
|
||||
if (!$book) {
|
||||
http_response_code(404);
|
||||
$this->render('errors/404');
|
||||
return;
|
||||
}
|
||||
$chapters = $bookModel->getPublishedChapters($book['id']);
|
||||
|
||||
// Получаем информацию об авторе
|
||||
$stmt = $this->pdo->prepare("SELECT id, username, display_name FROM users WHERE id = ?");
|
||||
$stmt->execute([$book['user_id']]);
|
||||
$author = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
$this->render('books/view_public', [
|
||||
'book' => $book,
|
||||
'chapters' => $chapters,
|
||||
'author' => $author,
|
||||
'page_title' => $book['title']
|
||||
]);
|
||||
}
|
||||
|
||||
public function normalizeContent($id) {
|
||||
$this->requireLogin();
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
$_SESSION['error'] = "Неверный метод запроса";
|
||||
$this->redirect("/books/{$id}/edit");
|
||||
}
|
||||
if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
|
||||
$_SESSION['error'] = "Ошибка безопасности";
|
||||
$this->redirect("/books/{$id}/edit");
|
||||
}
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$bookModel = new Book($this->pdo);
|
||||
if (!$bookModel->userOwnsBook($id, $user_id)) {
|
||||
$_SESSION['error'] = "У вас нет доступа к этой книге";
|
||||
$this->redirect('/books');
|
||||
}
|
||||
if ($bookModel->normalizeBookContent($id)) {
|
||||
$_SESSION['success'] = "Контент глав успешно нормализован";
|
||||
} else {
|
||||
$_SESSION['error'] = "Ошибка при нормализации контента";
|
||||
}
|
||||
$this->redirect("/books/{$id}/edit");
|
||||
}
|
||||
|
||||
public function regenerateToken($id) {
|
||||
$this->requireLogin();
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
$_SESSION['error'] = "Неверный метод запроса";
|
||||
$this->redirect("/books/{$id}/edit");
|
||||
}
|
||||
if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
|
||||
$_SESSION['error'] = "Ошибка безопасности";
|
||||
$this->redirect("/books/{$id}/edit");
|
||||
}
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$bookModel = new Book($this->pdo);
|
||||
if (!$bookModel->userOwnsBook($id, $user_id)) {
|
||||
$_SESSION['error'] = "У вас нет доступа к этой книге";
|
||||
$this->redirect('/books');
|
||||
}
|
||||
$new_token = $bookModel->generateNewShareToken($id);
|
||||
if ($new_token) {
|
||||
$_SESSION['success'] = "Ссылка успешно обновлена";
|
||||
} else {
|
||||
$_SESSION['error'] = "Ошибка при обновлении ссылки";
|
||||
}
|
||||
$this->redirect("/books/{$id}/edit");
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
// controllers/ChapterController.php
|
||||
require_once 'controllers/BaseController.php';
|
||||
require_once 'models/Chapter.php';
|
||||
require_once 'models/Book.php';
|
||||
require_once 'includes/parsedown/ParsedownExtra.php';
|
||||
|
||||
class ChapterController extends BaseController {
|
||||
|
||||
public function index($book_id) {
|
||||
$this->requireLogin();
|
||||
|
||||
$user_id = $_SESSION['user_id'];
|
||||
|
||||
$bookModel = new Book($this->pdo);
|
||||
$chapterModel = new Chapter($this->pdo);
|
||||
|
||||
// Проверяем права доступа к книге
|
||||
if (!$bookModel->userOwnsBook($book_id, $user_id)) {
|
||||
$_SESSION['error'] = "У вас нет доступа к этой книге";
|
||||
$this->redirect('/books');
|
||||
}
|
||||
|
||||
// Получаем информацию о книге и главах
|
||||
$book = $bookModel->findById($book_id);
|
||||
$chapters = $chapterModel->findByBook($book_id);
|
||||
|
||||
$this->render('chapters/index', [
|
||||
'book' => $book,
|
||||
'chapters' => $chapters,
|
||||
'page_title' => "Главы книги: " . e($book['title'])
|
||||
]);
|
||||
}
|
||||
|
||||
public function create($book_id) {
|
||||
$this->requireLogin();
|
||||
|
||||
$user_id = $_SESSION['user_id'];
|
||||
|
||||
$bookModel = new Book($this->pdo);
|
||||
$chapterModel = new Chapter($this->pdo);
|
||||
|
||||
// Проверяем права доступа к книге
|
||||
if (!$bookModel->userOwnsBook($book_id, $user_id)) {
|
||||
$_SESSION['error'] = "У вас нет доступа к этой книге";
|
||||
$this->redirect('/books');
|
||||
}
|
||||
|
||||
$book = $bookModel->findById($book_id);
|
||||
$error = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
|
||||
$error = "Ошибка безопасности";
|
||||
} else {
|
||||
$title = trim($_POST['title'] ?? '');
|
||||
$content = $_POST['content'] ?? '';
|
||||
$status = $_POST['status'] ?? 'draft';
|
||||
|
||||
if (empty($title)) {
|
||||
$error = "Название главы обязательно";
|
||||
} else {
|
||||
$data = [
|
||||
'book_id' => $book_id,
|
||||
'title' => $title,
|
||||
'content' => $content,
|
||||
'status' => $status
|
||||
];
|
||||
|
||||
if ($chapterModel->create($data)) {
|
||||
$_SESSION['success'] = "Глава успешно создана";
|
||||
$this->redirect("/books/{$book_id}/chapters");
|
||||
} else {
|
||||
$error = "Ошибка при создании главы";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->render('chapters/create', [
|
||||
'book' => $book,
|
||||
'error' => $error,
|
||||
'page_title' => "Новая глава для: " . e($book['title'])
|
||||
]);
|
||||
}
|
||||
|
||||
public function edit($id) {
|
||||
$this->requireLogin();
|
||||
|
||||
$user_id = $_SESSION['user_id'];
|
||||
|
||||
$chapterModel = new Chapter($this->pdo);
|
||||
$bookModel = new Book($this->pdo);
|
||||
|
||||
// Проверяем права доступа к главе
|
||||
if (!$chapterModel->userOwnsChapter($id, $user_id)) {
|
||||
$_SESSION['error'] = "У вас нет доступа к этой главе";
|
||||
$this->redirect('/books');
|
||||
}
|
||||
|
||||
$chapter = $chapterModel->findById($id);
|
||||
$book = $bookModel->findById($chapter['book_id']);
|
||||
$error = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
|
||||
$error = "Ошибка безопасности";
|
||||
} else {
|
||||
$title = trim($_POST['title'] ?? '');
|
||||
$content = $_POST['content'] ?? '';
|
||||
$status = $_POST['status'] ?? 'draft';
|
||||
|
||||
if (empty($title)) {
|
||||
$error = "Название главы обязательно";
|
||||
} else {
|
||||
$data = [
|
||||
'title' => $title,
|
||||
'content' => $content,
|
||||
'status' => $status
|
||||
];
|
||||
|
||||
if ($chapterModel->update($id, $data)) {
|
||||
$_SESSION['success'] = "Глава успешно обновлена";
|
||||
$this->redirect("/books/{$chapter['book_id']}/chapters");
|
||||
} else {
|
||||
$error = "Ошибка при обновлении главы";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->render('chapters/edit', [
|
||||
'chapter' => $chapter,
|
||||
'book' => $book,
|
||||
'error' => $error,
|
||||
'page_title' => "Редактирование главы: " . e($chapter['title'])
|
||||
]);
|
||||
}
|
||||
|
||||
public function delete($id) {
|
||||
$this->requireLogin();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
$_SESSION['error'] = "Неверный метод запроса";
|
||||
$this->redirect('/books');
|
||||
}
|
||||
|
||||
if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
|
||||
$_SESSION['error'] = "Ошибка безопасности";
|
||||
$this->redirect('/books');
|
||||
}
|
||||
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$chapterModel = new Chapter($this->pdo);
|
||||
|
||||
// Проверяем права доступа
|
||||
if (!$chapterModel->userOwnsChapter($id, $user_id)) {
|
||||
$_SESSION['error'] = "У вас нет доступа к этой главе";
|
||||
$this->redirect('/books');
|
||||
}
|
||||
|
||||
$chapter = $chapterModel->findById($id);
|
||||
$book_id = $chapter['book_id'];
|
||||
|
||||
// Удаляем главу
|
||||
if ($chapterModel->delete($id)) {
|
||||
$_SESSION['success'] = "Глава успешно удалена";
|
||||
} else {
|
||||
$_SESSION['error'] = "Ошибка при удалении главы";
|
||||
}
|
||||
|
||||
$this->redirect("/books/{$book_id}/chapters");
|
||||
}
|
||||
|
||||
public function preview() {
|
||||
$this->requireLogin();
|
||||
|
||||
require_once 'includes/parsedown/ParsedownExtra.php';
|
||||
$Parsedown = new ParsedownExtra();
|
||||
|
||||
$content = $_POST['content'] ?? '';
|
||||
$title = $_POST['title'] ?? 'Предпросмотр';
|
||||
$editor_type = $_POST['editor_type'] ?? 'markdown';
|
||||
|
||||
// Обрабатываем контент в зависимости от типа редактора
|
||||
if ($editor_type == 'markdown') {
|
||||
$html_content = $Parsedown->text($content);
|
||||
} else {
|
||||
$html_content = $content;
|
||||
}
|
||||
|
||||
$this->render('chapters/preview', [
|
||||
'content' => $html_content,
|
||||
'title' => $title,
|
||||
'page_title' => "Предпросмотр: " . e($title)
|
||||
]);
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
// controllers/DashboardController.php
|
||||
require_once 'controllers/BaseController.php';
|
||||
require_once 'models/Book.php';
|
||||
require_once 'models/Chapter.php';
|
||||
require_once 'models/Series.php';
|
||||
|
||||
class DashboardController extends BaseController {
|
||||
|
||||
public function index() {
|
||||
$this->requireLogin();
|
||||
|
||||
$user_id = $_SESSION['user_id'];
|
||||
|
||||
$bookModel = new Book($this->pdo);
|
||||
$chapterModel = new Chapter($this->pdo);
|
||||
$seriesModel = new Series($this->pdo);
|
||||
|
||||
// Получаем статистику
|
||||
$books = $bookModel->findByUser($user_id);
|
||||
$published_books = $bookModel->findByUser($user_id, true);
|
||||
|
||||
$total_books = count($books);
|
||||
$published_books_count = count($published_books);
|
||||
|
||||
// Общее количество слов и глав
|
||||
$total_words = 0;
|
||||
$total_chapters = 0;
|
||||
foreach ($books as $book) {
|
||||
$stats = $bookModel->getBookStats($book['id']);
|
||||
$total_words += $stats['total_words'] ?? 0;
|
||||
$total_chapters += $stats['chapter_count'] ?? 0;
|
||||
}
|
||||
|
||||
// Последние книги
|
||||
$recent_books = array_slice($books, 0, 5);
|
||||
|
||||
// Серии
|
||||
$series = $seriesModel->findByUser($user_id);
|
||||
|
||||
$this->render('dashboard/index', [
|
||||
'total_books' => $total_books,
|
||||
'published_books_count' => $published_books_count,
|
||||
'total_words' => $total_words,
|
||||
'total_chapters' => $total_chapters,
|
||||
'recent_books' => $recent_books,
|
||||
'series' => $series,
|
||||
'page_title' => 'Панель управления'
|
||||
]);
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,883 @@
|
||||
<?php
|
||||
// controllers/ExportController.php
|
||||
require_once 'controllers/BaseController.php';
|
||||
require_once 'models/Book.php';
|
||||
require_once 'models/Chapter.php';
|
||||
require_once 'vendor/autoload.php';
|
||||
require_once 'includes/parsedown/ParsedownExtra.php';
|
||||
|
||||
use PhpOffice\PhpWord\PhpWord;
|
||||
use PhpOffice\PhpWord\IOFactory;
|
||||
use TCPDF;
|
||||
|
||||
class ExportController extends BaseController {
|
||||
|
||||
public function export($book_id, $format = 'pdf') {
|
||||
$this->requireLogin();
|
||||
|
||||
$user_id = $_SESSION['user_id'];
|
||||
|
||||
$bookModel = new Book($this->pdo);
|
||||
$chapterModel = new Chapter($this->pdo);
|
||||
|
||||
$book = $bookModel->findById($book_id);
|
||||
if (!$book || $book['user_id'] != $user_id) {
|
||||
$_SESSION['error'] = "Доступ запрещен";
|
||||
$this->redirect('/books');
|
||||
}
|
||||
|
||||
// Для автора - все главы
|
||||
$chapters = $chapterModel->findByBook($book_id);
|
||||
|
||||
// Получаем информацию об авторе
|
||||
$author_name = $this->getAuthorName($book['user_id']);
|
||||
|
||||
$this->handleExport($book, $chapters, false, $author_name, $format);
|
||||
}
|
||||
|
||||
public function exportShared($share_token, $format = 'pdf') {
|
||||
$bookModel = new Book($this->pdo);
|
||||
$chapterModel = new Chapter($this->pdo);
|
||||
|
||||
$book = $bookModel->findByShareToken($share_token);
|
||||
if (!$book) {
|
||||
$_SESSION['error'] = "Книга не найдена";
|
||||
$this->redirect('/');
|
||||
}
|
||||
|
||||
// Для публичного доступа - только опубликованные главы
|
||||
$chapters = $bookModel->getPublishedChapters($book['id']);
|
||||
|
||||
// Получаем информацию об авторе
|
||||
$author_name = $this->getAuthorName($book['user_id']);
|
||||
|
||||
$this->handleExport($book, $chapters, true, $author_name, $format);
|
||||
}
|
||||
|
||||
private function getAuthorName($user_id) {
|
||||
$stmt = $this->pdo->prepare("SELECT display_name, username FROM users WHERE id = ?");
|
||||
$stmt->execute([$user_id]);
|
||||
$author_info = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($author_info && $author_info['display_name'] != "") {
|
||||
return $author_info['display_name'];
|
||||
} elseif ($author_info) {
|
||||
return $author_info['username'];
|
||||
}
|
||||
|
||||
return "Неизвестный автор";
|
||||
}
|
||||
|
||||
private function handleExport($book, $chapters, $is_public, $author_name, $format) {
|
||||
$Parsedown = new ParsedownExtra();
|
||||
|
||||
switch ($format) {
|
||||
case 'pdf':
|
||||
$this->exportPDF($book, $chapters, $is_public, $author_name, $Parsedown);
|
||||
break;
|
||||
case 'docx':
|
||||
$this->exportDOCX($book, $chapters, $is_public, $author_name, $Parsedown);
|
||||
break;
|
||||
case 'html':
|
||||
$this->exportHTML($book, $chapters, $is_public, $author_name, $Parsedown);
|
||||
break;
|
||||
case 'txt':
|
||||
$this->exportTXT($book, $chapters, $is_public, $author_name, $Parsedown);
|
||||
break;
|
||||
default:
|
||||
$_SESSION['error'] = "Неверный формат экспорта";
|
||||
$redirect_url = $is_public ?
|
||||
"/book/{$book['share_token']}" :
|
||||
"/books/{$book['id']}/edit";
|
||||
$this->redirect($redirect_url);
|
||||
}
|
||||
}
|
||||
|
||||
function exportPDF($book, $chapters, $is_public, $author_name) {
|
||||
global $Parsedown;
|
||||
|
||||
$pdf = new TCPDF(PDF_PAGE_ORIENTATION, PDF_UNIT, PDF_PAGE_FORMAT, true, 'UTF-8', false);
|
||||
|
||||
// Устанавливаем метаданные документа
|
||||
$pdf->SetCreator(APP_NAME);
|
||||
$pdf->SetAuthor($author_name);
|
||||
$pdf->SetTitle($book['title']);
|
||||
$pdf->SetSubject($book['genre'] ?? '');
|
||||
|
||||
// Устанавливаем margins
|
||||
$pdf->SetMargins(15, 25, 15);
|
||||
$pdf->SetHeaderMargin(10);
|
||||
$pdf->SetFooterMargin(10);
|
||||
|
||||
// Устанавливаем авто разрыв страниц
|
||||
$pdf->SetAutoPageBreak(TRUE, 15);
|
||||
|
||||
// Добавляем страницу
|
||||
$pdf->AddPage();
|
||||
|
||||
// Устанавливаем шрифт с поддержкой кириллицы
|
||||
$pdf->SetFont('dejavusans', '', 12);
|
||||
|
||||
// Заголовок книги
|
||||
$pdf->SetFont('dejavusans', 'B', 18);
|
||||
$pdf->Cell(0, 10, $book['title'], 0, 1, 'C');
|
||||
$pdf->Ln(2);
|
||||
|
||||
// Автор
|
||||
$pdf->SetFont('dejavusans', 'I', 14);
|
||||
$pdf->Cell(0, 10, $author_name, 0, 1, 'C');
|
||||
$pdf->Ln(5);
|
||||
|
||||
// Обложка книги
|
||||
if (!empty($book['cover_image'])) {
|
||||
$cover_path = COVERS_PATH . $book['cover_image'];
|
||||
if (file_exists($cover_path)) {
|
||||
list($width, $height) = getimagesize($cover_path);
|
||||
$max_width = 80;
|
||||
$ratio = $width / $height;
|
||||
$new_height = $max_width / $ratio;
|
||||
|
||||
$x = (210 - $max_width) / 2;
|
||||
$pdf->Image($cover_path, $x, $pdf->GetY(), $max_width, $new_height, '', '', 'N', false, 300, '', false, false, 0, false, false, false);
|
||||
$pdf->Ln($new_height + 5);
|
||||
}
|
||||
}
|
||||
|
||||
// Жанр
|
||||
if (!empty($book['genre'])) {
|
||||
$pdf->SetFont('dejavusans', 'I', 12);
|
||||
$pdf->Cell(0, 10, 'Жанр: ' . $book['genre'], 0, 1, 'C');
|
||||
$pdf->Ln(5);
|
||||
}
|
||||
|
||||
// Описание
|
||||
if (!empty($book['description'])) {
|
||||
$pdf->SetFont('dejavusans', '', 11);
|
||||
$pdf->MultiCell(0, 6, $book['description'], 0, 'J');
|
||||
$pdf->Ln(10);
|
||||
}
|
||||
|
||||
// Интерактивное оглавление
|
||||
$chapterLinks = [];
|
||||
if (!empty($chapters)) {
|
||||
$pdf->SetFont('dejavusans', 'B', 14);
|
||||
$pdf->Cell(0, 10, 'Оглавление', 0, 1, 'C');
|
||||
$pdf->Ln(5);
|
||||
|
||||
$toc_page = $pdf->getPage();
|
||||
|
||||
$pdf->SetFont('dejavusans', '', 11);
|
||||
foreach ($chapters as $index => $chapter) {
|
||||
$chapter_number = $index + 1;
|
||||
$link = $pdf->AddLink();
|
||||
$chapterLinks[$chapter['id']] = $link; // Сохраняем ссылку для этой главы
|
||||
$pdf->Cell(0, 6, "{$chapter_number}. {$chapter['title']}", 0, 1, 'L', false, $link);
|
||||
}
|
||||
$pdf->Ln(10);
|
||||
}
|
||||
|
||||
// Разделитель
|
||||
$pdf->Line(15, $pdf->GetY(), 195, $pdf->GetY());
|
||||
$pdf->Ln(10);
|
||||
|
||||
// Главы с закладками и правильными ссылками
|
||||
foreach ($chapters as $index => $chapter) {
|
||||
// Добавляем новую страницу для каждой главы
|
||||
$pdf->AddPage();
|
||||
|
||||
// УСТАНАВЛИВАЕМ ЯКОРЬ ДЛЯ ССЫЛКИ
|
||||
if (isset($chapterLinks[$chapter['id']])) {
|
||||
$pdf->SetLink($chapterLinks[$chapter['id']]);
|
||||
}
|
||||
|
||||
// Устанавливаем закладку для этой главы
|
||||
$pdf->Bookmark($chapter['title'], 0, 0, '', 'B', array(0, 0, 0));
|
||||
|
||||
// Название главы
|
||||
$pdf->SetFont('dejavusans', 'B', 14);
|
||||
$pdf->Cell(0, 8, $chapter['title'], 0, 1);
|
||||
$pdf->Ln(2);
|
||||
|
||||
// Контент главы
|
||||
$pdf->SetFont('dejavusans', '', 11);
|
||||
if ($book['editor_type'] == 'markdown') {
|
||||
$htmlContent = $Parsedown->text($chapter['content']);
|
||||
} else {
|
||||
$htmlContent = $chapter['content'];
|
||||
}
|
||||
$pdf->writeHTML($htmlContent, true, false, true, false, '');
|
||||
|
||||
$pdf->Ln(8);
|
||||
}
|
||||
|
||||
// Футер с информацией
|
||||
$pdf->SetY(-25);
|
||||
$pdf->SetFont('dejavusans', 'I', 8);
|
||||
$pdf->Cell(0, 6, 'Экспортировано из ' . APP_NAME . ' - ' . date('d.m.Y H:i'), 0, 1, 'C');
|
||||
$pdf->Cell(0, 6, 'Автор: ' . $author_name . ' | Всего глав: ' . count($chapters) . ' | Всего слов: ' . array_sum(array_column($chapters, 'word_count')), 0, 1, 'C');
|
||||
|
||||
// Отправляем файл
|
||||
$filename = cleanFilename($book['title']) . '.pdf';
|
||||
$pdf->Output($filename, 'D');
|
||||
exit;
|
||||
}
|
||||
|
||||
function exportDOCX($book, $chapters, $is_public, $author_name) {
|
||||
global $Parsedown;
|
||||
|
||||
$phpWord = new PhpWord();
|
||||
|
||||
// Стили документа
|
||||
$phpWord->setDefaultFontName('Times New Roman');
|
||||
$phpWord->setDefaultFontSize(12);
|
||||
|
||||
// Секция документа
|
||||
$section = $phpWord->addSection();
|
||||
|
||||
// Заголовок книги
|
||||
$section->addText($book['title'], ['bold' => true, 'size' => 16], ['alignment' => 'center']);
|
||||
$section->addTextBreak(1);
|
||||
|
||||
// Автор
|
||||
$section->addText($author_name, ['italic' => true, 'size' => 14], ['alignment' => 'center']);
|
||||
$section->addTextBreak(2);
|
||||
|
||||
// Обложка книги
|
||||
if (!empty($book['cover_image'])) {
|
||||
$cover_path = COVERS_PATH . $book['cover_image'];
|
||||
if (file_exists($cover_path)) {
|
||||
$section->addImage($cover_path, [
|
||||
'width' => 150,
|
||||
'height' => 225,
|
||||
'alignment' => 'center'
|
||||
]);
|
||||
$section->addTextBreak(2);
|
||||
}
|
||||
}
|
||||
|
||||
// Жанр
|
||||
if (!empty($book['genre'])) {
|
||||
$section->addText('Жанр: ' . $book['genre'], ['italic' => true], ['alignment' => 'center']);
|
||||
$section->addTextBreak(1);
|
||||
}
|
||||
|
||||
// Описание
|
||||
if (!empty($book['description'])) {
|
||||
if ($book['editor_type'] == 'markdown') {
|
||||
$descriptionParagraphs = $this->markdownToParagraphs($book['description']);
|
||||
} else {
|
||||
$descriptionParagraphs = $this->htmlToParagraphs($book['description']);
|
||||
}
|
||||
|
||||
foreach ($descriptionParagraphs as $paragraph) {
|
||||
if (!empty(trim($paragraph))) {
|
||||
$section->addText($paragraph);
|
||||
}
|
||||
}
|
||||
$section->addTextBreak(2);
|
||||
}
|
||||
|
||||
// Интерактивное оглавление
|
||||
if (!empty($chapters)) {
|
||||
$section->addText('Оглавление', ['bold' => true, 'size' => 14], ['alignment' => 'center']);
|
||||
$section->addTextBreak(1);
|
||||
|
||||
foreach ($chapters as $index => $chapter) {
|
||||
$chapter_number = $index + 1;
|
||||
// Создаем гиперссылку на заголовок главы
|
||||
$section->addLink("chapter_{$chapter['id']}", "{$chapter_number}. {$chapter['title']}", null, null, true);
|
||||
$section->addTextBreak(1);
|
||||
}
|
||||
$section->addTextBreak(2);
|
||||
}
|
||||
|
||||
// Разделитель
|
||||
$section->addPageBreak();
|
||||
|
||||
// Главы с закладками
|
||||
foreach ($chapters as $index => $chapter) {
|
||||
// Добавляем закладку для главы
|
||||
$section->addBookmark("chapter_{$chapter['id']}");
|
||||
|
||||
// Заголовок главы
|
||||
$section->addText($chapter['title'], ['bold' => true, 'size' => 14]);
|
||||
$section->addTextBreak(1);
|
||||
|
||||
// Получаем очищенный текст и разбиваем на абзацы в зависимости от типа редактора
|
||||
if ($book['editor_type'] == 'markdown') {
|
||||
$cleanContent = $this->cleanMarkdown($chapter['content']);
|
||||
$paragraphs = $this->markdownToParagraphs($cleanContent);
|
||||
} else {
|
||||
$cleanContent = strip_tags($chapter['content']);
|
||||
$paragraphs = $this->htmlToParagraphs($chapter['content']);
|
||||
}
|
||||
|
||||
// Добавляем каждый абзац
|
||||
foreach ($paragraphs as $paragraph) {
|
||||
if (!empty(trim($paragraph))) {
|
||||
$section->addText($paragraph);
|
||||
$section->addTextBreak(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Добавляем разрыв страницы между главами (кроме последней)
|
||||
if ($index < count($chapters) - 1) {
|
||||
$section->addPageBreak();
|
||||
}
|
||||
}
|
||||
|
||||
// Футер
|
||||
$section->addTextBreak(2);
|
||||
$section->addText('Экспортировано из ' . APP_NAME . ' - ' . date('d.m.Y H:i'), ['italic' => true, 'size' => 9]);
|
||||
$section->addText('Автор: ' . $author_name . ' | Всего глав: ' . count($chapters) . ' | Всего слов: ' . array_sum(array_column($chapters, 'word_count')), ['italic' => true, 'size' => 9]);
|
||||
|
||||
// Сохраняем и отправляем
|
||||
$filename = cleanFilename($book['title']) . '.docx';
|
||||
header('Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document');
|
||||
header('Content-Disposition: attachment; filename="' . $filename . '"');
|
||||
|
||||
$objWriter = IOFactory::createWriter($phpWord, 'Word2007');
|
||||
$objWriter->save('php://output');
|
||||
exit;
|
||||
}
|
||||
|
||||
function exportHTML($book, $chapters, $is_public, $author_name) {
|
||||
global $Parsedown;
|
||||
|
||||
$html = '<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>' . htmlspecialchars($book['title']) . '</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: "Times New Roman", serif;
|
||||
line-height: 1.6;
|
||||
margin: 40px;
|
||||
max-width: 900px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
color: #333;
|
||||
}
|
||||
.book-title {
|
||||
text-align: center;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.book-author {
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
font-style: italic;
|
||||
color: #666;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.book-cover {
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.book-cover img {
|
||||
max-width: 200px;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
.book-genre {
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
color: #666;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.book-description {
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
margin: 20px 0;
|
||||
border-left: 4px solid #007bff;
|
||||
}
|
||||
.table-of-contents {
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
margin: 20px 0;
|
||||
columns: 1;
|
||||
column-gap: 2rem;
|
||||
}
|
||||
.table-of-contents h3 {
|
||||
margin-top: 0;
|
||||
text-align: center;
|
||||
column-span: all;
|
||||
}
|
||||
.table-of-contents ul {
|
||||
list-style-type: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
.table-of-contents li {
|
||||
margin-bottom: 5px;
|
||||
padding: 5px 0;
|
||||
break-inside: avoid;
|
||||
}
|
||||
.table-of-contents a {
|
||||
text-decoration: none;
|
||||
color: #333;
|
||||
}
|
||||
.table-of-contents a:hover {
|
||||
color: #007bff;
|
||||
}
|
||||
.chapter-title {
|
||||
border-bottom: 2px solid #007bff;
|
||||
padding-bottom: 10px;
|
||||
margin-top: 30px;
|
||||
font-size: 20px;
|
||||
scroll-margin-top: 2rem;
|
||||
}
|
||||
.chapter-content {
|
||||
margin: 20px 0;
|
||||
text-align: justify;
|
||||
}
|
||||
.footer {
|
||||
margin-top: 40px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #ddd;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
/* Отображение абзацев */
|
||||
.chapter-content p {
|
||||
margin-bottom: 1em;
|
||||
text-align: justify;
|
||||
}
|
||||
.dialogue {
|
||||
margin-left: 2rem;
|
||||
font-style: italic;
|
||||
color: #2c5aa0;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
/* Остальные стили */
|
||||
.chapter-content h1, .chapter-content h2, .chapter-content h3 {
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
.chapter-content blockquote {
|
||||
border-left: 4px solid #007bff;
|
||||
padding-left: 15px;
|
||||
margin-left: 0;
|
||||
color: #555;
|
||||
font-style: italic;
|
||||
}
|
||||
.chapter-content code {
|
||||
background: #f5f5f5;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.chapter-content pre {
|
||||
background: #f5f5f5;
|
||||
padding: 1rem;
|
||||
border-radius: 5px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.chapter-content ul, .chapter-content ol {
|
||||
margin-bottom: 1rem;
|
||||
padding-left: 2rem;
|
||||
}
|
||||
.chapter-content table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.chapter-content th, .chapter-content td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
}
|
||||
.chapter-content th {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.table-of-contents {
|
||||
columns: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="book-title">' . htmlspecialchars($book['title']) . '</div>
|
||||
<div class="book-author">' . htmlspecialchars($author_name) . '</div>';
|
||||
|
||||
if (!empty($book['genre'])) {
|
||||
$html .= '<div class="book-genre">Жанр: ' . htmlspecialchars($book['genre']) . '</div>';
|
||||
}
|
||||
|
||||
// Обложка книги
|
||||
if (!empty($book['cover_image'])) {
|
||||
$cover_url = COVERS_URL . $book['cover_image'];
|
||||
$html .= '<div class="book-cover">';
|
||||
$html .= '<img src="' . $cover_url . '" alt="' . htmlspecialchars($book['title']) . '">';
|
||||
$html .= '</div>';
|
||||
}
|
||||
|
||||
if (!empty($book['description'])) {
|
||||
$html .= '<div class="book-description">';
|
||||
if ($book['editor_type'] == 'markdown') {
|
||||
$html .= nl2br(htmlspecialchars($book['description']));
|
||||
} else {
|
||||
$html .= $book['description'];
|
||||
}
|
||||
$html .= '</div>';
|
||||
}
|
||||
|
||||
// Интерактивное оглавление
|
||||
if (!empty($chapters)) {
|
||||
$html .= '<div class="table-of-contents">';
|
||||
$html .= '<h3>Оглавление</h3>';
|
||||
$html .= '<ul>';
|
||||
foreach ($chapters as $index => $chapter) {
|
||||
$chapter_number = $index + 1;
|
||||
$html .= '<li><a href="#chapter-' . $chapter['id'] . '">' . $chapter_number . '. ' . htmlspecialchars($chapter['title']) . '</a></li>';
|
||||
}
|
||||
$html .= '</ul>';
|
||||
$html .= '</div>';
|
||||
}
|
||||
|
||||
$html .= '<hr style="margin: 30px 0;">';
|
||||
|
||||
foreach ($chapters as $index => $chapter) {
|
||||
$html .= '<div class="chapter">';
|
||||
$html .= '<div class="chapter-title" id="chapter-' . $chapter['id'] . '" name="chapter-' . $chapter['id'] . '">' . htmlspecialchars($chapter['title']) . '</div>';
|
||||
|
||||
// Обрабатываем контент в зависимости от типа редактора
|
||||
if ($book['editor_type'] == 'markdown') {
|
||||
$htmlContent = $Parsedown->text($chapter['content']);
|
||||
} else {
|
||||
$htmlContent = $chapter['content'];
|
||||
}
|
||||
|
||||
$html .= '<div class="chapter-content">' . $htmlContent . '</div>';
|
||||
$html .= '</div>';
|
||||
|
||||
if ($index < count($chapters) - 1) {
|
||||
$html .= '<hr style="margin: 30px 0;">';
|
||||
}
|
||||
}
|
||||
|
||||
$html .= '<div class="footer">
|
||||
Экспортировано из ' . APP_NAME . ' - ' . date('d.m.Y H:i') . '<br>
|
||||
Автор: ' . htmlspecialchars($author_name) . ' | Всего глав: ' . count($chapters) . ' | Всего слов: ' . array_sum(array_column($chapters, 'word_count')) . '
|
||||
</div>
|
||||
</body>
|
||||
</html>';
|
||||
|
||||
$filename = cleanFilename($book['title']) . '.html';
|
||||
header('Content-Type: text/html; charset=utf-8');
|
||||
header('Content-Disposition: attachment; filename="' . $filename . '"');
|
||||
echo $html;
|
||||
exit;
|
||||
}
|
||||
|
||||
function exportTXT($book, $chapters, $is_public, $author_name) {
|
||||
$content = "=" . str_repeat("=", 80) . "=\n";
|
||||
$content .= str_pad($book['title'], 80, " ", STR_PAD_BOTH) . "\n";
|
||||
$content .= str_pad($author_name, 80, " ", STR_PAD_BOTH) . "\n";
|
||||
$content .= "=" . str_repeat("=", 80) . "=\n\n";
|
||||
|
||||
if (!empty($book['genre'])) {
|
||||
$content .= "Жанр: " . $book['genre'] . "\n\n";
|
||||
}
|
||||
|
||||
if (!empty($book['description'])) {
|
||||
$content .= "ОПИСАНИЕ:\n";
|
||||
|
||||
// Обрабатываем описание в зависимости от типа редактора
|
||||
if ($book['editor_type'] == 'markdown') {
|
||||
$descriptionText = $this->cleanMarkdown($book['description']);
|
||||
} else {
|
||||
$descriptionText = strip_tags($book['description']);
|
||||
}
|
||||
|
||||
$content .= wordwrap($descriptionText, 144) . "\n\n";
|
||||
}
|
||||
|
||||
// Оглавление
|
||||
if (!empty($chapters)) {
|
||||
$content .= "ОГЛАВЛЕНИЕ:\n";
|
||||
$content .= str_repeat("-", 60) . "\n";
|
||||
foreach ($chapters as $index => $chapter) {
|
||||
$chapter_number = $index + 1;
|
||||
$content .= "{$chapter_number}. {$chapter['title']}\n";
|
||||
}
|
||||
$content .= "\n";
|
||||
}
|
||||
|
||||
$content .= str_repeat("-", 144) . "\n\n";
|
||||
|
||||
foreach ($chapters as $index => $chapter) {
|
||||
$content .= $chapter['title'] . "\n";
|
||||
$content .= str_repeat("-", 60) . "\n\n";
|
||||
|
||||
// Получаем очищенный текст в зависимости от типа редактора
|
||||
if ($book['editor_type'] == 'markdown') {
|
||||
$cleanContent = $this->cleanMarkdown($chapter['content']);
|
||||
$paragraphs = $this->markdownToParagraphs($cleanContent);
|
||||
} else {
|
||||
$cleanContent = strip_tags($chapter['content']);
|
||||
$paragraphs = $this->htmlToPlainTextParagraphs($cleanContent);
|
||||
}
|
||||
|
||||
foreach ($paragraphs as $paragraph) {
|
||||
if (!empty(trim($paragraph))) {
|
||||
$content .= wordwrap($paragraph, 144) . "\n\n";
|
||||
}
|
||||
}
|
||||
|
||||
if ($index < count($chapters) - 1) {
|
||||
$content .= str_repeat("-", 144) . "\n\n";
|
||||
}
|
||||
}
|
||||
|
||||
$content .= "\n" . str_repeat("=", 144) . "\n";
|
||||
$content .= "Экспортировано из " . APP_NAME . " - " . date('d.m.Y H:i') . "\n";
|
||||
$content .= "Автор: " . $author_name . " | Всего глав: " . count($chapters) . " | Всего слов: " . array_sum(array_column($chapters, 'word_count')) . "\n";
|
||||
$content .= str_repeat("=", 144) . "\n";
|
||||
|
||||
$filename = cleanFilename($book['title']) . '.txt';
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
header('Content-Disposition: attachment; filename="' . $filename . '"');
|
||||
echo $content;
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Функция для преобразования Markdown в чистый текст с форматированием абзацев
|
||||
function markdownToPlainText($markdown) {
|
||||
// Обрабатываем диалоги (заменяем - на —)
|
||||
$markdown = preg_replace('/^- (.+)$/m', "— $1", $markdown);
|
||||
|
||||
// Убираем Markdown разметку, но сохраняем переносы строк
|
||||
$text = $markdown;
|
||||
|
||||
// Убираем заголовки
|
||||
$text = preg_replace('/^#+\s+/m', '', $text);
|
||||
|
||||
// Убираем жирный и курсив
|
||||
$text = preg_replace('/\*\*(.*?)\*\*/', '$1', $text);
|
||||
$text = preg_replace('/\*(.*?)\*/', '$1', $text);
|
||||
$text = preg_replace('/__(.*?)__/', '$1', $text);
|
||||
$text = preg_replace('/_(.*?)_/', '$1', $text);
|
||||
|
||||
// Убираем зачеркивание
|
||||
$text = preg_replace('/~~(.*?)~~/', '$1', $text);
|
||||
|
||||
// Убираем код (встроенный)
|
||||
$text = preg_replace('/`(.*?)`/', '$1', $text);
|
||||
|
||||
// Убираем блоки кода (сохраняем содержимое)
|
||||
$text = preg_replace('/```.*?\n(.*?)```/s', '$1', $text);
|
||||
|
||||
// Убираем ссылки
|
||||
$text = preg_replace('/\[(.*?)\]\(.*?\)/', '$1', $text);
|
||||
|
||||
// Обрабатываем списки - заменяем маркеры на *
|
||||
$text = preg_replace('/^[\*\-+]\s+/m', '* ', $text);
|
||||
$text = preg_replace('/^\d+\.\s+/m', '* ', $text);
|
||||
|
||||
// Обрабатываем цитаты
|
||||
$text = preg_replace('/^>\s+/m', '', $text);
|
||||
|
||||
return $text;
|
||||
}
|
||||
// Функция для разбивки Markdown на абзацы с сохранением структуры
|
||||
function markdownToParagraphs($markdown) {
|
||||
// Нормализуем переносы строк
|
||||
$text = str_replace(["\r\n", "\r"], "\n", $markdown);
|
||||
|
||||
// Обрабатываем диалоги (заменяем - на —)
|
||||
$text = preg_replace('/^- (.+)$/m', "— $1", $text);
|
||||
|
||||
// Разбиваем на строки
|
||||
$lines = explode("\n", $text);
|
||||
$paragraphs = [];
|
||||
$currentParagraph = '';
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$trimmedLine = trim($line);
|
||||
|
||||
// Пустая строка - конец абзаца
|
||||
if (empty($trimmedLine)) {
|
||||
if (!empty($currentParagraph)) {
|
||||
$paragraphs[] = $currentParagraph;
|
||||
$currentParagraph = '';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Диалог (начинается с —) всегда начинает новый абзац
|
||||
if (str_starts_with($trimmedLine, '—')) {
|
||||
if (!empty($currentParagraph)) {
|
||||
$paragraphs[] = $currentParagraph;
|
||||
}
|
||||
$currentParagraph = $trimmedLine;
|
||||
$paragraphs[] = $currentParagraph;
|
||||
$currentParagraph = '';
|
||||
continue;
|
||||
}
|
||||
|
||||
// Заголовки (начинаются с #) всегда начинают новый абзац
|
||||
if (str_starts_with($trimmedLine, '#')) {
|
||||
if (!empty($currentParagraph)) {
|
||||
$paragraphs[] = $currentParagraph;
|
||||
}
|
||||
$currentParagraph = preg_replace('/^#+\s+/', '', $trimmedLine);
|
||||
$paragraphs[] = $currentParagraph;
|
||||
$currentParagraph = '';
|
||||
continue;
|
||||
}
|
||||
|
||||
// Обычный текст - добавляем к текущему абзацу
|
||||
if (!empty($currentParagraph)) {
|
||||
$currentParagraph .= ' ' . $trimmedLine;
|
||||
} else {
|
||||
$currentParagraph = $trimmedLine;
|
||||
}
|
||||
}
|
||||
|
||||
// Добавляем последний абзац
|
||||
if (!empty($currentParagraph)) {
|
||||
$paragraphs[] = $currentParagraph;
|
||||
}
|
||||
|
||||
return $paragraphs;
|
||||
}
|
||||
|
||||
// Функция для очистки Markdown разметки
|
||||
function cleanMarkdown($markdown) {
|
||||
$text = $markdown;
|
||||
|
||||
// Убираем заголовки
|
||||
$text = preg_replace('/^#+\s+/m', '', $text);
|
||||
|
||||
// Убираем жирный и курсив
|
||||
$text = preg_replace('/\*\*(.*?)\*\*/', '$1', $text);
|
||||
$text = preg_replace('/\*(.*?)\*/', '$1', $text);
|
||||
$text = preg_replace('/__(.*?)__/', '$1', $text);
|
||||
$text = preg_replace('/_(.*?)_/', '$1', $text);
|
||||
|
||||
// Убираем зачеркивание
|
||||
$text = preg_replace('/~~(.*?)~~/', '$1', $text);
|
||||
|
||||
// Убираем код
|
||||
$text = preg_replace('/`(.*?)`/', '$1', $text);
|
||||
$text = preg_replace('/```.*?\n(.*?)```/s', '$1', $text);
|
||||
|
||||
// Убираем ссылки
|
||||
$text = preg_replace('/\[(.*?)\]\(.*?\)/', '$1', $text);
|
||||
|
||||
// Обрабатываем списки - убираем маркеры
|
||||
$text = preg_replace('/^[\*\-+]\s+/m', '', $text);
|
||||
$text = preg_replace('/^\d+\.\s+/m', '', $text);
|
||||
|
||||
// Обрабатываем цитаты
|
||||
$text = preg_replace('/^>\s+/m', '', $text);
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
// Функция для форматирования текста с сохранением абзацев и диалогов
|
||||
function formatPlainText($text) {
|
||||
$lines = explode("\n", $text);
|
||||
$formatted = [];
|
||||
$in_paragraph = false;
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
|
||||
if (empty($line)) {
|
||||
if ($in_paragraph) {
|
||||
$formatted[] = ''; // Пустая строка для разделения абзацев
|
||||
$in_paragraph = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Диалоги начинаются с —
|
||||
if (str_starts_with($line, '—')) {
|
||||
if ($in_paragraph) {
|
||||
$formatted[] = ''; // Разделяем абзацы перед диалогом
|
||||
}
|
||||
$formatted[] = $line;
|
||||
$formatted[] = ''; // Пустая строка после диалога
|
||||
$in_paragraph = false;
|
||||
} else {
|
||||
// Обычный текст
|
||||
$formatted[] = $line;
|
||||
$in_paragraph = true;
|
||||
}
|
||||
}
|
||||
|
||||
return implode("\n", array_filter($formatted, function($line) {
|
||||
return $line !== '' || !empty($line);
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
// // Новая функция для разбивки HTML на абзацы
|
||||
function htmlToParagraphs($html) {
|
||||
// Убираем HTML теги и нормализуем пробелы
|
||||
$text = strip_tags($html);
|
||||
$text = preg_replace('/\s+/', ' ', $text);
|
||||
|
||||
// Разбиваем на абзацы по точкам и переносам строк
|
||||
$paragraphs = preg_split('/(?<=[.!?])\s+/', $text);
|
||||
|
||||
// Фильтруем пустые абзацы
|
||||
$paragraphs = array_filter($paragraphs, function($paragraph) {
|
||||
return !empty(trim($paragraph));
|
||||
});
|
||||
|
||||
return $paragraphs;
|
||||
}
|
||||
function htmlToPlainTextParagraphs($html) {
|
||||
// Убираем HTML теги
|
||||
$text = strip_tags($html);
|
||||
|
||||
// Заменяем HTML entities
|
||||
$text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
|
||||
// Нормализуем переносы строк
|
||||
$text = str_replace(["\r\n", "\r"], "\n", $text);
|
||||
|
||||
// Разбиваем на строки
|
||||
$lines = explode("\n", $text);
|
||||
$paragraphs = [];
|
||||
$currentParagraph = '';
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$trimmedLine = trim($line);
|
||||
|
||||
// Пустая строка - конец абзаца
|
||||
if (empty($trimmedLine)) {
|
||||
if (!empty($currentParagraph)) {
|
||||
$paragraphs[] = $currentParagraph;
|
||||
$currentParagraph = '';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Добавляем к текущему абзацу
|
||||
if (!empty($currentParagraph)) {
|
||||
$currentParagraph .= ' ' . $trimmedLine;
|
||||
} else {
|
||||
$currentParagraph = $trimmedLine;
|
||||
}
|
||||
}
|
||||
|
||||
// Добавляем последний абзац
|
||||
if (!empty($currentParagraph)) {
|
||||
$paragraphs[] = $currentParagraph;
|
||||
}
|
||||
|
||||
return $paragraphs;
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,194 @@
|
||||
<?php
|
||||
// controllers/SeriesController.php
|
||||
require_once 'controllers/BaseController.php';
|
||||
require_once 'models/Series.php';
|
||||
require_once 'models/Book.php';
|
||||
require_once 'includes/parsedown/ParsedownExtra.php';
|
||||
|
||||
class SeriesController extends BaseController {
|
||||
|
||||
public function index() {
|
||||
$this->requireLogin();
|
||||
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$seriesModel = new Series($this->pdo);
|
||||
$series = $seriesModel->findByUser($user_id);
|
||||
|
||||
// Получаем статистику для каждой серии отдельно
|
||||
foreach ($series as &$ser) {
|
||||
$stats = $seriesModel->getSeriesStats($ser['id'], $user_id);
|
||||
$ser['book_count'] = $stats['book_count'] ?? 0;
|
||||
$ser['total_words'] = $stats['total_words'] ?? 0;
|
||||
}
|
||||
unset($ser);
|
||||
|
||||
$this->render('series/index', [
|
||||
'series' => $series,
|
||||
'page_title' => "Мои серии книг"
|
||||
]);
|
||||
}
|
||||
|
||||
public function create() {
|
||||
$this->requireLogin();
|
||||
|
||||
$error = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
|
||||
$error = "Ошибка безопасности";
|
||||
} else {
|
||||
$title = trim($_POST['title'] ?? '');
|
||||
$description = trim($_POST['description'] ?? '');
|
||||
|
||||
if (empty($title)) {
|
||||
$error = "Название серии обязательно";
|
||||
} else {
|
||||
$seriesModel = new Series($this->pdo);
|
||||
$data = [
|
||||
'title' => $title,
|
||||
'description' => $description,
|
||||
'user_id' => $_SESSION['user_id']
|
||||
];
|
||||
|
||||
if ($seriesModel->create($data)) {
|
||||
$_SESSION['success'] = "Серия успешно создана";
|
||||
$new_series_id = $this->pdo->lastInsertId();
|
||||
$this->redirect("/series/{$new_series_id}/edit");
|
||||
} else {
|
||||
$error = "Ошибка при создании серии";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->render('series/create', [
|
||||
'error' => $error,
|
||||
'page_title' => "Создание новой серии"
|
||||
]);
|
||||
}
|
||||
|
||||
public function edit($id) {
|
||||
$this->requireLogin();
|
||||
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$seriesModel = new Series($this->pdo);
|
||||
$series = $seriesModel->findById($id);
|
||||
|
||||
if (!$series || !$seriesModel->userOwnsSeries($id, $user_id)) {
|
||||
$_SESSION['error'] = "Серия не найдена или у вас нет доступа";
|
||||
$this->redirect('/series');
|
||||
}
|
||||
|
||||
$error = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
|
||||
$error = "Ошибка безопасности";
|
||||
} else {
|
||||
$title = trim($_POST['title'] ?? '');
|
||||
$description = trim($_POST['description'] ?? '');
|
||||
|
||||
if (empty($title)) {
|
||||
$error = "Название серии обязательно";
|
||||
} else {
|
||||
$data = [
|
||||
'title' => $title,
|
||||
'description' => $description,
|
||||
'user_id' => $user_id
|
||||
];
|
||||
|
||||
if ($seriesModel->update($id, $data)) {
|
||||
$_SESSION['success'] = "Серия успешно обновлена";
|
||||
$this->redirect('/series');
|
||||
} else {
|
||||
$error = "Ошибка при обновлении серии";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Получаем книги в серии
|
||||
$bookModel = new Book($this->pdo);
|
||||
$books_in_series = $bookModel->findBySeries($id);
|
||||
|
||||
$this->render('series/edit', [
|
||||
'series' => $series,
|
||||
'books_in_series' => $books_in_series,
|
||||
'error' => $error,
|
||||
'page_title' => "Редактирование серии: " . e($series['title'])
|
||||
]);
|
||||
}
|
||||
|
||||
public function delete($id) {
|
||||
$this->requireLogin();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
$_SESSION['error'] = "Неверный метод запроса";
|
||||
$this->redirect('/series');
|
||||
}
|
||||
|
||||
if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
|
||||
$_SESSION['error'] = "Ошибка безопасности";
|
||||
$this->redirect('/series');
|
||||
}
|
||||
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$seriesModel = new Series($this->pdo);
|
||||
|
||||
if (!$seriesModel->userOwnsSeries($id, $user_id)) {
|
||||
$_SESSION['error'] = "У вас нет доступа к этой серии";
|
||||
$this->redirect('/series');
|
||||
}
|
||||
|
||||
if ($seriesModel->delete($id, $user_id)) {
|
||||
$_SESSION['success'] = "Серия успешно удалена";
|
||||
} else {
|
||||
$_SESSION['error'] = "Ошибка при удалении серии";
|
||||
}
|
||||
|
||||
$this->redirect('/series');
|
||||
}
|
||||
|
||||
public function viewPublic($id) {
|
||||
$seriesModel = new Series($this->pdo);
|
||||
$series = $seriesModel->findById($id);
|
||||
|
||||
if (!$series) {
|
||||
http_response_code(404);
|
||||
$this->render('errors/404');
|
||||
return;
|
||||
}
|
||||
|
||||
// Получаем только опубликованные книги серии
|
||||
$books = $seriesModel->getBooksInSeries($id, true);
|
||||
|
||||
// Получаем информацию об авторе
|
||||
$stmt = $this->pdo->prepare("SELECT id, username, display_name FROM users WHERE id = ?");
|
||||
$stmt->execute([$series['user_id']]);
|
||||
$author = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
// Получаем статистику по опубликованным книгам
|
||||
$bookModel = new Book($this->pdo);
|
||||
$total_words = 0;
|
||||
$total_chapters = 0;
|
||||
|
||||
foreach ($books as $book) {
|
||||
$book_stats = $bookModel->getBookStats($book['id'], true);
|
||||
$total_words += $book_stats['total_words'] ?? 0;
|
||||
$total_chapters += $book_stats['chapter_count'] ?? 0;
|
||||
}
|
||||
|
||||
$Parsedown = new ParsedownExtra();
|
||||
|
||||
$this->render('series/view_public', [
|
||||
'series' => $series,
|
||||
'books' => $books,
|
||||
'author' => $author,
|
||||
'total_words' => $total_words,
|
||||
'total_chapters' => $total_chapters,
|
||||
'Parsedown' => $Parsedown,
|
||||
'page_title' => $series['title'] . ' — серия книг'
|
||||
]);
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
// controllers/UserController.php
|
||||
require_once 'controllers/BaseController.php';
|
||||
require_once 'models/User.php';
|
||||
require_once 'models/Book.php';
|
||||
require_once 'includes/parsedown/ParsedownExtra.php';
|
||||
|
||||
class UserController extends BaseController {
|
||||
|
||||
public function profile() {
|
||||
$this->requireLogin();
|
||||
|
||||
$user_id = $_SESSION['user_id'];
|
||||
$userModel = new User($this->pdo);
|
||||
$user = $userModel->findById($user_id);
|
||||
|
||||
$message = '';
|
||||
$avatar_error = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
|
||||
$message = "Ошибка безопасности";
|
||||
} else {
|
||||
$display_name = trim($_POST['display_name'] ?? '');
|
||||
$email = trim($_POST['email'] ?? '');
|
||||
$bio = trim($_POST['bio'] ?? '');
|
||||
|
||||
// Обработка загрузки аватарки
|
||||
if (isset($_FILES['avatar']) && $_FILES['avatar']['error'] === UPLOAD_ERR_OK) {
|
||||
$avatar_result = handleAvatarUpload($_FILES['avatar'], $user_id);
|
||||
if ($avatar_result['success']) {
|
||||
$userModel->updateAvatar($user_id, $avatar_result['filename']);
|
||||
// Обновляем данные пользователя
|
||||
$user = $userModel->findById($user_id);
|
||||
} else {
|
||||
$avatar_error = $avatar_result['error'];
|
||||
}
|
||||
}
|
||||
|
||||
// Обработка удаления аватарки
|
||||
if (isset($_POST['delete_avatar']) && $_POST['delete_avatar'] == '1') {
|
||||
deleteUserAvatar($user_id);
|
||||
$user = $userModel->findById($user_id);
|
||||
}
|
||||
|
||||
// Обновляем основные данные
|
||||
$data = [
|
||||
'display_name' => $display_name,
|
||||
'email' => $email,
|
||||
'bio' => $bio
|
||||
];
|
||||
|
||||
if ($userModel->updateProfile($user_id, $data)) {
|
||||
$_SESSION['display_name'] = $display_name ?: $user['username'];
|
||||
$message = "Профиль обновлен";
|
||||
// Обновляем данные пользователя
|
||||
$user = $userModel->findById($user_id);
|
||||
} else {
|
||||
$message = "Ошибка при обновлении профиля";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->render('user/profile', [
|
||||
'user' => $user,
|
||||
'message' => $message,
|
||||
'avatar_error' => $avatar_error,
|
||||
'page_title' => "Мой профиль"
|
||||
]);
|
||||
}
|
||||
|
||||
public function updateProfile() {
|
||||
$this->requireLogin();
|
||||
|
||||
// Эта функция обрабатывает AJAX или прямые POST запросы для обновления профиля
|
||||
// Можно объединить с методом profile() или оставить отдельно для API-like операций
|
||||
$this->profile(); // Перенаправляем на основной метод
|
||||
}
|
||||
|
||||
public function viewPublic($id) {
|
||||
$userModel = new User($this->pdo);
|
||||
$user = $userModel->findById($id);
|
||||
|
||||
if (!$user) {
|
||||
http_response_code(404);
|
||||
$this->render('errors/404');
|
||||
return;
|
||||
}
|
||||
|
||||
$bookModel = new Book($this->pdo);
|
||||
$books = $bookModel->findByUser($id, true); // только опубликованные
|
||||
|
||||
// Получаем статистику автора
|
||||
$total_books = count($books);
|
||||
$total_words = 0;
|
||||
$total_chapters = 0;
|
||||
|
||||
foreach ($books as $book) {
|
||||
$book_stats = $bookModel->getBookStats($book['id'], true);
|
||||
$total_words += $book_stats['total_words'] ?? 0;
|
||||
$total_chapters += $book_stats['chapter_count'] ?? 0;
|
||||
}
|
||||
|
||||
$Parsedown = new ParsedownExtra();
|
||||
|
||||
$this->render('user/view_public', [
|
||||
'user' => $user,
|
||||
'books' => $books,
|
||||
'total_books' => $total_books,
|
||||
'total_words' => $total_words,
|
||||
'total_chapters' => $total_chapters,
|
||||
'Parsedown' => $Parsedown,
|
||||
'page_title' => ($user['display_name'] ?: $user['username']) . ' — публичная страница'
|
||||
]);
|
||||
}
|
||||
}
|
||||
?>
|
||||
Reference in New Issue
Block a user