From d7fe90a615e58307802ae881bba20556ebb69338 Mon Sep 17 00:00:00 2001 From: mirivlad Date: Mon, 24 Nov 2025 11:53:56 +0800 Subject: [PATCH] add wisiwig editor TinyMCE. default content type = markdown. --- README.md | 174 ++-- assets/css/style.css | 1882 +++++++++++++++++++++--------------------- author.php | 382 ++++----- book_delete.php | 92 +-- book_delete_all.php | 88 +- book_edit.php | 757 +++++++++-------- books.php | 310 +++---- chapter_delete.php | 84 +- chapter_edit.php | 675 ++++++++------- chapters.php | 238 +++--- composer.json | 12 +- composer.lock | 578 ++++++------- dashboard.php | 380 ++++----- export_book.php | 1632 +++++++++++++++++++----------------- install.php | 1 + login.php | 198 ++--- models/Book.php | 701 +++++++++++----- models/Series.php | 314 +++---- models/User.php | 236 +++--- preview.php | 10 +- profile.php | 388 ++++----- series.php | 188 ++--- series_delete.php | 76 +- series_edit.php | 356 ++++---- view_book.php | 544 ++++++------ view_series.php | 320 +++---- views/header.php | 115 +-- 27 files changed, 5676 insertions(+), 5055 deletions(-) mode change 100755 => 100644 models/Series.php mode change 100755 => 100644 series.php mode change 100755 => 100644 series_delete.php mode change 100755 => 100644 series_edit.php mode change 100755 => 100644 view_series.php diff --git a/README.md b/README.md index d6e5a61..d9fb3ee 100644 --- a/README.md +++ b/README.md @@ -1,88 +1,88 @@ -# Web Writer - -**Лицензия:** AGPLv3 - -**Web Writer** — веб-приложение для написания, хранения и публикации книг и серий книг. Поддерживает Markdown с расширением для диалогов, автосохранение текста, экспорт и управление пользователями. - ---- - -## 🚀 Возможности - -- **Книги и серии:** создавайте серии и добавляйте книги с главами. -- **Редактор книг:** Markdown, автосохранение текста, интерактивное содержание. -- **Предпросмотр книг:** - - **Автор:** видит все черновики и опубликованные главы. - - **Публичный доступ:** только опубликованные главы по ссылке с `shared_token`. -- **Обложки и аватары:** добавляйте изображения к книгам и профилям. -- **Экспорт:** PDF, DOCX, HTML, TXT. -- **Администрирование пользователей:** - - Управление аккаунтами, активация/деактивация. - - При удалении пользователя удаляются все его книги. -- **Публичные ссылки:** делитесь `shared_token` для просмотра опубликованных глав. - ---- - -## ⚙️ Требования - -- **PHP:** 8.0 и выше -- **MySQL** с InnoDB и внешними ключами -- **PHP расширения:** `mbstring`, `json`, `PDO` -- Веб-сервер с правами на запись в папки `config/` и `uploads/` - -> Все библиотеки уже включены в `vendor/`. Composer не нужен. - ---- - -## 🛠 Установка - -1. Скопируйте файлы на веб-сервер. -2. Проверьте доступность папок `config/` и `uploads/` для записи. -3. Перейдите в браузере на `install.php` и следуйте шагам: - - **Шаг 1: Настройки базы данных** - - Хост БД - - Имя базы данных - - Пользователь и пароль - - **Шаг 2: Создание администратора** - - Имя пользователя - - Пароль - - Email (по желанию) - - Отображаемое имя (по желанию) - -4. После успешной установки файл `config/config.php` будет сгенерирован автоматически. -5. Перейдите на главную страницу приложения (`index.php`) и войдите под админом. -6. **Не забудьте удалить или переместить файл install.php!!!** - ---- - -## 📝 Конфигурация - -Файл `config/config.php` содержит: - -- Подключение к базе данных: `DB_HOST`, `DB_USER`, `DB_PASS`, `DB_NAME` -- Пути к файлам: - - `UPLOAD_PATH` — корневая папка загрузок - - `COVERS_PATH` / `COVERS_URL` — обложки книг - - `AVATARS_PATH` / `AVATARS_URL` — аватары пользователей -- Адрес сайта: `SITE_URL` -- Имя приложения: `APP_NAME` = "Web Writer" - ---- - -## 🛠 Дальнейшее развитие - -- Планирую вынести работу с сущностями (книги, главы, серии, пользователи) в контроллеры. -- Создать единую точку входа для приложения. - ---- - -## ❗ Поддержка - -Все ошибки и предложения шлите в issue - ---- - -## 📜 Лицензия - +# Web Writer + +**Лицензия:** AGPLv3 + +**Web Writer** — веб-приложение для написания, хранения и публикации книг и серий книг. Поддерживает Markdown с расширением для диалогов, автосохранение текста, экспорт и управление пользователями. + +--- + +## 🚀 Возможности + +- **Книги и серии:** создавайте серии и добавляйте книги с главами. +- **Редактор книг:** Markdown, автосохранение текста, интерактивное содержание. +- **Предпросмотр книг:** + - **Автор:** видит все черновики и опубликованные главы. + - **Публичный доступ:** только опубликованные главы по ссылке с `shared_token`. +- **Обложки и аватары:** добавляйте изображения к книгам и профилям. +- **Экспорт:** PDF, DOCX, HTML, TXT. +- **Администрирование пользователей:** + - Управление аккаунтами, активация/деактивация. + - При удалении пользователя удаляются все его книги. +- **Публичные ссылки:** делитесь `shared_token` для просмотра опубликованных глав. + +--- + +## ⚙️ Требования + +- **PHP:** 8.0 и выше +- **MySQL** с InnoDB и внешними ключами +- **PHP расширения:** `mbstring`, `json`, `PDO` +- Веб-сервер с правами на запись в папки `config/` и `uploads/` + +> Все библиотеки уже включены в `vendor/`. Composer не нужен. + +--- + +## 🛠 Установка + +1. Скопируйте файлы на веб-сервер. +2. Проверьте доступность папок `config/` и `uploads/` для записи. +3. Перейдите в браузере на `install.php` и следуйте шагам: + + **Шаг 1: Настройки базы данных** + - Хост БД + - Имя базы данных + - Пользователь и пароль + + **Шаг 2: Создание администратора** + - Имя пользователя + - Пароль + - Email (по желанию) + - Отображаемое имя (по желанию) + +4. После успешной установки файл `config/config.php` будет сгенерирован автоматически. +5. Перейдите на главную страницу приложения (`index.php`) и войдите под админом. +6. **Не забудьте удалить или переместить файл install.php!!!** + +--- + +## 📝 Конфигурация + +Файл `config/config.php` содержит: + +- Подключение к базе данных: `DB_HOST`, `DB_USER`, `DB_PASS`, `DB_NAME` +- Пути к файлам: + - `UPLOAD_PATH` — корневая папка загрузок + - `COVERS_PATH` / `COVERS_URL` — обложки книг + - `AVATARS_PATH` / `AVATARS_URL` — аватары пользователей +- Адрес сайта: `SITE_URL` +- Имя приложения: `APP_NAME` = "Web Writer" + +--- + +## 🛠 Дальнейшее развитие + +- Планирую вынести работу с сущностями (книги, главы, серии, пользователи) в контроллеры. +- Создать единую точку входа для приложения. + +--- + +## ❗ Поддержка + +Все ошибки и предложения шлите в issue + +--- + +## 📜 Лицензия + Приложение распространяется под лицензией [AGPLv3](https://www.gnu.org/licenses/agpl-3.0.html). \ No newline at end of file diff --git a/assets/css/style.css b/assets/css/style.css index 32377e2..8f53fb0 100755 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -1,925 +1,959 @@ -/* Базовые стили */ -h1, h2, h3, h4, h5, h6 { - margin-bottom: 1rem; -} - -article, textarea, main.container { - margin-top: 1rem; - margin-bottom: 1rem; - padding-top: 1rem; - padding-bottom: 1rem; -} - -article > header, article > footer { - margin-top: 0.1rem; - margin-bottom: 0.1rem; - padding-top: 0.2rem; - padding-bottom: 0.2rem; -} - -input:not([type="checkbox"], [type="radio"]), select { - padding: 4px; - height: 2em; -} - -/* Убираем конфликтующие стили Pico CSS */ -article header, article footer { - margin: 0; - padding: 0; -} - -article > header, article > footer { - margin: 0; - padding: 0; -} - -/* Уведомления */ -.alert { - padding: 1rem; - margin: 1rem 0; - border-radius: 5px; -} - -.alert-error { - background: #ffebee; - color: #c62828; - border: 1px solid #ffcdd2; -} - -.alert-success { - background: #e8f5e8; - color: #2e7d32; - border: 1px solid #c8e6c9; -} - -/* Кнопки */ -.compact-button { - padding: 3px 8px !important; - font-size: 0.85rem; - text-decoration: none; - display: inline-flex !important; - align-items: center; - justify-content: center; - border: 1px solid var(--secondary); - border-radius: 4px; - background: var(--secondary); - color: var(--secondary-inverse); - cursor: pointer; - height: 28px !important; - box-sizing: border-box; - line-height: 1 !important; - vertical-align: middle; -} - -.compact-button:hover { - opacity: 0.9; -} - -.button-group { - display: flex; - gap: 5px; - margin-bottom: 1rem; -} - -.button-group button, -.button-group a[role="button"] { - flex: 1; - padding: 0.5rem; - height: 38px; - display: flex; - align-items: center; - justify-content: center; - text-decoration: none; - box-sizing: border-box; -} - -.button-group .delete-btn { - background: #ff4444 !important; - border-color: #ff4444 !important; - color: white !important; -} - -.green-btn { - background: #449944 !important; - border-color: #449944 !important; - color: white !important; -} - -.green-btn:hover { - background: #44bb44 !important; - border-color: #44bb44 !important; - color: white !important; -} - -.profile-buttons { - display: flex; - gap: 10px; - align-items: stretch; -} - -.profile-button { - flex: 1; - padding: 0.75rem; - display: flex; - align-items: center; - justify-content: center; - text-decoration: none; - border: 1px solid; - border-radius: 4px; - font-size: 1rem; - cursor: pointer; - box-sizing: border-box; - height: 62px; - min-height: 44px; -} - -.profile-button.primary { - background: var(--primary); - border-color: var(--primary); - color: var(--primary-inverse); -} - -.profile-button.secondary { - background: var(--secondary); - border-color: var(--secondary); - color: var(--secondary-inverse); -} - -.adaptive-button { - padding: 8px 12px !important; - font-size: 0.85rem; - text-decoration: none; - display: inline-flex; - align-items: center; - justify-content: center; - border: 1px solid var(--secondary); - border-radius: 4px; - background: var(--secondary); - color: var(--secondary-inverse); - cursor: pointer; - box-sizing: border-box; - text-align: center; - white-space: normal; - word-break: break-word; - min-height: 44px; - flex: 1; - min-width: 120px; - margin: 2px; -} - -.adaptive-button:hover { - opacity: 0.9; -} - -.primary.adaptive-button { - background: var(--primary); - border-color: var(--primary); - color: var(--primary-inverse); -} - -.action-button { - display: inline-flex; - align-items: center; - justify-content: center; - padding: 0.75rem 1.5rem; - font-size: 0.9rem; - text-decoration: none; - border: 1px solid; - border-radius: 4px; - cursor: pointer; - box-sizing: border-box; - height: 44px; - min-width: 140px; - white-space: nowrap; - transition: all 0.3s ease; - text-align: center; -} - -.action-button.primary { - background: #007bff; - border-color: #007bff; - color: #fff; -} - -.action-button.primary:hover { - opacity: 0.9; -} - -.action-button.delete { - margin-top: 1rem; - background: #ff4444; - border-color: #ff4444; - color: white; -} - -.action-button.delete:hover { - background: #dd3333; - border-color: #dd3333; - color: white; -} - -/* Таблицы */ -.compact-table { - width: 100%; - font-size: 0.9rem; - border-collapse: collapse; -} - -.compact-table th, -.compact-table td { - padding: 6px 8px; - border-bottom: 1px solid #eee; -} - -.compact-table th { - background: #f5f5f5; - font-weight: bold; -} - -/* Markdown редактор */ -#content { - transition: all 0.3s ease; - border: 1px solid #ddd; - border-radius: 4px; - padding: 12px; - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; - font-size: 14px; - line-height: 1.5; - min-height: 400px; - color: #111; - background-color: #fff; - resize: vertical; -} - -#content:focus { - border-color: #007bff; - box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25); - outline: none; - color: #111; - background-color: #fff; -} - -/* Кастомный скроллбар для редактора */ -#content::-webkit-scrollbar { - width: 8px; -} - -#content::-webkit-scrollbar-track { - background: #f1f1f1; - border-radius: 4px; -} - -#content::-webkit-scrollbar-thumb { - background: #c1c1c1; - border-radius: 4px; -} - -#content::-webkit-scrollbar-thumb:hover { - background: #a8a8a8; -} - -#content { - scrollbar-width: thin; - scrollbar-color: #c1c1c1 #f1f1f1; -} - -/* Элементы управления редактором */ -.editor-controls { - position: sticky !important; - top: 10px !important; - right: 10px !important; - z-index: 100 !important; - display: flex; - gap: 5px; - justify-content: flex-end; - margin-bottom: 10px; -} - -.editor-controls button { - width: 40px !important; - height: 40px !important; - border-radius: 50% !important; - border: 1px solid #ddd !important; - background: white !important; - cursor: pointer !important; - font-size: 16px !important; - display: flex !important; - align-items: center !important; - justify-content: center !important; - box-shadow: 0 2px 5px rgba(0,0,0,0.2) !important; - transition: all 0.3s ease !important; - color: #333333 !important; -} - -.editor-controls button:hover { - transform: scale(1.1) !important; - background-color: #f8f9fa !important; - box-shadow: 0 4px 8px rgba(0,0,0,0.3) !important; -} - -/* Полноэкранный режим редактора */ -#fullscreen-controls { - position: fixed !important; - z-index: 9999 !important; - display: flex !important; - gap: 5px !important; -} - -#fullscreen-controls button { - width: 45px !important; - height: 45px !important; - border-radius: 50% !important; - border: 1px solid #ddd !important; - background: white !important; - cursor: pointer !important; - font-size: 18px !important; - display: flex !important; - align-items: center !important; - justify-content: center !important; - box-shadow: 0 2px 5px rgba(0,0,0,0.3) !important; - transition: all 0.3s ease !important; - color: #333333 !important; -} - -#fullscreen-controls button:hover { - transform: scale(1.1) !important; - background-color: #f8f9fa !important; - box-shadow: 0 4px 8px rgba(0,0,0,0.4) !important; -} - -/* Стили для отображения контента книг */ -.book-content { - line-height: 1.7; - font-family: Georgia, serif; -} - -.book-content h1 { - font-size: 2em; - margin-top: 2rem; - margin-bottom: 1rem; - border-bottom: 2px solid #eee; - padding-bottom: 0.5rem; -} - -.book-content h2 { - font-size: 1.6em; - margin-top: 1.5rem; - margin-bottom: 1rem; - border-bottom: 1px solid #eee; - padding-bottom: 0.3rem; -} - -.book-content h3 { - font-size: 1.3em; - margin-top: 1.2rem; - margin-bottom: 0.8rem; -} - -.book-content p { - margin-bottom: 1rem; - text-align: justify; -} - -.book-content blockquote { - border-left: 4px solid #007bff; - padding-left: 1.5rem; - margin-left: 0; - margin-right: 0; - color: #555; - font-style: italic; - background: #f8f9fa; - padding: 1rem; - border-radius: 0 5px 5px 0; -} - -.book-content code { - background: #f5f5f5; - padding: 2px 6px; - border-radius: 3px; - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; - font-size: 0.9em; -} - -.book-content pre { - background: #2d2d2d; - color: #f8f8f2; - padding: 1rem; - border-radius: 5px; - overflow-x: auto; - border-left: 4px solid #007bff; -} - -.book-content pre code { - background: none; - padding: 0; - color: inherit; -} - -.book-content ul, .book-content ol { - margin-bottom: 1rem; - padding-left: 2rem; -} - -.book-content li { - margin-bottom: 0.3rem; -} - -.book-content table { - width: 100%; - border-collapse: collapse; - margin-bottom: 1rem; - box-shadow: 0 1px 3px rgba(0,0,0,0.1); -} - -.book-content th, .book-content td { - border: 1px solid #ddd; - padding: 10px 12px; - text-align: left; -} - -.book-content th { - background: #f5f5f5; - font-weight: bold; -} - -.book-content tr:nth-child(even) { - background: #f9f9f9; -} - -.dialogue { - margin-left: 2rem; - font-style: italic; - color: #2c5aa0; -} - -/* Обложки и медиа */ -.book-cover { - transition: transform 0.3s ease; -} - -.book-cover:hover { - transform: scale(1.05); -} - -.cover-placeholder { - width: 120px; - height: 160px; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - border-radius: 4px; - display: flex; - align-items: center; - justify-content: center; - color: white; - font-size: 2rem; - margin: 0 auto 1rem; -} - -/* Аватарки и профиль */ -.avatar-container { - text-align: center; - margin-bottom: 1.5rem; -} - -.avatar { - width: 150px; - height: 150px; - border-radius: 50%; - border: 3px solid #007bff; - object-fit: cover; -} - -.avatar-placeholder { - width: 150px; - height: 150px; - border-radius: 50%; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - display: flex; - align-items: center; - justify-content: center; - color: white; - font-size: 3rem; - margin: 0 auto; -} - -.author-bio { - background: #f8f9fa; - padding: 1.5rem; - border-radius: 8px; - margin: 1rem 0; - line-height: 1.6; -} - -.author-bio h1, .author-bio h2, .author-bio h3 { - margin-top: 1rem; - margin-bottom: 0.5rem; -} - -.author-bio p { - margin-bottom: 1rem; -} - -.author-bio ul, .author-bio ol { - margin-bottom: 1rem; - padding-left: 2rem; -} - -.author-bio blockquote { - border-left: 4px solid #007bff; - padding-left: 1rem; - margin-left: 0; - color: #555; - font-style: italic; -} - -/* Статистика */ -.author-stats { - display: flex; - justify-content: center; - gap: 2rem; - flex-wrap: wrap; - margin: 1rem 0; -} - -.stat-item { - text-align: center; -} - -.stat-number { - font-size: 1.5em; - font-weight: bold; - color: #007bff; -} - -.stat-label { - font-size: 0.9em; - color: #666; -} - -/* Серии книг */ -.series-books article { - transition: transform 0.2s ease, box-shadow 0.2s ease; - border: 1px solid #e0e0e0; -} - -.series-books article:hover { - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0,0,0,0.1); -} - -.series-info { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - color: white; - padding: 1rem; - border-radius: 8px; - margin-bottom: 2rem; -} - -.series-badge { - display: inline-block; - background: #007bff; - color: white; - padding: 0.2rem 0.5rem; - border-radius: 12px; - font-size: 0.8rem; - margin-left: 0.5rem; -} - -/* Dashboard */ -.dashboard-buttons { - display: flex; - gap: 10px; - margin-top: 1rem; - flex-wrap: nowrap; -} - -.dashboard-button { - flex: 1; - text-align: center; - padding: 0.75rem 0.5rem; - text-decoration: none; - border: 1px solid var(--secondary); - border-radius: 4px; - background: var(--secondary); - color: var(--secondary-inverse); - font-size: 0.9rem; - transition: all 0.3s ease; - min-height: 44px; - display: flex; - align-items: center; - justify-content: center; - white-space: nowrap; -} - -.dashboard-button:hover { - opacity: 0.9; - transform: translateY(-1px); -} - -.dashboard-button.new { - background: var(--primary); - border-color: var(--primary); - color: var(--primary-inverse); - flex: 0.7; -} - -.stats-list { - margin-top: 1rem; -} - -.stats-list p { - margin: 0.5rem 0; - padding: 0.3rem 0; - border-bottom: 1px solid #f0f0f0; -} - -.stats-list p:last-child { - border-bottom: none; -} - -.series-stats { - margin-top: 1rem; - padding: 1rem; - background: #f8f9fa; - border-radius: 5px; - border-left: 4px solid #6f42c1; -} - -.series-stats p { - margin: 0.5rem 0; - font-size: 0.9rem; -} - -.dashboard-section { - margin-top: 2rem; - padding-top: 1rem; - border-top: 1px solid #eee; -} - -.dashboard-item { - transition: transform 0.2s ease, box-shadow 0.2s ease; - border: 1px solid #e0e0e0; - padding: 1rem; -} - -.dashboard-item:hover { - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0,0,0,0.1); -} - -.welcome-message { - text-align: center; - padding: 3rem; - background: #f9f9f9; - border-radius: 8px; - margin-top: 2rem; -} - -.welcome-buttons { - display: flex; - gap: 1rem; - justify-content: center; - flex-wrap: wrap; - margin-top: 1.5rem; -} - -.action-buttons { - display: flex; - gap: 5px; - flex-wrap: wrap; - margin-top: 0.5rem; -} - -.action-buttons .compact-button { - flex: 1; - min-width: 80px; - text-align: center; - font-size: 0.8rem; - padding: 0.3rem 0.5rem; -} - -/* Улучшения для grid в dashboard */ -.grid { - gap: 1rem; -} - -.grid article { - margin: 0; - padding: 1.5rem; -} - -/* Адаптивность */ -@media (max-width: 768px) { - .adaptive-button { - flex: 1 1 calc(50% - 10px); - min-width: calc(50% - 10px); - font-size: 0.8rem; - padding: 10px 8px !important; - } - - .action-button { - padding: 0.6rem 1rem; - font-size: 0.85rem; - min-width: 120px; - height: 42px; - } - - .book-content { - font-size: 16px; - line-height: 1.6; - } - - .book-content h1 { - font-size: 1.6em; - } - - .book-content h2 { - font-size: 1.4em; - } - - .book-content h3 { - font-size: 1.2em; - } - - .book-content pre { - font-size: 14px; - } - - .author-books article { - flex-direction: column; - } - - .author-books .book-cover { - align-self: center; - } - - .avatar, .avatar-placeholder { - width: 120px; - height: 120px; - font-size: 2.5rem; - } - - .author-stats { - gap: 1rem; - } - - .stat-number { - font-size: 1.3em; - } - - .dashboard-buttons { - flex-direction: column; - gap: 8px; - } - - .dashboard-button { - flex: none; - width: 100%; - } - - .dashboard-button.new { - flex: none; - width: 100%; - } - - .welcome-buttons { - flex-direction: column; - align-items: center; - } - - .welcome-buttons a { - width: 100%; - max-width: 250px; - } - - .action-buttons { - flex-direction: column; - } - - .action-buttons .compact-button { - width: 100%; - } - - #fullscreen-controls { - top: 10px !important; - right: 10px !important; - } - - #fullscreen-controls button { - width: 60px !important; - height: 60px !important; - font-size: 24px !important; - border: 2px solid #ddd !important; - } - - .editor-controls button { - width: 50px !important; - height: 50px !important; - font-size: 20px !important; - } -} - -@media (max-width: 480px) { - .adaptive-button { - flex: 1 1 100%; - min-width: 100%; - } - - .action-button { - padding: 0.5rem 0.8rem; - font-size: 0.8rem; - min-width: 110px; - height: 40px; - } - - .action-buttons-container { - flex-direction: column; - width: 100%; - } - - .action-buttons-container .action-button { - width: 100%; - min-width: auto; - } - - .avatar, .avatar-placeholder { - width: 100px; - height: 100px; - font-size: 2rem; - } - - .author-stats { - flex-direction: column; - gap: 0.5rem; - } - - .dashboard-item { - padding: 0.8rem; - } - - .dashboard-button { - font-size: 0.85rem; - padding: 0.6rem 0.4rem; - } - - .welcome-message { - padding: 2rem 1rem; - } - - #fullscreen-controls button { - width: 55px !important; - height: 55px !important; - font-size: 22px !important; - } -} - -@media (min-width: 769px) { - #fullscreen-controls { - top: 15px !important; - right: 15px !important; - } - - #fullscreen-controls button { - width: 50px !important; - height: 50px !important; - font-size: 20px !important; - } -} - -/* Полноэкранные режимы редактора */ -#content.mobile-fullscreen { - position: fixed !important; - top: 50px !important; - left: 0 !important; - width: 100vw !important; - height: calc(100vh - 100px) !important; - z-index: 9998 !important; - background-color: white !important; - border: 2px solid #007bff !important; - border-radius: 0 !important; - font-size: 18px !important; - padding: 15px !important; - margin: 0 !important; - box-sizing: border-box !important; - resize: none !important; - box-shadow: none !important; - overflow-y: auto !important; - -webkit-overflow-scrolling: touch !important; -} - -#content.desktop-fullscreen { - position: fixed !important; - top: 5vh !important; - left: 5vw !important; - width: 90vw !important; - height: 90vh !important; - z-index: 9998 !important; - background-color: white !important; - border: 2px solid #007bff !important; - border-radius: 8px !important; - font-size: 16px !important; - padding: 20px !important; - margin: 0 !important; - box-sizing: border-box !important; - resize: none !important; - box-shadow: 0 0 20px rgba(0,0,0,0.3) !important; +/* Базовые стили */ +h1, h2, h3, h4, h5, h6 { + margin-bottom: 1rem; +} + +article, textarea, main.container { + margin-top: 1rem; + margin-bottom: 1rem; + padding-top: 1rem; + padding-bottom: 1rem; +} + +article > header, article > footer { + margin-top: 0.1rem; + margin-bottom: 0.1rem; + padding-top: 0.2rem; + padding-bottom: 0.2rem; +} + +input:not([type="checkbox"], [type="radio"]), select { + padding: 4px; + height: 2em; +} + +/* Убираем конфликтующие стили Pico CSS */ +article header, article footer { + margin: 0; + padding: 0; +} + +article > header, article > footer { + margin: 0; + padding: 0; +} + +/* Уведомления */ +.alert { + padding: 1rem; + margin: 1rem 0; + border-radius: 5px; +} + +.alert-error { + background: #ffebee; + color: #c62828; + border: 1px solid #ffcdd2; +} + +.alert-success { + background: #e8f5e8; + color: #2e7d32; + border: 1px solid #c8e6c9; +} + +/* Кнопки */ +.compact-button { + padding: 3px 8px !important; + font-size: 0.85rem; + text-decoration: none; + display: inline-flex !important; + align-items: center; + justify-content: center; + border: 1px solid var(--secondary); + border-radius: 4px; + background: var(--secondary); + color: var(--secondary-inverse); + cursor: pointer; + height: 28px !important; + box-sizing: border-box; + line-height: 1 !important; + vertical-align: middle; +} + +.compact-button:hover { + opacity: 0.9; +} + +.button-group { + display: flex; + gap: 5px; + margin-bottom: 1rem; +} + +.button-group button, +.button-group a[role="button"] { + flex: 1; + padding: 0.5rem; + height: 38px; + display: flex; + align-items: center; + justify-content: center; + text-decoration: none; + box-sizing: border-box; +} + +.button-group .delete-btn { + background: #ff4444 !important; + border-color: #ff4444 !important; + color: white !important; +} + +.green-btn { + background: #449944 !important; + border-color: #449944 !important; + color: white !important; +} + +.green-btn:hover { + background: #44bb44 !important; + border-color: #44bb44 !important; + color: white !important; +} + +.profile-buttons { + display: flex; + gap: 10px; + align-items: stretch; +} + +.profile-button { + flex: 1; + padding: 0.75rem; + display: flex; + align-items: center; + justify-content: center; + text-decoration: none; + border: 1px solid; + border-radius: 4px; + font-size: 1rem; + cursor: pointer; + box-sizing: border-box; + height: 62px; + min-height: 44px; +} + +.profile-button.primary { + background: var(--primary); + border-color: var(--primary); + color: var(--primary-inverse); +} + +.profile-button.secondary { + background: var(--secondary); + border-color: var(--secondary); + color: var(--secondary-inverse); +} + +.adaptive-button { + padding: 8px 12px !important; + font-size: 0.85rem; + text-decoration: none; + display: inline-flex; + align-items: center; + justify-content: center; + border: 1px solid var(--secondary); + border-radius: 4px; + background: var(--secondary); + color: var(--secondary-inverse); + cursor: pointer; + box-sizing: border-box; + text-align: center; + white-space: normal; + word-break: break-word; + min-height: 44px; + flex: 1; + min-width: 120px; + margin: 2px; +} + +.adaptive-button:hover { + opacity: 0.9; +} + +.primary.adaptive-button { + background: var(--primary); + border-color: var(--primary); + color: var(--primary-inverse); +} + +.action-button { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.75rem 1.5rem; + font-size: 0.9rem; + text-decoration: none; + border: 1px solid; + border-radius: 4px; + cursor: pointer; + box-sizing: border-box; + height: 44px; + min-width: 140px; + white-space: nowrap; + transition: all 0.3s ease; + text-align: center; +} + +.action-button.primary { + background: #007bff; + border-color: #007bff; + color: #fff; +} + +.action-button.primary:hover { + opacity: 0.9; +} + +.action-button.delete { + margin-top: 1rem; + background: #ff4444; + border-color: #ff4444; + color: white; +} + +.action-button.delete:hover { + background: #dd3333; + border-color: #dd3333; + color: white; +} + +/* Таблицы */ +.compact-table { + width: 100%; + font-size: 0.9rem; + border-collapse: collapse; +} + +.compact-table th, +.compact-table td { + padding: 6px 8px; + border-bottom: 1px solid #eee; +} + +.compact-table th { + background: #f5f5f5; + font-weight: bold; +} + +/* Markdown редактор */ +#content { + transition: all 0.3s ease; + border: 1px solid #ddd; + border-radius: 4px; + padding: 12px; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 14px; + line-height: 1.5; + min-height: 400px; + color: #111; + background-color: #fff; + resize: vertical; +} + +#content:focus { + border-color: #007bff; + box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25); + outline: none; + color: #111; + background-color: #fff; +} + +/* Кастомный скроллбар для редактора */ +#content::-webkit-scrollbar { + width: 8px; +} + +#content::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 4px; +} + +#content::-webkit-scrollbar-thumb { + background: #c1c1c1; + border-radius: 4px; +} + +#content::-webkit-scrollbar-thumb:hover { + background: #a8a8a8; +} + +#content { + scrollbar-width: thin; + scrollbar-color: #c1c1c1 #f1f1f1; +} + +/* Элементы управления редактором */ +.editor-controls { + position: sticky !important; + top: 10px !important; + right: 10px !important; + z-index: 100 !important; + display: flex; + gap: 5px; + justify-content: flex-end; + margin-bottom: 10px; +} + +.editor-controls button { + width: 40px !important; + height: 40px !important; + border-radius: 50% !important; + border: 1px solid #ddd !important; + background: white !important; + cursor: pointer !important; + font-size: 16px !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + box-shadow: 0 2px 5px rgba(0,0,0,0.2) !important; + transition: all 0.3s ease !important; + color: #333333 !important; +} + +.editor-controls button:hover { + transform: scale(1.1) !important; + background-color: #f8f9fa !important; + box-shadow: 0 4px 8px rgba(0,0,0,0.3) !important; +} + +/* Полноэкранный режим редактора */ +#fullscreen-controls { + position: fixed !important; + z-index: 9999 !important; + display: flex !important; + gap: 5px !important; +} + +#fullscreen-controls button { + width: 45px !important; + height: 45px !important; + border-radius: 50% !important; + border: 1px solid #ddd !important; + background: white !important; + cursor: pointer !important; + font-size: 18px !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + box-shadow: 0 2px 5px rgba(0,0,0,0.3) !important; + transition: all 0.3s ease !important; + color: #333333 !important; +} + +#fullscreen-controls button:hover { + transform: scale(1.1) !important; + background-color: #f8f9fa !important; + box-shadow: 0 4px 8px rgba(0,0,0,0.4) !important; +} + +/* Стили для отображения контента книг */ +.book-content { + line-height: 1.7; + font-family: Georgia, serif; +} + +.book-content h1 { + font-size: 2em; + margin-top: 2rem; + margin-bottom: 1rem; + border-bottom: 2px solid #eee; + padding-bottom: 0.5rem; +} + +.book-content h2 { + font-size: 1.6em; + margin-top: 1.5rem; + margin-bottom: 1rem; + border-bottom: 1px solid #eee; + padding-bottom: 0.3rem; +} + +.book-content h3 { + font-size: 1.3em; + margin-top: 1.2rem; + margin-bottom: 0.8rem; +} + +.book-content p { + margin-bottom: 1rem; + text-align: justify; +} + +.book-content blockquote { + border-left: 4px solid #007bff; + padding-left: 1.5rem; + margin-left: 0; + margin-right: 0; + color: #555; + font-style: italic; + background: #f8f9fa; + padding: 1rem; + border-radius: 0 5px 5px 0; +} + +.book-content code { + background: #f5f5f5; + padding: 2px 6px; + border-radius: 3px; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 0.9em; +} + +.book-content pre { + background: #2d2d2d; + color: #f8f8f2; + padding: 1rem; + border-radius: 5px; + overflow-x: auto; + border-left: 4px solid #007bff; +} + +.book-content pre code { + background: none; + padding: 0; + color: inherit; +} + +.book-content ul, .book-content ol { + margin-bottom: 1rem; + padding-left: 2rem; +} + +.book-content li { + margin-bottom: 0.3rem; +} + +.book-content table { + width: 100%; + border-collapse: collapse; + margin-bottom: 1rem; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); +} + +.book-content th, .book-content td { + border: 1px solid #ddd; + padding: 10px 12px; + text-align: left; +} + +.book-content th { + background: #f5f5f5; + font-weight: bold; +} + +.book-content tr:nth-child(even) { + background: #f9f9f9; +} + +.dialogue { + margin-left: 2rem; + font-style: italic; + color: #2c5aa0; +} + +/* Обложки и медиа */ +.book-cover { + transition: transform 0.3s ease; +} + +.book-cover:hover { + transform: scale(1.05); +} + +.cover-placeholder { + width: 120px; + height: 160px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: 2rem; + margin: 0 auto 1rem; +} + +/* Аватарки и профиль */ +.avatar-container { + text-align: center; + margin-bottom: 1.5rem; +} + +.avatar { + width: 150px; + height: 150px; + border-radius: 50%; + border: 3px solid #007bff; + object-fit: cover; +} + +.avatar-placeholder { + width: 150px; + height: 150px; + border-radius: 50%; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: 3rem; + margin: 0 auto; +} + +.author-bio { + background: #f8f9fa; + padding: 1.5rem; + border-radius: 8px; + margin: 1rem 0; + line-height: 1.6; +} + +.author-bio h1, .author-bio h2, .author-bio h3 { + margin-top: 1rem; + margin-bottom: 0.5rem; +} + +.author-bio p { + margin-bottom: 1rem; +} + +.author-bio ul, .author-bio ol { + margin-bottom: 1rem; + padding-left: 2rem; +} + +.author-bio blockquote { + border-left: 4px solid #007bff; + padding-left: 1rem; + margin-left: 0; + color: #555; + font-style: italic; +} + +/* Статистика */ +.author-stats { + display: flex; + justify-content: center; + gap: 2rem; + flex-wrap: wrap; + margin: 1rem 0; +} + +.stat-item { + text-align: center; +} + +.stat-number { + font-size: 1.5em; + font-weight: bold; + color: #007bff; +} + +.stat-label { + font-size: 0.9em; + color: #666; +} + +/* Серии книг */ +.series-books article { + transition: transform 0.2s ease, box-shadow 0.2s ease; + border: 1px solid #e0e0e0; +} + +.series-books article:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0,0,0,0.1); +} + +.series-info { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 1rem; + border-radius: 8px; + margin-bottom: 2rem; +} + +.series-badge { + display: inline-block; + background: #007bff; + color: white; + padding: 0.2rem 0.5rem; + border-radius: 12px; + font-size: 0.8rem; + margin-left: 0.5rem; +} + +/* Dashboard */ +.dashboard-buttons { + display: flex; + gap: 10px; + margin-top: 1rem; + flex-wrap: nowrap; +} + +.dashboard-button { + flex: 1; + text-align: center; + padding: 0.75rem 0.5rem; + text-decoration: none; + border: 1px solid var(--secondary); + border-radius: 4px; + background: var(--secondary); + color: var(--secondary-inverse); + font-size: 0.9rem; + transition: all 0.3s ease; + min-height: 44px; + display: flex; + align-items: center; + justify-content: center; + white-space: nowrap; +} + +.dashboard-button:hover { + opacity: 0.9; + transform: translateY(-1px); +} + +.dashboard-button.new { + background: var(--primary); + border-color: var(--primary); + color: var(--primary-inverse); + flex: 0.7; +} + +.stats-list { + margin-top: 1rem; +} + +.stats-list p { + margin: 0.5rem 0; + padding: 0.3rem 0; + border-bottom: 1px solid #f0f0f0; +} + +.stats-list p:last-child { + border-bottom: none; +} + +.series-stats { + margin-top: 1rem; + padding: 1rem; + background: #f8f9fa; + border-radius: 5px; + border-left: 4px solid #6f42c1; +} + +.series-stats p { + margin: 0.5rem 0; + font-size: 0.9rem; +} + +.dashboard-section { + margin-top: 2rem; + padding-top: 1rem; + border-top: 1px solid #eee; +} + +.dashboard-item { + transition: transform 0.2s ease, box-shadow 0.2s ease; + border: 1px solid #e0e0e0; + padding: 1rem; +} + +.dashboard-item:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0,0,0,0.1); +} + +.welcome-message { + text-align: center; + padding: 3rem; + background: #f9f9f9; + border-radius: 8px; + margin-top: 2rem; +} + +.welcome-buttons { + display: flex; + gap: 1rem; + justify-content: center; + flex-wrap: wrap; + margin-top: 1.5rem; +} + +.action-buttons { + display: flex; + gap: 5px; + flex-wrap: wrap; + margin-top: 0.5rem; +} + +.action-buttons .compact-button { + flex: 1; + min-width: 80px; + text-align: center; + font-size: 0.8rem; + padding: 0.3rem 0.5rem; +} + +/* Улучшения для grid в dashboard */ +.grid { + gap: 1rem; +} + +.grid article { + margin: 0; + padding: 1.5rem; +} + +/* Адаптивность */ +@media (max-width: 768px) { + .adaptive-button { + flex: 1 1 calc(50% - 10px); + min-width: calc(50% - 10px); + font-size: 0.8rem; + padding: 10px 8px !important; + } + + .action-button { + padding: 0.6rem 1rem; + font-size: 0.85rem; + min-width: 120px; + height: 42px; + } + + .book-content { + font-size: 16px; + line-height: 1.6; + } + + .book-content h1 { + font-size: 1.6em; + } + + .book-content h2 { + font-size: 1.4em; + } + + .book-content h3 { + font-size: 1.2em; + } + + .book-content pre { + font-size: 14px; + } + + .author-books article { + flex-direction: column; + } + + .author-books .book-cover { + align-self: center; + } + + .avatar, .avatar-placeholder { + width: 120px; + height: 120px; + font-size: 2.5rem; + } + + .author-stats { + gap: 1rem; + } + + .stat-number { + font-size: 1.3em; + } + + .dashboard-buttons { + flex-direction: column; + gap: 8px; + } + + .dashboard-button { + flex: none; + width: 100%; + } + + .dashboard-button.new { + flex: none; + width: 100%; + } + + .welcome-buttons { + flex-direction: column; + align-items: center; + } + + .welcome-buttons a { + width: 100%; + max-width: 250px; + } + + .action-buttons { + flex-direction: column; + } + + .action-buttons .compact-button { + width: 100%; + } + + #fullscreen-controls { + top: 10px !important; + right: 10px !important; + } + + #fullscreen-controls button { + width: 60px !important; + height: 60px !important; + font-size: 24px !important; + border: 2px solid #ddd !important; + } + + .editor-controls button { + width: 50px !important; + height: 50px !important; + font-size: 20px !important; + } +} + +@media (max-width: 480px) { + .adaptive-button { + flex: 1 1 100%; + min-width: 100%; + } + + .action-button { + padding: 0.5rem 0.8rem; + font-size: 0.8rem; + min-width: 110px; + height: 40px; + } + + .action-buttons-container { + flex-direction: column; + width: 100%; + } + + .action-buttons-container .action-button { + width: 100%; + min-width: auto; + } + + .avatar, .avatar-placeholder { + width: 100px; + height: 100px; + font-size: 2rem; + } + + .author-stats { + flex-direction: column; + gap: 0.5rem; + } + + .dashboard-item { + padding: 0.8rem; + } + + .dashboard-button { + font-size: 0.85rem; + padding: 0.6rem 0.4rem; + } + + .welcome-message { + padding: 2rem 1rem; + } + + #fullscreen-controls button { + width: 55px !important; + height: 55px !important; + font-size: 22px !important; + } +} + +@media (min-width: 769px) { + #fullscreen-controls { + top: 15px !important; + right: 15px !important; + } + + #fullscreen-controls button { + width: 50px !important; + height: 50px !important; + font-size: 20px !important; + } +} + +/* Полноэкранные режимы редактора */ +#content.mobile-fullscreen { + position: fixed !important; + top: 50px !important; + left: 0 !important; + width: 100vw !important; + height: calc(100vh - 100px) !important; + z-index: 9998 !important; + background-color: white !important; + border: 2px solid #007bff !important; + border-radius: 0 !important; + font-size: 18px !important; + padding: 15px !important; + margin: 0 !important; + box-sizing: border-box !important; + resize: none !important; + box-shadow: none !important; + overflow-y: auto !important; + -webkit-overflow-scrolling: touch !important; +} + +#content.desktop-fullscreen { + position: fixed !important; + top: 5vh !important; + left: 5vw !important; + width: 90vw !important; + height: 90vh !important; + z-index: 9998 !important; + background-color: white !important; + border: 2px solid #007bff !important; + border-radius: 8px !important; + font-size: 16px !important; + padding: 20px !important; + margin: 0 !important; + box-sizing: border-box !important; + resize: none !important; + box-shadow: 0 0 20px rgba(0,0,0,0.3) !important; +} + +/* Стили для TinyMCE редактора */ +.tox-tinymce { + border-radius: 4px !important; + border: 1px solid #ddd !important; +} + +.tox-tinymce:focus { + border-color: #007bff !important; + box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25) !important; +} + +/* Адаптивность для TinyMCE */ +@media (max-width: 768px) { + .tox-tinymce { + border-radius: 0 !important; + } + + .tox-toolbar { + flex-wrap: wrap !important; + } +} + +.alert-info { + background: #d1ecf1; + color: #0c5460; + border: 1px solid #bee5eb; +} + +.alert-warning { + background: #fff3cd; + color: #856404; + border: 1px solid #ffeaa7; } \ No newline at end of file diff --git a/author.php b/author.php index c544d58..79dc9eb 100755 --- a/author.php +++ b/author.php @@ -1,192 +1,192 @@ -Неверный запрос"; - include 'views/footer.php'; - exit; -} - -$stmt = $pdo->prepare("SELECT id, username, display_name, avatar, bio FROM users WHERE id = ?"); -$stmt->execute([$author_id]); -$author = $stmt->fetch(PDO::FETCH_ASSOC); - -if (!$author) { - http_response_code(404); - echo "

