babel stuff

This commit is contained in:
mst 2025-02-28 07:14:24 +03:00
parent 38b012c276
commit cb22ef5c23
No known key found for this signature in database
14 changed files with 261 additions and 236 deletions

56
app.py
View file

@ -246,7 +246,7 @@ def login():
session.permanent = request.form.get('remember_me', False) session.permanent = request.form.get('remember_me', False)
return redirect(url_for('index')) return redirect(url_for('index'))
else: else:
flash("Wrong password", 'danger') flash(_("Wrong password"), 'danger')
return redirect(url_for('admin.login')) return redirect(url_for('admin.login'))
else: else:
if logged_in: if logged_in:
@ -321,7 +321,7 @@ def blacklist():
blacklist = request.form.get('blacklist') blacklist = request.form.get('blacklist')
with open(const.blacklistFile, 'w') as file: with open(const.blacklistFile, 'w') as file:
file.write(blacklist) file.write(blacklist)
return {'message': 'Blacklist updated!'}, 200 return {'message': _("Blacklist updated!")}, 200
else: else:
blacklist = func.readPlainFile(const.blacklistFile) blacklist = func.readPlainFile(const.blacklistFile)
@ -355,7 +355,7 @@ def postInstall():
# -- server routes -- # -- server routes --
@api_bp.errorhandler(404) @api_bp.errorhandler(404)
def notFound(): def notFound(e):
return jsonify({'error': 'Not found'}), 404 return jsonify({'error': 'Not found'}), 404
@api_bp.errorhandler(400) @api_bp.errorhandler(400)
@ -523,9 +523,9 @@ def addAnswer():
cw = request.form.get('cw', '') cw = request.form.get('cw', '')
if not question_id: if not question_id:
abort(400, "Missing 'question_id' attribute or 'question_id' is empty") abort(400, _("Missing 'question_id' attribute or 'question_id' is empty"))
if not answer: if not answer:
abort(400, "Missing 'answer' attribute or 'answer' is empty") abort(400, _("Missing 'answer' attribute or 'answer' is empty"))
return func.addAnswer(question_id, answer, cw) return func.addAnswer(question_id, answer, cw)
@ -543,24 +543,24 @@ def uploadFavicon():
func.generateFavicon(filename) func.generateFavicon(filename)
return {'message': 'Successfully updated favicon!'}, 201 return {'message': _("Successfully updated favicon!")}, 201
elif favicon and not func.allowedFile(favicon.filename): elif favicon and not func.allowedFile(favicon.filename):
return {'error': 'File type is not supported'}, 400 return {'error': _("File type is not supported")}, 400
elif not favicon: elif not favicon:
return {'error': "favicon is not specified"}, 400 return {'error': _("favicon is not specified")}, 400
@api_bp.route('/upload_emoji/', methods=['POST']) @api_bp.route('/upload_emoji/', methods=['POST'])
@loginRequired @loginRequired
def uploadEmoji(): def uploadEmoji():
if 'emoji' not in request.files: if 'emoji' not in request.files:
return jsonify({'error': 'No file part in the request'}), 400 return jsonify({'error': _("No file part in the request")}), 400
emoji = request.files['emoji'] emoji = request.files['emoji']
if emoji.filename == '': if emoji.filename == '':
return jsonify({'error': 'No file selected for uploading'}), 400 return jsonify({'error': _("No file selected for uploading")}), 400
if not func.allowedFile(emoji.filename): if not func.allowedFile(emoji.filename):
return jsonify({'error': 'Invalid file type. Only png, jpg, jpeg, webp, bmp, jxl supported'}), 400 return jsonify({'error': _("Invalid file type. Only png, jpg, jpeg, webp, bmp, jxl, gif supported")}), 400
# Secure the filename and determine the archive name # Secure the filename and determine the archive name
filename = secure_filename(emoji.filename) filename = secure_filename(emoji.filename)
@ -569,20 +569,20 @@ def uploadEmoji():
emoji.save(emoji_path) emoji.save(emoji_path)
return jsonify({'message': f'Emoji {filename} successfully uploaded'}), 201 return jsonify({'message': _("Emoji {} successfully uploaded").format(filename)}), 201
@api_bp.route('/upload_emoji_pack/', methods=['POST']) @api_bp.route('/upload_emoji_pack/', methods=['POST'])
@loginRequired @loginRequired
def uploadEmojiPack(): def uploadEmojiPack():
if 'emoji_archive' not in request.files: if 'emoji_archive' not in request.files:
return jsonify({'error': 'No file part in the request'}), 400 return jsonify({'error': _("No file part in the request")}), 400
emoji_archive = request.files['emoji_archive'] emoji_archive = request.files['emoji_archive']
if emoji_archive.filename == '': if emoji_archive.filename == '':
return jsonify({'error': 'No file selected for uploading'}), 400 return jsonify({'error': _("No file selected for uploading")}), 400
if not func.allowedArchive(emoji_archive.filename): if not func.allowedArchive(emoji_archive.filename):
return jsonify({'error': 'Invalid file type. Only .zip, .tar, .tar.gz, .tar.bz2 allowed'}), 400 return jsonify({'error': _("Invalid file type. Only .zip, .tar, .tar.gz, .tar.bz2 allowed")}), 400
filename = secure_filename(emoji_archive.filename) filename = secure_filename(emoji_archive.filename)
archive_name = func.stripArchExtension(filename) archive_name = func.stripArchExtension(filename)
@ -601,15 +601,15 @@ def uploadEmojiPack():
with tarfile.open(archive_path, 'r:*') as tar_ref: with tarfile.open(archive_path, 'r:*') as tar_ref:
tar_ref.extractall(extract_path) tar_ref.extractall(extract_path)
else: else:
return jsonify({'error': 'Unsupported archive format'}), 400 return jsonify({'error': _("Unsupported archive format")}), 400
# parse meta.json if it exists # parse meta.json if it exists
meta_json_path = extract_path / 'meta.json' meta_json_path = extract_path / 'meta.json'
if meta_json_path.exists(): if meta_json_path.exists():
processed_emojis = func.processEmojis(meta_json_path) processed_emojis = func.processEmojis(meta_json_path)
return jsonify({'message': f'Successfully uploaded and processed {len(processed_emojis)} emojis from archive "{filename}".'}), 201 return jsonify({'message': _('Successfully uploaded and processed {} emojis from archive "{}".').format(len(processed_emojis), filename)}), 201
else: else:
return jsonify({'message': f'Archive {filename} successfully uploaded and extracted.'}), 201 return jsonify({'message': _("Archive {} successfully uploaded and extracted.").format(filename)}), 201
except Exception as e: except Exception as e:
return jsonify({'error': str(e)}), 500 return jsonify({'error': str(e)}), 500
@ -654,11 +654,11 @@ def deleteEmoji():
emoji_ext = os.path.splitext(f"{emoji_base_path}/{emoji_file_path}") emoji_ext = os.path.splitext(f"{emoji_base_path}/{emoji_file_path}")
if not emoji_file_path.exists() or not emoji_file_path.is_file(): if not emoji_file_path.exists() or not emoji_file_path.is_file():
return jsonify({'error': 'Emoji not found'}), 404 return jsonify({'error': _("Emoji not found")}), 404
try: try:
emoji_file_path.unlink() emoji_file_path.unlink()
return jsonify({'message': f'Emoji "{emoji_name}" deleted successfully'}), 200 return jsonify({'message': _('Emoji "{}" deleted successfully').format(emoji_name)}), 200
except Exception as e: except Exception as e:
return jsonify({'error': str(e)}), 500 return jsonify({'error': str(e)}), 500
@ -670,14 +670,14 @@ def deleteEmojiPack():
emoji_base_path = Path.cwd() / 'static' / 'emojis' / pack_name.lower() emoji_base_path = Path.cwd() / 'static' / 'emojis' / pack_name.lower()
if not emoji_base_path.exists() or not emoji_base_path.is_dir(): if not emoji_base_path.exists() or not emoji_base_path.is_dir():
return jsonify({'error': f'Emoji pack "{pack_name.capitalize()}" not found'}), 404 return jsonify({'error': _('Emoji pack "{}" not found').format(pack_name)}), 404
try: try:
for json_file in emojis_path.glob(f'{pack_name.lower()}.json'): for json_file in emojis_path.glob(f'{pack_name.lower()}.json'):
json_file.unlink() json_file.unlink()
shutil.rmtree(emoji_base_path) shutil.rmtree(emoji_base_path)
return jsonify({'message': f'Emoji pack "{pack_name.capitalize()}" deleted successfully.'}), 200 return jsonify({'message': _('Emoji pack "{}" deleted successfully.').format(pack_name)}), 200
except Exception as e: except Exception as e:
return jsonify({'error': str(e)}), 500 return jsonify({'error': str(e)}), 500
@ -760,10 +760,10 @@ def deleteExport():
def viewQuestion(): def viewQuestion():
question_id = request.args.get('question_id', '') question_id = request.args.get('question_id', '')
if not question_id: if not question_id:
abort(400, "Missing 'question_id' attribute or 'question_id' is empty") abort(400, _("Missing 'question_id' attribute or 'question_id' is empty"))
conn = func.connectToDb() conn = func.connectToDb()
cursor = conn.cursor(dictionary=True) cursor = conn.cursor()
cursor.execute("SELECT id, from_who, creation_date, content, answered, answer_id FROM questions WHERE id=%s", (question_id,)) cursor.execute("SELECT id, from_who, creation_date, content, answered, answer_id FROM questions WHERE id=%s", (question_id,))
question = cursor.fetchone() question = cursor.fetchone()
cursor.close() cursor.close()
@ -775,10 +775,10 @@ def viewQuestion():
def viewAnswer(): def viewAnswer():
answer_id = request.args.get('answer_id', '') answer_id = request.args.get('answer_id', '')
if not answer_id: if not answer_id:
abort(400, "Missing 'answer_id' attribute or 'answer_id' is empty") abort(400, _("Missing 'answer_id' attribute or 'answer_id' is empty"))
conn = func.connectToDb() conn = func.connectToDb()
cursor = conn.cursor(dictionary=True) cursor = conn.cursor()
cursor.execute("SELECT id, question_id, creation_date, content FROM answers WHERE id=%s", (answer_id,)) cursor.execute("SELECT id, question_id, creation_date, content FROM answers WHERE id=%s", (answer_id,))
answer = cursor.fetchone() answer = cursor.fetchone()
cursor.close() cursor.close()
@ -809,7 +809,9 @@ def updateConfig():
func.saveJSON(cfg, const.configFile) func.saveJSON(cfg, const.configFile)
app.config.update(cfg) app.config.update(cfg)
return {'message': 'Settings saved!'} # loading the config file again to read new values
func.loadJSON(const.configFile)
return {'message': _("Settings saved!")}
app.register_blueprint(api_bp, url_prefix='/api/v1') app.register_blueprint(api_bp, url_prefix='/api/v1')
app.register_blueprint(admin_bp, url_prefix='/admin') app.register_blueprint(admin_bp, url_prefix='/admin')

