Customize SOGo theme from Web UI

This commit is contained in:
FreddleSpl0it
2023-04-24 12:54:56 +02:00
parent 73570cc8b5
commit 4c495200c8
19 changed files with 831 additions and 57 deletions

View File

@@ -103,9 +103,12 @@ $template_data = [
'rsettings' => $rsettings,
'rspamd_regex_maps' => $rspamd_regex_maps,
'logo_specs' => customize('get', 'main_logo_specs'),
'favicon_specs' => customize('get', 'favicon_specs'),
'ip_check' => customize('get', 'ip_check'),
'password_complexity' => password_complexity('get'),
'show_rspamd_global_filters' => @$_SESSION['show_rspamd_global_filters'],
'sogo_palettes' => $GLOBALS['SOGO_PALETTES'],
'sogo_theme' => customize('get', 'sogo_theme'),
'lang_admin' => json_encode($lang['admin']),
'lang_datatables' => json_encode($lang['datatables'])
];

View File

@@ -5602,6 +5602,109 @@ paths:
description: You can list all mailboxes existing in system for a specific domain.
operationId: Get mailboxes of a domain
summary: Get mailboxes of a domain
/api/v1/edit/sogo_theme/:
post:
responses:
"401":
$ref: "#/components/responses/Unauthorized"
"200":
content:
application/json:
examples:
response:
value:
- type: success
log:
- customize
- edit
- sogo_theme
- primary: "brown"
accent: "brown"
background: "amber"
msg:
- sogo_theme_modified
schema:
properties:
log:
description: contains request object
items: {}
type: array
msg:
items: {}
type: array
type:
enum:
- success
- danger
- error
type: string
type: object
description: OK
headers: {}
tags:
- Customize
description: >-
Using this endpoint you can edit the sogo theme. SOGo has to be restarted after each change.
operationId: Edit SOGo theme
requestBody:
content:
application/json:
schema:
example:
primary: "brown"
accent: "brown"
background: "amber"
summary: Edit SOGo theme
/api/v1/delete/sogo_theme/:
post:
responses:
"401":
$ref: "#/components/responses/Unauthorized"
"200":
content:
application/json:
examples:
response:
value:
- type: success
log:
- customize
- delete
- sogo_theme
- items:
- sogo-theme
msg: "sogo_theme_removed"
schema:
properties:
log:
description: contains request object
items: {}
type: array
msg:
items: {}
type: array
type:
enum:
- success
- danger
- error
type: string
type: object
description: OK
headers: {}
tags:
- Customize
description: >-
Using this endpoint you can reset the sogo theme. SOGo has to be restarted after each change.
operationId: Reset SOGo theme
requestBody:
content:
application/json:
schema:
example:
- items:
- "sogo-theme"
summary: Reset SOGo theme
tags:
- name: Domains
@@ -5646,3 +5749,5 @@ tags:
description: Get the status of your cow
- name: Ratelimits
description: Edit domain ratelimits
- name: Customize
description: You can customize mailcow's appearance

View File

