From 606f7cb474d82a2adeb387b146adfa8b251647ff Mon Sep 17 00:00:00 2001 From: logikonline Date: Sat, 31 Jan 2026 01:20:17 -0500 Subject: [PATCH] feat(mediakit): add unsplash search filters and pagination Add orientation filter (landscape, portrait, square) and color filter (12 color options including black & white) to Unsplash search. Implement pagination with prev/next buttons and page info display. Update API response structure to include total pages and current page. Add locale strings for all filter options. Update max image size message from 2MB to 5MB. --- options/locale/locale_en-US.json | 18 ++- routers/web/repo/setting/mediakit.go | 56 +++++++- templates/repo/settings/media_kit.tmpl | 171 ++++++++++++++++++------- 3 files changed, 193 insertions(+), 52 deletions(-) diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index 684b273f39..4729418c0f 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -4048,12 +4048,28 @@ "repo.settings.media_kit.bg_image_help": "Upload a JPG or PNG image (max 2 MB) or search Unsplash.", "repo.settings.media_kit.upload": "Upload Image", "repo.settings.media_kit.upload_error": "Failed to upload image.", - "repo.settings.media_kit.image_too_large": "Image must be smaller than 2 MB.", + "repo.settings.media_kit.image_too_large": "Image must be smaller than 5 MB.", "repo.settings.media_kit.not_an_image": "File must be a JPG or PNG image.", "repo.settings.media_kit.image_uploaded": "Background image uploaded.", "repo.settings.media_kit.delete_image": "Remove Image", "repo.settings.media_kit.unsplash_search": "Search Unsplash", "repo.settings.media_kit.unsplash_placeholder": "Search for photos…", + "repo.settings.media_kit.unsplash_orientation_any": "Any Orientation", + "repo.settings.media_kit.unsplash_orientation_landscape": "Landscape", + "repo.settings.media_kit.unsplash_orientation_portrait": "Portrait", + "repo.settings.media_kit.unsplash_orientation_square": "Square", + "repo.settings.media_kit.unsplash_color_any": "Any Color", + "repo.settings.media_kit.unsplash_color_bw": "Black & White", + "repo.settings.media_kit.unsplash_color_black": "Black", + "repo.settings.media_kit.unsplash_color_white": "White", + "repo.settings.media_kit.unsplash_color_yellow": "Yellow", + "repo.settings.media_kit.unsplash_color_orange": "Orange", + "repo.settings.media_kit.unsplash_color_red": "Red", + "repo.settings.media_kit.unsplash_color_purple": "Purple", + "repo.settings.media_kit.unsplash_color_magenta": "Magenta", + "repo.settings.media_kit.unsplash_color_green": "Green", + "repo.settings.media_kit.unsplash_color_teal": "Teal", + "repo.settings.media_kit.unsplash_color_blue": "Blue", "repo.settings.media_kit.unsplash_attribution": "Photo by %s on Unsplash", "repo.settings.media_kit.preview": "Preview", "repo.settings.license": "License", diff --git a/routers/web/repo/setting/mediakit.go b/routers/web/repo/setting/mediakit.go index 9ec4f213c5..913d0fc117 100644 --- a/routers/web/repo/setting/mediakit.go +++ b/routers/web/repo/setting/mediakit.go @@ -165,6 +165,35 @@ type unsplashSearchResult struct { AuthorURL string `json:"author_url"` } +// unsplashSearchResponse wraps results with pagination info. +type unsplashSearchResponse struct { + Results []unsplashSearchResult `json:"results"` + TotalPages int `json:"total_pages"` + Page int `json:"page"` +} + +// validUnsplashOrientations is the set of accepted orientation values. +var validUnsplashOrientations = map[string]bool{ + "landscape": true, + "portrait": true, + "squarish": true, +} + +// validUnsplashColors is the set of accepted color filter values. +var validUnsplashColors = map[string]bool{ + "black_and_white": true, + "black": true, + "white": true, + "yellow": true, + "orange": true, + "red": true, + "purple": true, + "magenta": true, + "green": true, + "teal": true, + "blue": true, +} + // MediaKitUnsplashSearch proxies a search request to the Unsplash API. func MediaKitUnsplashSearch(ctx *context.Context) { if !setting.Unsplash.Enabled { @@ -174,7 +203,7 @@ func MediaKitUnsplashSearch(ctx *context.Context) { query := ctx.FormString("q") if query == "" { - ctx.JSON(http.StatusOK, []unsplashSearchResult{}) + ctx.JSON(http.StatusOK, unsplashSearchResponse{Results: []unsplashSearchResult{}}) return } @@ -183,10 +212,17 @@ func MediaKitUnsplashSearch(ctx *context.Context) { page = "1" } - url := fmt.Sprintf("https://api.unsplash.com/search/photos?query=%s&orientation=landscape&per_page=12&page=%s", + apiURL := fmt.Sprintf("https://api.unsplash.com/search/photos?query=%s&per_page=12&page=%s", query, page) - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if orientation := ctx.FormString("orientation"); validUnsplashOrientations[orientation] { + apiURL += "&orientation=" + orientation + } + if color := ctx.FormString("color"); validUnsplashColors[color] { + apiURL += "&color=" + color + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, apiURL, nil) if err != nil { ctx.ServerError("NewRequest", err) return @@ -220,6 +256,7 @@ func MediaKitUnsplashSearch(ctx *context.Context) { } `json:"links"` } `json:"user"` } `json:"results"` + TotalPages int `json:"total_pages"` } if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil { @@ -238,7 +275,18 @@ func MediaKitUnsplashSearch(ctx *context.Context) { }) } - ctx.JSON(http.StatusOK, results) + pageNum := 1 + if p := ctx.FormString("page"); p != "" { + if n, err := fmt.Sscanf(p, "%d", &pageNum); n != 1 || err != nil { + pageNum = 1 + } + } + + ctx.JSON(http.StatusOK, unsplashSearchResponse{ + Results: results, + TotalPages: apiResp.TotalPages, + Page: pageNum, + }) } // MediaKitUnsplashSelect downloads an Unsplash photo and saves it as the bg image. diff --git a/templates/repo/settings/media_kit.tmpl b/templates/repo/settings/media_kit.tmpl index ab444b2a95..4ca4960ea2 100644 --- a/templates/repo/settings/media_kit.tmpl +++ b/templates/repo/settings/media_kit.tmpl @@ -92,12 +92,39 @@ {{svg "octicon-search" 16}} +
+ + +
+
+ + + +
{{end}} @@ -178,70 +205,120 @@ const searchInput = document.getElementById('unsplash-query'); const resultsDiv = document.getElementById('unsplash-results'); const loadingDiv = document.getElementById('unsplash-loading'); + const orientationSel = document.getElementById('unsplash-orientation'); + const colorSel = document.getElementById('unsplash-color'); + const paginationDiv = document.getElementById('unsplash-pagination'); + const prevBtn = document.getElementById('unsplash-prev'); + const nextBtn = document.getElementById('unsplash-next'); + const pageInfo = document.getElementById('unsplash-page-info'); if (searchBtn) { - async function doSearch() { + let currentPage = 1; + let totalPages = 1; + + function buildSearchURL(page) { + let url = '{{.Link}}/unsplash/search?q=' + encodeURIComponent(searchInput.value.trim()) + '&page=' + page; + if (orientationSel && orientationSel.value) url += '&orientation=' + orientationSel.value; + if (colorSel && colorSel.value) url += '&color=' + colorSel.value; + return url; + } + + function renderPhoto(photo) { + const el = document.createElement('div'); + el.style.cssText = 'cursor:pointer;border-radius:6px;overflow:hidden;border:2px solid transparent;position:relative;'; + el.innerHTML = '' + + '
' + photo.author + '
'; + el.addEventListener('click', async function() { + resultsDiv.querySelectorAll('div').forEach(function(d) { d.style.borderColor = 'transparent'; }); + el.style.borderColor = 'var(--color-primary)'; + el.style.opacity = '0.7'; + el.style.pointerEvents = 'none'; + const statusEl = document.createElement('div'); + statusEl.textContent = 'Downloading image…'; + statusEl.style.cssText = 'padding:8px;color:var(--color-text);font-size:13px;'; + resultsDiv.parentNode.insertBefore(statusEl, resultsDiv.nextSibling); + try { + const csrfToken = document.querySelector('meta[name=_csrf]')?.content || ''; + const fd = new FormData(); + fd.append('id', photo.id); + fd.append('author', photo.author); + fd.append('url', photo.regular); + const resp = await fetch('{{.Link}}/unsplash/select', { + method: 'POST', + headers: {'X-Csrf-Token': csrfToken}, + body: fd, + }); + if (resp.ok) { + statusEl.textContent = 'Image saved! Reloading…'; + window.location.reload(); + } else { + let detail = resp.status + ' ' + resp.statusText; + try { const j = await resp.json(); if (j.error) detail = j.error; } catch (_) {} + statusEl.textContent = 'Failed to save image: ' + detail; + statusEl.style.color = 'var(--color-error)'; + } + } catch (e) { + console.error('Unsplash select error:', e); + statusEl.textContent = 'Network error: ' + e.message; + statusEl.style.color = 'var(--color-error)'; + } finally { + el.style.opacity = '1'; + el.style.pointerEvents = ''; + } + }); + return el; + } + + function updatePagination() { + if (totalPages <= 1) { + paginationDiv.classList.add('tw-hidden'); + return; + } + paginationDiv.classList.remove('tw-hidden'); + pageInfo.textContent = currentPage + ' / ' + totalPages; + prevBtn.disabled = currentPage <= 1; + nextBtn.disabled = currentPage >= totalPages; + } + + async function doSearch(page) { const q = searchInput.value.trim(); if (!q) return; + if (!page) page = 1; + currentPage = page; loadingDiv.classList.remove('tw-hidden'); + paginationDiv.classList.add('tw-hidden'); resultsDiv.innerHTML = ''; + // Remove any leftover status messages + const oldStatus = resultsDiv.parentNode.querySelector('div[style*="padding:8px"]'); + if (oldStatus) oldStatus.remove(); try { - const resp = await fetch('{{.Link}}/unsplash/search?q=' + encodeURIComponent(q)); - const photos = await resp.json(); + const resp = await fetch(buildSearchURL(page)); + const data = await resp.json(); loadingDiv.classList.add('tw-hidden'); + totalPages = data.total_pages || 1; + currentPage = data.page || page; + const photos = data.results || []; photos.forEach(function(photo) { - const el = document.createElement('div'); - el.style.cssText = 'cursor:pointer;border-radius:6px;overflow:hidden;border:2px solid transparent;position:relative;'; - el.innerHTML = '' - + '
' + photo.author + '
'; - el.addEventListener('click', async function() { - // Deselect all others - resultsDiv.querySelectorAll('div').forEach(function(d) { d.style.borderColor = 'transparent'; }); - el.style.borderColor = 'var(--color-primary)'; - el.style.opacity = '0.7'; - el.style.pointerEvents = 'none'; - // Show a status message - const statusEl = document.createElement('div'); - statusEl.textContent = 'Downloading image…'; - statusEl.style.cssText = 'padding:8px;color:var(--color-text);font-size:13px;'; - resultsDiv.parentNode.insertBefore(statusEl, resultsDiv.nextSibling); - try { - const csrf = document.querySelector('input[name="_csrf"]').value; - const fd = new FormData(); - fd.append('_csrf', csrf); - fd.append('id', photo.id); - fd.append('author', photo.author); - fd.append('url', photo.regular); - const resp = await fetch('{{.Link}}/unsplash/select', {method: 'POST', body: fd}); - if (resp.ok) { - statusEl.textContent = 'Image saved! Reloading…'; - window.location.reload(); - } else { - let detail = resp.status + ' ' + resp.statusText; - try { const j = await resp.json(); if (j.error) detail = j.error; } catch (_) {} - statusEl.textContent = 'Failed to save image: ' + detail; - statusEl.style.color = 'var(--color-error)'; - } - } catch (e) { - console.error('Unsplash select error:', e); - statusEl.textContent = 'Network error: ' + e.message; - statusEl.style.color = 'var(--color-error)'; - } finally { - el.style.opacity = '1'; - el.style.pointerEvents = ''; - } - }); - resultsDiv.appendChild(el); + resultsDiv.appendChild(renderPhoto(photo)); }); + updatePagination(); } catch (e) { loadingDiv.classList.add('tw-hidden'); } } - searchBtn.addEventListener('click', doSearch); + searchBtn.addEventListener('click', function() { doSearch(1); }); searchInput.addEventListener('keydown', function(e) { - if (e.key === 'Enter') { e.preventDefault(); doSearch(); } + if (e.key === 'Enter') { e.preventDefault(); doSearch(1); } }); + + // Re-search on filter change if there's already a query + if (orientationSel) orientationSel.addEventListener('change', function() { if (searchInput.value.trim()) doSearch(1); }); + if (colorSel) colorSel.addEventListener('change', function() { if (searchInput.value.trim()) doSearch(1); }); + + // Pagination + if (prevBtn) prevBtn.addEventListener('click', function() { if (currentPage > 1) doSearch(currentPage - 1); }); + if (nextBtn) nextBtn.addEventListener('click', function() { if (currentPage < totalPages) doSearch(currentPage + 1); }); } })();