From ff47d331ae7410964b6ed2b6ba9c6479690d037a Mon Sep 17 00:00:00 2001 From: hiep200311 Date: Wed, 17 Jun 2026 16:58:50 +0700 Subject: [PATCH 1/3] ui: modernize UI to Mainframe design system --- routes/scraperZLibraryRoute.py | 46 +- routes/transcriptionRoute.py | 28 +- static/header/header.html | 31 +- static/header/headerLoader.js | 145 ++- .../scraperZLibraryMain.css | 436 +++++++ .../scraperZLibraryMain.js | 753 +++++++++++ .../transcriptionPages/transcriptionMain.css | 488 +++++++ .../transcriptionPages/transcriptionMain.js | 286 ++++ .../transcriptionReview.css | 155 +++ .../transcriptionPages/transcriptionReview.js | 3 + .../scraperZLibraryMain.html | 1159 +---------------- .../transcriptionPages/transcriptionMain.html | 526 +------- .../transcriptionReview.html | 58 +- 13 files changed, 2292 insertions(+), 1822 deletions(-) create mode 100644 static/scraperZLibraryPages/scraperZLibraryMain.css create mode 100644 static/scraperZLibraryPages/scraperZLibraryMain.js create mode 100644 static/transcriptionPages/transcriptionMain.css create mode 100644 static/transcriptionPages/transcriptionMain.js create mode 100644 static/transcriptionPages/transcriptionReview.css create mode 100644 static/transcriptionPages/transcriptionReview.js diff --git a/routes/scraperZLibraryRoute.py b/routes/scraperZLibraryRoute.py index da86e65..b825923 100644 --- a/routes/scraperZLibraryRoute.py +++ b/routes/scraperZLibraryRoute.py @@ -1,23 +1,23 @@ -# routes/scraperZLibraryRoute.py -from flask import Blueprint -from controllers.scraperZLibraryController import ScraperController - -# Create blueprint -scraperZLibraryRoute = Blueprint('scraperZLibrary', __name__) - -# Define routes -scraperZLibraryRoute.route('/', methods=['GET'])(ScraperController.index) -scraperZLibraryRoute.route('/api/search', methods=['POST'])(ScraperController.search_books) -scraperZLibraryRoute.route('/api/download/txt', methods=['POST'])(ScraperController.download_txt) -scraperZLibraryRoute.route('/api/download/json', methods=['POST'])(ScraperController.download_json) -scraperZLibraryRoute.route('/api/download/books', methods=['POST'])(ScraperController.download_books_zip) # New route -scraperZLibraryRoute.add_url_rule( - '/api/download/stream', - view_func=ScraperController.download_books_stream, - methods=['POST'], -) -scraperZLibraryRoute.add_url_rule( - '/api/download/file/', - view_func=ScraperController.fetch_stored_file, - methods=['GET'], -) \ No newline at end of file +# routes/scraperZLibraryRoute.py +from flask import Blueprint +from controllers.scraperZLibraryController import ScraperController + +# Create blueprint +scraperZLibraryRoute = Blueprint('scraperZLibrary', __name__) + +# Define routes +scraperZLibraryRoute.route('/', methods=['GET'])(ScraperController.index) +scraperZLibraryRoute.route('/api/search', methods=['POST'])(ScraperController.search_books) +scraperZLibraryRoute.route('/api/download/txt', methods=['POST'])(ScraperController.download_txt) +scraperZLibraryRoute.route('/api/download/json', methods=['POST'])(ScraperController.download_json) +scraperZLibraryRoute.route('/api/download/books', methods=['POST'])(ScraperController.download_books_zip) # New route +scraperZLibraryRoute.add_url_rule( + '/api/download/stream', + view_func=ScraperController.download_books_stream, + methods=['POST'], +) +scraperZLibraryRoute.add_url_rule( + '/api/download/file/', + view_func=ScraperController.fetch_stored_file, + methods=['GET'], +) \ No newline at end of file diff --git a/routes/transcriptionRoute.py b/routes/transcriptionRoute.py index 88c82e9..40679ec 100644 --- a/routes/transcriptionRoute.py +++ b/routes/transcriptionRoute.py @@ -1,14 +1,14 @@ -# routes/transcriptionRoute.py -from flask import Blueprint -from controllers.transcriptionController import TranscriptionController - -# Create blueprint -transcription_bp = Blueprint('transcription', __name__) - -# Define routes -transcription_bp.route('/', methods=['GET'])(TranscriptionController.index) -transcription_bp.route('/upload-chunk', methods=['POST'])(TranscriptionController.upload_chunk) -transcription_bp.route('/status/', methods=['GET'])(TranscriptionController.status) -transcription_bp.route('/transcript/', methods=['GET'])(TranscriptionController.download_transcript) -transcription_bp.route('/view/', methods=['GET'])(TranscriptionController.view_transcript) -transcription_bp.route('/jobs', methods=['GET'])(TranscriptionController.list_jobs) \ No newline at end of file +# routes/transcriptionRoute.py +from flask import Blueprint +from controllers.transcriptionController import TranscriptionController + +# Create blueprint +transcription_bp = Blueprint('transcription', __name__) + +# Define routes +transcription_bp.route('/', methods=['GET'])(TranscriptionController.index) +transcription_bp.route('/upload-chunk', methods=['POST'])(TranscriptionController.upload_chunk) +transcription_bp.route('/status/', methods=['GET'])(TranscriptionController.status) +transcription_bp.route('/transcript/', methods=['GET'])(TranscriptionController.download_transcript) +transcription_bp.route('/view/', methods=['GET'])(TranscriptionController.view_transcript) +transcription_bp.route('/jobs', methods=['GET'])(TranscriptionController.list_jobs) \ No newline at end of file diff --git a/static/header/header.html b/static/header/header.html index 608fd00..71979b9 100644 --- a/static/header/header.html +++ b/static/header/header.html @@ -2,16 +2,27 @@
diff --git a/static/header/headerLoader.js b/static/header/headerLoader.js index 8cecb41..22bed52 100644 --- a/static/header/headerLoader.js +++ b/static/header/headerLoader.js @@ -11,96 +11,153 @@ version: "1.0", }; - // Styles const styles = ` `; + + // Load header function async function loadHeader() { try { diff --git a/static/scraperZLibraryPages/scraperZLibraryMain.css b/static/scraperZLibraryPages/scraperZLibraryMain.css new file mode 100644 index 0000000..0726b59 --- /dev/null +++ b/static/scraperZLibraryPages/scraperZLibraryMain.css @@ -0,0 +1,436 @@ +/* ═══════════════════════════════════════════════════════════ + scraperZLibraryMain.css · Mainframe Design System v3 + ═══════════════════════════════════════════════════════════ */ + +:root { + --font-heading: 'HelveticaNowDisplay-Medium','Helvetica Neue',Arial,sans-serif; + --font-body: 'HelveticaNowDisplayW01-Rg','Helvetica Neue',Arial,sans-serif; + --ink: #0a0a0a; + --ink-70: rgba(10,10,10,0.70); + --ink-45: rgba(10,10,10,0.45); + --ink-20: rgba(10,10,10,0.20); + --ink-10: rgba(10,10,10,0.10); + --ink-06: rgba(10,10,10,0.06); + --ink-03: rgba(10,10,10,0.03); + --surface: #f4f3ef; + --surface-2: #eeecea; + --card: #ffffff; + --radius-pill: 9999px; + --radius-xl: 24px; + --radius-lg: 18px; + --radius-md: 12px; + --shadow-xs: 0 1px 2px rgba(0,0,0,0.06); + --shadow-sm: 0 2px 10px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.04); + --shadow-md: 0 6px 28px rgba(0,0,0,0.10), 0 2px 6px rgba(0,0,0,0.05); + --shadow-inset: inset 0 1px 0 rgba(255,255,255,0.8); +} + +@keyframes fade-up { + from { opacity:0; transform:translateY(14px); } + to { opacity:1; transform:none; } +} +@keyframes spin { to { transform:rotate(360deg); } } + +*,*::before,*::after { margin:0; padding:0; box-sizing:border-box; } + +html { + font-family: var(--font-body); + -webkit-font-smoothing: antialiased; + background: var(--surface); + color: var(--ink); +} + +body { + min-height: 100vh; + background: var(--surface); + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='300' height='300' filter='url(%23n)' opacity='0.028'/%3E%3C/svg%3E"); + padding: 0 28px 100px; + font-family: var(--font-body); +} + +/* ─── Layout ─────────────────────────────────────────────── */ +.container { + max-width: 1260px; + margin: 0 auto; + animation: fade-up .65s cubic-bezier(.22,1,.36,1) backwards; + animation-delay: .04s; +} + +/* ─── Page Header ────────────────────────────────────────── */ +.header { + padding: 42px 0 28px; + border-bottom: 1px solid var(--ink-10); + margin-bottom: 0; +} + +/* Eyebrow */ +.header::before { + content: '✦ Z-Library Search'; + display: inline-block; + font-size: 11px; + font-weight: 600; + letter-spacing: .1em; + text-transform: uppercase; + color: var(--ink-45); + margin-bottom: 14px; + padding: 6px 14px; + background: var(--card); + border: 1px solid var(--ink-10); + border-radius: var(--radius-pill); + box-shadow: var(--shadow-xs); +} + +.header h1 { + font-family: var(--font-heading); + font-size: clamp(30px, 5.5vw, 50px); + font-weight: 500; + letter-spacing: -0.055em; + line-height: 1.0; + color: var(--ink); + margin-bottom: 8px; +} + +.header p { + font-size: 14px; + color: var(--ink-45); + line-height: 1.5; +} + +/* ─── Search Section ─────────────────────────────────────── */ +.search-section { + padding: 26px 0; + border-bottom: 1px solid var(--ink-06); +} + +.search-form { + display: flex; + gap: 10px; + align-items: flex-end; + flex-wrap: wrap; +} + +.form-group { flex:1; min-width:200px; } + +.form-group label { + display: block; + margin-bottom: 7px; + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: .1em; + color: var(--ink-45); +} + +.form-group input { + width: 100%; + padding: 11px 18px; + border: 1px solid var(--ink-10); + border-radius: var(--radius-pill); + font-size: 14px; + font-family: var(--font-body); + background: var(--card); + color: var(--ink); + box-shadow: var(--shadow-xs), var(--shadow-inset); + transition: border-color .2s, box-shadow .2s; + -webkit-appearance: none; +} +.form-group input:focus { + outline: none; + border-color: var(--ink-45); + box-shadow: 0 0 0 3px rgba(10,10,10,.07), var(--shadow-inset); +} +.form-group input::placeholder { color: var(--ink-20); } +.form-group small { + display: block; + margin-top: 6px; + font-size: 11px; + color: var(--ink-45); + padding-left: 10px; +} + +/* ─── Checkbox pills ─────────────────────────────────────── */ +.checkbox-group { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-top: 16px; +} + +.checkbox-group label { + display: inline-flex; + align-items: center; + gap: 7px; + font-size: 13px; + letter-spacing: -0.01em; + color: var(--ink-70); + background: var(--card); + border: 1px solid var(--ink-10); + border-radius: var(--radius-pill); + padding: 6px 16px; + cursor: pointer; + box-shadow: var(--shadow-xs), var(--shadow-inset); + user-select: none; + transition: background .18s, color .18s, border-color .18s, box-shadow .18s; +} +.checkbox-group label:hover { + background: var(--ink); + color: #fff; + border-color: var(--ink); + box-shadow: var(--shadow-sm); +} +.checkbox-group input[type="checkbox"] { + accent-color: var(--ink); + width: 13px; height: 13px; cursor: pointer; +} + +/* ─── Buttons ────────────────────────────────────────────── */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 9px 22px; + border-radius: var(--radius-pill); + font-size: 13px; + font-family: var(--font-body); + letter-spacing: -0.01em; + cursor: pointer; + border: 1px solid var(--ink-10); + text-decoration: none; + white-space: nowrap; + background: var(--card); + color: var(--ink); + box-shadow: var(--shadow-xs), var(--shadow-inset); + margin: 0 5px 5px 0; + line-height: 1; + position: relative; + overflow: hidden; + transition: background .2s, color .2s, border-color .2s, box-shadow .2s, + transform .18s cubic-bezier(.22,1,.36,1); +} +.btn:hover:not(:disabled) { + background: var(--ink); + color: #fff; + border-color: var(--ink); + box-shadow: var(--shadow-md); + transform: translateY(-2px); +} +.btn:active:not(:disabled) { transform:scale(.97) translateY(0); box-shadow:var(--shadow-xs); } +.btn:disabled { opacity:.35; cursor:not-allowed; } + +.btn-primary { background:var(--ink); color:#fff; border-color:var(--ink); box-shadow:var(--shadow-sm); } +.btn-primary:hover:not(:disabled) { background:#1a1a1a; } + +.btn-download { background:var(--ink); color:#fff; border-color:var(--ink); box-shadow:var(--shadow-sm); } +.btn-download:hover:not(:disabled) { background:#1a1a1a; } + +/* ─── Results Section ────────────────────────────────────── */ +.results-section { padding: 26px 0; } + +/* ─── Stats Bar — horizontal card row ───────────────────── */ +.stats-bar { + display: flex; + background: var(--card); + border: 1px solid var(--ink-10); + border-radius: var(--radius-xl); + margin-bottom: 26px; + overflow: hidden; + box-shadow: var(--shadow-sm), var(--shadow-inset); + flex-wrap: wrap; +} +.stat-item { + flex: 1; + min-width: 120px; + padding: 22px 26px; + border-right: 1px solid var(--ink-06); + position: relative; +} +.stat-item:last-child { border-right: none; } +.stat-item::before { + content: ''; + position: absolute; + top: 0; left: 26px; right: 26px; + height: 1.5px; + background: linear-gradient(90deg, transparent 0%, var(--ink-10) 50%, transparent 100%); +} +.stat-label { + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: .1em; + color: var(--ink-45); + margin-bottom: 6px; +} +.stat-value { + font-family: var(--font-heading); + font-size: clamp(24px, 3.5vw, 34px); + font-weight: 500; + letter-spacing: -0.05em; + color: var(--ink); + line-height: 1.05; +} + +/* ─── Books Grid ─────────────────────────────────────────── */ +.books-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(310px, 1fr)); + gap: 1px; + background: var(--ink-10); + border: 1px solid var(--ink-10); + border-radius: var(--radius-xl); + overflow: hidden; + margin-top: 18px; + box-shadow: var(--shadow-sm); +} + +.book-card { + background: var(--card); + padding: 22px 20px 18px; + position: relative; + transition: background .18s; +} +.book-card:hover { background: #fdfcf9; } +.book-card.selected { + background: rgba(10,10,10,0.025); + box-shadow: inset 3px 0 0 var(--ink); +} + +.book-checkbox { + position: absolute; + top: 18px; right: 18px; + width: 15px; height: 15px; + accent-color: var(--ink); + cursor: pointer; +} + +.book-title { + font-size: 14px; + font-weight: 500; + letter-spacing: -0.02em; + color: var(--ink); + margin-bottom: 4px; + line-height: 1.4; + padding-right: 26px; +} +.book-authors { + font-size: 12px; + color: var(--ink-45); + margin-bottom: 14px; +} + +.book-details { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 8px 10px; + margin-bottom: 14px; + padding-bottom: 14px; + border-bottom: 1px solid var(--ink-06); +} +.detail-item { font-size: 11px; } +.detail-label { + color: var(--ink-45); + text-transform: uppercase; + font-size: 9.5px; + letter-spacing: .07em; + font-weight: 700; + margin-bottom: 1px; +} +.detail-value { color: var(--ink-70); } + +.book-link { + display: inline-block; + font-size: 12px; + color: var(--ink); + text-decoration: underline; + text-underline-offset: 2px; + text-decoration-color: var(--ink-20); + transition: text-decoration-color .18s; +} +.book-link:hover { text-decoration-color: var(--ink); } + +/* ─── Loading ─────────────────────────────────────────────── */ +.loading { text-align:center; padding:80px 20px; display:none; } +.spinner { + width:26px; height:26px; + border:1.5px solid var(--ink-10); + border-top-color: var(--ink); + border-radius:50%; + animation:spin .7s linear infinite; + margin:0 auto 16px; +} +.loading p { font-size:13px; color:var(--ink-45); } + +/* ─── Misc ───────────────────────────────────────────────── */ +.progress-container { margin-top:12px; } +.progress-text { font-size:11px; color:var(--ink-45); margin-top:5px; } + +.error-message { + background:var(--card); + border:1px solid var(--ink-10); + color:var(--ink-70); + padding:12px 18px; + border-radius:var(--radius-md); + margin:14px 0; + font-size:13px; + display:none; + box-shadow:var(--shadow-xs); +} + +.empty-state { text-align:center; padding:80px 24px; color:var(--ink-45); } +.empty-state h3 { + font-family:var(--font-heading); + font-size:20px; + font-weight:500; + letter-spacing:-0.04em; + color:var(--ink); + margin-bottom:8px; +} +.empty-state p { font-size:14px; } + +.download-section { display:flex; gap:6px; flex-wrap:wrap; margin:18px 0; } + +.selection-bar { + display:flex; + align-items:center; + justify-content:space-between; + padding:12px 0; + border-bottom:1px solid var(--ink-06); + margin-bottom:14px; + flex-wrap:wrap; + gap:10px; +} +.selection-controls { display:flex; align-items:center; gap:10px; } +.selection-controls label { + display:inline-flex; + align-items:center; + gap:7px; + font-size:13px; + color:var(--ink); + cursor:pointer; +} +.selection-controls label input { accent-color:var(--ink); } +.selection-info { font-size:12px; color:var(--ink-45); } + +.stats-detail { margin-top:30px; padding-top:22px; border-top:1px solid var(--ink-10); } +.stats-detail h3 { + font-size:10px; + font-weight:700; + letter-spacing:.1em; + text-transform:uppercase; + color:var(--ink-45); + margin-bottom:16px; +} + +hr { border:none; border-top:1px solid var(--ink-06); margin:14px 0; } + +.load-more-container { margin-top:28px; text-align:center; } +.load-more-btn { width:100%; padding:13px; font-size:13px; } + +#resultCountHeader { + font-size:10px; + font-weight:700; + letter-spacing:.1em; + text-transform:uppercase; + color:var(--ink-45); + margin:20px 0 14px; +} diff --git a/static/scraperZLibraryPages/scraperZLibraryMain.js b/static/scraperZLibraryPages/scraperZLibraryMain.js new file mode 100644 index 0000000..4e24d96 --- /dev/null +++ b/static/scraperZLibraryPages/scraperZLibraryMain.js @@ -0,0 +1,753 @@ +let currentResults = null; +let searchInProgress = false; +let downloadInProgress = false; +let loadMoreInProgress = false; +let selectedBooks = new Set(); +let currentQuery = ""; +let currentPage = 1; +let totalPagesAvailable = 0; +let headlessMode = true; +let totalBooksCount = 0; + +async function searchBooks(resetResults = true) { + if (searchInProgress) { + return; + } + + const query = document.getElementById("query").value.trim(); + const headless = document.getElementById("headless").checked; + + headlessMode = headless; + currentQuery = query; + + if (!query) { + showError("Vui lòng nhập từ khóa tìm kiếm"); + return; + } + + if (query.length < 2) { + showError("Từ khóa phải có ít nhất 2 ký tự"); + return; + } + + searchInProgress = true; + + if (resetResults) { + selectedBooks.clear(); + currentPage = 1; + currentResults = null; + } + + document.getElementById("loadingIndicator").style.display = "block"; + document.getElementById("loadingMessage").textContent = resetResults + ? "Đang tìm kiếm sách..." + : `Đang tải trang ${currentPage + 1}...`; + document.getElementById("errorMessage").style.display = "none"; + + if (resetResults) { + document.getElementById("resultsContent").innerHTML = ""; + } + + document.getElementById("progressText").textContent = + "Đang khởi tạo..."; + + const searchBtn = document.getElementById("searchBtn"); + searchBtn.disabled = true; + searchBtn.textContent = resetResults ? "Đang tìm..." : "Đang tải..."; + + try { + const nextPage = resetResults ? 1 : currentPage + 1; + + const response = await fetch("/scraperZLibrary/api/search", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + query: query, + page: nextPage, + headless: headless, + }), + }); + + const data = await response.json(); + + if (data.success) { + if (resetResults) { + currentResults = data; + currentPage = data.current_page || 1; + totalPagesAvailable = data.total_pages || 1; + totalBooksCount = data.total_books_count || data.books.length; + displayResults(data); + } else { + // Merge new books with existing results + if (currentResults) { + currentResults.books = [...currentResults.books, ...data.books]; + currentResults.total_count = currentResults.books.length; + currentPage = data.current_page; + totalPagesAvailable = data.total_pages; + totalBooksCount = data.total_books_count || totalBooksCount; + appendBooksToGrid( + data.books, + currentResults.books.length - data.books.length, + ); + updateStatsBar(currentResults); + updateResultCount(); + updateSelectionInfo(); + } else { + currentResults = data; + currentPage = data.current_page || 1; + totalPagesAvailable = data.total_pages || 1; + totalBooksCount = data.total_books_count || data.books.length; + displayResults(data); + } + } + } else { + showError(data.error || "Có lỗi xảy ra khi tìm kiếm"); + } + } catch (error) { + console.error("Search error:", error); + showError("Không thể kết nối đến máy chủ."); + } finally { + searchInProgress = false; + loadMoreInProgress = false; + document.getElementById("loadingIndicator").style.display = "none"; + searchBtn.disabled = false; + searchBtn.textContent = "Tìm kiếm"; + } +} + +async function loadMoreBooks() { + if (loadMoreInProgress || searchInProgress) { + return; + } + + if (currentPage >= totalPagesAvailable) { + alert("Đã tải hết tất cả các trang!"); + return; + } + + loadMoreInProgress = true; + await searchBooks(false); +} + +function appendBooksToGrid(newBooks, startIndex) { + const booksGrid = document.querySelector(".books-grid"); + if (!booksGrid) return; + + let html = ""; + newBooks.forEach((book, idx) => { + const index = startIndex + idx; + const bookId = `book-${index}`; + html += ` +
+ +
${index + 1}. ${escapeHtml(book.title)}
+
${escapeHtml(book.authors.join(", ") || "Không rõ")}
+ +
+
+
NXB
+
${escapeHtml(book.publisher)}
+
+
+
Năm
+
${escapeHtml(book.year)}
+
+
+
Số trang
+
${escapeHtml(book.pages)}
+
+
+
Ngôn ngữ
+
${escapeHtml(book.language)}
+
+
+
Định dạng
+
${escapeHtml(book.file)}
+
+
+ + ${ + book.link !== "N/A" + ? `Xem sách →` + : 'Không có liên kết' + } +
+ `; + }); + + booksGrid.insertAdjacentHTML("beforeend", html); + + // Update load more button + updateLoadMoreButton(); +} + +function updateResultCount() { + const resultHeader = document.querySelector("#resultCountHeader"); + if (resultHeader) { + resultHeader.textContent = `Kết quả (${currentResults.books.length} sách đã tải / ${totalBooksCount} tổng cộng)`; + } +} + +function updateStatsBar(data) { + const statsBar = document.querySelector(".stats-bar"); + if (statsBar) { + statsBar.innerHTML = ` +
+
Tổng số sách
+
${totalBooksCount}
+
+
+
Từ khóa
+
"${escapeHtml(data.query)}"
+
+
+
Trang hiện tại
+
${currentPage} / ${totalPagesAvailable}
+
+ `; + } +} + +function updateLoadMoreButton() { + const loadMoreContainer = document.getElementById("loadMoreContainer"); + if (loadMoreContainer) { + if (currentPage >= totalPagesAvailable) { + loadMoreContainer.innerHTML = ` + + `; + } else { + loadMoreContainer.innerHTML = ` + + `; + } + } +} + +function displayResults(data) { + const resultsDiv = document.getElementById("resultsContent"); + + if (!data.books || data.books.length === 0) { + resultsDiv.innerHTML = ` +
+
🔍
+

