bring back the badge editor
This commit is contained in:
14
app/db.py
14
app/db.py
@@ -49,6 +49,12 @@ class DB:
|
||||
yield conn
|
||||
|
||||
|
||||
@staticmethod
|
||||
def binding_list(num: int) -> str:
|
||||
"""Returns a bindings list string for the given number of bindings."""
|
||||
return '(%s)' % ','.join('?' * num)
|
||||
|
||||
|
||||
def query(self, sql, *args):
|
||||
"""Executes a query and returns a list of dictionaries."""
|
||||
with self.connection() as conn:
|
||||
@@ -104,8 +110,12 @@ class DB:
|
||||
conditions = []
|
||||
params = []
|
||||
for col, op, val in self._where:
|
||||
conditions.append(f"{col} {op} ?")
|
||||
params.append(val)
|
||||
if isinstance(val, tuple) or isinstance(val, list):
|
||||
conditions.append(f"{col} {op} {db.binding_list(len(val))}")
|
||||
params.extend(val)
|
||||
else:
|
||||
conditions.append(f"{col} {op} ?")
|
||||
params.append(val)
|
||||
|
||||
return " WHERE " + " AND ".join(conditions), params
|
||||
|
||||
|
||||
@@ -648,6 +648,12 @@ class BadgeUploads(Model):
|
||||
class Badges(Model):
|
||||
table = 'badges'
|
||||
|
||||
@classmethod
|
||||
def get_for_user(cls, user_id):
|
||||
q = 'SELECT * FROM badges WHERE user_id = ? ORDER BY sort_order ASC'
|
||||
res = db.query(q, user_id)
|
||||
return [cls.from_data(row) for row in res]
|
||||
|
||||
def get_image_url(self):
|
||||
bu = BadgeUploads.find({'id': int(self.upload)})
|
||||
return bu.file_path
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from flask import Blueprint, render_template, request, url_for
|
||||
from ..auth import get_active_user, is_logged_in, hard_login_required
|
||||
from ..models import BookmarkCollections, BookmarkedPosts, BookmarkedThreads, Threads, Posts
|
||||
from ..models import BookmarkCollections, BookmarkedPosts, BookmarkedThreads, Threads, Posts, Badges, BadgeUploads
|
||||
from functools import wraps
|
||||
|
||||
bp = Blueprint('hyperapi', __name__, url_prefix='/hyperapi/')
|
||||
@@ -124,3 +124,12 @@ def bookmark_post():
|
||||
})
|
||||
|
||||
return '', 204
|
||||
|
||||
@bp.get('/badges/editor/')
|
||||
@hard_login_required
|
||||
@user_required
|
||||
def badge_editor():
|
||||
user = get_active_user()
|
||||
badges = Badges.get_for_user(user.id)
|
||||
badge_uploads = BadgeUploads.get_for_user(user.id)
|
||||
return render_template('hyper/badge_editor.html', badges=badges, badge_uploads=badge_uploads)
|
||||
|
||||
@@ -14,7 +14,7 @@ from ..auth import (
|
||||
login_required, revoke_session, get_active_user,
|
||||
parse_display_name, revoke_all_sessions, csrf_verified
|
||||
)
|
||||
from ..models import Users, Posts, Reactions, Threads, Avatars, PostHistory, Mentions, BookmarkCollections, InviteKeys
|
||||
from ..models import Users, Posts, Reactions, Threads, Avatars, PostHistory, Mentions, BookmarkCollections, InviteKeys, Badges, BadgeUploads
|
||||
from ..constants import PermissionLevel, InfoboxKind
|
||||
from ..util import get_form_checkbox, time_now
|
||||
from ..lib.babycode import babycode_to_html
|
||||
@@ -24,6 +24,7 @@ import os
|
||||
import time
|
||||
|
||||
AVATAR_MAX_SIZE = 1000 * 1000 # 1MB
|
||||
BADGE_MAX_SIZE = 1000 * 500 # 500K
|
||||
|
||||
bp = Blueprint('users', __name__, url_prefix='/users/')
|
||||
|
||||
@@ -60,6 +61,22 @@ def validate_and_create_avatar(input_image, filename):
|
||||
except WandException:
|
||||
return False
|
||||
|
||||
def validate_and_create_badge(input_image, filename):
|
||||
try:
|
||||
with Image(blob=input_image) as img:
|
||||
if img.width != 88 or img.height != 31:
|
||||
return False
|
||||
if hasattr(img, 'sequence') and len(img.sequence) > 1:
|
||||
img = Image(image=img.sequence[0])
|
||||
img.strip()
|
||||
|
||||
img.format = 'webp'
|
||||
img.compression_quality = 90
|
||||
img.save(filename=filename)
|
||||
return True
|
||||
except WandException:
|
||||
return False
|
||||
|
||||
def anonymize_user(user_id):
|
||||
deleted_user = Users.find({'username': 'deleteduser'})
|
||||
|
||||
@@ -282,6 +299,7 @@ def posts(username):
|
||||
'users/posts.html', posts=posts,
|
||||
page=page, page_count=page_count,
|
||||
target_user=target_user,
|
||||
Reactions=Reactions,
|
||||
)
|
||||
|
||||
@bp.get('/<username>/threads/')
|
||||
@@ -305,6 +323,7 @@ def threads(username):
|
||||
'users/threads.html', threads=threads,
|
||||
page=page, page_count=page_count,
|
||||
target_user=target_user,
|
||||
Reactions=Reactions,
|
||||
)
|
||||
|
||||
@bp.get('/<username>/comments/')
|
||||
@@ -611,3 +630,107 @@ def revoke_invite_key(username):
|
||||
|
||||
invite.delete()
|
||||
return redirect(url_for('.settings', username=username, _anchor='invite'))
|
||||
|
||||
@bp.post('/<username>/settings/badges/')
|
||||
@login_required
|
||||
@redirect_to_own
|
||||
def save_badges(username):
|
||||
user = get_active_user()
|
||||
if user.is_guest():
|
||||
abort(403)
|
||||
|
||||
ids = request.form.getlist('id[]', type=int)
|
||||
badge_choices = request.form.getlist('badge_choice[]')
|
||||
files = request.files.getlist('badge_file[]')
|
||||
labels = request.form.getlist('label[]')
|
||||
links = request.form.getlist('link[]')
|
||||
|
||||
existing_badges = {badge.id: badge for badge in Badges.findall({'user_id': user.id})}
|
||||
|
||||
if not (len(ids) == len(badge_choices) == len(files) == len(labels) == len(links)):
|
||||
abort(400)
|
||||
|
||||
rejected_badges = []
|
||||
# print(ids)
|
||||
|
||||
# print(db.query(f'SELECT id FROM badges WHERE id NOT IN {db.binding_list(len(ids))}', *ids))
|
||||
deleted_badges = Badges.findall([
|
||||
('id', 'NOT IN', ids),
|
||||
('user_id', '=', user.id),
|
||||
])
|
||||
print(list(map(lambda x: x.id, deleted_badges)))
|
||||
|
||||
with db.transaction():
|
||||
for b in deleted_badges:
|
||||
b.delete()
|
||||
|
||||
for i, id in enumerate(ids):
|
||||
badge_upload_id = badge_choices[i]
|
||||
label = labels[i]
|
||||
link = links[i]
|
||||
pending_badge = {
|
||||
'label': label,
|
||||
'link': link,
|
||||
'sort_order': i,
|
||||
}
|
||||
if badge_upload_id == 'custom':
|
||||
file = files[i]
|
||||
if not file:
|
||||
rejected_badges.append(file.filename)
|
||||
continue
|
||||
file.seek(0, os.SEEK_END)
|
||||
file_size = file.tell()
|
||||
file.seek(0, os.SEEK_SET)
|
||||
|
||||
if file_size > BADGE_MAX_SIZE:
|
||||
rejected_badges.append(file.filename)
|
||||
continue
|
||||
|
||||
file_bytes = file.read()
|
||||
now = time_now()
|
||||
filename = f'u{user.id}d{now}s{i}.webp'
|
||||
output_path = os.path.join(current_app.config['BADGES_UPLOAD_PATH'], filename)
|
||||
proxied_filename = f'/static/badges/user/{filename}'
|
||||
res = validate_and_create_badge(file_bytes, output_path)
|
||||
if not res:
|
||||
rejected_badges.append(file.filename)
|
||||
continue
|
||||
|
||||
bu = BadgeUploads.create({
|
||||
'user_id': user.id,
|
||||
'uploaded_at': now,
|
||||
'file_path': proxied_filename,
|
||||
'original_filename': file.filename
|
||||
})
|
||||
else:
|
||||
bu = BadgeUploads.find({'id': badge_upload_id})
|
||||
if not bu:
|
||||
continue
|
||||
|
||||
pending_badge['upload'] = bu.id
|
||||
|
||||
if id == -1:
|
||||
pending_badge['user_id'] = user.id
|
||||
badge = Badges.create(pending_badge)
|
||||
else:
|
||||
badge = Badges.find({'id': id})
|
||||
if badge.user_id != user.id:
|
||||
continue
|
||||
if not badge:
|
||||
continue
|
||||
badge.update(pending_badge)
|
||||
|
||||
for stale_upload in BadgeUploads.get_unused_for_user(user.id):
|
||||
filename = os.path.join(current_app.config['BADGES_UPLOAD_PATH'], os.path.basename(stale_upload.file_path))
|
||||
os.remove(filename)
|
||||
stale_upload.delete()
|
||||
|
||||
message = 'Badges updated.'
|
||||
icon = InfoboxKind.INFO
|
||||
if rejected_badges:
|
||||
message += f';Some of your badges were incorrect and were not uploaded: {", ".join(rejected_badges)}.'
|
||||
icon = InfoboxKind.WARN
|
||||
|
||||
flash(message, icon)
|
||||
|
||||
return redirect(url_for('.settings', username=username))
|
||||
|
||||
@@ -161,7 +161,7 @@
|
||||
<a href="{{url_for('users.user_page', username=post.username)}}" class="usercard-username">{{post.display_name if post.display_name else post.username}}</a>
|
||||
<abbr title="mention">@{{post.username}}</abbr>
|
||||
<i>{{post.status}}</i>
|
||||
{%- set badges=post.badges_json | fromjson -%}
|
||||
{%- set badges=post.badges_json | fromjson | sort(attribute='sort_order') -%}
|
||||
<div class="badges-container">
|
||||
{%- for badge in badges -%}
|
||||
{%- if badge.link -%}<a href="{{badge.link}}">{%- endif -%}
|
||||
@@ -285,16 +285,16 @@
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro sortable_list(attr=none) -%}
|
||||
<ol class="sortable-list plank even no-shadow minimal tertiary-bg" {% if attr %}{{ dict_to_attr(attr) }}{% endif %}>
|
||||
<ol class="sortable-list" {% if attr %}{{ dict_to_attr(attr) }}{% endif %}>
|
||||
{%- if caller -%}
|
||||
{{ caller() }}
|
||||
{%- endif -%}
|
||||
</ol>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro sortable_list_item(key, immovable=false, attr=none) -%}
|
||||
{% macro sortable_list_item(key, immovable=false, attr=none, full=false) -%}
|
||||
<li class="sortable-item{{ ' immovable' if immovable else '' }} plank even no-shadow {{'secondary-bg' if immovable else ''}}" data-sortable-list-key="{{key}}" {% if attr %}{{ dict_to_attr(attr) }}{% endif %}>
|
||||
<span class="dragger plank minimal even no-shadow tertiary-bg" draggable="{{ 'true' if not immovable else 'false' }}">{{ icn_dragger() }}</span>
|
||||
<div class="sortable-item-inner">{{ caller() }}</div>
|
||||
<div class="sortable-item-inner {{full and 'full' or ''}}">{{ caller() }}</div>
|
||||
</li>
|
||||
{%- endmacro %}
|
||||
|
||||
51
app/templates/hyper/badge_editor.html
Normal file
51
app/templates/hyper/badge_editor.html
Normal file
@@ -0,0 +1,51 @@
|
||||
{%- macro badge_input(uploads, label='', link='', selected=none, id=none) -%}
|
||||
{%- set defaults = uploads | selectattr('user_id', 'none') | list | sort(attribute='file_path') -%}
|
||||
{%- set user = uploads | selectattr('user_id') | list -%}
|
||||
{%- if selected is not none -%}
|
||||
{%- set selected_href = (uploads | selectattr('id', 'equalto', selected) | list)[0].file_path -%}
|
||||
{%- else -%}
|
||||
{% set selected_href = defaults[0].file_path %}
|
||||
{%- endif -%}
|
||||
<input type="hidden" name="id[]" value="{{id and id or '-1'}}">
|
||||
<div class="badge-editor-badge-container">
|
||||
<div class="badge-editor-badge-select">
|
||||
<select name="badge_choice[]" required data-s="badgeEditorSetPreview badgeEditorToggleFilePicker" data-listen="change">
|
||||
<optgroup label="Default">
|
||||
{%- for upload in defaults -%}
|
||||
<option data-file-path="{{upload.file_path}}" value="{{upload.id}}" {{selected==upload.id and 'selected' or ''}}>{{upload.file_path | basename_noext}}</option>
|
||||
{%- endfor -%}
|
||||
</optgroup>
|
||||
<optgroup label="Your uploads">
|
||||
{%- for upload in user -%}
|
||||
<option data-file-path="{{upload.file_path}}" value="{{upload.id}}" {{selected==upload.id and 'selected' or ''}}>{{upload.original_filename | basename_noext}}</option>
|
||||
{%- endfor -%}
|
||||
<option value="custom">Upload new…</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
<img class="badge-button" src="{{selected_href}}" data-r="badgeEditorSetPreview badgeEditorSetPreviewCustom">
|
||||
</div>
|
||||
<div class="badge-editor-file-picker hidden" data-r="badgeEditorToggleFilePicker">
|
||||
<button data-s="badgeEditorShowFilePicker" type="button" class="alt">Upload…</button>
|
||||
<input data-s="badgeEditorFileSelected" data-r="badgeEditorShowFilePicker" type="file" accept="image/png, image/jpeg, image/jpg, image/webp" name="badge_file[]">
|
||||
</div>
|
||||
<input type="text" required placeholder="Label" value="{{label}}" autocomplete="off" name="label[]">
|
||||
<input type="url" placeholder="(Optional) Link" value="{{link}}" autocomplete="off" name="link[]" pattern="https://.*">
|
||||
<button type="button" class="critical" data-s="badgeEditorDelete">Delete</button>
|
||||
</div>
|
||||
{%- endmacro -%}
|
||||
{%- from 'common/macros.html' import sortable_list, sortable_list_item -%}
|
||||
<button type="button" data-s="badgeEditorAddBadge" data-r="setBadgeCount">Add badge</button>
|
||||
<input type="submit" value="Save badges">
|
||||
<span data-r="setBadgeCount">0/10</span>
|
||||
{%- call() sortable_list(attr={'data-r': 'badgeEditorAddBadge'}) -%}
|
||||
{%- for badge in badges -%}
|
||||
{%- call() sortable_list_item('badge', full=true, attr={'data-r': 'badgeEditorDelete badgeEditorAssignImgId'}) -%}
|
||||
{{badge_input(badge_uploads, badge.label, badge.link, badge.upload, badge.id)}}
|
||||
{%- endcall -%}
|
||||
{%- endfor -%}
|
||||
{%- endcall -%}
|
||||
<script type="text/html" id="badge-template">
|
||||
{%- call() sortable_list_item('badge', full=true, attr={'data-r': 'badgeEditorDelete'}) -%}
|
||||
{{- badge_input(badge_uploads) -}}
|
||||
{%- endcall -%}
|
||||
</script>
|
||||
@@ -72,9 +72,12 @@
|
||||
</form>
|
||||
</fieldset>#}
|
||||
<fieldset class="plank">
|
||||
<bitty-8 data-connect="/static/js/bits/badge-editor.js"></bitty-8>
|
||||
<legend>Badges</legend>
|
||||
<div>Loading badges…</div>
|
||||
<div>If badges fail to load, make sure JS is enabled.</div>
|
||||
<form method="POST" action="{{url_for('users.save_badges', username=get_active_user().username)}}" data-listen="submit" data-r="badgeEditorInit" enctype="multipart/form-data">
|
||||
<p>Loading badges…</p>
|
||||
<p>If badges fail to load, make sure JS is enabled.</p>
|
||||
</form>
|
||||
</fieldset>
|
||||
{%- if user.can_invite() -%}
|
||||
<fieldset class="plank" id="invite">
|
||||
|
||||
@@ -201,7 +201,7 @@ button, .linkbutton, input[type="submit"], input[type="file"]::file-selector-but
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
input[type="text"], input[type="password"], textarea, select {
|
||||
input[type="text"], input[type="password"], input[type="url"], textarea, select {
|
||||
--main-color: hsl(from var(--bg-color-primary) h s calc(l + 10));
|
||||
--active-color: hsl(from var(--main-color) h s calc(l + 5));
|
||||
--border-color: hsl(from var(--main-color) h calc(s * 1.3) 25);
|
||||
@@ -522,6 +522,7 @@ footer {
|
||||
border-radius: var(--base-padding);
|
||||
border: var(--base-padding) outset gray;
|
||||
box-shadow: 0px 0px 12px 2px #0006;
|
||||
align-self: center;
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
@@ -964,11 +965,52 @@ ol.sortable-list {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
&:not(.row) > * {
|
||||
&:not(.row):not(.full) > * {
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.badge-editor-badge-container {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: var(--base-padding);
|
||||
|
||||
& > input[type=text], & > input[type=url] {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.badge-editor-file-picker {
|
||||
display: flex;
|
||||
gap: var(--base-padding);
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 150px;
|
||||
|
||||
& > input[type=file] {
|
||||
width: 100%;
|
||||
|
||||
&::file-selector-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.badge-editor-badge-select {
|
||||
display: flex;
|
||||
gap: var(--base-padding);
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 200px;
|
||||
& > select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.js-only {
|
||||
display: none;
|
||||
}
|
||||
@@ -1038,4 +1080,9 @@ ol.sortable-list {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.badge-editor-badge-container {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
162
data/static/js/bits/badge-editor.js
Normal file
162
data/static/js/bits/badge-editor.js
Normal file
@@ -0,0 +1,162 @@
|
||||
async function getHTML(endpoint, options = {}) {
|
||||
let query = {};
|
||||
if (options._query !== undefined) {
|
||||
query = options._query;
|
||||
delete options._query;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams(query);
|
||||
const res = await fetch(`${endpoint}?${params}`, options);
|
||||
|
||||
return { body: await res.text(), status: res.status };
|
||||
}
|
||||
|
||||
const validateBase64Img = dataURL => new Promise(resolve => {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
resolve(img.width === 88 && img.height === 31);
|
||||
};
|
||||
img.src = dataURL;
|
||||
});
|
||||
|
||||
export const b = {
|
||||
init: 'badgeEditorInit',
|
||||
}
|
||||
|
||||
const badgeEditorEndpoint = '/hyperapi/badges/editor/'
|
||||
const MAX_BADGES = 10;
|
||||
let badgesCount = 0;
|
||||
let customImageDatas = {};
|
||||
|
||||
export async function badgeEditorInit(_, __, el) {
|
||||
const res = await getHTML(badgeEditorEndpoint);
|
||||
if (res.status != 200) {
|
||||
return;
|
||||
}
|
||||
el.innerHTML = res.body;
|
||||
badgesCount = el.querySelectorAll('.sortable-item').length;
|
||||
b.trigger('badgeEditorAssignImgId');
|
||||
b.trigger('setBadgeCount');
|
||||
}
|
||||
|
||||
export function badgeEditorAssignImgId(_, __, el) {
|
||||
if (el.dataset.imgId) return;
|
||||
|
||||
const id = b.uuid();
|
||||
const filePicker = el.querySelector('input[type=file]');
|
||||
const img = el.querySelector('img.badge-button');
|
||||
console.log(img);
|
||||
el.dataset.imgId = id;
|
||||
filePicker.dataset.imgId = id;
|
||||
img.dataset.imgId = id;
|
||||
}
|
||||
|
||||
export function badgeEditorSetPreview(ev, sender, el) {
|
||||
if (!sender.parentNode.contains(el)) return;
|
||||
|
||||
const selectedItem = sender.selectedOptions[0];
|
||||
if (selectedItem.value !== 'custom') {
|
||||
el.src = selectedItem.dataset.filePath;
|
||||
} else if (customImageDatas[el.dataset.imgId]) {
|
||||
el.src = customImageDatas[el.dataset.imgId];
|
||||
} else {
|
||||
el.removeAttribute('src');
|
||||
}
|
||||
}
|
||||
|
||||
export function badgeEditorSetPreviewCustom(payload, _, el) {
|
||||
if (!payload.badge.contains(el)) return;
|
||||
if (!customImageDatas[el.dataset.imgId]) {
|
||||
el.removeAttribute('src');
|
||||
} else {
|
||||
el.src = customImageDatas[el.dataset.imgId];
|
||||
}
|
||||
}
|
||||
|
||||
export function badgeEditorToggleFilePicker(ev, sender, el) {
|
||||
if (!sender.parentNode.parentNode.contains(el)) return;
|
||||
|
||||
const selectedItem = sender.selectedOptions[0];
|
||||
const picker = el.querySelector('input[type=file]');
|
||||
if (selectedItem.value !== 'custom') {
|
||||
el.classList.add('hidden');
|
||||
picker.required = false;
|
||||
picker.setCustomValidity('');
|
||||
} else {
|
||||
el.classList.remove('hidden');
|
||||
picker.required = true;
|
||||
picker.setCustomValidity(picker.dataset.validity || '');
|
||||
}
|
||||
}
|
||||
|
||||
export function badgeEditorAddBadge(ev, sender, el) {
|
||||
// TODO: page templates do not get updated on mutation
|
||||
const badge = document.getElementById('badge-template').innerText;
|
||||
el.innerHTML += badge;
|
||||
b.trigger('badgeEditorAssignImgId');
|
||||
badgesCount++;
|
||||
b.trigger('setBadgeCount');
|
||||
}
|
||||
|
||||
export function badgeEditorDelete(ev, sender, el) {
|
||||
if (!el.contains(sender)) return;
|
||||
el.remove();
|
||||
badgesCount--;
|
||||
b.trigger('setBadgeCount');
|
||||
}
|
||||
|
||||
export function badgeEditorShowFilePicker(ev, sender, el) {
|
||||
if (sender.nextElementSibling !== el) return;
|
||||
el.showPicker();
|
||||
}
|
||||
|
||||
export async function badgeEditorFileSelected(ev, sender, el) {
|
||||
const file = sender.files[0];
|
||||
const badge = sender.parentNode.parentNode;
|
||||
|
||||
if (
|
||||
!['image/png', 'image/jpeg', 'image/jpg', 'image/webp'].includes(file.type)
|
||||
) {
|
||||
sender.dataset.validity = 'The badge file must be an image.';
|
||||
sender.setCustomValidity(sender.dataset.validity);
|
||||
sender.reportValidity();
|
||||
customImageDatas[sender.dataset.imgId] = null;
|
||||
b.send({ badge: badge }, 'badgeEditorSetPreviewCustom');
|
||||
return;
|
||||
}
|
||||
if (file.size >= 1000 * 500) {
|
||||
sender.dataset.validity = 'The badge image must be smaller than 500KB.';
|
||||
sender.setCustomValidity(sender.dataset.validity);
|
||||
sender.reportValidity();
|
||||
customImageDatas[sender.dataset.imgId] = null;
|
||||
b.send({ badge: badge }, 'badgeEditorSetPreviewCustom');
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = async e => {
|
||||
const dimsValid = await validateBase64Img(e.target.result);
|
||||
if (!dimsValid) {
|
||||
sender.setCustomValidity('The badge image must be exactly 88x31 pixels.');
|
||||
sender.reportValidity();
|
||||
customImageDatas[sender.dataset.imgId] = null;
|
||||
b.send({ badge: badge }, 'badgeEditorSetPreviewCustom');
|
||||
return;
|
||||
}
|
||||
customImageDatas[sender.dataset.imgId] = e.target.result;
|
||||
|
||||
sender.dataset.validity = '';
|
||||
sender.setCustomValidity('');
|
||||
b.send({ badge: badge }, 'badgeEditorSetPreviewCustom');
|
||||
}
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
export function setBadgeCount(_, __, el) {
|
||||
if (el instanceof HTMLButtonElement) {
|
||||
el.disabled = badgesCount === MAX_BADGES;
|
||||
} else {
|
||||
el.innerText = `${badgesCount}/${MAX_BADGES}`;
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,9 @@
|
||||
if (!target || target === draggedItem) {
|
||||
return;
|
||||
}
|
||||
if (draggedItem === null) {
|
||||
return;
|
||||
}
|
||||
const inSameList = draggedItem.dataset.sortableListKey === target.dataset.sortableListKey;
|
||||
if (!inSameList) {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user