Автор не найден

"; - include 'views/footer.php'; - exit; -} - -$bookModel = new Book($pdo); -$books = $bookModel->findByUser($author_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; -} - -$page_title = ($author['display_name'] ?: $author['username']) . ' — публичная страница'; -include 'views/header.php'; -?> - -
-
-
- -
- - <?= e($author['display_name'] ?: $author['username']) ?> - -
- -
- -
- -

- - - -
- text($author['bio']) ?> -
- - - -
-
-
-
Книг
-
-
-
-
Глав
-
-
-
-
Слов
-
-
-
- -

Публикации автора

- - -
-

У этого автора пока нет опубликованных книг

-

Следите за обновлениями, скоро здесь появятся новые произведения!

-
- -
- -
- -
- <?= e($book['title']) ?> -
- -
-
📚
-
- - -
-

- - -

- - - -

- - - getBookStats($book['id'], true); - $chapter_count = $book_stats['chapter_count'] ?? 0; - $word_count = $book_stats['total_words'] ?? 0; - ?> - -
- - Читать книгу - - - - Глав: | Слов: - -
-
-
- -
- - -
-

- Страница автора создана в • - -

-
-
-
- - - +Неверный запрос"; + include 'views/footer.php'; + exit; +} + +$stmt = $pdo->prepare("SELECT id, username, display_name, avatar, bio FROM users WHERE id = ?"); +$stmt->execute([$author_id]); +$author = $stmt->fetch(PDO::FETCH_ASSOC); + +if (!$author) { + http_response_code(404); + echo "

