From c74926b8c184765ec0d4f1104c4091ace1c57e93 Mon Sep 17 00:00:00 2001 From: neobooru <50623835+neobooru@users.noreply.github.com> Date: Tue, 5 Dec 2023 21:25:11 +0100 Subject: [PATCH] Add pool feature in post upload view --- .eslintrc.cjs | 2 + src/api/index.ts | 39 ++++++- src/api/models.ts | 35 ++++-- src/background/main.ts | 41 +++++-- src/components/AutocompleteInput.vue | 149 ++++++++++++++++++++++++++ src/components/CompactPools.vue | 32 ++++++ src/components/CompactTags.vue | 3 +- src/components/PoolInput.vue | 48 +++++++++ src/components/TagInput.vue | 148 +++---------------------- src/composables/useStorageLocal.ts | 2 +- src/models/index.ts | 35 ++++-- src/popup/pages/PopupMain.vue | 154 +++++++++++++-------------- src/stores/index.ts | 6 +- src/styles/main.scss | 2 +- 14 files changed, 456 insertions(+), 240 deletions(-) create mode 100644 src/components/AutocompleteInput.vue create mode 100644 src/components/CompactPools.vue create mode 100644 src/components/PoolInput.vue diff --git a/.eslintrc.cjs b/.eslintrc.cjs index e39f3e1..c12c68e 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -23,5 +23,7 @@ module.exports = { }, ], "@typescript-eslint/no-explicit-any": "off", + "eol-last": ["error", "always"], + "comma-dangle": ["error", "always-multiline"], }, }; diff --git a/src/api/index.ts b/src/api/index.ts index c605a00..388ca41 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -10,6 +10,10 @@ import { TagFields, TemporaryFileUploadResult, UpdatePostRequest, + PoolsResult, + PoolFields, + Pool, + UpdatePoolRequest, } from "./models"; import { ScrapedPostDetails, SzuruSiteConfig } from "~/models"; @@ -63,7 +67,7 @@ export default class SzurubooruApi { offset = 0, limit = 100, fields?: TagFields[], - cancelToken?: CancelToken + cancelToken?: CancelToken, ): Promise { const params = new URLSearchParams(); params.append("offset", offset.toString()); @@ -99,6 +103,39 @@ export default class SzurubooruApi { return (await this.apiPost("posts", obj)).data; } + async createPool(name: string, category: string, posts?: number[]): Promise { + const obj = { + names: [name], + category, + }; + + if (posts) { + obj.posts = posts; + } + + return (await this.apiPost("pool", obj)).data; + } + + async getPools( + query?: string, + offset = 0, + limit = 100, + fields?: PoolFields[], + cancelToken?: CancelToken): Promise { + const params = new URLSearchParams(); + params.append("offset", offset.toString()); + params.append("limit", limit.toString()); + + if (fields && fields.length > 0) params.append("fields", fields.join()); + if (query) params.append("query", query); + + return (await this.apiGet("pools?" + params.toString(), {}, cancelToken)).data; + } + + async updatePool(id: number, updateRequest: UpdatePoolRequest): Promise { + return (await this.apiPut("pool/" + id, updateRequest)).data; + } + async reverseSearch(contentUrl: string): Promise { const obj = { contentUrl }; return (await this.apiPost("posts/reverse-search", obj)).data; diff --git a/src/api/models.ts b/src/api/models.ts index e9e41d6..9a5522b 100644 --- a/src/api/models.ts +++ b/src/api/models.ts @@ -3,9 +3,11 @@ */ export type Safety = "safe" | "sketchy" | "unsafe"; -export type TagFields = "names" | "category" | "usages" | "implications"; +export type TagFields = "version" | "names" | "category" | "usages" | "implications"; +export type PoolFields = "version" | "id" | "names" | "category" | "description" | "postCount" | "posts"; export type TagsResult = PagedSearchResult; +export type PoolsResult = PagedSearchResult; export type TagCategoriesResult = UnpagedSearchResult; export interface SzuruError { @@ -37,18 +39,28 @@ export interface MicroTag { usages: number; } -export interface Tag { - names: string[]; - category: string; +export interface Tag extends MicroTag { version: number; description?: string; // Markdown creationTime: Date; lastEditTime?: Date; - usages: number; suggestions: MicroTag[]; implications: MicroTag[]; } +/** + * All fields can be optional. + */ +export interface Pool { + id: number; + names: string[]; + category: string; + version: number; + description: null; + postCount: number; + posts: MicroPost[]; +} + export interface TagCategory { name: string; version: number; @@ -62,8 +74,12 @@ export interface MicroUser { avatarUrl: string; } -export interface Post { +export interface MicroPost { id: number; + thumbnailUrl: string; +} + +export interface Post extends MicroPost { version: number; creationTime: Date; lastEditTime?: Date; @@ -76,7 +92,6 @@ export interface Post { canvasWidth: number; canvasHeight: number; contentUrl: string; - thumbnailUrl: string; flags: string[]; tags: MicroTag[]; relations: any[]; // MicroPost resource @@ -127,3 +142,9 @@ export interface UpdatePostRequest { contentUrl: string | undefined; contentToken: string | undefined; } + +export interface UpdatePoolRequest { + version: number; + // names?: string[]; + posts: number[]; +} diff --git a/src/background/main.ts b/src/background/main.ts index 7bbee75..ffab376 100644 --- a/src/background/main.ts +++ b/src/background/main.ts @@ -8,7 +8,7 @@ import { PostUpdateCommandData, FetchCommandData, } from "~/models"; -import { PostAlreadyUploadedError } from "~/api/models"; +import { PostAlreadyUploadedError, UpdatePoolRequest } from "~/api/models"; import SzurubooruApi from "~/api"; // Only on dev mode @@ -26,7 +26,7 @@ async function uploadPost(data: PostUploadCommandData) { const pushInfo = () => browser.runtime.sendMessage( - new BrowserCommand("set_post_upload_info", new SetPostUploadInfoData(data.selectedSite.id, data.post.id, info)) + new BrowserCommand("set_post_upload_info", new SetPostUploadInfoData(data.selectedSite.id, data.post.id, info)), ); try { @@ -73,7 +73,7 @@ async function uploadPost(data: PostUploadCommandData) { categoriesChangedCount++; } else { console.log( - `Not adding the '${wantedCategory}' category to the tag '${tags[i].names[0]}' because the szurubooru instance does not have this category.` + `Not adding the '${wantedCategory}' category to the tag '${tags[i].names[0]}' because the szurubooru instance does not have this category.`, ); } } @@ -84,11 +84,39 @@ async function uploadPost(data: PostUploadCommandData) { pushInfo(); } } + + // TODO: This code shouldn't all be in the same try catch. + // Add post to pools + for (const scrapedPool of data.post.pools) { + // Attention! Don't use the .name getter as it does not exist. Just use names[0]. + const existingPools = await szuru.getPools(encodeTagName(scrapedPool.names[0]), 0, 1, ["id", "posts", "version"]); + + if (existingPools.results.length == 0) { + // Pool does not exist. Create a new pool and add the post to it in one API call. + console.log(`Creating new pool ${scrapedPool.names[0]} and adding post ${createdPost.id}.`); + await szuru.createPool(scrapedPool.names[0], "default", [createdPost.id]); + } else { + // Pool exists, so add it to the existing pool. + const existingPool = existingPools.results[0]; + const posts = existingPool.posts.map(x => x.id); + posts.push(createdPost.id); + + console.log(`Adding post ${createdPost.id} to existing pool ${existingPool.id}`); + + const updateRequest = { + version: existingPool.version, + posts, + }; + + await szuru.updatePool(existingPool.id, updateRequest); + } + } } catch (ex: any) { + console.error(ex); if (ex.name && ex.name == "PostAlreadyUploadedError") { const otherPostId = (ex as PostAlreadyUploadedError).otherPostId; browser.runtime.sendMessage( - new BrowserCommand("set_exact_post_id", new SetExactPostId(data.selectedSite.id, data.post.id, otherPostId)) + new BrowserCommand("set_exact_post_id", new SetExactPostId(data.selectedSite.id, data.post.id, otherPostId)), ); // We don't set an error message, because we have a different message for posts that are already uploaded. } else { @@ -110,8 +138,8 @@ async function updatePost(data: PostUpdateCommandData) { browser.runtime.sendMessage( new BrowserCommand( "set_post_update_info", - new SetPostUploadInfoData(data.selectedSite.id, `merge-${data.postId}`, info) - ) + new SetPostUploadInfoData(data.selectedSite.id, `merge-${data.postId}`, info), + ), ); try { @@ -124,6 +152,7 @@ async function updatePost(data: PostUpdateCommandData) { info.state = "uploaded"; pushInfo(); } catch (ex: any) { + console.error(ex); info.state = "error"; info.error = getErrorMessage(ex); pushInfo(); diff --git a/src/components/AutocompleteInput.vue b/src/components/AutocompleteInput.vue new file mode 100644 index 0000000..b860446 --- /dev/null +++ b/src/components/AutocompleteInput.vue @@ -0,0 +1,149 @@ + + + + + diff --git a/src/components/CompactPools.vue b/src/components/CompactPools.vue new file mode 100644 index 0000000..d7fdb0a --- /dev/null +++ b/src/components/CompactPools.vue @@ -0,0 +1,32 @@ + + + + + diff --git a/src/components/CompactTags.vue b/src/components/CompactTags.vue index ef6aed1..28a278a 100644 --- a/src/components/CompactTags.vue +++ b/src/components/CompactTags.vue @@ -24,7 +24,8 @@ defineEmits(["removeTag"]); diff --git a/src/components/PoolInput.vue b/src/components/PoolInput.vue new file mode 100644 index 0000000..351cf73 --- /dev/null +++ b/src/components/PoolInput.vue @@ -0,0 +1,48 @@ + + + diff --git a/src/components/TagInput.vue b/src/components/TagInput.vue index f47bd24..a5eb943 100644 --- a/src/components/TagInput.vue +++ b/src/components/TagInput.vue @@ -1,15 +1,11 @@ - - diff --git a/src/composables/useStorageLocal.ts b/src/composables/useStorageLocal.ts index b972ce0..ae015ff 100644 --- a/src/composables/useStorageLocal.ts +++ b/src/composables/useStorageLocal.ts @@ -20,5 +20,5 @@ const storageLocal: StorageLikeAsync = { export const useStorageLocal = ( key: string, initialValue: MaybeRef, - options?: UseStorageAsyncOptions + options?: UseStorageAsyncOptions, ): RemovableRef => useStorageAsync(key, initialValue, storageLocal, options); diff --git a/src/models/index.ts b/src/models/index.ts index 8a069d5..25f32c0 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -1,10 +1,10 @@ import { BooruTypes, ContentType, ScrapedNote, ScrapedPost, ScrapedTag } from "neo-scraper"; -import { MicroTag, Tag, UpdatePostRequest } from "~/api/models"; +import { MicroTag, Pool, Tag, UpdatePostRequest } from "~/api/models"; export class TagDetails { public implications: TagDetails[] = []; - constructor(public names: string[], public category?: string, public usages?: number) {} + constructor(public names: string[], public category?: string, public usages?: number) { } get name() { return this.names[0]; @@ -29,6 +29,18 @@ export class TagDetails { } } +export class PoolDetails { + constructor(public names: string[], public category?: string, public postCount?: number) { } + + get name() { + return this.names[0]; + } + + static fromPool(pool: Pool) { + return new PoolDetails(pool.names, pool.category, pool.postCount); + } +} + export class InstanceSpecificData { contentToken?: string; genericError?: string; @@ -44,6 +56,7 @@ export class ScrapedPostDetails { id = window.crypto.randomUUID(); name = ""; tags: TagDetails[] = []; + pools: PoolDetails[] = []; notes: ScrapedNote[]; contentUrl: string; extraContentUrl: string | undefined; @@ -83,7 +96,7 @@ export interface SimpleSimilarPost { } export class SimilarPostInfo { - constructor(public readonly id: number, public readonly percentage: number) {} + constructor(public readonly id: number, public readonly percentage: number) { } } export type PostUploadState = "uploading" | "uploaded" | "error"; @@ -119,31 +132,31 @@ export class BrowserCommand { } export class PostUploadCommandData { - constructor(public readonly post: ScrapedPostDetails, public readonly selectedSite: SzuruSiteConfig) {} + constructor(public readonly post: ScrapedPostDetails, public readonly selectedSite: SzuruSiteConfig) { } } export class SetPostUploadInfoData { - constructor(public instanceId: string, public postId: string, public info: PostUploadInfo) {} + constructor(public instanceId: string, public postId: string, public info: PostUploadInfo) { } } export class SetExactPostId { constructor( public readonly instanceId: string, public readonly postId: string, - public readonly exactPostId: number - ) {} + public readonly exactPostId: number, + ) { } } export class PostUpdateCommandData { constructor( public readonly postId: number, public readonly updateRequest: UpdatePostRequest, - public readonly selectedSite: SzuruSiteConfig - ) {} + public readonly selectedSite: SzuruSiteConfig, + ) { } } export class FetchCommandData { - constructor(public readonly url: string, public readonly options: RequestInit | undefined = undefined) {} + constructor(public readonly url: string, public readonly options: RequestInit | undefined = undefined) { } } export class SzuruSiteConfig { @@ -154,7 +167,7 @@ export class SzuruSiteConfig { } export class TagCategoryColor { - constructor(public name: string, public color: string) {} + constructor(public name: string, public color: string) { } } export const getDefaultTagCategories = () => [ diff --git a/src/popup/pages/PopupMain.vue b/src/popup/pages/PopupMain.vue index cf4824f..0e04d84 100644 --- a/src/popup/pages/PopupMain.vue +++ b/src/popup/pages/PopupMain.vue @@ -12,6 +12,7 @@ import { SimpleImageSearchResult, PostUploadCommandData, SzuruSiteConfig, + PoolDetails, } from "~/models"; import { ImageSearchResult } from "~/api/models"; import { isMobile } from "~/env"; @@ -40,7 +41,7 @@ const instanceSpecificData = readonly( if (pop.selectedPost && cfg.value.selectedSiteId) { return pop.selectedPost.instanceSpecificData[cfg.value.selectedSiteId]; } - }) + }), ); watch( @@ -51,7 +52,7 @@ watch( let selectedPost = pop.posts.find((x) => x.id == value); if (selectedPost) findSimilar(selectedPost); } - } + }, ); watch( @@ -66,7 +67,7 @@ watch( findSimilar(pop.selectedPost); } } - } + }, ); function openOptionsPage() { @@ -181,6 +182,15 @@ function removeTag(tag: TagDetails) { } } +function removePool(pool: PoolDetails) { + if (pop.selectedPost) { + const idx = pop.selectedPost.pools.indexOf(pool); + if (idx != -1) { + pop.selectedPost.pools.splice(idx, 1); + } + } +} + function getActiveSitePostUrl(postId: number): string { if (!selectedSite.value) return ""; return getUrl(selectedSite.value.domain, "post", postId.toString()); @@ -216,6 +226,15 @@ function addTag(tag: TagDetails) { } } +function addPool(pool: PoolDetails) { + if (pop.selectedPost) { + // Only add pool if it doesn't already exist + if (pool.name.length > 0 && pop.selectedPost.pools.find((x) => x.name == pool.name) == undefined) { + pop.selectedPost.pools.push(pool); + } + } +} + async function clickFindSimilar() { if (pop.selectedPost) return await findSimilar(pop.selectedPost); } @@ -361,21 +380,20 @@ useDark();
- Merge + Merge
- +
Couldn't upload post. {{ instanceSpecificData.uploadState.error }}
@@ -389,15 +407,10 @@ useDark(); --> -
- Uploaded post {{ instanceSpecificData.uploadState.instancePostId }} +
@@ -410,64 +423,45 @@ useDark();
Uploading...
-
+
{{ instanceSpecificData.uploadState?.updateTagsState?.total }} tags need a different category
-
- Updating tag {{ instanceSpecificData.uploadState?.updateTagsState?.current }}/{{ - instanceSpecificData.uploadState?.updateTagsState?.total - }} +
+ Updating tag {{ instanceSpecificData.uploadState?.updateTagsState?.current }}/{{ + instanceSpecificData.uploadState?.updateTagsState?.total + }}
Searching for similar posts...
- - +
- + +
+
+ + +
+ +
+ +
+
@@ -557,11 +558,8 @@ useDark();
diff --git a/src/stores/index.ts b/src/stores/index.ts index ca47c2d..98a4f49 100644 --- a/src/stores/index.ts +++ b/src/stores/index.ts @@ -30,6 +30,10 @@ export const cfg = useStorageLocal( appendSource: true, mergeSafety: true, }, + popup: { + expandTags: true, + expandPools: false, + }, tagCategories: [] as Array, }, { @@ -56,7 +60,7 @@ export const cfg = useStorageLocal( return cfg; }, - } + }, ); export const usePopupStore = defineStore("popup", { diff --git a/src/styles/main.scss b/src/styles/main.scss index 6cbc822..3c1829a 100644 --- a/src/styles/main.scss +++ b/src/styles/main.scss @@ -141,7 +141,7 @@ ul.compact { display: inline; } -.compact-tags { +.compact-tags, .compact-pools { list-style-type: none; padding: 0; display: inline;