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); }); } })();