Không tìm thấy sách

+

Thử từ khóa khác hoặc kiểm tra lại chính tả

+

Từ khóa: "${escapeHtml(data.query)}"

+
+ `; + return; + } + + currentPage = data.current_page || 1; + totalPagesAvailable = data.total_pages || 1; + totalBooksCount = data.total_books_count || data.books.length; + + let html = ` +
+
+
Tổng số sách
+
${totalBooksCount}
+
+
+
Từ khóa
+
"${escapeHtml(data.query)}"
+
+
+
Trang hiện tại
+
${currentPage} / ${totalPagesAvailable}
+
+
+ +
+ + + + +
+ +
+
+ + +
+
+ Đã chọn 0 sách +
+
+ +

+ Kết quả (${data.books.length} sách đã tải / ${totalBooksCount} tổng cộng) +

+
+ `; + + data.books.forEach((book, index) => { + const bookId = `book-${index}`; + html += ` +
+ +
${index + 1}. ${escapeHtml(book.title)}
+
${escapeHtml(book.authors.join(", ") || "Không rõ")}
+ +
+
+
NXB
+
${escapeHtml(book.publisher)}
+
+
+
Năm
+
${escapeHtml(book.year)}
+
+
+
Số trang
+
${escapeHtml(book.pages)}
+
+
+
Ngôn ngữ
+
${escapeHtml(book.language)}
+
+
+
Định dạng
+
${escapeHtml(book.file)}
+
+
+ + ${ + book.link !== "N/A" + ? `Xem sách →` + : 'Không có liên kết' + } +
+ `; + }); + + html += "
"; + + // Add load more button + html += `
`; + + if (currentPage < totalPagesAvailable) { + html += ` + + `; + } else { + html += ` + + `; + } + + html += `
`; + + // Add statistics if available + if (data.statistics && Object.keys(data.statistics).length > 0) { + html += '
'; + html += "

Thống kê

"; + html += '
'; + + const stats = data.statistics; + + if (stats.languages && Object.keys(stats.languages).length > 0) { + const langHtml = Object.entries(stats.languages) + .sort((a, b) => b[1] - a[1]) + .slice(0, 5) + .map(([lang, count]) => { + const percentage = ((count / totalBooksCount) * 100).toFixed(1); + return `
${escapeHtml(lang)}: ${count} (${percentage}%)
`; + }) + .join(""); + + html += ` +
+
Ngôn ngữ phổ biến
+
${langHtml}
+
+ `; + } + + if (stats.formats && Object.keys(stats.formats).length > 0) { + const formatHtml = Object.entries(stats.formats) + .sort((a, b) => b[1] - a[1]) + .slice(0, 5) + .map(([fmt, count]) => { + const percentage = ((count / totalBooksCount) * 100).toFixed(1); + return `
${escapeHtml(fmt)}: ${count} (${percentage}%)
`; + }) + .join(""); + + html += ` +
+
Định dạng phổ biến
+
${formatHtml}
+
+ `; + } + + if (stats.years && Object.keys(stats.years).length > 0) { + const yearHtml = Object.entries(stats.years) + .sort((a, b) => parseInt(b[0]) - parseInt(a[0])) + .slice(0, 5) + .map( + ([year, count]) => + `
${escapeHtml(year)}: ${count} sách
`, + ) + .join(""); + + html += ` +
+
Năm xuất bản gần đây
+
${yearHtml}
+
+ `; + } + + html += "
"; + html += "
"; + } + + resultsDiv.innerHTML = html; + updateSelectionInfo(); + resultsDiv.scrollIntoView({ behavior: "smooth", block: "nearest" }); +} + +function toggleBookSelection(index, checked) { + if (checked) { + selectedBooks.add(index); + const card = document.getElementById(`book-${index}`); + if (card) card.classList.add("selected"); + } else { + selectedBooks.delete(index); + const card = document.getElementById(`book-${index}`); + if (card) card.classList.remove("selected"); + } + updateSelectionInfo(); + updateSelectAllCheckbox(); +} + +function toggleSelectAll(checked) { + if (!currentResults || !currentResults.books) return; + + if (checked) { + currentResults.books.forEach((_, index) => { + selectedBooks.add(index); + const card = document.getElementById(`book-${index}`); + if (card) card.classList.add("selected"); + const checkbox = document.querySelector( + `[data-book-index="${index}"]`, + ); + if (checkbox) checkbox.checked = true; + }); + } else { + selectedBooks.clear(); + document + .querySelectorAll(".book-card") + .forEach((card) => card.classList.remove("selected")); + document + .querySelectorAll(".book-checkbox") + .forEach((cb) => (cb.checked = false)); + } + updateSelectionInfo(); +} + +function clearSelection() { + selectedBooks.clear(); + document + .querySelectorAll(".book-card") + .forEach((card) => card.classList.remove("selected")); + document + .querySelectorAll(".book-checkbox") + .forEach((cb) => (cb.checked = false)); + const selectAllCheckbox = document.getElementById("selectAllCheckbox"); + if (selectAllCheckbox) selectAllCheckbox.checked = false; + updateSelectionInfo(); +} + +function updateSelectionInfo() { + const count = selectedBooks.size; + const infoElement = document.getElementById("selectionInfo"); + if (infoElement) { + infoElement.textContent = `Đã chọn ${count} / ${currentResults?.books.length || 0} sách`; + } +} + +function updateSelectAllCheckbox() { + if (!currentResults || !currentResults.books) return; + const selectAll = document.getElementById("selectAllCheckbox"); + if (selectAll) { + selectAll.checked = + selectedBooks.size === currentResults.books.length && + currentResults.books.length > 0; + } +} + +async function downloadSelectedBooks() { + if (downloadInProgress) { + alert("Đang tải sách, vui lòng đợi..."); + return; + } + if (!currentResults || !currentResults.books) { + alert("Không có sách để tải"); + return; + } + if (selectedBooks.size === 0) { + alert("Vui lòng chọn ít nhất một cuốn sách"); + return; + } + + const selectedBooksList = Array.from(selectedBooks) + .sort((a, b) => a - b) + .map((index) => currentResults.books[index]); + + downloadInProgress = true; + const headless = document.getElementById("headless").checked; + + // Show progress UI + document.getElementById("loadingIndicator").style.display = "block"; + document.getElementById("loadingMessage").textContent = + `Đang tải ${selectedBooksList.length} sách...`; + + let successCount = 0; + let failedCount = 0; + + try { + // POST to get the SSE stream + const response = await fetch("/scraperZLibrary/api/download/stream", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + books: selectedBooksList, + query: currentResults.query, + headless: headless, + }), + }); + + if (!response.ok) throw new Error("Không thể bắt đầu tải xuống"); + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ""; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split("\n\n"); + buffer = lines.pop(); // keep incomplete chunk + + for (const chunk of lines) { + const line = chunk.replace(/^data:\s*/, "").trim(); + if (!line) continue; + + let event; + try { + event = JSON.parse(line); + } catch { + continue; + } + + if (event.type === "progress") { + document.getElementById("loadingMessage").textContent = + `[${event.index}/${event.total}] Đang tải: ${event.title.substring(0, 50)}...`; + document.getElementById("progressText").textContent = + `Đã xong: ${successCount} ✓ Lỗi: ${failedCount} ✗`; + } else if (event.type === "ready") { + successCount++; + document.getElementById("progressText").textContent = + `Đã xong: ${successCount} ✓ Lỗi: ${failedCount} ✗`; + + // Trigger instant download via hidden + const a = document.createElement("a"); + a.href = `/scraperZLibrary/api/download/file/${event.token}`; + a.download = event.filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + + // Small gap so the browser doesn't block rapid-fire downloads + await new Promise((r) => setTimeout(r, 600)); + } else if (event.type === "error") { + failedCount++; + console.warn(`Failed: ${event.title} — ${event.message}`); + document.getElementById("progressText").textContent = + `Đã xong: ${successCount} ✓ Lỗi: ${failedCount} ✗`; + } else if (event.type === "done") { + document.getElementById("loadingMessage").textContent = + `Hoàn tất! ${event.success} thành công, ${event.failed} thất bại`; + } + } + } + } catch (error) { + console.error("Stream download error:", error); + alert("Không thể tải sách: " + error.message); + } finally { + downloadInProgress = false; + setTimeout(() => { + document.getElementById("loadingIndicator").style.display = "none"; + }, 2000); + } +} + +function showError(message) { + const errorDiv = document.getElementById("errorMessage"); + errorDiv.textContent = message; + errorDiv.style.display = "block"; + + document.getElementById("resultsContent").innerHTML = ` +
+
+