Автор не найден

"; + include 'views/footer.php'; + exit; +} + +$bookModel = new Book($pdo); +$books = $bookModel->findByUser($author_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; +} + +$page_title = ($author['display_name'] ?: $author['username']) . ' — публичная страница'; +include 'views/header.php'; +?> + +
+
+
+ +
+ + <?= e($author['display_name'] ?: $author['username']) ?> + +
+ +
+ +
+ +

+ + + +
+ text($author['bio']) ?> +
+ + + +
+
+
+
Книг
+
+
+
+
Глав
+
+
+
+
Слов
+
+
+
+ +

Публикации автора

+ + +
+

У этого автора пока нет опубликованных книг

+

Следите за обновлениями, скоро здесь появятся новые произведения!

+
+ +
+ +
+ +
+ <?= e($book['title']) ?> +
+ +
+
📚
+
+ + +
+

+ + +

+ + + +

+ + + getBookStats($book['id'], true); + $chapter_count = $book_stats['chapter_count'] ?? 0; + $word_count = $book_stats['total_words'] ?? 0; + ?> + +
+ + Читать книгу + + + + Глав: | Слов: + +
+
+
+ +
+ + +
+

+ Страница автора создана в • + +

+
+
+
+ + + \ No newline at end of file diff --git a/book_delete.php b/book_delete.php index 6047d86..0e7243a 100755 --- a/book_delete.php +++ b/book_delete.php @@ -1,47 +1,47 @@ -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'); +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 index f9028d0..6e9f5af 100755 --- a/book_delete_all.php +++ b/book_delete_all.php @@ -1,45 +1,45 @@ -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'); +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 index 787fb2a..f06cc70 100755 --- a/book_edit.php +++ b/book_edit.php @@ -1,346 +1,413 @@ -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'] ?? ''); - - 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 - ]; - $data['published'] = isset($_POST['published']) ? 1 : 0; - - // Обработка загрузки обложки - 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); - $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'; -?> - -
- - -
- - - - - - - - - - - -
- - - -
-

