diff --git a/client/css/post-upload.styl b/client/css/post-upload.styl
index aa36e0a0..38362937 100644
--- a/client/css/post-upload.styl
+++ b/client/css/post-upload.styl
@@ -13,8 +13,10 @@ $cancel-button-color = tomato
&.inactive input[type=submit],
&.inactive .skip-duplicates
+ &.inactive .always-upload-similar
&.uploading input[type=submit],
&.uploading .skip-duplicates,
+ &.uploading .always-upload-similar
&:not(.uploading) .cancel
display: none
@@ -39,6 +41,9 @@ $cancel-button-color = tomato
.skip-duplicates
margin-left: 1em
+ .always-upload-similar
+ margin-left: 1em
+
form>.messages
margin-top: 1em
diff --git a/client/html/post_upload.tpl b/client/html/post_upload.tpl
index f1ed88fc..6374fe8c 100644
--- a/client/html/post_upload.tpl
+++ b/client/html/post_upload.tpl
@@ -13,6 +13,14 @@
}) %>
+
+ <%= ctx.makeCheckbox({
+ text: 'Always upload similar',
+ name: 'always-upload-similar',
+ checked: false,
+ }) %>
+
+
diff --git a/client/html/post_upload_row.tpl b/client/html/post_upload_row.tpl
index c7cc5dba..2885e3d7 100644
--- a/client/html/post_upload_row.tpl
+++ b/client/html/post_upload_row.tpl
@@ -61,6 +61,7 @@
text: 'Upload anonymously',
name: 'anonymous',
checked: ctx.uploadable.anonymous,
+ readonly: ctx.uploadable.forceAnonymous,
}) %>
<% } %>
diff --git a/client/js/controllers/home_controller.js b/client/js/controllers/home_controller.js
index ea2a411c..528ce622 100644
--- a/client/js/controllers/home_controller.js
+++ b/client/js/controllers/home_controller.js
@@ -17,7 +17,7 @@ class HomeController {
buildDate: config.meta.buildDate,
canListSnapshots: api.hasPrivilege("snapshots:list"),
canListPosts: api.hasPrivilege("posts:list"),
- isDevelopmentMode: config.environment == "development"
+ isDevelopmentMode: config.environment == "development",
});
Info.get().then(
diff --git a/client/js/controllers/post_upload_controller.js b/client/js/controllers/post_upload_controller.js
index d317be59..a54baec7 100644
--- a/client/js/controllers/post_upload_controller.js
+++ b/client/js/controllers/post_upload_controller.js
@@ -12,7 +12,7 @@ const PostUploadView = require("../views/post_upload_view.js");
const EmptyView = require("../views/empty_view.js");
const genericErrorMessage =
- "One of the posts needs your attention; " +
+ "One or more posts needs your attention; " +
'click "resume upload" when you\'re ready.';
class PostUploadController {
@@ -55,6 +55,7 @@ class PostUploadController {
_evtSubmit(e) {
this._view.disableForm();
this._view.clearMessages();
+ let anyFailures = false;
e.detail.uploadables
.reduce(
@@ -62,44 +63,51 @@ class PostUploadController {
promise.then(() =>
this._uploadSinglePost(
uploadable,
- e.detail.skipDuplicates
- )
+ e.detail.skipDuplicates,
+ e.detail.alwaysUploadSimilar
+ ).catch((error) => {
+ anyFailures = true;
+ if (error.uploadable) {
+ if (error.similarPosts) {
+ error.uploadable.lookalikes =
+ error.similarPosts;
+ this._view.updateUploadable(
+ error.uploadable
+ );
+ this._view.showInfo(
+ error.message,
+ error.uploadable
+ );
+ } else {
+ this._view.showError(
+ error.message,
+ error.uploadable
+ );
+ }
+ } else {
+ this._view.showError(
+ error.message,
+ uploadable
+ );
+ }
+ })
),
Promise.resolve()
)
- .then(
- () => {
+ .then(() => {
+ if (anyFailures) {
+ this._view.showError(genericErrorMessage);
+ this._view.enableForm();
+ } else {
this._view.clearMessages();
misc.disableExitConfirmation();
const ctx = router.show(uri.formatClientLink("posts"));
ctx.controller.showSuccess("Posts uploaded.");
- },
- (error) => {
- if (error.uploadable) {
- if (error.similarPosts) {
- error.uploadable.lookalikes = error.similarPosts;
- this._view.updateUploadable(error.uploadable);
- this._view.showInfo(genericErrorMessage);
- this._view.showInfo(
- error.message,
- error.uploadable
- );
- } else {
- this._view.showError(genericErrorMessage);
- this._view.showError(
- error.message,
- error.uploadable
- );
- }
- } else {
- this._view.showError(error.message);
- }
- this._view.enableForm();
}
- );
+ });
}
- _uploadSinglePost(uploadable, skipDuplicates) {
+ _uploadSinglePost(uploadable, skipDuplicates, alwaysUploadSimilar) {
progress.start();
let reverseSearchPromise = Promise.resolve();
if (!uploadable.lookalikesConfirmed) {
@@ -128,7 +136,10 @@ class PostUploadController {
}
// notify about similar posts
- if (searchResult.similarPosts.length) {
+ if (
+ searchResult.similarPosts.length &&
+ !alwaysUploadSimilar
+ ) {
let error = new Error(
`Found ${searchResult.similarPosts.length} similar ` +
"posts.\nYou can resume or discard this upload."
diff --git a/client/js/controls/tag_input_control.js b/client/js/controls/tag_input_control.js
index a383d449..cca58d5b 100644
--- a/client/js/controls/tag_input_control.js
+++ b/client/js/controls/tag_input_control.js
@@ -196,11 +196,9 @@ class TagInputControl extends events.EventTarget {
const listItemNode = this._createListItemNode(tag);
if (!tag.category) {
listItemNode.classList.add("new");
- }
- else if (source === SOURCE_IMPLICATION) {
+ } else if (source === SOURCE_IMPLICATION) {
listItemNode.classList.add("implication");
- }
- else {
+ } else {
listItemNode.classList.add("added");
}
this._tagListNode.prependChild(listItemNode);
diff --git a/client/js/main.js b/client/js/main.js
index 2be2cd53..c5bdc537 100644
--- a/client/js/main.js
+++ b/client/js/main.js
@@ -4,12 +4,12 @@ const config = require("./config.js");
if (config.environment == "development") {
var ws = new WebSocket("ws://" + location.hostname + ":8080");
- ws.addEventListener('open', function (event) {
+ ws.addEventListener("open", function (event) {
console.log("Live-reloading websocket connected.");
});
- ws.addEventListener('message', (event) => {
+ ws.addEventListener("message", (event) => {
console.log(event);
- if (event.data == 'reload'){
+ if (event.data == "reload") {
location.reload();
}
});
diff --git a/client/js/views/post_upload_view.js b/client/js/views/post_upload_view.js
index ce0cb426..7fdd4a4f 100644
--- a/client/js/views/post_upload_view.js
+++ b/client/js/views/post_upload_view.js
@@ -1,6 +1,7 @@
"use strict";
const events = require("../events.js");
+const api = require("../api.js");
const views = require("../util/views.js");
const FileDropperControl = require("../controls/file_dropper_control.js");
@@ -34,7 +35,8 @@ class Uploadable extends events.EventTarget {
this.flags = [];
this.tags = [];
this.relations = [];
- this.anonymous = false;
+ this.anonymous = !api.isLoggedIn();
+ this.forceAnonymous = !api.isLoggedIn();
}
destroy() {}
@@ -358,6 +360,8 @@ class PostUploadView extends events.EventTarget {
detail: {
uploadables: this._uploadables,
skipDuplicates: this._skipDuplicatesCheckboxNode.checked,
+ alwaysUploadSimilar: this._alwaysUploadSimilarCheckboxNode
+ .checked,
},
})
);
@@ -421,6 +425,12 @@ class PostUploadView extends events.EventTarget {
return this._hostNode.querySelector("form [name=skip-duplicates]");
}
+ get _alwaysUploadSimilarCheckboxNode() {
+ return this._hostNode.querySelector(
+ "form [name=always-upload-similar]"
+ );
+ }
+
get _submitButtonNode() {
return this._hostNode.querySelector("form [type=submit]");
}
diff --git a/server/szurubooru/func/image_hash.py b/server/szurubooru/func/image_hash.py
index a445e62d..05b27a42 100644
--- a/server/szurubooru/func/image_hash.py
+++ b/server/szurubooru/func/image_hash.py
@@ -5,14 +5,15 @@ from io import BytesIO
from typing import Any, Callable, List, Optional, Set, Tuple
import numpy as np
-from PIL import Image
import pillow_avif
import pyheif
+from PIL import Image
from pyheif_pillow_opener import register_heif_opener
-register_heif_opener()
from szurubooru import config, errors
+register_heif_opener()
+
logger = logging.getLogger(__name__)
# Math based on paper from H. Chi Wong, Marshall Bern and David Goldberg
diff --git a/server/szurubooru/func/images.py b/server/szurubooru/func/images.py
index 101bba8c..de41222f 100644
--- a/server/szurubooru/func/images.py
+++ b/server/szurubooru/func/images.py
@@ -6,6 +6,7 @@ import shlex
import subprocess
from io import BytesIO
from typing import List
+
from PIL import Image as PILImage
from szurubooru import errors
@@ -17,7 +18,7 @@ logger = logging.getLogger(__name__)
def convert_heif_to_png(content: bytes) -> bytes:
img = PILImage.open(BytesIO(content))
img_byte_arr = BytesIO()
- img.save(img_byte_arr, format='PNG')
+ img.save(img_byte_arr, format="PNG")
return img_byte_arr.getvalue()
diff --git a/server/szurubooru/func/mime.py b/server/szurubooru/func/mime.py
index 93c096b1..3be43f77 100644
--- a/server/szurubooru/func/mime.py
+++ b/server/szurubooru/func/mime.py
@@ -88,6 +88,7 @@ def is_animated_gif(content: bytes) -> bool:
and len(re.findall(pattern, content)) > 1
)
+
def is_heif(mime_type: str) -> bool:
return mime_type.lower() in (
"image/heif",