Skip to content

Commit

Permalink
Add pool feature in post upload view
Browse files Browse the repository at this point in the history
  • Loading branch information
neobooru committed Dec 5, 2023
1 parent b7f1ce3 commit c74926b
Show file tree
Hide file tree
Showing 14 changed files with 456 additions and 240 deletions.
2 changes: 2 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,7 @@ module.exports = {
},
],
"@typescript-eslint/no-explicit-any": "off",
"eol-last": ["error", "always"],
"comma-dangle": ["error", "always-multiline"],
},
};
39 changes: 38 additions & 1 deletion src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import {
TagFields,
TemporaryFileUploadResult,
UpdatePostRequest,
PoolsResult,
PoolFields,
Pool,
UpdatePoolRequest,
} from "./models";
import { ScrapedPostDetails, SzuruSiteConfig } from "~/models";

Expand Down Expand Up @@ -63,7 +67,7 @@ export default class SzurubooruApi {
offset = 0,
limit = 100,
fields?: TagFields[],
cancelToken?: CancelToken
cancelToken?: CancelToken,
): Promise<TagsResult> {
const params = new URLSearchParams();
params.append("offset", offset.toString());
Expand Down Expand Up @@ -99,6 +103,39 @@ export default class SzurubooruApi {
return (await this.apiPost("posts", obj)).data;
}

async createPool(name: string, category: string, posts?: number[]): Promise<Pool> {
const obj = <any>{
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<PoolsResult> {
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<Pool> {
return (await this.apiPut("pool/" + id, updateRequest)).data;
}

async reverseSearch(contentUrl: string): Promise<ImageSearchResult> {
const obj = { contentUrl };
return (await this.apiPost("posts/reverse-search", obj)).data;
Expand Down
35 changes: 28 additions & 7 deletions src/api/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Tag>;
export type PoolsResult = PagedSearchResult<Pool>;
export type TagCategoriesResult = UnpagedSearchResult<TagCategory>;

export interface SzuruError {
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -76,7 +92,6 @@ export interface Post {
canvasWidth: number;
canvasHeight: number;
contentUrl: string;
thumbnailUrl: string;
flags: string[];
tags: MicroTag[];
relations: any[]; // MicroPost resource
Expand Down Expand Up @@ -127,3 +142,9 @@ export interface UpdatePostRequest {
contentUrl: string | undefined;
contentToken: string | undefined;
}

export interface UpdatePoolRequest {
version: number;
// names?: string[];
posts: number[];
}
41 changes: 35 additions & 6 deletions src/background/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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.`,
);
}
}
Expand All @@ -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 = <UpdatePoolRequest>{
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 {
Expand All @@ -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 {
Expand All @@ -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();
Expand Down
149 changes: 149 additions & 0 deletions src/components/AutocompleteInput.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
<script setup lang="ts">
import axios, { CancelTokenSource } from "axios";
const inputText = ref("");
const autocompleteShown = ref(false);
const cancelSource = ref<CancelTokenSource | undefined>(undefined);
const autocompleteIndex = ref(-1);
const props = defineProps({
autocompleteItems: {
type: Array<any>,
required: true,
},
});
const emit = defineEmits(["addItem", "addFromCurrentInput", "autocompletePopulator"]);
function addItem(item: any) {
emit("addItem", item);
}
async function onAddItemKeyUp(e: KeyboardEvent) {
await autocompletePopulator((<HTMLInputElement>e.target).value);
}
function addItemFromCurrentInput() {
emit("addFromCurrentInput", inputText.value);
inputText.value = ""; // Reset input
// Only needed when the button is clicked
// When this is triggered by the enter key the `onAddItemKeyUp` will internally also hide the autocomplete.
// Though hiding it twice doesn't hurt so we don't care.
hideAutocomplete();
}
function onAddItemKeyDown(e: KeyboardEvent) {
if (e.code == "ArrowDown") {
e.preventDefault();
if (autocompleteIndex.value < props.autocompleteItems.length - 1) {
autocompleteIndex.value++;
}
} else if (e.code == "ArrowUp") {
e.preventDefault();
if (autocompleteIndex.value >= 0) {
autocompleteIndex.value--;
}
} else if (e.code == "Enter") {
if (autocompleteIndex.value == -1) {
addItemFromCurrentInput();
} else {
// Add auto completed item
const itemToAdd = props.autocompleteItems[autocompleteIndex.value];
addItem(itemToAdd);
inputText.value = ""; // Reset input
}
}
}
function onClickAutocompleteItem(item: any) {
addItem(item);
inputText.value = ""; // Reset input
autocompleteShown.value = false; // Hide autocomplete list
}
function hideAutocomplete() {
autocompleteIndex.value = -1;
autocompleteShown.value = false;
}
async function autocompletePopulator(input: string) {
// Based on https://www.w3schools.com/howto/howto_js_autocomplete.asp
// Hide autocomplete when the input is empty, and don't do anything else.
if (input.length == 0) {
hideAutocomplete();
return;
}
// Cancel previous request, not sure if this still works after the refactor.
if (cancelSource.value) {
cancelSource.value.cancel();
}
cancelSource.value = axios.CancelToken.source();
emit("autocompletePopulator", inputText.value, cancelSource.value.token);
}
watch(props, (newValue) => {
if (newValue.autocompleteItems.length > 0) {
autocompleteShown.value = true;
}
});
</script>

<template>
<div style="display: flex; flex-direction: column">
<div style="display: flex">
<input type="text" v-model="inputText" @keyup="onAddItemKeyUp" @keydown="onAddItemKeyDown" autocomplete="off" />
<button class="primary" style="margin-left: 5px" @click="addItemFromCurrentInput">Add</button>
</div>

<div class="autocomplete-items" v-bind:class="{ show: autocompleteShown }">
<div v-for="(item, idx) in autocompleteItems" @click="onClickAutocompleteItem(item)" :key="item.name" :class="{
active: idx == autocompleteIndex,
}">
<slot :item="item"></slot>
</div>
</div>
</div>
</template>

<style lang="scss">
.autocomplete-items {
position: absolute;
z-index: 10;
background-color: var(--bg-main-color);
border: 2px solid var(--primary-color);
margin-top: 34px;
display: none;
&.show {
display: block;
}
>div {
cursor: pointer;
padding: 2px 4px;
display: flex;
align-items: center;
gap: 0.5em;
&:hover {
background: var(--primary-color);
>span {
color: var(--text-color);
}
}
}
>div.active {
background: var(--primary-color);
>span {
color: var(--text-color);
}
}
}
</style>
Loading

0 comments on commit c74926b

Please sign in to comment.