Текущая обложка:

- Обложка -
- -
-
- - - - - Разрешены: JPG, PNG, GIF, WebP. Максимальный размер: 5MB. - Рекомендуемый размер: 300×450 пикселей. - - - -
- ❌ -
- -
- - - - -
- -
-
- -
- -
-
- -
- - - -
- - -
-

Публичная ссылка для чтения

-

Отправьте эту ссылку читателям для просмотра опубликованных глав:

- -
- - - - -
- - - -
-
- -

- Примечание: В публичном просмотре отображаются только главы со статусом "Опубликована" -

-
- - - - - -
-

Экспорт книги

-

Экспортируйте книгу в различные форматы:

- -
- - 📄 PDF - - - 📝 DOCX - - - 🌐 HTML - - - 📄 TXT - -
- -

- Примечание: Экспортируются все главы книги (включая черновики) -

-
- - - -
-

Главы этой книги

- - 📑 Все главы - -   - - ✏️ Добавить главу - - prepare("SELECT * FROM chapters WHERE book_id = ? ORDER BY sort_order, created_at"); - $stmt->execute([$book_id]); - $chapters = $stmt->fetchAll(); - - if ($chapters): ?> -
- - - - - - - - - - - - - - - - - - - -
НазваниеСтатусСловДействия
- - - - - - Редактировать - -
-
- -
-

В этой книге пока нет глав.

- - ✏️ Добавить первую главу - -
- -
- - +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'; +?> + +
+ + +
+ + + + + + + + + + + + + + + + + + +
+ + + +
+

Текущая обложка:

+ Обложка +
+ +
+
+ + + + + Разрешены: JPG, PNG, GIF, WebP. Максимальный размер: 5MB. + Рекомендуемый размер: 300×450 пикселей. + + + +
+ ❌ +
+ +
+ + + + +
+ +
+
+ +
+ +
+
+ +
+ + + +

+ Если контент глав отображается неправильно после смены редактора, можно нормализовать его структуру. +

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

Публичная ссылка для чтения

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

+ Примечание: В публичном просмотре отображаются только главы со статусом "Опубликована" +

+
+ + +
+ + + + + +
+

Экспорт книги

+

Экспортируйте книгу в различные форматы:

+ +
+ + 📄 PDF + + + 📝 DOCX + + + 🌐 HTML + + + 📄 TXT + +
+ +

+ Примечание: Экспортируются все главы книги (включая черновики) +

+
+ + + +
+

Главы этой книги

+ + 📑 Все главы + +   + + ✏️ Добавить главу + + prepare("SELECT * FROM chapters WHERE book_id = ? ORDER BY sort_order, created_at"); + $stmt->execute([$book_id]); + $chapters = $stmt->fetchAll(); + + if ($chapters): ?> +
+ + + + + + + + + + + + + + + + + + + +
НазваниеСтатусСловДействия
+ + + + + + Редактировать + +
+
+ +
+

В этой книге пока нет глав.

+ + ✏️ Добавить первую главу + +
+ +
+ + \ No newline at end of file diff --git a/books.php b/books.php index 29ffdb1..64bff7a 100755 --- a/books.php +++ b/books.php @@ -1,156 +1,156 @@ -findByUser($user_id); - -$page_title = "Мои книги"; -include 'views/header.php'; -?> - -

Мои книги

- - -
- - -
- - - -
- - -
- - -
-

Всего книг:

-
- ➕ Новая книга - - - -
-
- - -
-

У вас пока нет книг

-

Создайте свою первую книгу и начните писать!

- 📖 Создать первую книгу -
- -
- -
- -
- <?= e($book['title']) ?> -
- -
-

- - prepare("SELECT title FROM series WHERE id = ?"); - $series_stmt->execute([$book['series_id']]); - $series_title = $series_stmt->fetch()['title'] ?? ''; - ?> - -
- - 📚 Серия: - - (Книга ) - - -
- - - -

- - - -
- - -

- - -
-
- - Глав: | - Слов: - -
- -
-
- -
- - -

Удалить все книги?

-

Это действие удалит все ваши книги и все связанные с ними главы. Это действие нельзя отменить.

- -
- - - -
-
- - - - +findByUser($user_id); + +$page_title = "Мои книги"; +include 'views/header.php'; +?> + +

Мои книги

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

Всего книг:

+
+ ➕ Новая книга + + + +
+
+ + +
+

У вас пока нет книг

+

Создайте свою первую книгу и начните писать!

+ 📖 Создать первую книгу +
+ +
+ +
+ +
+ <?= e($book['title']) ?> +
+ +
+

+ + prepare("SELECT title FROM series WHERE id = ?"); + $series_stmt->execute([$book['series_id']]); + $series_title = $series_stmt->fetch()['title'] ?? ''; + ?> + +
+ + 📚 Серия: + + (Книга ) + + +
+ + + +

+ + + +
+ + +

+ + +
+
+ + Глав: | + Слов: + +
+ +
+
+ +
+ + +

Удалить все книги?

+

Это действие удалит все ваши книги и все связанные с ними главы. Это действие нельзя отменить.

+ +
+ + + +
+
+ + + + \ No newline at end of file diff --git a/chapter_delete.php b/chapter_delete.php index dfe5e03..5073cca 100755 --- a/chapter_delete.php +++ b/chapter_delete.php @@ -1,43 +1,43 @@ -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"); +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 index dc1c22d..22a54da 100755 --- a/chapter_edit.php +++ b/chapter_edit.php @@ -1,291 +1,386 @@ -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'; -?> - -
-
- findByBook($book_id); - $current_index = null; - - // Находим индекс текущей главы - foreach ($chapters as $index => $chap) { - if ($chap['id'] == $chapter_id) { - $current_index = $index; - break; - } - } - - if ($current_index !== null && $current_index > 0): - $prev_chapter = $chapters[$current_index - 1]; - ?> - - ⬅️ Предыдущая: - - - - - - Следующая: ➡️ - - -
-
- - -

-

Книга:

- - -
- - -
- - -
- - -
- - - - - - - - - -
- Статистика: слов - | Обновлено: -
- -
-
- -
- - - - ❌ Отмена - - - -
- - - - - -
- - ➕ Новая глава - - -
- - - -
-
- - - -
-
- findByBook($book_id); - $current_index = null; - - // Находим индекс текущей главы - foreach ($chapters as $index => $chap) { - if ($chap['id'] == $chapter_id) { - $current_index = $index; - break; - } - } - - if ($current_index !== null && $current_index > 0): - $prev_chapter = $chapters[$current_index - 1]; - ?> - - ⬅️ Предыдущая: - - - - - - Следующая: ➡️ - - -
-
- - - - - - - - - +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'; +?> + +
+
+ findByBook($book_id); + $current_index = null; + + // Находим индекс текущей главы + foreach ($chapters as $index => $chap) { + if ($chap['id'] == $chapter_id) { + $current_index = $index; + break; + } + } + + if ($current_index !== null && $current_index > 0): + $prev_chapter = $chapters[$current_index - 1]; + ?> + + ⬅️ Предыдущая: + + + + + + Следующая: ➡️ + + +
+
+ + +

+

Книга:

+ + +
+ + +
+ + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ + + + ❌ Отмена + + + +
+ + + + + +
+ + ➕ Новая глава + + +
+ + + +
+
+ + + +
+
+ findByBook($book_id); + $current_index = null; + + // Находим индекс текущей главы + foreach ($chapters as $index => $chap) { + if ($chap['id'] == $chapter_id) { + $current_index = $index; + break; + } + } + + if ($current_index !== null && $current_index > 0): + $prev_chapter = $chapters[$current_index - 1]; + ?> + + ⬅️ Предыдущая: + + + + + + Следующая: ➡️ + + +
+
+ + + + + + \ No newline at end of file diff --git a/chapters.php b/chapters.php index 95416ff..ea35053 100755 --- a/chapters.php +++ b/chapters.php @@ -1,120 +1,120 @@ -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'; -?> - -
-

Главы книги:

-
- ➕ Новая глава - ✏️ Редактировать книгу - 👁️ Просмотреть книгу - 📚 Все книги -
-
- - -
- - -
- - - -
- - -
- - - -
-

В этой книге пока нет глав

-

Создайте первую главу для вашей книги

- 📝 Создать первую главу -
- -
- - - - - - - - - - - - - $chapter): ?> - - - - - - - - - - -
Название главыСтатусСловОбновленоДействия
- - -
- -
- - - - - - -
- - ✏️ - -
- - - -
-
-
-
- -
- Статистика: - Всего глав: | - Всего слов: | - Опубликовано: -
- - +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'; +?> + +
+

Главы книги:

+
+ ➕ Новая глава + ✏️ Редактировать книгу + 👁️ Просмотреть книгу + 📚 Все книги +
+
+ + +
+ + +
+ + + +
+ + +
+ + + +
+

В этой книге пока нет глав

+

Создайте первую главу для вашей книги

