feat: unified modpack creation and upload form in admin panel
- Объединены формы создания модпака и загрузки файлов в одну - Slug генерируется автоматически из названия с возможностью редактирования - Поддержка загрузки файлов сразу при создании/обновлении модпака - Автоматическое распаковывание ZIP архивов - Сохранение файлов в CAS с дедупликацией по SHA-1 - Улучшен UI с прогресс баром и валидацией форм
This commit is contained in:
@@ -134,33 +134,101 @@ func (h *Handler) listModpacks(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) createModpack(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) createModpack(w http.ResponseWriter, r *http.Request) {
|
||||||
body, err := io.ReadAll(r.Body)
|
// Parse multipart form (for both modpack data and file uploads)
|
||||||
if err != nil {
|
if err := r.ParseMultipartForm(500 << 20); err != nil { // 500 MB max
|
||||||
utils.WriteError(w, http.StatusBadRequest, "Cannot read body")
|
utils.WriteError(w, http.StatusBadRequest, "Cannot parse form (max 500 MB)")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var req modpackRequest
|
// Extract modpack data from form
|
||||||
if err := json.Unmarshal(body, &req); err != nil {
|
slug := r.FormValue("slug")
|
||||||
utils.WriteError(w, http.StatusBadRequest, "Invalid JSON")
|
name := r.FormValue("name")
|
||||||
return
|
minecraftVersion := r.FormValue("minecraft_version")
|
||||||
}
|
javaVersionStr := r.FormValue("java_version")
|
||||||
|
serverIP := r.FormValue("server_ip")
|
||||||
|
|
||||||
if req.Slug == "" || req.Name == "" || req.MinecraftVersion == "" {
|
// Validate required fields
|
||||||
|
if slug == "" || name == "" || minecraftVersion == "" {
|
||||||
utils.WriteError(w, http.StatusBadRequest, "slug, name, and minecraft_version are required")
|
utils.WriteError(w, http.StatusBadRequest, "slug, name, and minecraft_version are required")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = h.db.Pool().Exec(r.Context(),
|
// Parse java_version
|
||||||
|
javaVersion := 8 // default
|
||||||
|
if javaVersionStr != "" {
|
||||||
|
if parsed, err := strconv.Atoi(javaVersionStr); err == nil {
|
||||||
|
javaVersion = parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert modpack into database
|
||||||
|
_, err := h.db.Pool().Exec(r.Context(),
|
||||||
`INSERT INTO modpacks (slug, name, minecraft_version, java_version, server_ip, is_active)
|
`INSERT INTO modpacks (slug, name, minecraft_version, java_version, server_ip, is_active)
|
||||||
VALUES ($1, $2, $3, $4, $5, true)`,
|
VALUES ($1, $2, $3, $4, $5, true)`,
|
||||||
req.Slug, req.Name, req.MinecraftVersion, req.JavaVersion, req.ServerIP,
|
slug, name, minecraftVersion, javaVersion, serverIP,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.WriteError(w, http.StatusConflict, "Modpack with this slug already exists")
|
utils.WriteError(w, http.StatusConflict, "Modpack with this slug already exists")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle file uploads if any
|
||||||
|
files := r.MultipartForm.File["files"]
|
||||||
|
if len(files) > 0 {
|
||||||
|
// Create instance directory for this modpack
|
||||||
|
uploadDir := filepath.Join(h.cfg.CASDir, "..", "instances", slug)
|
||||||
|
os.MkdirAll(uploadDir, 0o755)
|
||||||
|
|
||||||
|
var uploaded []string
|
||||||
|
for _, fh := range files {
|
||||||
|
src, err := fh.Open()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := io.ReadAll(src)
|
||||||
|
src.Close()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasSuffix(strings.ToLower(fh.Filename), ".zip") {
|
||||||
|
extracted, err := h.extractZip(data, uploadDir)
|
||||||
|
if err == nil {
|
||||||
|
uploaded = append(uploaded, extracted...)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hash := utils.SHA1Bytes(data)
|
||||||
|
destDir := filepath.Join(h.cfg.CASDir, hash[:2])
|
||||||
|
os.MkdirAll(destDir, 0o755)
|
||||||
|
os.WriteFile(filepath.Join(destDir, hash), data, 0o644)
|
||||||
|
|
||||||
|
h.db.Pool().Exec(r.Context(),
|
||||||
|
`INSERT INTO global_files (sha1, size_bytes, file_name) VALUES ($1, $2, $3)
|
||||||
|
ON CONFLICT (sha1) DO NOTHING`,
|
||||||
|
hash, int64(len(data)), fh.Filename,
|
||||||
|
)
|
||||||
|
|
||||||
|
instPath := filepath.Join(uploadDir, "mods", fh.Filename)
|
||||||
|
os.MkdirAll(filepath.Dir(instPath), 0o755)
|
||||||
|
os.WriteFile(instPath, data, 0o644)
|
||||||
|
|
||||||
|
uploaded = append(uploaded, fh.Filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Optionally generate manifest after upload
|
||||||
|
// For now, just return success with upload info
|
||||||
|
utils.WriteJSON(w, http.StatusCreated, map[string]interface{}{
|
||||||
|
"status": "created",
|
||||||
|
"uploaded": uploaded,
|
||||||
|
"count": len(uploaded),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// No files uploaded, just created modpack
|
||||||
utils.WriteJSON(w, http.StatusCreated, map[string]string{"status": "created"})
|
utils.WriteJSON(w, http.StatusCreated, map[string]string{"status": "created"})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,28 +239,101 @@ func (h *Handler) updateModpack(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
body, err := io.ReadAll(r.Body)
|
// Parse multipart form (for both modpack data and file uploads)
|
||||||
if err != nil {
|
if err := r.ParseMultipartForm(500 << 20); err != nil { // 500 MB max
|
||||||
utils.WriteError(w, http.StatusBadRequest, "Cannot read body")
|
utils.WriteError(w, http.StatusBadRequest, "Cannot parse form (max 500 MB)")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var req modpackRequest
|
// Extract modpack data from form
|
||||||
if err := json.Unmarshal(body, &req); err != nil {
|
name := r.FormValue("name")
|
||||||
utils.WriteError(w, http.StatusBadRequest, "Invalid JSON")
|
slug := r.FormValue("slug")
|
||||||
|
minecraftVersion := r.FormValue("minecraft_version")
|
||||||
|
javaVersionStr := r.FormValue("java_version")
|
||||||
|
serverIP := r.FormValue("server_ip")
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if name == "" || slug == "" || minecraftVersion == "" {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "name, slug, and minecraft_version are required")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse java_version
|
||||||
|
javaVersion := 8 // default
|
||||||
|
if javaVersionStr != "" {
|
||||||
|
if parsed, err := strconv.Atoi(javaVersionStr); err == nil {
|
||||||
|
javaVersion = parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update modpack in database
|
||||||
_, err = h.db.Pool().Exec(r.Context(),
|
_, err = h.db.Pool().Exec(r.Context(),
|
||||||
`UPDATE modpacks SET name=$1, slug=$2, minecraft_version=$3, java_version=$4, server_ip=$5
|
`UPDATE modpacks SET name=$1, slug=$2, minecraft_version=$3, java_version=$4, server_ip=$5
|
||||||
WHERE id=$6`,
|
WHERE id=$6`,
|
||||||
req.Name, req.Slug, req.MinecraftVersion, req.JavaVersion, req.ServerIP, id,
|
name, slug, minecraftVersion, javaVersion, serverIP, id,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.WriteError(w, http.StatusInternalServerError, "Update failed")
|
utils.WriteError(w, http.StatusInternalServerError, "Update failed")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle file uploads if any
|
||||||
|
files := r.MultipartForm.File["files"]
|
||||||
|
if len(files) > 0 {
|
||||||
|
// Create instance directory for this modpack
|
||||||
|
uploadDir := filepath.Join(h.cfg.CASDir, "..", "instances", slug)
|
||||||
|
os.MkdirAll(uploadDir, 0o755)
|
||||||
|
|
||||||
|
var uploaded []string
|
||||||
|
for _, fh := range files {
|
||||||
|
src, err := fh.Open()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := io.ReadAll(src)
|
||||||
|
src.Close()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasSuffix(strings.ToLower(fh.Filename), ".zip") {
|
||||||
|
extracted, err := h.extractZip(data, uploadDir)
|
||||||
|
if err == nil {
|
||||||
|
uploaded = append(uploaded, extracted...)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hash := utils.SHA1Bytes(data)
|
||||||
|
destDir := filepath.Join(h.cfg.CASDir, hash[:2])
|
||||||
|
os.MkdirAll(destDir, 0o755)
|
||||||
|
os.WriteFile(filepath.Join(destDir, hash), data, 0o644)
|
||||||
|
|
||||||
|
h.db.Pool().Exec(r.Context(),
|
||||||
|
`INSERT INTO global_files (sha1, size_bytes, file_name) VALUES ($1, $2, $3)
|
||||||
|
ON CONFLICT (sha1) DO NOTHING`,
|
||||||
|
hash, int64(len(data)), fh.Filename,
|
||||||
|
)
|
||||||
|
|
||||||
|
instPath := filepath.Join(uploadDir, "mods", fh.Filename)
|
||||||
|
os.MkdirAll(filepath.Dir(instPath), 0o755)
|
||||||
|
os.WriteFile(instPath, data, 0o644)
|
||||||
|
|
||||||
|
uploaded = append(uploaded, fh.Filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Optionally generate manifest after upload
|
||||||
|
// For now, just return success with upload info
|
||||||
|
utils.WriteJSON(w, http.StatusOK, map[string]interface{}{
|
||||||
|
"status": "updated",
|
||||||
|
"uploaded": uploaded,
|
||||||
|
"count": len(uploaded),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// No files uploaded, just updated modpack
|
||||||
utils.WriteJSON(w, http.StatusOK, map[string]string{"status": "updated"})
|
utils.WriteJSON(w, http.StatusOK, map[string]string{"status": "updated"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,56 +29,46 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Upload Form -->
|
<!-- Unified Create & Upload Form -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Загрузка нового модпака</h2>
|
<h2>Создание и загрузка модпака</h2>
|
||||||
<form id="modpack-form">
|
<form id="modpack-form" enctype="multipart/form-data">
|
||||||
<div class="grid-2">
|
<div class="grid-2">
|
||||||
|
<div>
|
||||||
|
<label for="name">Название модпака</label>
|
||||||
|
<input type="text" id="name" name="name" required placeholder="Например: HiTech Server">
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="slug">Slug (уникальный идентификатор)</label>
|
<label for="slug">Slug (уникальный идентификатор)</label>
|
||||||
<input type="text" id="slug" name="slug" required>
|
<input type="text" id="slug" name="slug" required>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label for="name">Название</label>
|
|
||||||
<input type="text" id="name" name="name" required>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<label for="minecraft_version">Версия Minecraft</label>
|
<label for="minecraft_version">Версия Minecraft</label>
|
||||||
<input type="text" id="minecraft_version" name="minecraft_version" required>
|
<input type="text" id="minecraft_version" name="minecraft_version" required placeholder="Например: 1.21">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="java_version">Версия Java</label>
|
<label for="java_version">Версия Java</label>
|
||||||
<input type="number" id="java_version" name="java_version" min="8" required>
|
<input type="number" id="java_version" name="java_version" min="8" required value="21">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="server_ip">IP сервера</label>
|
<label for="server_ip">IP сервера</label>
|
||||||
<input type="text" id="server_ip" name="server_ip" placeholder="Например: play.mrixs.me">
|
<input type="text" id="server_ip" name="server_ip" placeholder="Например: play.mrixs.me:25565">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn">Создать модпак</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<hr>
|
<div class="form-section">
|
||||||
|
<label for="files">Файлы модпака (ZIP архив или отдельные файлы)</label>
|
||||||
<h3>Загрузка файлов для модпака</h3>
|
|
||||||
<form id="upload-form" enctype="multipart/form-data">
|
|
||||||
<div>
|
|
||||||
<label for="upload-slug">Slug модпака</label>
|
|
||||||
<input type="text" id="upload-slug" name="slug" list="modpack-slugs" required>
|
|
||||||
<datalist id="modpack-slugs"></datalist>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="files">Файлы (перетащите или выберите)</label>
|
|
||||||
<input type="file" id="files" name="files" multiple>
|
<input type="file" id="files" name="files" multiple>
|
||||||
<p>Поддерживаются архивы .zip ( будут распакованы) и отдельные файлы.</p>
|
<p>Поддерживаются архивы .zip ( будут распакованы автоматически) и отдельные файлы.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="upload-progress" style="display: none; margin-top: 1rem;">
|
<div id="upload-progress" style="display: none; margin-top: 1rem;">
|
||||||
<div>Прогресс: <span id="upload-percent">0%</span></div>
|
<div>Прогресс загрузки: <span id="upload-percent">0%</span></div>
|
||||||
<progress id="upload-bar" value="0" max="100"></progress>
|
<progress id="upload-bar" value="0" max="100"></progress>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn" id="upload-btn">Загрузить файлы</button>
|
|
||||||
|
<button type="submit" class="btn">Создать модпак и загрузить файлы</button>
|
||||||
</form>
|
</form>
|
||||||
<div id="upload-result" style="margin-top: 1rem;"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -105,6 +95,25 @@ function getToken() {
|
|||||||
return localStorage.getItem('token');
|
return localStorage.getItem('token');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate slug from name (transliterate and clean)
|
||||||
|
function generateSlugFromName(name) {
|
||||||
|
if (!name) return '';
|
||||||
|
|
||||||
|
// Convert to lowercase and trim
|
||||||
|
let slug = name.toLowerCase().trim();
|
||||||
|
|
||||||
|
// Replace non-alphanumeric characters with hyphens
|
||||||
|
slug = slug.replace(/[^a-z0-9]+/g, '-');
|
||||||
|
|
||||||
|
// Remove leading/trailing hyphens
|
||||||
|
slug = slug.replace(/^-+|-+$/g, '');
|
||||||
|
|
||||||
|
// Limit length
|
||||||
|
slug = slug.substring(0, 32);
|
||||||
|
|
||||||
|
return slug || 'modpack';
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch and display modpacks
|
// Fetch and display modpacks
|
||||||
async function loadModpacks() {
|
async function loadModpacks() {
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
@@ -126,11 +135,9 @@ async function loadModpacks() {
|
|||||||
|
|
||||||
const modpacks = await response.json();
|
const modpacks = await response.json();
|
||||||
const tbody = document.getElementById('modpacks-tbody');
|
const tbody = document.getElementById('modpacks-tbody');
|
||||||
const datalist = document.getElementById('modpack-slugs');
|
|
||||||
|
|
||||||
// Clear existing
|
// Clear existing
|
||||||
tbody.innerHTML = '';
|
tbody.innerHTML = '';
|
||||||
datalist.innerHTML = '';
|
|
||||||
|
|
||||||
modpacks.forEach(mp => {
|
modpacks.forEach(mp => {
|
||||||
const tr = document.createElement('tr');
|
const tr = document.createElement('tr');
|
||||||
@@ -147,11 +154,6 @@ async function loadModpacks() {
|
|||||||
</td>
|
</td>
|
||||||
`;
|
`;
|
||||||
tbody.appendChild(tr);
|
tbody.appendChild(tr);
|
||||||
|
|
||||||
// Add to datalist for upload form
|
|
||||||
const option = document.createElement('option');
|
|
||||||
option.value = mp.slug;
|
|
||||||
datalist.appendChild(option);
|
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showAlert('Ошибка загрузки модпаков: ' + err.message, null);
|
showAlert('Ошибка загрузки модпаков: ' + err.message, null);
|
||||||
@@ -159,56 +161,40 @@ async function loadModpacks() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle modpack creation
|
// Handle unified form submission (create modpack + upload files)
|
||||||
document.getElementById('modpack-form').addEventListener('submit', async (e) => {
|
document.getElementById('modpack-form').addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
|
|
||||||
const formData = new FormData(e.target);
|
const form = e.target;
|
||||||
const data = {
|
const formData = new FormData(form);
|
||||||
slug: formData.get('slug'),
|
|
||||||
name: formData.get('name'),
|
|
||||||
minecraft_version: formData.get('minecraft_version'),
|
|
||||||
java_version: parseInt(formData.get('java_version')),
|
|
||||||
server_ip: formData.get('server_ip')
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
// Get values
|
||||||
const response = await fetch('/api/admin/modpacks', {
|
const name = formData.get('name');
|
||||||
method: 'POST',
|
let slug = formData.get('slug');
|
||||||
headers: {
|
const minecraftVersion = formData.get('minecraft_version');
|
||||||
'Content-Type': 'application/json',
|
const javaVersionStr = formData.get('java_version');
|
||||||
'Authorization': 'Bearer ' + token
|
const serverIP = formData.get('server_ip');
|
||||||
},
|
|
||||||
body: JSON.stringify(data)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
showAlert(null, 'Модпак успешно создан');
|
|
||||||
e.target.reset();
|
|
||||||
loadModpacks(); // Refresh list
|
|
||||||
} else {
|
|
||||||
const error = await response.json();
|
|
||||||
showAlert(error.errorMessage || 'Ошибка создания модпака', null);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
showAlert('Ошибка сети: ' + err.message, null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle file upload
|
|
||||||
document.getElementById('upload-form').addEventListener('submit', async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const token = getToken();
|
|
||||||
if (!token) return;
|
|
||||||
|
|
||||||
const formData = new FormData(e.target);
|
|
||||||
const slug = formData.get('slug');
|
|
||||||
const files = document.getElementById('files').files;
|
const files = document.getElementById('files').files;
|
||||||
|
|
||||||
if (files.length === 0) {
|
// Auto-generate slug from name if slug field is empty or matches name-based prediction
|
||||||
showAlert('Пожалуйста, выберите файлы для загрузки', null);
|
const predictedSlug = generateSlugFromName(name);
|
||||||
|
if (!slug || slug === predictedSlug) {
|
||||||
|
slug = predictedSlug;
|
||||||
|
document.getElementById('slug').value = slug;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if (!slug || !name || !minecraftVersion) {
|
||||||
|
showAlert('Slug, название и версия Minecraft обязательны', null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse java_version
|
||||||
|
const javaVersion = javaVersionStr ? parseInt(javaVersionStr) : 8;
|
||||||
|
if (isNaN(javaVersion)) {
|
||||||
|
showAlert('Неверная версия Java', null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,8 +207,8 @@ document.getElementById('upload-form').addEventListener('submit', async (e) => {
|
|||||||
progressBar.value = 0;
|
progressBar.value = 0;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// We'll use the existing upload endpoint: POST /api/admin/modpacks/{slug}/upload
|
// Send request to create modpack with files
|
||||||
const response = await fetch(`/api/admin/modpacks/${slug}/upload`, {
|
const response = await fetch('/api/admin/modpacks', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': 'Bearer ' + token
|
'Authorization': 'Bearer ' + token
|
||||||
@@ -232,12 +218,19 @@ document.getElementById('upload-form').addEventListener('submit', async (e) => {
|
|||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
showAlert(null, `Загружено файлов: ${result.count}`);
|
let message = 'Модпак успешно создан';
|
||||||
document.getElementById('upload-form').reset();
|
if result.uploaded && result.uploaded.length > 0 {
|
||||||
progressDiv.style.display = 'none';
|
message += ` и загружено ${result.uploaded.length} файлов`;
|
||||||
|
} else if result.count && result.count > 0 {
|
||||||
|
message += ` и загружено ${result.count} файлов`;
|
||||||
|
}
|
||||||
|
showAlert(null, message);
|
||||||
|
form.reset();
|
||||||
|
document.getElementById('slug').value = ''; // Clear slug for next time
|
||||||
|
loadModpacks(); // Refresh list
|
||||||
} else {
|
} else {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
showAlert(error.errorMessage || 'Ошибка загрузки файлов', null);
|
showAlert(error.errorMessage || 'Ошибка создания модпака', null);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showAlert('Ошибка сети: ' + err.message, null);
|
showAlert('Ошибка сети: ' + err.message, null);
|
||||||
@@ -246,15 +239,22 @@ document.getElementById('upload-form').addEventListener('submit', async (e) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Simulate upload progress (since we don't have progress events from fetch with FormData easily)
|
// Handle file input change for auto-generating slug
|
||||||
// For a real progress bar, we'd need to use XMLHttpSocket or a custom backend endpoint.
|
document.getElementById('name').addEventListener('input', function(e) {
|
||||||
// For now, we'll just show indeterminate or fake progress.
|
const name = e.target.value;
|
||||||
// We'll update this if we decide to implement proper progress later.
|
const predictedSlug = generateSlugFromName(name);
|
||||||
|
|
||||||
// Placeholder for edit function (to be implemented if we add edit endpoint)
|
// Only auto-update slug if user hasn't manually edited it
|
||||||
function loadModpackForEdit(slug) {
|
const slugInput = document.getElementById('slug');
|
||||||
alert('Редактирование пока не реализовано. Используйте API напрямую.');
|
if (!slugInput.dataset.manualEdit) {
|
||||||
}
|
slugInput.value = predictedSlug;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mark slug as manually edited when user changes it
|
||||||
|
document.getElementById('slug').addEventListener('input', function(e) {
|
||||||
|
e.target.dataset.manualEdit = 'true';
|
||||||
|
});
|
||||||
|
|
||||||
// Initialize
|
// Initialize
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user