View file

@ -24,6 +24,25 @@ import constants as const
app = Flask(const.appName) app = Flask(const.appName)
app.config['BABEL_DEFAULT_LOCALE'] = 'en'
app.config['BABEL_TRANSLATION_DIRECTORIES'] = 'locales'
# refreshing locale
refresh()
# update this once more languages are supported
app.config['available_languages'] = {
"en_US": _("English (US)"),
"ru_RU": _("Russian")
}
def getLocale():
if not session.get('language'):
app.config.update(cfg)
session['language'] = cfg['languages']['default']
return session.get('language')
babel = Babel(app, locale_selector=getLocale)
# load json file # load json file
def loadJSON(file_path): def loadJSON(file_path):
# open the file # open the file
@ -197,7 +216,7 @@ def addQuestion(from_who, question, cw, noAntispam=False):
antispam_valid = antispam in antispam_wordlist antispam_valid = antispam in antispam_wordlist
if not antispam_valid: if not antispam_valid:
# return a generic error message so bad actors wouldn't figure out the antispam list # return a generic error message so bad actors wouldn't figure out the antispam list
return {'error': 'An error has occurred'}, 500 return {'error': _('An error has occurred')}, 500
# it's probably bad to hardcode the siteverify urls, but meh, that will do for now # it's probably bad to hardcode the siteverify urls, but meh, that will do for now
elif cfg['antispam']['type'] == 'recaptcha': elif cfg['antispam']['type'] == 'recaptcha':
r = requests.post( r = requests.post(

View file

@ -1,9 +1,10 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block title %}{% block _title %}{% endblock %} - Admin{% endblock %} {% block title %}{% block _title %}{% endblock %} - {{ _('Admin') }}{% endblock %}
{% set adminLink = 'active' %} {% set adminLink = 'active' %}
{% block additionalHeadItems %} {% block additionalHeadItems %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/toastify.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/toastify.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/coloris.min.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/coloris.min.css') }}">
{% block _additionalHeadItems %}{% endblock %}
<style> <style>
[data-bs-theme=dark] .form-switch .form-check-input:focus:not(:checked) { [data-bs-theme=dark] .form-switch .form-check-input:focus:not(:checked) {
--bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23{{ cfg.style.accentDark[1:] }}'/%3e%3c/svg%3e"); --bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23{{ cfg.style.accentDark[1:] }}'/%3e%3c/svg%3e");
@ -39,39 +40,43 @@
<div id="sidebar" class="sticky-lg-top pt-2"> <div id="sidebar" class="sticky-lg-top pt-2">
<a class="ps-3 btn btn-outline-secondary my-1 fs-6 scale-parent {{ info_link }} d-flex align-items-center" href="{{ url_for('admin.information') }}"> <a class="ps-3 btn btn-outline-secondary my-1 fs-6 scale-parent {{ info_link }} d-flex align-items-center" href="{{ url_for('admin.information') }}">
<i class="bi bi-card-text fs-5 scale-child"></i> <i class="bi bi-card-text fs-5 scale-child"></i>
<span class="sidebar-btn-text ms-2 ps-1">Information</span> <span class="sidebar-btn-text ms-2 ps-1">{{ _('Information') }}</span>
</a> </a>
<a class="ps-3 btn btn-outline-secondary my-1 fs-6 scale-parent {{ access_link }} d-flex align-items-center" href="{{ url_for('admin.accessibility') }}"> <a class="ps-3 btn btn-outline-secondary my-1 fs-6 scale-parent {{ access_link }} d-flex align-items-center" href="{{ url_for('admin.accessibility') }}">
<i class="bi bi-universal-access fs-5 scale-child"></i> <i class="bi bi-universal-access fs-5 scale-child"></i>
<span class="sidebar-btn-text ms-2 ps-1">Accessibility</span> <span class="sidebar-btn-text ms-2 ps-1">{{ _('Accessibility') }}</span>
</a>
<a class="ps-3 btn btn-outline-secondary my-1 fs-6 scale-parent {{ langs_link }} d-flex align-items-center" href="{{ url_for('admin.languages') }}">
<i class="bi bi-translate fs-5 scale-child"></i>
<span class="sidebar-btn-text ms-2 ps-1">{{ _('Languages') }}</span>
</a> </a>
<a class="ps-3 btn btn-outline-secondary my-1 fs-6 scale-parent {{ notif_link }} d-flex align-items-center" href="{{ url_for('admin.notifications') }}"> <a class="ps-3 btn btn-outline-secondary my-1 fs-6 scale-parent {{ notif_link }} d-flex align-items-center" href="{{ url_for('admin.notifications') }}">
<i class="bi bi-bell fs-5 scale-child"></i> <i class="bi bi-bell fs-5 scale-child"></i>
<span class="sidebar-btn-text ms-2 ps-1">Notifications</span> <span class="sidebar-btn-text ms-2 ps-1">{{ _('Notifications') }}</span>
</a> </a>
<a class="ps-3 btn btn-outline-secondary my-1 fs-6 scale-parent {{ custom_link }} d-flex align-items-center" href="{{ url_for('admin.customize') }}"> <a class="ps-3 btn btn-outline-secondary my-1 fs-6 scale-parent {{ custom_link }} d-flex align-items-center" href="{{ url_for('admin.customize') }}">
<i class="bi bi-columns-gap fs-5 scale-child"></i> <i class="bi bi-columns-gap fs-5 scale-child"></i>
<span class="sidebar-btn-text ms-2 ps-1">Customize</span> <span class="sidebar-btn-text ms-2 ps-1">{{ _('Customize') }}</span>
</a> </a>
<a class="ps-3 btn btn-outline-secondary my-1 fs-6 scale-parent {{ antispam_link }} d-flex align-items-center" href="{{ url_for('admin.antispam') }}"> <a class="ps-3 btn btn-outline-secondary my-1 fs-6 scale-parent {{ antispam_link }} d-flex align-items-center" href="{{ url_for('admin.antispam') }}">
<i class="bi bi-shield fs-5 scale-child"></i> <i class="bi bi-shield fs-5 scale-child"></i>
<span class="sidebar-btn-text ms-2 ps-1">Anti-spam</span> <span class="sidebar-btn-text ms-2 ps-1">{{ _('Anti-spam') }}</span>
</a> </a>
<a class="ps-3 btn btn-outline-secondary my-1 fs-6 scale-parent {{ general_link }} d-flex align-items-center" href="{{ url_for('admin.general') }}"> <a class="ps-3 btn btn-outline-secondary my-1 fs-6 scale-parent {{ general_link }} d-flex align-items-center" href="{{ url_for('admin.general') }}">
<i class="bi bi-gear fs-5 scale-child"></i> <i class="bi bi-gear fs-5 scale-child"></i>
<span class="sidebar-btn-text ms-2 ps-1">General</span> <span class="sidebar-btn-text ms-2 ps-1">{{ _('General') }}</span>
</a> </a>
<a class="ps-3 btn btn-outline-secondary my-1 fs-6 scale-parent {{ emojis_link }} d-flex align-items-center" href="{{ url_for('admin.emojis') }}"> <a class="ps-3 btn btn-outline-secondary my-1 fs-6 scale-parent {{ emojis_link }} d-flex align-items-center" href="{{ url_for('admin.emojis') }}">
<i class="bi bi-emoji-smile fs-5 scale-child"></i> <i class="bi bi-emoji-smile fs-5 scale-child"></i>
<span class="sidebar-btn-text ms-2 ps-1">Emojis</span> <span class="sidebar-btn-text ms-2 ps-1">{{ _('Emojis') }}</span>
</a> </a>
<a class="ps-3 btn btn-outline-secondary my-1 fs-6 scale-parent {{ import_link }} d-flex align-items-center" href="{{ url_for('admin.importExport') }}"> <a class="ps-3 btn btn-outline-secondary my-1 fs-6 scale-parent {{ import_link }} d-flex align-items-center" href="{{ url_for('admin.importExport') }}">
<i class="bi bi-box-seam fs-5 scale-child"></i> <i class="bi bi-box-seam fs-5 scale-child"></i>
<span class="sidebar-btn-text ms-2 ps-1">Import/Export</span> <span class="sidebar-btn-text ms-2 ps-1">{{ _('Import/Export') }}</span>
</a> </a>
<a class="ps-3 btn btn-outline-secondary my-1 fs-6 scale-parent {{ blacklist_link }} d-flex align-items-center" href="{{ url_for('admin.blacklist') }}"> <a class="ps-3 btn btn-outline-secondary my-1 fs-6 scale-parent {{ blacklist_link }} d-flex align-items-center" href="{{ url_for('admin.blacklist') }}">
<i class="bi bi-ban fs-5 scale-child"></i> <i class="bi bi-ban fs-5 scale-child"></i>
<span class="sidebar-btn-text ms-2 ps-1">Word blacklist</span> <span class="sidebar-btn-text ms-2 ps-1">{{ _('Word blacklist') }}</span>
</a> </a>
<hr class="d-block d-md-none mt-4"> <hr class="d-block d-md-none mt-4">
</div> </div>

View file

@ -1,19 +1,20 @@
{% extends 'admin/base.html' %} {% extends 'admin/base.html' %}
{% block _title %}Accessibility{% endblock %} {% block _title %}{{ _('Accessibility') }}{% endblock %}
{% set access_link = 'active' %} {% set access_link = 'active' %}
{% block _content %} {% block _content %}
<form hx-post="{{ url_for('api.updateConfig') }}" hx-target="#response-container" hx-swap="none"> <form hx-post="{{ url_for('api.updateConfig') }}" hx-target="#response-container" hx-swap="none">
<h2 id="general" class="mb-2 fw-normal">Accessibility</h2> <h2 id="general" class="mb-2 fw-normal">{{ _('Accessibility') }}</h2>
<p class="fs-5 h3 text-body-secondary mb-3">Make {{ const.appName }} accessible to everyone</p> <p class="fs-5 h3 text-body-secondary mb-3">{{ _('Make {} accessible to everyone').format(const.appName) }}</p>
<div class="form-group mb-4"> <div class="form-group mb-4">
<label class="form-label" for="accessibility.font">Font</label> <label class="form-label" for="accessibility.font">{{ _('Font') }}</label>
<select id="accessibility.font" name="accessibility.font" class="form-select"> <select id="accessibility.font" name="accessibility.font" class="form-select">
<option value="default"{% if cfg.accessibility.font == 'default' %} selected{% endif %}>Default</option> <option value="default"{% if cfg.accessibility.font == 'default' %} selected{% endif %}>{{ _('Default') }}</option>
<option value="system"{% if cfg.accessibility.font == 'system' %} selected{% endif %}>System</option> <option value="system"{% if cfg.accessibility.font == 'system' %} selected{% endif %}>{{ _('System') }}</option>
<option value="atkinson"{% if cfg.accessibility.font == 'atkinson' %} selected{% endif %}>Atkinson Hyperlegible</option> <option value="atkinson"{% if cfg.accessibility.font == 'atkinson' %} selected{% endif %}>{{ _('Atkinson Hyperlegible') }}</option>
</select> </select>
</div> </div>
<h3 id="antispam" class="mb-2 fw-normal d-flex align-items-center gap-2">UserWay <a href="https://userway.org/" target="_blank" class="fs-5" title="what's this?"><i class="bi bi-question-circle"></i></a></h3> {#- i dont think userway should be translated since its a company name and those usually arent translated #}
<h3 id="antispam" class="mb-2 fw-normal d-flex align-items-center gap-2">UserWay <a href="https://userway.org/" target="_blank" class="fs-5" title="{{ _("what's this?") }}"><i class="bi bi-question-circle"></i></a></h3>
<div class="form-check form-switch mb-3"> <div class="form-check form-switch mb-3">
<input <input
class="form-check-input" class="form-check-input"
@ -24,22 +25,16 @@
role="switch" role="switch"
{% if cfg.accessibility.userway.enabled %}checked{% endif %}> {% if cfg.accessibility.userway.enabled %}checked{% endif %}>
<input type="hidden" id="accessibility.userway.enabled" name="accessibility.userway.enabled" value="{{ cfg.accessibility.userway.enabled }}"> <input type="hidden" id="accessibility.userway.enabled" name="accessibility.userway.enabled" value="{{ cfg.accessibility.userway.enabled }}">
<label for="_accessibility.userway.enabled" class="form-check-label">Enabled</label> <label for="_accessibility.userway.enabled" class="form-check-label">{{ _('Enabled') }}</label>
</div> </div>
<div class="form-group mb-3"> <div class="form-group mb-3">
<label class="form-label" for="accessibility.userway.account">Account key</label> <label class="form-label" for="accessibility.userway.account">{{ _('Account key') }}</label>
<input type="text" id="accessibility.userway.account" name="accessibility.userway.account" value="{{ cfg.accessibility.userway.account }}" class="form-control"> <input type="text" id="accessibility.userway.account" name="accessibility.userway.account" value="{{ cfg.accessibility.userway.account }}" class="form-control">
<p class="form-text"> <p class="form-text">
UserWay account key, find one at <a href="https://manage.userway.org/embed-code" target="_blank">manage.userway.org/embed-code</a> {{ _('UserWay account key, find one at') }} <a href="https://manage.userway.org/embed-code" target="_blank">manage.userway.org/embed-code</a>
</p> </p>
</div> </div>
<div class="form-group"> {% include 'snippets/admin/saveBtn.html' %}
<button type="submit" class="btn btn-primary mt-3" id="saveConfig">
<span class="spinner-border spinner-border-sm htmx-indicator" aria-hidden="true"></span>
<span class="visually-hidden" role="status">Loading...</span>
Save
</button>
</div>
</form> </form>
{% endblock %} {% endblock %}
{% block _scripts %} {% block _scripts %}

View file

@ -1,10 +1,10 @@
{% extends 'admin/base.html' %} {% extends 'admin/base.html' %}
{% block _title %}Anti-spam{% endblock %} {% block _title %}{{ _('Anti-spam') }}{% endblock %}
{% set antispam_link = 'active' %} {% set antispam_link = 'active' %}
{% block _content %} {% block _content %}
<form hx-post="{{ url_for('api.updateConfig') }}" hx-target="#response-container" hx-swap="none"> <form hx-post="{{ url_for('api.updateConfig') }}" hx-target="#response-container" hx-swap="none">
<h2 id="antispam" class="mb-2 fw-normal">Anti-spam</h2> <h2 id="antispam" class="mb-2 fw-normal">{{ _('Anti-spam') }}</h2>
<p class="fs-5 h3 text-body-secondary mb-3">Protect your {{ const.appName }} instance from spammers</p> <p class="fs-5 h3 text-body-secondary mb-3">{{ _('Protect your {} instance from spammers').format(const.appName) }}</p>
<div class="form-check form-switch mb-3"> <div class="form-check form-switch mb-3">
<input <input
class="form-check-input" class="form-check-input"
@ -15,73 +15,68 @@
role="switch" role="switch"
{% if cfg.antispam.enabled %}checked{% endif %}> {% if cfg.antispam.enabled %}checked{% endif %}>
<input type="hidden" id="antispam.enabled" name="antispam.enabled" value="{{ cfg.antispam.enabled }}"> <input type="hidden" id="antispam.enabled" name="antispam.enabled" value="{{ cfg.antispam.enabled }}">
<label for="_antispam.enabled" class="form-check-label">Enabled</label> <label for="_antispam.enabled" class="form-check-label">{{ _('Enabled') }}</label>
</div> </div>
{% if not cfg.antispam.enabled %} {% if not cfg.antispam.enabled %}
<div class="alert alert-warning border-0 small" role="alert"> <div class="alert alert-warning border-0 small" role="alert">
<p class="m-0 d-flex align-items-center gap-2 fw-medium"><i class="bi bi-exclamation-circle fs-5"></i> Warning</p> <p class="m-0 d-flex align-items-center gap-2 fw-medium"><i class="bi bi-exclamation-circle fs-5"></i> {{ _('Warning') }}</p>
<p class="m-0"> <p class="m-0">
It's highly encouraged to keep anti-spam enabled, otherwise you could become a target of a spam attack.<br><br> {{ _("It's highly encouraged to keep anti-spam enabled, otherwise you could become a target of a spam attack.") }}<br><br>
If you don't want your visitors to worry about completing a CAPTCHA, some providers have "non-interactive" and "invisible" CAPTCHA variants. {#- i hate quotes and their issues #}
{{ _("If you don't want your visitors to worry about completing a CAPTCHA, some providers have 'non-interactive' and 'invisible' CAPTCHA variants.") }}
</p> </p>
</div> </div>
{% endif %} {% endif %}
<div class="form-group mb-3{% if not cfg.antispam.enabled %}d-none{% endif %}"> <div class="form-group mb-3{% if not cfg.antispam.enabled %}d-none{% endif %}">
<label class="form-label" for="antispam.type">Anti-spam type</label> <label class="form-label" for="antispam.type">{{ _('Anti-spam type') }}</label>
<select id="antispam.type" name="antispam.type" class="form-select" onchange="checkForRecaptcha(this)"> <select id="antispam.type" name="antispam.type" class="form-select" onchange="checkForRecaptcha(this)">
<option value="basic"{% if cfg.antispam.type == 'basic' %} selected{% endif %}>Basic</option> <option value="basic"{% if cfg.antispam.type == 'basic' %} selected{% endif %}>{{ _('Basic') }}</option>
<option value="turnstile"{% if cfg.antispam.type == 'turnstile' %} selected{% endif %}>Cloudflare Turnstile</option> <option value="turnstile"{% if cfg.antispam.type == 'turnstile' %} selected{% endif %}>Cloudflare Turnstile</option>
<option value="frc"{% if cfg.antispam.type == 'frc' %} selected{% endif %}>Friendly Captcha</option> <option value="frc"{% if cfg.antispam.type == 'frc' %} selected{% endif %}>Friendly Captcha</option>
<option value="recaptcha"{% if cfg.antispam.type == 'recaptcha' %} selected{% endif %}>reCAPTCHA v2</option> <option value="recaptcha"{% if cfg.antispam.type == 'recaptcha' %} selected{% endif %}>reCAPTCHA v2</option>
</select> </select>
<p class="form-text">Anti-spam type to use</p> <p class="form-text">{{ _('Anti-spam type to use') }}</p>
</div> </div>
<div id="recaptcha-options"{% if cfg.antispam.type != 'recaptcha' %} class="d-none"{% endif %}> <div id="recaptcha-options"{% if cfg.antispam.type != 'recaptcha' %} class="d-none"{% endif %}>
<h3 id="recaptcha" class="mb-3 fw-light">reCAPTCHA options</h3> <h3 id="recaptcha" class="mb-3 fw-light">{{ _('reCAPTCHA options') }}</h3>
<div class="form-group mb-3"> <div class="form-group mb-3">
<label class="form-label" for="antispam.recaptcha.sitekey">Site key</label> <label class="form-label" for="antispam.recaptcha.sitekey">{{ _('Site key') }}</label>
<input type="text" id="antispam.recaptcha.sitekey" name="antispam.recaptcha.sitekey" value="{{ cfg.antispam.recaptcha.sitekey }}" class="form-control"> <input type="text" id="antispam.recaptcha.sitekey" name="antispam.recaptcha.sitekey" value="{{ cfg.antispam.recaptcha.sitekey }}" class="form-control">
<p class="form-text">reCAPTCHA site key</p> <p class="form-text">{{ _('reCAPTCHA site key') }}</p>
</div> </div>
<div class="form-group mb-3"> <div class="form-group mb-3">
<label class="form-label" for="antispam.recaptcha.secretkey">Secret key</label> <label class="form-label" for="antispam.recaptcha.secretkey">{{ _('Secret key') }}</label>
<input type="password" id="antispam.recaptcha.secretkey" name="antispam.recaptcha.secretkey" value="{{ cfg.antispam.recaptcha.secretkey }}" class="form-control"> <input type="password" id="antispam.recaptcha.secretkey" name="antispam.recaptcha.secretkey" value="{{ cfg.antispam.recaptcha.secretkey }}" class="form-control">
<p class="form-text">reCAPTCHA secret key</p> <p class="form-text">{{ _('reCAPTCHA secret key') }}</p>
</div> </div>
</div> </div>
<div id="turnstile-options"{% if cfg.antispam.type != 'turnstile' %} class="d-none"{% endif %}> <div id="turnstile-options"{% if cfg.antispam.type != 'turnstile' %} class="d-none"{% endif %}>
<h3 id="turnstile" class="mb-3 fw-light">Turnstile options</h3> <h3 id="turnstile" class="mb-3 fw-light">{{ _('Turnstile options') }}</h3>
<div class="form-group mb-3"> <div class="form-group mb-3">
<label class="form-label" for="antispam.turnstile.sitekey">Site key</label> <label class="form-label" for="antispam.turnstile.sitekey">{{ _('Site key') }}</label>
<input type="text" id="antispam.turnstile.sitekey" name="antispam.turnstile.sitekey" value="{{ cfg.antispam.turnstile.sitekey }}" class="form-control"> <input type="text" id="antispam.turnstile.sitekey" name="antispam.turnstile.sitekey" value="{{ cfg.antispam.turnstile.sitekey }}" class="form-control">
<p class="form-text">Turnstile site key</p> <p class="form-text">{{ _('Turnstile site key') }}</p>
</div> </div>
<div class="form-group mb-3"> <div class="form-group mb-3">
<label class="form-label" for="antispam.turnstile.secretkey">Secret key</label> <label class="form-label" for="antispam.turnstile.secretkey">{{ _('Secret key') }}</label>
<input type="password" id="antispam.turnstile.secretkey" name="antispam.turnstile.secretkey" value="{{ cfg.antispam.turnstile.secretkey }}" class="form-control"> <input type="password" id="antispam.turnstile.secretkey" name="antispam.turnstile.secretkey" value="{{ cfg.antispam.turnstile.secretkey }}" class="form-control">
<p class="form-text">Turnstile secret key</p> <p class="form-text">{{ _('Turnstile secret key') }}</p>
</div> </div>
</div> </div>
<div id="frc-options"{% if cfg.antispam.type != 'frc' %} class="d-none"{% endif %}> <div id="frc-options"{% if cfg.antispam.type != 'frc' %} class="d-none"{% endif %}>
<h3 id="frc" class="mb-3 fw-light">Friendly Captcha options</h3> <h3 id="frc" class="mb-3 fw-light">{{ _('Friendly Captcha options') }}</h3>
<div class="form-group mb-3"> <div class="form-group mb-3">
<label class="form-label" for="antispam.frc.sitekey">Site key</label> <label class="form-label" for="antispam.frc.sitekey">{{ _('Site key') }}</label>
<input type="text" id="antispam.frc.sitekey" name="antispam.frc.sitekey" value="{{ cfg.antispam.frc.sitekey }}" class="form-control"> <input type="text" id="antispam.frc.sitekey" name="antispam.frc.sitekey" value="{{ cfg.antispam.frc.sitekey }}" class="form-control">
<p class="form-text">Friendly Captcha site key</p> <p class="form-text">{{ _('Friendly Captcha site key') }}</p>
</div> </div>
<div class="form-group mb-3"> <div class="form-group mb-3">
<label class="form-label" for="antispam.frc.apikey">API key</label> <label class="form-label" for="antispam.frc.apikey">{{ _('API key') }}</label>
<input type="password" id="antispam.frc.apikey" name="antispam.frc.apikey" value="{{ cfg.antispam.frc.apikey }}" class="form-control"> <input type="password" id="antispam.frc.apikey" name="antispam.frc.apikey" value="{{ cfg.antispam.frc.apikey }}" class="form-control">
<p class="form-text">Friendly Captcha API key</p> <p class="form-text">{{ _('Friendly Captcha API key') }}</p>
</div> </div>
</div> </div>
<div class="form-group"> {% include 'snippets/admin/saveBtn.html' %}
<button type="submit" class="btn btn-primary mt-3" id="saveConfig">
<span class="spinner-border spinner-border-sm htmx-indicator" aria-hidden="true"></span>
<span class="visually-hidden" role="status">Loading...</span>
Save
</button>
</div>
</form> </form>
{% endblock %} {% endblock %}
{% block _scripts %} {% block _scripts %}
@ -103,7 +98,7 @@
} else if (element.value === 'frc') { } else if (element.value === 'frc') {
turnstileDiv.classList.add('d-none'); turnstileDiv.classList.add('d-none');
recaptchaDiv.classList.add('d-none'); recaptchaDiv.classList.add('d-none');
frcDiv.classList.remove('d-none'); frcDiv.classList.remove('d-none');
} else { } else {
recaptchaDiv.classList.add('d-none'); recaptchaDiv.classList.add('d-none');
turnstileDiv.classList.add('d-none'); turnstileDiv.classList.add('d-none');

View file

@ -1,20 +1,16 @@
{% extends 'admin/base.html' %} {% extends 'admin/base.html' %}
{% block _title %}Blacklist{% endblock %} {% block _title %}{{ _('Word blacklist') }}{% endblock %}
{% set blacklist_link = 'active' %} {% set blacklist_link = 'active' %}
{% block _content %} {% block _content %}
<h2 id="blacklist" class="fw-normal mb-2">Word blacklist</h2> <h2 id="blacklist" class="fw-normal mb-2">{{ _('Word blacklist') }}</h2>
<p class="fs-5 h3 text-body-secondary mb-3">Blacklist words that you don't want to see</p> <p class="fs-5 h3 text-body-secondary mb-3">{{ _("Blacklist words that you don't want to see") }}</p>
<form hx-put="{{ url_for('api.updateBlacklist') }}" hx-target="#response-container" hx-swap="none"> <form hx-put="{{ url_for('api.updateBlacklist') }}" hx-target="#response-container" hx-swap="none">
<input type="hidden" name="action" value="update_word_blacklist"> <input type="hidden" name="action" value="update_word_blacklist">
<div class="form-group mb-3"> <div class="form-group mb-3">
<label class="form-label" for="blacklist_cat">Blacklisted words for questions, one word per line</label> <label class="form-label" for="blacklist_cat">{{ _('Blacklisted words for questions, one word per line') }}</label>
<!-- <p class="text-body-secondary">Blacklisted words for questions; one word per line</p> --> <!-- <p class="text-body-secondary">Blacklisted words for questions; one word per line</p> -->
<textarea id="blacklist_cat" name="blacklist" style="height: 300px; resize: vertical;" class="form-control">{{ blacklist }}</textarea> <textarea id="blacklist_cat" name="blacklist" style="height: 300px; resize: vertical;" class="form-control">{{ blacklist }}</textarea>
<button type="submit" class="btn btn-primary mt-3" id="save-blacklist"> {% include 'snippets/admin/saveBtn.html' %}
<span class="me-1 spinner-border spinner-border-sm htmx-indicator" aria-hidden="true"></span>
<span class="visually-hidden" role="status">Loading...</span>
Save
</button>
</div> </div>
</form> </form>
{% endblock %} {% endblock %}

View file

@ -1,36 +1,61 @@
{% extends 'admin/base.html' %} {% extends 'admin/base.html' %}
{% block _title %}Customize{% endblock %} {% block _title %}{{ _('Customize') }}{% endblock %}
{% set custom_link = 'active' %} {% set custom_link = 'active' %}
{% block _additionalHeadItems %}
<style>
.cm-content, .cm-gutter {
min-height: 200px !important;
}
.cm-editor, .cm-scroller {
border-radius: var(--bs-border-radius);
}
.cm-editor {
font-size: 14px;
}
.ͼ1.cm-focused {
box-shadow: 0 0 0 .25rem color-mix(in srgb, var(--bs-primary) 25%, transparent);
outline: 0 !important;
}
#settings-dropdown {
min-width: 25rem;
}
@media screen and (max-width: 600px) {
#settings-dropdown {
min-width: 100vw;
}
}
</style>
{% endblock %}
{% block _content %} {% block _content %}
<h2 id="customize" class="mb-2 fw-normal">Customize</h2> <h2 id="customize" class="mb-2 fw-normal">{{ _('Customize') }}</h2>
<p class="fs-5 h3 text-body-secondary mb-3">Customize {{ const.appName }} to your liking</p> <p class="fs-5 h3 text-body-secondary mb-3">{{ _('Customize {} to your liking').format(const.appName) }}</p>
<h3 class="fw-light">Favicon</h3> <h3 class="fw-light">{{ _('Favicon') }}</h3>
<form hx-post="{{ url_for('api.uploadFavicon') }}" hx-target="#response-container" hx-swap="none" hx-encoding="multipart/form-data"> <form hx-post="{{ url_for('api.uploadFavicon') }}" hx-target="#response-container" hx-swap="none" hx-encoding="multipart/form-data">
<p class="m-0">Current favicon: <img src="{{ url_for('static', filename='icons/favicon/apple-touch-icon.png') }}" width="32" height="32" alt="{{ cfg.instance.title }}'s icon" class="rounded"></p> <p class="m-0">{{ _('Current favicon:') }} <img src="{{ url_for('static', filename='icons/favicon/apple-touch-icon.png') }}" width="32" height="32" alt="{{ cfg.instance.title }}'s icon" class="rounded"></p>
<div class="mt-2"> <div class="mt-2">
<label for="favicon" class="form-label">Upload favicon</label> <label for="favicon" class="form-label">{{ _('Upload favicon') }}</label>
<input class="form-control" type="file" id="favicon" name="favicon"> <input class="form-control" type="file" id="favicon" name="favicon">
</div> </div>
<div class="mb-4"> <div class="mb-4">
<button type="submit" class="btn btn-primary mt-3" id="saveFavicon"> <button type="submit" class="btn btn-primary mt-3" id="saveFavicon">
<span class="spinner-border spinner-border-sm htmx-indicator" aria-hidden="true"></span> <span class="spinner-border spinner-border-sm htmx-indicator" aria-hidden="true"></span>
<span class="visually-hidden" role="status">Loading...</span> <span class="visually-hidden" role="status">{{ _('Loading...') }}</span>
<i class="bi bi-file-earmark-arrow-up me-1"></i> Upload <i class="bi bi-file-earmark-arrow-up me-1"></i> {{ _('Upload') }}
</button> </button>
</div> </div>
</form> </form>
<form hx-post="{{ url_for('api.updateConfig') }}" hx-target="#response-container" hx-swap="none"> <form hx-post="{{ url_for('api.updateConfig') }}" hx-target="#response-container" hx-swap="none" hx-on::before-request="setCmTextareaValue()">
<h3 class="fw-light">Accent color</h3> <h3 class="fw-light">{{ _('Accent color') }}</h3>
{# <h4 class="fw-light">Color</h4> #} {# <h4 class="fw-light">Color</h4> #}
<div class="form-group d-flex flex-column"> <div class="form-group d-flex flex-column">
<label class="form-label" for="style.accentLight">Light theme</label> <label class="form-label" for="style.accentLight">{{ _('Light theme') }}</label>
<input type="text" name="style.accentLight" id="style.accentLight" value="{{ cfg.style.accentLight }}" class="form-control" data-coloris title="Click to open the color picker"> <input type="text" name="style.accentLight" id="style.accentLight" value="{{ cfg.style.accentLight }}" class="form-control" data-coloris title="{{ _('Click to open the color picker') }}">
<p class="form-text">Default: <b>#6345d9</b></p> <p class="form-text">{{ _('Default') }}: <b>#6345d9</b></p>
</div> </div>
<div class="form-group d-flex flex-column"> <div class="form-group d-flex flex-column">
<label class="form-label" for="style.accentDark">Dark theme</label> <label class="form-label" for="style.accentDark">{{ _('Dark theme') }}</label>
<input type="text" name="style.accentDark" id="style.accentDark" value="{{ cfg.style.accentDark }}" class="form-control" data-coloris title="Click to open the color picker"> <input type="text" name="style.accentDark" id="style.accentDark" value="{{ cfg.style.accentDark }}" class="form-control" data-coloris title="{{ _('Click to open the color picker') }}">
<p class="form-text">Default: <b>#7a63e3</b></p> <p class="form-text">{{ _('Default') }}: <b>#7a63e3</b></p>
</div> </div>
{# brain doesn't feel like implementing this rn (9/27/24) {# brain doesn't feel like implementing this rn (9/27/24)
<h4 class="fw-light mt-2">Background</h4> <h4 class="fw-light mt-2">Background</h4>
@ -55,7 +80,7 @@
role="switch" role="switch"
{% if cfg.style.tintColors == true %}checked{% endif %}> {% if cfg.style.tintColors == true %}checked{% endif %}>
<input type="hidden" id="style.tintColors" name="style.tintColors" value="{{ cfg.style.tintColors }}"> <input type="hidden" id="style.tintColors" name="style.tintColors" value="{{ cfg.style.tintColors }}">
<label for="_style.tintColors" class="form-check-label">Tint all colors with accent color</label> <label for="_style.tintColors" class="form-check-label">{{ _('Tint all colors with accent color') }}</label>
</div> </div>
<div class="form-check form-switch mb-1"> <div class="form-check form-switch mb-1">
<input <input
@ -67,9 +92,9 @@
role="switch" role="switch"
{% if cfg.style.navIcons == true %}checked{% endif %}> {% if cfg.style.navIcons == true %}checked{% endif %}>
<input type="hidden" id="style.navIcons" name="style.navIcons" value="{{ cfg.style.navIcons }}"> <input type="hidden" id="style.navIcons" name="style.navIcons" value="{{ cfg.style.navIcons }}">
<label for="_style.navIcons" class="form-check-label">Include icons in nav links</label> <label for="_style.navIcons" class="form-check-label">{{ _('Include icons in nav links') }}</label>
</div> </div>
<p class="form-text mb-2"><b>Note:</b> on mobile screens text will always be hidden if this option is enabled</p> <p class="form-text mb-2"><b>{{ _('Note') }}:</b> {{ _('on mobile screens text will always be hidden if this option is enabled') }}</p>
<div class="form-check form-switch mb-4 ms-3"> <div class="form-check form-switch mb-4 ms-3">
<input <input
class="form-check-input nav-icons-only-checkbox" class="form-check-input nav-icons-only-checkbox"
@ -80,7 +105,7 @@
role="switch" role="switch"
{% if cfg.style.navIconsOnly == true %}checked{% endif %}> {% if cfg.style.navIconsOnly == true %}checked{% endif %}>
<input type="hidden" id="style.navIconsOnly" name="style.navIconsOnly" value="{{ cfg.style.navIconsOnly }}"> <input type="hidden" id="style.navIconsOnly" name="style.navIconsOnly" value="{{ cfg.style.navIconsOnly }}">
<label for="_style.navIconsOnly" class="form-check-label">Include <b>only</b> icons in nav links</label> <label for="_style.navIconsOnly" class="form-check-label">{{ _('Include only icons in nav links') }}</label>
</div> </div>
{# {#
<div class="form-check form-switch mb-3"> <div class="form-check form-switch mb-3">

View file

@ -1,49 +1,49 @@
{% extends 'admin/base.html' %} {% extends 'admin/base.html' %}
{% block _title %}Emojis{% endblock %} {% block _title %}{{ _('Emojis') }}{% endblock %}
{% set emojis_link = 'active' %} {% set emojis_link = 'active' %}
{% block _content %} {% block _content %}
<h2 id="customEmojis" class="mb-2 fw-normal">Custom emojis</h2> <h2 id="customEmojis" class="mb-2 fw-normal">{{ _('Custom emojis') }}</h2>
<p class="fs-5 h3 text-body-secondary mb-3">Add custom emojis to your {{ const.appName }} instance</p> <p class="fs-5 h3 text-body-secondary mb-3">{{ _('Add custom emojis to your {} instance').format(const.appName) }}</p>
<h3 class="fw-light mb-3">Upload</h3> <h3 class="fw-light mb-3">{{ _('Upload') }}</h3>
<form hx-post="{{ url_for('api.uploadEmoji') }}" hx-target="#response-container" hx-swap="none" hx-encoding="multipart/form-data"> <form hx-post="{{ url_for('api.uploadEmoji') }}" hx-target="#response-container" hx-swap="none" hx-encoding="multipart/form-data">
<div> <div>
<label for="emoji" class="form-label">Upload an emoji</label> <label for="emoji" class="form-label">{{ _('Upload an emoji') }}</label>
<input class="form-control" type="file" id="emoji" name="emoji"> <input class="form-control" type="file" id="emoji" name="emoji">
</div> </div>
<div class="mb-4"> <div class="mb-4">
<button type="submit" class="btn btn-primary mt-3" id="uploadEmoji"> <button type="submit" class="btn btn-primary mt-3" id="uploadEmoji">
<span class="spinner-border spinner-border-sm htmx-indicator" aria-hidden="true"></span> <span class="spinner-border spinner-border-sm htmx-indicator" aria-hidden="true"></span>
<span class="visually-hidden" role="status">Loading...</span> <span class="visually-hidden" role="status">{{ _('Loading...') }}</span>
<i class="bi bi-file-earmark-plus me-1"></i> Upload emoji <i class="bi bi-file-earmark-plus me-1"></i> {{ _('Upload emoji') }}
</button> </button>
</div> </div>
</form> </form>
<hr> <hr>
<form id="pack-form" hx-post="{{ url_for('api.uploadEmojiPack') }}" hx-target="#response-container" hx-swap="none" hx-encoding="multipart/form-data"> <form id="pack-form" hx-post="{{ url_for('api.uploadEmojiPack') }}" hx-target="#response-container" hx-swap="none" hx-encoding="multipart/form-data">
<div> <div>
<label for="emoji_archive" class="form-label">Upload emoji pack</label> <label for="emoji_archive" class="form-label">{{ _('Upload emoji pack') }}</label>
<input class="form-control" type="file" id="emoji_archive" name="emoji_archive"> <input class="form-control" type="file" id="emoji_archive" name="emoji_archive">
<p class="form-text mb-0">Supported archive formats: <strong>.zip, .tar, .tar.gz, .tar.bz2, .tar.xz</strong></p> <p class="form-text mb-0">{{ _('Supported archive formats:') }} <strong>.zip, .tar, .tar.gz, .tar.bz2, .tar.xz</strong></p>
</div> </div>
<div class="mb-5"> <div class="mb-5">
<button type="submit" class="btn btn-primary mt-3" id="uploadEmojiPack"> <button type="submit" class="btn btn-primary mt-3" id="uploadEmojiPack">
<span class="spinner-border spinner-border-sm htmx-indicator" aria-hidden="true"></span> <span class="spinner-border spinner-border-sm htmx-indicator" aria-hidden="true"></span>
<span class="visually-hidden" role="status">Loading...</span> <span class="visually-hidden" role="status">{{ _('Loading...') }}</span>
<i class="bi bi-file-earmark-zip me-1"></i> Upload pack <i class="bi bi-file-earmark-zip me-1"></i> {{ _('Upload pack') }}
</button> </button>
</div> </div>
</form> </form>
<h3 class="fw-light mb-3">Manage</h3> <h3 class="fw-light mb-3">{{ _('Manage') }}</h3>
<h4 class="fw-light">Emojis</h4> <h4 class="fw-light">{{ _('Emojis') }}</h4>
{% if emojis %} {% if emojis %}
<div class="table-responsive"> <div class="table-responsive">
<table id="emojiTable" class="table align-middle"> <table id="emojiTable" class="table align-middle">
<thead> <thead>
<tr> <tr>
<th>Image</th> <th>{{ _('Image') }}</th>
<th>Name</th> <th>{{ _('Name') }}</th>
<th>Filename</th> <th>{{ _('Filename') }}</th>
<th>Actions</th> <th>{{ _('Actions') }}</th>
</tr> </tr>
</thead> </thead>
<tbody id="emojis"> <tbody id="emojis">
@ -54,23 +54,23 @@
</table> </table>
</div> </div>
{% else %} {% else %}
<p class="text-center">No emojis uploaded</p> <p class="text-center">{{ _('No emojis uploaded') }}</p>
{% endif %} {% endif %}
<h4 class="fw-light mt-3">Emoji packs</h4> <h4 class="fw-light mt-3">{{ _('Emoji packs') }}</h4>
<p class="text-body-secondary small">Please note that if meta.json is not found in the archive, preview images are selected by first emoji name to appear alphabetically, so they may not be accurate sometimes</p> <p class="text-body-secondary small">{{ _('Please note that if meta.json is not found in the archive, preview images are selected by first emoji name to appear alphabetically, so they may not be accurate sometimes') }}</p>
{% if packs %} {% if packs %}
<div class="table-responsive"> <div class="table-responsive">
<table id="emojiPackTable" class="table align-middle"> <table id="emojiPackTable" class="table align-middle">
<thead> <thead>
<tr> <tr>
<th>Image</th> <th>{{ _('Image') }}</th>
<th>Name</th> <th>{{ _('Name') }}</th>
{% if json_pack %} {% if json_pack %}
<th>Author</th> <th>{{ _('Author') }}</th>
<th>Released</th> <th>{{ _('Released') }}</th>
{% endif %} {% endif %}
<th>Folder</th> <th>{{ _('Folder') }}</th>
<th>Actions</th> <th>{{ _('Actions') }}</th>
</tr> </tr>
</thead> </thead>
<tbody id="packs"> <tbody id="packs">
@ -83,6 +83,6 @@
</table> </table>
</div> </div>
{% else %} {% else %}
<p class="text-center">No emoji packs uploaded</p> <p class="text-center">{{ _('No emoji packs uploaded') }}</p>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View file

@ -25,7 +25,7 @@
role="switch" role="switch"
{% if cfg.lockInbox %}checked{% endif %}> {% if cfg.lockInbox %}checked{% endif %}>
<input type="hidden" id="lockInbox" name="lockInbox" value="{{ cfg.lockInbox }}"> <input type="hidden" id="lockInbox" name="lockInbox" value="{{ cfg.lockInbox }}">
<label for="_lockInbox" class="form-check-label">Lock inbox and don't allow new questions</label> <label for="_lockInbox" class="form-check-label">{{ _("Lock inbox and don't allow new questions") }}</label>
</div> </div>
<div class="form-check form-switch mb-2"> <div class="form-check form-switch mb-2">
<input <input
@ -37,7 +37,7 @@
role="switch" role="switch"
{% if cfg.allowAnonQuestions %}checked{% endif %}> {% if cfg.allowAnonQuestions %}checked{% endif %}>
<input type="hidden" id="allowAnonQuestions" name="allowAnonQuestions" value="{{ cfg.allowAnonQuestions }}"> <input type="hidden" id="allowAnonQuestions" name="allowAnonQuestions" value="{{ cfg.allowAnonQuestions }}">
<label for="_allowAnonQuestions" class="form-check-label">Allow anonymous questions</label> <label for="_allowAnonQuestions" class="form-check-label">{{ _('Allow anonymous questions') }}</label>
</div> </div>
<div class="form-check form-switch mb-2"> <div class="form-check form-switch mb-2">
<input <input
@ -49,7 +49,7 @@
role="switch" role="switch"
{% if cfg.noDeleteConfirm %}checked{% endif %}> {% if cfg.noDeleteConfirm %}checked{% endif %}>
<input type="hidden" id="noDeleteConfirm" name="noDeleteConfirm" value="{{ cfg.noDeleteConfirm }}"> <input type="hidden" id="noDeleteConfirm" name="noDeleteConfirm" value="{{ cfg.noDeleteConfirm }}">
<label for="_noDeleteConfirm" class="form-check-label">Disable confirmation when deleting questions</label> <label for="_noDeleteConfirm" class="form-check-label">{{ _('Disable confirmation when deleting questions') }}</label>
</div> </div>
<div class="form-check form-switch mb-3"> <div class="form-check form-switch mb-3">
<input <input
@ -61,15 +61,9 @@
role="switch" role="switch"
{% if cfg.showQuestionCount %}checked{% endif %}> {% if cfg.showQuestionCount %}checked{% endif %}>
<input type="hidden" id="showQuestionCount" name="showQuestionCount" value="{{ cfg.showQuestionCount }}"> <input type="hidden" id="showQuestionCount" name="showQuestionCount" value="{{ cfg.showQuestionCount }}">
<label for="_showQuestionCount" class="form-check-label">Show question count in homepage</label> <label for="_showQuestionCount" class="form-check-label">{{ _('Show question count in homepage') }}</label>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary mt-3" id="saveConfig">
<span class="spinner-border spinner-border-sm htmx-indicator" aria-hidden="true"></span>
<span class="visually-hidden" role="status">Loading...</span>
Save
</button>
</div> </div>
{% include 'snippets/admin/saveBtn.html' %}
</form> </form>
{% endblock %} {% endblock %}
{% block _scripts %} {% block _scripts %}

View file

@ -1,20 +1,26 @@
{% extends 'admin/base.html' %} {% extends 'admin/base.html' %}
{% block _title %}Import/Export{% endblock %} {% block _title %}{{ _('Import/Export') }}{% endblock %}
{% set import_link = 'active' %} {% set import_link = 'active' %}
{% block _content %} {% block _content %}
<h2 id="general" class="mb-2 fw-normal">Import/Export</h2> <h2 id="general" class="mb-2 fw-normal">{{ _('Import/Export') }}</h2>
<p class="fs-5 h3 text-body-secondary mb-3">Import or export your {{ const.appName }} instance data</p> <p class="fs-5 h3 text-body-secondary mb-3">{{ _('Import or export your {} instance data').format(const.appName) }}</p>
<h3 id="import" class="mb-3 fw-light">Import</h3> <h3 id="import" class="mb-3 fw-light">{{ _('Import') }}</h3>
<div class="alert alert-info border-0 small" role="alert">
<p class="m-0 d-flex align-items-center gap-2 fw-medium"><i class="bi bi-info-circle fs-5"></i> {{ _('Note') }}</p>
<p class="m-0">
{{ _('Please note that import may take a while, depending on your database size') }}
</p>
</div>
<form class="mb-2" hx-disabled-elt="find button[type=submit]" hx-put="{{ url_for('api.importData') }}" hx-encoding="multipart/form-data" hx-target="#response-container" hx-swap="none"> <form class="mb-2" hx-disabled-elt="find button[type=submit]" hx-put="{{ url_for('api.importData') }}" hx-encoding="multipart/form-data" hx-target="#response-container" hx-swap="none">
<div class="form-group mb-3"> <div class="form-group mb-3">
<label class="form-label" for="import_archive">Import data</label> <label class="form-label" for="import_archive">{{ _('Import data') }}</label>
<input class="form-control" type="file" id="import_archive" name="import_archive" required> <input class="form-control" type="file" id="import_archive" name="import_archive" required>
<p class="form-text">Note: Retrospring exports are not supported yet</p> <p class="form-text">{{ _('Note: Retrospring exports are not supported yet') }}</p>
</div> </div>
<button type="submit" class="btn btn-primary mt-2" id="import-data"> <button type="submit" class="btn btn-primary mt-2" id="import-data">
<span class="me-1 spinner-border spinner-border-sm htmx-indicator" aria-hidden="true"></span> <span class="me-1 spinner-border spinner-border-sm htmx-indicator" aria-hidden="true"></span>
<span class="visually-hidden" role="status">Loading...</span> <span class="visually-hidden" role="status">{{ _('Loading...') }}</span>
<i class="bi bi-database-up me-1"></i> Import <i class="bi bi-database-up me-1"></i> {{ _('Import') }}
</button> </button>
</form> </form>
{# {#
@ -32,13 +38,13 @@
</button> </button>
</form> </form>
#} #}
<h3 id="export" class="mb-3 mt-5 fw-light">Export</h3> <h3 id="export" class="mb-3 mt-5 fw-light">{{ _('Export') }}</h3>
<div class="form-group mb-3"> <div class="form-group mb-3">
<form hx-post="{{ url_for('api.createExport') }}" hx-swap="none" hx-disabled-elt="#export-btn"> <form hx-post="{{ url_for('api.createExport') }}" hx-swap="none" hx-disabled-elt="#export-btn">
<button type="submit" class="btn btn-primary" id="export-btn"> <button type="submit" class="btn btn-primary" id="export-btn">
<span class="spinner-border spinner-border-sm htmx-indicator" aria-hidden="true"></span> <span class="spinner-border spinner-border-sm htmx-indicator" aria-hidden="true"></span>
<span class="visually-hidden" role="status">Loading...</span> <span class="visually-hidden" role="status">{{ _('Loading...') }}</span>
<i class="bi bi-plus-lg me-1"></i> New Export <i class="bi bi-plus-lg me-1"></i> {{ _('New Export') }}
</button> </button>
</form> </form>
{% if exports %} {% if exports %}
@ -46,8 +52,8 @@
<table class="table align-middle"> <table class="table align-middle">
<thead> <thead>
<tr> <tr>
<th>Date</th> <th>{{ _('Date') }}</th>
<th>Actions</th> <th>{{ _('Actions') }}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -55,8 +61,8 @@
<tr id="export-{{ export.timestamp_esc }}"> <tr id="export-{{ export.timestamp_esc }}">
<td>{{ export.timestamp }}</td> <td>{{ export.timestamp }}</td>
<td> <td>
<a href="/{{ export.downloadPath }}" class="btn btn-secondary btn-sm px-3 px-md-2"><i class="bi bi-download"></i> <span class="d-none d-lg-inline-block ms-1">Download</span></a> <a href="/{{ export.downloadPath }}" class="btn btn-secondary btn-sm px-3 px-md-2"><i class="bi bi-download"></i> <span class="d-none d-lg-inline-block ms-1">{{ _('Download') }}</span></a>
<button class="btn btn-outline-danger btn-sm px-3 px-md-2" hx-target="#export-{{ export.timestamp_esc }}" hx-swap="delete" hx-delete="{{ url_for('api.deleteExport', timestamp=export.timestamp_esc) }}"><i class="bi bi-trash"></i> <span class="d-none d-lg-inline-block ms-1">Delete</span></button> <button class="btn btn-outline-danger btn-sm px-3 px-md-2" hx-target="#export-{{ export.timestamp_esc }}" hx-swap="delete" hx-delete="{{ url_for('api.deleteExport', timestamp=export.timestamp_esc) }}"><i class="bi bi-trash"></i> <span class="d-none d-lg-inline-block ms-1">{{ _('Delete') }}</span></button>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
@ -64,7 +70,7 @@
</table> </table>
</div> </div>
{% else %} {% else %}
<p class="text-center my-3"><i>No exports created yet</i></p> <p class="text-center my-3">{{ _('No exports created yet') }}</p>
{% endif %} {% endif %}
</div> </div>
{% endblock %} {% endblock %}

View file

@ -1,47 +1,41 @@
{% extends 'admin/base.html' %} {% extends 'admin/base.html' %}
{% block _title %}Information{% endblock %} {% block _title %}{{ _('Information') }}{% endblock %}
{% set info_link = 'active' %} {% set info_link = 'active' %}
{% block _content %} {% block _content %}
<form hx-post="{{ url_for('api.updateConfig') }}" hx-target="#response-container" hx-swap="none"> <form hx-post="{{ url_for('api.updateConfig') }}" hx-target="#response-container" hx-swap="none">
<h2 id="instance" class="mb-2 fw-normal">Information</h2> <h2 id="instance" class="mb-2 fw-normal">{{ _('Information') }}</h2>
<p class="fs-5 h3 text-body-secondary mb-3">Essential information about your {{ const.appName }} instance</p> <p class="fs-5 h3 text-body-secondary mb-3">{{ _('Essential information about your {} instance').format(const.appName) }}</p>
<div class="form-group mb-3"> <div class="form-group mb-3">
<label class="form-label" for="instance.title">Title <small class="text-body-secondary">(e.g. My question box)</small></label> <label class="form-label" for="instance.title">{{ _('Title') }} <small class="text-body-secondary">{{ _('(e.g. My question box)') }}</small></label>
<input type="text" id="instance.title" name="instance.title" value="{{ cfg.instance.title }}" class="form-control"> <input type="text" id="instance.title" name="instance.title" value="{{ cfg.instance.title }}" class="form-control">
<p class="form-text">Title of this CatAsk instance</p> <p class="form-text">{{ _('Title of this {} instance').format(const.appName) }}</p>
</div> </div>
<div class="form-group mb-3"> <div class="form-group mb-3">
<label class="form-label d-flex flex-column flex-lg-row align-items-lg-center justify-content-between" for="instance.description"> <label class="form-label d-flex flex-column flex-lg-row align-items-lg-center justify-content-between" for="instance.description">
<span>Description <small class="text-body-secondary">(e.g. Ask me a question!)</small></span> <span>{{ _('Description') }} <small class="text-body-secondary">{{ _('(e.g. Ask me a question!)') }}</small></span>
<small class="text-body-secondary"><i class="bi bi-markdown me-1"></i> Markdown supported</small> <small class="text-body-secondary"><i class="bi bi-markdown me-1"></i> {{ _('Markdown supported') }}</small>
</label> </label>
<textarea spellcheck="false" id="instance.description" name="instance.description" class="form-control" style="height: 200px; resize: vertical;">{{ cfg.instance.description }}</textarea> <textarea spellcheck="false" id="instance.description" name="instance.description" class="form-control" style="height: 200px; resize: vertical;">{{ cfg.instance.description }}</textarea>
<p class="form-text">Description of this CatAsk instance</p> <p class="form-text">{{ _('Description of this {} instance').format(const.appName) }}</p>
</div> </div>
<div class="form-group mb-3"> <div class="form-group mb-3">
<label class="form-label d-flex flex-column flex-lg-row align-items-lg-center justify-content-between" for="instance.rules"> <label class="form-label d-flex flex-column flex-lg-row align-items-lg-center justify-content-between" for="instance.rules">
<span>Rules</span> <span>{{ _('Rules') }}</span>
<small class="text-body-secondary"><i class="bi bi-markdown me-1"></i> Markdown supported</small> <small class="text-body-secondary"><i class="bi bi-markdown me-1"></i> {{ _('Markdown supported') }}</small>
</label> </label>
<textarea spellcheck="false" id="instance.rules" name="instance.rules" class="form-control" style="height: 200px; resize: vertical;">{{ cfg.instance.rules }}</textarea> <textarea spellcheck="false" id="instance.rules" name="instance.rules" class="form-control" style="height: 200px; resize: vertical;">{{ cfg.instance.rules }}</textarea>
<p class="form-text">Rules of this CatAsk instance</p> <p class="form-text">{{ _('Rules of this {} instance').format(const.appName) }}</p>
</div> </div>
<div class="form-group mb-3"> <div class="form-group mb-3">
<label class="form-label" for="instance.image">Relative image path <small class="text-body-secondary">(default: /static/icons/favicon/android-chrome-512x512.png)</small></label> <label class="form-label" for="instance.image">{{ _('Relative image path') }}</label>
<input type="text" id="instance.image" name="instance.image" value="{{ cfg.instance.image }}" class="form-control"> <input type="text" id="instance.image" name="instance.image" value="{{ cfg.instance.image }}" placeholder="/static/icons/favicon/android-chrome-512x512.png" class="form-control">
<p class="form-text">Image that's going to be used in a link preview</p> <p class="form-text">{{ _("Image that's going to be used in a link preview") }}</p>
</div> </div>
<div class="form-group mb-2"> <div class="form-group mb-2">
<label class="form-label" for="instance.fullBaseUrl">Base URL <small class="text-body-secondary">(e.g. https://ask.example.com)</small></label> <label class="form-label" for="instance.fullBaseUrl">{{ _('Base URL') }}</label>
<input type="text" id="instance.fullBaseUrl" name="instance.fullBaseUrl" value="{{ cfg.instance.fullBaseUrl }}" class="form-control"> <input type="text" id="instance.fullBaseUrl" name="instance.fullBaseUrl" value="{{ cfg.instance.fullBaseUrl }}" placeholder="https://ask.example.com" class="form-control">
<p class="form-text">Full URL to homepage of this CatAsk instance without a trailing slash</p> <p class="form-text">{{ _('Full URL to homepage of this {} instance without a trailing slash').format(const.appName) }}</p>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary" id="saveConfig">
<span class="spinner-border spinner-border-sm htmx-indicator" aria-hidden="true"></span>
<span class="visually-hidden" role="status">Loading...</span>
Save
</button>
</div> </div>
{% include 'snippets/admin/saveBtn.html' %}
</form> </form>
{% endblock %} {% endblock %}

View file

@ -1,10 +1,10 @@
{% extends 'admin/base.html' %} {% extends 'admin/base.html' %}
{% block _title %}Notifications{% endblock %} {% block _title %}{{ _('Notifications') }}{% endblock %}
{% set notif_link = 'active' %} {% set notif_link = 'active' %}
{% block _content %} {% block _content %}
<form hx-post="{{ url_for('api.updateConfig') }}" hx-target="#response-container" hx-swap="none" hx-disabled-elt="#saveConfig"> <form hx-post="{{ url_for('api.updateConfig') }}" hx-target="#response-container" hx-swap="none" hx-disabled-elt="#saveConfig">
<h2 id="general" class="mb-22 fw-normal">Notifications</h2> <h2 id="general" class="mb-22 fw-normal">{{ _('Notifications') }}</h2>
<p class="fs-5 h3 text-body-secondary mb-3">Configure notifications for new questions using <a href="https://ntfy.sh/" target="_blank">ntfy</a></p> <p class="fs-5 h3 text-body-secondary mb-3">{{ _('Configure notifications for new questions using') }} <a href="https://ntfy.sh/" target="_blank">ntfy</a></p>
<div class="form-check form-switch mb-3"> <div class="form-check form-switch mb-3">
<input <input
class="form-check-input" class="form-check-input"
@ -15,37 +15,31 @@
role="switch" role="switch"
{% if cfg.ntfy.enabled %}checked{% endif %}> {% if cfg.ntfy.enabled %}checked{% endif %}>
<input type="hidden" id="ntfy.enabled" name="ntfy.enabled" value="{{ cfg.ntfy.enabled }}"> <input type="hidden" id="ntfy.enabled" name="ntfy.enabled" value="{{ cfg.ntfy.enabled }}">
<label for="_ntfy.enabled" class="form-check-label">Enabled</label> <label for="_ntfy.enabled" class="form-check-label">{{ _('Enabled') }}</label>
</div> </div>
<p class="form-label">Server &amp; Topic</p> <p class="form-label">{{ _('Server &amp; Topic') }}</p>
<div class="input-group mb-4"> <div class="input-group mb-4">
<input type="text" id="ntfy.host" name="ntfy.host" value="{{ cfg.ntfy.host }}" class="form-control" aria-label="Server"> <input type="text" id="ntfy.host" name="ntfy.host" value="{{ cfg.ntfy.host }}" class="form-control" aria-label="{{ _('Server') }}">
<span class="input-group-text">/</span> <span class="input-group-text">/</span>
<input type="text" id="ntfy.topic" name="ntfy.topic" value="{{ cfg.ntfy.topic }}" class="form-control" aria-label="Topic"> <input type="text" id="ntfy.topic" name="ntfy.topic" value="{{ cfg.ntfy.topic }}" class="form-control" aria-label="{{ _('Topic') }}">
</div> </div>
<h3 class="fw-light mb-2">Credentials (optional)</h3> <h3 class="fw-light mb-2">{{ _('Credentials (optional)') }}</h3>
<p class="text-body-secondary mb-3">Set credentials if the topic is protected</p> <p class="text-body-secondary mb-3">{{ _('Set credentials if the topic is protected') }}</p>
<div class="form-group mb-3 mt-2"> <div class="form-group mb-3 mt-2">
<label class="form-label" for="ntfy.user">Username</label> <label class="form-label" for="ntfy.user">{{ _('Username') }}</label>
<input type="text" id="ntfy.user" name="ntfy.user" value="{{ cfg.ntfy.user }}" class="form-control"> <input type="text" id="ntfy.user" name="ntfy.user" value="{{ cfg.ntfy.user }}" class="form-control">
<p class="form-text"> <p class="form-text">
Topic user {{ _('Topic user') }}
</p> </p>
</div> </div>
<div class="form-group mb-3"> <div class="form-group mb-3">
<label class="form-label" for="ntfy.pass">Password</label> <label class="form-label" for="ntfy.pass">{{ _('Password') }}</label>
<input type="password" id="ntfy.pass" name="ntfy.pass" value="{{ cfg.ntfy.pass }}" class="form-control"> <input type="password" id="ntfy.pass" name="ntfy.pass" value="{{ cfg.ntfy.pass }}" class="form-control">
<p class="form-text"> <p class="form-text">
Topic password {{ _('Topic password') }}
</p> </p>
</div> </div>
<div class="form-group"> {% include 'snippets/admin/saveBtn.html' %}
<button type="submit" class="btn btn-primary mt-3" id="saveConfig">
<span class="spinner-border spinner-border-sm htmx-indicator" aria-hidden="true"></span>
<span class="visually-hidden" role="status">Loading...</span>
Save
</button>
</div>
</form> </form>
{% endblock %} {% endblock %}
{% block _scripts %} {% block _scripts %}

View file

@ -1,22 +1,22 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block title %}Admin Login{% endblock %} {% block title %}{{ _('Admin Login') }}{% endblock %}
{% set loginLink = 'active' %} {% set loginLink = 'active' %}
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col-lg-4 m-auto mt-5"> <div class="col-lg-3 m-auto mt-5">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<h2 class="text-center mb-4 mt-2">Login to {{ cfg.instance.title }}</h2> <h2 class="text-center mb-4 mt-2">{{ _('Login to {}').format(cfg.instance.title) }}</h2>
<form action="{{ url_for('admin.login') }}" method="POST"> <form action="{{ url_for('admin.login') }}" method="POST">
<div class="form-floating mb-2"> <div class="form-floating mb-2">
<input type="password" required class="form-control" id="admin_password" name="admin_password" placeholder="Password"> <input type="password" required class="form-control" id="admin_password" name="admin_password" placeholder="{{ _('Password') }}">
<label for="admin_password">Password</label> <label for="admin_password">{{ _('Password') }}</label>
</div> </div>
<div class="form-check mb-3"> <div class="form-check mb-3">
<input type="checkbox" class="form-check-input" id="remember_me" name="remember_me" checked> <input type="checkbox" class="form-check-input" id="remember_me" name="remember_me" checked>
<label class="form-check-label" for="remember_me">Stay logged in</label> <label class="form-check-label" for="remember_me">{{ _('Stay logged in') }}</label>
</div> </div>
<button type="submit" class="btn btn-primary w-100">Login</button> <button type="submit" class="btn btn-primary w-100">{{ _('Login') }}</button>
</form> </form>
</div> </div>
</div> </div>

View file

@ -1,26 +1,26 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block title %}Inbox {% if len(questions) > 0 %}({{ len(questions) }}){% endif %}{% endblock %} {% block title %}{{ _('Inbox') }} {% if len(questions) > 0 %}({{ len(questions) }}){% endif %}{% endblock %}
{% set inboxLink = 'active' %} {% set inboxLink = 'active' %}
{% block additionalHeadItems %} {% block additionalHeadItems %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/toastify.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/toastify.css') }}">
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% if questions != [] %} {% if questions != [] %}
<h3 class="fs-4"><span id="question-count-inbox">{{ len(questions) }}</span> <span class="fw-light">question(s)</span></h3> <h3 class="fs-4"><span id="question-count-inbox">{{ len(questions) }}</span> <span class="fw-light">{{ _('question(s)') }}</span></h3>
<div class="row"> <div class="row">
{% for question in questions %} {% for question in questions %}
<div class="col-lg-8 m-auto"> <div class="col-lg-8 m-auto">
<div class="card mb-3 mt-3 alert-placeholder question" id="question-{{ question.id }}"> <div class="card mb-3 mt-3 alert-placeholder question" id="question-{{ question.id }}">
<div class="card-header"> <div class="card-header">
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<h5 class="card-title mt-1 mb-1 markdown-content w-50"> <h5 class="card-title mt-1 mb-0 markdown-content w-50">
{% if question.from_who %} {% if question.from_who %}
{{ render_markdown(question.from_who, fromWho=True) }} {{ render_markdown(question.from_who, fromWho=True) }}
{% else %} {% else %}
<i class="bi bi-incognito" data-bs-toggle="tooltip" data-bs-title="This question was asked anonymously" data-bs-placement="top"></i> {{ cfg.anonName }} <i class="bi bi-incognito" data-bs-toggle="tooltip" data-bs-title="{{ _('This question was asked anonymously') }}" data-bs-placement="top"></i> {{ cfg.anonName }}
{% endif %} {% endif %}
</h5> </h5>
<h6 class="card-subtitle fw-light text-body-secondary"> <h6 class="card-subtitle mt-1 fw-light text-body-secondary">
{# {#
reserved for version 1.6.0 or later reserved for version 1.6.0 or later
@ -38,8 +38,8 @@
{{ question.content | render_markdown }} {{ question.content | render_markdown }}
</div> </div>
<button class="z-0 cw-btn btn btn-sm btn-secondary shadow text-center w-100 sticky-bottom" type="button" data-bs-toggle="collapse" data-bs-target="#question-cw-{{ question.id }}" aria-expanded="false" aria-controls="question-cw-{{ question.id }}"> <button class="z-0 cw-btn btn btn-sm btn-secondary shadow text-center w-100 sticky-bottom" type="button" data-bs-toggle="collapse" data-bs-target="#question-cw-{{ question.id }}" aria-expanded="false" aria-controls="question-cw-{{ question.id }}">
<span class="fw-medium cw-btn-text">Show content</span> <span class="fw-medium cw-btn-text">{{ _('Show content') }}</span>
<span class="text-body-secondary cw-btn-chars">({{ len(question.content) }} characters)</span> <span class="text-body-secondary cw-btn-chars">({{ _("{} characters").format(len(question.content)) }})</span>
</button> </button>
{% else %} {% else %}
{{ question.content | render_markdown }} {{ question.content | render_markdown }}
@ -85,15 +85,15 @@
<div class="modal-dialog modal-dialog-centered"> <div class="modal-dialog modal-dialog-centered">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header border-0"> <div class="modal-header border-0">
<h1 class="modal-title fs-5 fw-normal" id="q-{{ question.id }}-modal-label">Confirmation</h1> <h1 class="modal-title fs-5 fw-normal" id="q-{{ question.id }}-modal-label">{{ _('Confirmation') }}</h1>
<button type="button" class="btn-close d-flex align-items-center fs-5" data-bs-dismiss="modal" aria-label="Close"><i class="bi bi-x-lg"></i></button> <button type="button" class="btn-close d-flex align-items-center fs-5" data-bs-dismiss="modal" aria-label="Close"><i class="bi bi-x-lg"></i></button>
</div> </div>
<div class="modal-body pt-0 pb-0"> <div class="modal-body pt-0 pb-0">
<p>Are you sure you want to delete this question?</p> <p>{{ _('Are you sure you want to delete this question?') }}</p>
</div> </div>
<div class="modal-footer pt-1 border-0"> <div class="modal-footer pt-1 border-0 flex-column flex-sm-row align-items-stretch align-items-sm-center">
<button type="button" class="btn btn-outline-secondary flex-fill flex-sm-grow-0" data-bs-dismiss="modal">Cancel</button> <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">{{ _('Cancel') }}</button>
<button type="button" class="btn btn-danger flex-fill flex-sm-grow-0" data-bs-dismiss="modal" hx-delete="{{ url_for('api.deleteQuestion', question_id=question.id) }}" hx-target="#question-{{ question.id }}" hx-swap="none">Confirm</button> <button type="button" class="btn btn-danger" data-bs-dismiss="modal" hx-delete="{{ url_for('api.deleteQuestion', question_id=question.id) }}" hx-target="#question-{{ question.id }}" hx-swap="none">{{ _('Confirm') }}</button>
</div> </div>
</div> </div>
</div> </div>
@ -103,7 +103,7 @@
{% endfor %} {% endfor %}
</div> </div>
{% else %} {% else %}
<h2 class="text-center mt-5">Inbox is currently empty.</h2> <h2 class="text-center mt-5">{{ _('Inbox is currently empty.') }}</h2>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}