+ 📝 Создать первую главу +
+ +
+ + + + + + + + + + + + + $chapter): ?> + + + + + + + + + + +
Название главыСтатусСловОбновленоДействия
+ + +
+ +
+ + + + + + +
+ + ✏️ + +
+ + + +
+
+
+
+ +
+ Статистика: + Всего глав: | + Всего слов: | + Опубликовано: +
+ + \ No newline at end of file diff --git a/composer.json b/composer.json index a7d4838..95734d5 100755 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ -{ - "require": { - "phpoffice/phpword": "^1.0", - "tecnickcom/tcpdf": "^6.6" - } -} +{ + "require": { + "phpoffice/phpword": "^1.0", + "tecnickcom/tcpdf": "^6.6" + } +} diff --git a/composer.lock b/composer.lock index 9b97242..b77f6d9 100755 --- a/composer.lock +++ b/composer.lock @@ -1,289 +1,289 @@ -{ - "_readme": [ - "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", - "This file is @generated automatically" - ], - "content-hash": "493a3be12648bbe702ed126df05ead04", - "packages": [ - { - "name": "cybermonde/odtphp", - "version": "v1.7", - "source": { - "type": "git", - "url": "https://github.com/cybermonde/odtphp.git", - "reference": "23aba70923ca3c07af15a5600f4072751c1b4a36" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/cybermonde/odtphp/zipball/23aba70923ca3c07af15a5600f4072751c1b4a36", - "reference": "23aba70923ca3c07af15a5600f4072751c1b4a36", - "shasum": "" - }, - "require": { - "php": ">=5.2.4" - }, - "type": "library", - "autoload": { - "classmap": [ - "library" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "GPL" - ], - "description": "ODT document generator", - "homepage": "https://github.com/cybermonde/odtphp", - "keywords": [ - "odt", - "php" - ], - "support": { - "issues": "https://github.com/cybermonde/odtphp/issues", - "source": "https://github.com/cybermonde/odtphp/tree/v1.7" - }, - "time": "2015-06-02T07:28:25+00:00" - }, - { - "name": "phpoffice/math", - "version": "0.3.0", - "source": { - "type": "git", - "url": "https://github.com/PHPOffice/Math.git", - "reference": "fc31c8f57a7a81f962cbf389fd89f4d9d06fc99a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/PHPOffice/Math/zipball/fc31c8f57a7a81f962cbf389fd89f4d9d06fc99a", - "reference": "fc31c8f57a7a81f962cbf389fd89f4d9d06fc99a", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-xml": "*", - "php": "^7.1|^8.0" - }, - "require-dev": { - "phpstan/phpstan": "^0.12.88 || ^1.0.0", - "phpunit/phpunit": "^7.0 || ^9.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "PhpOffice\\Math\\": "src/Math/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Progi1984", - "homepage": "https://lefevre.dev" - } - ], - "description": "Math - Manipulate Math Formula", - "homepage": "https://phpoffice.github.io/Math/", - "keywords": [ - "MathML", - "officemathml", - "php" - ], - "support": { - "issues": "https://github.com/PHPOffice/Math/issues", - "source": "https://github.com/PHPOffice/Math/tree/0.3.0" - }, - "time": "2025-05-29T08:31:49+00:00" - }, - { - "name": "phpoffice/phpword", - "version": "1.4.0", - "source": { - "type": "git", - "url": "https://github.com/PHPOffice/PHPWord.git", - "reference": "6d75328229bc93790b37e93741adf70646cea958" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/PHPOffice/PHPWord/zipball/6d75328229bc93790b37e93741adf70646cea958", - "reference": "6d75328229bc93790b37e93741adf70646cea958", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-gd": "*", - "ext-json": "*", - "ext-xml": "*", - "ext-zip": "*", - "php": "^7.1|^8.0", - "phpoffice/math": "^0.3" - }, - "require-dev": { - "dompdf/dompdf": "^2.0 || ^3.0", - "ext-libxml": "*", - "friendsofphp/php-cs-fixer": "^3.3", - "mpdf/mpdf": "^7.0 || ^8.0", - "phpmd/phpmd": "^2.13", - "phpstan/phpstan": "^0.12.88 || ^1.0.0", - "phpstan/phpstan-phpunit": "^1.0 || ^2.0", - "phpunit/phpunit": ">=7.0", - "symfony/process": "^4.4 || ^5.0", - "tecnickcom/tcpdf": "^6.5" - }, - "suggest": { - "dompdf/dompdf": "Allows writing PDF", - "ext-xmlwriter": "Allows writing OOXML and ODF", - "ext-xsl": "Allows applying XSL style sheet to headers, to main document part, and to footers of an OOXML template" - }, - "type": "library", - "autoload": { - "psr-4": { - "PhpOffice\\PhpWord\\": "src/PhpWord" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "LGPL-3.0-only" - ], - "authors": [ - { - "name": "Mark Baker" - }, - { - "name": "Gabriel Bull", - "email": "me@gabrielbull.com", - "homepage": "http://gabrielbull.com/" - }, - { - "name": "Franck Lefevre", - "homepage": "https://rootslabs.net/blog/" - }, - { - "name": "Ivan Lanin", - "homepage": "http://ivan.lanin.org" - }, - { - "name": "Roman Syroeshko", - "homepage": "http://ru.linkedin.com/pub/roman-syroeshko/34/a53/994/" - }, - { - "name": "Antoine de Troostembergh" - } - ], - "description": "PHPWord - A pure PHP library for reading and writing word processing documents (OOXML, ODF, RTF, HTML, PDF)", - "homepage": "https://phpoffice.github.io/PHPWord/", - "keywords": [ - "ISO IEC 29500", - "OOXML", - "Office Open XML", - "OpenDocument", - "OpenXML", - "PhpOffice", - "PhpWord", - "Rich Text Format", - "WordprocessingML", - "doc", - "docx", - "html", - "odf", - "odt", - "office", - "pdf", - "php", - "reader", - "rtf", - "template", - "template processor", - "word", - "writer" - ], - "support": { - "issues": "https://github.com/PHPOffice/PHPWord/issues", - "source": "https://github.com/PHPOffice/PHPWord/tree/1.4.0" - }, - "time": "2025-06-05T10:32:36+00:00" - }, - { - "name": "tecnickcom/tcpdf", - "version": "6.10.0", - "source": { - "type": "git", - "url": "https://github.com/tecnickcom/TCPDF.git", - "reference": "ca5b6de294512145db96bcbc94e61696599c391d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/tecnickcom/TCPDF/zipball/ca5b6de294512145db96bcbc94e61696599c391d", - "reference": "ca5b6de294512145db96bcbc94e61696599c391d", - "shasum": "" - }, - "require": { - "ext-curl": "*", - "php": ">=7.1.0" - }, - "type": "library", - "autoload": { - "classmap": [ - "config", - "include", - "tcpdf.php", - "tcpdf_barcodes_1d.php", - "tcpdf_barcodes_2d.php", - "include/tcpdf_colors.php", - "include/tcpdf_filters.php", - "include/tcpdf_font_data.php", - "include/tcpdf_fonts.php", - "include/tcpdf_images.php", - "include/tcpdf_static.php", - "include/barcodes/datamatrix.php", - "include/barcodes/pdf417.php", - "include/barcodes/qrcode.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "LGPL-3.0-or-later" - ], - "authors": [ - { - "name": "Nicola Asuni", - "email": "info@tecnick.com", - "role": "lead" - } - ], - "description": "TCPDF is a PHP class for generating PDF documents and barcodes.", - "homepage": "http://www.tcpdf.org/", - "keywords": [ - "PDFD32000-2008", - "TCPDF", - "barcodes", - "datamatrix", - "pdf", - "pdf417", - "qrcode" - ], - "support": { - "issues": "https://github.com/tecnickcom/TCPDF/issues", - "source": "https://github.com/tecnickcom/TCPDF/tree/6.10.0" - }, - "funding": [ - { - "url": "https://www.paypal.com/donate/?hosted_button_id=NZUEC5XS8MFBJ", - "type": "custom" - } - ], - "time": "2025-05-27T18:02:28+00:00" - } - ], - "packages-dev": [], - "aliases": [], - "minimum-stability": "stable", - "stability-flags": {}, - "prefer-stable": false, - "prefer-lowest": false, - "platform": {}, - "platform-dev": {}, - "plugin-api-version": "2.9.0" -} +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "493a3be12648bbe702ed126df05ead04", + "packages": [ + { + "name": "cybermonde/odtphp", + "version": "v1.7", + "source": { + "type": "git", + "url": "https://github.com/cybermonde/odtphp.git", + "reference": "23aba70923ca3c07af15a5600f4072751c1b4a36" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cybermonde/odtphp/zipball/23aba70923ca3c07af15a5600f4072751c1b4a36", + "reference": "23aba70923ca3c07af15a5600f4072751c1b4a36", + "shasum": "" + }, + "require": { + "php": ">=5.2.4" + }, + "type": "library", + "autoload": { + "classmap": [ + "library" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL" + ], + "description": "ODT document generator", + "homepage": "https://github.com/cybermonde/odtphp", + "keywords": [ + "odt", + "php" + ], + "support": { + "issues": "https://github.com/cybermonde/odtphp/issues", + "source": "https://github.com/cybermonde/odtphp/tree/v1.7" + }, + "time": "2015-06-02T07:28:25+00:00" + }, + { + "name": "phpoffice/math", + "version": "0.3.0", + "source": { + "type": "git", + "url": "https://github.com/PHPOffice/Math.git", + "reference": "fc31c8f57a7a81f962cbf389fd89f4d9d06fc99a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPOffice/Math/zipball/fc31c8f57a7a81f962cbf389fd89f4d9d06fc99a", + "reference": "fc31c8f57a7a81f962cbf389fd89f4d9d06fc99a", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xml": "*", + "php": "^7.1|^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.88 || ^1.0.0", + "phpunit/phpunit": "^7.0 || ^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "PhpOffice\\Math\\": "src/Math/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Progi1984", + "homepage": "https://lefevre.dev" + } + ], + "description": "Math - Manipulate Math Formula", + "homepage": "https://phpoffice.github.io/Math/", + "keywords": [ + "MathML", + "officemathml", + "php" + ], + "support": { + "issues": "https://github.com/PHPOffice/Math/issues", + "source": "https://github.com/PHPOffice/Math/tree/0.3.0" + }, + "time": "2025-05-29T08:31:49+00:00" + }, + { + "name": "phpoffice/phpword", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/PHPOffice/PHPWord.git", + "reference": "6d75328229bc93790b37e93741adf70646cea958" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPOffice/PHPWord/zipball/6d75328229bc93790b37e93741adf70646cea958", + "reference": "6d75328229bc93790b37e93741adf70646cea958", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-gd": "*", + "ext-json": "*", + "ext-xml": "*", + "ext-zip": "*", + "php": "^7.1|^8.0", + "phpoffice/math": "^0.3" + }, + "require-dev": { + "dompdf/dompdf": "^2.0 || ^3.0", + "ext-libxml": "*", + "friendsofphp/php-cs-fixer": "^3.3", + "mpdf/mpdf": "^7.0 || ^8.0", + "phpmd/phpmd": "^2.13", + "phpstan/phpstan": "^0.12.88 || ^1.0.0", + "phpstan/phpstan-phpunit": "^1.0 || ^2.0", + "phpunit/phpunit": ">=7.0", + "symfony/process": "^4.4 || ^5.0", + "tecnickcom/tcpdf": "^6.5" + }, + "suggest": { + "dompdf/dompdf": "Allows writing PDF", + "ext-xmlwriter": "Allows writing OOXML and ODF", + "ext-xsl": "Allows applying XSL style sheet to headers, to main document part, and to footers of an OOXML template" + }, + "type": "library", + "autoload": { + "psr-4": { + "PhpOffice\\PhpWord\\": "src/PhpWord" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-only" + ], + "authors": [ + { + "name": "Mark Baker" + }, + { + "name": "Gabriel Bull", + "email": "me@gabrielbull.com", + "homepage": "http://gabrielbull.com/" + }, + { + "name": "Franck Lefevre", + "homepage": "https://rootslabs.net/blog/" + }, + { + "name": "Ivan Lanin", + "homepage": "http://ivan.lanin.org" + }, + { + "name": "Roman Syroeshko", + "homepage": "http://ru.linkedin.com/pub/roman-syroeshko/34/a53/994/" + }, + { + "name": "Antoine de Troostembergh" + } + ], + "description": "PHPWord - A pure PHP library for reading and writing word processing documents (OOXML, ODF, RTF, HTML, PDF)", + "homepage": "https://phpoffice.github.io/PHPWord/", + "keywords": [ + "ISO IEC 29500", + "OOXML", + "Office Open XML", + "OpenDocument", + "OpenXML", + "PhpOffice", + "PhpWord", + "Rich Text Format", + "WordprocessingML", + "doc", + "docx", + "html", + "odf", + "odt", + "office", + "pdf", + "php", + "reader", + "rtf", + "template", + "template processor", + "word", + "writer" + ], + "support": { + "issues": "https://github.com/PHPOffice/PHPWord/issues", + "source": "https://github.com/PHPOffice/PHPWord/tree/1.4.0" + }, + "time": "2025-06-05T10:32:36+00:00" + }, + { + "name": "tecnickcom/tcpdf", + "version": "6.10.0", + "source": { + "type": "git", + "url": "https://github.com/tecnickcom/TCPDF.git", + "reference": "ca5b6de294512145db96bcbc94e61696599c391d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tecnickcom/TCPDF/zipball/ca5b6de294512145db96bcbc94e61696599c391d", + "reference": "ca5b6de294512145db96bcbc94e61696599c391d", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "php": ">=7.1.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "config", + "include", + "tcpdf.php", + "tcpdf_barcodes_1d.php", + "tcpdf_barcodes_2d.php", + "include/tcpdf_colors.php", + "include/tcpdf_filters.php", + "include/tcpdf_font_data.php", + "include/tcpdf_fonts.php", + "include/tcpdf_images.php", + "include/tcpdf_static.php", + "include/barcodes/datamatrix.php", + "include/barcodes/pdf417.php", + "include/barcodes/qrcode.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Nicola Asuni", + "email": "info@tecnick.com", + "role": "lead" + } + ], + "description": "TCPDF is a PHP class for generating PDF documents and barcodes.", + "homepage": "http://www.tcpdf.org/", + "keywords": [ + "PDFD32000-2008", + "TCPDF", + "barcodes", + "datamatrix", + "pdf", + "pdf417", + "qrcode" + ], + "support": { + "issues": "https://github.com/tecnickcom/TCPDF/issues", + "source": "https://github.com/tecnickcom/TCPDF/tree/6.10.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/donate/?hosted_button_id=NZUEC5XS8MFBJ", + "type": "custom" + } + ], + "time": "2025-05-27T18:02:28+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": {}, + "platform-dev": {}, + "plugin-api-version": "2.9.0" +} diff --git a/dashboard.php b/dashboard.php index 6cb14bf..841a62b 100755 --- a/dashboard.php +++ b/dashboard.php @@ -1,191 +1,191 @@ -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'; -?> - -

Добро пожаловать, !

- -
- ✏️ Редактировать профиль -
- -
- - -
-

📊 Статистика

-
-

Книг:

-

Глав:

-

Всего слов:

- 0): ?> -

Средняя глава: слов

- -
-
- - -
- - -
-

Недавние книги

-
- - - -
- - 3): ?> -
- 📚 Показать все книги () -
- -
- - - -
-

Недавние серии

-
- -
-

- - 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; - } - ?> - -

Книг: | Глав: | Слов:

- - -
- -
- - 3): ?> -
- 📖 Показать все серии () -
- -
- - - -
-

Добро пожаловать в !

-

Начните создавать свои литературные произведения

- -
- ✏️ Настроить профиль -
-
- - +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'; +?> + +

Добро пожаловать, !

+ +
+ ✏️ Редактировать профиль +
+ +
+ + +
+

📊 Статистика

+
+

Книг:

+

Глав:

+

Всего слов:

+ 0): ?> +

Средняя глава: слов

+ +
+
+ + +
+ + +
+

Недавние книги

+
+ + + +
+ + 3): ?> +
+ 📚 Показать все книги () +
+ +
+ + + +
+

Недавние серии

+
+ +
+

+ + 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; + } + ?> + +

Книг: | Глав: | Слов:

+ + +
+ +
+ + 3): ?> +
+ 📖 Показать все серии () +
+ +
+ + + +
+

Добро пожаловать в !

+

Начните создавать свои литературные произведения

