Files
dupe/client/src/routes/Trash.svelte
2025-12-14 13:30:26 +03:00

1135 lines
26 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script>
import { onMount, tick } from "svelte";
import { API, getAccessToken } from "../utils/api.js";
import { cleanFileName } from "../utils/filename.js";
import { refreshMovieCount } from "../stores/movieStore.js";
import { refreshTvShowCount } from "../stores/tvStore.js";
import { trashItems, fetchTrashItems, restoreItem, deleteItemPermanently } from "../stores/trashStore.js";
import {
activeSearchTerm,
setSearchScope,
clearSearch
} from "../stores/searchStore.js";
const FOLDER_ICON_PATH = "/folder.svg";
let selectedItems = new Set();
let allSelected = false;
let viewMode = "grid";
let activeMenu = null;
let menuPosition = { top: 0, left: 0 };
let searchTerm = "";
let hasSearch = false;
let isLoading = true;
const VIEW_KEY = "trashViewMode";
if (typeof window !== "undefined") {
const storedView = window.localStorage.getItem(VIEW_KEY);
if (storedView === "grid" || storedView === "list") {
viewMode = storedView;
}
}
$: searchTerm = $activeSearchTerm;
$: hasSearch = searchTerm.trim().length > 0;
function filterItemsBySearch(items, term) {
const query = term.trim().toLowerCase();
if (!query) return items;
return items.filter((item) => {
const labels = [
item.name,
item.displayName,
item.parentPath
]
.filter(Boolean)
.join(" ")
.toLowerCase();
return labels.includes(query);
});
}
$: {
const keys = filteredTrashItems.map((item) => item.trashName).filter(Boolean);
allSelected = keys.length > 0 && keys.every((key) => selectedItems.has(key));
}
$: filteredTrashItems = filterItemsBySearch($trashItems, searchTerm);
async function loadTrashItems() {
isLoading = true;
try {
await fetchTrashItems();
} catch (err) {
console.error("Çöp öğeleri yüklenemedi:", err);
} finally {
isLoading = false;
}
}
function formatSize(bytes) {
if (!bytes) return "0 MB";
if (bytes < 1e6) return (bytes / 1e3).toFixed(1) + " KB";
if (bytes < 1e9) return (bytes / 1e6).toFixed(1) + " MB";
return (bytes / 1e9).toFixed(2) + " GB";
}
function formatDateTime(timestamp) {
if (!timestamp) return "—";
const date = new Date(Number(timestamp));
if (Number.isNaN(date.getTime())) return "—";
return date.toLocaleString();
}
function buildThumbnailUrl(item) {
const token = getAccessToken();
const cacheBuster = `t=${Date.now()}`;
const authPart = token ? `token=${token}` : null;
const query = [authPart, cacheBuster].filter(Boolean).join("&");
return `${API}${item.thumbnail}?${query}`;
}
function toggleView() {
viewMode = viewMode === "grid" ? "list" : "grid";
if (typeof window !== "undefined") {
window.localStorage.setItem(VIEW_KEY, viewMode);
}
activeMenu = null;
}
function toggleSelection(item) {
if (!item?.trashName) return;
const next = new Set(selectedItems);
if (next.has(item.trashName)) next.delete(item.trashName);
else next.add(item.trashName);
selectedItems = next;
}
function selectAll() {
if (allSelected) {
selectedItems = new Set();
} else {
const keys = filteredTrashItems.map((item) => item.trashName).filter(Boolean);
selectedItems = new Set(keys);
}
}
function handleTrashClick(event) {
if (selectedItems.size === 0) return;
const card = event.target.closest(".media-card");
if (card) return;
selectedItems = new Set();
}
function toggleMenu(item, event) {
event.stopPropagation();
if (activeMenu?.trashName === item.trashName) {
activeMenu = null;
return;
}
activeMenu = item;
tick().then(() => {
const button = event.currentTarget;
const rect = button.getBoundingClientRect();
const menuWidth = 160;
const menuHeight = 140;
let top = rect.bottom + 4;
let left = rect.right - 8;
if (left + menuWidth > window.innerWidth) {
left = window.innerWidth - menuWidth - 12;
}
if (left < 0) {
left = 12;
}
if (top + menuHeight > window.innerHeight) {
top = rect.top - menuHeight - 4;
}
menuPosition = { top, left };
});
}
function closeMenu() {
activeMenu = null;
}
async function restoreItemFromMenu(item) {
if (!item?.trashName) return;
try {
const result = await restoreItem(item.trashName);
if (result.success) {
await Promise.all([refreshMovieCount(), refreshTvShowCount()]);
} else {
alert("Geri yükleme hatası: " + (result.error || "Bilinmeyen hata"));
}
} catch (err) {
console.error("Geri yükleme hatası:", err);
alert("Geri yükleme sırasında bir hata oluştu.");
}
closeMenu();
}
async function deleteItemFromMenu(item) {
if (!item?.trashName) return;
if (!confirm("Bu öğeyi kalıcı olarak silmek istediğinizden emin misiniz?")) {
closeMenu();
return;
}
try {
const result = await deleteItemPermanently(item.trashName);
if (!result.success) {
alert("Silme hatası: " + (result.error || "Bilinmeyen hata"));
}
} catch (err) {
console.error("Silme hatası:", err);
alert("Silme sırasında bir hata oluştu.");
}
closeMenu();
}
async function restoreSelectedItems() {
if (selectedItems.size === 0) return;
if (!confirm(`${selectedItems.size} öğeyi geri yüklemek istediğinizden emin misiniz?`))
return;
const ids = [...selectedItems];
let successCount = 0;
let errorCount = 0;
for (const id of ids) {
try {
const result = await restoreItem(id);
if (result.success) {
successCount++;
} else {
errorCount++;
}
} catch (err) {
console.error("Geri yükleme hatası:", err);
errorCount++;
}
}
await Promise.all([refreshMovieCount(), refreshTvShowCount()]);
if (errorCount > 0) {
alert(`${successCount} öğe geri yüklendi, ${errorCount} öğede hata oluştu.`);
}
selectedItems = new Set();
activeMenu = null;
}
async function deleteSelectedItems() {
if (selectedItems.size === 0) return;
if (!confirm(`${selectedItems.size} öğeyi kalıcı olarak silmek istediğinizden emin misiniz?`))
return;
const ids = [...selectedItems];
let successCount = 0;
let errorCount = 0;
for (const id of ids) {
try {
const result = await deleteItemPermanently(id);
if (result.success) {
successCount++;
} else {
errorCount++;
}
} catch (err) {
console.error("Silme hatası:", err);
errorCount++;
}
}
if (errorCount > 0) {
alert(`${successCount} öğe silindi, ${errorCount} öğede hata oluştu.`);
}
selectedItems = new Set();
activeMenu = null;
}
async function emptyTrash() {
if ($trashItems.length === 0) return;
if (!confirm("Tüm çöpü kalıcı olarak boşaltmak istediğinizden emin misiniz? Bu işlem geri alınamaz."))
return;
let successCount = 0;
let errorCount = 0;
for (const item of $trashItems) {
if (item.trashName) {
try {
const result = await deleteItemPermanently(item.trashName);
if (result.success) {
successCount++;
} else {
errorCount++;
}
} catch (err) {
console.error("Silme hatası:", err);
errorCount++;
}
}
}
if (errorCount > 0) {
alert(`${successCount} öğe silindi, ${errorCount} öğede hata oluştu.`);
}
}
onMount(async () => {
setSearchScope("trash");
await loadTrashItems();
function handleClickOutside(event) {
if (activeMenu && !event.target.closest(".media-card")) {
activeMenu = null;
}
}
window.addEventListener("click", handleClickOutside);
return () => {
window.removeEventListener("click", handleClickOutside);
};
});
</script>
<section class="files" on:click={handleTrashClick}>
<div class="files-header">
<div class="header-title">
<h2>Trash</h2>
</div>
<div class="header-actions">
{#if filteredTrashItems.length > 0 && selectedItems.size > 0}
<span class="selection-count">{selectedItems.size} öğe seçildi</span>
{/if}
{#if filteredTrashItems.length > 0 && selectedItems.size > 0}
<button
class="select-all-btn"
type="button"
on:click|stopPropagation={selectAll}
aria-label={allSelected ? "Seçimi temizle" : "Tümünü seç"}
>
<i class="fa-solid fa-square-check"></i>
</button>
{/if}
{#if selectedItems.size > 0}
<button
class="restore-btn"
type="button"
on:click|stopPropagation={restoreSelectedItems}
aria-label="Seçili öğeleri geri yükle"
>
<i class="fa-solid fa-rotate-left"></i>
</button>
<button
class="delete-btn"
type="button"
on:click|stopPropagation={deleteSelectedItems}
aria-label="Seçili öğeleri kalıcı olarak sil"
>
<i class="fa-solid fa-trash"></i>
</button>
{/if}
{#if $trashItems.length > 0}
<button
class="empty-trash-btn"
type="button"
on:click|stopPropagation={emptyTrash}
aria-label="Tüm çöpü boşalt"
>
<i class="fa-solid fa-broom"></i>
</button>
{/if}
<button
class="view-toggle"
class:list-active={viewMode === "list"}
type="button"
on:click|stopPropagation={toggleView}
aria-label={viewMode === "grid"
? "Liste görünümüne geç"
: "Izgara görünümüne geç"}
>
{#if viewMode === "grid"}
<i class="fa-solid fa-list"></i>
{:else}
<i class="fa-solid fa-border-all"></i>
{/if}
</button>
</div>
</div>
{#if isLoading}
<div class="loading">
<div class="loading-spinner"><i class="fa-solid fa-spinner fa-spin"></i></div>
<div>Çöp yükleniyor...</div>
</div>
{:else if filteredTrashItems.length === 0}
<div class="empty">
<div style="font-size:42px"><i class="fa-solid fa-trash"></i></div>
<div style="font-weight:700">
{#if hasSearch}
Aramanla eşleşen öğe bulunamadı
{:else}
Çöp boş
{/if}
</div>
</div>
{:else}
<div class="gallery" class:list-view={viewMode === "list"}>
{#each filteredTrashItems as item (item.trashName)}
<div
class="media-card"
class:folder-card={item.isDirectory}
class:list-view={viewMode === "list"}
class:is-selected={selectedItems.has(item.trashName)}
on:click={() => toggleSelection(item)}
>
{#if item.isDirectory}
<div class="folder-thumb">
<img src={FOLDER_ICON_PATH} alt={`${item.name} klasörü`} />
</div>
<div class="folder-info">
<div class="folder-name">{cleanFileName(item.displayName || item.name)}</div>
{#if item.parentPath}
<div class="folder-path">{item.parentPath}</div>
{/if}
<div class="folder-meta">Silinme: {formatDateTime(item.movedAt)}</div>
</div>
{:else}
{#if item.thumbnail}
<img
src={buildThumbnailUrl(item)}
alt={item.name}
class="thumb"
on:load={(e) => e.target.classList.add("loaded")}
/>
{:else}
<div class="thumb placeholder">
<i class="fa-regular fa-image"></i>
</div>
{/if}
<div class="info">
<div class="name">{cleanFileName(item.displayName || item.name)}</div>
{#if item.parentPath}
<div class="path">{item.parentPath}</div>
{/if}
<div class="size">{formatSize(item.size)}</div>
<div class="list-meta">
<div class="meta-line primary">
<span>Silinme: {formatDateTime(item.movedAt)}</span>
</div>
</div>
</div>
{/if}
<button
class="selection-toggle"
class:is-selected={selectedItems.has(item.trashName)}
type="button"
on:click|stopPropagation={() => toggleSelection(item)}
aria-label={selectedItems.has(item.trashName)
? "Seçimi kaldır"
: "Bu öğeyi seç"}
>
{#if selectedItems.has(item.trashName)}
<i class="fa-solid fa-circle-check"></i>
{:else}
<i class="fa-regular fa-circle"></i>
{/if}
</button>
<button
class="menu-toggle"
type="button"
on:click|stopPropagation={(e) => toggleMenu(item, e)}
aria-label="Menü"
>
<i class="fa-solid fa-ellipsis"></i>
</button>
</div>
{/each}
</div>
{/if}
</section>
{#if activeMenu}
<div
class="dropdown-menu-portal"
style="top: {menuPosition.top}px; left: {menuPosition.left}px;"
on:click|stopPropagation
>
<button
class="menu-item"
on:click|stopPropagation={() => restoreItemFromMenu(activeMenu)}
>
<i class="fa-solid fa-rotate-left"></i>
<span>Geri Yükle</span>
</button>
<div class="menu-divider"></div>
<button
class="menu-item delete"
on:click|stopPropagation={() => deleteItemFromMenu(activeMenu)}
>
<i class="fa-solid fa-trash"></i>
<span>Kalıcı Sil</span>
</button>
</div>
{/if}
<style>
:root {
--yellow: #ffc107;
--yellow-dark: #e0a800;
--green: #4caf50;
--green-dark: #388e3c;
--red: #f44336;
--red-dark: #d32f2f;
}
.files-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 18px;
gap: 12px;
}
.header-title {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 4px;
min-width: 0;
flex: 1;
}
.header-actions {
display: flex;
align-items: center;
gap: 10px;
}
.selection-count {
font-size: 13px;
color: #6a6a6a;
font-weight: 500;
}
.select-all-btn {
background: #2e2e2e;
border: none;
color: #f5f5f5;
width: 36px;
height: 36px;
border-radius: 8px;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
outline: none;
transition:
background 0.2s ease,
transform 0.2s ease;
}
.select-all-btn i {
font-size: 16px;
}
.restore-btn {
background: var(--green);
border: none;
color: white;
padding: 8px 12px;
border-radius: 6px;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 14px;
transition: all 0.2s ease;
}
.restore-btn:hover {
background: var(--green-dark);
transform: scale(1.05);
}
.delete-btn {
background: var(--red);
border: none;
color: white;
padding: 8px 12px;
border-radius: 6px;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 14px;
transition: all 0.2s ease;
}
.delete-btn:hover {
background: var(--red-dark);
transform: scale(1.05);
}
.empty-trash-btn {
background: transparent;
border: 1px solid #ddd;
color: #666;
padding: 8px 12px;
border-radius: 6px;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 14px;
transition: all 0.2s ease;
}
.empty-trash-btn:hover {
background: var(--red);
border-color: var(--red-dark);
color: white;
transform: scale(1.05);
}
.view-toggle {
background: transparent;
border: 1px solid #ddd;
color: #666;
padding: 10px 14px;
border-radius: 6px;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
height: 36px;
width: 36px;
transition: all 0.2s ease;
font-size: 14px;
}
.view-toggle:hover {
background: var(--yellow);
border-color: var(--yellow-dark);
color: #222;
transform: scale(1.05);
}
.view-toggle:active {
transform: scale(0.95);
}
.view-toggle.list-active {
background: var(--yellow);
border-color: var(--yellow-dark);
color: #222;
}
.loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px;
gap: 16px;
color: #666;
}
.loading-spinner {
font-size: 24px;
}
.gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 12px;
}
.gallery.list-view {
display: flex;
flex-direction: column;
gap: 14px;
}
.media-card {
position: relative;
background: #f6f6f6;
border-radius: 10px;
overflow: visible;
border: 1px solid #e2e2e2;
box-shadow: 0 1px 2px rgba(15, 15, 15, 0.04);
display: flex;
flex-direction: column;
isolation: isolate;
transition:
border-color 0.18s ease,
background 0.18s ease,
box-shadow 0.18s ease,
flex-direction 0.3s cubic-bezier(0.4, 0, 0.2, 1),
padding 0.3s cubic-bezier(0.4, 0, 0.2, 1),
gap 0.3s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
}
.media-card::after {
content: "";
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.08);
opacity: 0;
pointer-events: none;
transition: opacity 0.18s ease;
}
.media-card:hover {
border-color: #d4d4d4;
background: #f1f1f1;
box-shadow: 0 2px 4px rgba(15, 15, 15, 0.06);
}
.media-card:hover::after {
opacity: 0.16;
}
.media-card.is-selected {
border-color: #2d965a;
background: #f4fbf7;
box-shadow:
0 0 0 1px rgba(45, 150, 90, 0.35),
0 4px 12px rgba(45, 150, 90, 0.12);
}
.media-card.is-selected::after {
opacity: 0.12;
}
.media-card.is-selected:hover::after {
opacity: 0.18;
}
.media-card.list-view {
flex-direction: row;
align-items: center;
padding: 12px 16px;
gap: 16px;
min-height: 96px;
}
.media-card.list-view .thumb {
width: 128px;
height: 72px;
border-radius: 8px;
object-fit: cover;
flex-shrink: 0;
transition:
width 0.3s cubic-bezier(0.4, 0, 0.2, 1),
height 0.3s cubic-bezier(0.4, 0, 0.2, 1),
border-radius 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.selection-toggle {
position: absolute;
top: 12px;
left: 12px;
width: 34px;
height: 34px;
border-radius: 50%;
border: none;
background: rgba(0, 0, 0, 0.45);
color: #f5f5f5;
display: inline-flex;
align-items: center;
justify-content: center;
opacity: 0;
outline: none;
transform: scale(0.88);
transition:
opacity 0.2s ease,
transform 0.2s ease,
background 0.2s ease;
cursor: pointer;
pointer-events: none;
z-index: 2;
}
.selection-toggle i {
font-size: 14px;
}
.media-card:hover .selection-toggle,
.media-card.is-selected .selection-toggle {
opacity: 1;
transform: scale(1);
pointer-events: auto;
}
.selection-toggle.is-selected {
background: rgba(45, 150, 90, 0.85);
}
.selection-toggle.is-selected i {
color: #fff;
}
.media-card.list-view .selection-toggle {
top: 16px;
left: 16px;
}
.thumb {
width: 100%;
height: 110px;
object-fit: cover;
border-radius: 10px 10px 0 0;
transition:
width 0.3s cubic-bezier(0.4, 0, 0.2, 1),
height 0.3s cubic-bezier(0.4, 0, 0.2, 1),
border-radius 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.thumb.placeholder {
display: flex;
align-items: center;
justify-content: center;
font-size: 42px;
background: #ddd;
}
.info {
padding: 8px;
display: flex;
flex-direction: column;
gap: 4px;
transition:
padding 0.3s cubic-bezier(0.4, 0, 0.2, 1),
gap 0.3s cubic-bezier(0.4, 0, 0.2, 1),
flex 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.name {
font-weight: 600;
font-size: 14px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.info .path {
font-size: 12px;
color: #777;
word-break: break-word;
}
.size {
font-size: 12px;
color: #666;
transition:
opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1),
max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.media-card.list-view .info {
flex: 1;
padding: 0;
gap: 6px;
}
.media-card.list-view .name {
font-size: 15px;
}
.media-card.list-view .size {
display: none;
}
.list-meta {
display: none;
opacity: 0;
max-height: 0;
transition:
opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1),
max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.media-card.list-view .list-meta {
display: flex;
flex-direction: column;
gap: 4px;
font-size: 13px;
color: #7a7a7a;
opacity: 1;
max-height: 200px;
}
.meta-line.primary {
display: flex;
align-items: center;
gap: 6px;
}
.menu-toggle {
position: absolute;
top: 12px;
right: 12px;
width: 34px;
height: 34px;
border-radius: 50%;
border: none;
background: rgba(0, 0, 0, 0.45);
color: #f5f5f5;
display: inline-flex;
align-items: center;
justify-content: center;
opacity: 0;
outline: none;
transform: scale(0.88);
transition:
opacity 0.2s ease,
transform 0.2s ease,
background 0.2s ease;
cursor: pointer;
pointer-events: none;
z-index: 2;
}
.menu-toggle i {
font-size: 14px;
}
.media-card:hover .menu-toggle,
.media-card.is-selected .menu-toggle {
opacity: 1;
transform: scale(1);
pointer-events: auto;
}
.menu-toggle:hover {
background: rgba(0, 0, 0, 0.65);
}
.media-card.list-view .menu-toggle {
top: 16px;
right: 16px;
}
.dropdown-menu-portal {
position: fixed;
background: #ffffff;
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
min-width: 160px;
z-index: 10000;
animation: fadeIn 0.2s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.menu-item {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
padding: 12px 16px;
border: none;
background: transparent;
color: #333;
font-size: 14px;
cursor: pointer;
transition: background-color 0.2s ease;
text-align: left;
}
.menu-item:first-child {
border-radius: 8px 8px 0 0;
}
.menu-item:last-child {
border-radius: 0 0 8px 8px;
}
.menu-item:hover {
background-color: #f5f5f5;
}
.menu-item.delete {
color: #e53935;
}
.menu-item.delete:hover {
background-color: #ffebee;
}
.menu-item i {
font-size: 14px;
width: 16px;
text-align: center;
}
.menu-divider {
height: 1px;
background-color: #e0e0e0;
margin: 0;
}
.empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px;
gap: 16px;
color: #666;
}
/* Folder görünümü */
.folder-card {
background: transparent;
border: none;
box-shadow: none;
padding: 12px 12px 8px;
align-items: center;
}
.folder-card::after {
display: none;
}
.folder-card:hover {
background: rgba(0, 0, 0, 0.03);
border: none;
box-shadow: none;
}
.folder-card.is-selected {
background: rgba(45, 150, 90, 0.12);
box-shadow: none;
}
.folder-card.is-selected::after {
display: none;
}
.folder-thumb {
width: 100%;
height: 110px;
display: flex;
align-items: center;
justify-content: center;
flex: 1;
}
.folder-thumb img {
width: 95px;
height: 95px;
object-fit: contain;
filter: drop-shadow(0 6px 18px rgba(0, 0, 0, 0.16));
}
.folder-card.list-view .folder-thumb {
width: 128px;
height: 128px;
}
.folder-card.list-view .folder-thumb img {
width: 125px;
height: 125px;
}
.folder-info {
margin-top: 4px;
width: 100%;
text-align: center;
flex-shrink: 0;
}
.folder-card.list-view .folder-info {
margin-top: 0;
text-align: left;
}
.folder-name {
font-weight: 600;
font-size: 15px;
color: #2d2d2d;
line-height: 1.35;
word-break: break-word;
}
.folder-path,
.folder-meta {
font-size: 12px;
color: #666;
margin-top: 4px;
}
.folder-path {
word-break: break-word;
}
.folder-meta {
font-style: italic;
}
.folder-card:hover .folder-name,
.folder-card.is-selected .folder-name {
color: #333;
}
.folder-card.list-view .folder-name {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Responsive */
@media (max-width: 768px) {
.gallery {
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
}
.media-card.list-view {
flex-direction: column;
align-items: flex-start;
}
.media-card.list-view .thumb {
width: 100%;
height: 160px;
}
}
@media (max-width: 480px) {
.gallery {
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
}
}
</style>