From 66582fdec06edcfdfe2b9279f8a47c85f680f8ed Mon Sep 17 00:00:00 2001 From: AlejandroAkbal <37181533+AlejandroAkbal@users.noreply.github.com> Date: Fri, 13 Mar 2026 04:19:46 -0700 Subject: [PATCH 1/8] fix: enable tag search in saved posts --- pages/premium/saved-posts/[domain].vue | 139 +++++++++++++++++++++++-- 1 file changed, 129 insertions(+), 10 deletions(-) diff --git a/pages/premium/saved-posts/[domain].vue b/pages/premium/saved-posts/[domain].vue index 3bbcad14..7302c8e8 100644 --- a/pages/premium/saved-posts/[domain].vue +++ b/pages/premium/saved-posts/[domain].vue @@ -3,7 +3,8 @@ import { ArrowPathIcon, QuestionMarkCircleIcon } from '@heroicons/vue/24/solid' import { useInfiniteQuery } from '@tanstack/vue-query' import { useWindowVirtualizer } from '@tanstack/vue-virtual' - import { throttle } from 'es-toolkit' + import { cloneDeep, throttle } from 'es-toolkit' + import { FetchError } from 'ofetch' import type { Ref } from 'vue' import { toast } from 'vue-sonner' import type { Domain } from '~/assets/js/domain' @@ -17,6 +18,7 @@ const router = useRouter() const route = useRoute() + const config = useRuntimeConfig() const { $pocketBase } = useNuxtApp() @@ -228,7 +230,55 @@ * Listeners */ async function onSearchTag(tag: string) { - toast.error('Autocomplete not implemented') + const apiUrl = config.public.apiUrl + '/booru/' + selectedBooru.value.type.type + '/tags' + + const response = await $fetch(apiUrl, { + params: { + baseEndpoint: selectedBooru.value.domain, + + tag, + order: 'count', + limit: 20, + + // Booru options + httpScheme: selectedBooru.value.config?.options?.HTTPScheme ?? undefined + } + }) + // + .catch(async (error) => { + const Sentry = await import('@sentry/nuxt') + + Sentry.captureException(error) + + return error + }) + + if (response instanceof FetchError) { + switch (response.status) { + case 404: + toast.error('No tags found for query "' + tag + '"') + break + + case 429: + // TODO: Cant always check if 429 is the status code, always show? + toast.error(response.statusText, { + description: 'You sent too many requests in a short period of time', + action: { + label: 'Verify I am not a Bot', + onClick: () => window.open(config.public.apiUrl + '/status', '_blank') + } + }) + break + + default: + toast.error(`Failed to load tags: "${response.message}"`) + break + } + + return + } + + tagResults.value = response.data } async function onDomainChange(domain: Domain) { @@ -236,29 +286,62 @@ } async function onSearchSubmit({ tags, filters }) { - // TODO: Tags - await reflectChangesInUrl({ page: null, filters }) + await reflectChangesInUrl({ page: null, tags, filters }) } /** * Adds the tag, or removes it if it already exists */ async function onPostAddTag(tag: string) { - toast.error('Not implemented') + const isTagNegative = tag.startsWith('-') + + let newTags = cloneDeep(selectedTags.value) + + // Remove tag if it already exists + const isTagAlreadySelected = newTags.some((selectedTag) => selectedTag.name === tag) + + if (isTagAlreadySelected) { + newTags = newTags.filter((selectedTag) => selectedTag.name !== tag) + + await reflectChangesInUrl({ page: null, tags: newTags }) + return + } + + if (isTagNegative) { + const doesTagExistInPositive = newTags.some((selectedTag) => selectedTag.name === tag.slice(1)) + + if (doesTagExistInPositive) { + newTags = newTags.filter((selectedTag) => selectedTag.name !== tag.slice(1)) + } + } + + newTags.push(new Tag({ name: tag }).toJSON()) + + await reflectChangesInUrl({ page: null, tags: newTags }) } /** * Sets tags to only the given tag */ async function onPostSetTag(tag: string) { - toast.error('Not implemented') + await reflectChangesInUrl({ page: null, tags: [new Tag({ name: tag }).toJSON()] }) } /** * Opens the tag in a new tab */ async function onPostOpenTagInNewTab(tag: string) { - toast.error('Not implemented') + const tagUrl = generatePostsRoute( + '/premium/saved-posts', + selectedBooru.value.domain, + undefined, + [new Tag({ name: tag }).toJSON()], + undefined + ) + + const resolvedTagUrl = router.resolve(tagUrl).href + + window.open(resolvedTagUrl, '_blank') } async function onLoadNextPostPage() { @@ -309,6 +392,18 @@ */ interface IPostPageFromPocketBase extends Omit {} + const tagCategoryFields = ['tags_artist', 'tags_character', 'tags_copyright', 'tags_general', 'tags_meta'] as const + + function buildTagFilterInAnyCategory(tag: string, tagParamKey: string) { + const tagFilterByCategory = tagCategoryFields.map((tagField) => { + return $pocketBase.filter(`${tagField} ?= {:${tagParamKey}}`, { + [tagParamKey]: tag + }) + }) + + return `(${tagFilterByCategory.join(' || ')})` + } + async function fetchPosts(options: any): Promise { const page = options.pageParam @@ -344,11 +439,35 @@ }) } - // TODO - // if (selectedTags.value.length > 0) { - // } + if (selectedTags.value.length > 0) { + selectedTags.value.forEach((selectedTag, tagIndex) => { + const isNegativeTag = selectedTag.name.startsWith('-') + const normalizedTag = isNegativeTag ? selectedTag.name.slice(1) : selectedTag.name + + if (!normalizedTag) { + return + } + + if (pocketbaseRequestFilter !== '') { + pocketbaseRequestFilter += ' && ' + } + + const tagFilter = buildTagFilterInAnyCategory(normalizedTag, `tag_${tagIndex}`) + + if (isNegativeTag) { + pocketbaseRequestFilter += `!${tagFilter}` + return + } + + pocketbaseRequestFilter += tagFilter + }) + } if (selectedFilters.value.score) { + if (pocketbaseRequestFilter !== '') { + pocketbaseRequestFilter += ' && ' + } + pocketbaseRequestFilter += $pocketBase.filter('score >= {:score}', { score: selectedFilters.value.score }) From 107f308077963c1d675bd403891ecb2aa5f20a17 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Fri, 13 Mar 2026 12:19:20 +0000 Subject: [PATCH 2/8] fix: apply CodeRabbit auto-fixes Fixed 1 file(s) based on 3 unresolved review comments. Co-authored-by: CodeRabbit --- pages/premium/saved-posts/[domain].vue | 32 ++++++++++++++++++-------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/pages/premium/saved-posts/[domain].vue b/pages/premium/saved-posts/[domain].vue index 7302c8e8..396ad9f7 100644 --- a/pages/premium/saved-posts/[domain].vue +++ b/pages/premium/saved-posts/[domain].vue @@ -229,7 +229,10 @@ /** * Listeners */ + let currentSearchRequestId = 0 + async function onSearchTag(tag: string) { + const requestId = ++currentSearchRequestId const apiUrl = config.public.apiUrl + '/booru/' + selectedBooru.value.type.type + '/tags' const response = await $fetch(apiUrl, { @@ -246,17 +249,19 @@ }) // .catch(async (error) => { - const Sentry = await import('@sentry/nuxt') - - Sentry.captureException(error) - return error }) + // Ignore if this is not the latest request + if (requestId !== currentSearchRequestId) { + return + } + if (response instanceof FetchError) { switch (response.status) { case 404: toast.error('No tags found for query "' + tag + '"') + tagResults.value = [] break case 429: @@ -268,10 +273,14 @@ onClick: () => window.open(config.public.apiUrl + '/status', '_blank') } }) + tagResults.value = [] break default: + const Sentry = await import('@sentry/nuxt') + Sentry.captureException(response) toast.error(`Failed to load tags: "${response.message}"`) + tagResults.value = [] break } @@ -307,12 +316,15 @@ return } + // Remove opposite variant to prevent conflicts if (isTagNegative) { - const doesTagExistInPositive = newTags.some((selectedTag) => selectedTag.name === tag.slice(1)) - - if (doesTagExistInPositive) { - newTags = newTags.filter((selectedTag) => selectedTag.name !== tag.slice(1)) - } + // Removing negative prefix, check for positive counterpart + const positiveVariant = tag.slice(1) + newTags = newTags.filter((selectedTag) => selectedTag.name !== positiveVariant) + } else { + // Adding positive tag, check for negative counterpart + const negativeVariant = `-${tag}` + newTags = newTags.filter((selectedTag) => selectedTag.name !== negativeVariant) } newTags.push(new Tag({ name: tag }).toJSON()) @@ -992,4 +1004,4 @@ - + \ No newline at end of file From 67853fa90be365d2ef364b2d7c923cd82891b216 Mon Sep 17 00:00:00 2001 From: AlejandroAkbal <37181533+AlejandroAkbal@users.noreply.github.com> Date: Mon, 16 Mar 2026 22:18:48 -0700 Subject: [PATCH 3/8] fix: guard saved-post tag search config --- pages/premium/saved-posts/[domain].vue | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/pages/premium/saved-posts/[domain].vue b/pages/premium/saved-posts/[domain].vue index 396ad9f7..6153398b 100644 --- a/pages/premium/saved-posts/[domain].vue +++ b/pages/premium/saved-posts/[domain].vue @@ -233,7 +233,15 @@ async function onSearchTag(tag: string) { const requestId = ++currentSearchRequestId - const apiUrl = config.public.apiUrl + '/booru/' + selectedBooru.value.type.type + '/tags' + const apiBaseUrl = config.public.apiUrl + + if (!apiBaseUrl) { + toast.error('API URL is not configured') + tagResults.value = [] + return + } + + const apiUrl = apiBaseUrl + '/booru/' + selectedBooru.value.type.type + '/tags' const response = await $fetch(apiUrl, { params: { @@ -270,18 +278,19 @@ description: 'You sent too many requests in a short period of time', action: { label: 'Verify I am not a Bot', - onClick: () => window.open(config.public.apiUrl + '/status', '_blank') + onClick: () => window.open(apiBaseUrl + '/status', '_blank') } }) tagResults.value = [] break - default: + default: { const Sentry = await import('@sentry/nuxt') Sentry.captureException(response) toast.error(`Failed to load tags: "${response.message}"`) tagResults.value = [] break + } } return @@ -1004,4 +1013,4 @@ - \ No newline at end of file + From 851787d442906f3eddfb5ba09d3a47bd8dd07eb5 Mon Sep 17 00:00:00 2001 From: AlejandroAkbal <37181533+AlejandroAkbal@users.noreply.github.com> Date: Mon, 16 Mar 2026 23:26:38 -0700 Subject: [PATCH 4/8] fix: harden saved-post tag search --- pages/premium/saved-posts/[domain].vue | 103 +++++++++++++++---------- 1 file changed, 61 insertions(+), 42 deletions(-) diff --git a/pages/premium/saved-posts/[domain].vue b/pages/premium/saved-posts/[domain].vue index 6153398b..1d50db34 100644 --- a/pages/premium/saved-posts/[domain].vue +++ b/pages/premium/saved-posts/[domain].vue @@ -243,56 +243,75 @@ const apiUrl = apiBaseUrl + '/booru/' + selectedBooru.value.type.type + '/tags' - const response = await $fetch(apiUrl, { - params: { - baseEndpoint: selectedBooru.value.domain, + let response: { data: Tag[] } | undefined - tag, - order: 'count', - limit: 20, + try { + response = await $fetch<{ data: Tag[] }>(apiUrl, { + params: { + baseEndpoint: selectedBooru.value.domain, - // Booru options - httpScheme: selectedBooru.value.config?.options?.HTTPScheme ?? undefined - } - }) - // - .catch(async (error) => { - return error + tag, + order: 'count', + limit: 20, + + // Booru options + httpScheme: selectedBooru.value.config?.options?.HTTPScheme ?? undefined + } }) + } catch (error) { + // Ignore if this is not the latest request + if (requestId !== currentSearchRequestId) { + return + } + + if (error instanceof FetchError) { + switch (error.status) { + case 404: + toast.error('No tags found for query "' + tag + '"') + tagResults.value = [] + break + + case 429: + // TODO: Cant always check if 429 is the status code, always show? + toast.error(error.statusText, { + description: 'You sent too many requests in a short period of time', + action: { + label: 'Verify I am not a Bot', + onClick: () => window.open(apiBaseUrl + '/status', '_blank') + } + }) + tagResults.value = [] + break + + default: { + const Sentry = await import('@sentry/nuxt') + Sentry.captureException(error) + toast.error(`Failed to load tags: "${error.message}"`) + tagResults.value = [] + break + } + } + + return + } + + const Sentry = await import('@sentry/nuxt') + Sentry.captureException(error) + toast.error('Failed to load tags') + tagResults.value = [] + return + } // Ignore if this is not the latest request if (requestId !== currentSearchRequestId) { return } - if (response instanceof FetchError) { - switch (response.status) { - case 404: - toast.error('No tags found for query "' + tag + '"') - tagResults.value = [] - break - - case 429: - // TODO: Cant always check if 429 is the status code, always show? - toast.error(response.statusText, { - description: 'You sent too many requests in a short period of time', - action: { - label: 'Verify I am not a Bot', - onClick: () => window.open(apiBaseUrl + '/status', '_blank') - } - }) - tagResults.value = [] - break - - default: { - const Sentry = await import('@sentry/nuxt') - Sentry.captureException(response) - toast.error(`Failed to load tags: "${response.message}"`) - tagResults.value = [] - break - } - } - + if (!(response && typeof response === 'object' && 'data' in response && Array.isArray(response.data))) { + const Sentry = await import('@sentry/nuxt') + Sentry.captureException(response) + toast.error('Failed to load tags') + tagResults.value = [] return } @@ -484,7 +503,7 @@ }) } - if (selectedFilters.value.score) { + if (selectedFilters.value.score !== undefined) { if (pocketbaseRequestFilter !== '') { pocketbaseRequestFilter += ' && ' } From 6776bd30a91a21502b9dfe28016848d9042615f2 Mon Sep 17 00:00:00 2001 From: AlejandroAkbal <37181533+AlejandroAkbal@users.noreply.github.com> Date: Tue, 17 Mar 2026 00:15:26 -0700 Subject: [PATCH 5/8] fix: remove async from saved-post tag opener --- pages/premium/saved-posts/[domain].vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/premium/saved-posts/[domain].vue b/pages/premium/saved-posts/[domain].vue index 1d50db34..726966a5 100644 --- a/pages/premium/saved-posts/[domain].vue +++ b/pages/premium/saved-posts/[domain].vue @@ -370,7 +370,7 @@ /** * Opens the tag in a new tab */ - async function onPostOpenTagInNewTab(tag: string) { + function onPostOpenTagInNewTab(tag: string) { const tagUrl = generatePostsRoute( '/premium/saved-posts', selectedBooru.value.domain, From 3aa546ed204a4bd9947c974822117bfda4796faf Mon Sep 17 00:00:00 2001 From: AlejandroAkbal <37181533+AlejandroAkbal@users.noreply.github.com> Date: Sat, 21 Mar 2026 21:20:05 -0700 Subject: [PATCH 6/8] fix: harden saved-post tag links --- pages/premium/saved-posts/[domain].vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pages/premium/saved-posts/[domain].vue b/pages/premium/saved-posts/[domain].vue index 726966a5..4582fc89 100644 --- a/pages/premium/saved-posts/[domain].vue +++ b/pages/premium/saved-posts/[domain].vue @@ -277,7 +277,7 @@ description: 'You sent too many requests in a short period of time', action: { label: 'Verify I am not a Bot', - onClick: () => window.open(apiBaseUrl + '/status', '_blank') + onClick: () => window.open(apiBaseUrl + '/status', '_blank', 'noopener,noreferrer') } }) tagResults.value = [] @@ -381,7 +381,7 @@ const resolvedTagUrl = router.resolve(tagUrl).href - window.open(resolvedTagUrl, '_blank') + window.open(resolvedTagUrl, '_blank', 'noopener,noreferrer') } async function onLoadNextPostPage() { From 6e85a65f326eb988714605210efd1cefd0fcbe10 Mon Sep 17 00:00:00 2001 From: AlejandroAkbal <37181533+AlejandroAkbal@users.noreply.github.com> Date: Sat, 21 Mar 2026 22:19:50 -0700 Subject: [PATCH 7/8] fix: skip blank saved-post tag searches --- pages/premium/saved-posts/[domain].vue | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/pages/premium/saved-posts/[domain].vue b/pages/premium/saved-posts/[domain].vue index 4582fc89..fd1e6b81 100644 --- a/pages/premium/saved-posts/[domain].vue +++ b/pages/premium/saved-posts/[domain].vue @@ -232,6 +232,13 @@ let currentSearchRequestId = 0 async function onSearchTag(tag: string) { + const trimmedTag = tag.trim() + + if (!trimmedTag) { + tagResults.value = [] + return + } + const requestId = ++currentSearchRequestId const apiBaseUrl = config.public.apiUrl @@ -250,7 +257,7 @@ params: { baseEndpoint: selectedBooru.value.domain, - tag, + tag: trimmedTag, order: 'count', limit: 20, @@ -267,7 +274,7 @@ if (error instanceof FetchError) { switch (error.status) { case 404: - toast.error('No tags found for query "' + tag + '"') + toast.error('No tags found for query "' + trimmedTag + '"') tagResults.value = [] break @@ -309,7 +316,10 @@ if (!(response && typeof response === 'object' && 'data' in response && Array.isArray(response.data))) { const Sentry = await import('@sentry/nuxt') - Sentry.captureException(response) + const invalidTagsResponseError = Object.assign(new Error('Invalid tags response format'), { + response + }) + Sentry.captureException(invalidTagsResponseError) toast.error('Failed to load tags') tagResults.value = [] return From b83a144f5cf0a6d5ee2297d7d2b8bab8e45d5841 Mon Sep 17 00:00:00 2001 From: AlejandroAkbal <37181533+AlejandroAkbal@users.noreply.github.com> Date: Sun, 22 Mar 2026 21:16:06 -0700 Subject: [PATCH 8/8] fix: use real booru metadata for saved tag search --- pages/premium/saved-posts/[domain].vue | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/pages/premium/saved-posts/[domain].vue b/pages/premium/saved-posts/[domain].vue index fd1e6b81..9f0e5cbf 100644 --- a/pages/premium/saved-posts/[domain].vue +++ b/pages/premium/saved-posts/[domain].vue @@ -27,6 +27,7 @@ const { addUrlToPageHistory } = usePageHistory() const { savedPostList } = usePocketbase() + const { booruList: availableBooruList } = useBooruList() /** * URL @@ -34,12 +35,23 @@ const domainsFromPocketbase = await $pocketBase.collection('distinct_original_domain_from_posts').getFullList() const booruList = computed(() => { + const getKnownBooruMetadata = (domain: string) => { + const knownBooru = availableBooruList.value.find((booru) => booru.domain === domain) + + return { + type: knownBooru?.type ?? booruTypeList[0], + config: knownBooru?.config ?? null + } + } + + const productionBooruMetadata = getKnownBooruMetadata(project.urls.production.hostname) + const _booruList: Domain[] = [ // r34.app { domain: project.urls.production.hostname, - type: booruTypeList[0], - config: null, + type: productionBooruMetadata.type, + config: productionBooruMetadata.config, isCustom: false, isPremium: false } @@ -48,10 +60,12 @@ const booruNamesInDb: string[] = domainsFromPocketbase.map((domain) => domain.original_domain) booruNamesInDb.forEach((booruNameInDb) => { + const booruMetadata = getKnownBooruMetadata(booruNameInDb) + _booruList.push({ domain: booruNameInDb, - type: booruTypeList[0], - config: null, + type: booruMetadata.type, + config: booruMetadata.config, isCustom: false, isPremium: false }) @@ -232,6 +246,7 @@ let currentSearchRequestId = 0 async function onSearchTag(tag: string) { + const requestId = ++currentSearchRequestId const trimmedTag = tag.trim() if (!trimmedTag) { @@ -239,7 +254,6 @@ return } - const requestId = ++currentSearchRequestId const apiBaseUrl = config.public.apiUrl if (!apiBaseUrl) {