+ +
+ ✏️ Настроить профиль +
+
+ + \ No newline at end of file diff --git a/export_book.php b/export_book.php index 59ea564..00fcc5b 100755 --- a/export_book.php +++ b/export_book.php @@ -1,765 +1,869 @@ -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 = null; -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); -} - -$author_name = $author_info['display_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); - $htmlContent = $Parsedown->text($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'])) { - $descriptionParagraphs = markdownToParagraphs($book['description']); - foreach ($descriptionParagraphs as $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); - - // Получаем очищенный текст и разбиваем на абзацы - $cleanContent = cleanMarkdown($chapter['content']); - $paragraphs = markdownToParagraphs($cleanContent); - - // Добавляем каждый абзац - 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 .= '' . htmlspecialchars($book['title']) . ''; - $html .= '
'; - } - - if (!empty($book['description'])) { - $html .= '
' . nl2br(htmlspecialchars($book['description'])) . '
'; - } - - // Интерактивное оглавление - if (!empty($chapters)) { - $html .= '
'; - $html .= '

Оглавление

'; - $html .= ''; - $html .= '
'; - } - - $html .= '
'; - - foreach ($chapters as $index => $chapter) { - $html .= '
'; - $html .= '
' . htmlspecialchars($chapter['title']) . '
'; - - $htmlContent = $Parsedown->text($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"; - // Ширина до 144 символов - $content .= wordwrap($book['description'], 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"; - - // Получаем очищенный текст и разбиваем на абзацы - $cleanContent = cleanMarkdown($chapter['content']); - $paragraphs = markdownToParagraphs($cleanContent); - - foreach ($paragraphs as $paragraph) { - if (!empty(trim($paragraph))) { - // Увеличиваем ширину до 144 символов - $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; -} +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 .= '' . htmlspecialchars($book['title']) . ''; + $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/install.php b/install.php index d03376d..32e2d18 100755 --- a/install.php +++ b/install.php @@ -61,6 +61,7 @@ CREATE TABLE `books` ( `updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), `share_token` varchar(32) DEFAULT NULL, `published` tinyint(1) NOT NULL DEFAULT 0, + `editor_type` ENUM('markdown', 'html') DEFAULT 'markdown', PRIMARY KEY (`id`), UNIQUE KEY `share_token` (`share_token`), KEY `user_id` (`user_id`), diff --git a/login.php b/login.php index 38ab134..8f2847e 100755 --- a/login.php +++ b/login.php @@ -1,100 +1,100 @@ -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'; -?> - -
-

Вход в систему

- - -
- -
- - - -
- - -
- - -
- - -
- - -
- -
- - -
- - -
- -
-

Нет аккаунта? Зарегистрируйтесь здесь

-
-
- +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'; +?> + +
+

Вход в систему

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

Нет аккаунта? Зарегистрируйтесь здесь

+
+
+ \ No newline at end of file diff --git a/models/Book.php b/models/Book.php index f15324f..31c1be4 100755 --- a/models/Book.php +++ b/models/Book.php @@ -1,204 +1,499 @@ -pdo = $pdo; - } - - public function findById($id) { - $stmt = $this->pdo->prepare("SELECT * FROM books WHERE id = ?"); - $stmt->execute([$id]); - return $stmt->fetch(PDO::FETCH_ASSOC); - } - - public function findByShareToken($share_token) { - $stmt = $this->pdo->prepare("SELECT * FROM books WHERE share_token = ?"); - $stmt->execute([$share_token]); - return $stmt->fetch(PDO::FETCH_ASSOC); - } - - public function findByUser($user_id, $only_published = false) { - $sql = " - SELECT b.*, - COUNT(c.id) as chapter_count, - COALESCE(SUM(c.word_count), 0) as total_words - FROM books b - LEFT JOIN chapters c ON b.id = c.book_id - WHERE b.user_id = ? - "; - if ($only_published) { - $sql .= " AND b.published = 1 "; - } - $sql .= " GROUP BY b.id ORDER BY b.created_at DESC "; - $stmt = $this->pdo->prepare($sql); - $stmt->execute([$user_id]); - return $stmt->fetchAll(PDO::FETCH_ASSOC); - } - - public function create($data) { - $share_token = bin2hex(random_bytes(16)); - $published = isset($data['published']) ? (int)$data['published'] : 0; - - $stmt = $this->pdo->prepare(" - INSERT INTO books (title, description, genre, user_id, series_id, sort_order_in_series, share_token, published) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - "); - return $stmt->execute([ - $data['title'], - $data['description'] ?? null, - $data['genre'] ?? null, - $data['user_id'], - $data['series_id'] ?? null, - $data['sort_order_in_series'] ?? null, - $share_token, - $published - ]); - } - - public function update($id, $data) { - $published = isset($data['published']) ? (int)$data['published'] : 0; - - $stmt = $this->pdo->prepare(" - UPDATE books - SET title = ?, description = ?, genre = ?, series_id = ?, sort_order_in_series = ?, published = ? - WHERE id = ? AND user_id = ? - "); - return $stmt->execute([ - $data['title'], - $data['description'] ?? null, - $data['genre'] ?? null, - $data['series_id'] ?? null, - $data['sort_order_in_series'] ?? null, - $published, - $id, - $data['user_id'] - ]); - } - - public function delete($id, $user_id) { - try { - $this->pdo->beginTransaction(); - - // Удаляем главы книги - $stmt = $this->pdo->prepare("DELETE FROM chapters WHERE book_id = ?"); - $stmt->execute([$id]); - - // Удаляем саму книгу - $stmt = $this->pdo->prepare("DELETE FROM books WHERE id = ? AND user_id = ?"); - $result = $stmt->execute([$id, $user_id]); - - $this->pdo->commit(); - return $result; - } catch (Exception $e) { - $this->pdo->rollBack(); - return false; - } - } - - public function userOwnsBook($book_id, $user_id) { - $stmt = $this->pdo->prepare("SELECT id FROM books WHERE id = ? AND user_id = ?"); - $stmt->execute([$book_id, $user_id]); - return $stmt->fetch() !== false; - } - - public function generateNewShareToken($book_id) { - $new_token = bin2hex(random_bytes(16)); - $stmt = $this->pdo->prepare("UPDATE books SET share_token = ? WHERE id = ?"); - $success = $stmt->execute([$new_token, $book_id]); - return $success ? $new_token : false; - } - - public function getPublishedChapters($book_id) { - $stmt = $this->pdo->prepare(" - SELECT * FROM chapters - WHERE book_id = ? AND status = 'published' - ORDER BY sort_order, created_at - "); - $stmt->execute([$book_id]); - return $stmt->fetchAll(PDO::FETCH_ASSOC); - } - - public function updateCover($book_id, $filename) { - $stmt = $this->pdo->prepare("UPDATE books SET cover_image = ? WHERE id = ?"); - return $stmt->execute([$filename, $book_id]); - } - - public function deleteCover($book_id) { - - $book = $this->findById($book_id); - $old_filename = $book['cover_image']; - - if ($old_filename) { - $file_path = COVERS_PATH . $old_filename; - if (file_exists($file_path)) { - unlink($file_path); - } - } - - $stmt = $this->pdo->prepare("UPDATE books SET cover_image = NULL WHERE id = ?"); - return $stmt->execute([$book_id]); - } - - public function updateSeriesInfo($book_id, $series_id, $sort_order) { - $stmt = $this->pdo->prepare("UPDATE books SET series_id = ?, sort_order_in_series = ? WHERE id = ?"); - return $stmt->execute([$series_id, $sort_order, $book_id]); - } - - public function removeFromSeries($book_id) { - $stmt = $this->pdo->prepare("UPDATE books SET series_id = NULL, sort_order_in_series = NULL WHERE id = ?"); - return $stmt->execute([$book_id]); - } - - public function findBySeries($series_id) { - $stmt = $this->pdo->prepare(" - SELECT b.* - FROM books b - WHERE b.series_id = ? - ORDER BY b.sort_order_in_series, b.created_at - "); - $stmt->execute([$series_id]); - return $stmt->fetchAll(PDO::FETCH_ASSOC); - } - - public function reorderSeriesBooks($series_id, $new_order) { - try { - $this->pdo->beginTransaction(); - - foreach ($new_order as $order => $book_id) { - $stmt = $this->pdo->prepare("UPDATE books SET sort_order_in_series = ? WHERE id = ? AND series_id = ?"); - $stmt->execute([$order + 1, $book_id, $series_id]); - } - - $this->pdo->commit(); - return true; - } catch (Exception $e) { - $this->pdo->rollBack(); - return false; - } - } - - - public function getBookStats($book_id, $only_published_chapters = false) { - $sql = " - SELECT - COUNT(c.id) as chapter_count, - COALESCE(SUM(c.word_count), 0) as total_words - FROM books b - LEFT JOIN chapters c ON b.id = c.book_id - WHERE b.id = ? - "; - - if ($only_published_chapters) { - $sql .= " AND c.status = 'published'"; - } - - $stmt = $this->pdo->prepare($sql); - $stmt->execute([$book_id]); - return $stmt->fetch(PDO::FETCH_ASSOC); - } - -} +pdo = $pdo; + } + + public function findById($id) { + $stmt = $this->pdo->prepare("SELECT * FROM books WHERE id = ?"); + $stmt->execute([$id]); + return $stmt->fetch(PDO::FETCH_ASSOC); + } + + public function findByShareToken($share_token) { + $stmt = $this->pdo->prepare("SELECT * FROM books WHERE share_token = ?"); + $stmt->execute([$share_token]); + return $stmt->fetch(PDO::FETCH_ASSOC); + } + + public function findByUser($user_id, $only_published = false) { + $sql = " + SELECT b.*, + COUNT(c.id) as chapter_count, + COALESCE(SUM(c.word_count), 0) as total_words + FROM books b + LEFT JOIN chapters c ON b.id = c.book_id + WHERE b.user_id = ? + "; + if ($only_published) { + $sql .= " AND b.published = 1 "; + } + $sql .= " GROUP BY b.id ORDER BY b.created_at DESC "; + $stmt = $this->pdo->prepare($sql); + $stmt->execute([$user_id]); + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } + + public function create($data) { + $share_token = bin2hex(random_bytes(16)); + $published = isset($data['published']) ? (int)$data['published'] : 0; + $editor_type = $data['editor_type'] ?? 'markdown'; + + $stmt = $this->pdo->prepare(" + INSERT INTO books (title, description, genre, user_id, series_id, sort_order_in_series, share_token, published, editor_type) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + "); + return $stmt->execute([ + $data['title'], + $data['description'] ?? null, + $data['genre'] ?? null, + $data['user_id'], + $data['series_id'] ?? null, + $data['sort_order_in_series'] ?? null, + $share_token, + $published, + $editor_type + ]); + } + + public function update($id, $data) { + $published = isset($data['published']) ? (int)$data['published'] : 0; + $editor_type = $data['editor_type'] ?? 'markdown'; + + $stmt = $this->pdo->prepare(" + UPDATE books + SET title = ?, description = ?, genre = ?, series_id = ?, sort_order_in_series = ?, published = ?, editor_type = ? + WHERE id = ? AND user_id = ? + "); + return $stmt->execute([ + $data['title'], + $data['description'] ?? null, + $data['genre'] ?? null, + $data['series_id'] ?? null, + $data['sort_order_in_series'] ?? null, + $published, + $editor_type, + $id, + $data['user_id'] + ]); + } + + + public function delete($id, $user_id) { + try { + $this->pdo->beginTransaction(); + + // Удаляем главы книги + $stmt = $this->pdo->prepare("DELETE FROM chapters WHERE book_id = ?"); + $stmt->execute([$id]); + + // Удаляем саму книгу + $stmt = $this->pdo->prepare("DELETE FROM books WHERE id = ? AND user_id = ?"); + $result = $stmt->execute([$id, $user_id]); + + $this->pdo->commit(); + return $result; + } catch (Exception $e) { + $this->pdo->rollBack(); + return false; + } + } + + public function userOwnsBook($book_id, $user_id) { + $stmt = $this->pdo->prepare("SELECT id FROM books WHERE id = ? AND user_id = ?"); + $stmt->execute([$book_id, $user_id]); + return $stmt->fetch() !== false; + } + + public function generateNewShareToken($book_id) { + $new_token = bin2hex(random_bytes(16)); + $stmt = $this->pdo->prepare("UPDATE books SET share_token = ? WHERE id = ?"); + $success = $stmt->execute([$new_token, $book_id]); + return $success ? $new_token : false; + } + + public function getPublishedChapters($book_id) { + $stmt = $this->pdo->prepare(" + SELECT * FROM chapters + WHERE book_id = ? AND status = 'published' + ORDER BY sort_order, created_at + "); + $stmt->execute([$book_id]); + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } + + public function updateCover($book_id, $filename) { + $stmt = $this->pdo->prepare("UPDATE books SET cover_image = ? WHERE id = ?"); + return $stmt->execute([$filename, $book_id]); + } + + public function deleteCover($book_id) { + + $book = $this->findById($book_id); + $old_filename = $book['cover_image']; + + if ($old_filename) { + $file_path = COVERS_PATH . $old_filename; + if (file_exists($file_path)) { + unlink($file_path); + } + } + + $stmt = $this->pdo->prepare("UPDATE books SET cover_image = NULL WHERE id = ?"); + return $stmt->execute([$book_id]); + } + + public function updateSeriesInfo($book_id, $series_id, $sort_order) { + $stmt = $this->pdo->prepare("UPDATE books SET series_id = ?, sort_order_in_series = ? WHERE id = ?"); + return $stmt->execute([$series_id, $sort_order, $book_id]); + } + + public function removeFromSeries($book_id) { + $stmt = $this->pdo->prepare("UPDATE books SET series_id = NULL, sort_order_in_series = NULL WHERE id = ?"); + return $stmt->execute([$book_id]); + } + + public function findBySeries($series_id) { + $stmt = $this->pdo->prepare(" + SELECT b.* + FROM books b + WHERE b.series_id = ? + ORDER BY b.sort_order_in_series, b.created_at + "); + $stmt->execute([$series_id]); + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } + + public function reorderSeriesBooks($series_id, $new_order) { + try { + $this->pdo->beginTransaction(); + + foreach ($new_order as $order => $book_id) { + $stmt = $this->pdo->prepare("UPDATE books SET sort_order_in_series = ? WHERE id = ? AND series_id = ?"); + $stmt->execute([$order + 1, $book_id, $series_id]); + } + + $this->pdo->commit(); + return true; + } catch (Exception $e) { + $this->pdo->rollBack(); + return false; + } + } + + + public function getBookStats($book_id, $only_published_chapters = false) { + $sql = " + SELECT + COUNT(c.id) as chapter_count, + COALESCE(SUM(c.word_count), 0) as total_words + FROM books b + LEFT JOIN chapters c ON b.id = c.book_id + WHERE b.id = ? + "; + + if ($only_published_chapters) { + $sql .= " AND c.status = 'published'"; + } + + $stmt = $this->pdo->prepare($sql); + $stmt->execute([$book_id]); + return $stmt->fetch(PDO::FETCH_ASSOC); + } + + public function convertChaptersContent($book_id, $from_editor, $to_editor) { + try { + $this->pdo->beginTransaction(); + + $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; + } + } + + 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 countWords($text) { + $text = strip_tags($text); + $text = preg_replace('/[^\p{L}\p{N}\s]/u', ' ', $text); + $words = preg_split('/\s+/', $text); + $words = array_filter($words); + 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(); + + // Включаем разметку строк для лучшей обработки абзацев + $parsedown->setBreaksEnabled(true); + + // Обрабатываем Markdown + $html = $parsedown->text($markdown); + + // Дополнительная обработка для обеспечения правильной структуры абзацев + $html = $this->ensureParagraphStructure($html); + + return $html; + } + + private function ensureParagraphStructure($html) { + // Если HTML не содержит тегов абзацев или div'ов, оборачиваем в

+ if (!preg_match('/<(p|div|h[1-6]|blockquote|pre|ul|ol|li)/i', $html)) { + // Разбиваем на строки и оборачиваем каждую непустую строку в

+ $lines = explode("\n", trim($html)); + $wrappedLines = []; + + foreach ($lines as $line) { + $line = trim($line); + if (!empty($line)) { + // Пропускаем уже обернутые строки + if (!preg_match('/^<[^>]+>/', $line) || preg_match('/^<(p|div|h[1-6])/i', $line)) { + $wrappedLines[] = $line; + } else { + $wrappedLines[] = "

{$line}

"; + } + } + } + + $html = implode("\n", $wrappedLines); + } + + // Убеждаемся, что теги правильно закрыты + $html = $this->balanceTags($html); + + return $html; + } + + private function balanceTags($html) { + // Простая балансировка тегов - в реальном проекте лучше использовать DOMDocument + $tags = [ + 'p' => 0, + 'div' => 0, + 'span' => 0, + 'strong' => 0, + 'em' => 0, + ]; + + // Счетчик открывающих и закрывающих тегов + foreach ($tags as $tag => &$count) { + $open = substr_count($html, "<{$tag}>") + substr_count($html, "<{$tag} "); + $close = substr_count($html, ""); + $count = $open - $close; + } + + // Добавляем недостающие закрывающие теги + foreach ($tags as $tag => $count) { + if ($count > 0) { + $html .= str_repeat("", $count); + } + } + + return $html; + } + private function htmlToMarkdown($html) { + // Сначала нормализуем HTML структуру + $html = $this->normalizeHtml($html); + + // Базовая конвертация HTML в Markdown + $markdown = $html; + + // Обрабатываем абзацы - заменяем на двойные переносы строк + $markdown = preg_replace('/]*>(.*?)<\/p>/is', "$1\n\n", $markdown); + + // Обрабатываем разрывы строк + $markdown = preg_replace('/]*>\s*<\/br[^>]*>/i', "\n", $markdown); + $markdown = preg_replace('/]*>/i', " \n", $markdown); // Два пробела для Markdown разрыва + + // Заголовки + $markdown = preg_replace('/]*>(.*?)<\/h1>/is', "# $1\n\n", $markdown); + $markdown = preg_replace('/]*>(.*?)<\/h2>/is', "## $1\n\n", $markdown); + $markdown = preg_replace('/]*>(.*?)<\/h3>/is', "### $1\n\n", $markdown); + $markdown = preg_replace('/]*>(.*?)<\/h4>/is', "#### $1\n\n", $markdown); + $markdown = preg_replace('/]*>(.*?)<\/h5>/is', "##### $1\n\n", $markdown); + $markdown = preg_replace('/]*>(.*?)<\/h6>/is', "###### $1\n\n", $markdown); + + // Жирный текст + $markdown = preg_replace('/]*>(.*?)<\/strong>/is', '**$1**', $markdown); + $markdown = preg_replace('/]*>(.*?)<\/b>/is', '**$1**', $markdown); + + // Курсив + $markdown = preg_replace('/]*>(.*?)<\/em>/is', '*$1*', $markdown); + $markdown = preg_replace('/]*>(.*?)<\/i>/is', '*$1*', $markdown); + + // Подчеркивание (не стандартно в Markdown, но обрабатываем) + $markdown = preg_replace('/]*>(.*?)<\/u>/is', '$1', $markdown); + + // Зачеркивание + $markdown = preg_replace('/]*>(.*?)<\/s>/is', '~~$1~~', $markdown); + $markdown = preg_replace('/]*>(.*?)<\/strike>/is', '~~$1~~', $markdown); + $markdown = preg_replace('/]*>(.*?)<\/del>/is', '~~$1~~', $markdown); + + // Списки + $markdown = preg_replace('/]*>(.*?)<\/li>/is', '- $1', $markdown); + $markdown = preg_replace('/]*>(.*?)<\/ul>/is', "$1\n", $markdown); + $markdown = preg_replace('/]*>(.*?)<\/ol>/is', "$1\n", $markdown); + + // Блочные цитаты + $markdown = preg_replace('/]*>(.*?)<\/blockquote>/is', "> $1\n", $markdown); + + // Код + $markdown = preg_replace('/]*>(.*?)<\/code>/is', '`$1`', $markdown); + $markdown = preg_replace('/]*>(.*?)<\/pre>/is', "```\n$1\n```", $markdown); + + // Ссылки + $markdown = preg_replace('/]*href="([^"]*)"[^>]*>(.*?)<\/a>/is', '[$2]($1)', $markdown); + + // Изображения + $markdown = preg_replace('/]*src="([^"]*)"[^>]*alt="([^"]*)"[^>]*>/is', '![$2]($1)', $markdown); + + // Удаляем все остальные HTML-теги + $markdown = strip_tags($markdown); + + // Чистим лишние пробелы и переносы + $markdown = preg_replace('/\n\s*\n\s*\n/', "\n\n", $markdown); + $markdown = preg_replace('/^\s+|\s+$/m', '', $markdown); // Trim каждой строки + $markdown = trim($markdown); + + return $markdown; + } + + private function normalizeHtml($html) { + // Нормализуем HTML структуру перед конвертацией + $html = preg_replace('/]*>(.*?)<\/div>/is', "

$1

", $html); + + // Убираем лишние пробелы + $html = preg_replace('/\s+/', ' ', $html); + + // Восстанавливаем структуру абзацев + $html = preg_replace('/([^>])\s*<\/(p|div)>\s*([^<])/', "$1\n\n$3", $html); + + return $html; + } + + public function normalizeBookContent($book_id) { + try { + $chapters = $this->getAllChapters($book_id); + $book = $this->findById($book_id); + + foreach ($chapters as $chapter) { + $normalized_content = ''; + + if ($book['editor_type'] == 'html') { + // Нормализуем HTML контент + $normalized_content = $this->normalizeHtmlContent($chapter['content']); + } else { + // Нормализуем Markdown контент + $normalized_content = $this->normalizeMarkdownContent($chapter['content']); + } + + if ($normalized_content !== $chapter['content']) { + $this->updateChapterContent($chapter['id'], $normalized_content); + } + } + + return true; + } catch (Exception $e) { + error_log("Error normalizing book content: " . $e->getMessage()); + return false; + } + } + + private function normalizeHtmlContent($html) { + // Простая нормализация HTML - оборачиваем текст без тегов в

+ if (!preg_match('/<[^>]+>/', $html) && trim($html) !== '') { + // Если нет HTML тегов, оборачиваем в

+ $lines = explode("\n", trim($html)); + $wrapped = array_map(function($line) { + $line = trim($line); + return $line ? "

{$line}

" : ''; + }, $lines); + return implode("\n", array_filter($wrapped)); + } + + return $html; + } + + private function normalizeMarkdownContent($markdown) { + // Нормализация Markdown - убеждаемся, что есть пустые строки между абзацами + $lines = explode("\n", $markdown); + $normalized = []; + $inParagraph = false; + + foreach ($lines as $line) { + $trimmed = trim($line); + + if (empty($trimmed)) { + // Пустая строка - конец абзаца + if ($inParagraph) { + $normalized[] = ''; + $inParagraph = false; + } + } else { + // Непустая строка + if (!$inParagraph && !empty($normalized) && end($normalized) !== '') { + // Добавляем пустую строку перед новым абзацем + $normalized[] = ''; + } + $normalized[] = $line; + $inParagraph = true; + } + } + + return implode("\n", $normalized); + } +} ?> \ No newline at end of file diff --git a/models/Series.php b/models/Series.php old mode 100755 new mode 100644 index 6385abd..d53e2d8 --- a/models/Series.php +++ b/models/Series.php @@ -1,158 +1,158 @@ -pdo = $pdo; - } - - public function findById($id) { - $stmt = $this->pdo->prepare(" - SELECT s.*, - COUNT(b.id) as book_count, - COALESCE(( - SELECT SUM(c.word_count) - FROM chapters c - JOIN books b2 ON c.book_id = b2.id - WHERE b2.series_id = s.id AND b2.published = 1 - ), 0) as total_words - FROM series s - LEFT JOIN books b ON s.id = b.series_id AND b.published = 1 - WHERE s.id = ? - GROUP BY s.id - "); - $stmt->execute([$id]); - return $stmt->fetch(PDO::FETCH_ASSOC); - } - - public function findByUser($user_id, $include_stats = true) { - if ($include_stats) { - $sql = " - SELECT s.*, - COUNT(b.id) as book_count, - COALESCE(( - SELECT SUM(c.word_count) - FROM chapters c - JOIN books b2 ON c.book_id = b2.id - WHERE b2.series_id = s.id AND b2.user_id = ? - ), 0) as total_words - FROM series s - LEFT JOIN books b ON s.id = b.series_id - WHERE s.user_id = ? - GROUP BY s.id - ORDER BY s.created_at DESC - "; - $stmt = $this->pdo->prepare($sql); - $stmt->execute([$user_id, $user_id]); - } else { - $sql = "SELECT * FROM series WHERE user_id = ? ORDER BY created_at DESC"; - $stmt = $this->pdo->prepare($sql); - $stmt->execute([$user_id]); - } - - return $stmt->fetchAll(PDO::FETCH_ASSOC); - } - - public function create($data) { - $stmt = $this->pdo->prepare(" - INSERT INTO series (title, description, user_id) - VALUES (?, ?, ?) - "); - return $stmt->execute([ - $data['title'], - $data['description'] ?? null, - $data['user_id'] - ]); - } - - public function update($id, $data) { - $stmt = $this->pdo->prepare(" - UPDATE series - SET title = ?, description = ?, updated_at = CURRENT_TIMESTAMP - WHERE id = ? AND user_id = ? - "); - return $stmt->execute([ - $data['title'], - $data['description'] ?? null, - $id, - $data['user_id'] - ]); - } - - public function delete($id, $user_id) { - try { - $this->pdo->beginTransaction(); - - $stmt = $this->pdo->prepare("UPDATE books SET series_id = NULL, sort_order_in_series = NULL WHERE series_id = ? AND user_id = ?"); - $stmt->execute([$id, $user_id]); - - $stmt = $this->pdo->prepare("DELETE FROM series WHERE id = ? AND user_id = ?"); - $result = $stmt->execute([$id, $user_id]); - - $this->pdo->commit(); - return $result; - } catch (Exception $e) { - $this->pdo->rollBack(); - return false; - } - } - - public function userOwnsSeries($series_id, $user_id) { - $stmt = $this->pdo->prepare("SELECT id FROM series WHERE id = ? AND user_id = ?"); - $stmt->execute([$series_id, $user_id]); - return $stmt->fetch() !== false; - } - - public function getBooksInSeries($series_id, $only_published = false) { - $sql = "SELECT * FROM books WHERE series_id = ?"; - if ($only_published) { - $sql .= " AND published = 1"; - } - $sql .= " ORDER BY sort_order_in_series, created_at"; - - $stmt = $this->pdo->prepare($sql); - $stmt->execute([$series_id]); - return $stmt->fetchAll(PDO::FETCH_ASSOC); - } - - public function getNextSortOrder($series_id) { - $stmt = $this->pdo->prepare("SELECT MAX(sort_order_in_series) as max_order FROM books WHERE series_id = ?"); - $stmt->execute([$series_id]); - $result = $stmt->fetch(); - return ($result['max_order'] ?? 0) + 1; - } - - public function getSeriesStats($series_id, $user_id = null) { - $sql = " - SELECT - COUNT(b.id) as book_count, - COALESCE(SUM(stats.chapter_count), 0) as chapter_count, - COALESCE(SUM(stats.total_words), 0) as total_words - FROM series s - LEFT JOIN books b ON s.id = b.series_id - LEFT JOIN ( - SELECT - book_id, - COUNT(id) as chapter_count, - SUM(word_count) as total_words - FROM chapters - GROUP BY book_id - ) stats ON b.id = stats.book_id - WHERE s.id = ? - "; - - $params = [$series_id]; - - if ($user_id) { - $sql .= " AND s.user_id = ?"; - $params[] = $user_id; - } - - $stmt = $this->pdo->prepare($sql); - $stmt->execute($params); - return $stmt->fetch(PDO::FETCH_ASSOC); - } -} +pdo = $pdo; + } + + public function findById($id) { + $stmt = $this->pdo->prepare(" + SELECT s.*, + COUNT(b.id) as book_count, + COALESCE(( + SELECT SUM(c.word_count) + FROM chapters c + JOIN books b2 ON c.book_id = b2.id + WHERE b2.series_id = s.id AND b2.published = 1 + ), 0) as total_words + FROM series s + LEFT JOIN books b ON s.id = b.series_id AND b.published = 1 + WHERE s.id = ? + GROUP BY s.id + "); + $stmt->execute([$id]); + return $stmt->fetch(PDO::FETCH_ASSOC); + } + + public function findByUser($user_id, $include_stats = true) { + if ($include_stats) { + $sql = " + SELECT s.*, + COUNT(b.id) as book_count, + COALESCE(( + SELECT SUM(c.word_count) + FROM chapters c + JOIN books b2 ON c.book_id = b2.id + WHERE b2.series_id = s.id AND b2.user_id = ? + ), 0) as total_words + FROM series s + LEFT JOIN books b ON s.id = b.series_id + WHERE s.user_id = ? + GROUP BY s.id + ORDER BY s.created_at DESC + "; + $stmt = $this->pdo->prepare($sql); + $stmt->execute([$user_id, $user_id]); + } else { + $sql = "SELECT * FROM series WHERE user_id = ? ORDER BY created_at DESC"; + $stmt = $this->pdo->prepare($sql); + $stmt->execute([$user_id]); + } + + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } + + public function create($data) { + $stmt = $this->pdo->prepare(" + INSERT INTO series (title, description, user_id) + VALUES (?, ?, ?) + "); + return $stmt->execute([ + $data['title'], + $data['description'] ?? null, + $data['user_id'] + ]); + } + + public function update($id, $data) { + $stmt = $this->pdo->prepare(" + UPDATE series + SET title = ?, description = ?, updated_at = CURRENT_TIMESTAMP + WHERE id = ? AND user_id = ? + "); + return $stmt->execute([ + $data['title'], + $data['description'] ?? null, + $id, + $data['user_id'] + ]); + } + + public function delete($id, $user_id) { + try { + $this->pdo->beginTransaction(); + + $stmt = $this->pdo->prepare("UPDATE books SET series_id = NULL, sort_order_in_series = NULL WHERE series_id = ? AND user_id = ?"); + $stmt->execute([$id, $user_id]); + + $stmt = $this->pdo->prepare("DELETE FROM series WHERE id = ? AND user_id = ?"); + $result = $stmt->execute([$id, $user_id]); + + $this->pdo->commit(); + return $result; + } catch (Exception $e) { + $this->pdo->rollBack(); + return false; + } + } + + public function userOwnsSeries($series_id, $user_id) { + $stmt = $this->pdo->prepare("SELECT id FROM series WHERE id = ? AND user_id = ?"); + $stmt->execute([$series_id, $user_id]); + return $stmt->fetch() !== false; + } + + public function getBooksInSeries($series_id, $only_published = false) { + $sql = "SELECT * FROM books WHERE series_id = ?"; + if ($only_published) { + $sql .= " AND published = 1"; + } + $sql .= " ORDER BY sort_order_in_series, created_at"; + + $stmt = $this->pdo->prepare($sql); + $stmt->execute([$series_id]); + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } + + public function getNextSortOrder($series_id) { + $stmt = $this->pdo->prepare("SELECT MAX(sort_order_in_series) as max_order FROM books WHERE series_id = ?"); + $stmt->execute([$series_id]); + $result = $stmt->fetch(); + return ($result['max_order'] ?? 0) + 1; + } + + public function getSeriesStats($series_id, $user_id = null) { + $sql = " + SELECT + COUNT(b.id) as book_count, + COALESCE(SUM(stats.chapter_count), 0) as chapter_count, + COALESCE(SUM(stats.total_words), 0) as total_words + FROM series s + LEFT JOIN books b ON s.id = b.series_id + LEFT JOIN ( + SELECT + book_id, + COUNT(id) as chapter_count, + SUM(word_count) as total_words + FROM chapters + GROUP BY book_id + ) stats ON b.id = stats.book_id + WHERE s.id = ? + "; + + $params = [$series_id]; + + if ($user_id) { + $sql .= " AND s.user_id = ?"; + $params[] = $user_id; + } + + $stmt = $this->pdo->prepare($sql); + $stmt->execute($params); + return $stmt->fetch(PDO::FETCH_ASSOC); + } +} ?> \ No newline at end of file diff --git a/models/User.php b/models/User.php index 046aeca..fea8b84 100755 --- a/models/User.php +++ b/models/User.php @@ -1,119 +1,119 @@ -pdo = $pdo; - } - - public function findById($id) { - $stmt = $this->pdo->prepare("SELECT * FROM users WHERE id = ?"); - $stmt->execute([$id]); - return $stmt->fetch(PDO::FETCH_ASSOC); - } - - public function findByUsername($username) { - $stmt = $this->pdo->prepare("SELECT * FROM users WHERE username = ?"); - $stmt->execute([$username]); - return $stmt->fetch(PDO::FETCH_ASSOC); - } - - public function findByEmail($email) { - $stmt = $this->pdo->prepare("SELECT * FROM users WHERE email = ?"); - $stmt->execute([$email]); - return $stmt->fetch(PDO::FETCH_ASSOC); - } - - public function findAll() { - $stmt = $this->pdo->prepare("SELECT id, username, display_name, email, created_at, last_login, is_active FROM users ORDER BY created_at DESC"); - $stmt->execute(); - return $stmt->fetchAll(PDO::FETCH_ASSOC); - } - - public function create($data) { - $password_hash = password_hash($data['password'], PASSWORD_DEFAULT); - - $is_active = $data['is_active'] ?? 0; - - $stmt = $this->pdo->prepare(" - INSERT INTO users (username, display_name, email, password_hash, is_active) - VALUES (?, ?, ?, ?, ?) - "); - - return $stmt->execute([ - $data['username'], - $data['display_name'] ?? $data['username'], - $data['email'] ?? null, - $password_hash, - $is_active - ]); - } - - public function update($id, $data) { - $sql = "UPDATE users SET display_name = ?, email = ?"; - $params = [$data['display_name'], $data['email']]; - - if (!empty($data['password'])) { - $sql .= ", password_hash = ?"; - $params[] = password_hash($data['password'], PASSWORD_DEFAULT); - } - - $sql .= " WHERE id = ?"; - $params[] = $id; - - $stmt = $this->pdo->prepare($sql); - return $stmt->execute($params); - } - - public function updateStatus($id, $is_active) { - $stmt = $this->pdo->prepare("UPDATE users SET is_active = ? WHERE id = ?"); - return $stmt->execute([$is_active, $id]); - } - - public function delete($id) { - $stmt = $this->pdo->prepare("DELETE FROM users WHERE id = ?"); - return $stmt->execute([$id]); - } - - public function updateLastLogin($id) { - $stmt = $this->pdo->prepare("UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?"); - return $stmt->execute([$id]); - } - - public function verifyPassword($password, $hash) { - return password_verify($password, $hash); - } - - public function updateAvatar($id, $filename) { - $stmt = $this->pdo->prepare("UPDATE users SET avatar = ? WHERE id = ?"); - return $stmt->execute([$filename, $id]); - } - - public function updateBio($id, $bio) { - $stmt = $this->pdo->prepare("UPDATE users SET bio = ? WHERE id = ?"); - return $stmt->execute([$bio, $id]); - } - - public function updateProfile($id, $data) { - $sql = "UPDATE users SET display_name = ?, email = ?, bio = ?"; - $params = [ - $data['display_name'] ?? '', - $data['email'] ?? null, - $data['bio'] ?? null - ]; - - if (!empty($data['avatar'])) { - $sql .= ", avatar = ?"; - $params[] = $data['avatar']; - } - - $sql .= " WHERE id = ?"; - $params[] = $id; - - $stmt = $this->pdo->prepare($sql); - return $stmt->execute($params); - } -} +pdo = $pdo; + } + + public function findById($id) { + $stmt = $this->pdo->prepare("SELECT * FROM users WHERE id = ?"); + $stmt->execute([$id]); + return $stmt->fetch(PDO::FETCH_ASSOC); + } + + public function findByUsername($username) { + $stmt = $this->pdo->prepare("SELECT * FROM users WHERE username = ?"); + $stmt->execute([$username]); + return $stmt->fetch(PDO::FETCH_ASSOC); + } + + public function findByEmail($email) { + $stmt = $this->pdo->prepare("SELECT * FROM users WHERE email = ?"); + $stmt->execute([$email]); + return $stmt->fetch(PDO::FETCH_ASSOC); + } + + public function findAll() { + $stmt = $this->pdo->prepare("SELECT id, username, display_name, email, created_at, last_login, is_active FROM users ORDER BY created_at DESC"); + $stmt->execute(); + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } + + public function create($data) { + $password_hash = password_hash($data['password'], PASSWORD_DEFAULT); + + $is_active = $data['is_active'] ?? 0; + + $stmt = $this->pdo->prepare(" + INSERT INTO users (username, display_name, email, password_hash, is_active) + VALUES (?, ?, ?, ?, ?) + "); + + return $stmt->execute([ + $data['username'], + $data['display_name'] ?? $data['username'], + $data['email'] ?? null, + $password_hash, + $is_active + ]); + } + + public function update($id, $data) { + $sql = "UPDATE users SET display_name = ?, email = ?"; + $params = [$data['display_name'], $data['email']]; + + if (!empty($data['password'])) { + $sql .= ", password_hash = ?"; + $params[] = password_hash($data['password'], PASSWORD_DEFAULT); + } + + $sql .= " WHERE id = ?"; + $params[] = $id; + + $stmt = $this->pdo->prepare($sql); + return $stmt->execute($params); + } + + public function updateStatus($id, $is_active) { + $stmt = $this->pdo->prepare("UPDATE users SET is_active = ? WHERE id = ?"); + return $stmt->execute([$is_active, $id]); + } + + public function delete($id) { + $stmt = $this->pdo->prepare("DELETE FROM users WHERE id = ?"); + return $stmt->execute([$id]); + } + + public function updateLastLogin($id) { + $stmt = $this->pdo->prepare("UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?"); + return $stmt->execute([$id]); + } + + public function verifyPassword($password, $hash) { + return password_verify($password, $hash); + } + + public function updateAvatar($id, $filename) { + $stmt = $this->pdo->prepare("UPDATE users SET avatar = ? WHERE id = ?"); + return $stmt->execute([$filename, $id]); + } + + public function updateBio($id, $bio) { + $stmt = $this->pdo->prepare("UPDATE users SET bio = ? WHERE id = ?"); + return $stmt->execute([$bio, $id]); + } + + public function updateProfile($id, $data) { + $sql = "UPDATE users SET display_name = ?, email = ?, bio = ?"; + $params = [ + $data['display_name'] ?? '', + $data['email'] ?? null, + $data['bio'] ?? null + ]; + + if (!empty($data['avatar'])) { + $sql .= ", avatar = ?"; + $params[] = $data['avatar']; + } + + $sql .= " WHERE id = ?"; + $params[] = $id; + + $stmt = $this->pdo->prepare($sql); + return $stmt->execute($params); + } +} ?> \ No newline at end of file diff --git a/preview.php b/preview.php index 47ddb44..0f0446f 100755 --- a/preview.php +++ b/preview.php @@ -7,9 +7,15 @@ $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; +} -$Parsedown = new Parsedown(); -$html_content = $Parsedown->text($content); $page_title = "Предпросмотр: " . e($title); ?> diff --git a/profile.php b/profile.php index 3b14bb6..838790c 100755 --- a/profile.php +++ b/profile.php @@ -1,195 +1,195 @@ -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 = "Ошибка при обновлении профиля"; - } - } -} - -$page_title = "Мой профиль"; -include 'views/header.php'; -?> - -

Мой профиль

- - -
- -
- - -
-
-

Основная информация

-
- - -
- - -
- -
- - -
- -
- - -
- -
- - - - Поддерживается Markdown форматирование - -
- -
- - - ↩️ Назад - -
-
-
- -
-

Аватарка

- -
- - Аватарка - -
- -
- -
- -
- - -
- - - - Разрешены: JPG, PNG, GIF, WebP. Максимальный размер: 2MB. - Рекомендуемый размер: 200×200 пикселей. - - - -
- ❌ -
- -
- -
- - - - - -
-
- - -
-

- Примечание: Аватарка отображается на вашей публичной странице автора -

-
- -
-
- - - +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 = "Ошибка при обновлении профиля"; + } + } +} + +$page_title = "Мой профиль"; +include 'views/header.php'; +?> + +

Мой профиль

+ + +
+ +
+ + +
+
+

Основная информация

+
+ + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + + Поддерживается Markdown форматирование + +
+ +
+ + + ↩️ Назад + +
+
+
+ +
+

Аватарка

+ +
+ + Аватарка + +
+ +
+ +
+ +
+ + +
+ + + + Разрешены: JPG, PNG, GIF, WebP. Максимальный размер: 2MB. + Рекомендуемый размер: 200×200 пикселей. + + + +
+ ❌ +
+ +
+ +
+ + + + + +
+
+ + +
+

+ Примечание: Аватарка отображается на вашей публичной странице автора +

+
+ +
+
+ + + \ No newline at end of file diff --git a/series.php b/series.php old mode 100755 new mode 100644 index bb02401..9f90f28 --- a/series.php +++ b/series.php @@ -1,95 +1,95 @@ -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'; -?> - -

Мои серии книг

- - -
- - -
- - - -
- - -
- - -
-

Всего серий:

- ➕ Новая серия -
- - - - -
- - - -
- - +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'; +?> + +

Мои серии книг

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

Всего серий:

+ ➕ Новая серия +
+ + + + +
+ + + +
+ + \ No newline at end of file diff --git a/series_delete.php b/series_delete.php old mode 100755 new mode 100644 index 1bdd740..372093f --- a/series_delete.php +++ b/series_delete.php @@ -1,39 +1,39 @@ -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'); +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 old mode 100755 new mode 100644 index a9b1ac2..e84a039 --- a/series_edit.php +++ b/series_edit.php @@ -1,179 +1,179 @@ -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'; -?> - -

- - -
- - -
- - -
- - -
- - - - - -
- - -
- - -
-

Книги в этой серии

- - 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; - } - ?> - - -
-

В этой серии пока нет книг.

- 📚 Добавить книги -
- -
- - - - - - - - - - - - - - - - - - - - - -
ПорядокНазвание книгиЖанрСтатусДействия
- - -
- -
- - - - - - Редактировать - -
-
- -
- Статистика серии: - Книг: | - Глав: | - Слов: -
- -
- - +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'; +?> + +

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

Книги в этой серии

+ + 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; + } + ?> + + +
+

В этой серии пока нет книг.

+ 📚 Добавить книги +
+ +
+ + + + + + + + + + + + + + + + + + + + + +
ПорядокНазвание книгиЖанрСтатусДействия
+ + +
+ +
+ + + + + + Редактировать + +
+
+ +
+ Статистика серии: + Книг: | + Глав: | + Слов: +
+ +
+ + \ No newline at end of file diff --git a/view_book.php b/view_book.php index a96959f..2fb01e0 100755 --- a/view_book.php +++ b/view_book.php @@ -1,270 +1,276 @@ -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(); - ?> - -

- 📚 Часть серии: - - - - (Книга ) - - -

- - -

- - -

- Жанр: -

- - - -
-

-
- - -
- Глав: - Слов: - - | - Вернуться к редактированию - -
-
- - - -
-

📖 Оглавление

- -
- $chapter): ?> - - -
-
- - -
-

Экспорт книги

- - - -

- Примечание: Экспортируются только опубликованные главы -

-
- - -
-

В этой книге пока нет опубликованных глав

-

Автор еще не опубликовал ни одной главы

-
- -
- $chapter): ?> -
-

- - 🔗 -

-
- text($chapter['content']) ?> -
-
- Обновлено: - ↑ Наверх -
-
- -
- - -
-

- Книга создана в • - Автор: • - -

-
-
-
- - - +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(); + ?> + +

+ 📚 Часть серии: + + + + (Книга ) + + +

+ + +

+ + +

+ Жанр: +

+ + + +
+

+
+ + +
+ Глав: + Слов: + + | + Вернуться к редактированию + +
+
+ + + +
+

📖 Оглавление

+ +
+ $chapter): ?> + + +
+
+ + +
+

