2025-12-06 20:57:38 +08:00

824 lines
33 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Pages模块 - 页面渲染逻辑
*/
const Pages = (function() {
// ========== 工具方法 ==========
// 格式化时间
function formatTime(timestamp) {
if (!timestamp) return '';
const date = new Date(timestamp);
const now = new Date();
const diff = now - date;
if (diff < 60000) return '刚刚';
if (diff < 3600000) return Math.floor(diff / 60000) + '分钟前';
if (diff < 86400000) return Math.floor(diff / 3600000) + '小时前';
if (diff < 604800000) return Math.floor(diff / 86400000) + '天前';
return date.toLocaleDateString('zh-CN');
}
// 处理封面图片错误
function handleCoverError(img) {
img.onerror = null;
img.src = 'data:image/svg+xml,' + encodeURIComponent(`
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="160" viewBox="0 0 120 160">
<rect fill="#2d2d4a" width="120" height="160"/>
<text fill="#6a6a7a" font-family="serif" font-size="14" x="50%" y="50%" text-anchor="middle" dy=".3em">暂无封面</text>
</svg>
`);
}
// 清理HTML内容中的换行标签
function cleanContent(content) {
if (!content) return '';
return content
.replace(/<br\s*\/?>/gi, '\n')
.replace(/&nbsp;/g, ' ')
.replace(/<[^>]+>/g, '')
.trim();
}
// ========== 书架页面 ==========
async function renderShelf() {
const $content = $('#app-content');
$content.html(`
<div class="shelf-page page">
<div class="shelf-header">
<h2 class="shelf-title">我的书架</h2>
</div>
<div class="shelf-grid" id="shelf-grid">
${renderSkeletonGrid(6)}
</div>
</div>
`);
try {
const userId = Store.getUserId();
const data = await API.getShelf({ userId, page: 0, size: 50 });
const items = data.content || [];
if (items.length === 0) {
$('#shelf-grid').html(`
<div class="empty-state" style="grid-column: 1/-1;">
<svg viewBox="0 0 24 24"><path d="M4 6H2v14c0 1.1.9 2 2 2h14v-2H4V6zm16-4H8c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-1 9H9V9h10v2zm-4 4H9v-2h6v2zm4-8H9V5h10v2z"/></svg>
<h3>书架空空如也</h3>
<p>去发现页面找本好书吧</p>
</div>
`);
return;
}
// 获取每本书的详情
const novels = await Promise.all(
items.map(item => API.getNovelDetail(item.novelId).catch(() => null))
);
// 更新书架缓存
Store.setShelfCache(items.map(i => i.novelId));
let html = '';
novels.forEach((novel, index) => {
if (!novel) return;
const position = Store.getReadingPosition(novel.id);
html += renderShelfItem(novel, position);
});
$('#shelf-grid').html(html);
// 绑定图片错误处理
$('#shelf-grid img').on('error', function() {
handleCoverError(this);
});
} catch (error) {
console.error('加载书架失败:', error);
Toast.show('加载失败,请重试');
}
}
function renderShelfItem(novel, position) {
const progress = position ? Math.min(100, Math.round((position.chapterIndex / (novel.lastChapterIndex || 1)) * 100)) : 0;
return `
<div class="shelf-item" data-novel-id="${novel.id}">
<div class="novel-grid-card">
<div class="novel-cover">
<img src="${novel.cover || ''}" alt="${novel.name}" loading="lazy">
</div>
<div class="novel-info">
<div class="novel-title text-ellipsis">${novel.name}</div>
<div class="novel-author text-ellipsis">${novel.author}</div>
</div>
</div>
${progress > 0 ? `
<div class="reading-progress">
<div class="reading-progress-bar" style="width: ${progress}%"></div>
</div>
` : ''}
</div>
`;
}
// ========== 发现页面 ==========
async function renderExplore() {
const $content = $('#app-content');
$content.html(`
<div class="explore-page page">
<div class="explore-banner">
<div class="explore-banner-content">
<h2>发现好书</h2>
<p>探索精彩小说世界</p>
</div>
</div>
<div class="page-section">
<div class="section-header">
<h3 class="section-title">🎲 随机推荐</h3>
<button class="section-more" id="refresh-random">换一批</button>
</div>
<div class="novel-scroll" id="random-novels">
${renderSkeletonScroll(5)}
</div>
</div>
<div class="page-section">
<div class="section-header">
<h3 class="section-title">热门标签</h3>
</div>
<div class="tags-cloud" id="tags-cloud">
<div class="skeleton" style="width:60px;height:32px;border-radius:16px;"></div>
<div class="skeleton" style="width:80px;height:32px;border-radius:16px;"></div>
<div class="skeleton" style="width:50px;height:32px;border-radius:16px;"></div>
</div>
</div>
<div class="page-section">
<div class="section-header">
<h3 class="section-title">全部小说</h3>
</div>
<div class="novel-list" id="novel-list">
${renderSkeletonList(5)}
</div>
<div class="load-more" id="load-more">
<button class="load-more-btn">加载更多</button>
</div>
</div>
</div>
`);
// 加载随机推荐
loadRandomNovels();
// 加载标签
loadTags();
// 加载小说列表
loadNovels(0);
// 绑定刷新随机推荐按钮
$('#refresh-random').on('click', function() {
loadRandomNovels();
});
}
// 加载随机推荐
async function loadRandomNovels() {
$('#random-novels').html(renderSkeletonScroll(6));
try {
const novels = await API.getRandomNovels(12);
if (novels.length === 0) {
$('#random-novels').html('<p style="padding:20px;color:var(--text-muted);">暂无推荐</p>');
return;
}
let html = '';
novels.forEach(novel => {
const isInShelf = Store.isInShelfCache(novel.id);
html += `
<div class="novel-grid-card" data-novel-id="${novel.id}">
<div class="novel-cover">
<img src="${novel.cover || ''}" alt="${novel.name}" loading="lazy">
${isInShelf ? '<div class="shelf-badge" title="已在书架"><svg viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg></div>' : ''}
</div>
<div class="novel-info">
<div class="novel-title text-ellipsis">${novel.name}</div>
<div class="novel-author text-ellipsis clickable-author" data-author="${novel.author || ''}">${novel.author || '未知'}</div>
</div>
</div>
`;
});
$('#random-novels').html(html);
// 绑定图片错误处理
$('#random-novels img').on('error', function() {
handleCoverError(this);
});
} catch (error) {
console.error('加载随机推荐失败:', error);
$('#random-novels').html('<p style="padding:20px;color:var(--text-muted);">加载失败</p>');
}
}
// 渲染横向滚动骨架屏
function renderSkeletonScroll(count) {
let html = '';
for (let i = 0; i < count; i++) {
html += `
<div class="novel-grid-card" style="flex-shrink:0;width:100px;">
<div class="novel-cover skeleton" style="width:100px;height:140px;"></div>
<div style="padding:8px;">
<div class="skeleton" style="height:14px;width:80%;margin-bottom:4px;"></div>
<div class="skeleton" style="height:12px;width:50%;"></div>
</div>
</div>
`;
}
return html;
}
let currentPage = 0;
let isLoadingMore = false;
let hasMore = true;
let currentTag = null; // 保存当前选择的标签
async function loadTags() {
try {
const tags = await API.getPopularTags(15);
let html = '';
tags.forEach(tag => {
html += `<button class="tag-btn" data-tag="${tag}">${tag}</button>`;
});
$('#tags-cloud').html(html);
} catch (error) {
console.error('加载标签失败:', error);
}
}
let currentAuthor = null; // 保存当前选择的作者
async function loadNovels(page, tag = null, author = null, append = false) {
if (isLoadingMore) return;
isLoadingMore = true;
// 如果不是追加加载,保存当前筛选条件
if (!append) {
currentPage = 0;
hasMore = true;
currentTag = tag; // 保存当前选择的标签
currentAuthor = author; // 保存当前选择的作者
$('#novel-list').html(renderSkeletonList(5));
}
// 追加加载时使用已保存的筛选条件
const useTag = append ? currentTag : tag;
const useAuthor = append ? currentAuthor : author;
try {
const params = { page, size: 20 };
if (useTag) params.tag = useTag;
if (useAuthor) params.keyword = useAuthor; // 使用keyword参数搜索作者
const data = await API.searchNovels(params);
const novels = data.content || [];
hasMore = !data.last;
// 如果有作者筛选,只保留该作者的作品
let filteredNovels = novels;
if (useAuthor) {
filteredNovels = novels.filter(novel => novel.author === useAuthor);
}
let html = '';
filteredNovels.forEach(novel => {
const isInShelf = Store.isInShelfCache(novel.id);
html += renderNovelCard(novel, isInShelf);
});
if (append) {
$('#novel-list').append(html);
} else {
// 显示筛选信息
let filterInfo = '';
if (useAuthor) {
filterInfo = `<div class="filter-info"><span class="filter-label">作者:</span><span class="filter-value">${useAuthor}</span><button class="clear-filter-btn" data-type="author">×</button></div>`;
} else if (useTag) {
filterInfo = `<div class="filter-info"><span class="filter-label">标签:</span><span class="filter-value">${useTag}</span><button class="clear-filter-btn" data-type="tag">×</button></div>`;
}
const listHtml = html || '<div class="empty-state"><p>暂无小说</p></div>';
$('#novel-list').html(filterInfo ? (filterInfo + listHtml) : listHtml);
}
// 更新加载更多按钮
if (hasMore && filteredNovels.length > 0) {
$('#load-more').html('<button class="load-more-btn">加载更多</button>');
} else {
$('#load-more').html('<span style="color:var(--text-muted);font-size:14px;">没有更多了</span>');
}
currentPage = page;
// 绑定图片错误处理
$('#novel-list img').on('error', function() {
handleCoverError(this);
});
// 重新绑定清除筛选按钮事件(只在非追加模式下绑定,避免重复绑定)
if (!append) {
$(document).off('click', '.clear-filter-btn').on('click', '.clear-filter-btn', function(e) {
e.preventDefault();
e.stopPropagation();
const filterType = $(this).data('type');
if (filterType === 'author') {
currentAuthor = null;
loadNovels(0, currentTag, null);
} else if (filterType === 'tag') {
currentTag = null;
$('.tag-btn').removeClass('active');
loadNovels(0, null, currentAuthor);
}
});
}
} catch (error) {
console.error('加载小说失败:', error);
Toast.show('加载失败,请重试');
} finally {
isLoadingMore = false;
}
}
function renderNovelCard(novel, inShelf = false) {
const synopsis = cleanContent(novel.synopsis).substring(0, 80);
const tags = (novel.tags || []).slice(0, 2);
// 检查书架状态(如果没有传入,则从缓存中检查)
const isInShelf = inShelf !== undefined ? inShelf : Store.isInShelfCache(novel.id);
return `
<div class="novel-card" data-novel-id="${novel.id}">
<div class="novel-cover">
<img src="${novel.cover || ''}" alt="${novel.name}" loading="lazy">
${isInShelf ? '<div class="shelf-badge" title="已在书架"><svg viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg><span class="shelf-badge-text">已收藏</span></div>' : ''}
</div>
<div class="novel-info">
<div class="novel-title text-ellipsis">${novel.name}</div>
<div class="novel-author clickable-author" data-author="${novel.author || ''}">${novel.author || '未知'}</div>
<div class="novel-synopsis text-clamp-2">${synopsis}...</div>
<div class="novel-tags">
${tags.map(t => `<span class="tag">${t}</span>`).join('')}
</div>
</div>
</div>
`;
}
// ========== 历史页面 ==========
async function renderHistory() {
const $content = $('#app-content');
$content.html(`
<div class="history-page page">
<div class="history-header">
<h2 class="history-title">阅读历史</h2>
<button class="clear-history-btn" id="clear-history">清空</button>
</div>
<div class="history-list" id="history-list">
${renderSkeletonList(5)}
</div>
</div>
`);
try {
const userId = Store.getUserId();
const data = await API.getHistory({ userId, page: 0, size: 50 });
const items = data.content || [];
if (items.length === 0) {
$('#history-list').html(`
<div class="empty-state">
<svg viewBox="0 0 24 24"><path d="M13 3c-4.97 0-9 4.03-9 9H1l3.89 3.89.07.14L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42C8.27 19.99 10.51 21 13 21c4.97 0 9-4.03 9-9s-4.03-9-9-9zm-1 5v5l4.28 2.54.72-1.21-3.5-2.08V8H12z"/></svg>
<h3>暂无阅读记录</h3>
<p>开始阅读后会在这里显示</p>
</div>
`);
return;
}
// 获取小说详情和章节信息
const historyWithDetails = await Promise.all(
items.map(async (item) => {
try {
const [novel, chapter] = await Promise.all([
API.getNovelDetail(item.novelId),
API.getChapterById(item.chapterId)
]);
return { ...item, novel, chapter };
} catch {
return null;
}
})
);
let html = '';
historyWithDetails.filter(Boolean).forEach(item => {
html += renderHistoryItem(item);
});
$('#history-list').html(html || '<div class="empty-state"><p>暂无记录</p></div>');
// 绑定图片错误处理
$('#history-list img').on('error', function() {
handleCoverError(this);
});
} catch (error) {
console.error('加载历史失败:', error);
Toast.show('加载失败,请重试');
}
}
function renderHistoryItem(item) {
return `
<div class="history-item" data-novel-id="${item.novelId}" data-chapter-index="${item.chapter?.index || 0}">
<div class="novel-cover">
<img src="${item.novel?.cover || ''}" alt="${item.novel?.name || ''}" loading="lazy">
</div>
<div class="history-info">
<div class="novel-title text-ellipsis">${item.novel?.name || '未知'}</div>
<div class="history-chapter text-ellipsis">${item.chapter?.title || '未知章节'}</div>
<div class="history-time">${formatTime(item.lastReadAt)}</div>
</div>
<div class="item-actions">
<button class="delete-history-btn" data-novel-id="${item.novelId}">
<svg viewBox="0 0 24 24"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>
</button>
</div>
</div>
`;
}
// ========== 书签页面 ==========
async function renderBookmarks() {
const $content = $('#app-content');
$content.html(`
<div class="bookmarks-page page">
<div class="bookmarks-header">
<h2 class="bookmarks-title">我的书签</h2>
</div>
<div class="bookmarks-list" id="bookmarks-list">
${renderSkeletonList(5)}
</div>
</div>
`);
try {
const userId = Store.getUserId();
const data = await API.getBookmarks({ userId, page: 0, size: 50 });
const items = data.content || [];
if (items.length === 0) {
$('#bookmarks-list').html(`
<div class="empty-state">
<svg viewBox="0 0 24 24"><path d="M17 3H7c-1.1 0-1.99.9-1.99 2L5 21l7-3 7 3V5c0-1.1-.9-2-2-2z"/></svg>
<h3>暂无书签</h3>
<p>阅读时点击书签按钮添加</p>
</div>
`);
return;
}
// 获取小说和章节详情
const bookmarksWithDetails = await Promise.all(
items.map(async (item) => {
try {
const [novel, chapter] = await Promise.all([
API.getNovelDetail(item.novelId),
API.getChapterById(item.chapterId)
]);
return { ...item, novel, chapter };
} catch {
return null;
}
})
);
let html = '';
bookmarksWithDetails.filter(Boolean).forEach(item => {
html += renderBookmarkItem(item);
});
$('#bookmarks-list').html(html || '<div class="empty-state"><p>暂无书签</p></div>');
// 绑定图片错误处理
$('#bookmarks-list img').on('error', function() {
handleCoverError(this);
});
} catch (error) {
console.error('加载书签失败:', error);
Toast.show('加载失败,请重试');
}
}
function renderBookmarkItem(item) {
return `
<div class="bookmark-item" data-novel-id="${item.novelId}" data-chapter-index="${item.chapter?.index || 0}">
<div class="novel-cover">
<img src="${item.novel?.cover || ''}" alt="${item.novel?.name || ''}" loading="lazy">
</div>
<div class="bookmark-info">
<div class="novel-title text-ellipsis">${item.novel?.name || '未知'}</div>
<div class="bookmark-chapter text-ellipsis">${item.chapter?.title || '未知章节'}</div>
${item.note ? `<div class="bookmark-note">${item.note}</div>` : ''}
<div class="history-time">${formatTime(item.createdAt)}</div>
</div>
<div class="item-actions">
<button class="delete-bookmark-btn" data-bookmark-id="${item.id}">
<svg viewBox="0 0 24 24"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>
</button>
</div>
</div>
`;
}
// ========== 小说详情页面 ==========
async function renderDetail(novelId) {
const $content = $('#app-content');
$content.html(`
<div class="detail-page">
<div class="detail-header">
<div class="detail-cover-wrap">
<div class="novel-cover detail-cover skeleton"></div>
<div class="detail-meta">
<div class="skeleton" style="height:24px;width:80%;"></div>
<div class="skeleton" style="height:18px;width:50%;"></div>
</div>
</div>
</div>
</div>
`);
try {
const novel = await API.getNovelDetail(novelId);
Store.setState('currentNovel', novel);
// 检查是否在书架
const inShelf = Store.isInShelfCache(novelId);
// 获取阅读位置
const position = Store.getReadingPosition(novelId);
$content.html(renderDetailPage(novel, inShelf, position));
// 加载前几章节
loadPreviewChapters(novelId);
// 绑定图片错误处理
$('.detail-cover img').on('error', function() {
handleCoverError(this);
});
} catch (error) {
console.error('加载详情失败:', error);
Toast.show('加载失败,请重试');
}
}
function renderDetailPage(novel, inShelf, position) {
const synopsis = cleanContent(novel.synopsis);
const tags = novel.tags || [];
return `
<div class="detail-page">
<div class="detail-header">
<div class="detail-cover-wrap">
<div class="novel-cover detail-cover">
<img src="${novel.cover || ''}" alt="${novel.name}">
</div>
<div class="detail-meta">
<h1 class="detail-title">${novel.name}</h1>
<div class="detail-author clickable-author" data-author="${novel.author || ''}">${novel.author || '未知'}</div>
<div class="detail-stats">
<div class="detail-stat">
<div class="detail-stat-value">${novel.chapterCount || 0}</div>
<div class="detail-stat-label">章节</div>
</div>
<div class="detail-stat">
<div class="detail-stat-value">${novel.status === 1 ? '完结' : '连载'}</div>
<div class="detail-stat-label">状态</div>
</div>
</div>
</div>
</div>
</div>
<div class="detail-tags">
${tags.map(t => `<span class="tag">${t}</span>`).join('')}
</div>
<div class="detail-synopsis">
<h3>简介</h3>
<p class="text-clamp-3" id="synopsis-text">${synopsis}</p>
<button class="expand-btn" id="expand-synopsis">展开</button>
</div>
<div class="detail-chapters">
<h3>
目录
<span class="chapter-count">共${novel.chapterCount || 0}章</span>
</h3>
<div class="chapters-preview" id="chapters-preview">
<div class="chapter-loading">
<div class="loading-spinner"></div>
<span>加载中...</span>
</div>
</div>
<button class="view-all-chapters" id="view-all-chapters" data-novel-id="${novel.id}">
查看全部章节
<svg viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>
</button>
</div>
<div class="detail-actions">
<button class="btn-shelf ${inShelf ? 'in-shelf' : ''}" id="btn-shelf" data-novel-id="${novel.id}">
<svg viewBox="0 0 24 24"><path d="${inShelf ? 'M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z' : 'M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z'}"/></svg>
${inShelf ? '已在书架' : '加入书架'}
</button>
<button class="btn-read" id="btn-read" data-novel-id="${novel.id}" data-chapter-index="${position?.chapterIndex || 0}">
${position ? '继续阅读' : '开始阅读'}
</button>
</div>
</div>
`;
}
async function loadPreviewChapters(novelId) {
try {
const data = await API.getChapters(novelId, { page: 0, size: 5 });
const chapters = data.content || [];
let html = '';
chapters.forEach(chapter => {
html += `
<div class="chapter-item" data-novel-id="${novelId}" data-chapter-index="${chapter.index}">
<span class="chapter-index">${chapter.index + 1}</span>
<span class="chapter-title text-ellipsis">${chapter.title}</span>
</div>
`;
});
$('#chapters-preview').html(html || '<p style="padding:16px;color:var(--text-muted);">暂无章节</p>');
} catch (error) {
console.error('加载章节失败:', error);
}
}
// ========== 搜索功能 ==========
async function renderSearchResults(keyword) {
const $results = $('#search-results');
$results.html('<div class="chapter-loading"><div class="loading-spinner"></div><span>搜索中...</span></div>');
// 保存当前搜索关键词用于高亮
currentSearchKeyword = keyword;
try {
const data = await API.searchNovels({ keyword, page: 0, size: 30 });
const novels = data.content || [];
if (novels.length === 0) {
$results.html('<div class="search-hint">未找到相关小说</div>');
return;
}
let html = '<div class="novel-list">';
novels.forEach(novel => {
const isInShelf = Store.isInShelfCache(novel.id);
html += renderNovelCard(novel, isInShelf, keyword);
});
html += '</div>';
$results.html(html);
// 绑定图片错误处理
$results.find('img').on('error', function() {
handleCoverError(this);
});
} catch (error) {
console.error('搜索失败:', error);
$results.html('<div class="search-hint">搜索失败,请重试</div>');
}
}
async function renderSearchByTag(tag) {
const $results = $('#search-results');
$results.html('<div class="chapter-loading"><div class="loading-spinner"></div><span>加载中...</span></div>');
try {
const data = await API.searchNovels({ tag, page: 0, size: 30 });
const novels = data.content || [];
if (novels.length === 0) {
$results.html('<div class="search-hint">该标签下暂无小说</div>');
return;
}
let html = '<div class="novel-list">';
novels.forEach(novel => {
const isInShelf = Store.isInShelfCache(novel.id);
html += renderNovelCard(novel, isInShelf);
});
html += '</div>';
$results.html(html);
// 绑定图片错误处理
$results.find('img').on('error', function() {
handleCoverError(this);
});
} catch (error) {
console.error('加载失败:', error);
$results.html('<div class="search-hint">加载失败,请重试</div>');
}
}
// ========== 骨架屏 ==========
function renderSkeletonGrid(count) {
let html = '';
for (let i = 0; i < count; i++) {
html += `
<div class="shelf-item">
<div class="novel-grid-card">
<div class="novel-cover skeleton"></div>
<div class="novel-info">
<div class="skeleton" style="height:14px;width:80%;margin-bottom:4px;"></div>
<div class="skeleton" style="height:12px;width:50%;"></div>
</div>
</div>
</div>
`;
}
return html;
}
function renderSkeletonList(count) {
let html = '';
for (let i = 0; i < count; i++) {
html += `
<div class="skeleton-card">
<div class="skeleton skeleton-cover"></div>
<div class="skeleton-info">
<div class="skeleton skeleton-title"></div>
<div class="skeleton skeleton-text"></div>
<div class="skeleton skeleton-desc"></div>
</div>
</div>
`;
}
return html;
}
// 按作者搜索(公开函数)
function searchByAuthor(author) {
if (!author) return;
// 切换到发现页面
if (window.location.hash !== '#/explore') {
window.location.hash = '#/explore';
// 等待页面渲染完成后加载该作者的作品
setTimeout(() => {
loadNovels(0, null, author);
}, 300);
} else {
// 如果已经在发现页面,直接加载
loadNovels(0, null, author);
}
}
// 公开API
return {
renderShelf,
renderExplore,
renderHistory,
renderBookmarks,
renderDetail,
renderSearchResults,
renderSearchByTag,
loadNovels,
searchByAuthor,
currentPage: () => currentPage,
hasMore: () => hasMore
};
})();