diff --git a/index.c b/index.c index 5485e31..7578505 100644 --- a/index.c +++ b/index.c @@ -1,4 +1,5 @@ // /home/romkazvo/www/cgi-bin/index.c +#define _GNU_SOURCE #include #include #include @@ -10,6 +11,13 @@ #include #include +// === ПРОТОТИПЫ ФУНКЦИЙ === +void url_encode(const char *src, char *dst, size_t dst_size); +void url_decode_enhanced(const char *src, char *dst, size_t dst_size); +void format_size(long long size, char* buffer); +void format_date(time_t mtime, char* buffer, size_t size); +void html_escape(const char *src, char *dst, size_t dst_size); + // === КОНФИГУРАЦИЯ === typedef struct { @@ -30,6 +38,51 @@ typedef struct { config_t g_config; +// === ПАРАМЕТРЫ ЗАПРОСА (сортировка, поиск) === +typedef struct { + char sort_by[16]; // "name", "size", "date" + int sort_order; // 1 = asc, -1 = desc + char search[256]; + int recursive; // 1 = рекурсивный поиск, 0 = только текущая папка +} query_params_t; + +query_params_t g_params; + +// === ПОИСК БЕЗ УЧЁТА РЕГИСТРА (поддерживает UTF-8) === +char* my_strcasestr(const char *haystack, const char *needle) { + if (!haystack || !needle || *needle == '\0') return (char*)haystack; + + const char *h = haystack; + const char *n = needle; + size_t needle_len = strlen(needle); + + while (*h) { + if (strncasecmp(h, n, needle_len) == 0) { + return (char*)h; + } + h++; + } + return NULL; +} + +// === HTML ЭКРАНИРОВАНИЕ (защита от XSS) === +void html_escape(const char *src, char *dst, size_t dst_size) { + if (!src || !dst || dst_size == 0) return; + char *p = dst; + size_t i = 0; + while (*src && i < dst_size - 6) { + switch (*src) { + case '<': strcpy(p, "<"); p += 4; i += 4; break; + case '>': strcpy(p, ">"); p += 4; i += 4; break; + case '"': strcpy(p, """); p += 6; i += 6; break; + case '&': strcpy(p, "&"); p += 5; i += 5; break; + default: *p++ = *src; i++; break; + } + src++; + } + *p = '\0'; +} + // Простой парсер INI (без зависимостей) int load_config(const char *path, config_t *cfg) { FILE *f = fopen(path, "r"); @@ -147,7 +200,7 @@ int is_string_in_list(const char *str, const char *list) { return 0; } -// === ОСТАЛЬНОЙ КОД (без изменений, только теперь используем g_config вместо #define) === +// === ОСТАЛЬНОЙ КОД === typedef struct { char name[256]; @@ -219,7 +272,10 @@ int check_folder_password(const char *folder_name, const char *password) { *colon = '\0'; char *folder = line; char *pass = colon + 1; - if (strcmp(folder, folder_name) == 0) { + // Проверяем как полный путь, так и имя папки + const char *last_slash = strrchr(folder_name, '/'); + const char *base_name = last_slash ? last_slash + 1 : folder_name; + if (strcmp(folder, folder_name) == 0 || strcmp(folder, base_name) == 0) { fclose(f); return strcmp(pass, password) == 0; } @@ -228,8 +284,8 @@ int check_folder_password(const char *folder_name, const char *password) { return 1; } -int is_folder_protected(const char *folder_name) { - if (!folder_name || folder_name[0] == '\0') return 0; +int is_folder_protected(const char *folder_path) { + if (!folder_path || folder_path[0] == '\0') return 0; FILE *f = fopen(g_config.passwd_file, "r"); if (!f) return 0; @@ -240,7 +296,11 @@ int is_folder_protected(const char *folder_name) { char *colon = strchr(line, ':'); if (!colon) continue; *colon = '\0'; - if (strcmp(line, folder_name) == 0) { + // Получаем имя папки (последний сегмент пути) + const char *last_slash = strrchr(folder_path, '/'); + const char *folder_name = last_slash ? last_slash + 1 : folder_path; + // Проверяем как полный путь, так и имя папки + if (strcmp(line, folder_path) == 0 || strcmp(line, folder_name) == 0) { fclose(f); return 1; } @@ -316,12 +376,105 @@ int is_previewable(const char* filename) { is_string_in_list(ext, g_config.preview_pdf)); } -int compare_entries(const void *a, const void *b) { +// === РЕКУРСИВНЫЙ ПОИСК (игнорирует защищённые папки) === +void search_recursive(const char *base_path, const char *display_path, const char *search_term, entry_t *results, int *count, int max_results) { + DIR *dir = opendir(base_path); + if (!dir) return; + + struct dirent *entry; + struct stat file_stat; + char full_path[1024]; + char display_subpath[1024]; + + while ((entry = readdir(dir)) != NULL && *count < max_results) { + if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue; + if (entry->d_name[0] == '.') continue; + + // Проверяем скрытые папки/файлы из конфига + int hidden = 0; + for (int i = 0; i < g_config.hidden_dirs_count; i++) { + if (strcmp(entry->d_name, g_config.hidden_dirs[i]) == 0) { hidden = 1; break; } + } + for (int i = 0; i < g_config.hidden_files_count; i++) { + if (strcmp(entry->d_name, g_config.hidden_files[i]) == 0) { hidden = 1; break; } + } + if (hidden) continue; + + snprintf(full_path, sizeof(full_path), "%s/%s", base_path, entry->d_name); + if (display_path[0] == '\0') { + snprintf(display_subpath, sizeof(display_subpath), "%s", entry->d_name); + } else { + snprintf(display_subpath, sizeof(display_subpath), "%s/%s", display_path, entry->d_name); + } + + // Если папка защищена — не заходим и не показываем + if (is_folder_protected(display_subpath) || is_folder_protected(entry->d_name)) { + continue; + } + + if (stat(full_path, &file_stat) == 0) { + if (my_strcasestr(entry->d_name, search_term) != NULL) { + entry_t *result = &results[*count]; + strncpy(result->name, entry->d_name, sizeof(result->name) - 1); + result->name[sizeof(result->name) - 1] = '\0'; + + if (S_ISDIR(file_stat.st_mode)) { + result->is_dir = 1; + result->size = 0; + result->dir_size = get_dir_size(full_path); + result->file_count = count_files_in_dir(full_path); + result->mtime = file_stat.st_mtime; + strcpy(result->icon, "📁"); + result->is_protected = 0; + + url_encode(display_subpath, result->encoded_path, sizeof(result->encoded_path)); + strncpy(result->file_url, display_subpath, sizeof(result->file_url) - 1); + (*count)++; + } else if (S_ISREG(file_stat.st_mode)) { + result->is_dir = 0; + result->size = file_stat.st_size; + result->dir_size = 0; + result->file_count = 0; + result->mtime = file_stat.st_mtime; + strcpy(result->icon, get_file_icon(entry->d_name)); + result->is_protected = 0; + + url_encode(display_subpath, result->encoded_path, sizeof(result->encoded_path)); + strncpy(result->file_url, display_subpath, sizeof(result->file_url) - 1); + (*count)++; + } + } + + if (S_ISDIR(file_stat.st_mode)) { + search_recursive(full_path, display_subpath, search_term, results, count, max_results); + } + } + } + closedir(dir); +} + +// === СОРТИРОВКА === + +int compare_entries_sorted(const void *a, const void *b) { const entry_t *entryA = (const entry_t *)a; const entry_t *entryB = (const entry_t *)b; + + // Папки всегда сверху if (entryA->is_dir && !entryB->is_dir) return -1; if (!entryA->is_dir && entryB->is_dir) return 1; - return strcasecmp(entryA->name, entryB->name); + + int result = 0; + if (strcmp(g_params.sort_by, "size") == 0) { + long long sizeA = entryA->is_dir ? entryA->dir_size : entryA->size; + long long sizeB = entryB->is_dir ? entryB->dir_size : entryB->size; + result = (sizeA > sizeB) - (sizeA < sizeB); + } else if (strcmp(g_params.sort_by, "date") == 0) { + result = (entryA->mtime > entryB->mtime) - (entryA->mtime < entryB->mtime); + } else { + result = strcasecmp(entryA->name, entryB->name); + } + + return result * g_params.sort_order; } void format_size(long long size, char* buffer) { @@ -360,18 +513,29 @@ void url_decode_enhanced(const char *src, char *dst, size_t dst_size) { size_t decoded_len = 0; while (*src && decoded_len < dst_size - 1) { if (*src == '%') { + // Проверяем, что есть ещё минимум 2 символа if (src[1] && src[2] && isxdigit(src[1]) && isxdigit(src[2])) { char hex[3] = {src[1], src[2], '\0'}; unsigned char c = (unsigned char)strtol(hex, NULL, 16); - if (c >= 0x80) { *p++ = c; decoded_len++; } - else if (c >= 0x20 || c == 0x0A || c == 0x0D) { *p++ = c; decoded_len++; } - else { *p++ = '_'; decoded_len++; } + if (c >= 0x80 || c >= 0x20 || c == 0x0A || c == 0x0D) { + *p++ = c; + decoded_len++; + } else { + *p++ = '_'; + decoded_len++; + } src += 3; - } else { *p++ = *src++; decoded_len++; } + } else { + // Невалидный % — просто пропускаем + src++; + } } else if (*src == '+') { - *p++ = ' '; decoded_len++; src++; + *p++ = ' '; + decoded_len++; + src++; } else { - *p++ = *src++; decoded_len++; + *p++ = *src++; + decoded_len++; } } *p = '\0'; @@ -391,7 +555,9 @@ void print_breadcrumb(buffer_t *buf, const char *display_path) { strcat(temp_path, token); url_encode(temp_path, encoded, sizeof(encoded)); char link[4096]; - snprintf(link, sizeof(link), "%s", encoded, token); + char safe_token[512]; + html_escape(token, safe_token, sizeof(safe_token)); + snprintf(link, sizeof(link), "%s", encoded, safe_token); buffer_append(buf, link); token = strtok(NULL, "/"); } @@ -406,7 +572,9 @@ void print_password_form(buffer_t *buf, const char *folder_name) { buffer_append(buf, "
🔒
\n"); buffer_append(buf, "

