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