Экспорт книги

+ + + +

+ Примечание: Экспортируются только опубликованные главы +

+
+ + +
+

В этой книге пока нет опубликованных глав

+

Автор еще не опубликовал ни одной главы

+
+ +
+ $chapter): ?> +
+

+ + 🔗 +

+
+ + text($chapter['content']) ?> + + + +
+ + +
+ Обновлено: + ↑ Наверх +
+
+ +
+ + +
+

+ Книга создана в • + Автор: • + +

+
+
+
+ + + \ No newline at end of file diff --git a/view_series.php b/view_series.php old mode 100755 new mode 100644 index 878ce61..63fe8ca --- a/view_series.php +++ b/view_series.php @@ -1,161 +1,161 @@ -Неверный запрос"; - include 'views/footer.php'; - exit; -} - -$seriesModel = new Series($pdo); -$series = $seriesModel->findById($series_id); - -if (!$series) { - http_response_code(404); - echo "

Серия не найдена

"; - include 'views/footer.php'; - exit; -} - -// Получаем только опубликованные книги серии -$books = $seriesModel->getBooksInSeries($series_id, true); - -// Получаем информацию об авторе -$stmt = $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($pdo); -$total_words = 0; -$total_chapters = 0; - -foreach ($books as $book) { - $book_stats = $bookModel->getBookStats($book['id'], true); // true - только опубликованные главы - $total_words += $book_stats['total_words'] ?? 0; - $total_chapters += $book_stats['chapter_count'] ?? 0; -} - -$page_title = $series['title'] . ' — серия книг'; -include 'views/header.php'; -?> - -
-
-
-