Tìm kiếm thất bại

+

${escapeHtml(message)}

+

Vui lòng thử lại với từ khóa khác.

+
+ `; + + errorDiv.scrollIntoView({ behavior: "smooth", block: "nearest" }); +} + +function downloadResults(format) { + if (!currentResults) { + alert("Không có kết quả để tải xuống"); + return; + } + + fetch(`/scraperZLibrary/api/download/${format}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(currentResults), + }) + .then((response) => { + if (!response.ok) { + throw new Error("Tải xuống thất bại"); + } + return response.blob(); + }) + .then((blob) => { + const url = window.URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `zlib_${currentResults.query.replace(/\s+/g, "_")}_ket_qua.${format}`; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + }) + .catch((error) => { + console.error("Download error:", error); + alert("Không thể tải file: " + error.message); + }); +} + +function copyResultsToClipboard() { + if (!currentResults) { + alert("Không có kết quả để sao chép"); + return; + } + + let summary = `KẾT QUẢ TÌM KIẾM Z-LIBRARY\n`; + summary += `========================\n`; + summary += `Từ khóa: ${currentResults.query}\n`; + summary += `Tổng số sách: ${totalBooksCount}\n`; + summary += `Đã tải: ${currentResults.books.length} sách\n`; + summary += `========================\n\n`; + + currentResults.books.slice(0, 20).forEach((book, index) => { + summary += `${index + 1}. ${book.title}\n`; + summary += ` Tác giả: ${book.authors.join(", ")}\n`; + summary += ` Năm: ${book.year} | Ngôn ngữ: ${book.language} | Định dạng: ${book.file}\n`; + summary += ` Link: ${book.link}\n\n`; + }); + + if (currentResults.books.length > 20) { + summary += `... và ${currentResults.books.length - 20} sách khác\n`; + } + + navigator.clipboard + .writeText(summary) + .then(() => { + alert("Đã sao chép tóm tắt!"); + }) + .catch((err) => { + console.error("Failed to copy:", err); + alert("Không thể sao chép"); + }); +} + +function escapeHtml(text) { + if (typeof text !== "string") return text; + const div = document.createElement("div"); + div.textContent = text; + return div.innerHTML; +} + +document + .getElementById("query") + .addEventListener("keypress", function (e) { + if (e.key === "Enter") { + searchBooks(true); + } + }); + +document.addEventListener("keydown", function (e) { + if ((e.ctrlKey || e.metaKey) && e.key === "k") { + e.preventDefault(); + document.getElementById("query").focus(); + } +}); + +const progressMessages = [ + "Đang khởi tạo...", + "Đang kết nối Z-Library...", + "Đang lấy dữ liệu sách...", + "Đang xử lý kết quả...", + "Sắp xong...", +]; + +let messageIndex = 0; +const progressInterval = setInterval(() => { + if (searchInProgress || downloadInProgress) { + messageIndex = (messageIndex + 1) % progressMessages.length; + const progressElement = document.getElementById("progressText"); + if (progressElement) { + progressElement.textContent = progressMessages[messageIndex]; + } + } +}, 2000); + +window.addEventListener("beforeunload", () => { + clearInterval(progressInterval); +}); diff --git a/static/transcriptionPages/transcriptionMain.css b/static/transcriptionPages/transcriptionMain.css new file mode 100644 index 0000000..16c10ba --- /dev/null +++ b/static/transcriptionPages/transcriptionMain.css @@ -0,0 +1,488 @@ +/* ═══════════════════════════════════════════════════════════ + transcriptionMain.css · Mainframe Design System v3 + ═══════════════════════════════════════════════════════════ */ + +:root { + --font-heading: 'HelveticaNowDisplay-Medium','Helvetica Neue',Arial,sans-serif; + --font-body: 'HelveticaNowDisplayW01-Rg','Helvetica Neue',Arial,sans-serif; + --ink: #0a0a0a; + --ink-70: rgba(10,10,10,0.70); + --ink-45: rgba(10,10,10,0.45); + --ink-20: rgba(10,10,10,0.20); + --ink-10: rgba(10,10,10,0.10); + --ink-05: rgba(10,10,10,0.05); + --surface: #f4f3ef; + --surface-2: #eeecea; + --card: #ffffff; + --radius-pill: 9999px; + --radius-xl: 24px; + --radius-lg: 18px; + --radius-md: 12px; + --shadow-xs: 0 1px 2px rgba(0,0,0,0.06); + --shadow-sm: 0 2px 10px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.04); + --shadow-md: 0 6px 28px rgba(0,0,0,0.10), 0 2px 6px rgba(0,0,0,0.05); + --shadow-inset: inset 0 1px 0 rgba(255,255,255,0.8); +} + +/* ─── Animations ─────────────────────────────────────────── */ +@keyframes fade-up { + from { opacity:0; transform:translateY(16px); } + to { opacity:1; transform:none; } +} +@keyframes spin { to { transform:rotate(360deg); } } + +/* ─── Base ───────────────────────────────────────────────── */ +*,*::before,*::after { margin:0; padding:0; box-sizing:border-box; } + +html { + font-family: var(--font-body); + -webkit-font-smoothing: antialiased; + background: var(--surface); + color: var(--ink); +} + +body { + min-height: 100vh; + background: var(--surface); + padding: 0 20px 100px; + font-family: var(--font-body); + /* subtle paper grain */ + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='300' height='300' filter='url(%23n)' opacity='0.028'/%3E%3C/svg%3E"); +} + +/* ─── Page Layout ────────────────────────────────────────── */ +.container { + width: 100%; + max-width: 600px; + margin: 0 auto; + padding-top: 48px; +} + +/* ─── Upload View ────────────────────────────────────────── */ +.upload-view { + animation: fade-up .7s cubic-bezier(.22,1,.36,1) backwards; + animation-delay: .05s; +} + +/* ── Section label above heading ── */ +.upload-view::before { + content: '✦ Powered by Whisper AI'; + display: block; + font-size: 11px; + font-weight: 600; + letter-spacing: .1em; + text-transform: uppercase; + color: var(--ink-45); + margin-bottom: 14px; + padding: 6px 14px; + background: var(--card); + border: 1px solid var(--ink-10); + border-radius: var(--radius-pill); + box-shadow: var(--shadow-xs); + display: inline-block; +} + +/* ─── Page Heading ───────────────────────────────────────── */ +.upload-view h1 { + font-family: var(--font-heading); + font-size: clamp(32px, 6vw, 52px); + font-weight: 500; + letter-spacing: -0.05em; + line-height: 1.0; + color: var(--ink); + margin-bottom: 12px; +} + +/* ─── Subtitle ───────────────────────────────────────────── */ +.subtitle { + font-size: 14px; + color: var(--ink-45); + line-height: 1.6; + margin-bottom: 36px; + max-width: 400px; +} + +/* ─── Drop Zone ──────────────────────────────────────────── */ +.upload-area { + background: var(--card); + border: 1.5px dashed var(--ink-20); + border-radius: var(--radius-xl); + padding: 0; + cursor: pointer; + margin-bottom: 16px; + position: relative; + overflow: hidden; + box-shadow: var(--shadow-sm), var(--shadow-inset); + transition: + border-color .25s ease, + box-shadow .25s ease, + transform .25s cubic-bezier(.22,1,.36,1); +} + +/* Dashed border via gradient trick for rounded corners */ +.upload-area::before { + content: ''; + position: absolute; + inset: 0; + border-radius: inherit; + background: radial-gradient(ellipse 70% 55% at 50% -10%, + rgba(10,10,10,0.04) 0%, transparent 70%); + pointer-events: none; + z-index: 0; +} + +/* Inner content wrapper */ +.upload-area > :not(.file-input) { + position: relative; + z-index: 1; +} + +/* Actual click target padding */ +.upload-area { + padding: 52px 32px 44px; + text-align: center; +} + +.upload-area:hover, +.upload-area.dragover { + border-color: var(--ink-45); + box-shadow: var(--shadow-md), var(--shadow-inset); + transform: translateY(-3px); +} + +.upload-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 44px; height: 44px; + border-radius: var(--radius-md); + background: var(--surface-2); + border: 1px solid var(--ink-10); + box-shadow: var(--shadow-xs); + margin-bottom: 16px; + font-size: 18px; + color: var(--ink-45); + line-height: 1; +} + +.upload-area h3 { + font-size: 15px; + font-weight: 500; + letter-spacing: -0.02em; + margin-bottom: 6px; + color: var(--ink); +} + +.upload-area p { + font-size: 12px; + color: var(--ink-45); + line-height: 1.55; + max-width: 300px; + margin: 0 auto; +} + +.file-input { display:none; } + +/* ─── Controls Card ──────────────────────────────────────── */ +/* Wrap language + model in a subtle grouped container */ +.language-select, +.model-select { + animation: fade-up .7s cubic-bezier(.22,1,.36,1) backwards; +} + +/* ─── Language Pills ─────────────────────────────────────── */ +.language-select { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-bottom: 10px; + animation-delay: .18s; + padding: 14px 16px; + background: var(--card); + border-radius: var(--radius-lg); + border: 1px solid var(--ink-10); + box-shadow: var(--shadow-xs); +} + +.language-select::before { + content: 'Ngôn ngữ'; + display: block; + width: 100%; + font-size: 10px; + font-weight: 600; + letter-spacing: .09em; + text-transform: uppercase; + color: var(--ink-45); + margin-bottom: 8px; +} + +.language-select label { + display: inline-flex; + align-items: center; + gap: 7px; + font-size: 13px; + letter-spacing: -0.01em; + color: var(--ink-70); + background: var(--surface); + border: 1px solid var(--ink-10); + border-radius: var(--radius-pill); + padding: 5px 16px; + cursor: pointer; + box-shadow: var(--shadow-xs); + transition: background .18s, color .18s, border-color .18s, box-shadow .18s; + user-select: none; + white-space: nowrap; +} + +.language-select label:hover { + background: var(--ink); + color: #fff; + border-color: var(--ink); + box-shadow: var(--shadow-sm); +} + +.language-select input[type="radio"] { + accent-color: var(--ink); + width: 13px; height: 13px; + cursor: pointer; +} + +/* ─── Model Row ──────────────────────────────────────────── */ +.model-select { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 22px; + animation-delay: .24s; + padding: 12px 16px; + background: var(--card); + border-radius: var(--radius-lg); + border: 1px solid var(--ink-10); + box-shadow: var(--shadow-xs); + font-size: 12px; + font-weight: 600; + letter-spacing: .06em; + text-transform: uppercase; + color: var(--ink-45); +} + +.model-select select { + padding: 6px 14px; + border-radius: var(--radius-pill); + border: 1px solid var(--ink-10); + background: var(--surface); + color: var(--ink); + font-size: 13px; + font-family: var(--font-body); + cursor: pointer; + appearance: none; + -webkit-appearance: none; + box-shadow: var(--shadow-xs); + transition: border-color .18s, box-shadow .18s; + flex: 1; +} + +.model-select select:focus { + outline: none; + border-color: var(--ink-45); + box-shadow: 0 0 0 3px rgba(10,10,10,.07); +} + +.model-select select option { background:#fff; color:#000; } + +/* ─── File Info ──────────────────────────────────────────── */ +.file-info { + background: var(--card); + border: 1px solid var(--ink-10); + border-radius: var(--radius-md); + padding: 12px 16px; + margin-bottom: 14px; + font-size: 12px; + color: var(--ink-45); + line-height: 1.8; + word-break: break-all; + display: none; + box-shadow: var(--shadow-xs); +} + +/* ─── Buttons ────────────────────────────────────────────── */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 7px; + padding: 9px 24px; + border-radius: var(--radius-pill); + font-size: 13px; + font-family: var(--font-body); + letter-spacing: -0.01em; + cursor: pointer; + border: 1px solid var(--ink-10); + text-decoration: none; + white-space: nowrap; + background: var(--card); + color: var(--ink); + box-shadow: var(--shadow-xs), var(--shadow-inset); + margin: 0 5px 5px 0; + position: relative; + overflow: hidden; + transition: + background .2s ease, + color .2s ease, + border-color .2s ease, + box-shadow .2s ease, + transform .18s cubic-bezier(.22,1,.36,1); +} + +.btn:hover:not(:disabled) { + background: var(--ink); + color: #fff; + border-color: var(--ink); + box-shadow: var(--shadow-md); + transform: translateY(-2px); +} + +.btn:active:not(:disabled) { + transform: scale(.97) translateY(0); + box-shadow: var(--shadow-xs); +} + +.btn:disabled { opacity:.35; cursor:not-allowed; } + +.btn-outline { + background: transparent; + box-shadow: none; + border-color: var(--ink-20); +} +.btn-outline:hover:not(:disabled) { + background: var(--ink); + color: #fff; + border-color: var(--ink); + box-shadow: var(--shadow-md); +} + +/* ─── Progress Card ──────────────────────────────────────── */ +.progress-container { + margin-top: 18px; + display: none; + background: var(--card); + border: 1px solid var(--ink-10); + border-radius: var(--radius-xl); + padding: 22px 24px 20px; + box-shadow: var(--shadow-sm), var(--shadow-inset); + animation: fade-up .5s cubic-bezier(.22,1,.36,1) backwards; +} + +.progress-container h3 { + font-size: 10px; + font-weight: 700; + letter-spacing: .1em; + text-transform: uppercase; + color: var(--ink-45); + margin-bottom: 16px; +} + +.progress-bar { + width:100%; height:2px; + background: var(--ink-05); + border-radius:99px; + overflow:hidden; + margin-bottom:6px; +} +.progress-fill { + height:100%; + background: var(--ink); + width:0%; + border-radius:99px; + transition: width .45s cubic-bezier(.22,1,.36,1); +} +.upload-progress-bar { + width:100%; height:2px; + background: var(--ink-05); + border-radius:99px; + overflow:hidden; + margin:10px 0 4px; +} +.upload-progress-fill { + height:100%; + background: var(--ink-45); + width:0%; + border-radius:99px; + transition: width .45s cubic-bezier(.22,1,.36,1); +} +.progress-text { + font-size:11px; + color:var(--ink-45); + margin-top:6px; + letter-spacing:.01em; +} + +/* ─── Result ─────────────────────────────────────────────── */ +.result-area { + margin-top:22px; + display:none; + animation: fade-up .5s cubic-bezier(.22,1,.36,1) backwards; +} +.result-label { + font-size:10px; + font-weight:700; + letter-spacing:.1em; + text-transform:uppercase; + color:var(--ink-45); + margin-bottom:10px; +} +.result-preview { + background: var(--card); + border: 1px solid var(--ink-10); + border-radius: var(--radius-lg); + padding: 16px 20px; + font-size:14px; + color:var(--ink-70); + line-height:1.75; + margin-bottom:14px; + box-shadow: var(--shadow-xs); +} + +/* ─── Transcript View ────────────────────────────────────── */ +.transcript-view { display:none; } + +.transcript-view h1 { + font-family: var(--font-heading); + font-size: clamp(26px, 4.5vw, 40px); + font-weight: 500; + letter-spacing: -0.05em; + color: var(--ink); + margin-bottom: 8px; +} + +.meta { + font-size: 12px; + color: var(--ink-45); + margin-bottom: 22px; + padding-bottom: 20px; + border-bottom: 1px solid var(--ink-10); + line-height: 1.9; +} +.meta strong { color: var(--ink); font-weight: 500; } + +.transcript-content { + background: var(--card); + border: 1px solid var(--ink-10); + border-radius: var(--radius-xl); + padding: 26px 28px; + font-size:15px; + line-height:1.9; + white-space:pre-wrap; + word-wrap:break-word; + max-height:580px; + overflow-y:auto; + color:var(--ink-70); + box-shadow: var(--shadow-sm), var(--shadow-inset); + scrollbar-width:thin; + scrollbar-color: var(--ink-10) transparent; +} +.transcript-content::-webkit-scrollbar { width:4px; } +.transcript-content::-webkit-scrollbar-track { background:transparent; } +.transcript-content::-webkit-scrollbar-thumb { background:var(--ink-10); border-radius:99px; } + +.nav-buttons { display:flex; gap:6px; flex-wrap:wrap; margin-bottom:22px; } diff --git a/static/transcriptionPages/transcriptionMain.js b/static/transcriptionPages/transcriptionMain.js new file mode 100644 index 0000000..047bae1 --- /dev/null +++ b/static/transcriptionPages/transcriptionMain.js @@ -0,0 +1,286 @@ +if (document.querySelector(".upload-view").style.display !== "none") { + const uploadArea = document.getElementById("uploadArea"); + const fileInput = document.getElementById("fileInput"); + const uploadBtn = document.getElementById("uploadBtn"); + const fileInfo = document.getElementById("fileInfo"); + const progressContainer = document.getElementById("progressContainer"); + const progressFill = document.getElementById("progressFill"); + const uploadProgressFill = + document.getElementById("uploadProgressFill"); + const progressText = document.getElementById("progressText"); + const resultArea = document.getElementById("resultArea"); + const transcriptPreview = document.getElementById("transcriptPreview"); + let selectedFile = null; + + uploadArea.addEventListener("click", () => fileInput.click()); + uploadArea.addEventListener("dragover", (e) => { + e.preventDefault(); + uploadArea.classList.add("dragover"); + }); + uploadArea.addEventListener("dragleave", () => + uploadArea.classList.remove("dragover"), + ); + uploadArea.addEventListener("drop", (e) => { + e.preventDefault(); + uploadArea.classList.remove("dragover"); + if (e.dataTransfer.files.length > 0) + handleFileSelect(e.dataTransfer.files[0]); + }); + fileInput.addEventListener("change", (e) => { + if (e.target.files.length > 0) handleFileSelect(e.target.files[0]); + }); + + function handleFileSelect(file) { + selectedFile = file; + const sizeInMB = (file.size / (1024 * 1024)).toFixed(2); + fileInfo.style.display = "block"; + fileInfo.innerHTML = `Tệp đã chọn: ${file.name}
Kích thước: ${sizeInMB} MB`; + uploadBtn.style.display = "inline-block"; + } + + uploadBtn.addEventListener("click", async () => { + if (!selectedFile) return; + + const language = document.querySelector( + 'input[name="language"]:checked', + ).value; + const model = document.getElementById("modelSelect").value; + + uploadBtn.disabled = true; + progressContainer.style.display = "block"; + resultArea.style.display = "none"; + + try { + await uploadFileInChunks(selectedFile, language, model); + } catch (error) { + alert("Tải lên thất bại: " + error.message); + uploadBtn.disabled = false; + progressContainer.style.display = "none"; + } + }); + + async function uploadFileInChunks(file, language, model) { + const CHUNK_SIZE = 3 * 60 * 1000; // 3 minutes in milliseconds + let jobId = null; + + // Create audio context to get duration + const audioContext = new ( + window.AudioContext || window.webkitAudioContext + )(); + const arrayBuffer = await file.arrayBuffer(); + const audioBuffer = await audioContext.decodeAudioData(arrayBuffer); + const duration = audioBuffer.duration * 1000; // Duration in milliseconds + + const totalChunks = Math.ceil(duration / CHUNK_SIZE); + progressText.textContent = `Đang chuẩn bị tải lên ${totalChunks} phần...`; + + for (let i = 0; i < totalChunks; i++) { + const start = i * CHUNK_SIZE; + const end = Math.min(start + CHUNK_SIZE, duration); + + progressText.textContent = `Đang tải lên phần ${i + 1}/${totalChunks}...`; + + // Extract chunk from audio + const chunkBlob = await extractAudioChunk( + audioBuffer, + start, + end, + file.type, + ); + + const formData = new FormData(); + formData.append("chunk", chunkBlob, `chunk_${i}.mp3`); + formData.append("chunk_index", i); + formData.append("total_chunks", totalChunks); + formData.append("filename", file.name); + formData.append("language", language); + formData.append("model", model); + + if (jobId) { + formData.append("job_id", jobId); + } + + const response = await fetch("/upload-chunk", { + method: "POST", + body: formData, + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || "Upload failed"); + } + + jobId = data.job_id; + + // Update upload progress + const uploadProgress = ((i + 1) / totalChunks) * 100; + uploadProgressFill.style.width = uploadProgress + "%"; + + if (data.status === "processing") { + progressText.textContent = + "Tất cả các phần đã được tải lên! Đang bắt đầu phiên âm..."; + startPolling(jobId); + return; + } + } + } + + async function extractAudioChunk( + audioBuffer, + startTime, + endTime, + mimeType, + ) { + const sampleRate = audioBuffer.sampleRate; + const startSample = Math.floor((startTime * sampleRate) / 1000); + const endSample = Math.floor((endTime * sampleRate) / 1000); + const frameCount = endSample - startSample; + + // Create new audio buffer for chunk + const chunkBuffer = new AudioContext().createBuffer( + audioBuffer.numberOfChannels, + frameCount, + sampleRate, + ); + + // Copy data + for ( + let channel = 0; + channel < audioBuffer.numberOfChannels; + channel++ + ) { + const channelData = audioBuffer.getChannelData(channel); + const chunkData = chunkBuffer.getChannelData(channel); + for (let i = 0; i < frameCount; i++) { + chunkData[i] = channelData[startSample + i]; + } + } + + // Convert to WAV first, then let server handle conversion + const wavBlob = audioBufferToWav(chunkBuffer); + + // If it's already an audio file, return as is, otherwise convert + if (mimeType.includes("audio")) { + return wavBlob; + } else { + // For video files, we'll send the audio portion + return wavBlob; + } + } + + function audioBufferToWav(buffer) { + const numChannels = buffer.numberOfChannels; + const sampleRate = buffer.sampleRate; + const format = 1; // PCM + const bitDepth = 16; + + let resultBuffer; + + if (numChannels === 2) { + resultBuffer = interleave( + buffer.getChannelData(0), + buffer.getChannelData(1), + ); + } else { + resultBuffer = buffer.getChannelData(0); + } + + const dataLength = resultBuffer.length * 2; + const bufferLength = 44 + dataLength; + const arrayBuffer = new ArrayBuffer(bufferLength); + const view = new DataView(arrayBuffer); + + // Write WAV header + writeString(view, 0, "RIFF"); + view.setUint32(4, 36 + dataLength, true); + writeString(view, 8, "WAVE"); + writeString(view, 12, "fmt "); + view.setUint32(16, 16, true); + view.setUint16(20, format, true); + view.setUint16(22, numChannels, true); + view.setUint32(24, sampleRate, true); + view.setUint32(28, sampleRate * 4, true); + view.setUint16(32, numChannels * 2, true); + view.setUint16(34, bitDepth, true); + writeString(view, 36, "data"); + view.setUint32(40, dataLength, true); + + // Write audio data + const offset = 44; + for (let i = 0; i < resultBuffer.length; i++) { + const sample = Math.max(-1, Math.min(1, resultBuffer[i])); + view.setInt16( + offset + i * 2, + sample < 0 ? sample * 0x8000 : sample * 0x7fff, + true, + ); + } + + return new Blob([arrayBuffer], { type: "audio/wav" }); + } + + function writeString(view, offset, string) { + for (let i = 0; i < string.length; i++) { + view.setUint8(offset + i, string.charCodeAt(i)); + } + } + + function interleave(left, right) { + const length = left.length + right.length; + const result = new Float32Array(length); + + let inputIndex = 0; + for (let i = 0; i < length; i += 2) { + result[i] = left[inputIndex]; + result[i + 1] = right[inputIndex]; + inputIndex++; + } + + return result; + } + + function startPolling(jobId) { + const pollInterval = setInterval(async () => { + try { + const data = await (await fetch(`/status/${jobId}`)).json(); + progressFill.style.width = data.progress + "%"; + + if (data.status === "processing") { + progressText.textContent = data.current_chunk + ? `Đang phiên âm đoạn ${data.current_chunk}...` + : `Đang xử lý: ${data.progress}%`; + } else if (data.status === "completed") { + clearInterval(pollInterval); + progressText.textContent = "Hoàn tất!"; + showResult(jobId); + } else if (data.status === "error") { + clearInterval(pollInterval); + progressText.textContent = "Lỗi: " + data.error; + uploadBtn.disabled = false; + } + } catch (e) { + console.error("Lỗi kiểm tra:", e); + } + }, 2000); + } + + async function showResult(jobId) { + try { + const html = await (await fetch(`/view/${jobId}`)).text(); + const doc = new DOMParser().parseFromString(html, "text/html"); + const content = + doc.querySelector(".transcript-content")?.textContent || + "Đã tải bản phiên âm"; + transcriptPreview.innerHTML = `
${content.substring(0, 300)}...
`; + resultArea.style.display = "block"; + uploadBtn.disabled = false; + document.getElementById("viewFullBtn").onclick = () => + window.open(`/view/${jobId}`, "_blank"); + document.getElementById("downloadBtn").onclick = () => + (window.location.href = `/transcript/${jobId}`); + } catch (e) { + console.error("Lỗi hiển thị:", e); + } + } +} diff --git a/static/transcriptionPages/transcriptionReview.css b/static/transcriptionPages/transcriptionReview.css new file mode 100644 index 0000000..c2b8c42 --- /dev/null +++ b/static/transcriptionPages/transcriptionReview.css @@ -0,0 +1,155 @@ +/* ═══════════════════════════════════════════════════════════ + transcriptionReview.css · Mainframe Design System v3 + ═══════════════════════════════════════════════════════════ */ + +:root { + --font-heading: 'HelveticaNowDisplay-Medium','Helvetica Neue',Arial,sans-serif; + --font-body: 'HelveticaNowDisplayW01-Rg','Helvetica Neue',Arial,sans-serif; + --ink: #0a0a0a; + --ink-70: rgba(10,10,10,0.70); + --ink-45: rgba(10,10,10,0.45); + --ink-20: rgba(10,10,10,0.20); + --ink-10: rgba(10,10,10,0.10); + --ink-05: rgba(10,10,10,0.05); + --surface: #f4f3ef; + --card: #ffffff; + --radius-pill: 9999px; + --radius-xl: 24px; + --radius-lg: 18px; + --shadow-xs: 0 1px 2px rgba(0,0,0,0.06); + --shadow-sm: 0 2px 10px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.04); + --shadow-md: 0 6px 28px rgba(0,0,0,0.10), 0 2px 6px rgba(0,0,0,0.05); + --shadow-inset: inset 0 1px 0 rgba(255,255,255,0.8); +} + +@keyframes fade-up { + from { opacity:0; transform:translateY(14px); } + to { opacity:1; transform:none; } +} + +*,*::before,*::after { margin:0; padding:0; box-sizing:border-box; } + +html { + font-family: var(--font-body); + -webkit-font-smoothing: antialiased; + background: var(--surface); + color: var(--ink); +} + +body { + min-height: 100vh; + background: var(--surface); + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='300' height='300' filter='url(%23n)' opacity='0.028'/%3E%3C/svg%3E"); + padding: 0 20px 100px; + font-family: var(--font-body); +} + +.container { + width: 100%; + max-width: 820px; + margin: 0 auto; + padding-top: 48px; + animation: fade-up .7s cubic-bezier(.22,1,.36,1) backwards; + animation-delay: .05s; +} + +/* ─── Eyebrow label above h1 ─────────────────────────────── */ +.container::before { + content: '✦ Kết quả phiên âm'; + display: inline-block; + font-size: 11px; + font-weight: 600; + letter-spacing: .1em; + text-transform: uppercase; + color: var(--ink-45); + margin-bottom: 14px; + padding: 6px 14px; + background: var(--card); + border: 1px solid var(--ink-10); + border-radius: var(--radius-pill); + box-shadow: var(--shadow-xs); +} + +h1 { + font-family: var(--font-heading); + font-size: clamp(28px, 5vw, 42px); + font-weight: 500; + letter-spacing: -0.05em; + line-height: 1.0; + color: var(--ink); + margin-bottom: 12px; +} + +.meta { + font-size: 12px; + color: var(--ink-45); + line-height: 2; + margin-bottom: 24px; + padding: 14px 18px; + background: var(--card); + border: 1px solid var(--ink-10); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-xs); +} +.meta strong { color: var(--ink); font-weight: 500; } + +/* Nav row */ +div[style*="margin-bottom"] { + display: flex !important; + gap: 6px !important; + flex-wrap: wrap; + margin-bottom: 22px !important; +} + +/* ─── Buttons ────────────────────────────────────────────── */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 7px; + padding: 9px 24px; + border-radius: var(--radius-pill); + font-size: 13px; + font-family: var(--font-body); + letter-spacing: -0.01em; + cursor: pointer; + border: 1px solid var(--ink-10); + text-decoration: none; + white-space: nowrap; + background: var(--card); + color: var(--ink); + box-shadow: var(--shadow-xs), var(--shadow-inset); + margin: 0 5px 5px 0; + transition: background .2s, color .2s, border-color .2s, box-shadow .2s, + transform .18s cubic-bezier(.22,1,.36,1); +} +.btn:hover:not(:disabled) { + background: var(--ink); + color: #fff; + border-color: var(--ink); + box-shadow: var(--shadow-md); + transform: translateY(-2px); +} +.btn:active:not(:disabled) { transform: scale(.97); box-shadow: var(--shadow-xs); } +.btn:disabled { opacity:.36; cursor:not-allowed; } + +/* ─── Transcript body ────────────────────────────────────── */ +.transcript { + background: var(--card); + border: 1px solid var(--ink-10); + border-radius: var(--radius-xl); + padding: 28px 30px; + font-size: 15px; + line-height: 1.95; + white-space: pre-wrap; + word-wrap: break-word; + color: var(--ink-70); + box-shadow: var(--shadow-sm), var(--shadow-inset); + animation: fade-up .7s cubic-bezier(.22,1,.36,1) backwards; + animation-delay: .18s; + scrollbar-width: thin; + scrollbar-color: var(--ink-10) transparent; +} +.transcript::-webkit-scrollbar { width:4px; } +.transcript::-webkit-scrollbar-track { background:transparent; } +.transcript::-webkit-scrollbar-thumb { background:var(--ink-10); border-radius:99px; } diff --git a/static/transcriptionPages/transcriptionReview.js b/static/transcriptionPages/transcriptionReview.js new file mode 100644 index 0000000..55915ce --- /dev/null +++ b/static/transcriptionPages/transcriptionReview.js @@ -0,0 +1,3 @@ +// transcriptionReview.js +// Custom JavaScript for Transcription Review +console.log("Transcription Review initialized."); diff --git a/views/scraperZLibraryPages/scraperZLibraryMain.html b/views/scraperZLibraryPages/scraperZLibraryMain.html index 21a3c29..0f91a8f 100644 --- a/views/scraperZLibraryPages/scraperZLibraryMain.html +++ b/views/scraperZLibraryPages/scraperZLibraryMain.html @@ -5,406 +5,9 @@ Tìm Kiếm Sách Z-Library - + + +
@@ -470,761 +73,7 @@
- + diff --git a/views/transcriptionPages/transcriptionMain.html b/views/transcriptionPages/transcriptionMain.html index e035009..c4701d5 100644 --- a/views/transcriptionPages/transcriptionMain.html +++ b/views/transcriptionPages/transcriptionMain.html @@ -5,240 +5,9 @@ Phiên Âm - + + +
@@ -336,294 +105,7 @@
- + diff --git a/views/transcriptionPages/transcriptionReview.html b/views/transcriptionPages/transcriptionReview.html index 6c6aff1..424813f 100644 --- a/views/transcriptionPages/transcriptionReview.html +++ b/views/transcriptionPages/transcriptionReview.html @@ -5,60 +5,9 @@ Transcript - {{ filename }} - + + +
@@ -75,5 +24,6 @@
{{ content }}
+ From 9f7824825c2c7b80f1213fe824d09e951cb6e940 Mon Sep 17 00:00:00 2001 From: hiep200311 Date: Wed, 24 Jun 2026 16:22:14 +0700 Subject: [PATCH 2/3] Move CSS/JS assets from static/ into views/ feature folders Co-locate each feature's CSS and JS alongside its HTML template. Remove the standalone static/ directory entirely. --- static/header/header.html | 28 -- static/header/headerLoader.js | 244 ------------------ .../scraperZLibraryMain.css | 0 .../scraperZLibraryMain.js | 0 .../transcriptionPages/transcriptionMain.css | 0 .../transcriptionPages/transcriptionMain.js | 0 .../transcriptionReview.css | 0 .../transcriptionPages/transcriptionReview.js | 0 8 files changed, 272 deletions(-) delete mode 100644 static/header/header.html delete mode 100644 static/header/headerLoader.js rename {static => views}/scraperZLibraryPages/scraperZLibraryMain.css (100%) rename {static => views}/scraperZLibraryPages/scraperZLibraryMain.js (100%) rename {static => views}/transcriptionPages/transcriptionMain.css (100%) rename {static => views}/transcriptionPages/transcriptionMain.js (100%) rename {static => views}/transcriptionPages/transcriptionReview.css (100%) rename {static => views}/transcriptionPages/transcriptionReview.js (100%) diff --git a/static/header/header.html b/static/header/header.html deleted file mode 100644 index 71979b9..0000000 --- a/static/header/header.html +++ /dev/null @@ -1,28 +0,0 @@ - -
- -
diff --git a/static/header/headerLoader.js b/static/header/headerLoader.js deleted file mode 100644 index 22bed52..0000000 --- a/static/header/headerLoader.js +++ /dev/null @@ -1,244 +0,0 @@ -// static/header/headerLoader.js -(function () { - "use strict"; - - // Configuration - const CONFIG = { - headerPath: "/static/header/header.html", - insertPosition: "afterbegin", - containerSelector: "body", - activeClass: "active", - version: "1.0", - }; - - const styles = ` - - `; - - - - // Load header function - async function loadHeader() { - try { - // Fetch header HTML - const response = await fetch(`${CONFIG.headerPath}?v=${CONFIG.version}`); - - if (!response.ok) { - throw new Error(`Failed to load header: ${response.status}`); - } - - const headerHTML = await response.text(); - - // Inject styles if not already present - if (!document.getElementById("header-component-styles")) { - document.head.insertAdjacentHTML("beforeend", styles); - } - - // Insert header - const container = document.querySelector(CONFIG.containerSelector); - container.insertAdjacentHTML(CONFIG.insertPosition, headerHTML); - - // Highlight active link - highlightActiveLink(); - - console.log("✅ Header component loaded v" + CONFIG.version); - } catch (error) { - console.error("❌ Error loading header:", error); - createFallbackHeader(); - } - } - - // Highlight current page link - function highlightActiveLink() { - const currentPath = window.location.pathname; - const links = document.querySelectorAll(".nav-link"); - - links.forEach((link) => { - const href = link.getAttribute("href"); - - // Check if current path matches - if ( - currentPath === href || - (href !== "/" && currentPath.startsWith(href)) - ) { - link.classList.add(CONFIG.activeClass); - } - - // Special case for root - if (currentPath === "/" && href === "/transcription") { - link.classList.add(CONFIG.activeClass); - } - }); - } - - // Fallback header in case of error - function createFallbackHeader() { - const fallback = document.createElement("div"); - fallback.style.cssText = ` - padding: 15px 20px; - background: #fff; - border-bottom: 1px solid #e0e0e0; - position: fixed; - top: 0; - left: 0; - right: 0; - z-index: 1000; - `; - fallback.innerHTML = ` - - `; - document.body.insertAdjacentElement("afterbegin", fallback); - document.body.style.paddingTop = "50px"; - } - - // Initialize when DOM is ready - if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", loadHeader); - } else { - loadHeader(); - } -})(); diff --git a/static/scraperZLibraryPages/scraperZLibraryMain.css b/views/scraperZLibraryPages/scraperZLibraryMain.css similarity index 100% rename from static/scraperZLibraryPages/scraperZLibraryMain.css rename to views/scraperZLibraryPages/scraperZLibraryMain.css diff --git a/static/scraperZLibraryPages/scraperZLibraryMain.js b/views/scraperZLibraryPages/scraperZLibraryMain.js similarity index 100% rename from static/scraperZLibraryPages/scraperZLibraryMain.js rename to views/scraperZLibraryPages/scraperZLibraryMain.js diff --git a/static/transcriptionPages/transcriptionMain.css b/views/transcriptionPages/transcriptionMain.css similarity index 100% rename from static/transcriptionPages/transcriptionMain.css rename to views/transcriptionPages/transcriptionMain.css diff --git a/static/transcriptionPages/transcriptionMain.js b/views/transcriptionPages/transcriptionMain.js similarity index 100% rename from static/transcriptionPages/transcriptionMain.js rename to views/transcriptionPages/transcriptionMain.js diff --git a/static/transcriptionPages/transcriptionReview.css b/views/transcriptionPages/transcriptionReview.css similarity index 100% rename from static/transcriptionPages/transcriptionReview.css rename to views/transcriptionPages/transcriptionReview.css diff --git a/static/transcriptionPages/transcriptionReview.js b/views/transcriptionPages/transcriptionReview.js similarity index 100% rename from static/transcriptionPages/transcriptionReview.js rename to views/transcriptionPages/transcriptionReview.js From adc1934e9cf9e6b62ec24d143c55189fd8384cc5 Mon Sep 17 00:00:00 2001 From: hiep200311 Date: Wed, 24 Jun 2026 16:22:31 +0700 Subject: [PATCH 3/3] Refactor static serving: use Blueprint static_folder, inline header - Disable Flask default /static/ folder (static_folder=None in app.py) - Add headerRoute blueprint to serve views/header/ at /assets/header/ - Configure transcription and scraper blueprints with static_folder pointing to their respective views/ subfolders - Inline header HTML directly in headerLoader.js (remove fetch call) - Update all HTML templates to use new /assets/... URLs --- app.py | 5 +- routes/headerRoute.py | 11 + routes/scraperZLibraryRoute.py | 53 ++-- routes/transcriptionRoute.py | 34 +-- views/header/headerLoader.js | 234 ++++++++++++++++++ .../scraperZLibraryMain.html | 6 +- .../transcriptionPages/transcriptionMain.html | 6 +- .../transcriptionReview.html | 4 +- 8 files changed, 307 insertions(+), 46 deletions(-) create mode 100644 routes/headerRoute.py create mode 100644 views/header/headerLoader.js diff --git a/app.py b/app.py index 25f7c6a..b5e4b1f 100644 --- a/app.py +++ b/app.py @@ -3,10 +3,12 @@ from flask import Flask from config import Config from routes.transcriptionRoute import transcription_bp from routes.scraperZLibraryRoute import scraperZLibraryRoute +from routes.headerRoute import header_bp def create_app(): """Application factory function""" - app = Flask(__name__, template_folder='views') + # static_folder=None → tắt hoàn toàn thư mục /static mặc định của Flask + app = Flask(__name__, template_folder='views', static_folder=None) # Load configuration app.config.from_object(Config) @@ -18,6 +20,7 @@ def create_app(): Config.init_app() # Register blueprints + app.register_blueprint(header_bp) app.register_blueprint(transcription_bp) app.register_blueprint(scraperZLibraryRoute, url_prefix='/scraperZLibrary') diff --git a/routes/headerRoute.py b/routes/headerRoute.py new file mode 100644 index 0000000..228db15 --- /dev/null +++ b/routes/headerRoute.py @@ -0,0 +1,11 @@ +# routes/headerRoute.py +from flask import Blueprint + +# Blueprint để serve headerLoader.js từ views/header/ +# → URL: /assets/header/headerLoader.js +header_bp = Blueprint( + 'header', + __name__, + static_folder='../views/header', + static_url_path='/assets/header', +) diff --git a/routes/scraperZLibraryRoute.py b/routes/scraperZLibraryRoute.py index b825923..ebf0598 100644 --- a/routes/scraperZLibraryRoute.py +++ b/routes/scraperZLibraryRoute.py @@ -1,23 +1,30 @@ -# routes/scraperZLibraryRoute.py -from flask import Blueprint -from controllers.scraperZLibraryController import ScraperController - -# Create blueprint -scraperZLibraryRoute = Blueprint('scraperZLibrary', __name__) - -# Define routes -scraperZLibraryRoute.route('/', methods=['GET'])(ScraperController.index) -scraperZLibraryRoute.route('/api/search', methods=['POST'])(ScraperController.search_books) -scraperZLibraryRoute.route('/api/download/txt', methods=['POST'])(ScraperController.download_txt) -scraperZLibraryRoute.route('/api/download/json', methods=['POST'])(ScraperController.download_json) -scraperZLibraryRoute.route('/api/download/books', methods=['POST'])(ScraperController.download_books_zip) # New route -scraperZLibraryRoute.add_url_rule( - '/api/download/stream', - view_func=ScraperController.download_books_stream, - methods=['POST'], -) -scraperZLibraryRoute.add_url_rule( - '/api/download/file/', - view_func=ScraperController.fetch_stored_file, - methods=['GET'], -) \ No newline at end of file +# routes/scraperZLibraryRoute.py +from flask import Blueprint +from controllers.scraperZLibraryController import ScraperController + +# Blueprint với static_folder trỏ vào views/scraperZLibraryPages +# → Flask serve CSS/JS qua URL /scraperZLibrary/assets/scraper/... +# Lưu ý: static_url_path chỉ cần phần sau /scraperZLibrary/ (url_prefix đã được thêm khi register) +scraperZLibraryRoute = Blueprint( + 'scraperZLibrary', + __name__, + static_folder='../views/scraperZLibraryPages', + static_url_path='/assets/scraper', +) + +# Define routes +scraperZLibraryRoute.route('/', methods=['GET'])(ScraperController.index) +scraperZLibraryRoute.route('/api/search', methods=['POST'])(ScraperController.search_books) +scraperZLibraryRoute.route('/api/download/txt', methods=['POST'])(ScraperController.download_txt) +scraperZLibraryRoute.route('/api/download/json', methods=['POST'])(ScraperController.download_json) +scraperZLibraryRoute.route('/api/download/books', methods=['POST'])(ScraperController.download_books_zip) # New route +scraperZLibraryRoute.add_url_rule( + '/api/download/stream', + view_func=ScraperController.download_books_stream, + methods=['POST'], +) +scraperZLibraryRoute.add_url_rule( + '/api/download/file/', + view_func=ScraperController.fetch_stored_file, + methods=['GET'], +) \ No newline at end of file diff --git a/routes/transcriptionRoute.py b/routes/transcriptionRoute.py index 40679ec..3fdb84a 100644 --- a/routes/transcriptionRoute.py +++ b/routes/transcriptionRoute.py @@ -1,14 +1,20 @@ -# routes/transcriptionRoute.py -from flask import Blueprint -from controllers.transcriptionController import TranscriptionController - -# Create blueprint -transcription_bp = Blueprint('transcription', __name__) - -# Define routes -transcription_bp.route('/', methods=['GET'])(TranscriptionController.index) -transcription_bp.route('/upload-chunk', methods=['POST'])(TranscriptionController.upload_chunk) -transcription_bp.route('/status/', methods=['GET'])(TranscriptionController.status) -transcription_bp.route('/transcript/', methods=['GET'])(TranscriptionController.download_transcript) -transcription_bp.route('/view/', methods=['GET'])(TranscriptionController.view_transcript) -transcription_bp.route('/jobs', methods=['GET'])(TranscriptionController.list_jobs) \ No newline at end of file +# routes/transcriptionRoute.py +from flask import Blueprint +from controllers.transcriptionController import TranscriptionController + +# Blueprint với static_folder trỏ vào views/transcriptionPages +# → Flask serve CSS/JS qua URL /assets/transcription/... +transcription_bp = Blueprint( + 'transcription', + __name__, + static_folder='../views/transcriptionPages', + static_url_path='/assets/transcription', +) + +# Define routes +transcription_bp.route('/', methods=['GET'])(TranscriptionController.index) +transcription_bp.route('/upload-chunk', methods=['POST'])(TranscriptionController.upload_chunk) +transcription_bp.route('/status/', methods=['GET'])(TranscriptionController.status) +transcription_bp.route('/transcript/', methods=['GET'])(TranscriptionController.download_transcript) +transcription_bp.route('/view/', methods=['GET'])(TranscriptionController.view_transcript) +transcription_bp.route('/jobs', methods=['GET'])(TranscriptionController.list_jobs) \ No newline at end of file diff --git a/views/header/headerLoader.js b/views/header/headerLoader.js new file mode 100644 index 0000000..38f961b --- /dev/null +++ b/views/header/headerLoader.js @@ -0,0 +1,234 @@ +// views/header/headerLoader.js +(function () { + "use strict"; + + // Configuration + const CONFIG = { + insertPosition: "afterbegin", + containerSelector: "body", + activeClass: "active", + version: "1.1", + }; + + // Header HTML inlined — không cần fetch file nữa + const headerHTML = ` +
+ +
`; + + const styles = ` + + `; + + // Inject header directly (no fetch needed) + function loadHeader() { + // Inject styles if not already present + if (!document.getElementById("header-component-styles")) { + document.head.insertAdjacentHTML("beforeend", styles); + } + + // Insert header HTML inline + const container = document.querySelector(CONFIG.containerSelector); + container.insertAdjacentHTML(CONFIG.insertPosition, headerHTML); + + // Highlight active link + highlightActiveLink(); + + console.log("✅ Header component loaded v" + CONFIG.version); + } + + // Highlight current page link + function highlightActiveLink() { + const currentPath = window.location.pathname; + const links = document.querySelectorAll(".nav-link"); + + links.forEach((link) => { + const href = link.getAttribute("href"); + + // Check if current path matches + if ( + currentPath === href || + (href !== "/" && currentPath.startsWith(href)) + ) { + link.classList.add(CONFIG.activeClass); + } + + // Special case for root + if (currentPath === "/" && href === "/transcription") { + link.classList.add(CONFIG.activeClass); + } + }); + } + + // Initialize when DOM is ready + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", loadHeader); + } else { + loadHeader(); + } +})(); diff --git a/views/scraperZLibraryPages/scraperZLibraryMain.html b/views/scraperZLibraryPages/scraperZLibraryMain.html index 0f91a8f..0438d9b 100644 --- a/views/scraperZLibraryPages/scraperZLibraryMain.html +++ b/views/scraperZLibraryPages/scraperZLibraryMain.html @@ -7,7 +7,7 @@ Tìm Kiếm Sách Z-Library - +
@@ -73,7 +73,7 @@
- - + + diff --git a/views/transcriptionPages/transcriptionMain.html b/views/transcriptionPages/transcriptionMain.html index c4701d5..e43f60c 100644 --- a/views/transcriptionPages/transcriptionMain.html +++ b/views/transcriptionPages/transcriptionMain.html @@ -7,7 +7,7 @@ Phiên Âm - +
@@ -105,7 +105,7 @@
- - + + diff --git a/views/transcriptionPages/transcriptionReview.html b/views/transcriptionPages/transcriptionReview.html index 424813f..d9874b8 100644 --- a/views/transcriptionPages/transcriptionReview.html +++ b/views/transcriptionPages/transcriptionReview.html @@ -7,7 +7,7 @@ Transcript - {{ filename }} - +
@@ -24,6 +24,6 @@
{{ content }}
- +