diff --git a/admin/index.php b/admin/index.php
deleted file mode 100755
index e70c8ba..0000000
--- a/admin/index.php
+++ /dev/null
@@ -1,2 +0,0 @@
-findAll();
-
-// Обработка действий
-if ($_SERVER['REQUEST_METHOD'] === 'POST') {
- if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
- $_SESSION['error'] = "Ошибка безопасности";
- } else {
- $action = $_POST['action'] ?? '';
- $user_id = $_POST['user_id'] ?? null;
-
- if ($user_id && $user_id != $_SESSION['user_id']) { // Нельзя изменять себя
- switch ($action) {
- case 'toggle_active':
- $user = $userModel->findById($user_id);
- if ($user) {
- $new_status = $user['is_active'] ? 0 : 1;
- if ($userModel->updateStatus($user_id, $new_status)) {
- $_SESSION['success'] = 'Статус пользователя обновлен';
- } else {
- $_SESSION['error'] = 'Ошибка при обновлении статуса';
- }
- }
- break;
-
- case 'delete':
- if ($userModel->delete($user_id)) {
- $_SESSION['success'] = 'Пользователь удален';
- } else {
- $_SESSION['error'] = 'Ошибка при удалении пользователя';
- }
- break;
- }
- } else {
- $_SESSION['error'] = 'Нельзя изменить собственный аккаунт';
- }
-
- redirect('users.php');
- }
-}
-
-$page_title = "Управление пользователями";
-include '../views/header.php';
-?>
-
-
-
Управление пользователями
-
-
-
- = e($_SESSION['success']) ?>
-
-
-
-
-
-
- = e($_SESSION['error']) ?>
-
-
-
-
-
-
-
-
- Пользователи не найдены
- Зарегистрируйте первого пользователя
- 📝 Добавить пользователя
-
-
-
-
-
-
- ID
- Имя пользователя
- Отображаемое имя
- Email
- Дата регистрации
- Статус
- Действия
-
-
-
-
-
- = $user['id'] ?>
-
- = e($user['username']) ?>
-
- (Вы)
-
-
- = e($user['display_name']) ?>
- = e($user['email']) ?>
-
- = date('d.m.Y H:i', strtotime($user['created_at'])) ?>
-
- Вход: = date('d.m.Y H:i', strtotime($user['last_login'])) ?>
-
-
-
-
- = $user['is_active'] ? '✅ Активен' : '❌ Неактивен' ?>
-
-
-
-
-
-
-
-
-
- Текущий пользователь
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/book_delete.php b/book_delete.php
deleted file mode 100755
index 0e7243a..0000000
--- a/book_delete.php
+++ /dev/null
@@ -1,47 +0,0 @@
-userOwnsBook($book_id, $user_id)) {
- $_SESSION['error'] = "У вас нет доступа к этой книге";
- redirect('books.php');
-}
-
-// Получаем информацию о книге перед удалением
-$book = $bookModel->findById($book_id);
-if (!empty($book['cover_image'])) {
- $cover_path = COVERS_PATH . $book['cover_image'];
- if (file_exists($cover_path)) {
- unlink($cover_path);
- }
-}
-// Удаляем книгу
-if ($bookModel->delete($book_id, $user_id)) {
- $_SESSION['success'] = "Книга «" . e($book['title']) . "» успешно удалена";
-} else {
- $_SESSION['error'] = "Ошибка при удалении книги";
-}
-
-redirect('books.php');
-?>
\ No newline at end of file
diff --git a/book_delete_all.php b/book_delete_all.php
deleted file mode 100755
index 6e9f5af..0000000
--- a/book_delete_all.php
+++ /dev/null
@@ -1,45 +0,0 @@
-findByUser($user_id);
-
-if (empty($books)) {
- $_SESSION['error'] = "У вас нет книг для удаления";
- redirect('books.php');
-}
-
-$deleted_count = 0;
-$error_count = 0;
-
-// Удаляем каждую книгу
-foreach ($books as $book) {
- if ($bookModel->delete($book['id'], $user_id)) {
- $deleted_count++;
- } else {
- $error_count++;
- }
-}
-
-if ($error_count === 0) {
- $_SESSION['success'] = "Все книги успешно удалены ($deleted_count книг)";
-} else {
- $_SESSION['error'] = "Удалено $deleted_count книг, не удалось удалить $error_count книг";
-}
-
-redirect('books.php');
-?>
\ No newline at end of file
diff --git a/book_edit.php b/book_edit.php
deleted file mode 100755
index f06cc70..0000000
--- a/book_edit.php
+++ /dev/null
@@ -1,413 +0,0 @@
-findById($book_id);
- if (!$book || $book['user_id'] != $user_id) {
- $_SESSION['error'] = "Книга не найдена или у вас нет доступа";
- redirect('books.php');
- }
- $is_edit = true;
-}
-
-// Обработка формы
-$cover_error = '';
-if ($_SERVER['REQUEST_METHOD'] === 'POST') {
- if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
- $_SESSION['error'] = "Ошибка безопасности";
- redirect($is_edit ? "book_edit.php?id=$book_id" : 'book_edit.php');
- }
-
- $title = trim($_POST['title'] ?? '');
- $description = trim($_POST['description'] ?? '');
- $genre = trim($_POST['genre'] ?? '');
- $editor_type = $_POST['editor_type'] ?? 'markdown';
-
- if (empty($title)) {
- $_SESSION['error'] = "Название книги обязательно";
- } else {
- $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;
-
- if ($series_id && !$sort_order_in_series) {
- $seriesModel = new Series($pdo);
- $sort_order_in_series = $seriesModel->getNextSortOrder($series_id);
- }
-
- $data = [
- 'title' => $title,
- 'description' => $description,
- 'genre' => $genre,
- 'user_id' => $user_id,
- 'series_id' => $series_id,
- 'sort_order_in_series' => $sort_order_in_series,
- 'editor_type' => $editor_type
- ];
- $data['published'] = isset($_POST['published']) ? 1 : 0;
-
- // Проверяем, изменился ли тип редактора
- $editor_changed = false;
- $old_editor_type = null;
-
- if ($is_edit && $book['editor_type'] !== $editor_type) {
- $editor_changed = true;
- $old_editor_type = $book['editor_type'];
- }
- // Обработка загрузки обложки
- if (isset($_FILES['cover_image']) && $_FILES['cover_image']['error'] === UPLOAD_ERR_OK) {
- $cover_result = handleCoverUpload($_FILES['cover_image'], $book_id);
- if ($cover_result['success']) {
- $bookModel->updateCover($book_id, $cover_result['filename']);
- // Обновляем данные книги
- $book = $bookModel->findById($book_id);
- } else {
- $cover_error = $cover_result['error'];
- }
- }
-
- // Обработка удаления обложки
- if (isset($_POST['delete_cover']) && $_POST['delete_cover'] == '1') {
- $bookModel->deleteCover($book_id);
- $book = $bookModel->findById($book_id);
- }
-
- if ($is_edit) {
- $success = $bookModel->update($book_id, $data);
-
- // Конвертируем контент глав, если изменился редактор
- if ($success && $editor_changed) {
- $conversion_success = $bookModel->convertChaptersContent($book_id, $old_editor_type, $editor_type);
- if (!$conversion_success) {
- $_SESSION['warning'] = "Книга обновлена, но возникли ошибки при конвертации содержания глав";
- } else {
- $_SESSION['info'] = "Книга обновлена. Содержание глав сконвертировано в новый формат редактора.";
- }
- }
-
- $message = $success ? "Книга успешно обновлена" : "Ошибка при обновлении книги";
- } else {
- $success = $bookModel->create($data);
- $message = $success ? "Книга успешно создана" : "Ошибка при создании книги";
-
- if ($success) {
- $new_book_id = $pdo->lastInsertId();
- redirect("book_edit.php?id=$new_book_id");
- }
- }
-
- if ($success) {
- $_SESSION['success'] = $message;
- redirect('books.php');
- } else {
- $_SESSION['error'] = $message;
- }
- }
-}
-
-$page_title = $is_edit ? "Редактирование книги" : "Создание новой книги";
-include 'views/header.php';
-?>
-
-
-
-
-
-
- 🔄 Нормализовать контент глав
-
- Если контент глав отображается неправильно после смены редактора, можно нормализовать его структуру.
-
-
-
-
-
-
-
-
-
- 🗑️ Удалить главу
-
-
-
-
-
-
-
Публичная ссылка для чтения
-
-
-
-
- 📋 Копировать
-
-
-
-
-
-
- 🔄 Обновить
-
-
-
-
- Примечание: В публичном просмотре отображаются только главы со статусом "Опубликована"
-
-
-
-
-
-
-
-
-
-
-
-
Экспорт книги
-
Экспортируйте книгу в различные форматы:
-
-
-
-
- Примечание: Экспортируются все главы книги (включая черновики)
-
-
-
-
-
-
-
Главы этой книги
-
- 📑 Все главы
-
-
-
- ✏️ Добавить главу
-
- prepare("SELECT * FROM chapters WHERE book_id = ? ORDER BY sort_order, created_at");
- $stmt->execute([$book_id]);
- $chapters = $stmt->fetchAll();
-
- if ($chapters): ?>
-
-
-
-
- Название
- Статус
- Слов
- Действия
-
-
-
-
-
- = e($chapter['title']) ?>
-
-
- = $chapter['status'] == 'published' ? 'Опубликована' : 'Черновик' ?>
-
-
- = $chapter['word_count'] ?>
-
-
- Редактировать
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/book_regenerate_token.php b/book_regenerate_token.php
deleted file mode 100755
index 6d84783..0000000
--- a/book_regenerate_token.php
+++ /dev/null
@@ -1,41 +0,0 @@
-userOwnsBook($book_id, $user_id)) {
- $_SESSION['error'] = "У вас нет доступа к этой книге";
- redirect('books.php');
-}
-
-// Генерируем новый токен
-$new_token = $bookModel->generateNewShareToken($book_id);
-
-if ($new_token) {
- $_SESSION['success'] = "Публичная ссылка обновлена";
-} else {
- $_SESSION['error'] = "Ошибка при обновлении ссылки";
-}
-
-redirect("book_edit.php?id=$book_id");
-?>
\ No newline at end of file
diff --git a/books.php b/books.php
deleted file mode 100755
index 64bff7a..0000000
--- a/books.php
+++ /dev/null
@@ -1,156 +0,0 @@
-findByUser($user_id);
-
-$page_title = "Мои книги";
-include 'views/header.php';
-?>
-
-Мои книги
-
-
-
- = e($_SESSION['success']) ?>
-
-
-
-
-
-
- = e($_SESSION['error']) ?>
-
-
-
-
-
-
Всего книг: = count($books) ?>
-
-
-
-
-
- У вас пока нет книг
- Создайте свою первую книгу и начните писать!
- 📖 Создать первую книгу
-
-
-
-
-
-
-
-
-
-
-
-
-
- = e(mb_strimwidth($book['description'], 0, 150, '...')) ?>
-
-
-
-
-
-
-
-
- Удалить все книги?
- Это действие удалит все ваши книги и все связанные с ними главы. Это действие нельзя отменить.
-
-
-
-
- ❌ Отмена
-
-
- 🗑️ Удалить все
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/chapter_delete.php b/chapter_delete.php
deleted file mode 100755
index 5073cca..0000000
--- a/chapter_delete.php
+++ /dev/null
@@ -1,43 +0,0 @@
-userOwnsChapter($chapter_id, $user_id)) {
- $_SESSION['error'] = "У вас нет доступа к этой главе";
- redirect('books.php');
-}
-
-
-$chapter = $chapterModel->findById($chapter_id);
-$book_id = $chapter['book_id'];
-
-// Удаляем главу
-if ($chapterModel->delete($chapter_id)) {
- $_SESSION['success'] = "Глава успешно удалена";
-} else {
- $_SESSION['error'] = "Ошибка при удалении главы";
-}
-
-redirect("chapters.php?book_id=$book_id");
-?>
\ No newline at end of file
diff --git a/chapter_edit.php b/chapter_edit.php
deleted file mode 100755
index 22a54da..0000000
--- a/chapter_edit.php
+++ /dev/null
@@ -1,386 +0,0 @@
-findById($chapter_id);
- if (!$chapter || $chapter['user_id'] != $user_id) {
- $_SESSION['error'] = "Глава не найдена или у вас нет доступа";
- redirect('books.php');
- }
- $book_id = $chapter['book_id'];
- $is_edit = true;
-}
-
-if (!$book_id) {
- $_SESSION['error'] = "Не указана книга";
- redirect('books.php');
-}
-
-if (!$bookModel->userOwnsBook($book_id, $user_id)) {
- $_SESSION['error'] = "У вас нет доступа к этой книге";
- redirect('books.php');
-}
-
-// Получаем информацию о книге
-$book = $bookModel->findById($book_id);
-
-// Обработка формы
-if ($_SERVER['REQUEST_METHOD'] === 'POST') {
- if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
- $_SESSION['error'] = "Ошибка безопасности";
- redirect($is_edit ? "chapter_edit.php?id=$chapter_id" : "chapter_edit.php?book_id=$book_id");
- }
-
- // Обработка автосохранения
- if (isset($_POST['autosave']) && $_POST['autosave'] === 'true') {
- // Автосохранение работает только для существующих глав
- // Если это не редактирование, игнорируем автосохранение
- if (!$is_edit) {
-
- header('Content-Type: application/json');
- echo json_encode(['success' => false, 'message' => 'Автосохранение недоступно для новых глав']);
- exit;
- }
-
- $title = trim($_POST['title'] ?? '');
- $content = trim($_POST['content'] ?? '');
- $status = $_POST['status'] ?? 'draft';
-
- if (empty($title)) {
- header('Content-Type: application/json');
- echo json_encode(['success' => false, 'message' => 'Название главы обязательно']);
- exit;
- }
-
- $data = [
- 'title' => $title,
- 'content' => $content,
- 'status' => $status,
- 'book_id' => $book_id
- ];
-
- $success = $chapterModel->update($chapter_id, $data);
-
- header('Content-Type: application/json');
- echo json_encode(['success' => $success]);
- exit;
- }
-
- // Обычная обработка формы (не автосохранение)
- $title = trim($_POST['title'] ?? '');
- $content = trim($_POST['content'] ?? '');
- $status = $_POST['status'] ?? 'draft';
-
- if (empty($title)) {
- $_SESSION['error'] = "Название главы обязательно";
- } else {
- $data = [
- 'title' => $title,
- 'content' => $content,
- 'status' => $status,
- 'book_id' => $book_id
- ];
-
- if ($is_edit) {
- $success = $chapterModel->update($chapter_id, $data);
- $message = $success ? "Глава успешно обновлена" : "Ошибка при обновлении главы";
- } else {
- $success = $chapterModel->create($data);
- $message = $success ? "Глава успешно создана" : "Ошибка при создании главы";
-
- if ($success) {
- $new_chapter_id = $pdo->lastInsertId();
- redirect("chapter_edit.php?id=$new_chapter_id");
- }
- }
-
- if ($success) {
- $_SESSION['success'] = $message;
- redirect("book_edit.php?id=$book_id");
- } else {
- $_SESSION['error'] = $message;
- }
- }
-}
-
-$page_title = $is_edit ? "Редактирование главы" : "Создание новой главы";
-include 'views/header.php';
-?>
-
-
-
-
-= $is_edit ? "Редактирование главы" : "Создание новой главы" ?>
-Книга: = e($book['title']) ?>
-
-
-
- = e($_SESSION['error']) ?>
-
-
-
-
-
-
-
-
-
- Название главы *
-
-
-
-
- Статус
-
-
- >Черновик
- >Опубликована
-
-
-
- Содержание главы
-
-
- (Режим: = $book['editor_type'] == 'markdown' ? 'Markdown' : 'HTML' ?>)
-
-
-
-
-
-
-
- = e($chapter['content'] ?? $_POST['content'] ?? '') ?>
-
-
-
-
-
-
-
- = e($chapter['content'] ?? $_POST['content'] ?? '') ?>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/config/config.php b/config/config.php
new file mode 100644
index 0000000..35e767f
--- /dev/null
+++ b/config/config.php
@@ -0,0 +1,51 @@
+setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+} catch(PDOException $e) {
+ error_log("DB Error: " . $e->getMessage());
+ die("Ошибка подключения к базе данных");
+}
+
+// Добавляем константы для новых путей
+define('CONTROLLERS_PATH', __DIR__ . '/../controllers/');
+define('VIEWS_PATH', __DIR__ . '/../views/');
+define('LAYOUTS_PATH', VIEWS_PATH . 'layouts/');
+
+// Автозагрузка контроллеров
+spl_autoload_register(function ($class_name) {
+ $controller_file = CONTROLLERS_PATH . $class_name . '.php';
+ if (file_exists($controller_file)) {
+ require_once $controller_file;
+ }
+});
+?>
\ No newline at end of file
diff --git a/controllers/AuthController.php b/controllers/AuthController.php
new file mode 100644
index 0000000..2f6bdb9
--- /dev/null
+++ b/controllers/AuthController.php
@@ -0,0 +1,137 @@
+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' => 'Регистрация'
+ ]);
+ }
+}
+?>
\ No newline at end of file
diff --git a/controllers/BaseController.php b/controllers/BaseController.php
new file mode 100644
index 0000000..572b033
--- /dev/null
+++ b/controllers/BaseController.php
@@ -0,0 +1,33 @@
+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;
+ }
+}
+?>
\ No newline at end of file
diff --git a/controllers/BookController.php b/controllers/BookController.php
new file mode 100644
index 0000000..f4377fe
--- /dev/null
+++ b/controllers/BookController.php
@@ -0,0 +1,268 @@
+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");
+ }
+}
+?>
\ No newline at end of file
diff --git a/controllers/ChapterController.php b/controllers/ChapterController.php
new file mode 100644
index 0000000..bc57f2f
--- /dev/null
+++ b/controllers/ChapterController.php
@@ -0,0 +1,199 @@
+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)
+ ]);
+ }
+}
+?>
\ No newline at end of file
diff --git a/controllers/DashboardController.php b/controllers/DashboardController.php
new file mode 100644
index 0000000..b738410
--- /dev/null
+++ b/controllers/DashboardController.php
@@ -0,0 +1,52 @@
+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' => 'Панель управления'
+ ]);
+ }
+}
+?>
\ No newline at end of file
diff --git a/controllers/ExportController.php b/controllers/ExportController.php
new file mode 100644
index 0000000..da07a15
--- /dev/null
+++ b/controllers/ExportController.php
@@ -0,0 +1,883 @@
+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 = '
+
+
+
+
+ ' . htmlspecialchars($book['title']) . '
+
+
+
+ ' . htmlspecialchars($book['title']) . '
+ ' . htmlspecialchars($author_name) . '
';
+
+ if (!empty($book['genre'])) {
+ $html .= 'Жанр: ' . htmlspecialchars($book['genre']) . '
';
+ }
+
+ // Обложка книги
+ if (!empty($book['cover_image'])) {
+ $cover_url = COVERS_URL . $book['cover_image'];
+ $html .= '';
+ $html .= '
';
+ $html .= '
';
+ }
+
+ if (!empty($book['description'])) {
+ $html .= '';
+ if ($book['editor_type'] == 'markdown') {
+ $html .= nl2br(htmlspecialchars($book['description']));
+ } else {
+ $html .= $book['description'];
+ }
+ $html .= '
';
+ }
+
+ // Интерактивное оглавление
+ if (!empty($chapters)) {
+ $html .= '';
+ $html .= '
Оглавление ';
+ $html .= '
';
+ $html .= '
';
+ }
+
+ $html .= ' ';
+
+ foreach ($chapters as $index => $chapter) {
+ $html .= '';
+ $html .= '
' . htmlspecialchars($chapter['title']) . '
';
+
+ // Обрабатываем контент в зависимости от типа редактора
+ if ($book['editor_type'] == 'markdown') {
+ $htmlContent = $Parsedown->text($chapter['content']);
+ } else {
+ $htmlContent = $chapter['content'];
+ }
+
+ $html .= '
' . $htmlContent . '
';
+ $html .= '
';
+
+ if ($index < count($chapters) - 1) {
+ $html .= ' ';
+ }
+ }
+
+ $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;
+ }
+}
+?>
\ No newline at end of file
diff --git a/controllers/SeriesController.php b/controllers/SeriesController.php
new file mode 100644
index 0000000..2bc3e16
--- /dev/null
+++ b/controllers/SeriesController.php
@@ -0,0 +1,194 @@
+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'] . ' — серия книг'
+ ]);
+ }
+}
+?>
\ No newline at end of file
diff --git a/controllers/UserController.php b/controllers/UserController.php
new file mode 100644
index 0000000..6306ce4
--- /dev/null
+++ b/controllers/UserController.php
@@ -0,0 +1,117 @@
+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']) . ' — публичная страница'
+ ]);
+ }
+}
+?>
\ No newline at end of file
diff --git a/dashboard.php b/dashboard.php
deleted file mode 100755
index 841a62b..0000000
--- a/dashboard.php
+++ /dev/null
@@ -1,191 +0,0 @@
-findByUser($user_id);
-$series = $seriesModel->findByUser($user_id);
-
-// Статистика по книгам
-$total_chapters = 0;
-$total_words = 0;
-foreach ($books as $book) {
- $total_chapters += $book['chapter_count'];
- $total_words += $book['total_words'];
-}
-
-// Статистика по сериям
-$series_stats = [
- 'total_series' => count($series),
- 'series_with_books' => 0,
- 'total_books_in_series' => 0
-];
-
-foreach ($series as $ser) {
- $series_books = $seriesModel->getBooksInSeries($ser['id']);
- $series_stats['total_books_in_series'] += count($series_books);
- if (count($series_books) > 0) {
- $series_stats['series_with_books']++;
- }
-}
-
-$page_title = "Панель управления";
-include 'views/header.php';
-?>
-
-Добро пожаловать, = e($_SESSION['display_name']) ?>!
-
-
-
-
-
- 📚 Мои книги
- Управляйте вашими книгами и главами
-
-
-
-
- 📊 Статистика
-
-
Книг: = count($books) ?>
-
Глав: = $total_chapters ?>
-
Всего слов: = $total_words ?>
- 0): ?>
-
Средняя глава: = round($total_words / max(1, $total_chapters)) ?> слов
-
-
-
-
-
- 📖 Мои серии
- Управляйте сериями книг
-
-
- 0): ?>
-
-
Книг в сериях: = $series_stats['total_books_in_series'] ?>
-
Заполненных серий: = $series_stats['series_with_books'] ?>
-
-
-
-
-
-
-
-
Недавние книги
-
-
-
-
- = e($book['title']) ?>
-
- prepare("SELECT title FROM series WHERE id = ?");
- $series_stmt->execute([$book['series_id']]);
- $series_title = $series_stmt->fetch()['title'] ?? '';
- ?>
-
- 📚 = e($series_title) ?>
-
-
-
- Глав: = $book['chapter_count'] ?> | Слов: = $book['total_words'] ?>
-
-
-
-
-
- 3): ?>
-
-
-
-
-
-
-
-
Недавние серии
-
-
-
- = e($ser['title']) ?>
-
- getBooksInSeries($ser['id']);
- $series_words = 0;
- $series_chapters = 0;
-
- foreach ($books_in_series as $book) {
- $book_stats = $bookModel->getBookStats($book['id']);
- $series_words += $book_stats['total_words'] ?? 0;
- $series_chapters += $book_stats['chapter_count'] ?? 0;
- }
- ?>
-
- Книг: = count($books_in_series) ?> | Глав: = $series_chapters ?> | Слов: = $series_words ?>
-
-
-
-
-
-
- 3): ?>
-
-
-
-
-
-
-
-
Добро пожаловать в = e(APP_NAME) ?>!
-
Начните создавать свои литературные произведения
-
-
-
-
-
-
\ No newline at end of file
diff --git a/export_book.php b/export_book.php
deleted file mode 100755
index 00fcc5b..0000000
--- a/export_book.php
+++ /dev/null
@@ -1,869 +0,0 @@
-findByShareToken($share_token);
- // Для публичного доступа - только опубликованные главы
- $chapters = $bookModel->getPublishedChapters($book['id']);
- $is_public = true;
-} elseif ($book_id && $user_id) {
- $book = $bookModel->findById($book_id);
- if (!$book || $book['user_id'] != $user_id) {
- $_SESSION['error'] = "Доступ запрещен";
- redirect('books.php');
- }
- // Для автора - все главы
- $chapters = $chapterModel->findByBook($book_id);
- $is_public = false;
-} else {
- $_SESSION['error'] = "Книга не найдена";
- redirect('books.php');
-}
-
-if (!$book) {
- $_SESSION['error'] = "Книга не найдена";
- redirect('books.php');
-}
-// Получаем информацию об авторе
-$author_info = "Неизвестный автор";
-if ($book) {
- $stmt = $pdo->prepare("SELECT display_name, username FROM users WHERE id = ?");
- $stmt->execute([$book['user_id']]);
- $author_info = $stmt->fetch(PDO::FETCH_ASSOC);
- if ($author_info['display_name'] !=""){
- $author_name = $author_info['display_name'];
- }else{
- $author_name = $author_info['username'] ;
- }
-
-}
-
-
-// Функция для преобразования 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);
- }));
-}
-
-// Обработка экспорта
-switch ($format) {
- case 'pdf':
- exportPDF($book, $chapters, $is_public, $author_name);
- break;
- case 'docx':
- exportDOCX($book, $chapters, $is_public, $author_name);
- break;
- case 'html':
- exportHTML($book, $chapters, $is_public, $author_name);
- break;
- case 'txt':
- exportTXT($book, $chapters, $is_public, $author_name);
- break;
- default:
- $_SESSION['error'] = "Неверный формат экспорта";
- redirect($share_token ? "view_book.php?share_token=$share_token" : "book_edit.php?id=$book_id");
-}
-
-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 = markdownToParagraphs($book['description']);
- } else {
- $descriptionParagraphs = 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 = cleanMarkdown($chapter['content']);
- $paragraphs = markdownToParagraphs($cleanContent);
- } else {
- $cleanContent = strip_tags($chapter['content']);
- $paragraphs = 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;
-}
-
-// Новая функция для разбивки 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 exportHTML($book, $chapters, $is_public, $author_name) {
- global $Parsedown;
-
- $html = '
-
-
-
-
- ' . htmlspecialchars($book['title']) . '
-
-
-
- ' . htmlspecialchars($book['title']) . '
- ' . htmlspecialchars($author_name) . '
';
-
- if (!empty($book['genre'])) {
- $html .= 'Жанр: ' . htmlspecialchars($book['genre']) . '
';
- }
-
- // Обложка книги
- if (!empty($book['cover_image'])) {
- $cover_url = COVERS_URL . $book['cover_image'];
- $html .= '';
- $html .= '
';
- $html .= '
';
- }
-
- if (!empty($book['description'])) {
- $html .= '';
- if ($book['editor_type'] == 'markdown') {
- $html .= nl2br(htmlspecialchars($book['description']));
- } else {
- $html .= $book['description'];
- }
- $html .= '
';
- }
-
- // Интерактивное оглавление
- if (!empty($chapters)) {
- $html .= '';
- $html .= '
Оглавление ';
- $html .= '
';
- $html .= '
';
- }
-
- $html .= ' ';
-
- foreach ($chapters as $index => $chapter) {
- $html .= '';
- $html .= '
' . htmlspecialchars($chapter['title']) . '
';
-
- // Обрабатываем контент в зависимости от типа редактора
- if ($book['editor_type'] == 'markdown') {
- $htmlContent = $Parsedown->text($chapter['content']);
- } else {
- $htmlContent = $chapter['content'];
- }
-
- $html .= '
' . $htmlContent . '
';
- $html .= '
';
-
- if ($index < count($chapters) - 1) {
- $html .= ' ';
- }
- }
-
- $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 = 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 = cleanMarkdown($chapter['content']);
- $paragraphs = markdownToParagraphs($cleanContent);
- } else {
- $cleanContent = strip_tags($chapter['content']);
- $paragraphs = 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;
-}
-
-// Новая функция для разбивки HTML на абзацы в виде простого текста
-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;
-}
-?>
\ No newline at end of file
diff --git a/index.php b/index.php
index 17c4b3b..b8a4886 100755
--- a/index.php
+++ b/index.php
@@ -1,9 +1,126 @@
routes[$pattern] = $handler;
+ }
+
+ public function handle($uri) {
+ // Убираем базовый URL если есть
+ $basePath = parse_url(SITE_URL, PHP_URL_PATH) ?? '';
+ $uri = str_replace($basePath, '', $uri);
+ $uri = parse_url($uri, PHP_URL_PATH) ?? '/';
+
+ foreach ($this->routes as $pattern => $handler) {
+ if ($this->match($pattern, $uri)) {
+ return $this->callHandler($handler, $this->params);
+ }
+ }
+
+ // 404
+ http_response_code(404);
+ include 'views/errors/404.php';
+ exit;
+ }
+
+ private function match($pattern, $uri) {
+ $pattern = preg_replace('/\{(\w+)\}/', '(?P<$1>[^/]+)', $pattern);
+ $pattern = "#^$pattern$#";
+
+ if (preg_match($pattern, $uri, $matches)) {
+ $this->params = array_filter($matches, 'is_string', ARRAY_FILTER_USE_KEY);
+ return true;
+ }
+ return false;
+ }
+
+ private function callHandler($handler, $params) {
+ if (is_callable($handler)) {
+ return call_user_func_array($handler, $params);
+ }
+
+ if (is_string($handler)) {
+ list($controller, $method) = explode('@', $handler);
+ $controllerFile = "controllers/{$controller}.php";
+
+ if (file_exists($controllerFile)) {
+ require_once $controllerFile;
+ $controllerInstance = new $controller();
+
+ if (method_exists($controllerInstance, $method)) {
+ return call_user_func_array([$controllerInstance, $method], $params);
+ }
+ }
+ }
+
+ throw new Exception("Handler not found");
+ }
}
+
+// Инициализация роутера
+$router = new Router();
+
+// Маршруты
+$router->add('/', 'DashboardController@index');
+$router->add('/login', 'AuthController@login');
+$router->add('/logout', 'AuthController@logout');
+$router->add('/register', 'AuthController@register');
+
+// Книги
+$router->add('/books', 'BookController@index');
+$router->add('/books/create', 'BookController@create');
+$router->add('/books/{id}/edit', 'BookController@edit');
+$router->add('/books/{id}/delete', 'BookController@delete');
+$router->add('/books/{id}/normalize', 'BookController@normalizeContent');
+$router->add('/books/{id}/regenerate-token', 'BookController@regenerateToken');
+
+// Главы
+$router->add('/books/{book_id}/chapters', 'ChapterController@index');
+$router->add('/books/{book_id}/chapters/create', 'ChapterController@create');
+$router->add('/chapters/{id}/edit', 'ChapterController@edit');
+$router->add('/chapters/{id}/delete', 'ChapterController@delete');
+$router->add('/chapters/preview', 'ChapterController@preview');
+
+// Серии
+$router->add('/series', 'SeriesController@index');
+$router->add('/series/create', 'SeriesController@create');
+$router->add('/series/{id}/edit', 'SeriesController@edit');
+$router->add('/series/{id}/delete', 'SeriesController@delete');
+
+
+// Профиль
+$router->add('/profile', 'UserController@profile');
+$router->add('/profile/update', 'UserController@updateProfile');
+
+// Экспорт с параметром формата
+$router->add('/export/{book_id}/{format}', 'ExportController@export');
+$router->add('/export/{book_id}', 'ExportController@export'); // по умолчанию pdf
+$router->add('/export/shared/{share_token}/{format}', 'ExportController@exportShared');
+$router->add('/export/shared/{share_token}', 'ExportController@exportShared'); // по умолчанию pdf
+
+// Публичные страницы
+$router->add('/book/{share_token}', 'BookController@viewPublic');
+$router->add('/author/{id}', 'UserController@viewPublic');
+$router->add('/series/{id}/view', 'SeriesController@viewPublic');
+
+// Обработка запроса
+$requestUri = $_SERVER['REQUEST_URI'];
+$router->handle($requestUri);
+
+// Редирект с корня на dashboard для авторизованных
+$router->add('/', function() {
+ if (is_logged_in()) {
+ header("Location: " . SITE_URL . "/dashboard");
+ } else {
+ header("Location: " . SITE_URL . "/login");
+ }
+ exit;
+});
+
+
?>
\ No newline at end of file
diff --git a/login.php b/login.php
deleted file mode 100755
index 8f2847e..0000000
--- a/login.php
+++ /dev/null
@@ -1,100 +0,0 @@
-findByUsername($username);
-
- if ($user && $userModel->verifyPassword($password, $user['password_hash'])) {
- if (!$user['is_active']) {
- $error = 'Ваш аккаунт деактивирован или ожидает активации администратором.';
- } else {
- // Успешный вход
- $_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']) . '!';
- redirect('dashboard.php');
- }
- } else {
- $error = 'Неверное имя пользователя или пароль';
- }
- }
- }
-}
-
-$page_title = 'Вход в систему';
-include 'views/header.php';
-?>
-
-
-
Вход в систему
-
-
-
- = e($error) ?>
-
-
-
-
-
- = e($_SESSION['success']) ?>
-
-
-
-
-
-
-
-
-
- Имя пользователя
-
-
-
-
-
-
- Пароль
-
-
-
-
-
- 🔑 Войти
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/logout.php b/logout.php
deleted file mode 100755
index bd5123d..0000000
--- a/logout.php
+++ /dev/null
@@ -1,20 +0,0 @@
-
\ No newline at end of file
diff --git a/models/Book.php b/models/Book.php
index 31c1be4..c52e6d3 100755
--- a/models/Book.php
+++ b/models/Book.php
@@ -64,6 +64,10 @@ class Book {
$published = isset($data['published']) ? (int)$data['published'] : 0;
$editor_type = $data['editor_type'] ?? 'markdown';
+ // Преобразуем пустые строки в NULL для integer полей
+ $series_id = !empty($data['series_id']) ? (int)$data['series_id'] : null;
+ $sort_order_in_series = !empty($data['sort_order_in_series']) ? (int)$data['sort_order_in_series'] : null;
+
$stmt = $this->pdo->prepare("
UPDATE books
SET title = ?, description = ?, genre = ?, series_id = ?, sort_order_in_series = ?, published = ?, editor_type = ?
@@ -73,8 +77,8 @@ class Book {
$data['title'],
$data['description'] ?? null,
$data['genre'] ?? null,
- $data['series_id'] ?? null,
- $data['sort_order_in_series'] ?? null,
+ $series_id, // Теперь это либо integer, либо NULL
+ $sort_order_in_series, // Теперь это либо integer, либо NULL
$published,
$editor_type,
$id,
@@ -206,46 +210,73 @@ class Book {
}
public function convertChaptersContent($book_id, $from_editor, $to_editor) {
- try {
- $this->pdo->beginTransaction();
+ try {
+ $this->pdo->beginTransaction();
+
+ // Получаем все главы книги
+ $chapters = $this->getAllChapters($book_id);
+
+ foreach ($chapters as $chapter) {
+ $converted_content = $this->convertContent(
+ $chapter['content'],
+ $from_editor,
+ $to_editor
+ );
- $chapters = $this->getAllChapters($book_id);
-
- foreach ($chapters as $chapter) {
- $converted_content = $this->convertContent(
- $chapter['content'],
- $from_editor,
- $to_editor
- );
-
- $this->updateChapterContent($chapter['id'], $converted_content);
- }
-
- $this->pdo->commit();
- return true;
- } catch (Exception $e) {
- $this->pdo->rollBack();
- error_log("Error converting chapters: " . $e->getMessage());
- return false;
+ // Обновляем контент главы
+ $this->updateChapterContent($chapter['id'], $converted_content);
}
+
+ $this->pdo->commit();
+ return true;
+ } catch (Exception $e) {
+ $this->pdo->rollBack();
+ error_log("Error converting chapters: " . $e->getMessage());
+ return false;
}
+}
- private function getAllChapters($book_id) {
- $stmt = $this->pdo->prepare("SELECT id, content FROM chapters WHERE book_id = ?");
- $stmt->execute([$book_id]);
- return $stmt->fetchAll(PDO::FETCH_ASSOC);
+private function getAllChapters($book_id) {
+ $stmt = $this->pdo->prepare("SELECT id, content FROM chapters WHERE book_id = ?");
+ $stmt->execute([$book_id]);
+ return $stmt->fetchAll(PDO::FETCH_ASSOC);
+}
+
+private function updateChapterContent($chapter_id, $content) {
+ $word_count = $this->countWords($content);
+ $stmt = $this->pdo->prepare("
+ UPDATE chapters
+ SET content = ?, word_count = ?, updated_at = CURRENT_TIMESTAMP
+ WHERE id = ?
+ ");
+ return $stmt->execute([$content, $word_count, $chapter_id]);
+}
+
+private function convertContent($content, $from_editor, $to_editor) {
+ if ($from_editor === $to_editor) {
+ return $content;
}
- private function updateChapterContent($chapter_id, $content) {
- $word_count = $this->countWords($content);
- $stmt = $this->pdo->prepare("
- UPDATE chapters
- SET content = ?, word_count = ?, updated_at = CURRENT_TIMESTAMP
- WHERE id = ?
- ");
- return $stmt->execute([$content, $word_count, $chapter_id]);
+ require_once __DIR__ . '/../includes/parsedown/ParsedownExtra.php';
+
+ try {
+ if ($from_editor === 'markdown' && $to_editor === 'html') {
+ // Markdown to HTML
+ $parsedown = new ParsedownExtra();
+ return $parsedown->text($content);
+ } elseif ($from_editor === 'html' && $to_editor === 'markdown') {
+ // HTML to Markdown (упрощенная версия)
+ return $this->htmlToMarkdown($content);
+ }
+ } catch (Exception $e) {
+ error_log("Error converting content from {$from_editor} to {$to_editor}: " . $e->getMessage());
+ return $content;
}
+ return $content;
+}
+
+
private function countWords($text) {
$text = strip_tags($text);
$text = preg_replace('/[^\p{L}\p{N}\s]/u', ' ', $text);
@@ -254,26 +285,7 @@ class Book {
return count($words);
}
- private function convertContent($content, $from_editor, $to_editor) {
- if ($from_editor === $to_editor) {
- return $content;
- }
-
- try {
- if ($from_editor === 'markdown' && $to_editor === 'html') {
- // Markdown to HTML с улучшенной обработкой абзацев
- return $this->markdownToHtmlWithParagraphs($content);
- } elseif ($from_editor === 'html' && $to_editor === 'markdown') {
- // HTML to Markdown
- return $this->htmlToMarkdown($content);
- }
- } catch (Exception $e) {
- error_log("Error converting content from {$from_editor} to {$to_editor}: " . $e->getMessage());
- return $content;
- }
-
- return $content;
- }
+
private function markdownToHtmlWithParagraphs($markdown) {
$parsedown = new ParsedownExtra();
diff --git a/register.php b/register.php
deleted file mode 100755
index 25a69d9..0000000
--- a/register.php
+++ /dev/null
@@ -1,180 +0,0 @@
-findByUsername($username)) {
- $error = 'Это имя пользователя уже занято';
- } elseif (!empty($email) && $userModel->findByEmail($email)) {
- $error = 'Этот email уже используется';
- } else {
- // Подготавливаем данные для создания пользователя
- $user_data = [
- 'username' => $username,
- 'display_name' => $display_name ?: $username,
- 'email' => $email,
- 'password' => $password
- ];
-
- // Если пользователя создает администратор - сразу активный
- // Если пользователь регистрируется сам - требует активации
- if ($is_admin) {
- $user_data['is_active'] = 1;
- } else {
- $user_data['is_active'] = 0;
- }
-
- // Создаем пользователя
- $success = $userModel->create($user_data);
-
- if ($success) {
- if ($is_admin) {
- $_SESSION['success'] = 'Пользователь успешно создан и активирован';
- redirect('admin/users.php');
- } else {
- $_SESSION['success'] = 'Регистрация прошла успешно. Ваш аккаунт ожидает активации администратором.';
- redirect('login.php');
- }
- } else {
- $error = 'Произошла ошибка при регистрации. Попробуйте еще раз.';
- }
- }
- }
- }
-}
-
-$page_title = $is_admin ? 'Добавление пользователя' : 'Регистрация';
-include 'views/header.php';
-?>
-
-
-
= $is_admin ? 'Добавление пользователя' : 'Регистрация' ?>
-
-
-
- = e($error) ?>
-
-
-
-
-
- = e($success) ?>
-
-
-
-
-
-
-
-
- Имя пользователя *
-
-
-
-
-
-
- Отображаемое имя
-
-
-
-
-
-
- Email
-
-
-
-
-
-
- Пароль *
-
-
-
-
-
-
- Подтверждение пароля *
-
-
-
-
-
-
- = $is_admin ? '👥 Добавить пользователя' : '📝 Зарегистрироваться' ?>
-
-
-
-
- ❌ Отмена
-
-
-
-
-
-
-
-
Уже есть аккаунт? Войдите здесь
-
-
- Примечание: После регистрации ваш аккаунт должен быть активирован администратором.
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/series.php b/series.php
deleted file mode 100644
index 9f90f28..0000000
--- a/series.php
+++ /dev/null
@@ -1,95 +0,0 @@
-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);
-
-$page_title = "Мои серии книг";
-include 'views/header.php';
-?>
-
-Мои серии книг
-
-
-
- = e($_SESSION['success']) ?>
-
-
-
-
-
-
- = e($_SESSION['error']) ?>
-
-
-
-
-
-
-
-
- У вас пока нет серий книг
- Создайте свою первую серию для организации книг!
- 📚 Создать первую серию
-
-
-
-
-
-
-
- = e($ser['title']) ?>
-
-
-
-
-
- = e(mb_strimwidth($ser['description'], 0, 200, '...')) ?>
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/series_delete.php b/series_delete.php
deleted file mode 100644
index 372093f..0000000
--- a/series_delete.php
+++ /dev/null
@@ -1,39 +0,0 @@
-userOwnsSeries($series_id, $user_id)) {
- $_SESSION['error'] = "У вас нет доступа к этой серии";
- redirect('series.php');
-}
-
-$series = $seriesModel->findById($series_id);
-
-if ($seriesModel->delete($series_id, $user_id)) {
- $_SESSION['success'] = "Серия «" . e($series['title']) . "» успешно удалена";
-} else {
- $_SESSION['error'] = "Ошибка при удалении серии";
-}
-
-redirect('series.php');
-?>
\ No newline at end of file
diff --git a/series_edit.php b/series_edit.php
deleted file mode 100644
index e84a039..0000000
--- a/series_edit.php
+++ /dev/null
@@ -1,179 +0,0 @@
-findById($series_id);
- if (!$series || !$seriesModel->userOwnsSeries($series_id, $user_id)) {
- $_SESSION['error'] = "Серия не найдена или у вас нет доступа";
- redirect('series.php');
- }
- $is_edit = true;
-}
-
-if ($_SERVER['REQUEST_METHOD'] === 'POST') {
- if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
- $_SESSION['error'] = "Ошибка безопасности";
- redirect($is_edit ? "series_edit.php?id=$series_id" : 'series_edit.php');
- }
-
- $title = trim($_POST['title'] ?? '');
- $description = trim($_POST['description'] ?? '');
-
- if (empty($title)) {
- $_SESSION['error'] = "Название серии обязательно";
- } else {
- $data = [
- 'title' => $title,
- 'description' => $description,
- 'user_id' => $user_id
- ];
-
- if ($is_edit) {
- $success = $seriesModel->update($series_id, $data);
- $message = $success ? "Серия успешно обновлена" : "Ошибка при обновлении серии";
- } else {
- $success = $seriesModel->create($data);
- $message = $success ? "Серия успешно создана" : "Ошибка при создании серии";
-
- if ($success) {
- $new_series_id = $pdo->lastInsertId();
- redirect("series_edit.php?id=$new_series_id");
- }
- }
-
- if ($success) {
- $_SESSION['success'] = $message;
- redirect('series.php');
- } else {
- $_SESSION['error'] = $message;
- }
- }
-}
-
-$page_title = $is_edit ? "Редактирование серии" : "Создание новой серии";
-include 'views/header.php';
-?>
-
-= $is_edit ? "Редактирование серии" : "Создание новой серии" ?>
-
-
-
- = e($_SESSION['error']) ?>
-
-
-
-
-
-
-
-
-
- Название серии *
-
-
-
-
- Описание серии
-
- = e($series['description'] ?? $_POST['description'] ?? '') ?>
-
-
-
-
- = $is_edit ? '💾 Сохранить изменения' : '📚 Создать серию' ?>
-
-
-
- ❌ Отмена
-
-
-
-
-
-
-
Книги в этой серии
-
- findBySeries($series_id);
-
- // Вычисляем общую статистику
- $total_chapters = 0;
- $total_words = 0;
- foreach ($books_in_series as $book) {
- $stats = $bookModel->getBookStats($book['id']);
- $total_chapters += $stats['chapter_count'] ?? 0;
- $total_words += $stats['total_words'] ?? 0;
- }
- ?>
-
-
-
-
-
-
-
-
- Порядок
- Название книги
- Жанр
- Статус
- Действия
-
-
-
-
-
- = $book['sort_order_in_series'] ?>
-
- = e($book['title']) ?>
-
- = e(mb_strimwidth($book['description'], 0, 100, '...')) ?>
-
-
- = e($book['genre']) ?>
-
-
- = $book['published'] ? '✅ Опубликована' : '📝 Черновик' ?>
-
-
-
-
- Редактировать
-
-
-
-
-
-
-
-
-
- Статистика серии:
- Книг: = count($books_in_series) ?> |
- Глав: = $total_chapters ?> |
- Слов: = $total_words ?>
-
-
-
-
-
-
\ No newline at end of file
diff --git a/view_book.php b/view_book.php
deleted file mode 100755
index 2fb01e0..0000000
--- a/view_book.php
+++ /dev/null
@@ -1,276 +0,0 @@
-findByShareToken($share_token);
-} elseif ($book_id) {
- $book = $bookModel->findById($book_id);
-}
-
-if (!$book) {
- http_response_code(404);
- $page_title = "Книга не найдена";
- include 'views/header.php';
- ?>
-
-
- Книга не найдена
- Запрошенная книга не существует или была удалена.
- На главную
-
-
- getPublishedChapters($book['id']);
-$total_words = array_sum(array_column($chapters, 'word_count'));
-
-// Получаем информацию об авторе
-$stmt = $pdo->prepare("SELECT display_name, username FROM users WHERE id = ?");
-$stmt->execute([$book['user_id']]);
-$author_info = $stmt->fetch(PDO::FETCH_ASSOC);
-$author_name = $author_info['display_name'] ?? $author_info['username'] ?? 'Неизвестный автор';
-
-$page_title = $book['title'];
-include 'views/header.php';
-?>
-
-
-
-
-
-
-
-
-
-
- = e($book['title']) ?>
-
- prepare("SELECT id, title FROM series WHERE id = ?");
- $series_stmt->execute([$book['series_id']]);
- $series = $series_stmt->fetch();
- ?>
-
-
- 📚 Часть серии:
-
- = e($series['title']) ?>
-
- (Книга = $book['sort_order_in_series'] ?>)
-
-
-
-
-
- = e($author_name) ?>
-
-
-
- Жанр: = e($book['genre']) ?>
-
-
-
-
-
-
= nl2br(e($book['description'])) ?>
-
-
-
-
-
-
-
-
-
-
-
-
-
Экспорт книги
-
-
-
-
- Примечание: Экспортируются только опубликованные главы
-
-
-
-
-
-
В этой книге пока нет опубликованных глав
-
Автор еще не опубликовал ни одной главы
-
-
-
- $chapter): ?>
-
-
- = e($chapter['title']) ?>
- 🔗
-
-
-
- = $Parsedown->text($chapter['content']) ?>
-
- = $chapter['content'] ?>
-
-
-
-
-
-
Обновлено: = date('d.m.Y', strtotime($chapter['updated_at'])) ?>
-
↑ Наверх
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/views/auth/login.php b/views/auth/login.php
new file mode 100644
index 0000000..6c16850
--- /dev/null
+++ b/views/auth/login.php
@@ -0,0 +1,49 @@
+
+
+
+
Вход в систему
+
+
+
+ = e($error) ?>
+
+
+
+
+
+
+
+
+ Имя пользователя
+
+
+
+
+
+
+ Пароль
+
+
+
+
+
+ 🔑 Войти
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/views/auth/register.php b/views/auth/register.php
new file mode 100644
index 0000000..1e3720b
--- /dev/null
+++ b/views/auth/register.php
@@ -0,0 +1,85 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/views/books/create.php b/views/books/create.php
new file mode 100644
index 0000000..2751717
--- /dev/null
+++ b/views/books/create.php
@@ -0,0 +1,66 @@
+
+Создание новой книги
+
+
+
+
+ Название книги *
+
+
+
+ Жанр
+
+
+
+ Режим редактора
+
+
+ >Markdown редактор
+ >HTML редактор (TinyMCE)
+
+
+ Серия
+
+
+ -- Без серии --
+
+ >
+ = e($ser['title']) ?>
+
+
+
+
+ Описание книги
+
+
= e($_POST['description'] ?? '') ?>
+
+
+ >
+ Опубликовать книгу (показывать на публичной странице автора)
+
+
+
+
+
+
\ No newline at end of file
diff --git a/views/books/edit.php b/views/books/edit.php
new file mode 100644
index 0000000..5bb9c58
--- /dev/null
+++ b/views/books/edit.php
@@ -0,0 +1,259 @@
+
+Редактирование книги
+
+
+
+
+ Название книги *
+
+
+
+ Жанр
+
+
+
+ Режим редактора
+
+
+ $label): ?>
+ >
+ = e($label) ?>
+
+
+
+
+ Внимание: При смене редактора содержимое всех глав будет автоматически сконвертировано в новый формат.
+
+
+ Серия
+
+
+ -- Без серии --
+
+ >
+ = e($ser['title']) ?>
+
+
+
+
+ Порядок в серии
+
+
+
+
+
+ Обложка книги
+
+
+
+
Текущая обложка:
+
+
+
+
+ Удалить обложку
+
+
+
+
+
+
+ Разрешены: JPG, PNG, GIF, WebP. Максимальный размер: 5MB.
+ Рекомендуемый размер: 300×450 пикселей.
+
+
+
+ ❌ = e($cover_error) ?>
+
+
+
+
+ Описание книги
+
+
= e($book['description'] ?? '') ?>
+
+
+ >
+ Опубликовать книгу (показывать на публичной странице автора)
+
+
+
+
+
+
+
+
+
+
+ 🔄 Нормализовать контент глав
+
+ Если контент глав отображается неправильно после смены редактора, можно нормализовать его структуру.
+
+
+
+
+
Публичная ссылка для чтения
+
+
+
+ 📋 Копировать
+
+
+
+
+ 🔄 Обновить
+
+
+
+
+ Примечание: В публичном просмотре отображаются только главы со статусом "Опубликована"
+
+
+
+
Экспорт книги
+
Экспортируйте книгу в различные форматы:
+
+
+ Примечание: Экспортируются все главы книги (включая черновики)
+
+
+
+
Главы этой книги
+
+
+
+
+
+
+ Название
+ Статус
+ Слов
+ Действия
+
+
+
+
+
+ = e($chapter['title']) ?>
+
+
+ = $chapter['status'] == 'published' ? 'Опубликована' : 'Черновик' ?>
+
+
+ = $chapter['word_count'] ?>
+
+
+ Редактировать
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 🗑️ Удалить книгу
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/views/books/index.php b/views/books/index.php
new file mode 100644
index 0000000..cec2560
--- /dev/null
+++ b/views/books/index.php
@@ -0,0 +1,66 @@
+
+
+Мои книги
+
+
+
+
+
+ У вас пока нет книг
+ Создайте свою первую книгу и начните писать!
+ 📖 Создать первую книгу
+
+
+
+
+
+
+
+ = e($book['title']) ?>
+
+
+
+ = e($book['genre']) ?>
+
+
+
+
+ = e(mb_strimwidth($book['description'], 0, 200, '...')) ?>
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/views/books/view_public.php b/views/books/view_public.php
new file mode 100644
index 0000000..131492c
--- /dev/null
+++ b/views/books/view_public.php
@@ -0,0 +1,151 @@
+
+
+
+
+
+
+
+
+
+
+
+ = e($book['title']) ?>
+
+
+ Автор: = e($author_name) ?>
+
+
+
+
+ = e($book['genre']) ?>
+
+
+
+
+
+ = nl2br(e($book['description'])) ?>
+
+
+
+
+
Глав: = count($chapters) ?>
+
Слов: = array_sum(array_column($chapters, 'word_count')) ?>
+
+ 📄 Скачать книгу
+
+
+
+
+
+
+
В этой книге пока нет глав
+
Автор еще не опубликовал содержание книги
+
+
+ Оглавление
+
+
+ $chapter): ?>
+
+
+
+
+ Слов: = $chapter['word_count'] ?>
+ | Обновлено: = date('d.m.Y', strtotime($chapter['updated_at'])) ?>
+
+
+
+
+
+
+
+
+ $chapter): ?>
+
+
+ Глава = $index + 1 ?>: = e($chapter['title']) ?>
+
+
+
+
+ = $Parsedown->text($chapter['content']) ?>
+
+ = $chapter['content'] ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/views/chapters/create.php b/views/chapters/create.php
new file mode 100644
index 0000000..07ddb6b
--- /dev/null
+++ b/views/chapters/create.php
@@ -0,0 +1,134 @@
+
+
+Новая глава для: = e($book['title']) ?>
+
+
+
+ = e($error) ?>
+
+
+
+
+
+
+
+
+ Название главы *
+
+
+
+
+ Содержание главы *
+
+
+
+
= e($_POST['content'] ?? '') ?>
+
+
+
+ **B**
+ *I*
+ ~~S~~
+ `code`
+ - список
+ > цитата
+ # Заголовок
+ — диалог
+
+
+
+
= e($_POST['content'] ?? '') ?>
+
+
+
+
+
+
+ Статус главы
+
+
+ >📝 Черновик
+ >✅ Опубликована
+
+
+ Опубликованные главы видны в публичном доступе
+
+
+
+
+
+
+ 💾 Сохранить главу
+
+
+
+ 👁️ Предпросмотр
+
+
+
+ ❌ Отмена
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/views/chapters/edit.php b/views/chapters/edit.php
new file mode 100644
index 0000000..cb8d848
--- /dev/null
+++ b/views/chapters/edit.php
@@ -0,0 +1,123 @@
+
+
+Редактирование главы: = e($chapter['title']) ?>
+
+
+
+ = e($error) ?>
+
+
+
+
+
+
+
+
+ Название главы *
+
+
+
+
+ Содержание главы *
+
+
+
+
= e($chapter['content']) ?>
+
+
= e($chapter['content']) ?>
+
+
+
+
+
+
+ Статус главы
+
+
+ >📝 Черновик
+ >✅ Опубликована
+
+
+ Опубликованные главы видны в публичном доступе
+
+
+
+
+
+
+ 💾 Сохранить изменения
+
+
+
+ 👁️ Предпросмотр
+
+
+
+ ❌ Отмена
+
+
+
+
+
+
Информация о главе
+
Книга: = e($book['title']) ?>
+
Количество слов: = $chapter['word_count'] ?>
+
Создана: = date('d.m.Y H:i', strtotime($chapter['created_at'])) ?>
+
Обновлена: = date('d.m.Y H:i', strtotime($chapter['updated_at'])) ?>
+
+
+
+
+
+
\ No newline at end of file
diff --git a/chapters.php b/views/chapters/index.php
old mode 100755
new mode 100644
similarity index 52%
rename from chapters.php
rename to views/chapters/index.php
index ea35053..0a34003
--- a/chapters.php
+++ b/views/chapters/index.php
@@ -1,120 +1,81 @@
-userOwnsBook($book_id, $user_id)) {
- $_SESSION['error'] = "У вас нет доступа к этой книге";
- redirect('books.php');
-}
-
-// Получаем информацию о книге и главах
-$book = $bookModel->findById($book_id);
-$chapters = $chapterModel->findByBook($book_id);
-
-$page_title = "Главы книги: " . e($book['title']);
-include 'views/header.php';
-?>
-
-
-
Главы книги: = e($book['title']) ?>
-
-
-
-
-
- = e($_SESSION['success']) ?>
-
-
-
-
-
-
- = e($_SESSION['error']) ?>
-
-
-
-
-
-
-
-
-
-
-
- №
- Название главы
- Статус
- Слов
- Обновлено
- Действия
-
-
-
- $chapter): ?>
-
- = $index + 1 ?>
-
- = e($chapter['title']) ?>
-
- = e(mb_strimwidth($chapter['description'], 0, 100, '...')) ?>
-
-
-
-
- = $chapter['status'] == 'published' ? '✅ Опубликована' : '📝 Черновик' ?>
-
-
- = $chapter['word_count'] ?>
-
- = date('d.m.Y H:i', strtotime($chapter['updated_at'])) ?>
-
-
-
-
-
-
-
-
-
-
-
- Статистика:
- Всего глав: = count($chapters) ?> |
- Всего слов: = array_sum(array_column($chapters, 'word_count')) ?> |
- Опубликовано: = count(array_filter($chapters, function($ch) { return $ch['status'] == 'published'; })) ?>
-
-
-
-
\ No newline at end of file
+
+
+
+
Главы книги: = e($book['title']) ?>
+
+
+
+
+
+
+
+
+
+
+ №
+ Название главы
+ Статус
+ Слов
+ Обновлено
+ Действия
+
+
+
+ $chapter): ?>
+
+ = $index + 1 ?>
+
+ = e($chapter['title']) ?>
+
+ = e(mb_strimwidth($chapter['description'], 0, 100, '...')) ?>
+
+
+
+
+ = $chapter['status'] == 'published' ? '✅ Опубликована' : '📝 Черновик' ?>
+
+
+ = $chapter['word_count'] ?>
+
+ = date('d.m.Y H:i', strtotime($chapter['updated_at'])) ?>
+
+
+
+
+
+
+
+
+
+
+
+ Статистика:
+ Всего глав: = count($chapters) ?> |
+ Всего слов: = array_sum(array_column($chapters, 'word_count')) ?> |
+ Опубликовано: = count(array_filter($chapters, function($ch) { return $ch['status'] == 'published'; })) ?>
+
+
+
+
\ No newline at end of file
diff --git a/preview.php b/views/chapters/preview.php
old mode 100755
new mode 100644
similarity index 63%
rename from preview.php
rename to views/chapters/preview.php
index 0f0446f..864c553
--- a/preview.php
+++ b/views/chapters/preview.php
@@ -1,23 +1,6 @@
text($content);
-} else {
- $html_content = $content;
-}
-
-
-$page_title = "Предпросмотр: " . e($title);
+// views/chapters/preview.php
+include 'views/layouts/header.php';
?>
@@ -25,7 +8,8 @@ $page_title = "Предпросмотр: " . e($title);
= e($page_title) ?>
-
+
+
@@ -102,11 +86,14 @@ $page_title = "Предпросмотр: " . e($title);
- = $html_content ?>
+ = $content ?>
-
+
Сгенерировано = date('d.m.Y H:i') ?> | Markdown Preview
+
+ Закрыть
+ Печать