Add variant support to the logo fetching feature

This commit is contained in:
Bubka 2025-06-13 16:46:28 +02:00
parent 7ddb4276a6
commit 6909be3318
14 changed files with 218 additions and 76 deletions

View File

@ -57,7 +57,11 @@ class IconController extends Controller
? $validated['iconCollection'] ? $validated['iconCollection']
: $request->user()->preferences['iconCollection']; : $request->user()->preferences['iconCollection'];
$icon = LogoLib::driver($iconCollection)->getIcon($validated['service']); $variant = Arr::has($validated, 'variant') && $validated['variant']
? $validated['variant']
: 'regular';
$icon = LogoLib::driver($iconCollection)->getIcon($validated['service'], $variant);
return $icon return $icon
? response()->json(['filename' => $icon], 201) ? response()->json(['filename' => $icon], 201)

View File

@ -24,10 +24,29 @@ class IconFetchRequest extends FormRequest
*/ */
public function rules() public function rules()
{ {
return [ $rules = [
'service' => 'string', 'service' => 'string',
'iconCollection' => 'nullable|string|in:tfa,selfh,dashboardicons', 'iconCollection' => 'sometimes|required|string|in:tfa,selfh,dashboardicons',
'variant' => [
'sometimes',
'required',
'string',
]
]; ];
if ($this->input('iconCollection', null) === 'selfh') {
$rules['variant'][] = 'in:regular,light,dark';
}
if ($this->input('iconCollection', null) === 'dashboardicons') {
$rules['variant'][] = 'in:regular,light,dark';
}
if ($this->input('iconCollection', null) === 'tfa') {
$rules['variant'][] = 'in:regular';
}
return $rules;
} }
/** /**
@ -40,7 +59,7 @@ class IconFetchRequest extends FormRequest
protected function prepareForValidation() protected function prepareForValidation()
{ {
$this->merge([ $this->merge([
'service' => strip_tags(strval($this->service)), 'service' => strip_tags(strval($this->input('service'))),
]); ]);
} }
} }

View File

@ -4,6 +4,7 @@ namespace App\Services\LogoLib;
use App\Facades\IconStore; use App\Facades\IconStore;
use App\Services\LogoLib\LogoLibInterface; use App\Services\LogoLib\LogoLibInterface;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
@ -25,14 +26,21 @@ abstract class AbstractLogoLib implements LogoLibInterface
*/ */
protected string $format = 'svg'; protected string $format = 'svg';
/**
* Suffix to append to the queried resource to get a specific variant
*/
protected string $variant = '';
/** /**
* Fetch a logo for the given service and save it as an icon * Fetch a logo for the given service and save it as an icon
* *
* @param string|null $serviceName Name of the service to fetch a logo for * @param string|null $serviceName Name of the service to fetch a logo for
* @param string|null $variant The theme variant to fetch (light, dark, etc...)
* @return string|null The icon filename or null if no logo has been found * @return string|null The icon filename or null if no logo has been found
*/ */
public function getIcon(?string $serviceName) : string|null public function getIcon(?string $serviceName, string $variant = null) : string|null
{ {
$this->setVariant($variant);
$logoFilename = $this->getLogo(strval($serviceName)); $logoFilename = $this->getLogo(strval($serviceName));
if (!$logoFilename) { if (!$logoFilename) {
@ -50,6 +58,20 @@ abstract class AbstractLogoLib implements LogoLibInterface
} }
} }
/**
* Sets the variant using passed parameter or default
*/
protected function setVariant(?string $variant) : void
{
if (! $variant || ! in_array($variant, ['regular', 'dark', 'light'])) {
$this->variant = Auth::user()
? Auth::user()->preferences['iconVariant']
: 'regular';
} else {
$this->variant = $variant;
}
}
/** /**
* Return the logo's filename for a given service * Return the logo's filename for a given service
* *
@ -59,7 +81,7 @@ abstract class AbstractLogoLib implements LogoLibInterface
protected function getLogo(string $serviceName) protected function getLogo(string $serviceName)
{ {
$referenceName = $this->sanitizeServiceName(strval($serviceName)); $referenceName = $this->sanitizeServiceName(strval($serviceName));
$logoFilename = $referenceName . '.' . $this->format; $logoFilename = $referenceName . $this->suffix() . '.' . $this->format;
$cachedFilename = $this->cachePrefix . $logoFilename; $cachedFilename = $this->cachePrefix . $logoFilename;
if ($referenceName && ! Storage::disk('logos')->exists($cachedFilename)) { if ($referenceName && ! Storage::disk('logos')->exists($cachedFilename)) {
@ -69,6 +91,28 @@ abstract class AbstractLogoLib implements LogoLibInterface
return Storage::disk('logos')->exists($cachedFilename) ? $cachedFilename : null; return Storage::disk('logos')->exists($cachedFilename) ? $cachedFilename : null;
} }
/**
* Suffix to append to the reference name to get a specific variant
*/
protected function suffix() : string
{
switch ($this->variant) {
case 'light':
$suffix = '-light';
break;
case 'dark':
$suffix = '-dark';
break;
default:
$suffix = '';
break;
}
return $suffix;
}
/** /**
* Url to use in http request to get a specific logo from the logo lib * Url to use in http request to get a specific logo from the logo lib
*/ */
@ -92,10 +136,13 @@ abstract class AbstractLogoLib implements LogoLibInterface
*/ */
protected function fetchLogo(string $logoFilename) : void protected function fetchLogo(string $logoFilename) : void
{ {
$url = $this->logoUrl($logoFilename);
try { try {
$response = Http::withOptions([ // $response = Http::withOptions([
'proxy' => config('2fauth.config.outgoingProxy'), // 'proxy' => config('2fauth.config.outgoingProxy'),
])->retry(3, 100)->get($this->logoUrl($logoFilename)); // ])->retry(3, 100)->get($url);
$response = Http::get($url);
if ($response->successful()) { if ($response->successful()) {
$filename = $this->cachePrefix . $logoFilename; $filename = $this->cachePrefix . $logoFilename;

View File

@ -4,5 +4,5 @@ namespace App\Services\LogoLib;
interface LogoLibInterface interface LogoLibInterface
{ {
public function getIcon(?string $serviceName): string|null; public function getIcon(?string $serviceName, string $variant = null): string|null;
} }

View File

@ -24,7 +24,7 @@ class TfaLogoLib extends AbstractLogoLib implements LogoLibInterface
/** /**
* @var string * @var string
*/ */
const TFA_URL = 'https://2fa.directory/api/v3/tfa.json'; const TFA_JSON_URL = 'https://2fa.directory/api/v3/tfa.json';
/** /**
* @var string * @var string
@ -45,7 +45,7 @@ class TfaLogoLib extends AbstractLogoLib implements LogoLibInterface
* @param string|null $serviceName Name of the service to fetch a logo for * @param string|null $serviceName Name of the service to fetch a logo for
* @return string|null The icon filename or null if no logo has been found * @return string|null The icon filename or null if no logo has been found
*/ */
public function getIcon(?string $serviceName) : string|null public function getIcon(?string $serviceName, string $variant = null) : string|null
{ {
$logoFilename = $this->getLogo(strval($serviceName)); $logoFilename = $this->getLogo(strval($serviceName));
@ -58,6 +58,14 @@ class TfaLogoLib extends AbstractLogoLib implements LogoLibInterface
} }
} }
/**
* Suffix to append to the reference name to get a specific variant
*/
protected function suffix() : string
{
return '';
}
/** /**
* Return the logo's filename for a given service * Return the logo's filename for a given service
* *
@ -104,7 +112,7 @@ class TfaLogoLib extends AbstractLogoLib implements LogoLibInterface
try { try {
$response = Http::withOptions([ $response = Http::withOptions([
'proxy' => config('2fauth.config.outgoingProxy'), 'proxy' => config('2fauth.config.outgoingProxy'),
])->retry(3, 100)->get(self::TFA_URL); ])->retry(3, 100)->get(self::TFA_JSON_URL);
$coll = collect(json_decode(htmlspecialchars_decode($response->body()), true)) /* @phpstan-ignore-line */ $coll = collect(json_decode(htmlspecialchars_decode($response->body()), true)) /* @phpstan-ignore-line */
->mapWithKeys(function ($item, $key) { ->mapWithKeys(function ($item, $key) {

View File

@ -14,6 +14,7 @@ $preferences = [
'displayMode' => envUnlessEmpty('USERPREF_DEFAULT__DISPLAY_MODE', 'list'), 'displayMode' => envUnlessEmpty('USERPREF_DEFAULT__DISPLAY_MODE', 'list'),
'showAccountsIcons' => envUnlessEmpty('USERPREF_DEFAULT__SHOW_ACCOUNTS_ICONS', true), 'showAccountsIcons' => envUnlessEmpty('USERPREF_DEFAULT__SHOW_ACCOUNTS_ICONS', true),
'iconCollection' => envUnlessEmpty('USERPREF_DEFAULT__ICON_COLLECTION', 'selfh'), 'iconCollection' => envUnlessEmpty('USERPREF_DEFAULT__ICON_COLLECTION', 'selfh'),
'iconVariant' => envUnlessEmpty('USERPREF_DEFAULT__ICON_VARIANT', 'regular'),
'kickUserAfter' => envUnlessEmpty('USERPREF_DEFAULT__KICK_USER_AFTER', 15), 'kickUserAfter' => envUnlessEmpty('USERPREF_DEFAULT__KICK_USER_AFTER', 15),
'activeGroup' => 0, 'activeGroup' => 0,
'rememberActiveGroup' => envUnlessEmpty('USERPREF_DEFAULT__REMEMBER_ACTIVE_GROUP', true), 'rememberActiveGroup' => envUnlessEmpty('USERPREF_DEFAULT__REMEMBER_ACTIVE_GROUP', true),

View File

@ -1,8 +1,8 @@
<script setup> <script setup>
import { useIdGenerator, useValidationErrorIdGenerator } from '@/composables/helpers' import { useIdGenerator, useValidationErrorIdGenerator } from '@/composables/helpers'
const model = defineModel()
const props = defineProps({ const props = defineProps({
modelValue: [String, Number, Boolean],
label: { label: {
type: String, type: String,
default: '' default: ''
@ -30,7 +30,6 @@
}, },
}) })
const selected = ref(props.modelValue)
const { inputId } = useIdGenerator('select', props.fieldName + props.idSuffix) const { inputId } = useIdGenerator('select', props.fieldName + props.idSuffix)
const { valErrorId } = useValidationErrorIdGenerator(props.fieldName) const { valErrorId } = useValidationErrorIdGenerator(props.fieldName)
const legendId = useIdGenerator('legend', props.fieldName + props.idSuffix).inputId const legendId = useIdGenerator('legend', props.fieldName + props.idSuffix).inputId
@ -49,8 +48,7 @@
<div class="select"> <div class="select">
<select <select
:id="inputId" :id="inputId"
v-model="selected" v-model="model"
v-on:change="$emit('update:modelValue', $event.target.value)"
:disabled="isDisabled || isLocked" :disabled="isDisabled || isLocked"
:aria-describedby="help ? legendId : undefined" :aria-describedby="help ? legendId : undefined"
:aria-invalid="fieldError != undefined" :aria-invalid="fieldError != undefined"

View File

@ -23,8 +23,8 @@ export default {
return apiClient.post('/twofaccounts', { uri: uri }, { ...config }) return apiClient.post('/twofaccounts', { uri: uri }, { ...config })
}, },
getLogo(service, iconCollection, config = {}) { getLogo(service, iconCollection, variant, config = {}) {
return apiClient.post('/icons/default', { service: service, iconCollection: iconCollection }, { ...config }) return apiClient.post('/icons/default', { service: service, iconCollection: iconCollection, variant: variant }, { ...config })
}, },
deleteIcon(icon, config = {}) { deleteIcon(icon, config = {}) {

View File

@ -24,10 +24,22 @@
{ text: 'settings.forms.automatic', value: 'system', icon: 'desktop' }, { text: 'settings.forms.automatic', value: 'system', icon: 'desktop' },
] ]
const iconCollections = [ const iconCollections = [
{ text: 'selfh.st', value: 'selfh', url: 'https://selfh.st/icons/' }, { text: 'selfh.st', value: 'selfh', url: 'https://selfh.st/icons/', defaultVariant: 'regular' },
{ text: 'dashboardicons.com', value: 'dashboardicons', url: 'https://dashboardicons.com/' }, { text: 'dashboardicons.com', value: 'dashboardicons', url: 'https://dashboardicons.com/', defaultVariant: 'regular' },
{ text: '2fa.directory', value: 'tfa', url: 'https://2fa.directory/' }, { text: '2fa.directory', value: 'tfa', url: 'https://2fa.directory/', defaultVariant: 'regular' },
] ]
const iconCollectionVariants = {
selfh: [
{ text: 'commons.regular', value: 'regular' },
{ text: 'settings.forms.light', value: 'light' },
{ text: 'settings.forms.dark', value: 'dark' },
],
dashboardicons: [
{ text: 'commons.regular', value: 'regular' },
{ text: 'settings.forms.light', value: 'light' },
{ text: 'settings.forms.dark', value: 'dark' },
]
}
const passwordFormats = [ const passwordFormats = [
{ text: '12 34 56', value: 2, legend: 'settings.forms.pair', title: 'settings.forms.pair_legend' }, { text: '12 34 56', value: 2, legend: 'settings.forms.pair', title: 'settings.forms.pair_legend' },
{ text: '123 456', value: 3, legend: 'settings.forms.trio', title: 'settings.forms.trio_legend' }, { text: '123 456', value: 3, legend: 'settings.forms.trio', title: 'settings.forms.trio_legend' },
@ -118,6 +130,28 @@
}) })
} }
/**
* Saves the iconCollection preference on the backend
* @param {string} preference
* @param {any} value
*/
function saveIconCollection(value) {
savePreference('iconCollection', value)
if (! Object.prototype.hasOwnProperty.call(iconCollectionVariants, value)) {
if (user.preferences.iconVariant != 'regular') {
user.preferences.iconVariant = 'regular'
userService.updatePreference('iconVariant', user.preferences.iconVariant)
}
}
else {
if (iconCollectionVariants[value].find((variant) => variant.value == user.preferences.iconVariant) == undefined) {
user.preferences.iconVariant = iconCollections.find((collection) => collection.value == value).defaultVariant
userService.updatePreference('iconVariant', user.preferences.iconVariant)
}
}
}
onBeforeRouteLeave((to) => { onBeforeRouteLeave((to) => {
if (! to.name.startsWith('settings.')) { if (! to.name.startsWith('settings.')) {
notify.clear() notify.clear()
@ -159,11 +193,13 @@
<!-- Official icons --> <!-- Official icons -->
<FormCheckbox v-model="user.preferences.getOfficialIcons" @update:model-value="val => savePreference('getOfficialIcons', val)" fieldName="getOfficialIcons" :isLocked="appSettings.lockedPreferences.includes('getOfficialIcons')" label="settings.forms.get_official_icons.label" help="settings.forms.get_official_icons.help" /> <FormCheckbox v-model="user.preferences.getOfficialIcons" @update:model-value="val => savePreference('getOfficialIcons', val)" fieldName="getOfficialIcons" :isLocked="appSettings.lockedPreferences.includes('getOfficialIcons')" label="settings.forms.get_official_icons.label" help="settings.forms.get_official_icons.help" />
<!-- icon collections --> <!-- icon collections -->
<FormSelect v-model="user.preferences.iconCollection" @update:model-value="val => savePreference('iconCollection', val)" :options="iconCollections" fieldName="iconCollection" :isLocked="appSettings.lockedPreferences.includes('iconCollection')" :isDisabled="!user.preferences.getOfficialIcons" label="settings.forms.icon_collection.label" help="settings.forms.icon_collection.help" :isIndented="true"> <FormSelect v-model="user.preferences.iconCollection" @update:model-value="val => saveIconCollection(val)" :options="iconCollections" fieldName="iconCollection" :isLocked="appSettings.lockedPreferences.includes('iconCollection')" :isDisabled="!user.preferences.getOfficialIcons" label="settings.forms.icon_collection.label" help="settings.forms.icon_collection.help" :isIndented="true">
<a class="button is-ghost" :href="iconCollectionUrl" target="_blank" :title="$t('commons.visit_x', { website: iconCollectionDomain})"> <a class="button is-ghost" :href="iconCollectionUrl" target="_blank" :title="$t('commons.visit_x', { website: iconCollectionDomain})">
<FontAwesomeIcon :icon="['fas', 'external-link-alt']" /> <FontAwesomeIcon :icon="['fas', 'external-link-alt']" />
</a> </a>
</FormSelect> </FormSelect>
<!-- icon variant -->
<FormSelect v-if="iconCollectionVariants[user.preferences.iconCollection]" v-model="user.preferences.iconVariant" @update:model-value="val => savePreference('iconVariant', val)" :options="iconCollectionVariants[user.preferences.iconCollection]" fieldName="iconVariant" :isLocked="appSettings.lockedPreferences.includes('iconVariant')" :isDisabled="!user.preferences.getOfficialIcons" label="settings.forms.icon_variant.label" help="settings.forms.icon_variant.help" :isIndented="true" />
<!-- password format --> <!-- password format -->
<FormCheckbox v-model="user.preferences.formatPassword" @update:model-value="val => savePreference('formatPassword', val)" fieldName="formatPassword" :isLocked="appSettings.lockedPreferences.includes('formatPassword')" label="settings.forms.password_format.label" help="settings.forms.password_format.help" /> <FormCheckbox v-model="user.preferences.formatPassword" @update:model-value="val => savePreference('formatPassword', val)" fieldName="formatPassword" :isLocked="appSettings.lockedPreferences.includes('formatPassword')" label="settings.forms.password_format.label" help="settings.forms.password_format.help" />
<FormToggle v-model="user.preferences.formatPasswordBy" @update:model-value="val => savePreference('formatPasswordBy', val)" :choices="passwordFormats" fieldName="formatPasswordBy" :isLocked="appSettings.lockedPreferences.includes('formatPasswordBy')" :isDisabled="!user.preferences.formatPassword" /> <FormToggle v-model="user.preferences.formatPasswordBy" @update:model-value="val => savePreference('formatPasswordBy', val)" :choices="passwordFormats" fieldName="formatPasswordBy" :isLocked="appSettings.lockedPreferences.includes('formatPasswordBy')" :isDisabled="!user.preferences.formatPassword" />

View File

@ -38,10 +38,22 @@
icon: null icon: null
})) }))
const iconCollections = [ const iconCollections = [
{ text: 'selfh.st', value: 'selfh' }, { text: 'selfh.st', value: 'selfh', asVariant: true },
{ text: 'dashboardicons.com', value: 'dashboardicons' }, { text: 'dashboardicons.com', value: 'dashboardicons', asVariant: true },
{ text: '2fa.directory', value: 'tfa' }, { text: '2fa.directory', value: 'tfa', asVariant: false },
] ]
const iconCollectionVariants = {
selfh: [
{ text: 'commons.regular', value: 'regular' },
{ text: 'settings.forms.light', value: 'light' },
{ text: 'settings.forms.dark', value: 'dark' },
],
dashboardicons: [
{ text: 'commons.regular', value: 'regular' },
{ text: 'settings.forms.light', value: 'light' },
{ text: 'settings.forms.dark', value: 'dark' },
]
}
const otpDisplayProps = ref({ const otpDisplayProps = ref({
otp_type: '', otp_type: '',
account : '', account : '',
@ -75,6 +87,8 @@
const ShowTwofaccountInModal = ref(false) const ShowTwofaccountInModal = ref(false)
const fetchingLogo = ref(false) const fetchingLogo = ref(false)
const iconCollection = ref(user.preferences.iconCollection) const iconCollection = ref(user.preferences.iconCollection)
const iconCollectionVariant = ref(user.preferences.iconVariant)
// $refs // $refs
const iconInput = ref(null) const iconInput = ref(null)
@ -152,6 +166,12 @@
} }
}) })
watch(iconCollection, (val) => {
iconCollectionVariant.value = Object.prototype.hasOwnProperty.call(iconCollectionVariants, val)
? iconCollectionVariants[val][0].value
: ''
})
watch(tempIcon, (val) => { watch(tempIcon, (val) => {
if( showQuickForm.value ) { if( showQuickForm.value ) {
nextTick().then(() => { nextTick().then(() => {
@ -385,17 +405,17 @@
if (user.preferences.getOfficialIcons) { if (user.preferences.getOfficialIcons) {
fetchingLogo.value = true fetchingLogo.value = true
twofaccountService.getLogo(form.service, iconCollection.value, { returnError: true }) twofaccountService.getLogo(form.service, iconCollection.value, iconCollectionVariant.value, { returnError: true })
.then(response => { .then(response => {
if (response.status === 201) { if (response.status === 201) {
// clean possible already uploaded temp icon // clean possible already uploaded temp icon
deleteTempIcon() deleteTempIcon()
tempIcon.value = response.data.filename; tempIcon.value = response.data.filename;
} }
else notify.warn( {text: trans('errors.no_logo_found_for_x', {service: strip_tags(form.service)}) }) else notify.warn( {text: trans('errors.no_icon_for_this_variant') })
}) })
.catch(() => { .catch(() => {
notify.warn({ text: trans('errors.no_logo_found_for_x', {service: strip_tags(form.service)}) }) notify.warn({ text: trans('errors.no_icon_for_this_variant') })
}) })
.finally(() => { .finally(() => {
fetchingLogo.value = false fetchingLogo.value = false
@ -492,52 +512,56 @@
<FormField v-model="form.account" fieldName="account" :fieldError="form.errors.get('account')" label="twofaccounts.account" :placeholder="$t('twofaccounts.forms.account.placeholder')" /> <FormField v-model="form.account" fieldName="account" :fieldError="form.errors.get('account')" label="twofaccounts.account" :placeholder="$t('twofaccounts.forms.account.placeholder')" />
<!-- icon upload --> <!-- icon upload -->
<label for="filUploadIcon" class="label">{{ $t('twofaccounts.icon') }}</label> <label for="filUploadIcon" class="label">{{ $t('twofaccounts.icon') }}</label>
<div class="columns is-mobile mb-0"> <!-- try my luck -->
<div class="column pt-0"> <!-- <fieldset v-if="user.preferences.getOfficialIcons" :disabled="!form.service"> -->
<!-- try my luck --> <div class="field has-addons">
<fieldset v-if="user.preferences.getOfficialIcons" :disabled="!form.service"> <div class="control">
<div class="field is-grouped"> <div class="select">
<div class="control"> <select :disabled="!form.service" name="icon-collection" v-model="iconCollection">
<VueButton @click="fetchLogo" :color="mode == 'dark' ? 'is-dark' : ''" :nativeType="'button'" :is-loading="fetchingLogo" aria-describedby="lgdTryMyLuck"> <option v-for="collection in iconCollections" :key="collection.text" :value="collection.value">
<span class="icon is-small"> {{ collection.text }}
<FontAwesomeIcon :icon="['fas', 'globe']" /> </option>
</span> </select>
<span>{{ $t('twofaccounts.forms.i_m_lucky') }}</span>
</VueButton>
</div>
<div class="control">
<div class="select">
<select name="icon-collection" v-model="iconCollection">
<option v-for="collection in iconCollections" :key="collection.text" :value="collection.value">
{{ collection.text }}
</option>
</select>
</div>
</div>
</div> </div>
</fieldset> </div>
<div class="field is-grouped"> <div v-if="iconCollectionVariants[iconCollection]" class="control">
<!-- upload icon button --> <div class="select">
<div class="control is-flex"> <select :disabled="!form.service" name="icon-collection-variant" v-model="iconCollectionVariant">
<div role="button" tabindex="0" class="file mr-3" :class="mode == 'dark' ? 'is-dark' : 'is-white'" @keyup.enter="iconInputLabel.click()"> <option v-for="variant in iconCollectionVariants[iconCollection]" :key="variant.value" :value="variant.value">
<label for="filUploadIcon" class="file-label" ref="iconInputLabel"> {{ $t(variant.text) }}
<input id="filUploadIcon" tabindex="-1" class="file-input" type="file" accept="image/*" v-on:change="uploadIcon" ref="iconInput"> </option>
<span class="file-cta"> </select>
<span class="file-icon">
<FontAwesomeIcon :icon="['fas', 'upload']" />
</span>
<span class="file-label">{{ $t('twofaccounts.forms.choose_image') }}</span>
</span>
</label>
</div>
<span class="tag is-large" :class="mode =='dark' ? 'is-dark' : 'is-white'" v-if="tempIcon">
<img class="icon-preview" :src="$2fauth.config.subdirectory + '/storage/icons/' + tempIcon" :alt="$t('twofaccounts.icon_to_illustrate_the_account')">
<button type="button" class="clear-selection delete is-small" @click.prevent="deleteTempIcon" :aria-label="$t('twofaccounts.remove_icon')"></button>
</span>
</div> </div>
</div> </div>
</div> </div>
<div class="column is-narrow" style="width: 200px;"> <!-- </fieldset> -->
<div class="field is-grouped">
<!-- try my luck button -->
<div class="control">
<VueButton @click="fetchLogo" :color="mode == 'dark' ? 'is-dark' : ''" :nativeType="'button'" :is-loading="fetchingLogo" :disabled="!form.service" aria-describedby="lgdTryMyLuck">
<span class="icon is-small">
<FontAwesomeIcon :icon="['fas', 'globe']" />
</span>
<span>{{ $t('twofaccounts.forms.i_m_lucky') }}</span>
</VueButton>
</div>
<!-- upload icon button -->
<div class="control is-flex">
<div role="button" tabindex="0" class="file mr-3" :class="mode == 'dark' ? 'is-dark' : 'is-white'" @keyup.enter="iconInputLabel.click()">
<label for="filUploadIcon" class="file-label" ref="iconInputLabel">
<input id="filUploadIcon" tabindex="-1" class="file-input" type="file" accept="image/*" v-on:change="uploadIcon" ref="iconInput">
<span class="file-cta">
<span class="file-icon">
<FontAwesomeIcon :icon="['fas', 'upload']" />
</span>
<span class="file-label">{{ $t('twofaccounts.forms.choose_image') }}</span>
</span>
</label>
</div>
<span class="tag is-large" :class="mode =='dark' ? 'is-dark' : 'is-white'" v-if="tempIcon">
<img class="icon-preview" :src="$2fauth.config.subdirectory + '/storage/icons/' + tempIcon" :alt="$t('twofaccounts.icon_to_illustrate_the_account')">
<button type="button" class="clear-selection delete is-small" @click.prevent="deleteTempIcon" :aria-label="$t('twofaccounts.remove_icon')"></button>
</span>
</div> </div>
</div> </div>
<div class="field"> <div class="field">

View File

@ -92,5 +92,6 @@ return [
'x_month' => ':x mos.', 'x_month' => ':x mos.',
'one_year' => '1 yr.', 'one_year' => '1 yr.',
'copy_next_password' => 'Copy next password to clipboard', 'copy_next_password' => 'Copy next password to clipboard',
'visit_x' => 'Visit :website' 'visit_x' => 'Visit :website',
'regular' => 'Regular',
]; ];

View File

@ -53,7 +53,7 @@ return [
'unsupported_migration' => 'Data do not match any supported format', 'unsupported_migration' => 'Data do not match any supported format',
'unsupported_otp_type' => 'Unsupported OTP type', 'unsupported_otp_type' => 'Unsupported OTP type',
'encrypted_migration' => 'Unreadable, the data seem encrypted', 'encrypted_migration' => 'Unreadable, the data seem encrypted',
'no_logo_found_for_x' => 'No logo available for :service', 'no_icon_for_this_variant' => 'No icon available in this variant',
'file_upload_failed' => 'File upload failed', 'file_upload_failed' => 'File upload failed',
'unauthorized' => 'Unauthorized', 'unauthorized' => 'Unauthorized',
'unauthorized_legend' => 'You do not have permissions to view this resource or to perform this action', 'unauthorized_legend' => 'You do not have permissions to view this resource or to perform this action',

View File

@ -126,8 +126,12 @@ return [
'help' => '(Try to) Get the official icon of the 2FA issuer when adding an account' 'help' => '(Try to) Get the official icon of the 2FA issuer when adding an account'
], ],
'icon_collection' => [ 'icon_collection' => [
'label' => 'Preferred icons source', 'label' => 'Favorite icon source',
'help' => 'The icons collection to be queried when an official icon is required. Changing this setting does not refresh icons that have already been fetched.' 'help' => 'The icons collection to be queried at first when an official icon is required. Changing this setting does not refresh icons that have already been fetched.'
],
'icon_variant' => [
'label' => 'Icon variant',
'help' => 'Some icons may be available in several flavors to best suit dark or light UIs. Set the one you want to look for first.'
], ],
'auto_lock' => [ 'auto_lock' => [
'label' => 'Auto lock', 'label' => 'Auto lock',

View File

@ -66,7 +66,7 @@ return [
], ],
'choose_image' => 'Upload', 'choose_image' => 'Upload',
'i_m_lucky' => 'Try my luck', 'i_m_lucky' => 'Try my luck',
'i_m_lucky_legend' => 'The "Try my luck" button tries to get a standard icon from the selected icon collection. The simpler the service value, the more likely you are to get the correct icon: Do not append any extension (like ".com"), use the exact name of the service, avoid special chars.', 'i_m_lucky_legend' => 'The "Try my luck" button tries to get a standard icon from the selected icon collection. The simpler the Service field value, the more likely you are to get the expected icon: Do not append any extension (like ".com"), use the exact name of the service, avoid special chars.',
'test' => 'Test', 'test' => 'Test',
'group' => [ 'group' => [
'label' => 'Group', 'label' => 'Group',