mirror of
https://gitlab.linphone.org/BC/public/flexisip-account-manager.git
synced 2026-04-17 19:58:27 +00:00
Merge branch 'feature/441-Customize-App-Theme' into 'master'
FIX FLEXIAPI-441 add color picker and logo uploader to space settings See merge request BC/public/flexisip-account-manager!683
This commit is contained in:
commit
1023c858de
6 changed files with 280 additions and 25 deletions
|
|
@ -159,6 +159,7 @@ class SpaceController extends Controller
|
|||
'custom_provisioning_entries' => ['nullable', new Ini(Space::FORBIDDEN_KEYS)]
|
||||
]);
|
||||
|
||||
$space->theme_hue = $request->get('theme_hue');
|
||||
$space->copyright_text = $request->get('copyright_text');
|
||||
$space->intro_registration_text = $request->get('intro_registration_text');
|
||||
$space->newsletter_registration_address = $request->get('newsletter_registration_address');
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('spaces', function (Blueprint $table) {
|
||||
$table->integer('theme_hue')->nullable();
|
||||
$table->string('logo')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('spaces', function (Blueprint $table) {
|
||||
$table->dropColumn('theme_hue');
|
||||
$table->dropColumn('logo');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -121,6 +121,7 @@
|
|||
"Files": "Fichiers",
|
||||
"From": "Depuis",
|
||||
"Hello":"Bonjour",
|
||||
"Hex" : "Hex",
|
||||
"Host": "Hôte",
|
||||
"I accept the Privacy Policy": "J'accepte la Politique de Confidentialité",
|
||||
"I accept the Terms and Conditions": "J'accepte les Conditions Générales",
|
||||
|
|
@ -183,6 +184,7 @@
|
|||
"Phone Countries": "Numéros Internationaux",
|
||||
"Phone number": "Numéro de téléphone",
|
||||
"Phone registration": "Inscription par téléphone",
|
||||
"Plateforme customization": "Customisation de la plateforme",
|
||||
"Please enter the new email that you would like to link to your account.": "Veuillez entre l'adresse email que vous souhaitez lier à votre compte.",
|
||||
"Please enter the new phone number that you would like to link to your account.": "Veuillez entrer le numéro de téléphone que vous souhaitez lier à votre compte.",
|
||||
"Priority rule": "Rêgle prioritaire",
|
||||
|
|
@ -215,9 +217,11 @@
|
|||
"Scan the following QR Code using an authenticated device and wait a few seconds.": "Scanner le QR Code avec un appareil authentifié et attendez quelques secondes",
|
||||
"Search by username":"Rechercher par nom d'utilisateur",
|
||||
"Search": "Rechercher",
|
||||
"Select a color": "Sélectionner une couleur",
|
||||
"Select a contacts list": "Sélectionner une liste de contact",
|
||||
"Select a domain": "Sélectionner un domaine",
|
||||
"Select a file": "Choisir un fichier",
|
||||
"Select an image": "Choisir une image",
|
||||
"Send an email to the user to reset the password": "Envoyer un email à l'utilisateur pour réinitialiser son mot de passe",
|
||||
"Send an email to the user with provisioning information": "Envoyer un email à l'utilisateur avec les informations de déploiement",
|
||||
"Send": "Envoyer",
|
||||
|
|
@ -293,8 +297,9 @@
|
|||
"You can now continue your registration process in the application": "Vous pouvez maintenant continuer le processus d'inscription dans l'application",
|
||||
"You didn't receive the code?": "Vous n'avez pas reçu le code ?",
|
||||
"Your account recovery code":"Votre code de récupération de compte",
|
||||
"Your color will be snapped to the nearest 500 shade" : "La couleur que vous entrez sera automatiquement remplacée par sa variante 500 la plus proche.",
|
||||
"Your password was updated properly.": "Votre mot de passe a été mis à jour.",
|
||||
"Your password" : "Votre mot de passe",
|
||||
"Your space :space is expiring in :count days": "Votre espace :space expire dans :count jours",
|
||||
"Your space has expired. Access to your interface is now disabled, and your users can no longer benefit from the service. To reactivate your space, please contact your account manager.": "Votre espace est arrivé à expiration. L’accès à votre interface est désormais désactivé, et vos utilisateurs ne peuvent plus bénéficier du service. Pour réactiver votre espace, veuillez contacter votre responsable de compte."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
76
flexiapi/public/css/form.css
vendored
76
flexiapi/public/css/form.css
vendored
|
|
@ -183,7 +183,7 @@ form div .btn.oppose {
|
|||
right: 0;
|
||||
}
|
||||
|
||||
form div input,
|
||||
form div input:not([type=range]),
|
||||
form div textarea,
|
||||
form div select {
|
||||
padding: 1rem 2rem;
|
||||
|
|
@ -261,6 +261,7 @@ input[type=number].digit {
|
|||
height: 5rem;
|
||||
-moz-appearance: textfield;
|
||||
-webkit-appearance: textfield;
|
||||
appearance:textfield;
|
||||
border-radius: 1.5rem;
|
||||
background-color: white;
|
||||
padding: 0.5rem;
|
||||
|
|
@ -282,7 +283,7 @@ form div input[type=checkbox] {
|
|||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
form div input:not([type=checkbox]):not([type=radio]):not([type="number"].digit):not(.btn),
|
||||
form div input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type="number"].digit):not(.btn),
|
||||
form div textarea,
|
||||
form div select {
|
||||
margin-top: 2.5rem;
|
||||
|
|
@ -338,6 +339,75 @@ form div textarea:invalid:not(:placeholder-shown)+label {
|
|||
color: var(--danger);
|
||||
}
|
||||
|
||||
/* Logo Picker */
|
||||
|
||||
form canvas {
|
||||
background-color: var(--grey-1);
|
||||
border : 1px solid var(--grey-2);
|
||||
border-radius: 20px;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
/* Color Picker */
|
||||
|
||||
form div .color-picker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 110px;
|
||||
gap: 10px;
|
||||
padding-top: 10px;
|
||||
|
||||
}
|
||||
|
||||
.color-picker #color-div {
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
border : 1px solid var(--grey-2);
|
||||
border-radius: 20px;
|
||||
background-color: var(--main-5);
|
||||
|
||||
}
|
||||
|
||||
form input[type=range] {
|
||||
appearance: none;
|
||||
margin-top: 2.5rem;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
border-radius: 3rem;
|
||||
overflow: visible;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
hsl(0, 100%, 50%),
|
||||
hsl(60, 100%, 50%),
|
||||
hsl(120, 100%, 50%),
|
||||
hsl(180, 100%, 50%),
|
||||
hsl(240, 100%, 50%),
|
||||
hsl(300, 100%, 50%),
|
||||
hsl(360, 100%, 50%)
|
||||
);
|
||||
}
|
||||
|
||||
input[type=range]::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
border: 4px solid var(--grey-1);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Mandatory for Mozilla Firefox */
|
||||
input[type=range]::-moz-range-thumb {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
border: 4px solid var(--grey-1);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Checkbox element */
|
||||
|
||||
form div.checkbox {
|
||||
|
|
@ -417,4 +487,4 @@ div.select[data-value=sip_uri] ~ div.togglable.sip_uri {
|
|||
form section.block div.checkbox:has(input:not(:checked)) ~ div {
|
||||
opacity: 0.25;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
102
flexiapi/public/scripts/colorPicker.js
vendored
Normal file
102
flexiapi/public/scripts/colorPicker.js
vendored
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
// Color picker
|
||||
|
||||
let colorDiv = document.getElementById("color-div");
|
||||
let colorInput = document.getElementById("color-input");
|
||||
let themeHue = document.getElementById("theme_hue");
|
||||
let colorSlider = document.getElementById("color-slider");
|
||||
|
||||
colorSlider.addEventListener("input", (e) => {
|
||||
const hue = e.target.value;
|
||||
themeHue.value = hue;
|
||||
let color = hslToHex(hue,100, 50);
|
||||
colorDiv.style.backgroundColor = color;
|
||||
colorInput.value = color;
|
||||
});
|
||||
|
||||
colorInput.addEventListener("change", (e) => {
|
||||
const hex = e.target.value;
|
||||
let colorHsl = hexToHsl(hex);
|
||||
themeHue.value = colorHsl.h;
|
||||
colorSlider.value = colorHsl.h;
|
||||
let colorHex = hslToHex(colorHsl.h, 100, 50);
|
||||
colorInput.value = colorHex;
|
||||
colorDiv.style.backgroundColor = colorHex;
|
||||
});
|
||||
|
||||
function hslToHex(h, s, l) {
|
||||
s /= 100;
|
||||
l /= 100;
|
||||
|
||||
const k = n => (n + h / 30) % 12;
|
||||
const a = s * Math.min(l, 1 - l);
|
||||
const f = n => Math.round(255 * (l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)))));
|
||||
|
||||
const toHex = v => v.toString(16).padStart(2, '0');
|
||||
return `#${toHex(f(0))}${toHex(f(8))}${toHex(f(4))}`;
|
||||
}
|
||||
|
||||
function hexToHsl(hex) {
|
||||
hex = hex.replace(/^#/, '');
|
||||
if (hex.length === 3) hex = hex.split('').map(c => c + c).join('');
|
||||
|
||||
const r = parseInt(hex.slice(0, 2), 16) / 255;
|
||||
const g = parseInt(hex.slice(2, 4), 16) / 255;
|
||||
const b = parseInt(hex.slice(4, 6), 16) / 255;
|
||||
|
||||
const max = Math.max(r, g, b);
|
||||
const min = Math.min(r, g, b);
|
||||
const d = max - min;
|
||||
let h, s;
|
||||
let l = (max + min) / 2;
|
||||
|
||||
if (d === 0) {
|
||||
h = s = 0;
|
||||
} else {
|
||||
s = d / (l > 0.5 ? 2 - max - min : max + min);
|
||||
switch (max) {
|
||||
case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
|
||||
case g: h = ((b - r) / d + 2) / 6; break;
|
||||
case b: h = ((r - g) / d + 4) / 6; break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
h: Math.round(h * 360),
|
||||
s: Math.round(s * 100),
|
||||
l: Math.round(l * 100),
|
||||
};
|
||||
}
|
||||
|
||||
// Logo Picker
|
||||
|
||||
let imgInput = document.getElementById("logo-input");
|
||||
|
||||
imgInput.addEventListener("change", (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const url = URL.createObjectURL(file);
|
||||
const img = new Image();
|
||||
|
||||
img.onload = () => {
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
ctx.drawImage(img, 0, 0);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
img.src = url;
|
||||
|
||||
|
||||
});
|
||||
|
||||
const canvas = document.getElementById("logo-canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.fillStyle = "#4e6074";
|
||||
ctx.font = "20px Noto Sans";
|
||||
ctx.textBaseline = "middle";
|
||||
|
||||
const text = "Logo";
|
||||
const textWidth = ctx.measureText(text).width;
|
||||
|
||||
ctx.fillText(text, (canvas.width - textWidth) / 2, canvas.height / 2);
|
||||
|
|
@ -17,12 +17,39 @@
|
|||
@include('admin.space.head')
|
||||
@include('admin.space.tabs')
|
||||
|
||||
<form method="POST"
|
||||
action="{{ route('admin.spaces.configuration.update', $space) }}"
|
||||
accept-charset="UTF-8">
|
||||
<form method="POST" action="{{ route('admin.spaces.configuration.update', $space) }}" accept-charset="UTF-8">
|
||||
@csrf
|
||||
@method('put')
|
||||
|
||||
<h3 class="large">{{ __('Plateforme customization') }}</h3>
|
||||
|
||||
<div>
|
||||
<label for="logo-picker" style="font-weight: 700;">{{ __('Select an image') }} </label>
|
||||
<div class="color-picker">
|
||||
{{-- Gérer les types de fichiers accepter --}}
|
||||
<input id="logo-input" name="logo" type="file" accept=".png">
|
||||
@include('parts.errors', ['name' => 'logo'])
|
||||
<canvas id="logo-canvas"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="color-slider" style="font-weight: 700;">{{ __('Select a color') }} </label>
|
||||
<input name="theme_hue" id="theme_hue" type="text" style="display:none;" value="22">
|
||||
<div class="color-picker">
|
||||
<div id="color-div"></div>
|
||||
<div>
|
||||
<input id="color-input" type="text" value="#ff5e00">
|
||||
<label for="color-input">{{ __('Hex') }}</label>
|
||||
<input class="range" type="range" name="color-slider" id="color-slider" min="0" max="360"
|
||||
value="22">
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<span class="supporting"> {{ __('Your color will be snapped to the nearest 500 shade') }}</span>
|
||||
</div>
|
||||
|
||||
<div class="large">
|
||||
<textarea name="copyright_text" id="copyright_text">{{ $space->copyright_text }}</textarea>
|
||||
<label for="copyright_text">{{ __('Copyright text') }}</label>
|
||||
|
|
@ -37,21 +64,26 @@
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<input name="newsletter_registration_address" id="newsletter_registration_address" placeholder="email@server.tld" type="email" value="{{ $space->newsletter_registration_address }}">
|
||||
<input name="newsletter_registration_address" id="newsletter_registration_address"
|
||||
placeholder="email@server.tld" type="email" value="{{ $space->newsletter_registration_address }}">
|
||||
<label for="newsletter_registration_address">{{ __('Newsletter registration email address') }}</label>
|
||||
<span class="supporting">{{ __('An email will be sent to this email when someone join the newsletter') }}</span>
|
||||
<span
|
||||
class="supporting">{{ __('An email will be sent to this email when someone join the newsletter') }}</span>
|
||||
@include('parts.errors', ['name' => 'newsletter_registration_address'])
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input name="account_proxy_registrar_address" id="account_proxy_registrar_address" placeholder="server.tld" value="{{ $space->account_proxy_registrar_address }}">
|
||||
<input name="account_proxy_registrar_address" id="account_proxy_registrar_address" placeholder="server.tld"
|
||||
value="{{ $space->account_proxy_registrar_address }}">
|
||||
<label for="account_proxy_registrar_address">Account proxy registrar address</label>
|
||||
<span class="supporting">Will be used for informational purpose in the user panel and communication emails</span>
|
||||
<span class="supporting">Will be used for informational purpose in the user panel and communication
|
||||
emails</span>
|
||||
@include('parts.errors', ['name' => 'account_proxy_registrar_address'])
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input name="account_realm" @if ($space->accounts()->count() > 0)disabled @endif id="account_realm" placeholder="server.tld" value="{{ $space->account_realm }}">
|
||||
<input name="account_realm" @if ($space->accounts()->count() > 0) disabled @endif id="account_realm"
|
||||
placeholder="server.tld" value="{{ $space->account_realm }}">
|
||||
<label for="account_realm">Account realm</label>
|
||||
<span class="supporting">A custom realm for the Space accounts</span>
|
||||
@include('parts.errors', ['name' => 'account_realm'])
|
||||
|
|
@ -63,31 +95,52 @@
|
|||
<textarea style="min-height: 200px;" name="custom_provisioning_entries" id="custom_provisioning_entries">{{ $space->custom_provisioning_entries }}</textarea>
|
||||
<label for="custom_provisioning_entries">{{ __('Custom entries') }}</label>
|
||||
<span class="supporting">{{ __('In ini format, will complete the other settings.') }}</span>
|
||||
<span class="supporting">{{ __('Use ; to comment, key="value" to declare a complex string.') }} <a target="_blank" href="https://cheatsheets.zip/ini.html">{{ __('Checkout the cheatsheets to know how to format things correctly.') }}</a></span>
|
||||
<span class="supporting">{{ __('Use ; to comment, key="value" to declare a complex string.') }} <a
|
||||
target="_blank"
|
||||
href="https://cheatsheets.zip/ini.html">{{ __('Checkout the cheatsheets to know how to format things correctly.') }}</a></span>
|
||||
@include('parts.errors', ['name' => 'custom_provisioning_entries'])
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@include('parts.form.toggle', ['object' => $space, 'key' => 'custom_provisioning_overwrite_all', 'label' => __('Allow client settings to be overwritten by the provisioning ones')])
|
||||
@include('parts.form.toggle', [
|
||||
'object' => $space,
|
||||
'key' => 'custom_provisioning_overwrite_all',
|
||||
'label' => __('Allow client settings to be overwritten by the provisioning ones'),
|
||||
])
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@include('parts.form.toggle', ['object' => $space, 'key' => 'provisioning_use_linphone_provisioning_header', 'label' => 'Enforce X-Linphone-Provisioning header'])
|
||||
@include('parts.form.toggle', [
|
||||
'object' => $space,
|
||||
'key' => 'provisioning_use_linphone_provisioning_header',
|
||||
'label' => 'Enforce X-Linphone-Provisioning header',
|
||||
])
|
||||
</div>
|
||||
|
||||
<h3 class="large">{{ __('Features') }}</h3>
|
||||
<div>
|
||||
@include('parts.form.toggle', ['object' => $space, 'key' => 'public_registration', 'label' => __('Public registration')])
|
||||
</div>
|
||||
<div
|
||||
@include('parts.form.toggle', ['object' => $space, 'key' => 'phone_registration', 'label' => __('Phone registration')])
|
||||
</div>
|
||||
<div>
|
||||
@include('parts.form.toggle', ['object' => $space, 'key' => 'intercom_features', 'label' => __('Intercom features')])
|
||||
@include('parts.form.toggle', [
|
||||
'object' => $space,
|
||||
'key' => 'public_registration',
|
||||
'label' => __('Public registration'),
|
||||
])
|
||||
</div>
|
||||
<div @include('parts.form.toggle', [
|
||||
'object' => $space,
|
||||
'key' => 'phone_registration',
|
||||
'label' => __('Phone registration'),
|
||||
]) </div>
|
||||
<div>
|
||||
@include('parts.form.toggle', [
|
||||
'object' => $space,
|
||||
'key' => 'intercom_features',
|
||||
'label' => __('Intercom features'),
|
||||
])
|
||||
</div>
|
||||
|
||||
<div class="large">
|
||||
<input class="btn" type="submit" value="{{ __('Update') }}">
|
||||
</div>
|
||||
<div class="large">
|
||||
<input class="btn" type="submit" value="{{ __('Update') }}">
|
||||
</div>
|
||||
</form>
|
||||
<script src="{{ asset('scripts/colorPicker.js') }}"></script>
|
||||
@endsection
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue