Skip to content

Commit c74926b

Browse files
committed
Add pool feature in post upload view
1 parent b7f1ce3 commit c74926b

14 files changed

+456
-240
lines changed

.eslintrc.cjs

+2
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,7 @@ module.exports = {
2323
},
2424
],
2525
"@typescript-eslint/no-explicit-any": "off",
26+
"eol-last": ["error", "always"],
27+
"comma-dangle": ["error", "always-multiline"],
2628
},
2729
};

src/api/index.ts

+38-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ import {
1010
TagFields,
1111
TemporaryFileUploadResult,
1212
UpdatePostRequest,
13+
PoolsResult,
14+
PoolFields,
15+
Pool,
16+
UpdatePoolRequest,
1317
} from "./models";
1418
import { ScrapedPostDetails, SzuruSiteConfig } from "~/models";
1519

@@ -63,7 +67,7 @@ export default class SzurubooruApi {
6367
offset = 0,
6468
limit = 100,
6569
fields?: TagFields[],
66-
cancelToken?: CancelToken
70+
cancelToken?: CancelToken,
6771
): Promise<TagsResult> {
6872
const params = new URLSearchParams();
6973
params.append("offset", offset.toString());
@@ -99,6 +103,39 @@ export default class SzurubooruApi {
99103
return (await this.apiPost("posts", obj)).data;
100104
}
101105

106+
async createPool(name: string, category: string, posts?: number[]): Promise<Pool> {
107+
const obj = <any>{
108+
names: [name],
109+
category,
110+
};
111+
112+
if (posts) {
113+
obj.posts = posts;
114+
}
115+
116+
return (await this.apiPost("pool", obj)).data;
117+
}
118+
119+
async getPools(
120+
query?: string,
121+
offset = 0,
122+
limit = 100,
123+
fields?: PoolFields[],
124+
cancelToken?: CancelToken): Promise<PoolsResult> {
125+
const params = new URLSearchParams();
126+
params.append("offset", offset.toString());
127+
params.append("limit", limit.toString());
128+
129+
if (fields && fields.length > 0) params.append("fields", fields.join());
130+
if (query) params.append("query", query);
131+
132+
return (await this.apiGet("pools?" + params.toString(), {}, cancelToken)).data;
133+
}
134+
135+
async updatePool(id: number, updateRequest: UpdatePoolRequest): Promise<Pool> {
136+
return (await this.apiPut("pool/" + id, updateRequest)).data;
137+
}
138+
102139
async reverseSearch(contentUrl: string): Promise<ImageSearchResult> {
103140
const obj = { contentUrl };
104141
return (await this.apiPost("posts/reverse-search", obj)).data;

src/api/models.ts

+28-7
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
*/
44

55
export type Safety = "safe" | "sketchy" | "unsafe";
6-
export type TagFields = "names" | "category" | "usages" | "implications";
6+
export type TagFields = "version" | "names" | "category" | "usages" | "implications";
7+
export type PoolFields = "version" | "id" | "names" | "category" | "description" | "postCount" | "posts";
78

89
export type TagsResult = PagedSearchResult<Tag>;
10+
export type PoolsResult = PagedSearchResult<Pool>;
911
export type TagCategoriesResult = UnpagedSearchResult<TagCategory>;
1012

1113
export interface SzuruError {
@@ -37,18 +39,28 @@ export interface MicroTag {
3739
usages: number;
3840
}
3941

40-
export interface Tag {
41-
names: string[];
42-
category: string;
42+
export interface Tag extends MicroTag {
4343
version: number;
4444
description?: string; // Markdown
4545
creationTime: Date;
4646
lastEditTime?: Date;
47-
usages: number;
4847
suggestions: MicroTag[];
4948
implications: MicroTag[];
5049
}
5150

51+
/**
52+
* All fields can be optional.
53+
*/
54+
export interface Pool {
55+
id: number;
56+
names: string[];
57+
category: string;
58+
version: number;
59+
description: null;
60+
postCount: number;
61+
posts: MicroPost[];
62+
}
63+
5264
export interface TagCategory {
5365
name: string;
5466
version: number;
@@ -62,8 +74,12 @@ export interface MicroUser {
6274
avatarUrl: string;
6375
}
6476

65-
export interface Post {
77+
export interface MicroPost {
6678
id: number;
79+
thumbnailUrl: string;
80+
}
81+
82+
export interface Post extends MicroPost {
6783
version: number;
6884
creationTime: Date;
6985
lastEditTime?: Date;
@@ -76,7 +92,6 @@ export interface Post {
7692
canvasWidth: number;
7793
canvasHeight: number;
7894
contentUrl: string;
79-
thumbnailUrl: string;
8095
flags: string[];
8196
tags: MicroTag[];
8297
relations: any[]; // MicroPost resource
@@ -127,3 +142,9 @@ export interface UpdatePostRequest {
127142
contentUrl: string | undefined;
128143
contentToken: string | undefined;
129144
}
145+
146+
export interface UpdatePoolRequest {
147+
version: number;
148+
// names?: string[];
149+
posts: number[];
150+
}

src/background/main.ts

+35-6
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
PostUpdateCommandData,
99
FetchCommandData,
1010
} from "~/models";
11-
import { PostAlreadyUploadedError } from "~/api/models";
11+
import { PostAlreadyUploadedError, UpdatePoolRequest } from "~/api/models";
1212
import SzurubooruApi from "~/api";
1313

1414
// Only on dev mode
@@ -26,7 +26,7 @@ async function uploadPost(data: PostUploadCommandData) {
2626

2727
const pushInfo = () =>
2828
browser.runtime.sendMessage(
29-
new BrowserCommand("set_post_upload_info", new SetPostUploadInfoData(data.selectedSite.id, data.post.id, info))
29+
new BrowserCommand("set_post_upload_info", new SetPostUploadInfoData(data.selectedSite.id, data.post.id, info)),
3030
);
3131

3232
try {
@@ -73,7 +73,7 @@ async function uploadPost(data: PostUploadCommandData) {
7373
categoriesChangedCount++;
7474
} else {
7575
console.log(
76-
`Not adding the '${wantedCategory}' category to the tag '${tags[i].names[0]}' because the szurubooru instance does not have this category.`
76+
`Not adding the '${wantedCategory}' category to the tag '${tags[i].names[0]}' because the szurubooru instance does not have this category.`,
7777
);
7878
}
7979
}
@@ -84,11 +84,39 @@ async function uploadPost(data: PostUploadCommandData) {
8484
pushInfo();
8585
}
8686
}
87+
88+
// TODO: This code shouldn't all be in the same try catch.
89+
// Add post to pools
90+
for (const scrapedPool of data.post.pools) {
91+
// Attention! Don't use the .name getter as it does not exist. Just use names[0].
92+
const existingPools = await szuru.getPools(encodeTagName(scrapedPool.names[0]), 0, 1, ["id", "posts", "version"]);
93+
94+
if (existingPools.results.length == 0) {
95+
// Pool does not exist. Create a new pool and add the post to it in one API call.
96+
console.log(`Creating new pool ${scrapedPool.names[0]} and adding post ${createdPost.id}.`);
97+
await szuru.createPool(scrapedPool.names[0], "default", [createdPost.id]);
98+
} else {
99+
// Pool exists, so add it to the existing pool.
100+
const existingPool = existingPools.results[0];
101+
const posts = existingPool.posts.map(x => x.id);
102+
posts.push(createdPost.id);
103+
104+
console.log(`Adding post ${createdPost.id} to existing pool ${existingPool.id}`);
105+
106+
const updateRequest = <UpdatePoolRequest>{
107+
version: existingPool.version,
108+
posts,
109+
};
110+
111+
await szuru.updatePool(existingPool.id, updateRequest);
112+
}
113+
}
87114
} catch (ex: any) {
115+
console.error(ex);
88116
if (ex.name && ex.name == "PostAlreadyUploadedError") {
89117
const otherPostId = (ex as PostAlreadyUploadedError).otherPostId;
90118
browser.runtime.sendMessage(
91-
new BrowserCommand("set_exact_post_id", new SetExactPostId(data.selectedSite.id, data.post.id, otherPostId))
119+
new BrowserCommand("set_exact_post_id", new SetExactPostId(data.selectedSite.id, data.post.id, otherPostId)),
92120
);
93121
// We don't set an error message, because we have a different message for posts that are already uploaded.
94122
} else {
@@ -110,8 +138,8 @@ async function updatePost(data: PostUpdateCommandData) {
110138
browser.runtime.sendMessage(
111139
new BrowserCommand(
112140
"set_post_update_info",
113-
new SetPostUploadInfoData(data.selectedSite.id, `merge-${data.postId}`, info)
114-
)
141+
new SetPostUploadInfoData(data.selectedSite.id, `merge-${data.postId}`, info),
142+
),
115143
);
116144

117145
try {
@@ -124,6 +152,7 @@ async function updatePost(data: PostUpdateCommandData) {
124152
info.state = "uploaded";
125153
pushInfo();
126154
} catch (ex: any) {
155+
console.error(ex);
127156
info.state = "error";
128157
info.error = getErrorMessage(ex);
129158
pushInfo();

src/components/AutocompleteInput.vue

+149
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
<script setup lang="ts">
2+
import axios, { CancelTokenSource } from "axios";
3+
4+
const inputText = ref("");
5+
const autocompleteShown = ref(false);
6+
const cancelSource = ref<CancelTokenSource | undefined>(undefined);
7+
const autocompleteIndex = ref(-1);
8+
9+
const props = defineProps({
10+
autocompleteItems: {
11+
type: Array<any>,
12+
required: true,
13+
},
14+
});
15+
const emit = defineEmits(["addItem", "addFromCurrentInput", "autocompletePopulator"]);
16+
17+
function addItem(item: any) {
18+
emit("addItem", item);
19+
}
20+
21+
async function onAddItemKeyUp(e: KeyboardEvent) {
22+
await autocompletePopulator((<HTMLInputElement>e.target).value);
23+
}
24+
25+
function addItemFromCurrentInput() {
26+
emit("addFromCurrentInput", inputText.value);
27+
inputText.value = ""; // Reset input
28+
29+
// Only needed when the button is clicked
30+
// When this is triggered by the enter key the `onAddItemKeyUp` will internally also hide the autocomplete.
31+
// Though hiding it twice doesn't hurt so we don't care.
32+
hideAutocomplete();
33+
}
34+
35+
function onAddItemKeyDown(e: KeyboardEvent) {
36+
if (e.code == "ArrowDown") {
37+
e.preventDefault();
38+
if (autocompleteIndex.value < props.autocompleteItems.length - 1) {
39+
autocompleteIndex.value++;
40+
}
41+
} else if (e.code == "ArrowUp") {
42+
e.preventDefault();
43+
if (autocompleteIndex.value >= 0) {
44+
autocompleteIndex.value--;
45+
}
46+
} else if (e.code == "Enter") {
47+
if (autocompleteIndex.value == -1) {
48+
addItemFromCurrentInput();
49+
} else {
50+
// Add auto completed item
51+
const itemToAdd = props.autocompleteItems[autocompleteIndex.value];
52+
addItem(itemToAdd);
53+
inputText.value = ""; // Reset input
54+
}
55+
}
56+
}
57+
58+
function onClickAutocompleteItem(item: any) {
59+
addItem(item);
60+
inputText.value = ""; // Reset input
61+
autocompleteShown.value = false; // Hide autocomplete list
62+
}
63+
64+
function hideAutocomplete() {
65+
autocompleteIndex.value = -1;
66+
autocompleteShown.value = false;
67+
}
68+
69+
async function autocompletePopulator(input: string) {
70+
// Based on https://www.w3schools.com/howto/howto_js_autocomplete.asp
71+
72+
// Hide autocomplete when the input is empty, and don't do anything else.
73+
if (input.length == 0) {
74+
hideAutocomplete();
75+
return;
76+
}
77+
78+
// Cancel previous request, not sure if this still works after the refactor.
79+
if (cancelSource.value) {
80+
cancelSource.value.cancel();
81+
}
82+
cancelSource.value = axios.CancelToken.source();
83+
84+
emit("autocompletePopulator", inputText.value, cancelSource.value.token);
85+
}
86+
87+
watch(props, (newValue) => {
88+
if (newValue.autocompleteItems.length > 0) {
89+
autocompleteShown.value = true;
90+
}
91+
});
92+
</script>
93+
94+
<template>
95+
<div style="display: flex; flex-direction: column">
96+
<div style="display: flex">
97+
<input type="text" v-model="inputText" @keyup="onAddItemKeyUp" @keydown="onAddItemKeyDown" autocomplete="off" />
98+
<button class="primary" style="margin-left: 5px" @click="addItemFromCurrentInput">Add</button>
99+
</div>
100+
101+
<div class="autocomplete-items" v-bind:class="{ show: autocompleteShown }">
102+
<div v-for="(item, idx) in autocompleteItems" @click="onClickAutocompleteItem(item)" :key="item.name" :class="{
103+
active: idx == autocompleteIndex,
104+
}">
105+
<slot :item="item"></slot>
106+
</div>
107+
</div>
108+
</div>
109+
</template>
110+
111+
<style lang="scss">
112+
.autocomplete-items {
113+
position: absolute;
114+
z-index: 10;
115+
background-color: var(--bg-main-color);
116+
border: 2px solid var(--primary-color);
117+
margin-top: 34px;
118+
display: none;
119+
120+
&.show {
121+
display: block;
122+
}
123+
124+
>div {
125+
cursor: pointer;
126+
padding: 2px 4px;
127+
128+
display: flex;
129+
align-items: center;
130+
gap: 0.5em;
131+
132+
&:hover {
133+
background: var(--primary-color);
134+
135+
>span {
136+
color: var(--text-color);
137+
}
138+
}
139+
}
140+
141+
>div.active {
142+
background: var(--primary-color);
143+
144+
>span {
145+
color: var(--text-color);
146+
}
147+
}
148+
}
149+
</style>

0 commit comments

Comments
 (0)