mirror of
https://codeberg.org/catask-org/catask.git
synced 2025-04-19 21:33:41 -05:00
babel stuff
This commit is contained in:
parent
38b012c276
commit
cb22ef5c23
14 changed files with 261 additions and 236 deletions
56
app.py
56
app.py
|
@ -246,7 +246,7 @@ def login():
|
|||
session.permanent = request.form.get('remember_me', False)
|
||||
return redirect(url_for('index'))
|
||||
else:
|
||||
flash("Wrong password", 'danger')
|
||||
flash(_("Wrong password"), 'danger')
|
||||
return redirect(url_for('admin.login'))
|
||||
else:
|
||||
if logged_in:
|
||||
|
@ -321,7 +321,7 @@ def blacklist():
|
|||
blacklist = request.form.get('blacklist')
|
||||
with open(const.blacklistFile, 'w') as file:
|
||||
file.write(blacklist)
|
||||
return {'message': 'Blacklist updated!'}, 200
|
||||
return {'message': _("Blacklist updated!")}, 200
|
||||
|
||||
else:
|
||||
blacklist = func.readPlainFile(const.blacklistFile)
|
||||
|
@ -355,7 +355,7 @@ def postInstall():
|
|||
# -- server routes --
|
||||
|
||||
@api_bp.errorhandler(404)
|
||||
def notFound():
|
||||
def notFound(e):
|
||||
return jsonify({'error': 'Not found'}), 404
|
||||
|
||||
@api_bp.errorhandler(400)
|
||||
|
@ -523,9 +523,9 @@ def addAnswer():
|
|||
cw = request.form.get('cw', '')
|
||||
|
||||
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:
|
||||
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)
|
||||
|
||||
|
@ -543,24 +543,24 @@ def uploadFavicon():
|
|||
|
||||
func.generateFavicon(filename)
|
||||
|
||||
return {'message': 'Successfully updated favicon!'}, 201
|
||||
return {'message': _("Successfully updated favicon!")}, 201
|
||||
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:
|
||||
return {'error': "favicon is not specified"}, 400
|
||||
return {'error': _("favicon is not specified")}, 400
|
||||
|
||||
@api_bp.route('/upload_emoji/', methods=['POST'])
|
||||
@loginRequired
|
||||
def uploadEmoji():
|
||||
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']
|
||||
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):
|
||||
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
|
||||
filename = secure_filename(emoji.filename)
|
||||
|
@ -569,20 +569,20 @@ def uploadEmoji():
|
|||
|
||||
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'])
|
||||
@loginRequired
|
||||
def uploadEmojiPack():
|
||||
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']
|
||||
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):
|
||||
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)
|
||||
archive_name = func.stripArchExtension(filename)
|
||||
|
@ -601,15 +601,15 @@ def uploadEmojiPack():
|
|||
with tarfile.open(archive_path, 'r:*') as tar_ref:
|
||||
tar_ref.extractall(extract_path)
|
||||
else:
|
||||
return jsonify({'error': 'Unsupported archive format'}), 400
|
||||
return jsonify({'error': _("Unsupported archive format")}), 400
|
||||
|
||||
# parse meta.json if it exists
|
||||
meta_json_path = extract_path / 'meta.json'
|
||||
if meta_json_path.exists():
|
||||
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:
|
||||
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:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
@ -654,11 +654,11 @@ def deleteEmoji():
|
|||
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():
|
||||
return jsonify({'error': 'Emoji not found'}), 404
|
||||
return jsonify({'error': _("Emoji not found")}), 404
|
||||
|
||||
try:
|
||||
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:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
@ -670,14 +670,14 @@ def deleteEmojiPack():
|
|||
emoji_base_path = Path.cwd() / 'static' / 'emojis' / pack_name.lower()
|
||||
|
||||
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:
|
||||
for json_file in emojis_path.glob(f'{pack_name.lower()}.json'):
|
||||
json_file.unlink()
|
||||
|
||||
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:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
@ -760,10 +760,10 @@ def deleteExport():
|
|||
def viewQuestion():
|
||||
question_id = request.args.get('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()
|
||||
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,))
|
||||
question = cursor.fetchone()
|
||||
cursor.close()
|
||||
|
@ -775,10 +775,10 @@ def viewQuestion():
|
|||
def viewAnswer():
|
||||
answer_id = request.args.get('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()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT id, question_id, creation_date, content FROM answers WHERE id=%s", (answer_id,))
|
||||
answer = cursor.fetchone()
|
||||
cursor.close()
|
||||
|
@ -809,7 +809,9 @@ def updateConfig():
|
|||
|
||||
func.saveJSON(cfg, const.configFile)
|
||||
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(admin_bp, url_prefix='/admin')
|
||||
|
|
21
functions.py
21
functions.py
|
@ -24,6 +24,25 @@ import constants as const
|
|||
|
||||
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
|
||||
def loadJSON(file_path):
|
||||
# open the file
|
||||
|
@ -197,7 +216,7 @@ def addQuestion(from_who, question, cw, noAntispam=False):
|
|||
antispam_valid = antispam in antispam_wordlist
|
||||
if not antispam_valid:
|
||||
# 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
|
||||
elif cfg['antispam']['type'] == 'recaptcha':
|
||||
r = requests.post(
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
{% extends 'base.html' %}
|
||||
{% block title %}{% block _title %}{% endblock %} - Admin{% endblock %}
|
||||
{% block title %}{% block _title %}{% endblock %} - {{ _('Admin') }}{% endblock %}
|
||||
{% set adminLink = 'active' %}
|
||||
{% block additionalHeadItems %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/toastify.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/coloris.min.css') }}">
|
||||
{% block _additionalHeadItems %}{% endblock %}
|
||||
<style>
|
||||
[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");
|
||||
|
@ -39,39 +40,43 @@
|
|||
<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') }}">
|
||||
<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 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>
|
||||
<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 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>
|
||||
<span class="sidebar-btn-text ms-2 ps-1">Notifications</span>
|
||||
<span class="sidebar-btn-text ms-2 ps-1">{{ _('Notifications') }}</span>
|
||||
</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') }}">
|
||||
<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 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>
|
||||
<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 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>
|
||||
<span class="sidebar-btn-text ms-2 ps-1">General</span>
|
||||
<span class="sidebar-btn-text ms-2 ps-1">{{ _('General') }}</span>
|
||||
</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') }}">
|
||||
<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 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>
|
||||
<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 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>
|
||||
<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>
|
||||
<hr class="d-block d-md-none mt-4">
|
||||
</div>
|
||||
|
|
|
@ -1,19 +1,20 @@
|
|||
{% extends 'admin/base.html' %}
|
||||
{% block _title %}Accessibility{% endblock %}
|
||||
{% block _title %}{{ _('Accessibility') }}{% endblock %}
|
||||
{% set access_link = 'active' %}
|
||||
{% block _content %}
|
||||
<form hx-post="{{ url_for('api.updateConfig') }}" hx-target="#response-container" hx-swap="none">
|
||||
<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>
|
||||
<h2 id="general" class="mb-2 fw-normal">{{ _('Accessibility') }}</h2>
|
||||
<p class="fs-5 h3 text-body-secondary mb-3">{{ _('Make {} accessible to everyone').format(const.appName) }}</p>
|
||||
<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">
|
||||
<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="atkinson"{% if cfg.accessibility.font == 'atkinson' %} selected{% endif %}>Atkinson Hyperlegible</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="atkinson"{% if cfg.accessibility.font == 'atkinson' %} selected{% endif %}>{{ _('Atkinson Hyperlegible') }}</option>
|
||||
</select>
|
||||
</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">
|
||||
<input
|
||||
class="form-check-input"
|
||||
|
@ -24,22 +25,16 @@
|
|||
role="switch"
|
||||
{% if cfg.accessibility.userway.enabled %}checked{% endif %}>
|
||||
<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 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">
|
||||
<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>
|
||||
</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>
|
||||
{% include 'snippets/admin/saveBtn.html' %}
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% block _scripts %}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
{% extends 'admin/base.html' %}
|
||||
{% block _title %}Anti-spam{% endblock %}
|
||||
{% block _title %}{{ _('Anti-spam') }}{% endblock %}
|
||||
{% set antispam_link = 'active' %}
|
||||
{% block _content %}
|
||||
<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>
|
||||
<p class="fs-5 h3 text-body-secondary mb-3">Protect your {{ const.appName }} instance from spammers</p>
|
||||
<h2 id="antispam" class="mb-2 fw-normal">{{ _('Anti-spam') }}</h2>
|
||||
<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">
|
||||
<input
|
||||
class="form-check-input"
|
||||
|
@ -15,73 +15,68 @@
|
|||
role="switch"
|
||||
{% if cfg.antispam.enabled %}checked{% endif %}>
|
||||
<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>
|
||||
{% if not cfg.antispam.enabled %}
|
||||
<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">
|
||||
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.
|
||||
{{ _("It's highly encouraged to keep anti-spam enabled, otherwise you could become a target of a spam attack.") }}<br><br>
|
||||
{#- 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>
|
||||
</div>
|
||||
{% 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)">
|
||||
<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="frc"{% if cfg.antispam.type == 'frc' %} selected{% endif %}>Friendly Captcha</option>
|
||||
<option value="recaptcha"{% if cfg.antispam.type == 'recaptcha' %} selected{% endif %}>reCAPTCHA v2</option>
|
||||
</select>
|
||||
<p class="form-text">Anti-spam type to use</p>
|
||||
<p class="form-text">{{ _('Anti-spam type to use') }}</p>
|
||||
</div>
|
||||
<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">
|
||||
<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">
|
||||
<p class="form-text">reCAPTCHA site key</p>
|
||||
<p class="form-text">{{ _('reCAPTCHA site key') }}</p>
|
||||
</div>
|
||||
<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">
|
||||
<p class="form-text">reCAPTCHA secret key</p>
|
||||
<p class="form-text">{{ _('reCAPTCHA secret key') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<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">
|
||||
<p class="form-text">Turnstile site key</p>
|
||||
<p class="form-text">{{ _('Turnstile site key') }}</p>
|
||||
</div>
|
||||
<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">
|
||||
<p class="form-text">Turnstile secret key</p>
|
||||
<p class="form-text">{{ _('Turnstile secret key') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<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">
|
||||
<p class="form-text">Friendly Captcha site key</p>
|
||||
<p class="form-text">{{ _('Friendly Captcha site key') }}</p>
|
||||
</div>
|
||||
<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">
|
||||
<p class="form-text">Friendly Captcha API key</p>
|
||||
<p class="form-text">{{ _('Friendly Captcha API key') }}</p>
|
||||
</div>
|
||||
</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>
|
||||
{% include 'snippets/admin/saveBtn.html' %}
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% block _scripts %}
|
||||
|
@ -103,7 +98,7 @@
|
|||
} else if (element.value === 'frc') {
|
||||
turnstileDiv.classList.add('d-none');
|
||||
recaptchaDiv.classList.add('d-none');
|
||||
frcDiv.classList.remove('d-none');
|
||||
frcDiv.classList.remove('d-none');
|
||||
} else {
|
||||
recaptchaDiv.classList.add('d-none');
|
||||
turnstileDiv.classList.add('d-none');
|
||||
|
|
|
@ -1,20 +1,16 @@
|
|||
{% extends 'admin/base.html' %}
|
||||
{% block _title %}Blacklist{% endblock %}
|
||||
{% block _title %}{{ _('Word blacklist') }}{% endblock %}
|
||||
{% set blacklist_link = 'active' %}
|
||||
{% block _content %}
|
||||
<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>
|
||||
<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>
|
||||
<form hx-put="{{ url_for('api.updateBlacklist') }}" hx-target="#response-container" hx-swap="none">
|
||||
<input type="hidden" name="action" value="update_word_blacklist">
|
||||
<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> -->
|
||||
<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">
|
||||
<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>
|
||||
{% include 'snippets/admin/saveBtn.html' %}
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,36 +1,61 @@
|
|||
{% extends 'admin/base.html' %}
|
||||
{% block _title %}Customize{% endblock %}
|
||||
{% block _title %}{{ _('Customize') }}{% endblock %}
|
||||
{% 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 %}
|
||||
<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>
|
||||
<h3 class="fw-light">Favicon</h3>
|
||||
<h2 id="customize" class="mb-2 fw-normal">{{ _('Customize') }}</h2>
|
||||
<p class="fs-5 h3 text-body-secondary mb-3">{{ _('Customize {} to your liking').format(const.appName) }}</p>
|
||||
<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">
|
||||
<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">
|
||||
<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">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<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="visually-hidden" role="status">Loading...</span>
|
||||
<i class="bi bi-file-earmark-arrow-up me-1"></i> Upload
|
||||
<span class="visually-hidden" role="status">{{ _('Loading...') }}</span>
|
||||
<i class="bi bi-file-earmark-arrow-up me-1"></i> {{ _('Upload') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<form hx-post="{{ url_for('api.updateConfig') }}" hx-target="#response-container" hx-swap="none">
|
||||
<h3 class="fw-light">Accent color</h3>
|
||||
<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>
|
||||
{# <h4 class="fw-light">Color</h4> #}
|
||||
<div class="form-group d-flex flex-column">
|
||||
<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">
|
||||
<p class="form-text">Default: <b>#6345d9</b></p>
|
||||
<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') }}">
|
||||
<p class="form-text">{{ _('Default') }}: <b>#6345d9</b></p>
|
||||
</div>
|
||||
<div class="form-group d-flex flex-column">
|
||||
<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">
|
||||
<p class="form-text">Default: <b>#7a63e3</b></p>
|
||||
<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') }}">
|
||||
<p class="form-text">{{ _('Default') }}: <b>#7a63e3</b></p>
|
||||
</div>
|
||||
{# brain doesn't feel like implementing this rn (9/27/24)
|
||||
<h4 class="fw-light mt-2">Background</h4>
|
||||
|
@ -55,7 +80,7 @@
|
|||
role="switch"
|
||||
{% if cfg.style.tintColors == true %}checked{% endif %}>
|
||||
<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 class="form-check form-switch mb-1">
|
||||
<input
|
||||
|
@ -67,9 +92,9 @@
|
|||
role="switch"
|
||||
{% if cfg.style.navIcons == true %}checked{% endif %}>
|
||||
<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>
|
||||
<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">
|
||||
<input
|
||||
class="form-check-input nav-icons-only-checkbox"
|
||||
|
@ -80,7 +105,7 @@
|
|||
role="switch"
|
||||
{% if cfg.style.navIconsOnly == true %}checked{% endif %}>
|
||||
<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 class="form-check form-switch mb-3">
|
||||
|
|
|
@ -1,49 +1,49 @@
|
|||
{% extends 'admin/base.html' %}
|
||||
{% block _title %}Emojis{% endblock %}
|
||||
{% block _title %}{{ _('Emojis') }}{% endblock %}
|
||||
{% set emojis_link = 'active' %}
|
||||
{% block _content %}
|
||||
<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>
|
||||
<h3 class="fw-light mb-3">Upload</h3>
|
||||
<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 {} instance').format(const.appName) }}</p>
|
||||
<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">
|
||||
<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">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<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="visually-hidden" role="status">Loading...</span>
|
||||
<i class="bi bi-file-earmark-plus me-1"></i> Upload emoji
|
||||
<span class="visually-hidden" role="status">{{ _('Loading...') }}</span>
|
||||
<i class="bi bi-file-earmark-plus me-1"></i> {{ _('Upload emoji') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<hr>
|
||||
<form id="pack-form" hx-post="{{ url_for('api.uploadEmojiPack') }}" hx-target="#response-container" hx-swap="none" hx-encoding="multipart/form-data">
|
||||
<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">
|
||||
<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 class="mb-5">
|
||||
<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="visually-hidden" role="status">Loading...</span>
|
||||
<i class="bi bi-file-earmark-zip me-1"></i> Upload pack
|
||||
<span class="visually-hidden" role="status">{{ _('Loading...') }}</span>
|
||||
<i class="bi bi-file-earmark-zip me-1"></i> {{ _('Upload pack') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<h3 class="fw-light mb-3">Manage</h3>
|
||||
<h4 class="fw-light">Emojis</h4>
|
||||
<h3 class="fw-light mb-3">{{ _('Manage') }}</h3>
|
||||
<h4 class="fw-light">{{ _('Emojis') }}</h4>
|
||||
{% if emojis %}
|
||||
<div class="table-responsive">
|
||||
<table id="emojiTable" class="table align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Image</th>
|
||||
<th>Name</th>
|
||||
<th>Filename</th>
|
||||
<th>Actions</th>
|
||||
<th>{{ _('Image') }}</th>
|
||||
<th>{{ _('Name') }}</th>
|
||||
<th>{{ _('Filename') }}</th>
|
||||
<th>{{ _('Actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="emojis">
|
||||
|
@ -54,23 +54,23 @@
|
|||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-center">No emojis uploaded</p>
|
||||
<p class="text-center">{{ _('No emojis uploaded') }}</p>
|
||||
{% endif %}
|
||||
<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>
|
||||
<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>
|
||||
{% if packs %}
|
||||
<div class="table-responsive">
|
||||
<table id="emojiPackTable" class="table align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Image</th>
|
||||
<th>Name</th>
|
||||
<th>{{ _('Image') }}</th>
|
||||
<th>{{ _('Name') }}</th>
|
||||
{% if json_pack %}
|
||||
<th>Author</th>
|
||||
<th>Released</th>
|
||||
<th>{{ _('Author') }}</th>
|
||||
<th>{{ _('Released') }}</th>
|
||||
{% endif %}
|
||||
<th>Folder</th>
|
||||
<th>Actions</th>
|
||||
<th>{{ _('Folder') }}</th>
|
||||
<th>{{ _('Actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="packs">
|
||||
|
@ -83,6 +83,6 @@
|
|||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-center">No emoji packs uploaded</p>
|
||||
<p class="text-center">{{ _('No emoji packs uploaded') }}</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
role="switch"
|
||||
{% if cfg.lockInbox %}checked{% endif %}>
|
||||
<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 class="form-check form-switch mb-2">
|
||||
<input
|
||||
|
@ -37,7 +37,7 @@
|
|||
role="switch"
|
||||
{% if cfg.allowAnonQuestions %}checked{% endif %}>
|
||||
<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 class="form-check form-switch mb-2">
|
||||
<input
|
||||
|
@ -49,7 +49,7 @@
|
|||
role="switch"
|
||||
{% if cfg.noDeleteConfirm %}checked{% endif %}>
|
||||
<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 class="form-check form-switch mb-3">
|
||||
<input
|
||||
|
@ -61,15 +61,9 @@
|
|||
role="switch"
|
||||
{% if cfg.showQuestionCount %}checked{% endif %}>
|
||||
<input type="hidden" id="showQuestionCount" name="showQuestionCount" value="{{ cfg.showQuestionCount }}">
|
||||
<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>
|
||||
<label for="_showQuestionCount" class="form-check-label">{{ _('Show question count in homepage') }}</label>
|
||||
</div>
|
||||
{% include 'snippets/admin/saveBtn.html' %}
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% block _scripts %}
|
||||
|
|
|
@ -1,20 +1,26 @@
|
|||
{% extends 'admin/base.html' %}
|
||||
{% block _title %}Import/Export{% endblock %}
|
||||
{% block _title %}{{ _('Import/Export') }}{% endblock %}
|
||||
{% set import_link = 'active' %}
|
||||
{% block _content %}
|
||||
<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>
|
||||
<h3 id="import" class="mb-3 fw-light">Import</h3>
|
||||
<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 {} instance data').format(const.appName) }}</p>
|
||||
<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">
|
||||
<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>
|
||||
<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>
|
||||
<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="visually-hidden" role="status">Loading...</span>
|
||||
<i class="bi bi-database-up me-1"></i> Import
|
||||
<span class="visually-hidden" role="status">{{ _('Loading...') }}</span>
|
||||
<i class="bi bi-database-up me-1"></i> {{ _('Import') }}
|
||||
</button>
|
||||
</form>
|
||||
{#
|
||||
|
@ -32,13 +38,13 @@
|
|||
</button>
|
||||
</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">
|
||||
<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">
|
||||
<span class="spinner-border spinner-border-sm htmx-indicator" aria-hidden="true"></span>
|
||||
<span class="visually-hidden" role="status">Loading...</span>
|
||||
<i class="bi bi-plus-lg me-1"></i> New Export
|
||||
<span class="visually-hidden" role="status">{{ _('Loading...') }}</span>
|
||||
<i class="bi bi-plus-lg me-1"></i> {{ _('New Export') }}
|
||||
</button>
|
||||
</form>
|
||||
{% if exports %}
|
||||
|
@ -46,8 +52,8 @@
|
|||
<table class="table align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Actions</th>
|
||||
<th>{{ _('Date') }}</th>
|
||||
<th>{{ _('Actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -55,8 +61,8 @@
|
|||
<tr id="export-{{ export.timestamp_esc }}">
|
||||
<td>{{ export.timestamp }}</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>
|
||||
<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>
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
@ -64,7 +70,7 @@
|
|||
</table>
|
||||
</div>
|
||||
{% 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 %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,47 +1,41 @@
|
|||
{% extends 'admin/base.html' %}
|
||||
{% block _title %}Information{% endblock %}
|
||||
{% block _title %}{{ _('Information') }}{% endblock %}
|
||||
{% set info_link = 'active' %}
|
||||
{% block _content %}
|
||||
<form hx-post="{{ url_for('api.updateConfig') }}" hx-target="#response-container" hx-swap="none">
|
||||
<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>
|
||||
<h2 id="instance" class="mb-2 fw-normal">{{ _('Information') }}</h2>
|
||||
<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">
|
||||
<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">
|
||||
<p class="form-text">Title of this CatAsk instance</p>
|
||||
<p class="form-text">{{ _('Title of this {} instance').format(const.appName) }}</p>
|
||||
</div>
|
||||
<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">
|
||||
<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>
|
||||
<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>
|
||||
</label>
|
||||
<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 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">
|
||||
<span>Rules</span>
|
||||
<small class="text-body-secondary"><i class="bi bi-markdown me-1"></i> Markdown supported</small>
|
||||
<span>{{ _('Rules') }}</span>
|
||||
<small class="text-body-secondary"><i class="bi bi-markdown me-1"></i> {{ _('Markdown supported') }}</small>
|
||||
</label>
|
||||
<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 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>
|
||||
<input type="text" id="instance.image" name="instance.image" value="{{ cfg.instance.image }}" class="form-control">
|
||||
<p class="form-text">Image that's going to be used in a link preview</p>
|
||||
<label class="form-label" for="instance.image">{{ _('Relative image path') }}</label>
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
<input type="text" id="instance.fullBaseUrl" name="instance.fullBaseUrl" value="{{ cfg.instance.fullBaseUrl }}" class="form-control">
|
||||
<p class="form-text">Full URL to homepage of this CatAsk instance without a trailing slash</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>
|
||||
<label class="form-label" for="instance.fullBaseUrl">{{ _('Base URL') }}</label>
|
||||
<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 {} instance without a trailing slash').format(const.appName) }}</p>
|
||||
</div>
|
||||
{% include 'snippets/admin/saveBtn.html' %}
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
{% extends 'admin/base.html' %}
|
||||
{% block _title %}Notifications{% endblock %}
|
||||
{% block _title %}{{ _('Notifications') }}{% endblock %}
|
||||
{% set notif_link = 'active' %}
|
||||
{% block _content %}
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<div class="form-check form-switch mb-3">
|
||||
<input
|
||||
class="form-check-input"
|
||||
|
@ -15,37 +15,31 @@
|
|||
role="switch"
|
||||
{% if cfg.ntfy.enabled %}checked{% endif %}>
|
||||
<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>
|
||||
<p class="form-label">Server & Topic</p>
|
||||
<p class="form-label">{{ _('Server & Topic') }}</p>
|
||||
<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>
|
||||
<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>
|
||||
<h3 class="fw-light mb-2">Credentials (optional)</h3>
|
||||
<p class="text-body-secondary mb-3">Set credentials if the topic is protected</p>
|
||||
<h3 class="fw-light mb-2">{{ _('Credentials (optional)') }}</h3>
|
||||
<p class="text-body-secondary mb-3">{{ _('Set credentials if the topic is protected') }}</p>
|
||||
<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">
|
||||
<p class="form-text">
|
||||
Topic user
|
||||
{{ _('Topic user') }}
|
||||
</p>
|
||||
</div>
|
||||
<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">
|
||||
<p class="form-text">
|
||||
Topic password
|
||||
{{ _('Topic password') }}
|
||||
</p>
|
||||
</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>
|
||||
{% include 'snippets/admin/saveBtn.html' %}
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% block _scripts %}
|
||||
|
|
|
@ -1,22 +1,22 @@
|
|||
{% extends 'base.html' %}
|
||||
{% block title %}Admin Login{% endblock %}
|
||||
{% block title %}{{ _('Admin Login') }}{% endblock %}
|
||||
{% set loginLink = 'active' %}
|
||||
{% block content %}
|
||||
<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-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">
|
||||
<div class="form-floating mb-2">
|
||||
<input type="password" required class="form-control" id="admin_password" name="admin_password" placeholder="Password">
|
||||
<label for="admin_password">Password</label>
|
||||
<input type="password" required class="form-control" id="admin_password" name="admin_password" placeholder="{{ _('Password') }}">
|
||||
<label for="admin_password">{{ _('Password') }}</label>
|
||||
</div>
|
||||
<div class="form-check mb-3">
|
||||
<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>
|
||||
<button type="submit" class="btn btn-primary w-100">Login</button>
|
||||
<button type="submit" class="btn btn-primary w-100">{{ _('Login') }}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,26 +1,26 @@
|
|||
{% 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' %}
|
||||
{% block additionalHeadItems %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/toastify.css') }}">
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
{% 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">
|
||||
{% for question in questions %}
|
||||
<div class="col-lg-8 m-auto">
|
||||
<div class="card mb-3 mt-3 alert-placeholder question" id="question-{{ question.id }}">
|
||||
<div class="card-header">
|
||||
<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 %}
|
||||
{{ render_markdown(question.from_who, fromWho=True) }}
|
||||
{% 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 %}
|
||||
</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
|
||||
|
||||
|
@ -38,8 +38,8 @@
|
|||
{{ question.content | render_markdown }}
|
||||
</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 }}">
|
||||
<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="fw-medium cw-btn-text">{{ _('Show content') }}</span>
|
||||
<span class="text-body-secondary cw-btn-chars">({{ _("{} characters").format(len(question.content)) }})</span>
|
||||
</button>
|
||||
{% else %}
|
||||
{{ question.content | render_markdown }}
|
||||
|
@ -85,15 +85,15 @@
|
|||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<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>
|
||||
</div>
|
||||
<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 class="modal-footer pt-1 border-0">
|
||||
<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-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>
|
||||
<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" data-bs-dismiss="modal">{{ _('Cancel') }}</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>
|
||||
|
@ -103,7 +103,7 @@
|
|||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<h2 class="text-center mt-5">Inbox is currently empty.</h2>
|
||||
<h2 class="text-center mt-5">{{ _('Inbox is currently empty.') }}</h2>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block scripts %}
|
||||
|
|
Loading…
Add table
Reference in a new issue