@@ -73,6 +73,81 @@ function customize($_action, $_item, $_data = null) {
);
return false;
}
$docker_return = docker('post', 'sogo-mailcow', 'exec', array('cmd' => 'sogo', 'task' => 'set_logo'));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => 'upload_success'
);
break;
case 'favicon':
if (in_array($_data['favicon']['type'], array('image/png', 'image/x-icon'))) {
try {
if (file_exists($_data['favicon']['tmp_name']) !== true) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => 'img_tmp_missing'
);
return false;
}
$image = new Imagick($_data['favicon']['tmp_name']);
if ($image->valid() !== true) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => 'img_invalid'
);
return false;
}
$available_sizes = array(32, 128, 180, 192, 256);
if ($image->getImageWidth() != $image->getImageHeight()) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => 'img_invalid'
);
return false;
}
if (!in_array($image->getImageWidth(), $available_sizes)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => 'img_invalid'
);
return false;
}
$image->destroy();
}
catch (ImagickException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => 'img_invalid'
);
return false;
}
}
else {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => 'invalid_mime_type'
);
return false;
}
try {
$redis->Set('FAVICON', 'data:' . $_data['favicon']['type'] . ';base64,' . base64_encode(file_get_contents($_data['favicon']['tmp_name'])));
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => array('redis_error', $e)
);
return false;
}
$docker_return = docker('post', 'sogo-mailcow', 'exec', array('cmd' => 'sogo', 'task' => 'set_favicon'));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_item, $_data),
@@ -179,6 +254,34 @@ function customize($_action, $_item, $_data = null) {
'msg' => 'ip_check_opt_in_modified'
);
break;
case 'sogo_theme':
$_data['primary'] = (isset($_data['primary']) && in_array($_data['primary'], $GLOBALS['SOGO_PALETTES'])) ? $_data['primary'] : 'green';
$_data['accent'] = (isset($_data['accent']) && in_array($_data['accent'], $GLOBALS['SOGO_PALETTES'])) ? $_data['accent'] : 'green';
$_data['background'] = (isset($_data['background']) && in_array($_data['background'], $GLOBALS['SOGO_PALETTES'])) ? $_data['background'] : 'grey';
try {
$redis->hSet('SOGO_THEME', 'primary', $_data['primary']);
$redis->hSet('SOGO_THEME', 'accent', $_data['accent']);
$redis->hSet('SOGO_THEME', 'background', $_data['background']);
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => array('redis_error', $e)
);
return false;
}
$docker_return = docker('post', 'sogo-mailcow', 'exec', array('cmd' => 'sogo', 'task' => 'customize_enable'));
$docker_return = docker('post', 'sogo-mailcow', 'exec', array('cmd' => 'sogo', 'task' => 'set_theme'));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => 'sogo_theme_modified'
);
return true;
break;
}
break;
case 'delete':
@@ -203,6 +306,7 @@ function customize($_action, $_item, $_data = null) {
case 'main_logo':
try {
if ($redis->del('MAIN_LOGO')) {
$docker_return = docker('post', 'sogo-mailcow', 'exec', array('cmd' => 'sogo', 'task' => 'remove_logo'));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_item, $_data),
@@ -219,6 +323,51 @@ function customize($_action, $_item, $_data = null) {
);
return false;
}
break;
case 'favicon':
try {
if ($redis->del('FAVICON')) {
$docker_return = docker('post', 'sogo-mailcow', 'exec', array('cmd' => 'sogo', 'task' => 'remove_favicon'));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => 'reset_favicon'
);
return true;
}
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => array('redis_error', $e)
);
return false;
}
break;
case 'sogo_theme':
try {
$redis->hSet('SOGO_THEME', 'primary', 'sogo-blue');
$redis->hSet('SOGO_THEME', 'accent', 'sogo-green');
$redis->hSet('SOGO_THEME', 'background', 'sogo-grey');
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => array('redis_error', $e)
);
return false;
}
$docker_return = docker('post', 'sogo-mailcow', 'exec', array('cmd' => 'sogo', 'task' => 'set_theme'));
$docker_return = docker('post', 'sogo-mailcow', 'exec', array('cmd' => 'sogo', 'task' => 'customize_disable'));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => 'sogo_theme_removed'
);
return true;
break;
}
break;
@@ -251,6 +400,19 @@ function customize($_action, $_item, $_data = null) {
return false;
}
break;
case 'favicon':
try {
return $redis->get('FAVICON');
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => array('redis_error', $e)
);
return false;
}
break;
case 'ui_texts':
try {
$data['title_name'] = ($title_name = $redis->get('TITLE_NAME')) ? $title_name : 'mailcow UI';
@@ -295,6 +457,25 @@ function customize($_action, $_item, $_data = null) {
return false;
}
break;
case 'favicon_specs':
try {
$image = new Imagick();
$img_data = explode('base64,', customize('get', 'favicon'));
if ($img_data[1]) {
$image->readImageBlob(base64_decode($img_data[1]));
return $image->identifyImage();
}
return false;
}
catch (ImagickException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => 'imagick_exception'
);
return false;
}
break;
case 'ip_check':
try {
$ip_check = ($ip_check = $redis->get('IP_CHECK')) ? $ip_check : 0;
@@ -309,6 +490,28 @@ function customize($_action, $_item, $_data = null) {
return false;
}
break;
case 'sogo_theme':
$data = array();
try {
$data['primary'] = $redis->hGet('SOGO_THEME', 'primary');
$data['accent'] = $redis->hGet('SOGO_THEME', 'accent');
$data['background'] = $redis->hGet('SOGO_THEME', 'background');
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => array('redis_error', $e)
);
return false;
}
$data['primary'] = empty($data['primary']) ? 'sogo-blue' : $data['primary'];
$data['accent'] = empty($data['accent']) ? 'sogo-green' : $data['accent'];
$data['background'] = empty($data['background']) ? 'sogo-grey' : $data['background'];
return $data;
break;
}
break;
}

