Files
Maxim Dolgolyov 1c20bafd05 style(library): аккуратная карточка файла — действия отдельным рядом
Раньше метаданные и «Открыть»+3 иконки делили одну строку → на ширине грида
дата переносилась криво, кнопки наезжали. Теперь подвал колонкой: метаданные
строкой, действия — отдельным рядом (Открыть растягивается, иконки — компактные
квадраты 34×34 с лёгким фоном). Без наложений и кривых переносов.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 23:32:43 +03:00

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>