Папка защищена паролем

\n"); buffer_append(buf, "

Введите пароль для доступа к папке "); - buffer_append(buf, folder_name); + char safe_name[512]; + html_escape(folder_name, safe_name, sizeof(safe_name)); + buffer_append(buf, safe_name); buffer_append(buf, "

\n"); buffer_append(buf, "
\n"); buffer_append(buf, " \n"); - buffer_append(buf, "
📁
\n"); - buffer_append(buf, "

Ошибка открытия директории

\n"); - char error_msg[512]; - snprintf(error_msg, sizeof(error_msg), "

Путь: %s

\n", base_path); - buffer_append(buf, error_msg); - snprintf(error_msg, sizeof(error_msg), "

Ошибка: %s

\n", strerror(errno)); - buffer_append(buf, error_msg); - buffer_append(buf, "

← Вернуться на главную

\n"); + buffer_append(buf, "
🔍
\n"); + buffer_append(buf, "

Ничего не найдено

\n"); + buffer_append(buf, "

По запросу \""); + char safe_search[512]; + html_escape(g_params.search, safe_search, sizeof(safe_search)); + buffer_append(buf, safe_search); + buffer_append(buf, "\" ничего не найдено

\n"); + buffer_append(buf, "\n"); + free(entries); + return; + } + + entry_count = result_count; + is_recursive_search = 1; + + for (int i = 0; i < entry_count; i++) { + if (entries[i].is_dir) { + dir_count++; + total_size += entries[i].dir_size; + } else { + file_count++; + total_size += entries[i].size; + } + } + } else { + // === ОБЫЧНЫЙ ОБХОД ТЕКУЩЕЙ ПАПКИ === + dir = opendir(base_path); + if (!dir) { + char alt_path[1024]; + snprintf(alt_path, sizeof(alt_path), "%s", g_config.base_path); + if (display_path[0]) { + char *encoded = strdup(display_path); + url_decode_enhanced(encoded, alt_path + strlen(alt_path), sizeof(alt_path) - strlen(alt_path)); + free(encoded); + } + dir = opendir(alt_path); + if (!dir) { + buffer_append(buf, "
\n"); + buffer_append(buf, "
📁
\n"); + buffer_append(buf, "

Ошибка открытия директории

\n"); + char error_msg[512]; + snprintf(error_msg, sizeof(error_msg), "

Путь: %s

\n", base_path); + buffer_append(buf, error_msg); + snprintf(error_msg, sizeof(error_msg), "

Ошибка: %s

\n", strerror(errno)); + buffer_append(buf, error_msg); + buffer_append(buf, "

← Вернуться на главную

\n"); + buffer_append(buf, "
\n"); + free(entries); + return; + } + } + + while ((entry = readdir(dir)) != NULL && entry_count < g_config.max_entries) { + if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue; + + int hidden = 0; + for (int i = 0; i < g_config.hidden_dirs_count; i++) { + if (strcmp(entry->d_name, g_config.hidden_dirs[i]) == 0) { hidden = 1; break; } + } + for (int i = 0; i < g_config.hidden_files_count; i++) { + if (strcmp(entry->d_name, g_config.hidden_files[i]) == 0) { hidden = 1; break; } + } + if (entry->d_name[0] == '.') hidden = 1; + if (hidden) continue; + + snprintf(full_path, sizeof(full_path), "%s/%s", base_path, entry->d_name); + if (stat(full_path, &file_stat) == 0) { + strncpy(entries[entry_count].name, entry->d_name, sizeof(entries[0].name) - 1); + entries[entry_count].name[sizeof(entries[0].name) - 1] = '\0'; + + if (S_ISDIR(file_stat.st_mode)) { + entries[entry_count].is_dir = 1; + entries[entry_count].size = 0; + entries[entry_count].mtime = file_stat.st_mtime; + entries[entry_count].file_count = count_files_in_dir(full_path); + entries[entry_count].dir_size = get_dir_size(full_path); + total_size += entries[entry_count].dir_size; + + if (is_folder_protected(entry->d_name)) { + strcpy(entries[entry_count].icon, "🔒"); + entries[entry_count].is_protected = 1; + } else { + strcpy(entries[entry_count].icon, "📁"); + entries[entry_count].is_protected = 0; + } + + char new_path[2048]; + snprintf(new_path, sizeof(new_path), "%s%s%s", display_path, display_path[0] ? "/" : "", entries[entry_count].name); + url_encode(new_path, entries[entry_count].encoded_path, sizeof(entries[0].encoded_path)); + dir_count++; + } else if (S_ISREG(file_stat.st_mode)) { + entries[entry_count].is_dir = 0; + entries[entry_count].size = file_stat.st_size; + entries[entry_count].mtime = file_stat.st_mtime; + entries[entry_count].file_count = 0; + entries[entry_count].dir_size = 0; + strcpy(entries[entry_count].icon, get_file_icon(entry->d_name)); + entries[entry_count].is_protected = 0; + + char encoded_filename[1024]; + url_encode(entry->d_name, encoded_filename, sizeof(encoded_filename)); + snprintf(entries[entry_count].file_url, sizeof(entries[0].file_url), "%s%s%s", display_path, display_path[0] ? "/" : "", encoded_filename); + + total_size += file_stat.st_size; + file_count++; + } else { + continue; + } + entry_count++; + } + } + closedir(dir); + + if (entry_count == 0) { + buffer_append(buf, "
\n"); + buffer_append(buf, "
📄
\n"); + buffer_append(buf, "

Здесь пусто

\n"); + buffer_append(buf, "

В этой директории нет файлов или папок

\n"); buffer_append(buf, "
\n"); free(entries); return; } } - while ((entry = readdir(dir)) != NULL && entry_count < g_config.max_entries) { - if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue; - - int hidden = 0; - for (int i = 0; i < g_config.hidden_dirs_count; i++) { - if (strcmp(entry->d_name, g_config.hidden_dirs[i]) == 0) { hidden = 1; break; } - } - for (int i = 0; i < g_config.hidden_files_count; i++) { - if (strcmp(entry->d_name, g_config.hidden_files[i]) == 0) { hidden = 1; break; } - } - if (entry->d_name[0] == '.') hidden = 1; - if (hidden) continue; - - snprintf(full_path, sizeof(full_path), "%s/%s", base_path, entry->d_name); - if (stat(full_path, &file_stat) == 0) { - strncpy(entries[entry_count].name, entry->d_name, sizeof(entries[0].name) - 1); - entries[entry_count].name[sizeof(entries[0].name) - 1] = '\0'; - - if (S_ISDIR(file_stat.st_mode)) { - entries[entry_count].is_dir = 1; - entries[entry_count].size = 0; - entries[entry_count].mtime = file_stat.st_mtime; - entries[entry_count].file_count = count_files_in_dir(full_path); - entries[entry_count].dir_size = get_dir_size(full_path); - total_size += entries[entry_count].dir_size; - - if (is_folder_protected(entry->d_name)) { - strcpy(entries[entry_count].icon, "🔒"); - entries[entry_count].is_protected = 1; - } else { - strcpy(entries[entry_count].icon, "📁"); - entries[entry_count].is_protected = 0; - } - - char new_path[2048]; - snprintf(new_path, sizeof(new_path), "%s%s%s", display_path, display_path[0] ? "/" : "", entries[entry_count].name); - url_encode(new_path, entries[entry_count].encoded_path, sizeof(entries[0].encoded_path)); - dir_count++; - } else if (S_ISREG(file_stat.st_mode)) { - entries[entry_count].is_dir = 0; - entries[entry_count].size = file_stat.st_size; - entries[entry_count].mtime = file_stat.st_mtime; - entries[entry_count].file_count = 0; - entries[entry_count].dir_size = 0; - strcpy(entries[entry_count].icon, get_file_icon(entry->d_name)); - entries[entry_count].is_protected = 0; - snprintf(entries[entry_count].file_url, sizeof(entries[0].file_url), "%s%s%s", display_path, display_path[0] ? "/" : "", entries[entry_count].name); - total_size += file_stat.st_size; - file_count++; - } else { - continue; - } - entry_count++; - } - } - closedir(dir); + qsort(entries, entry_count, sizeof(entry_t), compare_entries_sorted); - if (entry_count == 0) { - buffer_append(buf, "
\n"); - buffer_append(buf, "
📄
\n"); - buffer_append(buf, "

Здесь пусто

\n"); - buffer_append(buf, "

В этой директории нет файлов или папок

\n"); - buffer_append(buf, "
\n"); - free(entries); - return; + // === ТУЛБАР (поиск + сортировка) === + char encoded_path[1024]; + url_encode(display_path, encoded_path, sizeof(encoded_path)); + char safe_search[512]; + html_escape(g_params.search, safe_search, sizeof(safe_search)); + + buffer_append(buf, "
\n"); + buffer_append(buf, "
\n"); + buffer_append(buf, " \n"); + buffer_append(buf, " \n"); + buffer_append(buf, " \n"); + buffer_append(buf, " \n"); + buffer_append(buf, " \n"); + buffer_append(buf, "
\n"); + buffer_append(buf, "
\n"); + buffer_append(buf, " Сортировка:\n"); + buffer_append(buf, " 📝 Имя\n"); + buffer_append(buf, " 📊 Размер\n"); + buffer_append(buf, " 📅 Дата\n"); + buffer_append(buf, "
\n"); + buffer_append(buf, "
\n"); + + // === ВЫВОД РЕЗУЛЬТАТОВ === + if (is_recursive_search) { + buffer_append(buf, "
"); + buffer_append(buf, "🔍 Результаты поиска по всему сайту: "); + char tmp[32]; + snprintf(tmp, sizeof(tmp), "%d", entry_count); + buffer_append(buf, tmp); + buffer_append(buf, " файлов/папок найдено
\n"); } - qsort(entries, entry_count, sizeof(entry_t), compare_entries); buffer_append(buf, "\n"); - char total_size_str[32]; - format_size(total_size, total_size_str); - char stats[256]; - snprintf(stats, sizeof(stats), - "
\n Папки: %d | Файлы: %d | Общий размер: %s\n
\n", - dir_count, file_count, total_size_str); - buffer_append(buf, stats); + // Статистика + if (is_recursive_search) { + char stats[256]; + snprintf(stats, sizeof(stats), + "
\n Найдено: папок %d | файлов %d\n
\n", + dir_count, file_count); + buffer_append(buf, stats); + } else { + char total_size_str[32]; + format_size(total_size, total_size_str); + char stats[256]; + snprintf(stats, sizeof(stats), + "
\n Папки: %d | Файлы: %d | Общий размер: %s\n
\n", + dir_count, file_count, total_size_str); + buffer_append(buf, stats); + } free(entries); } @@ -724,19 +1050,23 @@ int main() { // Загружаем конфиг if (load_config("/home/romkazvo/www/cgi-bin/config.ini", &g_config) != 0) { - // Если конфиг не найден — используем дефолты set_default_config(&g_config); } - printf("Content-type: text/html; charset=utf-8\n\n"); - - char base_path[1024]; - strcpy(base_path, g_config.base_path); - char display_path[1024] = ""; - char safe_display_path[1024] = ""; + // === ПАРСИМ ПАРАМЕТРЫ ЗАПРОСА === + strcpy(g_params.sort_by, "name"); + g_params.sort_order = 1; + g_params.search[0] = '\0'; + g_params.recursive = 0; char *query_string = getenv("QUERY_STRING"); + char display_path[1024] = ""; + char safe_display_path[1024] = ""; + char base_path[1024]; + strcpy(base_path, g_config.base_path); + if (query_string) { + // === ПАРСИМ PATH === char *path_start = strstr(query_string, "path="); if (path_start) { path_start += 5; @@ -758,6 +1088,36 @@ int main() { } } + // === ПАРСИМ SORT === + char *sort_start = strstr(query_string, "sort="); + if (sort_start) { + sort_start += 5; + char *sort_end = strchr(sort_start, '&'); + int sort_len = sort_end ? sort_end - sort_start : strlen(sort_start); + if (sort_len > 0 && sort_len < sizeof(g_params.sort_by)) { + strncpy(g_params.sort_by, sort_start, sort_len); + g_params.sort_by[sort_len] = '\0'; + } + } + + // === ПАРСИМ SEARCH (отдельно от path!) === + char *search_start = strstr(query_string, "search="); + if (search_start) { + search_start += 7; + char *search_end = strchr(search_start, '&'); + int search_len = search_end ? search_end - search_start : strlen(search_start); + if (search_len > 0 && search_len < sizeof(g_params.search) - 1) { + char encoded_search[256]; + strncpy(encoded_search, search_start, search_len); + encoded_search[search_len] = '\0'; + url_decode_enhanced(encoded_search, g_params.search, sizeof(g_params.search)); + if (g_params.search[0] != '\0') { + g_params.recursive = 1; + } + } + } + + // === ОБРАБОТКА ПАРОЛЯ === if (strstr(query_string, "password=")) { char *error = strstr(query_string, "error=1"); if (!error) { @@ -781,19 +1141,22 @@ int main() { if (decoded_pass[0] == '\0') { char encoded_path[1024]; url_encode(display_path, encoded_path, sizeof(encoded_path)); - printf("Location: /cgi-bin/index.cgi?path=%s&error=1\n\n", encoded_path); + printf("Status: 302 Found\r\n"); + printf("Location: /cgi-bin/index.cgi?path=%s&error=1\r\n\r\n", encoded_path); return 0; } if (!check_folder_password(folder_name, decoded_pass)) { char encoded_path[1024]; url_encode(display_path, encoded_path, sizeof(encoded_path)); - printf("Location: /cgi-bin/index.cgi?path=%s&error=1\n\n", encoded_path); + printf("Status: 302 Found\r\n"); + printf("Location: /cgi-bin/index.cgi?path=%s&error=1\r\n\r\n", encoded_path); return 0; } } else { char encoded_path[1024]; url_encode(display_path, encoded_path, sizeof(encoded_path)); - printf("Location: /cgi-bin/index.cgi?path=%s&error=1\n\n", encoded_path); + printf("Status: 302 Found\r\n"); + printf("Location: /cgi-bin/index.cgi?path=%s&error=1\r\n\r\n", encoded_path); return 0; } } @@ -803,6 +1166,8 @@ int main() { } } + printf("Content-type: text/html; charset=utf-8\n\n"); + print_template(base_path, display_path); return 0; } diff --git a/style.css b/style.css index 0959e48..29a4367 100755 --- a/style.css +++ b/style.css @@ -174,11 +174,11 @@ ul { } li { - padding: 0.75rem 0.5rem; + padding: 1rem 0.5rem; border-bottom: 1px solid var(--border-light); display: flex; align-items: center; - flex-wrap: nowrap; /* Исправлено: теперь не переносятся */ + flex-wrap: nowrap; } li:last-child { border-bottom: none; } @@ -194,8 +194,8 @@ li:hover { justify-content: space-between; flex-grow: 1; min-width: 0; - gap: 0.75rem; - flex-wrap: nowrap; /* Запрещаем перенос */ + gap: 1rem; + flex-wrap: nowrap; } .file-link { @@ -222,11 +222,12 @@ li:hover { align-items: center; gap: 0.5rem; flex-shrink: 0; + flex-wrap: nowrap; } .file-meta { color: var(--text-muted); - font-size: 0.85rem; + font-size: 0.75rem; white-space: nowrap; text-align: right; font-variant-numeric: tabular-nums; @@ -240,6 +241,28 @@ li:hover { min-width: 70px; } +@media (max-width: 768px) { + .file-meta { + font-size: 0.7rem; + } + .file-meta.date { + min-width: 75px; + } + .file-meta.size { + min-width: 60px; + } +} + +@media (max-width: 480px) { + .file-meta.date { + display: none !important; + } + .file-meta.size { + font-size: 0.65rem; + min-width: 50px; + } +} + .file-icon { margin-right: 0.75rem; font-size: 1.3rem; @@ -324,8 +347,10 @@ li:hover { text-decoration: underline; } -/* Стили для кнопок */ -.preview-btn, .copy-btn { +/* ================================================ + ЕДИНЫЙ СТИЛЬ ДЛЯ ВСЕХ КНОПОК + ================================================ */ +.preview-btn, .copy-btn, .download-btn { background: transparent; color: var(--text-secondary); border: 1px solid var(--border-color); @@ -342,7 +367,7 @@ li:hover { flex-shrink: 0; } -.preview-btn:hover, .copy-btn:hover { +.preview-btn:hover, .copy-btn:hover, .download-btn:hover { background: var(--accent-blue); color: white; border-color: var(--accent-blue); @@ -350,7 +375,103 @@ li:hover { transform: scale(1.05); } -/* Стили для формы ввода пароля */ +/* ================================================ + ТУЛБАР (поиск + сортировка) + ================================================ */ +.toolbar { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 1rem; + padding: 0.75rem 0.5rem; + margin-bottom: 1rem; + background: var(--bg-primary); + border-radius: 8px; + border: 1px solid var(--border-color); +} + +.search-box { + flex: 1 1 200px; + min-width: 150px; +} + +.search-box form { + display: flex; + gap: 0.5rem; +} + +.search-box input[type="text"] { + flex: 1; + padding: 0.5rem 0.75rem; + border: 1px solid var(--border-color); + border-radius: 6px; + background: var(--bg-secondary); + color: var(--text-primary); + font-size: 0.9rem; + min-width: 100px; +} + +.search-box input[type="text"]:focus { + outline: none; + border-color: var(--accent-blue); +} + +.search-box button { + padding: 0.5rem 1rem; + background: var(--accent-blue); + color: white; + border: none; + border-radius: 6px; + cursor: pointer; + font-weight: 500; + white-space: nowrap; +} + +.search-box button:hover { + background: var(--accent-green); +} + +.sort-buttons { + display: flex; + align-items: center; + gap: 0.3rem; + flex-wrap: wrap; +} + +.sort-label { + font-size: 0.8rem; + color: var(--text-secondary); + font-weight: 500; + margin-right: 0.2rem; + white-space: nowrap; +} + +.sort-btn { + padding: 0.4rem 0.75rem; + background: var(--bg-secondary); + color: var(--text-secondary); + border: 1px solid var(--border-color); + border-radius: 6px; + text-decoration: none; + font-size: 0.8rem; + transition: all 0.2s ease; + white-space: nowrap; +} + +.sort-btn:hover { + background: var(--hover-bg); + border-color: var(--accent-blue); +} + +.sort-btn.active { + background: var(--accent-blue); + color: white; + border-color: var(--accent-blue); +} + +/* ================================================ + Стили для формы ввода пароля + ================================================ */ .password-form-container { display: flex; justify-content: center; @@ -457,7 +578,9 @@ li:hover { position: relative; } -/* Модальное окно предпросмотра */ +/* ================================================ + Модальное окно предпросмотра + ================================================ */ .preview-modal { display: none; position: fixed; @@ -575,7 +698,6 @@ li:hover { background: rgba(255,255,255,0.5); } -/* Уведомление */ #copyNotification { position: fixed; bottom: 80px; @@ -592,18 +714,10 @@ li:hover { opacity: 1; } -/* === АДАПТИВНОСТЬ === */ - +/* ================================================ + Адаптивность + ================================================ */ @media (max-width: 768px) { - .file-meta { - font-size: 0.75rem; - } - .file-meta.date { - min-width: 75px; - } - .file-meta.size { - min-width: 60px; - } .preview-content { margin: 2.5% auto; } @@ -616,39 +730,117 @@ li:hover { .preview-image-container { padding: 5px; } + + li { + padding: 0.5rem 0.25rem !important; + flex-wrap: nowrap !important; + } + .file-row { + flex-wrap: nowrap !important; + gap: 0.3rem !important; + } + .file-controls { + flex-wrap: nowrap !important; + gap: 0.15rem !important; + } + .file-meta.date { + min-width: 60px !important; + } + .file-meta.size { + min-width: 50px !important; + font-size: 0.65rem !important; + } + .toolbar { + flex-wrap: wrap !important; + gap: 0.5rem !important; + padding: 0.5rem !important; + } + .search-box { + flex: 1 1 100% !important; + min-width: 0 !important; + } + .sort-buttons { + flex-wrap: wrap !important; + justify-content: center !important; + } } @media (max-width: 480px) { - /* Скрываем дату на телефонах */ + .toolbar { + flex-direction: column !important; + align-items: stretch !important; + gap: 0.5rem !important; + padding: 0.5rem !important; + } + .search-box { + width: 100% !important; + min-width: 0 !important; + flex: 1 1 auto !important; + } + .search-box form { + flex-wrap: nowrap !important; + width: 100% !important; + } + .search-box input[type="text"] { + flex: 1 1 auto !important; + min-width: 60px !important; + font-size: 0.85rem !important; + padding: 0.5rem 0.6rem !important; + } + .search-box button { + flex: 0 0 auto !important; + font-size: 0.8rem !important; + padding: 0.4rem 0.7rem !important; + white-space: nowrap !important; + } + .sort-buttons { + display: flex !important; + flex-wrap: wrap !important; + justify-content: center !important; + gap: 0.3rem !important; + width: 100% !important; + } + .sort-label { + font-size: 0.65rem !important; + width: 100% !important; + text-align: center !important; + margin-right: 0 !important; + } + .sort-btn { + flex: 0 1 auto !important; + font-size: 0.65rem !important; + padding: 0.25rem 0.4rem !important; + white-space: nowrap !important; + } + .preview-btn, .copy-btn, .download-btn { + width: 26px !important; + height: 26px !important; + font-size: 0.65rem !important; + padding: 0 !important; + } .file-meta.date { display: none !important; } - /* Уменьшаем размер кнопок */ - .preview-btn, .copy-btn { - width: 28px; - height: 28px; - font-size: 0.75rem; - } .file-meta.size { - font-size: 0.7rem; - min-width: 50px; + font-size: 0.55rem !important; + min-width: 35px !important; } .file-name { - font-size: 0.85rem; + font-size: 0.75rem !important; } .file-icon { - font-size: 1.1rem; - width: 20px; - margin-right: 0.5rem; + font-size: 1rem !important; + width: 18px !important; + margin-right: 0.3rem !important; } .file-controls { - gap: 0.3rem; + gap: 0.1rem !important; } li { - padding: 0.5rem 0.25rem; + padding: 0.3rem 0.15rem !important; } .file-row { - gap: 0.4rem; + gap: 0.15rem !important; } .theme-toggle { top: 5px; @@ -711,15 +903,28 @@ li:hover { margin: 0; } .file-name { - font-size: 0.8rem; + font-size: 0.7rem !important; } .file-meta.size { - font-size: 0.65rem; - min-width: 40px; + font-size: 0.5rem !important; + min-width: 28px !important; } - .preview-btn, .copy-btn { - width: 24px; - height: 24px; - font-size: 0.7rem; + .preview-btn, .copy-btn, .download-btn { + width: 20px !important; + height: 20px !important; + font-size: 0.5rem !important; + } + .search-box input[type="text"] { + font-size: 0.75rem !important; + padding: 0.3rem 0.4rem !important; + min-width: 40px !important; + } + .search-box button { + font-size: 0.7rem !important; + padding: 0.3rem 0.5rem !important; + } + .sort-btn { + font-size: 0.6rem !important; + padding: 0.2rem 0.3rem !important; } } diff --git a/template.html b/template.html index 69c8db5..c067eb2 100644 --- a/template.html +++ b/template.html @@ -37,29 +37,36 @@ })(); @@ -213,7 +223,17 @@ return true; }; - // ========== НОВАЯ ФУНКЦИЯ КОПИРОВАНИЯ ========== + // === СКАЧИВАНИЕ ФАЙЛА === + window.downloadFile = function(path) { + const link = document.createElement('a'); + link.href = path; + link.download = path.split('/').pop(); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }; + + // === КОПИРОВАНИЕ ССЫЛКИ === window.copyLink = function(path) { const url = window.location.origin + path; if (navigator.clipboard && navigator.clipboard.writeText) { @@ -256,7 +276,6 @@ setTimeout(() => div.remove(), 400); }, 2500); } - // ============================================== window.scrollToTop = function() { window.scrollTo({