-

- Серия книг от - -

- - -
- text($series['description']) ?> -
- - -
- Книг: - Глав: - Слов: -
-
- - -
-

В этой серии пока нет опубликованных книг

-

Автор еще не опубликовал книги из этой серии

-
- -
-

Книги серии

- - -
- -
- <?= e($book['title']) ?> -
- -
-
📚
-
- - -
-

- - Книга
- - -

- - -

- - - -

- - -
- - Читать - - - getBookStats($book['id'], true); // true - только опубликованные главы - ?> - - - Глав: | Слов: - -
-
-
- -
- - -
-

- Серия создана в • - Автор: -

-
-
-
- - - +Неверный запрос"; + include 'views/footer.php'; + exit; +} + +$seriesModel = new Series($pdo); +$series = $seriesModel->findById($series_id); + +if (!$series) { + http_response_code(404); + echo "

Серия не найдена

"; + include 'views/footer.php'; + exit; +} + +// Получаем только опубликованные книги серии +$books = $seriesModel->getBooksInSeries($series_id, true); + +// Получаем информацию об авторе +$stmt = $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($pdo); +$total_words = 0; +$total_chapters = 0; + +foreach ($books as $book) { + $book_stats = $bookModel->getBookStats($book['id'], true); // true - только опубликованные главы + $total_words += $book_stats['total_words'] ?? 0; + $total_chapters += $book_stats['chapter_count'] ?? 0; +} + +$page_title = $series['title'] . ' — серия книг'; +include 'views/header.php'; +?> + +
+
+
+

+

+ Серия книг от + +

+ + +
+ text($series['description']) ?> +
+ + +
+ Книг: + Глав: + Слов: +
+
+ + +
+

В этой серии пока нет опубликованных книг

+

Автор еще не опубликовал книги из этой серии

+
+ +
+

Книги серии

+ + +
+ +
+ <?= e($book['title']) ?> +
+ +
+
📚
+
+ + +
+

+ + Книга
+ + +

+ + +

+ + + +

+ + +
+ + Читать + + + getBookStats($book['id'], true); // true - только опубликованные главы + ?> + + + Глав: | Слов: + +
+
+
+ +
+ + +
+

+ Серия создана в • + Автор: +

+
+
+
+ + + \ No newline at end of file diff --git a/views/header.php b/views/header.php index 7bc23dd..a0e1b5e 100755 --- a/views/header.php +++ b/views/header.php @@ -1,51 +1,64 @@ - - - - - - - <?= e(APP_NAME) ?> - <?= e($page_title ?? 'Платформа для писателей') ?> - - - - - - -
\ No newline at end of file + + + + + + + <?= e(APP_NAME) ?> - <?= e($page_title ?? 'Платформа для писателей') ?> + + + + + + +
+ +
+ + +
+ + + +
+ + +
+ \ No newline at end of file