View File

@@ -40,6 +40,7 @@ $globalVariables = [
'ui_texts' => $UI_TEXTS,
'css_path' => '/cache/'.basename($CSSPath),
'logo' => customize('get', 'main_logo'),
'favicon' => customize('get', 'favicon'),
'available_languages' => $AVAILABLE_LANGUAGES,
'lang' => $lang,
'skip_sogo' => (getenv('SKIP_SOGO') == 'y'),

View File

@@ -125,6 +125,14 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admi
if (isset($_POST["reset_main_logo"])) {
customize('delete', 'main_logo');
}
if (isset($_POST["submit_favicon"])) {
if ($_FILES['favicon']['error'] == 0) {
customize('add', 'favicon', $_FILES);
}
}
if (isset($_POST["reset_favicon"])) {
customize('delete', 'favicon');
}
// Some actions will not be available via API
if (isset($_POST["license_validate_now"])) {
license('verify');

View File

@@ -210,6 +210,30 @@ $FIDO2_USER_PRESENT_FLAG = true;
$FIDO2_FORMATS = array('apple', 'android-key', 'android-safetynet', 'fido-u2f', 'none', 'packed', 'tpm');
$SOGO_PALETTES = array(
'sogo-green',
'sogo-blue',
'sogo-grey',
'red',
'pink',
'purple',
'deep-purple',
'indigo',
'blue',
'light-blue',
'cyan',
'teal',
'green',
'light-green',
'lime',
'yellow',
'amber',
'orange',
'deep-orange',
'brown',
'grey',
'blue-grey'
);
// Set visible Rspamd maps in mailcow UI, do not change unless you know what you are doing
$RSPAMD_MAPS = array(

View File

@@ -288,18 +288,18 @@ if (isset($_GET['query'])) {
case "domain-admin":
process_add_return(domain_admin('add', $attr));
break;
case "sso":
switch ($object) {
case "domain-admin":
$data = domain_admin_sso('issue', $attr);
if($data) {
echo json_encode($data);
exit(0);
}
process_add_return($data);
break;
}
break;
case "sso":
switch ($object) {
case "domain-admin":
$data = domain_admin_sso('issue', $attr);
if($data) {
echo json_encode($data);
exit(0);
}
process_add_return($data);
break;
}
break;
case "admin":
process_add_return(admin('add', $attr));
break;
@@ -1744,6 +1744,9 @@ if (isset($_GET['query'])) {
case "rlhash":
echo ratelimit('delete', null, implode($items));
break;
case "sogo_theme":
process_delete_return(customize('delete', 'sogo_theme', $items));
break;
// return no route found if no case is matched
default:
http_response_code(404);
@@ -1938,6 +1941,9 @@ if (isset($_GET['query'])) {
case "ip_check":
process_edit_return(customize('edit', 'ip_check', $attr));
break;
case "sogo_theme":
process_edit_return(customize('edit', 'sogo_theme', $attr));
break;
case "self":
if ($_SESSION['mailcow_cc_role'] == "domainadmin") {
process_edit_return(domain_admin('edit', $attr));

View File

@@ -145,6 +145,7 @@
"ays": "Soll der Vorgang wirklich ausgeführt werden?",
"ban_list_info": "Übersicht ausgesperrter Netzwerke: <b>Netzwerk (verbleibende Bannzeit) - [Aktionen]</b>.<br />IPs, die zum Entsperren eingereiht werden, verlassen die Liste aktiver Banns nach wenigen Sekunden.<br />Rote Labels sind Indikatoren für aktive Blacklist-Einträge.",
"change_logo": "Logo ändern",
"change_favicon": "Favicon ändern",
"configuration": "Konfiguration",
"convert_html_to_text": "Konvertiere HTML zu reinem Text",
"credentials_transport_warning": "<b>Warnung</b>: Das Hinzufügen einer neuen Regel bewirkt die Aktualisierung der Authentifizierungsdaten aller vorhandenen Einträge mit identischem Next Hop.",
@@ -216,6 +217,7 @@
"loading": "Bitte warten...",
"login_time": "Zeit",
"logo_info": "Die hochgeladene Grafik wird für die Navigationsleiste auf eine Höhe von 40px skaliert. Für die Darstellung auf der Login-Maske beträgt die skalierte Breite maximal 250px. Eine frei skalierbare Grafik (etwa SVG) wird empfohlen.",
"favicon_info": "Das Bild muss eine PNG- oder ICO-Datei mit den Abmessungen <code>32 x 32</code>, <code>128 x 128</code>, <code>180 x 180</code>, <code>192 x 192</code> oder <code>256 x 256</code> sein. SOGo muss nachdem ändern neugestartet werden.",
"lookup_mx": "Ziel mit MX vergleichen (Regex, etwa <code>.*google\\.com</code>, um alle Ziele mit MX *google.com zu routen)",
"main_name": "\"mailcow UI\" Name",
"merged_vars_hint": "Ausgegraute Reihen wurden aus der Datei <code>vars.(local.)inc.php</code> gelesen und können hier nicht verändert werden.",
@@ -306,6 +308,8 @@
"sender": "Sender",
"service": "Dienst",
"service_id": "Service",
"sogo_theme": "SOGo Theme",
"sogo_theme_info": "SOGo muss nachdem ändern neugestartet werden.",
"source": "Quelle",
"spamfilter": "Spamfilter",
"subject": "Betreff",
@@ -1040,6 +1044,7 @@
"relayhost_added": "Map-Eintrag %s wurde hinzugefügt",
"relayhost_removed": "Map-Eintrag %s wurde entfernt",
"reset_main_logo": "Standardgrafik wurde wiederhergestellt",
"reset_favicon": "Standard favicon wurde wiederhergestellt",
"resource_added": "Ressource %s wurde angelegt",
"resource_modified": "Änderungen an Ressource %s wurden gespeichert",
"resource_removed": "Ressource %s wurde entfernt",
@@ -1052,6 +1057,8 @@
"template_modified": "Änderungen am Template %s wurden gespeichert",
"template_removed": "Template ID %s wurde gelöscht",
"sogo_profile_reset": "ActiveSync-Gerät des Benutzers %s wurde zurückgesetzt",
"sogo_theme_modified": "SOGo Theme wurde gespeichert",
"sogo_theme_removed": "SOGo Theme wurde entfernt",
"tls_policy_map_entry_deleted": "TLS-Richtlinie mit der ID %s wurde gelöscht",
"tls_policy_map_entry_saved": "TLS-Richtlinieneintrag \"%s\" wurde gespeichert",
"ui_texts": "Änderungen an UI-Texten",

View File

@@ -147,6 +147,7 @@
"ays": "Are you sure you want to proceed?",
"ban_list_info": "See a list of banned IPs below: <b>network (remaining ban time) - [actions]</b>.<br />IPs queued to be unbanned will be removed from the active ban list within a few seconds.<br />Red labels indicate active permanent bans by blacklisting.",
"change_logo": "Change logo",
"change_favicon": "Change favicon",
"configuration": "Configuration",
"convert_html_to_text": "Convert HTML to plain text",
"credentials_transport_warning": "<b>Warning</b>: Adding a new transport map entry will update the credentials for all entries with a matching next hop column.",
@@ -218,6 +219,7 @@
"loading": "Please wait...",
"login_time": "Login time",
"logo_info": "Your image will be scaled to a height of 40px for the top navigation bar and a max. width of 250px for the start page. A scalable graphic is highly recommended.",
"favicon_info": "The image has to be a PNG or ICO file in the dimensions <code>32 x 32</code>, <code>128 x 128</code>, <code>180 x 180</code>, <code>192 x 192</code>, or <code>256 x 256</code>. Restart SOGo after changing the favicon",
"lookup_mx": "Destination is a regular expression to match against MX name (<code>.*google\\.com</code> to route all mail targeted to a MX ending in google.com over this hop)",
"main_name": "\"mailcow UI\" name",
"merged_vars_hint": "Greyed out rows were merged from <code>vars.(local.)inc.php</code> and cannot be modified.",
@@ -311,6 +313,11 @@
"sender": "Sender",
"service": "Service",
"service_id": "Service ID",
"sogo_theme": "SOGo Theme",
"sogo_theme_info": "Restart SOGo after changing the theme.",
"sogo_theme_primary": "Primary Color:",
"sogo_theme_accent": "Accent Color:",
"sogo_theme_background": "Background Color:",
"source": "Source",
"spamfilter": "Spam filter",
"subject": "Subject",
@@ -1047,6 +1054,7 @@
"relayhost_added": "Map entry %s has been added",
"relayhost_removed": "Map entry %s has been removed",
"reset_main_logo": "Reset to default logo",
"reset_favicon": "Reset to default favicon",
"resource_added": "Resource %s has been added",
"resource_modified": "Changes to mailbox %s have been saved",
"resource_removed": "Resource %s has been removed",
@@ -1056,6 +1064,8 @@
"settings_map_added": "Added settings map entry",
"settings_map_removed": "Removed settings map ID %s",
"sogo_profile_reset": "SOGo profile for user %s was reset",
"sogo_theme_modified": "SOGo Theme has been modified",
"sogo_theme_removed": "SOGo Theme has been removed",
"template_added": "Added template %s",
"template_modified": "Changes to template %s have been saved",
"template_removed": "Template ID %s has been deleted",

View File

@@ -7,31 +7,89 @@
<span class="d-none d-md-block">{{ lang.admin.customize }}</span>
</div>
<div id="collapse-tab-config-customize" class="card-body collapse" data-bs-parent="#admin-content">
<legend><i class="bi bi-file-image"></i> {{ lang.admin.change_logo }}</legend><hr />
<p class="text-muted">{{ lang.admin.logo_info }}</p>
<form class="form-inline" role="form" method="post" enctype="multipart/form-data">
<p>
<input class="mb-4" type="file" name="main_logo" accept="image/gif, image/jpeg, image/pjpeg, image/x-png, image/png, image/svg+xml"><br>
<button name="submit_main_logo" type="submit" class="btn btn-sm d-block d-sm-inline btn-secondary"><i class="bi bi-upload"></i> {{ lang.admin.upload }}</button>
</p>
</form>
{% if logo %}
<div class="row">
<div class="col-sm-4">
<div class="thumbnail">
<div class="row">
<div class="col-12 col-lg-6 d-flex flex-column">
<legend><i class="bi bi-file-image"></i> {{ lang.admin.change_logo }}</legend><hr />
<p class="text-muted">{{ lang.admin.logo_info }}</p>
<form class="form-inline" role="form" method="post" enctype="multipart/form-data">
<p>
<input class="mb-4" type="file" name="main_logo" accept="image/gif, image/jpeg, image/pjpeg, image/x-png, image/png, image/svg+xml"><br>
<button name="submit_main_logo" type="submit" class="btn btn-sm d-block d-sm-inline btn-secondary"><i class="bi bi-upload"></i> {{ lang.admin.upload }}</button>
</p>
</form>
{% if logo %}
<div class="thumbnail mt-auto">
<img class="img-thumbnail" src="{{ logo }}" alt="mailcow logo">
<div class="caption">
<div class="caption d-flex flex-wrap mt-2 mb-4">
<span class="badge fs-5 bg-info">{{ logo_specs.geometry.width }}x{{ logo_specs.geometry.height }} px</span>
<span class="badge fs-5 bg-info">{{ logo_specs.mimetype }}</span>
<span class="badge fs-5 bg-info mx-2">{{ logo_specs.mimetype }}</span>
<span class="badge fs-5 bg-info">{{ logo_specs.fileSize }}</span>
</div>
</div>
<hr>
<form class="form-inline" role="form" method="post">
<p><button name="reset_main_logo" type="submit" class="btn btn-sm d-block d-sm-inline btn-secondary">{{ lang.admin.reset_default }}</button></p>
</form>
{% endif %}
</div>
<div class="col-12 col-lg-6 d-flex flex-column">
<legend><i class="bi bi-file-image"></i> {{ lang.admin.change_favicon }}</legend><hr />
<p class="text-muted">{{ lang.admin.favicon_info|raw }}</p>
<form class="form-inline" role="form" method="post" enctype="multipart/form-data">
<p>
<input class="mb-4" type="file" name="favicon" accept="image/x-icon, image/png"><br>
<button name="submit_favicon" type="submit" class="btn btn-sm d-block d-sm-inline btn-secondary"><i class="bi bi-upload"></i> {{ lang.admin.upload }}</button>
</p>
</form>
{% if favicon %}
<div class="thumbnail mt-auto">
<img class="img-thumbnail" src="{{ favicon }}" alt="mailcow favicon">
<div class="caption d-flex flex-wrap mt-2 mb-4">
<span class="badge fs-5 bg-info">{{ favicon_specs.geometry.width }}x{{ favicon_specs.geometry.height }} px</span>
<span class="badge fs-5 bg-info mx-2">{{ favicon_specs.mimetype }}</span>
<span class="badge fs-5 bg-info">{{ favicon_specs.fileSize }}</span>
</div>
</div>
<form class="form-inline" role="form" method="post">
<p><button name="reset_favicon" type="submit" class="btn btn-sm d-block d-sm-inline btn-secondary">{{ lang.admin.reset_default }}</button></p>
</form>
{% endif %}
</div>
</div>
{% if not skip_sogo %}
<legend style="padding-top:20px" unselectable="on">{{ lang.admin.sogo_theme }}</legend><hr />
<form class="form" data-id="sogo_theme" role="form" method="post">
<div class="mb-4 row">
<div class="col-12 col-md-4 mb-2">
<label class="d-block" for="sogo_primary">{{ lang.admin.sogo_theme_primary }}</label>
<select multiple data-width="100%" id="sogo_primary" name="primary" class="selectpicker show-tick" data-max-options="1" data-id="sogo_theme">
{% for sogo_palette in sogo_palettes %}
<option {% if sogo_palette == sogo_theme.primary %}selected{% endif %} value="{{ sogo_palette }}">{{ sogo_palette }}</option>
{% endfor %}
</select>
</div>
<div class="col-12 col-md-4 mb-2">
<label class="d-block" for="sogo_accent">{{ lang.admin.sogo_theme_accent }}</label>
<select multiple data-width="100%" id="sogo_accent" name="accent" class="selectpicker show-tick" data-max-options="1" data-id="sogo_theme">
{% for sogo_palette in sogo_palettes %}
<option {% if sogo_palette == sogo_theme.accent %}selected{% endif %} value="{{ sogo_palette }}">{{ sogo_palette }}</option>
{% endfor %}
</select>
</div>
<div class="col-12 col-md-4 mb-2">
<label class="d-block" for="sogo_background">{{ lang.admin.sogo_theme_background }}</label>
<select multiple data-width="100%" id="sogo_background" name="background" class="selectpicker show-tick" data-max-options="1" data-id="sogo_theme">
{% for sogo_palette in sogo_palettes %}
<option {% if sogo_palette == sogo_theme.background %}selected{% endif %} value="{{ sogo_palette }}">{{ sogo_palette }}</option>
{% endfor %}
</select>
</div>
</div>
<p class="text-muted">{{ lang.admin.sogo_theme_info }}</p>
<p><div class="btn-group">
<button class="btn btn-sm btn-xs-half d-block d-sm-inline btn-success" type="button" data-action="edit_selected" data-item="sogo-theme" data-id="sogo_theme" data-reload="no" data-api-url='edit/sogo_theme' data-api-attr='{}'><i class="bi bi-check-lg"></i> {{ lang.admin.save }}</button>
<button class="btn btn-sm btn-xs-half d-block d-sm-inline btn-danger" type="button" data-action="delete_selected" data-item="sogo-theme" data-id="sogo_theme" data-api-url="delete/sogo_theme"><i class="bi bi-trash"></i> {{ lang.admin.remove }}</button>
</div></p>
</form>
{% endif %}
<legend style="padding-top:20px" unselectable="on">{{ lang.admin.ip_check }}</legend><hr />
<div id="ip_check">

View File

@@ -23,8 +23,8 @@
}
</script>
<link rel="shortcut icon" href="/favicon.png" type="image/png">
<link rel="icon" href="/favicon.png" type="image/png">
<link rel="shortcut icon" href="{{ favicon|default('/favicon.png') }}" type="image/png">
<link rel="icon" href="{{ favicon|default('/favicon.png') }}" type="image/png">
</head>
<body>
<div class="overlay"></div>