1c20bafd05
Раньше метаданные и «Открыть»+3 иконки делили одну строку → на ширине грида дата переносилась криво, кнопки наезжали. Теперь подвал колонкой: метаданные строкой, действия — отдельным рядом (Открыть растягивается, иконки — компактные квадраты 34×34 с лёгким фоном). Без наложений и кривых переносов. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1090 lines
58 KiB
HTML
1090 lines
58 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ru">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>Библиотека материалов — LearnSpace</title>
|
|
<link href="https://fonts.googleapis.com/css2?family=Unbounded:wght@400;700;800&family=Manrope:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
|
<link rel="stylesheet" href="/css/ls.css" />
|
|
<style>
|
|
.btn-upload { padding: 8px 20px; border: none; border-radius: var(--r-pill); background: var(--grad-1); color: #fff; font-family: 'Manrope', sans-serif; font-size: 0.85rem; font-weight: 700; cursor: pointer; transition: opacity var(--tr); }
|
|
.btn-upload:hover { opacity: 0.88; }
|
|
|
|
.container { max-width: 1100px; margin: 0 auto; padding: 36px 20px 80px; }
|
|
.page-title { font-family: 'Unbounded', sans-serif; font-size: 1.4rem; font-weight: 800; margin-bottom: 4px; }
|
|
.page-sub { font-size: 0.9rem; color: var(--text-2); margin-bottom: 24px; }
|
|
|
|
/* ── breadcrumb ── */
|
|
.lib-bc { display: none; align-items: center; gap: 6px; margin-bottom: 20px; font-size: 0.88rem; }
|
|
.lib-bc.visible { display: flex; }
|
|
.lib-bc-link { color: var(--violet); cursor: pointer; font-weight: 600; background: none; border: none; font-family: 'Manrope', sans-serif; font-size: inherit; padding: 0; display: inline-flex; align-items: center; gap: 4px; }
|
|
.lib-bc-link:hover { text-decoration: underline; }
|
|
.lib-bc-sep { color: var(--text-3); font-size: 0.75rem; }
|
|
.lib-bc-curr { font-weight: 700; color: var(--text); }
|
|
|
|
/* toolbar */
|
|
.toolbar { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-bottom: 20px; }
|
|
.t-select { padding: 8px 14px; border: 1.5px solid var(--border-h); border-radius: var(--r-pill); font-family: 'Manrope', sans-serif; font-size: 0.85rem; font-weight: 600; background: var(--surface); color: var(--text); cursor: pointer; }
|
|
.t-select:focus { outline: none; border-color: var(--violet); }
|
|
.t-input { padding: 8px 14px; border: 1.5px solid var(--border-h); border-radius: var(--r-pill); font-family: 'Manrope', sans-serif; font-size: 0.85rem; background: var(--surface); color: var(--text); width: 220px; }
|
|
.t-input:focus { outline: none; border-color: var(--violet); }
|
|
.t-count { font-size: 0.82rem; color: var(--text-3); margin-left: auto; }
|
|
|
|
/* section labels */
|
|
.section-label {
|
|
font-family: 'Unbounded', sans-serif; font-size: 0.68rem; font-weight: 700;
|
|
color: var(--text-3); text-transform: uppercase; letter-spacing: 0.07em;
|
|
display: flex; align-items: center; gap: 10px; margin-bottom: 12px;
|
|
}
|
|
.section-label::after { content: ''; flex: 1; height: 1px; background: var(--border); }
|
|
|
|
/* ── folder grid ── */
|
|
.folder-grid { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 32px; }
|
|
.folder-card {
|
|
display: flex; align-items: center; gap: 10px;
|
|
padding: 12px 14px 12px 16px;
|
|
background: var(--surface); backdrop-filter: var(--blur);
|
|
border: 1.5px solid var(--border); border-radius: 14px;
|
|
cursor: pointer; transition: all var(--tr);
|
|
min-width: 160px; max-width: 240px;
|
|
position: relative;
|
|
}
|
|
.folder-card:hover { border-color: var(--violet); box-shadow: var(--shadow); }
|
|
.folder-card-icon { font-size: 1.3rem; flex-shrink: 0; line-height: 1; }
|
|
.folder-card-info { flex: 1; min-width: 0; }
|
|
.folder-card-name { font-size: 0.87rem; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
.folder-card-count { font-size: 0.7rem; color: var(--text-3); margin-top: 1px; }
|
|
.folder-card-acts { display: none; align-items: center; gap: 2px; flex-shrink: 0; }
|
|
.folder-card:hover .folder-card-acts { display: flex; }
|
|
.btn-fol-act {
|
|
width: 24px; height: 24px; border-radius: 6px; border: none;
|
|
background: rgba(15,23,42,0.06); color: var(--text-3);
|
|
cursor: pointer; display: flex; align-items: center; justify-content: center;
|
|
transition: all var(--tr); padding: 0; flex-shrink: 0;
|
|
}
|
|
.btn-fol-act:hover { background: rgba(155,93,229,0.1); color: var(--violet); }
|
|
.btn-fol-del:hover { background: rgba(241,91,181,0.1) !important; color: var(--pink) !important; }
|
|
.btn-fol-act svg { width: 12px; height: 12px; stroke-width: 2.2; pointer-events: none; }
|
|
.folder-locked-badge {
|
|
position: absolute; top: -5px; right: -5px;
|
|
width: 16px; height: 16px; border-radius: 50%;
|
|
background: var(--violet); display: flex; align-items: center; justify-content: center;
|
|
}
|
|
.folder-locked-badge svg { width: 9px; height: 9px; stroke: #fff; stroke-width: 2.5; pointer-events: none; }
|
|
|
|
/* student picker */
|
|
.student-pick-list { max-height: 170px; overflow-y: auto; border: 1.5px solid rgba(15,23,42,0.16); border-radius: 10px; background: #f8f9ff; margin-top: 6px; }
|
|
.student-pick-item { padding: 8px 12px; cursor: pointer; font-size: 0.85rem; transition: background 0.15s; border-bottom: 1px solid rgba(15,23,42,0.05); display: flex; align-items: center; gap: 8px; }
|
|
.student-pick-item:last-child { border-bottom: none; }
|
|
.student-pick-item:hover { background: rgba(155,93,229,0.07); }
|
|
.student-pick-item.sp-selected { background: rgba(155,93,229,0.12); }
|
|
.sp-check { width: 16px; height: 16px; border-radius: 50%; border: 1.5px solid rgba(155,93,229,0.3); flex-shrink: 0; display: flex; align-items: center; justify-content: center; }
|
|
.student-pick-item.sp-selected .sp-check { background: var(--violet); border-color: var(--violet); }
|
|
.student-pick-item.sp-selected .sp-check::after { content: ''; width: 8px; height: 8px; background: #fff; border-radius: 50%; }
|
|
.sp-info { flex: 1; min-width: 0; }
|
|
.sp-name { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
.sp-email { font-size: 0.72rem; color: var(--text-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
|
|
/* access status bar */
|
|
.fa-status-bar { display: flex; align-items: center; justify-content: space-between; padding: 10px 14px; border-radius: 12px; margin-bottom: 18px; gap: 10px; border: 1.5px solid; transition: all 0.2s; }
|
|
.fa-status-bar.public { background: rgba(6,214,100,0.07); border-color: rgba(6,214,100,0.25); }
|
|
.fa-status-bar.restrict { background: rgba(155,93,229,0.06); border-color: rgba(155,93,229,0.2); }
|
|
.fa-status-text { font-size: 0.85rem; font-weight: 600; display: flex; align-items: center; gap: 7px; }
|
|
.btn-clear-access { padding: 5px 14px; border: 1.5px solid rgba(6,214,100,0.4); border-radius: 99px; background: transparent; font-family: 'Manrope', sans-serif; font-size: 0.78rem; font-weight: 700; color: var(--green); cursor: pointer; white-space: nowrap; transition: all 0.18s; }
|
|
.btn-clear-access:hover { background: rgba(6,214,100,0.1); }
|
|
|
|
/* ── file grid ── */
|
|
.file-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 16px; }
|
|
.file-card { background: var(--surface); backdrop-filter: var(--blur); border: 1px solid var(--border); border-radius: var(--r-lg); padding: 20px; display: flex; flex-direction: column; gap: 12px; transition: border-color var(--tr), box-shadow var(--tr); }
|
|
.file-card:hover { border-color: var(--border-h); box-shadow: var(--shadow); }
|
|
.file-head { display: flex; align-items: flex-start; gap: 14px; }
|
|
.file-icon { font-size: 2.2rem; flex-shrink: 0; line-height: 1; }
|
|
.file-meta { flex: 1; min-width: 0; }
|
|
.file-title { font-size: 0.95rem; font-weight: 700; line-height: 1.35; margin-bottom: 4px; word-break: break-word; }
|
|
.file-desc { font-size: 0.8rem; color: var(--text-3); line-height: 1.45; word-break: break-word; }
|
|
.file-folder-tag { font-size: 0.7rem; color: var(--text-3); margin-top: 2px; display: flex; align-items: center; gap: 4px; }
|
|
.file-tags { display: flex; gap: 6px; flex-wrap: wrap; }
|
|
.tag { display: inline-block; padding: 2px 9px; border-radius: var(--r-pill); font-size: 0.68rem; font-weight: 700; }
|
|
.tag-subj { background: rgba(155,93,229,0.1); color: var(--violet); }
|
|
.tag-type { background: rgba(15,23,42,0.06); color: var(--text-3); }
|
|
.tag-private{ background: rgba(241,91,181,0.1); color: var(--pink); }
|
|
.tag-public { background: rgba(6,214,100,0.1); color: var(--green); }
|
|
/* Подвал карточки: метаданные строкой, действия — отдельным рядом (не теснятся) */
|
|
.file-footer { display: flex; flex-direction: column; gap: 12px; margin-top: auto; }
|
|
.file-meta-text { min-width: 0; }
|
|
.file-size { font-size: 0.75rem; color: var(--text-3); }
|
|
.file-actions { display: flex; gap: 6px; align-items: center; }
|
|
.btn-dl { flex: 1; display: inline-flex; align-items: center; justify-content: center; gap: 5px; padding: 8px 14px; border: none; border-radius: var(--r-pill); background: var(--grad-1); color: #fff; font-family: 'Manrope', sans-serif; font-size: 0.8rem; font-weight: 700; cursor: pointer; text-decoration: none; transition: opacity var(--tr); white-space: nowrap; }
|
|
.btn-dl:hover { opacity: 0.85; }
|
|
.btn-del, .btn-assign, .btn-move { flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; width: 34px; height: 34px; padding: 0; border: 1.5px solid transparent; border-radius: 10px; background: rgba(15,23,42,0.04); color: var(--text-3); cursor: pointer; transition: all var(--tr); }
|
|
.btn-del:hover { border-color: var(--pink); color: var(--pink); background: rgba(241,91,181,0.08); }
|
|
.btn-assign:hover { border-color: var(--violet); color: var(--violet); background: rgba(155,93,229,0.08); }
|
|
.btn-move:hover { border-color: var(--cyan); color: #0aa6b0; background: rgba(6,214,224,0.08); }
|
|
|
|
/* assign modal */
|
|
.src-toggle { display: flex; gap: 6px; margin-bottom: 16px; }
|
|
.src-btn { padding: 7px 16px; border: 1.5px solid var(--border-h); border-radius: var(--r-pill); background: transparent; font-family: 'Manrope', sans-serif; font-size: 0.82rem; font-weight: 600; color: var(--text-3); cursor: pointer; transition: all 0.18s; }
|
|
.src-btn.active { border-color: var(--violet); color: var(--violet); background: rgba(155,93,229,0.07); }
|
|
.assign-list-item { display: flex; align-items: center; gap: 10px; padding: 8px 0; border-bottom: 1px solid var(--border); font-size: 0.85rem; }
|
|
.assign-list-item:last-child { border-bottom: none; }
|
|
.assign-list-name { flex: 1; }
|
|
.assign-list-type { font-size: 0.72rem; font-weight: 700; padding: 2px 8px; border-radius: 99px; background: rgba(155,93,229,0.1); color: var(--violet); }
|
|
.btn-rm-assign { padding: 4px 10px; border: 1.5px solid transparent; border-radius: 99px; background: transparent; font-size: 0.76rem; font-weight: 600; color: var(--text-3); cursor: pointer; transition: all 0.18s; }
|
|
.btn-rm-assign:hover { border-color: var(--pink); color: var(--pink); }
|
|
.uploader { font-size: 0.72rem; color: var(--text-3); margin-top: 3px; }
|
|
|
|
.spinner { width: 32px; height: 32px; border: 3px solid var(--border); border-top-color: var(--violet); border-radius: 50%; animation: spin 0.8s linear infinite; margin: 60px auto; display: block; }
|
|
.empty { text-align: center; padding: 60px 20px; color: var(--text-3); }
|
|
.empty-icon { font-size: 3rem; margin-bottom: 12px; }
|
|
.empty-text { font-size: 0.9rem; }
|
|
.error-msg { color: var(--pink); font-size: 0.85rem; text-align: center; padding: 20px; }
|
|
|
|
/* modal */
|
|
.modal-overlay { position: fixed; inset: 0; z-index: 400; display: none; align-items: flex-start; justify-content: center; padding: 40px 20px 60px; background: rgba(15,23,42,0.5); backdrop-filter: blur(10px); overflow-y: auto; }
|
|
.modal-overlay.open { display: flex; }
|
|
.modal-box { background: #fff; border-radius: 24px; padding: 36px; width: 100%; max-width: 560px; box-shadow: 0 40px 100px rgba(15,23,42,0.24); }
|
|
.modal-title { font-family: 'Unbounded', sans-serif; font-size: 1rem; font-weight: 800; margin-bottom: 24px; }
|
|
.form-row { margin-bottom: 16px; }
|
|
.form-label { display: block; font-size: 0.72rem; font-weight: 700; color: var(--text-3); text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 6px; }
|
|
.form-ctrl { width: 100%; padding: 10px 14px; border: 1.5px solid rgba(15,23,42,0.16); border-radius: 10px; font-family: 'Manrope', sans-serif; font-size: 0.9rem; color: var(--text); background: #f8f9ff; transition: border-color 0.2s; resize: vertical; }
|
|
.form-ctrl:focus { outline: none; border-color: var(--violet); background: #fff; }
|
|
.file-drop { border: 2px dashed rgba(15,23,42,0.18); border-radius: 14px; padding: 32px; text-align: center; cursor: pointer; transition: border-color 0.2s, background 0.2s; }
|
|
.file-drop:hover, .file-drop.dragover { border-color: var(--violet); background: rgba(155,93,229,0.04); }
|
|
.file-drop-icon { font-size: 2.5rem; margin-bottom: 8px; }
|
|
.file-drop-text { font-size: 0.88rem; color: var(--text-3); line-height: 1.5; }
|
|
.file-drop-name { font-size: 0.88rem; font-weight: 700; color: var(--violet); margin-top: 6px; }
|
|
.toggle-row { display: flex; align-items: center; gap: 12px; }
|
|
.toggle-label { font-size: 0.88rem; font-weight: 600; }
|
|
.toggle { position: relative; width: 44px; height: 24px; }
|
|
.toggle input { opacity: 0; width: 0; height: 0; }
|
|
.toggle-track { position: absolute; inset: 0; border-radius: 12px; background: rgba(15,23,42,0.15); transition: background 0.2s; cursor: pointer; }
|
|
.toggle input:checked + .toggle-track { background: var(--green); }
|
|
.toggle-track::after { content:''; position: absolute; left: 3px; top: 3px; width: 18px; height: 18px; border-radius: 50%; background: #fff; transition: transform 0.2s; }
|
|
.toggle input:checked + .toggle-track::after { transform: translateX(20px); }
|
|
.modal-footer { display: flex; gap: 10px; justify-content: flex-end; margin-top: 24px; border-top: 1px solid rgba(15,23,42,0.08); padding-top: 20px; }
|
|
.btn-cancel { padding: 10px 22px; border: 1.5px solid rgba(15,23,42,0.2); border-radius: 999px; background: transparent; font-family: 'Manrope', sans-serif; font-size: 0.88rem; font-weight: 600; color: var(--text-3); cursor: pointer; }
|
|
.btn-save { padding: 10px 28px; border: none; border-radius: 999px; background: var(--grad-1); color: #fff; font-family: 'Manrope', sans-serif; font-size: 0.88rem; font-weight: 700; cursor: pointer; min-width: 120px; }
|
|
.btn-save:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
.form-error { font-size: 0.82rem; color: var(--pink); margin-top: 8px; min-height: 18px; }
|
|
|
|
/* ── Mobile responsive ── */
|
|
@media (max-width: 768px) {
|
|
.container { padding: 20px 14px 80px; }
|
|
.file-grid { grid-template-columns: 1fr; gap: 12px; }
|
|
.folder-grid { gap: 8px; }
|
|
.folder-card { min-width: 140px; }
|
|
.t-input { width: 100%; flex: 1 1 140px; }
|
|
.t-select { flex: 1 1 120px; min-width: 0; }
|
|
.toolbar { gap: 8px; }
|
|
.modal-overlay { align-items: flex-end; padding: 0; }
|
|
.modal-box { border-radius: 22px 22px 0 0; max-height: 90vh; overflow-y: auto; padding: 28px 20px 36px; }
|
|
}
|
|
@media (max-width: 480px) {
|
|
.container { padding: 16px 12px 80px; }
|
|
.folder-card { max-width: 100%; width: 100%; }
|
|
.t-select, .t-input { font-size: 16px; }
|
|
.btn-upload { width: 100%; text-align: center; }
|
|
}
|
|
</style>
|
|
<script src="https://cdn.jsdelivr.net/npm/lucide@0.469.0/dist/umd/lucide.min.js"></script>
|
|
</head>
|
|
<body>
|
|
<div class="app-layout">
|
|
<aside class="sidebar" id="app-sidebar"></aside>
|
|
<div class="notif-drop" id="notif-drop"></div>
|
|
<div class="sb-content">
|
|
|
|
<div class="container">
|
|
<div class="page-title"><i data-lucide="book-open" style="width:22px;height:22px;vertical-align:-4px;margin-right:6px"></i>Библиотека материалов</div>
|
|
<div class="page-sub">Методички, конспекты, задачники и другие учебные материалы</div>
|
|
|
|
<!-- Breadcrumb (shown inside folder) -->
|
|
<div class="lib-bc" id="lib-bc">
|
|
<button class="lib-bc-link" onclick="openRoot()">
|
|
<i data-lucide="book-open" style="width:13px;height:13px"></i> Библиотека
|
|
</button>
|
|
<span class="lib-bc-sep">/</span>
|
|
<span class="lib-bc-curr" id="lib-bc-name"></span>
|
|
</div>
|
|
|
|
<!-- Toolbar -->
|
|
<div class="toolbar">
|
|
<select class="t-select" id="f-subject" onchange="loadAll()">
|
|
<option value="">Все предметы</option>
|
|
<option value="bio">Биология</option>
|
|
<option value="chem">Химия</option>
|
|
<option value="math">Математика</option>
|
|
<option value="phys">Физика</option>
|
|
<option value="other">Другое</option>
|
|
</select>
|
|
<select class="t-select" id="f-type" onchange="render()">
|
|
<option value="">Все форматы</option>
|
|
<option value="pdf">PDF</option>
|
|
<option value="image">Изображения</option>
|
|
<option value="doc">Word / PPT</option>
|
|
</select>
|
|
<input class="t-input" id="f-search" type="text" placeholder="Поиск по названию…" oninput="render()" />
|
|
<span class="t-count" id="f-count"></span>
|
|
<button class="btn-upload" id="btn-new-folder" style="display:none;background:rgba(155,93,229,0.1);color:var(--violet);border:1.5px solid rgba(155,93,229,0.2)" onclick="openFolderModal()">
|
|
<i data-lucide="folder-plus" style="width:13px;height:13px;vertical-align:-2px"></i> Папка
|
|
</button>
|
|
<button class="btn-upload" id="btn-upload-main" style="display:none" onclick="openUpload()">
|
|
<i data-lucide="upload" style="width:13px;height:13px;vertical-align:-2px"></i> Загрузить
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Folders section (hidden inside a folder) -->
|
|
<div id="folders-section">
|
|
<div class="section-label" id="folders-label" style="display:none">Папки</div>
|
|
<div class="folder-grid" id="folder-grid"></div>
|
|
</div>
|
|
|
|
<!-- Files section -->
|
|
<div class="section-label" id="files-label" style="display:none">Файлы</div>
|
|
<div id="f-grid"><div class="spinner"></div></div>
|
|
</div>
|
|
|
|
<!-- ─── Folder Modal (create / rename) ─────────────────────────────── -->
|
|
<!-- ─── Folder Access Modal ─────────────────────────────────────────── -->
|
|
<div class="modal-overlay" id="folder-access-modal" onclick="if(event.target===this)closeFolderAccess()">
|
|
<div class="modal-box">
|
|
<div class="modal-title">
|
|
<i data-lucide="lock" style="width:16px;height:16px;vertical-align:-3px;margin-right:6px"></i>Доступ:
|
|
<span id="fa-folder-name" style="color:var(--violet)"></span>
|
|
</div>
|
|
|
|
<!-- Status bar -->
|
|
<div class="fa-status-bar public" id="fa-status-bar">
|
|
<div class="fa-status-text" id="fa-status-text">
|
|
<span style="color:var(--green)"><svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8" fill="currentColor" stroke="none"/></svg></span> Открыта всем
|
|
</div>
|
|
<button class="btn-clear-access" id="fa-clear-btn" style="display:none" onclick="doClearFolderAccess()">
|
|
<i data-lucide="unlock" style="width:11px;height:11px;vertical-align:-1px"></i> Открыть всем
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Add access section -->
|
|
<div style="font-size:0.72rem;font-weight:700;color:var(--text-3);text-transform:uppercase;letter-spacing:.06em;margin-bottom:10px">Добавить доступ</div>
|
|
|
|
<div class="src-toggle">
|
|
<button class="src-btn active" id="fasrc-class" onclick="setFolderAccessType('class')">
|
|
<i data-lucide="users" style="width:13px;height:13px;vertical-align:-2px"></i> Классу
|
|
</button>
|
|
<button class="src-btn" id="fasrc-user" onclick="setFolderAccessType('user')">
|
|
<i data-lucide="user" style="width:13px;height:13px;vertical-align:-2px"></i> Ученику
|
|
</button>
|
|
</div>
|
|
|
|
<div id="fa-class-block">
|
|
<div class="form-row">
|
|
<label class="form-label">Выберите класс</label>
|
|
<select class="form-ctrl" id="fa-class-sel"></select>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="fa-user-block" style="display:none">
|
|
<div class="form-row">
|
|
<label class="form-label">Поиск ученика</label>
|
|
<input type="text" class="form-ctrl" id="fa-student-filter" placeholder="Имя или email…" oninput="filterStudentList()" autocomplete="off" />
|
|
<div class="student-pick-list" id="fa-student-list">
|
|
<div style="padding:12px;font-size:0.82rem;color:var(--text-3);text-align:center">Загрузка…</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-error" id="fa-error"></div>
|
|
<div class="modal-footer">
|
|
<button class="btn-cancel" onclick="closeFolderAccess()">Закрыть</button>
|
|
<button class="btn-save" id="fa-save" onclick="doFolderAssign()">Добавить</button>
|
|
</div>
|
|
|
|
<!-- Current access list -->
|
|
<div style="margin-top:20px;border-top:1px solid rgba(15,23,42,0.08);padding-top:16px">
|
|
<div style="font-size:0.72rem;font-weight:700;color:var(--text-3);text-transform:uppercase;letter-spacing:.06em;margin-bottom:10px">Текущий доступ</div>
|
|
<div id="fa-current"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ─── Assign Modal ─────────────────────────────────────────────────── -->
|
|
<div class="modal-overlay" id="assign-modal" onclick="if(event.target===this)closeAssign()">
|
|
<div class="modal-box">
|
|
<div class="modal-title">Назначить доступ к файлу</div>
|
|
|
|
<div class="src-toggle">
|
|
<button class="src-btn active" id="asrc-class" onclick="setAssignType('class')"><i data-lucide="users" style="width:13px;height:13px;vertical-align:-2px"></i> Классу</button>
|
|
<button class="src-btn" id="asrc-user" onclick="setAssignType('user')"><i data-lucide="user" style="width:13px;height:13px;vertical-align:-2px"></i> Ученику</button>
|
|
</div>
|
|
|
|
<div id="assign-class-block">
|
|
<div class="form-row">
|
|
<label class="form-label">Выберите класс</label>
|
|
<select class="form-ctrl" id="assign-class-sel"></select>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="assign-user-block" style="display:none">
|
|
<div class="form-row">
|
|
<label class="form-label">Email ученика</label>
|
|
<input type="email" class="form-ctrl" id="assign-email" placeholder="student@example.com" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-error" id="assign-error"></div>
|
|
<div class="modal-footer">
|
|
<button class="btn-cancel" onclick="closeAssign()">Закрыть</button>
|
|
<button class="btn-save" id="assign-save" onclick="doAssign()">Назначить</button>
|
|
</div>
|
|
|
|
<div style="margin-top:20px;border-top:1px solid rgba(15,23,42,0.08);padding-top:16px">
|
|
<div style="font-size:0.72rem;font-weight:700;color:var(--text-3);text-transform:uppercase;letter-spacing:.06em;margin-bottom:10px">Текущий доступ</div>
|
|
<div id="assign-current"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ─── Upload Modal ─────────────────────────────────────────────────── -->
|
|
<div class="modal-overlay" id="upload-modal" onclick="if(event.target===this)closeUpload()">
|
|
<div class="modal-box">
|
|
<div class="modal-title">Загрузить материал</div>
|
|
|
|
<div class="file-drop" id="drop-zone" onclick="document.getElementById('file-input').click()"
|
|
ondragover="event.preventDefault();this.classList.add('dragover')"
|
|
ondragleave="this.classList.remove('dragover')"
|
|
ondrop="onDrop(event)">
|
|
<div class="file-drop-icon"><i data-lucide="upload-cloud" style="width:40px;height:40px;stroke:var(--violet);stroke-width:1.4"></i></div>
|
|
<div class="file-drop-text">Нажмите или перетащите файл сюда<br><small>PDF, Word, PPT, Excel, изображения · до 50 МБ</small></div>
|
|
<div class="file-drop-name" id="drop-name" style="display:none"></div>
|
|
<input type="file" id="file-input" style="display:none" accept=".pdf,.doc,.docx,.ppt,.pptx,.xls,.xlsx,.png,.jpg,.jpeg,.gif,.webp,.txt" onchange="onFileSelect(this)" />
|
|
</div>
|
|
|
|
<div class="form-row" style="margin-top:18px">
|
|
<label class="form-label">Название *</label>
|
|
<input type="text" class="form-ctrl" id="uf-title" placeholder="Например: Конспект — Клетка и её строение" />
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<label class="form-label">Описание</label>
|
|
<textarea class="form-ctrl" id="uf-desc" rows="2" placeholder="Краткое описание содержимого…"></textarea>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<label class="form-label">Предмет</label>
|
|
<select class="form-ctrl" id="uf-subject">
|
|
<option value="">Без предмета</option>
|
|
<option value="bio">Биология</option>
|
|
<option value="chem">Химия</option>
|
|
<option value="math">Математика</option>
|
|
<option value="phys">Физика</option>
|
|
<option value="other">Другое</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<label class="form-label">Папка</label>
|
|
<select class="form-ctrl" id="uf-folder">
|
|
<option value="">— Корневая папка —</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<div class="toggle-row">
|
|
<label class="toggle">
|
|
<input type="checkbox" id="uf-public" checked />
|
|
<span class="toggle-track"></span>
|
|
</label>
|
|
<span class="toggle-label">Доступен всем</span>
|
|
<span style="font-size:0.78rem;color:var(--text-3)">(снимите галочку — только учителя/администратор)</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-error" id="uf-error"></div>
|
|
<div class="modal-footer">
|
|
<button class="btn-cancel" onclick="closeUpload()">Отмена</button>
|
|
<button class="btn-save" id="uf-save" onclick="doUpload()">Загрузить</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="/js/api.js"></script>
|
|
<script src="/js/sidebar.js"></script>
|
|
<script src="/js/notifications.js"></script>
|
|
<script src="/js/search.js"></script>
|
|
<script src="/js/mobile.js"></script>
|
|
<script>
|
|
const { user, isTeacher, isAdmin } = LS.initPage();
|
|
|
|
LS.showBoardIfAllowed();
|
|
if (isTeacher) {
|
|
document.getElementById('btn-upload-main').style.display = '';
|
|
document.getElementById('btn-new-folder').style.display = '';
|
|
}
|
|
|
|
/* ── constants ── */
|
|
const SUBJ_NAMES = { bio:'Биология', chem:'Химия', math:'Математика', phys:'Физика', other:'Другое' };
|
|
const TYPE_ICONS = {
|
|
'application/pdf': 'file-text',
|
|
'image/png':'image','image/jpeg':'image','image/gif':'image','image/webp':'image',
|
|
'application/msword':'file-type-2',
|
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document':'file-type-2',
|
|
'application/vnd.ms-powerpoint':'presentation',
|
|
'application/vnd.openxmlformats-officedocument.presentationml.presentation':'presentation',
|
|
'application/vnd.ms-excel':'table-2',
|
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':'table-2',
|
|
'text/plain':'file',
|
|
};
|
|
const TYPE_LABELS = {
|
|
'application/pdf':'PDF',
|
|
'image/png':'PNG','image/jpeg':'JPEG','image/gif':'GIF','image/webp':'WebP',
|
|
'application/msword':'Word','application/vnd.openxmlformats-officedocument.wordprocessingml.document':'Word',
|
|
'application/vnd.ms-powerpoint':'PPT','application/vnd.openxmlformats-officedocument.presentationml.presentation':'PPT',
|
|
'application/vnd.ms-excel':'Excel','application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':'Excel',
|
|
'text/plain':'TXT',
|
|
};
|
|
|
|
function typeIcon(mime) {
|
|
const name = TYPE_ICONS[mime] || 'file';
|
|
return `<i data-lucide="${name}" style="width:28px;height:28px;stroke:var(--violet);stroke-width:1.5"></i>`;
|
|
}
|
|
function typeGroup(mime) {
|
|
if (!mime) return 'other';
|
|
if (mime === 'application/pdf') return 'pdf';
|
|
if (mime.startsWith('image/')) return 'image';
|
|
if (mime.includes('word') || mime.includes('presentation') || mime.includes('sheet') || mime.includes('powerpoint') || mime.includes('excel')) return 'doc';
|
|
return 'other';
|
|
}
|
|
function fmtSize(bytes) {
|
|
if (!bytes) return '';
|
|
if (bytes < 1024) return bytes + ' Б';
|
|
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(0) + ' КБ';
|
|
return (bytes / 1024 / 1024).toFixed(1) + ' МБ';
|
|
}
|
|
function fmtDate(d) { return new Date(d).toLocaleDateString('ru',{day:'numeric',month:'short',year:'numeric'}); }
|
|
|
|
/* ── state ── */
|
|
let allFiles = [];
|
|
let allFolders = [];
|
|
let currentFolder = null; // null = root view
|
|
|
|
/* ── load ── */
|
|
async function loadAll() {
|
|
document.getElementById('f-grid').innerHTML = '<div class="spinner"></div>';
|
|
document.getElementById('folder-grid').innerHTML = '';
|
|
try {
|
|
const subject = document.getElementById('f-subject').value || undefined;
|
|
[allFiles, allFolders] = await Promise.all([
|
|
LS.getFiles({ subject }),
|
|
LS.getFolders(),
|
|
]);
|
|
render();
|
|
} catch (e) {
|
|
document.getElementById('f-grid').innerHTML = `<div class="error-msg">Ошибка загрузки: ${esc(e.message)}</div>`;
|
|
}
|
|
}
|
|
|
|
/* ── navigation ── */
|
|
function openFolder(id, name) {
|
|
currentFolder = id;
|
|
document.getElementById('lib-bc').classList.add('visible');
|
|
document.getElementById('lib-bc-name').textContent = name;
|
|
document.getElementById('folders-section').style.display = 'none';
|
|
document.getElementById('files-label').style.display = '';
|
|
render();
|
|
}
|
|
|
|
function openRoot() {
|
|
currentFolder = null;
|
|
document.getElementById('lib-bc').classList.remove('visible');
|
|
document.getElementById('folders-section').style.display = '';
|
|
render();
|
|
}
|
|
|
|
/* ── render ── */
|
|
function render() {
|
|
renderFolders();
|
|
renderFiles();
|
|
}
|
|
|
|
function renderFolders() {
|
|
if (currentFolder !== null) return; // inside folder — don't show folder grid
|
|
|
|
const search = document.getElementById('f-search').value.toLowerCase();
|
|
const subject = document.getElementById('f-subject').value;
|
|
|
|
// Count files per folder (respecting subject filter)
|
|
const foldersWithCount = allFolders.map(fo => {
|
|
const count = allFiles.filter(f => {
|
|
if (f.folder_id !== fo.id) return false;
|
|
if (subject && f.subject_slug !== subject) return false;
|
|
return true;
|
|
}).length;
|
|
return { ...fo, count };
|
|
});
|
|
|
|
const grid = document.getElementById('folder-grid');
|
|
const label = document.getElementById('folders-label');
|
|
|
|
if (!foldersWithCount.length) {
|
|
grid.innerHTML = '';
|
|
label.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
label.style.display = '';
|
|
|
|
// Highlight matching folders when searching
|
|
const visibleFolders = search
|
|
? foldersWithCount.filter(fo => fo.name.toLowerCase().includes(search) || fo.count > 0)
|
|
: foldersWithCount;
|
|
|
|
if (!visibleFolders.length) { grid.innerHTML = ''; label.style.display = 'none'; return; }
|
|
|
|
label.style.display = '';
|
|
grid.innerHTML = visibleFolders.map(fo => `
|
|
<div class="folder-card stagger-item" style="--i:${visibleFolders.indexOf(fo)}" onclick="openFolder(${fo.id}, '${esc(fo.name).replace(/'/g,"\\'")}')">
|
|
<div class="folder-card-icon">
|
|
<i data-lucide="folder" style="width:22px;height:22px;stroke:var(--amber);stroke-width:1.6;fill:rgba(255,179,71,0.15)"></i>
|
|
</div>
|
|
<div class="folder-card-info">
|
|
<div class="folder-card-name">${esc(fo.name)}</div>
|
|
<div class="folder-card-count">${fo.count} ${pluralFiles(fo.count)}</div>
|
|
</div>
|
|
${isTeacher ? `
|
|
<div class="folder-card-acts" onclick="event.stopPropagation()">
|
|
<button class="btn-fol-act" title="Управление доступом" onclick="openFolderAccess(${fo.id}, '${esc(fo.name).replace(/'/g,"\\'")}')">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
|
</button>
|
|
<button class="btn-fol-act" title="Переименовать" onclick="openRenameFolder(${fo.id}, '${esc(fo.name).replace(/'/g,"\\'")}')">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
|
|
</button>
|
|
<button class="btn-fol-act btn-fol-del" title="Удалить папку" onclick="doDeleteFolder(${fo.id}, '${esc(fo.name).replace(/'/g,"\\'")}')">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6M14 11v6"/><path d="M9 6V4h6v2"/></svg>
|
|
</button>
|
|
</div>
|
|
${fo.access_count > 0 ? `<div class="folder-locked-badge" title="Ограниченный доступ"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg></div>` : ''}
|
|
` : ''}
|
|
</div>`).join('');
|
|
if (window.lucide) lucide.createIcons();
|
|
}
|
|
|
|
function pluralFiles(n) {
|
|
if (n % 10 === 1 && n % 100 !== 11) return 'файл';
|
|
if ([2,3,4].includes(n % 10) && ![12,13,14].includes(n % 100)) return 'файла';
|
|
return 'файлов';
|
|
}
|
|
|
|
function renderFiles() {
|
|
const typeF = document.getElementById('f-type').value;
|
|
const searchF = document.getElementById('f-search').value.toLowerCase();
|
|
const subject = document.getElementById('f-subject').value;
|
|
|
|
let filtered = allFiles.filter(f => {
|
|
// folder filter
|
|
if (currentFolder !== null && f.folder_id !== currentFolder) return false;
|
|
if (currentFolder === null && searchF === '' && f.folder_id !== null) return false; // root: hide filed files (show them in folder), unless searching
|
|
// type filter
|
|
if (typeF && typeGroup(f.mimetype) !== typeF) return false;
|
|
// search
|
|
if (searchF && !f.title.toLowerCase().includes(searchF) && !(f.description||'').toLowerCase().includes(searchF)) return false;
|
|
return true;
|
|
});
|
|
|
|
// When searching globally, show all matching files regardless of folder
|
|
if (searchF && currentFolder === null) {
|
|
filtered = allFiles.filter(f => {
|
|
if (subject && f.subject_slug !== subject) return false;
|
|
if (typeF && typeGroup(f.mimetype) !== typeF) return false;
|
|
if (!f.title.toLowerCase().includes(searchF) && !(f.description||'').toLowerCase().includes(searchF)) return false;
|
|
return true;
|
|
});
|
|
}
|
|
|
|
const filesLabel = document.getElementById('files-label');
|
|
if (currentFolder !== null || searchF) {
|
|
filesLabel.style.display = filtered.length ? '' : 'none';
|
|
} else {
|
|
filesLabel.style.display = filtered.length ? '' : 'none';
|
|
}
|
|
|
|
document.getElementById('f-count').textContent = filtered.length ? `${filtered.length} ${pluralFiles(filtered.length)}` : '';
|
|
|
|
if (!filtered.length) {
|
|
const emptyText = isTeacher && currentFolder === null
|
|
? 'Материалов не найдено<br><button class="btn-upload" style="margin-top:16px" onclick="openUpload()">Загрузить первый</button>'
|
|
: currentFolder !== null ? 'Папка пуста' : 'Нет доступных материалов';
|
|
document.getElementById('f-grid').innerHTML = `
|
|
<div class="empty">
|
|
<div class="empty-icon"><i data-lucide="inbox" style="width:48px;height:48px;stroke:var(--text-3);stroke-width:1.2"></i></div>
|
|
<div class="empty-text">${emptyText}</div>
|
|
</div>`;
|
|
if (window.lucide) lucide.createIcons();
|
|
return;
|
|
}
|
|
|
|
document.getElementById('f-grid').innerHTML = `<div class="file-grid">${filtered.map((f, idx) => {
|
|
const icon = typeIcon(f.mimetype);
|
|
const label = TYPE_LABELS[f.mimetype] || 'Файл';
|
|
const folderName = f.folder_id ? (allFolders.find(fo => fo.id === f.folder_id)?.name || '') : '';
|
|
return `<div class="file-card stagger-item" style="--i:${idx}">
|
|
<div class="file-head">
|
|
<div class="file-icon">${icon}</div>
|
|
<div class="file-meta">
|
|
<div class="file-title">${esc(f.title)}</div>
|
|
${f.description ? `<div class="file-desc">${esc(f.description)}</div>` : ''}
|
|
${searchF && folderName && currentFolder === null ? `<div class="file-folder-tag"><i data-lucide="folder" style="width:10px;height:10px;stroke:var(--amber);stroke-width:2"></i> ${esc(folderName)}</div>` : ''}
|
|
</div>
|
|
</div>
|
|
<div class="file-tags">
|
|
${f.subject_slug ? `<span class="tag tag-subj">${SUBJ_NAMES[f.subject_slug]||f.subject_slug}</span>` : ''}
|
|
<span class="tag tag-type">${label}</span>
|
|
<span class="tag ${f.is_public ? 'tag-public' : 'tag-private'}">${f.is_public ? '<i data-lucide="globe" style="width:10px;height:10px;vertical-align:-1px"></i> Публичный' : '<i data-lucide="lock" style="width:10px;height:10px;vertical-align:-1px"></i> Приватный'}</span>
|
|
</div>
|
|
<div class="file-footer">
|
|
<div class="file-meta-text">
|
|
<div class="file-size">${fmtSize(f.size)} · ${fmtDate(f.created_at)}</div>
|
|
<div class="uploader">Загрузил: ${esc(f.uploader_name)}</div>
|
|
</div>
|
|
<div class="file-actions">
|
|
<a class="btn-dl" href="${LS.downloadFileUrl(f.id)}" target="_blank" download="${esc(f.original_name)}"><i data-lucide="external-link" style="width:12px;height:12px;vertical-align:-1px"></i> Открыть</a>
|
|
${isTeacher ? `<button class="btn-move" onclick="openMoveModal(${f.id})" title="Переместить в папку"><i data-lucide="folder-open" style="width:13px;height:13px"></i></button>` : ''}
|
|
${isTeacher ? `<button class="btn-assign" onclick="openAssign(${f.id})" title="Назначить"><i data-lucide="send" style="width:13px;height:13px"></i></button>` : ''}
|
|
${isTeacher ? `<button class="btn-del" onclick="deleteF(${f.id}, '${esc(f.title).replace(/'/g,"\\'")}')"><i data-lucide="x" style="width:13px;height:13px"></i></button>` : ''}
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
}).join('')}</div>`;
|
|
if (window.lucide) lucide.createIcons();
|
|
}
|
|
|
|
async function deleteF(id, title) {
|
|
if (!await LS.confirm(`Удалить файл «${title}»?`, { title: 'Удалить файл', confirmText: 'Удалить' })) return;
|
|
try {
|
|
await LS.deleteFile(id);
|
|
allFiles = allFiles.filter(f => f.id !== id);
|
|
render();
|
|
} catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); }
|
|
}
|
|
|
|
/* ─── Folder modal (create / rename) ─── */
|
|
let _editingFolderId = null;
|
|
let _folderModal = null;
|
|
|
|
function openFolderModal(opts = {}) {
|
|
_editingFolderId = opts.id || null;
|
|
const isRename = !!_editingFolderId;
|
|
const body = `
|
|
<div class="form-row">
|
|
<label class="form-label">Название папки</label>
|
|
<input type="text" class="form-ctrl" id="fm-name" placeholder="Например: Биология 10 класс" maxlength="80" value="${LS.esc(opts.name || '')}" />
|
|
</div>`;
|
|
_folderModal = LS.modal({
|
|
title: isRename ? 'Переименовать папку' : 'Новая папка',
|
|
content: body, size: 'sm',
|
|
actions: [
|
|
{ label: 'Отмена', onClick: () => _folderModal.close() },
|
|
{ label: isRename ? 'Сохранить' : 'Создать', primary: true, id: 'fm-save', onClick: saveFolderModal },
|
|
],
|
|
});
|
|
// Enter to submit
|
|
const nameEl = _folderModal.body.querySelector('#fm-name');
|
|
nameEl.addEventListener('keydown', e => { if (e.key === 'Enter') saveFolderModal(); });
|
|
if (isRename) setTimeout(() => nameEl.select(), 50);
|
|
}
|
|
|
|
function openRenameFolder(id, name) {
|
|
openFolderModal({ id, name });
|
|
}
|
|
|
|
async function saveFolderModal() {
|
|
const name = document.getElementById('fm-name').value.trim();
|
|
if (!name) { _folderModal?.setError('Введите название папки'); return; }
|
|
const btn = document.getElementById('fm-save');
|
|
btn.disabled = true;
|
|
try {
|
|
if (_editingFolderId) {
|
|
await LS.renameFolder(_editingFolderId, name);
|
|
const fo = allFolders.find(f => f.id === _editingFolderId);
|
|
if (fo) fo.name = name;
|
|
if (currentFolder === _editingFolderId) {
|
|
document.getElementById('lib-bc-name').textContent = name;
|
|
}
|
|
} else {
|
|
const { id } = await LS.createFolder(name);
|
|
allFolders.push({ id, name, file_count: 0 });
|
|
}
|
|
_folderModal?.close();
|
|
render();
|
|
} catch (e) {
|
|
_folderModal?.setError(e.message);
|
|
} finally {
|
|
btn.disabled = false;
|
|
}
|
|
}
|
|
|
|
async function doDeleteFolder(id, name) {
|
|
if (!await LS.confirm(`Удалить папку «${name}»? Файлы внутри переместятся в корневую папку.`, { title: 'Удалить папку', confirmText: 'Удалить' })) return;
|
|
try {
|
|
await LS.deleteFolder(id);
|
|
allFolders = allFolders.filter(f => f.id !== id);
|
|
allFiles.forEach(f => { if (f.folder_id === id) f.folder_id = null; });
|
|
if (currentFolder === id) openRoot();
|
|
else render();
|
|
} catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); }
|
|
}
|
|
|
|
/* ─── Folder Access ─── */
|
|
let _folderAccessId = null, _folderAccessType = 'class';
|
|
let _folderClasses = [], _allStudents = [], _selectedStudentId = null;
|
|
|
|
async function openFolderAccess(folderId, folderName) {
|
|
_folderAccessId = folderId;
|
|
_selectedStudentId = null;
|
|
document.getElementById('fa-folder-name').textContent = folderName;
|
|
document.getElementById('fa-error').textContent = '';
|
|
document.getElementById('fa-student-filter').value = '';
|
|
setFolderAccessType('class');
|
|
document.getElementById('folder-access-modal').classList.add('open');
|
|
|
|
// Load classes + students in parallel
|
|
try {
|
|
const [classes, students] = await Promise.all([
|
|
LS.getClasses(),
|
|
_allStudents.length ? Promise.resolve(_allStudents) : LS.getStudentsList(),
|
|
]);
|
|
_folderClasses = classes;
|
|
if (!_allStudents.length) _allStudents = students.slice().sort((a, b) => a.name.localeCompare(b.name, 'ru'));
|
|
|
|
const sel = document.getElementById('fa-class-sel');
|
|
sel.innerHTML = _folderClasses.length
|
|
? _folderClasses.map(c => `<option value="${c.id}">${esc(c.name)}</option>`).join('')
|
|
: '<option value="">Нет доступных классов</option>';
|
|
|
|
renderStudentList(_allStudents);
|
|
} catch {}
|
|
|
|
renderFolderAccessCurrent(folderId);
|
|
}
|
|
|
|
function closeFolderAccess() {
|
|
document.getElementById('folder-access-modal').classList.remove('open');
|
|
}
|
|
|
|
function setFolderAccessType(t) {
|
|
_folderAccessType = t;
|
|
document.getElementById('fasrc-class').classList.toggle('active', t === 'class');
|
|
document.getElementById('fasrc-user').classList.toggle('active', t === 'user');
|
|
document.getElementById('fa-class-block').style.display = t === 'class' ? '' : 'none';
|
|
document.getElementById('fa-user-block').style.display = t === 'user' ? '' : 'none';
|
|
}
|
|
|
|
/* Student list */
|
|
function filterStudentList() {
|
|
const q = document.getElementById('fa-student-filter').value.toLowerCase();
|
|
const filtered = q
|
|
? _allStudents.filter(s => s.name.toLowerCase().includes(q) || s.email.toLowerCase().includes(q))
|
|
: _allStudents;
|
|
renderStudentList(filtered);
|
|
}
|
|
|
|
function renderStudentList(students) {
|
|
const el = document.getElementById('fa-student-list');
|
|
if (!students.length) {
|
|
el.innerHTML = '<div style="padding:12px;font-size:0.82rem;color:var(--text-3);text-align:center">Ученики не найдены</div>';
|
|
return;
|
|
}
|
|
el.innerHTML = students.map(s => `
|
|
<div class="student-pick-item ${s.id === _selectedStudentId ? 'sp-selected' : ''}" onclick="selectStudent(${s.id})">
|
|
<div class="sp-check"></div>
|
|
<div class="sp-info">
|
|
<div class="sp-name">${esc(s.name)}</div>
|
|
<div class="sp-email">${esc(s.email)}</div>
|
|
</div>
|
|
</div>`).join('');
|
|
}
|
|
|
|
function selectStudent(id) {
|
|
_selectedStudentId = _selectedStudentId === id ? null : id;
|
|
filterStudentList();
|
|
}
|
|
|
|
/* Status bar */
|
|
function updateFolderStatusBar(count) {
|
|
const bar = document.getElementById('fa-status-bar');
|
|
const text = document.getElementById('fa-status-text');
|
|
const btn = document.getElementById('fa-clear-btn');
|
|
if (count === 0) {
|
|
bar.className = 'fa-status-bar public';
|
|
text.innerHTML = '<span style="color:var(--green)"><svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8" fill="currentColor" stroke="none"/></svg></span> Открыта всем — без ограничений';
|
|
btn.style.display = 'none';
|
|
} else {
|
|
const w = count === 1 ? 'запись' : count < 5 ? 'записи' : 'записей';
|
|
bar.className = 'fa-status-bar restrict';
|
|
text.innerHTML = `<span style="color:var(--violet)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" width="13" height="13" stroke-width="2.5" style="vertical-align:-2px"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg></span> Ограниченный доступ — ${count} ${w}`;
|
|
btn.style.display = '';
|
|
}
|
|
}
|
|
|
|
async function renderFolderAccessCurrent(folderId) {
|
|
const el = document.getElementById('fa-current');
|
|
el.innerHTML = '<div style="font-size:0.8rem;color:var(--text-3)">Загрузка…</div>';
|
|
try {
|
|
const rows = await LS.getFolderAccess(folderId);
|
|
updateFolderStatusBar(rows.length);
|
|
if (!rows.length) {
|
|
el.innerHTML = '<div style="font-size:0.82rem;color:var(--text-3)">Нет ограничений — папка видна всем</div>';
|
|
} else {
|
|
el.innerHTML = rows.map(r => `
|
|
<div class="assign-list-item">
|
|
<span class="assign-list-type">${r.type === 'class' ? 'Класс' : 'Ученик'}</span>
|
|
<span class="assign-list-name">${esc(r.target_name || String(r.target_id))}</span>
|
|
<button class="btn-rm-assign" onclick="doFolderUnassign('${r.type}',${r.target_id})"><svg class="ic" viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>
|
|
</div>`).join('');
|
|
}
|
|
const fo = allFolders.find(f => f.id === folderId);
|
|
if (fo) { fo.access_count = rows.length; render(); }
|
|
} catch { el.innerHTML = ''; }
|
|
}
|
|
|
|
async function doFolderAssign() {
|
|
const errEl = document.getElementById('fa-error');
|
|
errEl.textContent = '';
|
|
const data = { type: _folderAccessType };
|
|
if (_folderAccessType === 'class') {
|
|
data.target_id = Number(document.getElementById('fa-class-sel').value);
|
|
if (!data.target_id) { errEl.textContent = 'Выберите класс'; return; }
|
|
} else {
|
|
if (!_selectedStudentId) { errEl.textContent = 'Выберите ученика из списка'; return; }
|
|
data.target_id = _selectedStudentId;
|
|
}
|
|
const btn = document.getElementById('fa-save');
|
|
btn.disabled = true;
|
|
try {
|
|
await LS.assignFolder(_folderAccessId, data);
|
|
_selectedStudentId = null;
|
|
filterStudentList();
|
|
await renderFolderAccessCurrent(_folderAccessId);
|
|
} catch (e) { errEl.textContent = e.message; }
|
|
finally { btn.disabled = false; }
|
|
}
|
|
|
|
async function doFolderUnassign(type, targetId) {
|
|
try {
|
|
await LS.unassignFolder(_folderAccessId, type, targetId);
|
|
await renderFolderAccessCurrent(_folderAccessId);
|
|
} catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); }
|
|
}
|
|
|
|
async function doClearFolderAccess() {
|
|
if (!await LS.confirm('Снять все ограничения? Папка станет видна всем пользователям.', { title: 'Открыть всем', confirmText: 'Открыть' })) return;
|
|
try {
|
|
await LS.clearFolderAccess(_folderAccessId);
|
|
await renderFolderAccessCurrent(_folderAccessId);
|
|
} catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); }
|
|
}
|
|
|
|
/* ─── Move file modal ─── */
|
|
let _movingFileId = null;
|
|
let _moveModal = null;
|
|
|
|
function openMoveModal(fileId) {
|
|
_movingFileId = fileId;
|
|
const currentFolderId = allFiles.find(f => f.id === fileId)?.folder_id || '';
|
|
const options = `<option value="">— Корневая папка —</option>` +
|
|
allFolders.map(fo => `<option value="${fo.id}" ${fo.id === currentFolderId ? 'selected' : ''}>${esc(fo.name)}</option>`).join('');
|
|
const body = `
|
|
<div class="form-row">
|
|
<label class="form-label">Папка назначения</label>
|
|
<select class="form-ctrl" id="mv-folder">${options}</select>
|
|
</div>`;
|
|
_moveModal = LS.modal({
|
|
title: 'Переместить файл', content: body, size: 'sm',
|
|
actions: [
|
|
{ label: 'Отмена', onClick: () => _moveModal.close() },
|
|
{ label: 'Переместить', primary: true, id: 'mv-save', onClick: doMoveFile },
|
|
],
|
|
});
|
|
}
|
|
|
|
async function doMoveFile() {
|
|
const folderId = document.getElementById('mv-folder').value || null;
|
|
const btn = document.getElementById('mv-save');
|
|
btn.disabled = true;
|
|
try {
|
|
await LS.moveFile(_movingFileId, folderId ? Number(folderId) : null);
|
|
const f = allFiles.find(f => f.id === _movingFileId);
|
|
if (f) f.folder_id = folderId ? Number(folderId) : null;
|
|
_moveModal?.close();
|
|
render();
|
|
LS.toast('Файл перемещён', 'success');
|
|
} catch (e) {
|
|
_moveModal?.setError(e.message);
|
|
} finally {
|
|
btn.disabled = false;
|
|
}
|
|
}
|
|
|
|
/* ─── Upload ─── */
|
|
let selectedFile = null;
|
|
|
|
function openUpload() {
|
|
selectedFile = null;
|
|
document.getElementById('uf-title').value = '';
|
|
document.getElementById('uf-desc').value = '';
|
|
document.getElementById('uf-subject').value = '';
|
|
document.getElementById('uf-public').checked = true;
|
|
document.getElementById('uf-error').textContent = '';
|
|
document.getElementById('drop-name').style.display = 'none';
|
|
document.getElementById('file-input').value = '';
|
|
|
|
// Populate folder selector
|
|
const sel = document.getElementById('uf-folder');
|
|
sel.innerHTML = `<option value="">— Корневая папка —</option>` +
|
|
allFolders.map(fo => `<option value="${fo.id}" ${fo.id === currentFolder ? 'selected' : ''}>${esc(fo.name)}</option>`).join('');
|
|
|
|
document.getElementById('upload-modal').classList.add('open');
|
|
}
|
|
|
|
function closeUpload() {
|
|
document.getElementById('upload-modal').classList.remove('open');
|
|
}
|
|
|
|
function onFileSelect(input) {
|
|
if (!input.files[0]) return;
|
|
selectedFile = input.files[0];
|
|
const nameEl = document.getElementById('drop-name');
|
|
nameEl.textContent = selectedFile.name;
|
|
nameEl.style.display = '';
|
|
if (!document.getElementById('uf-title').value) {
|
|
document.getElementById('uf-title').value = selectedFile.name.replace(/\.[^.]+$/, '');
|
|
}
|
|
}
|
|
|
|
function onDrop(e) {
|
|
e.preventDefault();
|
|
document.getElementById('drop-zone').classList.remove('dragover');
|
|
const file = e.dataTransfer.files[0];
|
|
if (!file) return;
|
|
selectedFile = file;
|
|
const nameEl = document.getElementById('drop-name');
|
|
nameEl.textContent = file.name;
|
|
nameEl.style.display = '';
|
|
if (!document.getElementById('uf-title').value) {
|
|
document.getElementById('uf-title').value = file.name.replace(/\.[^.]+$/, '');
|
|
}
|
|
}
|
|
|
|
async function doUpload() {
|
|
const title = document.getElementById('uf-title').value.trim();
|
|
const desc = document.getElementById('uf-desc').value.trim();
|
|
const subject = document.getElementById('uf-subject').value;
|
|
const folderId = document.getElementById('uf-folder').value;
|
|
const isPublic = document.getElementById('uf-public').checked;
|
|
const errEl = document.getElementById('uf-error');
|
|
errEl.textContent = '';
|
|
|
|
if (!selectedFile) { errEl.textContent = 'Выберите файл'; return; }
|
|
if (!title) { errEl.textContent = 'Введите название'; return; }
|
|
|
|
const fd = new FormData();
|
|
fd.append('file', selectedFile);
|
|
fd.append('title', title);
|
|
fd.append('description', desc);
|
|
fd.append('subject_slug', subject);
|
|
fd.append('is_public', isPublic ? '1' : '0');
|
|
if (folderId) fd.append('folder_id', folderId);
|
|
|
|
const btn = document.getElementById('uf-save');
|
|
btn.disabled = true; btn.textContent = 'Загрузка…';
|
|
try {
|
|
await LS.uploadFile(fd);
|
|
closeUpload();
|
|
loadAll();
|
|
} catch (e) {
|
|
errEl.textContent = 'Ошибка: ' + e.message;
|
|
} finally {
|
|
btn.disabled = false; btn.textContent = 'Загрузить';
|
|
}
|
|
}
|
|
|
|
/* ─── Assign ─── */
|
|
let _assignFileId = null, _assignType = 'class', _assignClasses = [];
|
|
|
|
async function openAssign(fileId) {
|
|
_assignFileId = fileId;
|
|
_assignType = 'class';
|
|
document.getElementById('assign-error').textContent = '';
|
|
document.getElementById('assign-email').value = '';
|
|
setAssignType('class');
|
|
document.getElementById('assign-modal').classList.add('open');
|
|
try {
|
|
_assignClasses = await LS.getClasses();
|
|
const sel = document.getElementById('assign-class-sel');
|
|
sel.innerHTML = _assignClasses.length
|
|
? _assignClasses.map(c => `<option value="${c.id}">${esc(c.name)}</option>`).join('')
|
|
: '<option value="">Нет доступных классов</option>';
|
|
} catch {}
|
|
renderAssignCurrent(fileId);
|
|
}
|
|
|
|
async function renderAssignCurrent(fileId) {
|
|
const el = document.getElementById('assign-current');
|
|
el.innerHTML = '<div style="font-size:0.8rem;color:var(--text-3)">Загрузка…</div>';
|
|
try {
|
|
const rows = await LS.getFileAccess(fileId);
|
|
if (!rows.length) { el.innerHTML = '<div style="font-size:0.82rem;color:var(--text-3)">Нет назначений</div>'; return; }
|
|
el.innerHTML = rows.map(r => `
|
|
<div class="assign-list-item">
|
|
<span class="assign-list-type">${r.type === 'class' ? 'Класс' : 'Ученик'}</span>
|
|
<span class="assign-list-name">${esc(r.target_name || r.target_id)}</span>
|
|
<button class="btn-rm-assign" onclick="doUnassign('${r.type}',${r.target_id})"><svg class="ic" viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>
|
|
</div>`).join('');
|
|
} catch { el.innerHTML = ''; }
|
|
}
|
|
|
|
function setAssignType(t) {
|
|
_assignType = t;
|
|
document.getElementById('asrc-class').classList.toggle('active', t === 'class');
|
|
document.getElementById('asrc-user').classList.toggle('active', t === 'user');
|
|
document.getElementById('assign-class-block').style.display = t === 'class' ? '' : 'none';
|
|
document.getElementById('assign-user-block').style.display = t === 'user' ? '' : 'none';
|
|
}
|
|
|
|
function closeAssign() { document.getElementById('assign-modal').classList.remove('open'); }
|
|
|
|
async function doAssign() {
|
|
const errEl = document.getElementById('assign-error');
|
|
errEl.textContent = '';
|
|
const data = { type: _assignType };
|
|
if (_assignType === 'class') {
|
|
data.target_id = Number(document.getElementById('assign-class-sel').value);
|
|
if (!data.target_id) { errEl.textContent = 'Выберите класс'; return; }
|
|
} else {
|
|
data.email = document.getElementById('assign-email').value.trim();
|
|
if (!data.email) { errEl.textContent = 'Введите email'; return; }
|
|
}
|
|
const btn = document.getElementById('assign-save');
|
|
btn.disabled = true;
|
|
try {
|
|
await LS.assignFile(_assignFileId, data);
|
|
await renderAssignCurrent(_assignFileId);
|
|
document.getElementById('assign-email').value = '';
|
|
} catch (e) { errEl.textContent = e.message; }
|
|
finally { btn.disabled = false; }
|
|
}
|
|
|
|
async function doUnassign(type, targetId) {
|
|
try {
|
|
await LS.unassignFile(_assignFileId, type, targetId);
|
|
await renderAssignCurrent(_assignFileId);
|
|
} catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); }
|
|
}
|
|
|
|
loadAll();
|
|
if (window.lucide) lucide.createIcons();
|
|
LS.notif.init();
|
|
</script>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|