diff --git a/app/Core/Configuration/DefaultConfig.php b/app/Core/Configuration/DefaultConfig.php index 835392e754..493359ee7b 100644 --- a/app/Core/Configuration/DefaultConfig.php +++ b/app/Core/Configuration/DefaultConfig.php @@ -64,6 +64,12 @@ class DefaultConfig */ public string $secondarycolor; + /** + * @var string Unsplash API access key. Users can pick Unsplash photos as their background + * when this is set; without it only uploaded images are offered. + */ + public string $unsplashAccessKey = ''; + /** * @var string Default timezone */ diff --git a/app/Core/UI/Theme.php b/app/Core/UI/Theme.php index 83fd66ff21..4534afe2a8 100644 --- a/app/Core/UI/Theme.php +++ b/app/Core/UI/Theme.php @@ -113,6 +113,63 @@ class Theme private array|false $iniData; private array $colorSchemes = [ + 'themeDefault' => 'themeDefault', + 'companyColors' => 'companyColors', + 'copper' => [ + 'name' => 'Copper', + 'primaryColor' => '#894329', + 'secondaryColor' => '#12727d', + ], + 'mustard' => [ + 'name' => 'Mustard', + 'primaryColor' => '#99472f', + 'secondaryColor' => '#f3aa38', + ], + 'nebular' => [ + 'name' => 'Nebula', + 'primaryColor' => '#bf5440', + 'secondaryColor' => '#0f1b31', + ], + 'fuchsia' => [ + 'name' => 'Fuchsia', + 'primaryColor' => '#b2104f', + 'secondaryColor' => '#81adc4', + ], + 'fruit' => [ + 'name' => 'Fruit', + 'primaryColor' => '#3a5a81', + 'secondaryColor' => '#d31336', + ], + 'highlands' => [ + 'name' => 'Highlands', + 'primaryColor' => '#33697b', + 'secondaryColor' => '#fc9885', + ], + 'moss' => [ + 'name' => 'Moss', + 'primaryColor' => '#696c29', + 'secondaryColor' => '#84bd26', + ], + 'morning' => [ + 'name' => 'Morning', + 'primaryColor' => '#58607b', + 'secondaryColor' => '#F3AA34', + ], + 'ocean' => [ + 'name' => 'Ocean', + 'primaryColor' => '#298089', + 'secondaryColor' => '#fdd78c', + ], + 'energy' => [ + 'name' => 'Energy', + 'primaryColor' => '#cf4705', + 'secondaryColor' => '#91372b', + ], + 'forrest' => [ + 'name' => 'Forest', + 'primaryColor' => '#74211c', + 'secondaryColor' => '#32251e', + ], 'grayscale1' => [ 'name' => 'Grayscale', 'primaryColor' => '#000000', @@ -123,8 +180,6 @@ class Theme 'primaryColor' => '#d1d1d1', 'secondaryColor' => '#000000', ], - 'themeDefault' => 'themeDefault', - 'companyColors' => 'companyColors', ]; /** @@ -137,30 +192,34 @@ class Theme * * @api */ + /** + * Dark-mode colors of the predefined schemes. In dark mode the primary color is also text on + * dark panels (active menu and dropdown items, links), so each one reaches about 4.5:1 on + * #404040. Primary buttons switch to a dark label on these light accents (sections/header). + */ + private const DARK_MODE_COLOR_SCHEMES = [ + 'grayscale1' => ['primaryColor' => '#e8e8e8', 'secondaryColor' => '#b0b0b0'], + 'grayscale2' => ['primaryColor' => '#a0a0a0', 'secondaryColor' => '#d1d1d1'], + 'copper' => ['primaryColor' => '#dc9f89', 'secondaryColor' => '#1ebfd2'], + 'mustard' => ['primaryColor' => '#dc9f8d', 'secondaryColor' => '#f3aa38'], + 'nebular' => ['primaryColor' => '#db9e92', 'secondaryColor' => '#94aede'], + 'fuchsia' => ['primaryColor' => '#f58db5', 'secondaryColor' => '#81adc4'], + 'fruit' => ['primaryColor' => '#96b0d0', 'secondaryColor' => '#f58fa2'], + 'highlands' => ['primaryColor' => '#7eb6c9', 'secondaryColor' => '#fc9885'], + 'moss' => ['primaryColor' => '#afb444', 'secondaryColor' => '#84bd26'], + 'morning' => ['primaryColor' => '#a7adc0', 'secondaryColor' => '#f3aa34'], + 'ocean' => ['primaryColor' => '#4abdc9', 'secondaryColor' => '#fdd78c'], + 'energy' => ['primaryColor' => '#fb9360', 'secondaryColor' => '#df9d94'], + 'forrest' => ['primaryColor' => '#e59a95', 'secondaryColor' => '#c2a89a'], + ]; + private function getColorSchemeForMode(string $schemeName, array $scheme): array { - $colorMode = $this->getColorMode(); - - // Adjust grayscale schemes for dark mode to ensure readability - if ($colorMode === 'dark') { - if ($schemeName === 'grayscale1') { - return [ - 'name' => $scheme['name'], - 'primaryColor' => '#e8e8e8', // Light gray - 8.5:1 contrast on #292929 - 'secondaryColor' => '#b0b0b0', // Medium gray - 4.2:1 contrast - ]; - } - if ($schemeName === 'grayscale2') { - return [ - 'name' => $scheme['name'], - 'primaryColor' => '#a0a0a0', // Light gray - 5.0:1 contrast on #292929 - 'secondaryColor' => '#d1d1d1', // Light gray - 9.1:1 contrast - ]; - } + if ($this->getColorMode() !== 'dark' || ! isset(self::DARK_MODE_COLOR_SCHEMES[$schemeName])) { + return $scheme; } - // Light mode: use original values - return $scheme; + return ['name' => $scheme['name']] + self::DARK_MODE_COLOR_SCHEMES[$schemeName]; } /** @@ -298,13 +357,10 @@ public function getAvailableFonts() */ public function getBackgroundImage(): ?string { + // Read from the settings cache on every request, not a session copy: a change made in + // another session (which may delete the old uploaded image) has to show up here too. if (Auth::isLoggedIn()) { - if (session()->exists('usersettings.backgroundImage')) { - return session('usersettings.backgroundImage') ?: null; - } - $image = $this->settingsRepo->getSetting('usersettings.'.session('userdata.id').'.backgroundImage'); - session(['usersettings.backgroundImage' => $image ?: '']); return $image ?: null; } @@ -317,8 +373,6 @@ public function setBackgroundImage(string $url): void if (Auth::isLoggedIn()) { $this->settingsRepo->saveSetting('usersettings.'.session('userdata.id').'.backgroundType', 'image'); $this->settingsRepo->saveSetting('usersettings.'.session('userdata.id').'.backgroundImage', $url); - session(['usersettings.backgroundType' => 'image']); - session(['usersettings.backgroundImage' => $url]); } } @@ -331,12 +385,7 @@ public function setBackgroundImage(string $url): void public function getBackgroundType(): string { if (Auth::isLoggedIn()) { - if (session()->exists('usersettings.backgroundType')) { - return session('usersettings.backgroundType') ?: 'gradient'; - } - $type = $this->settingsRepo->getSetting('usersettings.'.session('userdata.id').'.backgroundType'); - session(['usersettings.backgroundType' => $type ?: 'gradient']); return $type ?: 'gradient'; } @@ -348,10 +397,8 @@ public function setBackgroundType(string $type): void { if (Auth::isLoggedIn()) { $this->settingsRepo->saveSetting('usersettings.'.session('userdata.id').'.backgroundType', $type); - session(['usersettings.backgroundType' => $type]); if ($type == 'gradient') { $this->settingsRepo->deleteSetting('usersettings.'.session('userdata.id').'.backgroundImage'); - session(['usersettings.backgroundImage' => '']); } } } @@ -1098,7 +1145,5 @@ public static function clearCache(): void session()->forget('usersettings.colorScheme'); session()->forget('usersettings.themeFont'); session()->forget('usersettings.theme'); - session()->forget('usersettings.backgroundType'); - session()->forget('usersettings.backgroundImage'); } } diff --git a/app/Domain/Auth/Listeners/ShowPersonalTokenContent.php b/app/Domain/Auth/Listeners/ShowPersonalTokenContent.php deleted file mode 100644 index cdf125716f..0000000000 --- a/app/Domain/Auth/Listeners/ShowPersonalTokenContent.php +++ /dev/null @@ -1,24 +0,0 @@ - - '; - } -} diff --git a/app/Domain/Auth/Listeners/ShowPersonalTokenTab.php b/app/Domain/Auth/Listeners/ShowPersonalTokenTab.php deleted file mode 100644 index 188415dc97..0000000000 --- a/app/Domain/Auth/Listeners/ShowPersonalTokenTab.php +++ /dev/null @@ -1,17 +0,0 @@ - '.__('tabs.personal_access_tokens').''; - } -} diff --git a/app/Domain/Auth/Templates/partials/loginInfo.blade.php b/app/Domain/Auth/Templates/partials/loginInfo.blade.php index 16c7c43067..8463929552 100644 --- a/app/Domain/Auth/Templates/partials/loginInfo.blade.php +++ b/app/Domain/Auth/Templates/partials/loginInfo.blade.php @@ -26,6 +26,11 @@ {!! __("menu.theme") !!} +
  • + + {!! __("menu.background") !!} + +
  • @dispatchEvent('afterTheme')
  • diff --git a/app/Domain/Auth/register.php b/app/Domain/Auth/register.php index 5f64648718..f6531fcab4 100644 --- a/app/Domain/Auth/register.php +++ b/app/Domain/Auth/register.php @@ -1,19 +1,6 @@ make(\Leantime\Core\Language::class); diff --git a/app/Domain/Users/Controllers/EditOwn.php b/app/Domain/Users/Controllers/EditOwn.php index 699250b803..8dfb3a11f9 100644 --- a/app/Domain/Users/Controllers/EditOwn.php +++ b/app/Domain/Users/Controllers/EditOwn.php @@ -44,7 +44,7 @@ public function get(): Response /** * Saves the submitted section of the own-profile form (profile, password, - * theme, locale or notifications) and redirects back to its tab with a + * theme, locale, notifications or webhook) and redirects back to its tab with a * success or error notification. * * @throws \Exception @@ -120,12 +120,21 @@ public function post(): Response if (isset($_POST['savenotifications'])) { $tab = '#notifications'; + $this->userService->saveOwnNotificationPreferences($this->userId, $_POST); + + $this->tpl->setNotification($this->language->__('notifications.changed_profile_settings_successfully'), 'success', 'profilesettings_updated'); + } + + // Save personal webhook + if (isset($_POST['savewebhooks'])) { + $tab = '#webhooks'; + try { - $this->userService->saveOwnNotificationPreferences($this->userId, $_POST); + $this->userService->saveOwnWebhookSettings($this->userId, $_POST); $this->tpl->setNotification($this->language->__('notifications.changed_profile_settings_successfully'), 'success', 'profilesettings_updated'); } catch (ValidationException $e) { - // The only validated field is the personal webhook URL; nothing was saved. + // The only validated field is the webhook URL; nothing was saved. $this->tpl->setNotification($this->language->__('notification.invalid_webhook_url'), 'error'); } catch (WebhookSettingNotSavedException $e) { // Already logged without the URL; nothing was saved. diff --git a/app/Domain/Users/Hxcontrollers/Background.php b/app/Domain/Users/Hxcontrollers/Background.php new file mode 100644 index 0000000000..f9f542faad --- /dev/null +++ b/app/Domain/Users/Hxcontrollers/Background.php @@ -0,0 +1,150 @@ +backgrounds = $backgrounds; + $this->theme = $theme; + } + + /** + * The whole tab. + */ + public function get(): void + { + $this->assignTabVariables(); + } + + /** + * Unsplash search results for the "q" query. "page" is sent by the refresh button only, so a + * changed query starts at page 1. + */ + public function search(): string + { + $query = (string) $this->incomingRequest->query->get('q', ''); + $page = (int) $this->incomingRequest->query->get('page', 1); + + $searchResult = $this->backgrounds->searchUnsplash($query, $page); + + $this->assignTabVariables(); + $this->tpl->assign('photos', $searchResult['photos']); + $this->tpl->assign('nextPage', $searchResult['nextPage']); + + return 'unsplash-results'; + } + + /** + * Switch between gradient and image. + */ + public function setType(): string|Response + { + if (! $this->incomingRequest->isMethod('POST')) { + return $this->tpl->emptyResponse(405); + } + + try { + if ($this->backgrounds->setBackgroundType((string) $this->incomingRequest->request->get('type', ''))) { + $this->tpl->setNotification(__('notifications.background_settings_saved'), 'success'); + } else { + $this->tpl->setNotification(__('notifications.background_not_saved'), 'error'); + } + } catch (ValidationException $exception) { + $this->tpl->setNotification(__('notifications.background_not_saved'), 'error'); + } + + $this->assignTabVariables(); + + return 'current-background'; + } + + /** + * Use the Unsplash photo with the posted "photoId". + */ + public function useUnsplashPhoto(): string|Response + { + if (! $this->incomingRequest->isMethod('POST')) { + return $this->tpl->emptyResponse(405); + } + + $photoId = (string) $this->incomingRequest->request->get('photoId', ''); + + if ($this->backgrounds->useUnsplashPhoto($photoId)) { + $this->tpl->setNotification(__('notifications.background_settings_saved'), 'success'); + } else { + $this->tpl->setNotification(__('notifications.background_not_saved'), 'error'); + } + + $this->assignTabVariables(); + + return 'current-background'; + } + + /** + * Use the uploaded image ("file") as background. + */ + public function upload(): string|Response + { + if (! $this->incomingRequest->isMethod('POST')) { + return $this->tpl->emptyResponse(405); + } + + if ($this->backgrounds->useUploadedImage($_FILES)) { + $this->tpl->setNotification(__('notifications.background_settings_saved'), 'success'); + } else { + $this->tpl->setNotification(__('notifications.background_upload_failed'), 'error'); + } + + $this->assignTabVariables(); + + return 'current-background'; + } + + /** + * Template variables of the whole tab. Blade renders the whole view even when only one + * fragment is returned, so every action assigns all of them. + */ + private function assignTabVariables(): void + { + $currentBackground = $this->backgrounds->getCurrentBackground(); + + $this->tpl->assign('unsplashEnabled', $this->backgrounds->isUnsplashEnabled()); + $this->tpl->assign('backgroundType', $currentBackground['type']); + $this->tpl->assign('backgroundImage', $currentBackground['image']); + $this->tpl->assign('primaryColor', $this->theme->getPrimaryColor()); + $this->tpl->assign('secondaryColor', $this->theme->getSecondaryColor()); + + // Same variables the page header uses for its background style (sections.backgroundStyle). + $this->tpl->assign('themeType', $currentBackground['type']); + $this->tpl->assign('themeBg', $currentBackground['image']); + $this->tpl->assign('themeBgCss', Header::toCssString($currentBackground['image'])); + $this->tpl->assign('themeOpacity', $currentBackground['type'] === Backgrounds::TYPE_IMAGE ? 1 : 0.1); + } +} diff --git a/app/Domain/Users/Services/Backgrounds.php b/app/Domain/Users/Services/Backgrounds.php new file mode 100644 index 0000000000..5c288b0631 --- /dev/null +++ b/app/Domain/Users/Services/Backgrounds.php @@ -0,0 +1,396 @@ + $this->theme->getBackgroundType(), + 'image' => (string) $this->theme->getBackgroundImage(), + ]; + } + + /** + * Whether Unsplash photos can be offered (an access key is configured). + */ + public function isUnsplashEnabled(): bool + { + return trim((string) $this->config->unsplashAccessKey) !== ''; + } + + /** + * Switch the current user's background type. Switching to the gradient removes the image. + * + * @param string $type 'gradient' or 'image' + * @return bool False when another background change kept it from being saved. + * + * @throws ValidationException When the type is unknown. + */ + public function setBackgroundType(string $type): bool + { + if (! in_array($type, [self::TYPE_GRADIENT, self::TYPE_IMAGE], true)) { + $message = 'Unknown background type'; + + throw new ValidationException(['type' => [$message]], $message); + } + + // Switching to "image" keeps the current image; no file changes, so no lock needed. + if ($type === self::TYPE_IMAGE) { + $this->theme->setBackgroundType($type); + + return true; + } + + // The gradient removes the image, so an uploaded one is no longer needed. + return $this->changeBackground(fn () => $this->theme->setBackgroundType($type), newFileId: null); + } + + /** + * Search Unsplash for landscape photos. Without a query a random page of wallpapers is returned. + * No photos when Unsplash is not configured or the request fails. nextPage is the page that + * shows other photos for the same query (back to 1 after the last one). + * + * @return array{photos: array, nextPage: int} + */ + public function searchUnsplash(string $query, int $page = 1): array + { + $noPhotos = ['photos' => [], 'nextPage' => 1]; + + if (! $this->isUnsplashEnabled()) { + return $noPhotos; + } + + $query = trim($query); + $page = max(1, $page); + if ($query === '') { + $query = self::UNSPLASH_DEFAULT_QUERY; + $page = random_int(1, self::UNSPLASH_DEFAULT_QUERY_PAGES); + } + + try { + $response = $this->unsplashRequest()->get('/search/photos', [ + 'query' => $query, + 'page' => $page, + 'per_page' => self::UNSPLASH_RESULTS_PER_PAGE, + 'orientation' => 'landscape', + 'content_filter' => 'high', + ]); + } catch (\Throwable $exception) { + Log::warning('Unsplash search failed: '.$exception->getMessage()); + + return $noPhotos; + } + + if (! $response->successful()) { + Log::warning('Unsplash search failed with status '.$response->status()); + + return $noPhotos; + } + + $photos = []; + foreach ((array) $response->json('results', []) as $photo) { + if (! isset($photo['id'], $photo['urls']['small'])) { + continue; + } + + $photos[] = [ + 'id' => (string) $photo['id'], + 'thumbnailUrl' => (string) $photo['urls']['small'], + 'description' => (string) ($photo['alt_description'] ?? ''), + 'photographerName' => (string) ($photo['user']['name'] ?? ''), + 'photographerUrl' => $this->withReferral((string) ($photo['user']['links']['html'] ?? 'https://unsplash.com')), + ]; + } + + $totalPages = (int) $response->json('total_pages', 1); + + return [ + 'photos' => $photos, + 'nextPage' => $page < $totalPages ? $page + 1 : 1, + ]; + } + + /** + * Use an Unsplash photo as the current user's background. + * + * Looks the photo up by id, stores its image URL and reports the download to Unsplash, as their + * API guidelines require when a photo is used. + * + * @param string $photoId The Unsplash photo id. + * @return bool False when Unsplash is not configured or the photo can't be loaded. + */ + public function useUnsplashPhoto(string $photoId): bool + { + if (! $this->isUnsplashEnabled() || preg_match('/^[A-Za-z0-9_-]{1,64}$/', $photoId) !== 1) { + return false; + } + + try { + $response = $this->unsplashRequest()->get('/photos/'.$photoId); + } catch (\Throwable $exception) { + Log::warning('Unsplash photo lookup failed: '.$exception->getMessage()); + + return false; + } + + if (! $response->successful()) { + return false; + } + + $rawUrl = (string) $response->json('urls.raw', ''); + if (! $this->isUnsplashImageUrl($rawUrl)) { + return false; + } + + $saved = $this->changeBackground( + fn () => $this->theme->setBackgroundImage($rawUrl.self::UNSPLASH_BACKGROUND_SIZE), + newFileId: null, + ); + if (! $saved) { + return false; + } + + $this->trackUnsplashDownload((string) $response->json('links.download_location', '')); + + return true; + } + + /** + * Use an uploaded image as the current user's background. The file is stored privately for + * the user (module "user"), so only they can load it. + * + * @param array $files The request's files ($_FILES); the image is expected under "file". + * @return bool False when no valid image was uploaded or storing it failed. + */ + public function useUploadedImage(array $files): bool + { + $userId = (int) session('userdata.id'); + $upload = $files['file'] ?? null; + + if ($userId <= 0 || ! is_array($upload) || ! $this->isImageUpload($upload)) { + return false; + } + + // A unique name: with LEAN_FILESYSTEM_RENAME_FILES=false files are stored under their own + // name, so two uploads of "background.png" would share one file and deleting the replaced + // one would delete the new background too. + $extension = strtolower(pathinfo((string) $upload['name'], PATHINFO_EXTENSION)); + // Letters and digits only: files/get strips everything else from the name before the lookup. + $upload['name'] = 'background'.bin2hex(random_bytes(8)).'.'.$extension; + + $storedFile = $this->files->upload(['file' => $upload], 'user', $userId); + + // Files::upload returns the file row on success, an error message or false otherwise. + if (! is_array($storedFile)) { + return false; + } + + $fileUrl = BASE_URL.'/files/get?'.http_build_query([ + 'module' => 'user', + 'encName' => $storedFile['encName'], + 'ext' => $storedFile['extension'], + 'realName' => $storedFile['realName'], + ]); + + $saved = $this->changeBackground( + fn () => $this->theme->setBackgroundImage($fileUrl), + newFileId: (int) $storedFile['fileId'], + ); + if (! $saved) { + $this->files->deleteFile((int) $storedFile['fileId']); + + return false; + } + + return true; + } + + /** + * Apply a background change and clean up the upload it replaces, under a per-user lock. Two + * changes running at the same time (two tabs) would otherwise both see the same previous + * upload, and one could delete the file the other just saved as background. + * + * @param \Closure $applyChange Saves the new background. + * @param int|null $newFileId The uploaded file the background now uses, null for none. + * @return bool False when another change held the lock for too long; nothing was saved. + */ + private function changeBackground(\Closure $applyChange, ?int $newFileId): bool + { + $userId = (int) session('userdata.id'); + $lock = $this->cache->lock('users.background.'.$userId, self::LOCK_SECONDS); + + try { + $lock->block(self::LOCK_WAIT_SECONDS); + } catch (LockTimeoutException $exception) { + Log::warning('Background change skipped: another change for this user is still running', ['userId' => $userId]); + + return false; + } + + try { + $applyChange(); + $this->replaceUploadedBackgroundFile($userId, $newFileId); + } finally { + $lock->release(); + } + + return true; + } + + /** + * Remember the uploaded file the background now uses (null: none) and delete the one it used + * before. Only that file is deleted: the "user" files module also holds profile pictures. + * Runs inside changeBackground's lock. + */ + private function replaceUploadedBackgroundFile(int $userId, ?int $newFileId): void + { + $settingKey = 'usersettings.'.$userId.'.backgroundFileId'; + $previousFileId = (int) $this->settings->getSetting($settingKey, 0); + + if ($newFileId !== null) { + $this->settings->saveSetting($settingKey, $newFileId); + } elseif ($previousFileId > 0) { + $this->settings->deleteSetting($settingKey); + } + + if ($previousFileId > 0 && $previousFileId !== $newFileId) { + $this->files->deleteFile($previousFileId); + } + } + + /** + * A request to the Unsplash API authenticated with the configured access key. + */ + private function unsplashRequest(): PendingRequest + { + return $this->http + ->baseUrl(self::UNSPLASH_API_URL) + ->withHeaders([ + 'Authorization' => 'Client-ID '.trim((string) $this->config->unsplashAccessKey), + 'Accept-Version' => 'v1', + ]) + ->connectTimeout(5) + ->timeout(10); + } + + /** + * Report a photo download to Unsplash. Failures are logged and otherwise ignored, the + * background is already set. + */ + private function trackUnsplashDownload(string $downloadLocation): void + { + if (! str_starts_with($downloadLocation, self::UNSPLASH_API_URL.'/')) { + return; + } + + try { + $this->unsplashRequest()->get(substr($downloadLocation, strlen(self::UNSPLASH_API_URL))); + } catch (\Throwable $exception) { + Log::warning('Unsplash download tracking failed: '.$exception->getMessage()); + } + } + + private function isUnsplashImageUrl(string $url): bool + { + return parse_url($url, PHP_URL_SCHEME) === 'https' + && parse_url($url, PHP_URL_HOST) === self::UNSPLASH_IMAGE_HOST; + } + + /** + * Whether an uploaded file is a JPEG, PNG, GIF or WebP image, by name and by content. + * SVG is refused, it can carry scripts. + * + * @param array $upload One entry of $_FILES. + */ + private function isImageUpload(array $upload): bool + { + if (($upload['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK || empty($upload['tmp_name'])) { + return false; + } + + $extension = strtolower(pathinfo((string) ($upload['name'] ?? ''), PATHINFO_EXTENSION)); + if (! in_array($extension, self::IMAGE_EXTENSIONS, true)) { + return false; + } + + $imageInfo = @getimagesize((string) $upload['tmp_name']); + + return $imageInfo !== false && in_array($imageInfo[2], self::IMAGE_TYPES, true); + } + + /** + * Unsplash asks that links back to them carry the app as referral source. + */ + private function withReferral(string $url): string + { + return $url.(str_contains($url, '?') ? '&' : '?').'utm_source=leantime&utm_medium=referral'; + } +} diff --git a/app/Domain/Users/Services/Users.php b/app/Domain/Users/Services/Users.php index 49b9fb9eef..996495b2f1 100644 --- a/app/Domain/Users/Services/Users.php +++ b/app/Domain/Users/Services/Users.php @@ -1310,25 +1310,11 @@ public function saveOwnLocaleSettings(int $userId, array $post): void * per-project notification levels (validated against the known relevance * levels). Cleans up the legacy muted-projects format when present. * - * Also stores the personal webhook: webhookUrl (https only, validated - * before anything is written) and the webhookEnabled checkbox. An empty - * URL disables the webhook; unchecking the box keeps the URL on file. - * Both go into one setting (Webhooks::settingKey()), written before any - * other preference and verified, so a failed save never leaves an old URL - * or opt-in live behind a success message. - * - * Omitting the webhookUrl key leaves the stored webhook unchanged, and any - * webhookEnabled sent without it is ignored. Sending webhookUrl, even as an - * empty string, sets the URL and the opt-in together, with a missing - * webhookEnabled meaning unchecked. API clients that change only the opt-in - * must therefore send the current URL as well. + * The personal webhook is saved separately, with saveOwnWebhookSettings(). * * @param int $userId The id of the user being edited. * @param array $post Raw request input. * - * @throws ValidationException When webhookUrl is not an acceptable endpoint; nothing is saved. - * @throws WebhookSettingNotSavedException When the webhook setting could not be persisted; nothing is saved. - * * @api */ public function saveOwnNotificationPreferences(int $userId, array $post): void @@ -1336,37 +1322,6 @@ public function saveOwnNotificationPreferences(int $userId, array $post): void // Self-service: pin to the authenticated user (ignore any caller-supplied id — prevents RPC IDOR). $userId = (int) session('userdata.id'); - // A payload without webhookUrl (API clients predating the webhook) leaves the webhook as it is. - if (array_key_exists('webhookUrl', $post)) { - // Validate the webhook first so a rejected URL leaves every preference untouched. - $webhookUrl = is_string($post['webhookUrl']) ? trim($post['webhookUrl']) : null; - if ($webhookUrl === null || ($webhookUrl !== '' && ! Webhooks::isValidEndpointUrl($webhookUrl))) { - throw ValidationException::withMessages(['webhookUrl' => ['notification.invalid_webhook_url']]); - } - $webhookEnabled = $webhookUrl !== '' && filter_var($post['webhookEnabled'] ?? false, FILTER_VALIDATE_BOOLEAN); - - // Persist the webhook first: if it can't be saved, fail before any other preference is touched. - $webhookSettingKey = Webhooks::settingKey($userId); - $webhookSetting = Webhooks::encodeSetting($webhookUrl, $webhookEnabled); - $webhookSaveException = null; - try { - // updateOrInsert also reports false when the row already held this exact value, so a - // false result only counts as a failure when reading the setting back shows otherwise. - $webhookSaved = $this->settingsService->saveSetting($webhookSettingKey, $webhookSetting) - || $this->settingsService->getSetting($webhookSettingKey) === $webhookSetting; - } catch (\Throwable $e) { - $webhookSaved = false; - $webhookSaveException = get_class($e); - } - - if (! $webhookSaved) { - // Class name only: the setting value and DB exception messages carry the URL and its secret. - Log::error('Personal webhook setting could not be saved', ['userId' => $userId, 'exception' => $webhookSaveException]); - - throw new WebhookSettingNotSavedException; - } - } - $row = $this->getUser($userId); $values = [ @@ -1421,6 +1376,59 @@ public function saveOwnNotificationPreferences(int $userId, array $post): void } } + /** + * Stores the current user's personal webhook: webhookUrl (https only, + * validated before anything is written) and the webhookEnabled checkbox. + * An empty or missing URL disables the webhook; unchecking the box keeps the URL on + * file. Both go into one setting (Webhooks::settingKey()), which is read + * back when the write reports no change, so a failed save never leaves an + * old URL or opt-in live behind a success message. + * + * A missing webhookEnabled means unchecked, so API clients that change + * only the opt-in must send the current URL as well. + * + * @param int $userId The id of the user being edited. + * @param array $post Raw request input. + * + * @throws ValidationException When webhookUrl is not an acceptable endpoint; nothing is saved. + * @throws WebhookSettingNotSavedException When the webhook setting could not be persisted. + * + * @api + */ + public function saveOwnWebhookSettings(int $userId, array $post): void + { + // Self-service: pin to the authenticated user (ignore any caller-supplied id — prevents RPC IDOR). + $userId = (int) session('userdata.id'); + + // A cleared field arrives as null (ConvertEmptyStringsToNull), so null clears the webhook too. + $rawWebhookUrl = $post['webhookUrl'] ?? ''; + $webhookUrl = is_string($rawWebhookUrl) ? trim($rawWebhookUrl) : null; + if ($webhookUrl === null || ($webhookUrl !== '' && ! Webhooks::isValidEndpointUrl($webhookUrl))) { + throw ValidationException::withMessages(['webhookUrl' => ['notification.invalid_webhook_url']]); + } + $webhookEnabled = $webhookUrl !== '' && filter_var($post['webhookEnabled'] ?? false, FILTER_VALIDATE_BOOLEAN); + + $webhookSettingKey = Webhooks::settingKey($userId); + $webhookSetting = Webhooks::encodeSetting($webhookUrl, $webhookEnabled); + $webhookSaveException = null; + try { + // updateOrInsert also reports false when the row already held this exact value, so a + // false result only counts as a failure when reading the setting back shows otherwise. + $webhookSaved = $this->settingsService->saveSetting($webhookSettingKey, $webhookSetting) + || $this->settingsService->getSetting($webhookSettingKey) === $webhookSetting; + } catch (\Throwable $e) { + $webhookSaved = false; + $webhookSaveException = get_class($e); + } + + if (! $webhookSaved) { + // Class name only: the setting value and DB exception messages carry the URL and its secret. + Log::error('Personal webhook setting could not be saved', ['userId' => $userId, 'exception' => $webhookSaveException]); + + throw new WebhookSettingNotSavedException; + } + } + /** * Returns the list of supported varying date-time formats used by the * profile settings screen. diff --git a/app/Domain/Users/Templates/editOwn.blade.php b/app/Domain/Users/Templates/editOwn.blade.php index d3e5c26e15..fbcc7abb29 100644 --- a/app/Domain/Users/Templates/editOwn.blade.php +++ b/app/Domain/Users/Templates/editOwn.blade.php @@ -20,8 +20,11 @@
  • {!! __('tabs.security') !!}
  • {!! __('tabs.settings') !!}
  • {!! __('tabs.notifications') !!}
  • +
  • {!! __('tabs.webhooks') !!}
  • {!! __('tabs.theme') !!}
  • +
  • {!! __('tabs.background') !!}
  • @dispatchEvent('tabs') +
  • {{ __('tabs.personal_access_tokens') }}
  • @@ -348,6 +351,11 @@ @dispatchEvent('themecontent')
    +
    + {{-- Loads when the tab is shown, so Unsplash is only queried by users who open it. --}} + +
    +
    @@ -453,10 +461,16 @@ class="tw-text-sm tw-border tw-border-gray-300 tw-rounded tw-px-2 tw-py-1 tw-min @endif
    -
    + + + + - {{ __('label.webhook_notifications') }} -

    {{ __('label.webhook_notifications_description') }}

    +
    +
    + +

    {{ __('label.webhook_notifications_description') }}

    +
    {{ __('label.enable_webhook_notifications') }}
    @@ -471,12 +485,16 @@ class="tw-text-sm tw-border tw-border-gray-300 tw-rounded tw-px-2 tw-py-1 tw-min
    - + @dispatchEvent('tabsContent') + +
    + +
    diff --git a/app/Domain/Users/Templates/partials/background.blade.php b/app/Domain/Users/Templates/partials/background.blade.php new file mode 100644 index 0000000000..47c69c8874 --- /dev/null +++ b/app/Domain/Users/Templates/partials/background.blade.php @@ -0,0 +1,156 @@ +{{-- + "Background" tab of the user's own settings (Hxcontrollers\Background). + + Fragments: + current-background The gradient / image choice plus the page's background style (swapped + out of band). Returned after every change. + unsplash-results Unsplash search results (only reachable when Unsplash is configured). + + One change at a time: every change (gradient, photo, upload) syncs on #backgroundPicker with + "drop", so clicks while a change is running are ignored and a slower response can't repaint a + background the user already replaced. ("queue" would lose them: the response replaces the + gradient button, and htmx drops queued requests of removed elements.) While a change runs, + #backgroundPicker has .htmx-request (hx-indicator) and shows it is busy. +--}} + +
    +

    {{ __('text.set_your_background') }}

    +
    + + @fragment('current-background') +
    + + {{-- Native buttons (not global::selectable) so the choice works with the keyboard. --}} +
    + + + @if($backgroundType === 'image' && $backgroundImage !== '') + {{-- The current image: shown, not a control (pick another image below). --}} +
    + + + + {{ __('label.image') }} +
    + @else +
    + {{ __('text.pick_background_image_below') }} +
    + @endif +
    + + @include('global::sections.backgroundStyle', ['oob' => true]) +
    + @endfragment + +
    + + + + @if($unsplashEnabled) + + + + + +
    +
    + + {{-- Only the refresh button sends "page" (a changed query starts at 1); search results update it. --}} + + +
    + +
    + + +
    + @fragment('unsplash-results') + @isset($nextPage) + + @endisset + @isset($photos) + @forelse($photos as $photo) +
    + +
    + {!! sprintf( + e(__('text.photo_by_on_unsplash')), + ''.e($photo['photographerName']).'', + 'Unsplash' + ) !!} +
    +
    + @empty +
    + +
    + @endforelse + @endisset + @endfragment +
    +
    + @endif + +
    $unsplashEnabled])> +
    + + +
    + +
    + +
    +
    diff --git a/app/Language/ar-SA.ini b/app/Language/ar-SA.ini index 16195b68ef..6e2ae70924 100644 --- a/app/Language/ar-SA.ini +++ b/app/Language/ar-SA.ini @@ -2619,7 +2619,7 @@ label.notifications_my_work_description="المهام المسندة إليك و label.notifications_muted="مكتوم" label.notifications_muted_description="لا إشعارات باستثناء الإشارات المباشرة (@)" label.webhook_notifications="ويب هوك شخصي" -label.webhook_notifications_description="أرسل إشعاراتك إلى نقطة نهاية HTTPS خاصة بك. لكل إشعار مشروع يحق لك تلقيه، يرسل Leantime طلب HTTP POST واحدًا بمحتوى JSON إلى هذا الرابط. تُرسل الطلبات في الخلفية وقد يستغرق وصولها عدة دقائق، ولا تُعاد محاولة الطلب الذي يفشل. تحدد إعدادات تلقي الإشعارات والفئات وإعدادات كل مشروع أعلاه الإشعاراتِ المؤهلة. لا يرسل الويب هوك إلا عندما يكون مفعّلاً ومحددًا له رابط. يبقى رابطك محفوظًا أثناء تعطيل الويب هوك — امسح الرابط لإزالته." +label.webhook_notifications_description="أرسل إشعاراتك إلى نقطة نهاية HTTPS خاصة بك. لكل إشعار مشروع يحق لك تلقيه، يرسل Leantime طلب HTTP POST واحدًا بمحتوى JSON إلى هذا الرابط. تُرسل الطلبات في الخلفية وقد يستغرق وصولها عدة دقائق، ولا تُعاد محاولة الطلب الذي يفشل. تحدد إعداداتك في تبويب الإشعارات (تلقي الإشعارات والفئات ومستويات كل مشروع) الإشعاراتِ المؤهلة. لا يرسل الويب هوك إلا عندما يكون مفعّلاً ومحددًا له رابط. يبقى رابطك محفوظًا أثناء تعطيل الويب هوك — امسح الرابط لإزالته." label.enable_webhook_notifications="تفعيل إشعارات الويب هوك" label.webhook_url_hint="HTTPS فقط. اتركه فارغًا لإزالة الويب هوك." label.default_notification_types="أنواع الإشعارات الافتراضية للمستخدمين الجدد" @@ -4043,3 +4043,24 @@ modules.files.title="الملفات" modules.files.description="مكتبة ملفات المشروع. تبقى مرفقات المهام والصور داخل النصوص متاحة." modules.reports.title="التقارير" modules.reports.description="تقارير المشروع وملخصات الحالة." + +tabs.background = " الخلفية" +menu.background = " الخلفية" +text.set_your_background = "اختر خلفية صفحاتك: تدرج ألوان نظام الألوان الخاص بك أو صورة." +label.background_type = "الخلفية الحالية" +label.gradient = "تدرج الألوان" +label.image = "صورة" +text.pick_background_image_below = "اختر صورة أدناه لاستخدامها كخلفية." +label.background_image = "اختر صورة" +label.background_source = "مصدر الصورة" +label.unsplash = "Unsplash" +input.search_unsplash = "ابحث في Unsplash" +label.search_for_image = "ابحث عن صورة" +buttons.refresh_images = "عرض صور أخرى" +buttons.use_image = "استخدام كخلفية" +text.photo_by_on_unsplash = "صورة بواسطة %s على %s" +text.no_images_found = "لم يتم العثور على صور" +notifications.background_settings_saved = "تم حفظ الخلفية" +notifications.background_not_saved = "تعذر حفظ الخلفية" +notifications.background_upload_failed = "تعذر حفظ الخلفية. ارفع صورة بصيغة JPG أو PNG أو GIF أو WebP." +tabs.webhooks = " ويب هوك" diff --git a/app/Language/cs-CZ.ini b/app/Language/cs-CZ.ini index c1dc154158..fa531035a2 100644 --- a/app/Language/cs-CZ.ini +++ b/app/Language/cs-CZ.ini @@ -2622,7 +2622,7 @@ label.notifications_my_work_description="Pouze úkoly přiřazené vám, vaše p label.notifications_muted="Ztlumeno" label.notifications_muted_description="Žádná oznámení kromě přímých @zmínek" label.webhook_notifications="Osobní webhook" -label.webhook_notifications_description="Posílejte svá oznámení na vlastní HTTPS endpoint. Pro každé oznámení projektu, které byste měli dostat, odešle Leantime na tuto URL jeden požadavek HTTP POST s tělem ve formátu JSON. Požadavky se odesílají na pozadí a jejich doručení může trvat několik minut; neúspěšný požadavek se neopakuje. O tom, která oznámení se odešlou, rozhodují vaše výše uvedená nastavení příjmu oznámení, kategorií a jednotlivých projektů. Webhook odesílá pouze tehdy, je-li zapnutý a je nastavena URL. Vaše URL zůstane uložená i při vypnutém webhooku — chcete-li ji odstranit, vymažte ji." +label.webhook_notifications_description="Posílejte svá oznámení na vlastní HTTPS endpoint. Pro každé oznámení projektu, které byste měli dostat, odešle Leantime na tuto URL jeden požadavek HTTP POST s tělem ve formátu JSON. Požadavky se odesílají na pozadí a jejich doručení může trvat několik minut; neúspěšný požadavek se neopakuje. O tom, která oznámení se odešlou, rozhodují vaše nastavení na kartě Oznámení (příjem oznámení, kategorie a úrovně jednotlivých projektů). Webhook odesílá pouze tehdy, je-li zapnutý a je nastavena URL. Vaše URL zůstane uložená i při vypnutém webhooku — chcete-li ji odstranit, vymažte ji." label.enable_webhook_notifications="Zapnout oznámení přes webhook" label.webhook_url_hint="Pouze HTTPS. Ponecháním prázdného pole webhook odstraníte." label.default_notification_types="Výchozí typy oznámení pro nové uživatele" @@ -4046,3 +4046,24 @@ modules.files.title="Soubory" modules.files.description="Knihovna souborů projektu. Přílohy úkolů a obrázky v textech zůstávají dostupné." modules.reports.title="Zprávy" modules.reports.description="Projektové zprávy a přehledy stavu." + +tabs.background = " Pozadí" +menu.background = " Pozadí" +text.set_your_background = "Zvolte pozadí svých stránek: barevný přechod vašeho barevného schématu nebo obrázek." +label.background_type = "Aktuální pozadí" +label.gradient = "Barevný přechod" +label.image = "Obrázek" +text.pick_background_image_below = "Vyberte níže obrázek, který se použije jako pozadí." +label.background_image = "Vyberte obrázek" +label.background_source = "Zdroj obrázku" +label.unsplash = "Unsplash" +input.search_unsplash = "Hledat na Unsplash" +label.search_for_image = "Hledat obrázek" +buttons.refresh_images = "Zobrazit jiné obrázky" +buttons.use_image = "Použít jako pozadí" +text.photo_by_on_unsplash = "Foto: %s na %s" +text.no_images_found = "Nebyly nalezeny žádné obrázky" +notifications.background_settings_saved = "Pozadí bylo uloženo" +notifications.background_not_saved = "Pozadí se nepodařilo uložit" +notifications.background_upload_failed = "Pozadí se nepodařilo uložit. Nahrajte obrázek JPG, PNG, GIF nebo WebP." +tabs.webhooks = " Webhooky" diff --git a/app/Language/da-DK.ini b/app/Language/da-DK.ini index 538b945819..fa06eab0db 100644 --- a/app/Language/da-DK.ini +++ b/app/Language/da-DK.ini @@ -2621,7 +2621,7 @@ label.notifications_my_work_description="Kun opgaver tildelt dig, dine elementer label.notifications_muted="Lyd slået fra" label.notifications_muted_description="Ingen notifikationer undtagen direkte @omtaler" label.webhook_notifications="Personlig webhook" -label.webhook_notifications_description="Send dine notifikationer til dit eget HTTPS-endpoint. For hver projektnotifikation, du er berettiget til, sender Leantime én HTTP POST-anmodning med en JSON-body til denne URL. Anmodninger sendes i baggrunden og kan tage flere minutter om at nå frem; en anmodning, der fejler, bliver ikke forsøgt igen. Dine indstillinger ovenfor for modtagelse af notifikationer, kategorier og pr. projekt afgør, hvilke notifikationer der er berettigede. Webhooken sender kun, mens den er slået til, og en URL er angivet. Din URL forbliver gemt, mens webhooken er slået fra — ryd URL'en for at fjerne den." +label.webhook_notifications_description="Send dine notifikationer til dit eget HTTPS-endpoint. For hver projektnotifikation, du er berettiget til, sender Leantime én HTTP POST-anmodning med en JSON-body til denne URL. Anmodninger sendes i baggrunden og kan tage flere minutter om at nå frem; en anmodning, der fejler, bliver ikke forsøgt igen. Dine indstillinger under fanen Notifikationer (modtagelse af notifikationer, kategorier og niveauer pr. projekt) afgør, hvilke notifikationer der er berettigede. Webhooken sender kun, mens den er slået til, og en URL er angivet. Din URL forbliver gemt, mens webhooken er slået fra — ryd URL'en for at fjerne den." label.enable_webhook_notifications="Slå webhook-notifikationer til" label.webhook_url_hint="Kun HTTPS. Lad stå tomt for at fjerne webhooken." label.default_notification_types="Standardnotifikationstyper for nye brugere" @@ -4045,3 +4045,24 @@ modules.files.title="Filer" modules.files.description="Projektets filbibliotek. Vedhæftede filer på to-dos og billeder i tekst forbliver tilgængelige." modules.reports.title="Rapporter" modules.reports.description="Projektrapporter og statusoversigter." + +tabs.background = " Baggrund" +menu.background = " Baggrund" +text.set_your_background = "Vælg baggrunden på dine sider: farveovergangen i dit farveskema eller et billede." +label.background_type = "Nuværende baggrund" +label.gradient = "Farveovergang" +label.image = "Billede" +text.pick_background_image_below = "Vælg et billede nedenfor for at bruge det som baggrund." +label.background_image = "Vælg et billede" +label.background_source = "Billedkilde" +label.unsplash = "Unsplash" +input.search_unsplash = "Søg på Unsplash" +label.search_for_image = "Søg efter et billede" +buttons.refresh_images = "Vis andre billeder" +buttons.use_image = "Brug som baggrund" +text.photo_by_on_unsplash = "Foto af %s på %s" +text.no_images_found = "Der blev ikke fundet nogen billeder" +notifications.background_settings_saved = "Baggrunden er gemt" +notifications.background_not_saved = "Baggrunden kunne ikke gemmes" +notifications.background_upload_failed = "Baggrunden kunne ikke gemmes. Upload et JPG-, PNG-, GIF- eller WebP-billede." +tabs.webhooks = " Webhooks" diff --git a/app/Language/de-CH.ini b/app/Language/de-CH.ini index 80a20c424d..260038eafe 100644 --- a/app/Language/de-CH.ini +++ b/app/Language/de-CH.ini @@ -1844,7 +1844,7 @@ label.notifications_my_work_description="Nur Ihnen zugewiesene Aufgaben, Ihre El label.notifications_muted="Stummgeschaltet" label.notifications_muted_description="Keine Benachrichtigungen ausser direkten @Erwähnungen" label.webhook_notifications="Persönlicher Webhook" -label.webhook_notifications_description="Senden Sie Ihre Benachrichtigungen an Ihren eigenen HTTPS-Endpunkt. Für jede Projektbenachrichtigung, die für Sie infrage kommt, sendet Leantime eine HTTP-POST-Anfrage mit einem JSON-Body an diese URL. Die Anfragen werden im Hintergrund gesendet und können einige Minuten bis zur Zustellung benötigen; fehlgeschlagene Anfragen werden nicht wiederholt. Welche Benachrichtigungen infrage kommen, bestimmen Ihre obigen Einstellungen zu Benachrichtigungsempfang, Kategorien und Projekten. Der Webhook sendet nur, solange er aktiviert und eine URL gesetzt ist. Ihre URL bleibt gespeichert, während der Webhook deaktiviert ist – leeren Sie die URL, um sie zu entfernen." +label.webhook_notifications_description="Senden Sie Ihre Benachrichtigungen an Ihren eigenen HTTPS-Endpunkt. Für jede Projektbenachrichtigung, die für Sie infrage kommt, sendet Leantime eine HTTP-POST-Anfrage mit einem JSON-Body an diese URL. Die Anfragen werden im Hintergrund gesendet und können einige Minuten bis zur Zustellung benötigen; fehlgeschlagene Anfragen werden nicht wiederholt. Welche Benachrichtigungen infrage kommen, bestimmen Ihre Einstellungen im Tab Benachrichtigungen (Benachrichtigungsempfang, Kategorien und Stufen pro Projekt). Der Webhook sendet nur, solange er aktiviert und eine URL gesetzt ist. Ihre URL bleibt gespeichert, während der Webhook deaktiviert ist – leeren Sie die URL, um sie zu entfernen." label.enable_webhook_notifications="Webhook-Benachrichtigungen aktivieren" label.webhook_url_hint="Nur HTTPS. Leer lassen, um den Webhook zu entfernen." label.default_notification_types="Standard-Benachrichtigungsarten für neue Benutzer" @@ -4152,3 +4152,24 @@ modules.files.title="Dateien" modules.files.description="Die Dateibibliothek des Projekts. Anhänge an Aufgaben und Bilder in Texten bleiben verfügbar." modules.reports.title="Berichte" modules.reports.description="Projektberichte und Statusübersichten." + +tabs.background = " Hintergrund" +menu.background = " Hintergrund" +text.set_your_background = "Wählen Sie den Hintergrund Ihrer Seiten: den Farbverlauf Ihres Farbschemas oder ein Bild." +label.background_type = "Aktueller Hintergrund" +label.gradient = "Farbverlauf" +label.image = "Bild" +text.pick_background_image_below = "Wählen Sie unten ein Bild aus, um es als Hintergrund zu verwenden." +label.background_image = "Bild auswählen" +label.background_source = "Bildquelle" +label.unsplash = "Unsplash" +input.search_unsplash = "Unsplash durchsuchen" +label.search_for_image = "Nach einem Bild suchen" +buttons.refresh_images = "Andere Bilder anzeigen" +buttons.use_image = "Als Hintergrund verwenden" +text.photo_by_on_unsplash = "Foto von %s auf %s" +text.no_images_found = "Keine Bilder gefunden" +notifications.background_settings_saved = "Hintergrund gespeichert" +notifications.background_not_saved = "Der Hintergrund konnte nicht gespeichert werden" +notifications.background_upload_failed = "Der Hintergrund konnte nicht gespeichert werden. Laden Sie ein Bild im Format JPG, PNG, GIF oder WebP hoch." +tabs.webhooks = " Webhooks" diff --git a/app/Language/de-DE-inf.ini b/app/Language/de-DE-inf.ini index 616d85052a..4ec19c2f96 100644 --- a/app/Language/de-DE-inf.ini +++ b/app/Language/de-DE-inf.ini @@ -2621,7 +2621,7 @@ label.notifications_my_work_description="Nur dir zugewiesene Aufgaben, deine Ele label.notifications_muted="Stummgeschaltet" label.notifications_muted_description="Keine Benachrichtigungen außer direkten @Erwähnungen" label.webhook_notifications="Persönlicher Webhook" -label.webhook_notifications_description="Sende deine Benachrichtigungen an deinen eigenen HTTPS-Endpunkt. Für jede Projektbenachrichtigung, die für dich infrage kommt, sendet Leantime eine HTTP-POST-Anfrage mit einem JSON-Body an diese URL. Die Anfragen werden im Hintergrund gesendet und können einige Minuten bis zur Zustellung benötigen; fehlgeschlagene Anfragen werden nicht wiederholt. Welche Benachrichtigungen infrage kommen, bestimmen deine obigen Einstellungen zu Benachrichtigungsempfang, Kategorien und Projekten. Der Webhook sendet nur, solange er aktiviert und eine URL gesetzt ist. Deine URL bleibt gespeichert, während der Webhook deaktiviert ist – leere die URL, um sie zu entfernen." +label.webhook_notifications_description="Sende deine Benachrichtigungen an deinen eigenen HTTPS-Endpunkt. Für jede Projektbenachrichtigung, die für dich infrage kommt, sendet Leantime eine HTTP-POST-Anfrage mit einem JSON-Body an diese URL. Die Anfragen werden im Hintergrund gesendet und können einige Minuten bis zur Zustellung benötigen; fehlgeschlagene Anfragen werden nicht wiederholt. Welche Benachrichtigungen infrage kommen, bestimmen deine Einstellungen im Tab Benachrichtigungen (Benachrichtigungsempfang, Kategorien und Stufen pro Projekt). Der Webhook sendet nur, solange er aktiviert und eine URL gesetzt ist. Deine URL bleibt gespeichert, während der Webhook deaktiviert ist – leere die URL, um sie zu entfernen." label.enable_webhook_notifications="Webhook-Benachrichtigungen aktivieren" label.webhook_url_hint="Nur HTTPS. Leer lassen, um den Webhook zu entfernen." label.default_notification_types="Standard-Benachrichtigungsarten für neue Benutzer" @@ -4045,3 +4045,24 @@ modules.files.title="Dateien" modules.files.description="Die Dateibibliothek des Projekts. Anhänge an Aufgaben und Bilder in Texten bleiben verfügbar." modules.reports.title="Berichte" modules.reports.description="Projektberichte und Statusübersichten." + +tabs.background = " Hintergrund" +menu.background = " Hintergrund" +text.set_your_background = "Wähle den Hintergrund deiner Seiten: den Farbverlauf deines Farbschemas oder ein Bild." +label.background_type = "Aktueller Hintergrund" +label.gradient = "Farbverlauf" +label.image = "Bild" +text.pick_background_image_below = "Wähle unten ein Bild aus, um es als Hintergrund zu verwenden." +label.background_image = "Bild auswählen" +label.background_source = "Bildquelle" +label.unsplash = "Unsplash" +input.search_unsplash = "Unsplash durchsuchen" +label.search_for_image = "Nach einem Bild suchen" +buttons.refresh_images = "Andere Bilder anzeigen" +buttons.use_image = "Als Hintergrund verwenden" +text.photo_by_on_unsplash = "Foto von %s auf %s" +text.no_images_found = "Keine Bilder gefunden" +notifications.background_settings_saved = "Hintergrund gespeichert" +notifications.background_not_saved = "Der Hintergrund konnte nicht gespeichert werden" +notifications.background_upload_failed = "Der Hintergrund konnte nicht gespeichert werden. Lade ein Bild im Format JPG, PNG, GIF oder WebP hoch." +tabs.webhooks = " Webhooks" diff --git a/app/Language/de-DE.ini b/app/Language/de-DE.ini index 6c7d10b12f..bda3088710 100644 --- a/app/Language/de-DE.ini +++ b/app/Language/de-DE.ini @@ -2621,7 +2621,7 @@ label.notifications_my_work_description="Nur Ihnen zugewiesene Aufgaben, Ihre El label.notifications_muted="Stummgeschaltet" label.notifications_muted_description="Keine Benachrichtigungen außer direkten @Erwähnungen" label.webhook_notifications="Persönlicher Webhook" -label.webhook_notifications_description="Senden Sie Ihre Benachrichtigungen an Ihren eigenen HTTPS-Endpunkt. Für jede Projektbenachrichtigung, die für Sie infrage kommt, sendet Leantime eine HTTP-POST-Anfrage mit einem JSON-Body an diese URL. Die Anfragen werden im Hintergrund gesendet und können einige Minuten bis zur Zustellung benötigen; fehlgeschlagene Anfragen werden nicht wiederholt. Welche Benachrichtigungen infrage kommen, bestimmen Ihre obigen Einstellungen zu Benachrichtigungsempfang, Kategorien und Projekten. Der Webhook sendet nur, solange er aktiviert und eine URL gesetzt ist. Ihre URL bleibt gespeichert, während der Webhook deaktiviert ist – leeren Sie die URL, um sie zu entfernen." +label.webhook_notifications_description="Senden Sie Ihre Benachrichtigungen an Ihren eigenen HTTPS-Endpunkt. Für jede Projektbenachrichtigung, die für Sie infrage kommt, sendet Leantime eine HTTP-POST-Anfrage mit einem JSON-Body an diese URL. Die Anfragen werden im Hintergrund gesendet und können einige Minuten bis zur Zustellung benötigen; fehlgeschlagene Anfragen werden nicht wiederholt. Welche Benachrichtigungen infrage kommen, bestimmen Ihre Einstellungen im Tab Benachrichtigungen (Benachrichtigungsempfang, Kategorien und Stufen pro Projekt). Der Webhook sendet nur, solange er aktiviert und eine URL gesetzt ist. Ihre URL bleibt gespeichert, während der Webhook deaktiviert ist – leeren Sie die URL, um sie zu entfernen." label.enable_webhook_notifications="Webhook-Benachrichtigungen aktivieren" label.webhook_url_hint="Nur HTTPS. Leer lassen, um den Webhook zu entfernen." label.default_notification_types="Standard-Benachrichtigungsarten für neue Benutzer" @@ -4045,3 +4045,24 @@ modules.files.title="Dateien" modules.files.description="Die Dateibibliothek des Projekts. Anhänge an Aufgaben und Bilder in Texten bleiben verfügbar." modules.reports.title="Berichte" modules.reports.description="Projektberichte und Statusübersichten." + +tabs.background = " Hintergrund" +menu.background = " Hintergrund" +text.set_your_background = "Wählen Sie den Hintergrund Ihrer Seiten: den Farbverlauf Ihres Farbschemas oder ein Bild." +label.background_type = "Aktueller Hintergrund" +label.gradient = "Farbverlauf" +label.image = "Bild" +text.pick_background_image_below = "Wählen Sie unten ein Bild aus, um es als Hintergrund zu verwenden." +label.background_image = "Bild auswählen" +label.background_source = "Bildquelle" +label.unsplash = "Unsplash" +input.search_unsplash = "Unsplash durchsuchen" +label.search_for_image = "Nach einem Bild suchen" +buttons.refresh_images = "Andere Bilder anzeigen" +buttons.use_image = "Als Hintergrund verwenden" +text.photo_by_on_unsplash = "Foto von %s auf %s" +text.no_images_found = "Keine Bilder gefunden" +notifications.background_settings_saved = "Hintergrund gespeichert" +notifications.background_not_saved = "Der Hintergrund konnte nicht gespeichert werden" +notifications.background_upload_failed = "Der Hintergrund konnte nicht gespeichert werden. Laden Sie ein Bild im Format JPG, PNG, GIF oder WebP hoch." +tabs.webhooks = " Webhooks" diff --git a/app/Language/el-GR.ini b/app/Language/el-GR.ini index d81cca86e2..b6b91cb05c 100644 --- a/app/Language/el-GR.ini +++ b/app/Language/el-GR.ini @@ -2621,7 +2621,7 @@ label.notifications_my_work_description="Μόνο εργασίες που σας label.notifications_muted="Σε σίγαση" label.notifications_muted_description="Καμία ειδοποίηση εκτός από άμεσες @αναφορές" label.webhook_notifications="Προσωπικό webhook" -label.webhook_notifications_description="Στείλτε τις ειδοποιήσεις σας στο δικό σας endpoint HTTPS. Για κάθε ειδοποίηση έργου για την οποία είστε επιλέξιμοι, το Leantime στέλνει ένα αίτημα HTTP POST με σώμα JSON σε αυτή τη διεύθυνση URL. Τα αιτήματα αποστέλλονται στο παρασκήνιο και μπορεί να χρειαστούν αρκετά λεπτά για να φτάσουν· ένα αίτημα που αποτυγχάνει δεν επαναλαμβάνεται. Οι παραπάνω ρυθμίσεις λήψης ειδοποιήσεων, κατηγοριών και ανά έργο καθορίζουν ποιες ειδοποιήσεις είναι επιλέξιμες. Το webhook στέλνει μόνο όταν είναι ενεργοποιημένο και έχει οριστεί διεύθυνση URL. Η διεύθυνση URL σας παραμένει αποθηκευμένη όσο το webhook είναι απενεργοποιημένο — διαγράψτε τη διεύθυνση URL για να την αφαιρέσετε." +label.webhook_notifications_description="Στείλτε τις ειδοποιήσεις σας στο δικό σας endpoint HTTPS. Για κάθε ειδοποίηση έργου για την οποία είστε επιλέξιμοι, το Leantime στέλνει ένα αίτημα HTTP POST με σώμα JSON σε αυτή τη διεύθυνση URL. Τα αιτήματα αποστέλλονται στο παρασκήνιο και μπορεί να χρειαστούν αρκετά λεπτά για να φτάσουν· ένα αίτημα που αποτυγχάνει δεν επαναλαμβάνεται. Οι ρυθμίσεις σας στην καρτέλα Ειδοποιήσεις (λήψη ειδοποιήσεων, κατηγορίες και επίπεδα ανά έργο) καθορίζουν ποιες ειδοποιήσεις είναι επιλέξιμες. Το webhook στέλνει μόνο όταν είναι ενεργοποιημένο και έχει οριστεί διεύθυνση URL. Η διεύθυνση URL σας παραμένει αποθηκευμένη όσο το webhook είναι απενεργοποιημένο — διαγράψτε τη διεύθυνση URL για να την αφαιρέσετε." label.enable_webhook_notifications="Ενεργοποίηση ειδοποιήσεων webhook" label.webhook_url_hint="Μόνο HTTPS. Αφήστε κενό για να αφαιρέσετε το webhook." label.default_notification_types="Προεπιλεγμένοι τύποι ειδοποιήσεων για νέους χρήστες" @@ -4045,3 +4045,24 @@ modules.files.title="Αρχεία" modules.files.description="Η βιβλιοθήκη αρχείων του έργου. Τα συνημμένα σε εργασίες και οι εικόνες σε κείμενα παραμένουν διαθέσιμα." modules.reports.title="Εκθέσεις" modules.reports.description="Εκθέσεις έργου και επισκοπήσεις κατάστασης." + +tabs.background = " Φόντο" +menu.background = " Φόντο" +text.set_your_background = "Επιλέξτε το φόντο των σελίδων σας: την διαβάθμιση χρωμάτων του συνδυασμού χρωμάτων σας ή μια εικόνα." +label.background_type = "Τρέχον φόντο" +label.gradient = "Διαβάθμιση χρωμάτων" +label.image = "Εικόνα" +text.pick_background_image_below = "Επιλέξτε παρακάτω μια εικόνα για να τη χρησιμοποιήσετε ως φόντο." +label.background_image = "Επιλέξτε μια εικόνα" +label.background_source = "Πηγή εικόνας" +label.unsplash = "Unsplash" +input.search_unsplash = "Αναζήτηση στο Unsplash" +label.search_for_image = "Αναζήτηση εικόνας" +buttons.refresh_images = "Εμφάνιση άλλων εικόνων" +buttons.use_image = "Χρήση ως φόντο" +text.photo_by_on_unsplash = "Φωτογραφία από %s στο %s" +text.no_images_found = "Δεν βρέθηκαν εικόνες" +notifications.background_settings_saved = "Το φόντο αποθηκεύτηκε" +notifications.background_not_saved = "Δεν ήταν δυνατή η αποθήκευση του φόντου" +notifications.background_upload_failed = "Δεν ήταν δυνατή η αποθήκευση του φόντου. Ανεβάστε μια εικόνα JPG, PNG, GIF ή WebP." +tabs.webhooks = " Webhooks" diff --git a/app/Language/en-GB.ini b/app/Language/en-GB.ini index aa03faca87..2655d61bd4 100644 --- a/app/Language/en-GB.ini +++ b/app/Language/en-GB.ini @@ -2621,7 +2621,7 @@ label.notifications_my_work_description="Only tasks assigned to you, your items, label.notifications_muted="Muted" label.notifications_muted_description="No notifications except direct @mentions" label.webhook_notifications="Personal Webhook" -label.webhook_notifications_description="Send your notifications to your own HTTPS endpoint. For every project notification you are eligible for, Leantime sends one HTTP POST request with a JSON body to this URL. Requests are sent in the background and can take several minutes to arrive; a request that fails is not retried. Your receive-notifications, category, and per-project settings above decide which notifications are eligible. The webhook only sends while it is enabled and a URL is set. Your URL stays saved while the webhook is disabled — clear the URL to remove it." +label.webhook_notifications_description="Send your notifications to your own HTTPS endpoint. For every project notification you are eligible for, Leantime sends one HTTP POST request with a JSON body to this URL. Requests are sent in the background and can take several minutes to arrive; a request that fails is not retried. Your settings in the Notifications tab (receive notifications, categories and per-project levels) decide which notifications are eligible. The webhook only sends while it is enabled and a URL is set. Your URL stays saved while the webhook is disabled — clear the URL to remove it." label.enable_webhook_notifications="Enable webhook notifications" label.webhook_url_hint="HTTPS only. Leave empty to remove the webhook." label.default_notification_types="Default notification types for new users" @@ -4045,3 +4045,24 @@ modules.files.title="Files" modules.files.description="The project's file library. Attachments on to-dos and images in text stay available." modules.reports.title="Reports" modules.reports.description="Project reports and status overviews." + +tabs.background = " Background" +menu.background = " Background" +text.set_your_background = "Choose the background of your pages: the colour gradient of your colour scheme or an image." +label.background_type = "Current background" +label.gradient = "Colour gradient" +label.image = "Image" +text.pick_background_image_below = "Pick an image below to use it as background." +label.background_image = "Choose an image" +label.background_source = "Image source" +label.unsplash = "Unsplash" +input.search_unsplash = "Search Unsplash" +label.search_for_image = "Search for an image" +buttons.refresh_images = "Show other images" +buttons.use_image = "Use as background" +text.photo_by_on_unsplash = "Photo by %s on %s" +text.no_images_found = "No images found" +notifications.background_settings_saved = "Background saved" +notifications.background_not_saved = "The background could not be saved" +notifications.background_upload_failed = "The background could not be saved. Upload a JPG, PNG, GIF or WebP image." +tabs.webhooks = " Webhooks" diff --git a/app/Language/en-US.ini b/app/Language/en-US.ini index 236fc73af0..0a0c60959f 100644 --- a/app/Language/en-US.ini +++ b/app/Language/en-US.ini @@ -918,7 +918,7 @@ label.notifications_my_work_description = "Only tasks assigned to you, your item label.notifications_muted = "Muted" label.notifications_muted_description = "No notifications except direct @mentions" label.webhook_notifications = "Personal Webhook" -label.webhook_notifications_description = "Send your notifications to your own HTTPS endpoint. For every project notification you are eligible for, Leantime sends one HTTP POST request with a JSON body to this URL. Requests are sent in the background and can take several minutes to arrive; a request that fails is not retried. Your receive-notifications, category, and per-project settings above decide which notifications are eligible. The webhook only sends while it is enabled and a URL is set. Your URL stays saved while the webhook is disabled — clear the URL to remove it." +label.webhook_notifications_description = "Send your notifications to your own HTTPS endpoint. For every project notification you are eligible for, Leantime sends one HTTP POST request with a JSON body to this URL. Requests are sent in the background and can take several minutes to arrive; a request that fails is not retried. Your settings in the Notifications tab (receive notifications, categories and per-project levels) decide which notifications are eligible. The webhook only sends while it is enabled and a URL is set. Your URL stays saved while the webhook is disabled — clear the URL to remove it." label.enable_webhook_notifications = "Enable webhook notifications" label.webhook_url_hint = "HTTPS only. Leave empty to remove the webhook." label.default_notification_types = "Default notification types for new users" @@ -4193,3 +4193,24 @@ label.onboarding_goal_objective = "Build My Productivity System" label.onboarding_step_name_first_task = "Name your first task" label.onboarding_step_invite_team = "Invite your team" label.onboarding_step_name_project = "Name your project" + +tabs.background = " Background" +menu.background = " Background" +text.set_your_background = "Choose the background of your pages: the color gradient of your color scheme or an image." +label.background_type = "Current background" +label.gradient = "Color gradient" +label.image = "Image" +text.pick_background_image_below = "Pick an image below to use it as background." +label.background_image = "Choose an image" +label.background_source = "Image source" +label.unsplash = "Unsplash" +input.search_unsplash = "Search Unsplash" +label.search_for_image = "Search for an image" +buttons.refresh_images = "Show other images" +buttons.use_image = "Use as background" +text.photo_by_on_unsplash = "Photo by %s on %s" +text.no_images_found = "No images found" +notifications.background_settings_saved = "Background saved" +notifications.background_not_saved = "The background could not be saved" +notifications.background_upload_failed = "The background could not be saved. Upload a JPG, PNG, GIF or WebP image." +tabs.webhooks = " Webhooks" diff --git a/app/Language/es-419.ini b/app/Language/es-419.ini index 854310ce70..2b60fe0000 100644 --- a/app/Language/es-419.ini +++ b/app/Language/es-419.ini @@ -2621,7 +2621,7 @@ label.notifications_my_work_description="Solo las tareas asignadas a ti, tus ele label.notifications_muted="Silenciado" label.notifications_muted_description="Ninguna notificación, salvo las @menciones directas" label.webhook_notifications="Webhook personal" -label.webhook_notifications_description="Envía tus notificaciones a tu propio endpoint HTTPS. Por cada notificación de proyecto que te corresponda, Leantime envía a esta URL una solicitud HTTP POST con un cuerpo JSON. Las solicitudes se envían en segundo plano y pueden tardar varios minutos en llegar; si una solicitud falla, no se reintenta. Tu configuración de recepción de notificaciones, de categorías y por proyecto (más arriba) determina qué notificaciones te corresponden. El webhook solo envía mientras está activado y tiene una URL configurada. La URL se conserva aunque desactives el webhook; bórrala para eliminarla." +label.webhook_notifications_description="Envía tus notificaciones a tu propio endpoint HTTPS. Por cada notificación de proyecto que te corresponda, Leantime envía a esta URL una solicitud HTTP POST con un cuerpo JSON. Las solicitudes se envían en segundo plano y pueden tardar varios minutos en llegar; si una solicitud falla, no se reintenta. Tu configuración en la pestaña Notificaciones (recepción de notificaciones, categorías y niveles por proyecto) determina qué notificaciones te corresponden. El webhook solo envía mientras está activado y tiene una URL configurada. La URL se conserva aunque desactives el webhook; bórrala para eliminarla." label.enable_webhook_notifications="Activar notificaciones por webhook" label.webhook_url_hint="Solo HTTPS. Déjalo vacío para eliminar el webhook." label.default_notification_types="Tipos de notificación predeterminados para usuarios nuevos" @@ -4045,3 +4045,24 @@ modules.files.title="Archivos" modules.files.description="La biblioteca de archivos del proyecto. Los adjuntos de las tareas y las imágenes en los textos siguen disponibles." modules.reports.title="Informes" modules.reports.description="Informes del proyecto y resúmenes de estado." + +tabs.background = " Fondo" +menu.background = " Fondo" +text.set_your_background = "Elige el fondo de tus páginas: el degradado de color de tu esquema de color o una imagen." +label.background_type = "Fondo actual" +label.gradient = "Degradado de color" +label.image = "Imagen" +text.pick_background_image_below = "Elige una imagen abajo para usarla como fondo." +label.background_image = "Elige una imagen" +label.background_source = "Fuente de la imagen" +label.unsplash = "Unsplash" +input.search_unsplash = "Buscar en Unsplash" +label.search_for_image = "Buscar una imagen" +buttons.refresh_images = "Mostrar otras imágenes" +buttons.use_image = "Usar como fondo" +text.photo_by_on_unsplash = "Foto de %s en %s" +text.no_images_found = "No se encontraron imágenes" +notifications.background_settings_saved = "Fondo guardado" +notifications.background_not_saved = "No se pudo guardar el fondo" +notifications.background_upload_failed = "No se pudo guardar el fondo. Sube una imagen JPG, PNG, GIF o WebP." +tabs.webhooks = " Webhooks" diff --git a/app/Language/es-ES.ini b/app/Language/es-ES.ini index a920fc7a1e..0ff954d86d 100644 --- a/app/Language/es-ES.ini +++ b/app/Language/es-ES.ini @@ -2623,7 +2623,7 @@ label.notifications_my_work_description="Solo las tareas asignadas a ti, tus ele label.notifications_muted="Silenciado" label.notifications_muted_description="Ninguna notificación, salvo las @menciones directas" label.webhook_notifications="Webhook personal" -label.webhook_notifications_description="Envía tus notificaciones a tu propio endpoint HTTPS. Por cada notificación de proyecto que te corresponda, Leantime envía a esta URL una petición HTTP POST con un cuerpo JSON. Las peticiones se envían en segundo plano y pueden tardar varios minutos en llegar; si una petición falla, no se reintenta. Tus ajustes de recepción de notificaciones, de categorías y por proyecto (más arriba) determinan qué notificaciones te corresponden. El webhook solo envía mientras está activado y tiene una URL configurada. La URL se conserva aunque desactives el webhook; bórrala para eliminarla." +label.webhook_notifications_description="Envía tus notificaciones a tu propio endpoint HTTPS. Por cada notificación de proyecto que te corresponda, Leantime envía a esta URL una petición HTTP POST con un cuerpo JSON. Las peticiones se envían en segundo plano y pueden tardar varios minutos en llegar; si una petición falla, no se reintenta. Tus ajustes en la pestaña Notificaciones (recepción de notificaciones, categorías y niveles por proyecto) determinan qué notificaciones te corresponden. El webhook solo envía mientras está activado y tiene una URL configurada. La URL se conserva aunque desactives el webhook; bórrala para eliminarla." label.enable_webhook_notifications="Activar notificaciones por webhook" label.webhook_url_hint="Solo HTTPS. Déjalo vacío para eliminar el webhook." label.default_notification_types="Tipos de notificación predeterminados para usuarios nuevos" @@ -4047,3 +4047,24 @@ modules.files.title="Archivos" modules.files.description="La biblioteca de archivos del proyecto. Los adjuntos de las tareas y las imágenes de los textos siguen disponibles." modules.reports.title="Informes" modules.reports.description="Informes del proyecto y resúmenes de estado." + +tabs.background = " Fondo" +menu.background = " Fondo" +text.set_your_background = "Elige el fondo de tus páginas: el degradado de color de tu esquema de color o una imagen." +label.background_type = "Fondo actual" +label.gradient = "Degradado de color" +label.image = "Imagen" +text.pick_background_image_below = "Elige una imagen abajo para usarla como fondo." +label.background_image = "Elige una imagen" +label.background_source = "Fuente de la imagen" +label.unsplash = "Unsplash" +input.search_unsplash = "Buscar en Unsplash" +label.search_for_image = "Buscar una imagen" +buttons.refresh_images = "Mostrar otras imágenes" +buttons.use_image = "Usar como fondo" +text.photo_by_on_unsplash = "Foto de %s en %s" +text.no_images_found = "No se encontraron imágenes" +notifications.background_settings_saved = "Fondo guardado" +notifications.background_not_saved = "No se pudo guardar el fondo" +notifications.background_upload_failed = "No se pudo guardar el fondo. Sube una imagen JPG, PNG, GIF o WebP." +tabs.webhooks = " Webhooks" diff --git a/app/Language/es-MX.ini b/app/Language/es-MX.ini index 55bf3f18cd..076eb4edfa 100644 --- a/app/Language/es-MX.ini +++ b/app/Language/es-MX.ini @@ -2619,7 +2619,7 @@ label.notifications_my_work_description="Solo las tareas asignadas a ti, tus ele label.notifications_muted="Silenciado" label.notifications_muted_description="Ninguna notificación, salvo las @menciones directas" label.webhook_notifications="Webhook personal" -label.webhook_notifications_description="Envía tus notificaciones a tu propio endpoint HTTPS. Por cada notificación de proyecto que te corresponda, Leantime envía a esta URL una solicitud HTTP POST con un cuerpo JSON. Las solicitudes se envían en segundo plano y pueden tardar varios minutos en llegar; si una solicitud falla, no se reintenta. Tu configuración de recepción de notificaciones, de categorías y por proyecto (más arriba) determina qué notificaciones te corresponden. El webhook solo envía mientras está activado y tiene una URL configurada. La URL se conserva aunque desactives el webhook; bórrala para eliminarla." +label.webhook_notifications_description="Envía tus notificaciones a tu propio endpoint HTTPS. Por cada notificación de proyecto que te corresponda, Leantime envía a esta URL una solicitud HTTP POST con un cuerpo JSON. Las solicitudes se envían en segundo plano y pueden tardar varios minutos en llegar; si una solicitud falla, no se reintenta. Tu configuración en la pestaña Notificaciones (recepción de notificaciones, categorías y niveles por proyecto) determina qué notificaciones te corresponden. El webhook solo envía mientras está activado y tiene una URL configurada. La URL se conserva aunque desactives el webhook; bórrala para eliminarla." label.enable_webhook_notifications="Activar notificaciones por webhook" label.webhook_url_hint="Solo HTTPS. Déjalo vacío para eliminar el webhook." label.default_notification_types="Tipos de notificación predeterminados para usuarios nuevos" @@ -4029,3 +4029,24 @@ modules.files.title="Archivos" modules.files.description="La biblioteca de archivos del proyecto. Los adjuntos de las tareas y las imágenes en los textos siguen disponibles." modules.reports.title="Informes" modules.reports.description="Informes del proyecto y resúmenes de estado." + +tabs.background = " Fondo" +menu.background = " Fondo" +text.set_your_background = "Elige el fondo de tus páginas: el degradado de color de tu esquema de color o una imagen." +label.background_type = "Fondo actual" +label.gradient = "Degradado de color" +label.image = "Imagen" +text.pick_background_image_below = "Elige una imagen abajo para usarla como fondo." +label.background_image = "Elige una imagen" +label.background_source = "Fuente de la imagen" +label.unsplash = "Unsplash" +input.search_unsplash = "Buscar en Unsplash" +label.search_for_image = "Buscar una imagen" +buttons.refresh_images = "Mostrar otras imágenes" +buttons.use_image = "Usar como fondo" +text.photo_by_on_unsplash = "Foto de %s en %s" +text.no_images_found = "No se encontraron imágenes" +notifications.background_settings_saved = "Fondo guardado" +notifications.background_not_saved = "No se pudo guardar el fondo" +notifications.background_upload_failed = "No se pudo guardar el fondo. Sube una imagen JPG, PNG, GIF o WebP." +tabs.webhooks = " Webhooks" diff --git a/app/Language/et-EE.ini b/app/Language/et-EE.ini index 64b13b2b0c..060b65b3c6 100644 --- a/app/Language/et-EE.ini +++ b/app/Language/et-EE.ini @@ -2621,7 +2621,7 @@ label.notifications_my_work_description="Ainult sulle määratud ülesanded, sin label.notifications_muted="Vaigistatud" label.notifications_muted_description="Teavitusi ei tule, välja arvatud otsesed @mainimised" label.webhook_notifications="Isiklik webhook" -label.webhook_notifications_description="Saada oma teavitused enda HTTPS-lõpp-punkti. Iga projektiteavituse kohta, mis sulle ette nähtud on, saadab Leantime sellele URL-ile ühe JSON-sisuga HTTP POST päringu. Päringud saadetakse taustal ja nende kohalejõudmine võib võtta mitu minutit; ebaõnnestunud päringut uuesti ei saadeta. Milliseid teavitusi saadetakse, otsustavad ülaltoodud teavituste saamise, kategooria ja projektipõhised seaded. Webhook saadab teavitusi ainult siis, kui see on lubatud ja URL on määratud. Kui webhook on keelatud, jääb sinu URL salvestatuks — selle eemaldamiseks tühjenda URL." +label.webhook_notifications_description="Saada oma teavitused enda HTTPS-lõpp-punkti. Iga projektiteavituse kohta, mis sulle ette nähtud on, saadab Leantime sellele URL-ile ühe JSON-sisuga HTTP POST päringu. Päringud saadetakse taustal ja nende kohalejõudmine võib võtta mitu minutit; ebaõnnestunud päringut uuesti ei saadeta. Milliseid teavitusi saadetakse, otsustavad sinu seaded vahekaardil Teavitused (teavituste saamine, kategooriad ja projektipõhised tasemed). Webhook saadab teavitusi ainult siis, kui see on lubatud ja URL on määratud. Kui webhook on keelatud, jääb sinu URL salvestatuks — selle eemaldamiseks tühjenda URL." label.enable_webhook_notifications="Luba webhooki teavitused" label.webhook_url_hint="Ainult HTTPS. Webhooki eemaldamiseks jäta tühjaks." label.default_notification_types="Uute kasutajate vaikimisi teavitustüübid" @@ -4045,3 +4045,24 @@ modules.files.title="Failid" modules.files.description="Projekti failikogu. Ülesannete manused ja tekstides olevad pildid jäävad kättesaadavaks." modules.reports.title="Aruanded" modules.reports.description="Projekti aruanded ja staatuse ülevaated." + +tabs.background = " Taust" +menu.background = " Taust" +text.set_your_background = "Vali oma lehtede taust: värviskeemi värviüleminek või pilt." +label.background_type = "Praegune taust" +label.gradient = "Värviüleminek" +label.image = "Pilt" +text.pick_background_image_below = "Vali allpool pilt, mida taustana kasutada." +label.background_image = "Vali pilt" +label.background_source = "Pildi allikas" +label.unsplash = "Unsplash" +input.search_unsplash = "Otsi Unsplashist" +label.search_for_image = "Otsi pilti" +buttons.refresh_images = "Näita teisi pilte" +buttons.use_image = "Kasuta taustana" +text.photo_by_on_unsplash = "Foto: %s, %s" +text.no_images_found = "Pilte ei leitud" +notifications.background_settings_saved = "Taust salvestati" +notifications.background_not_saved = "Tausta ei õnnestunud salvestada" +notifications.background_upload_failed = "Tausta ei õnnestunud salvestada. Laadi üles JPG-, PNG-, GIF- või WebP-pilt." +tabs.webhooks = " Webhookid" diff --git a/app/Language/fa-IR.ini b/app/Language/fa-IR.ini index b432fd4e05..7c26e5572d 100644 --- a/app/Language/fa-IR.ini +++ b/app/Language/fa-IR.ini @@ -2621,7 +2621,7 @@ label.notifications_my_work_description="فقط کارهای واگذارشده label.notifications_muted="بی‌صدا شده" label.notifications_muted_description="هیچ اعلانی به‌جز @اشاره‌های مستقیم" label.webhook_notifications="وب‌هوک شخصی" -label.webhook_notifications_description="اعلان‌های خود را به نقطه پایانی HTTPS خودتان بفرستید. برای هر اعلان پروژه که واجد دریافت آن هستید، Leantime یک درخواست HTTP POST با بدنه JSON به این نشانی ارسال می‌کند. درخواست‌ها در پس‌زمینه ارسال می‌شوند و ممکن است رسیدنشان چند دقیقه طول بکشد؛ درخواستی که ناموفق باشد دوباره ارسال نمی‌شود. تنظیمات دریافت اعلان، دسته‌بندی و تنظیمات هر پروژه در بالا تعیین می‌کنند کدام اعلان‌ها واجد شرایط هستند. وب‌هوک فقط زمانی ارسال می‌کند که فعال باشد و نشانی آن تنظیم شده باشد. وقتی وب‌هوک غیرفعال است نشانی شما ذخیره می‌ماند — برای حذف آن، نشانی را پاک کنید." +label.webhook_notifications_description="اعلان‌های خود را به نقطه پایانی HTTPS خودتان بفرستید. برای هر اعلان پروژه که واجد دریافت آن هستید، Leantime یک درخواست HTTP POST با بدنه JSON به این نشانی ارسال می‌کند. درخواست‌ها در پس‌زمینه ارسال می‌شوند و ممکن است رسیدنشان چند دقیقه طول بکشد؛ درخواستی که ناموفق باشد دوباره ارسال نمی‌شود. تنظیمات شما در تب اعلان‌ها (دریافت اعلان، دسته‌بندی‌ها و سطح هر پروژه) تعیین می‌کنند کدام اعلان‌ها واجد شرایط هستند. وب‌هوک فقط زمانی ارسال می‌کند که فعال باشد و نشانی آن تنظیم شده باشد. وقتی وب‌هوک غیرفعال است نشانی شما ذخیره می‌ماند — برای حذف آن، نشانی را پاک کنید." label.enable_webhook_notifications="فعال‌سازی اعلان‌های وب‌هوک" label.webhook_url_hint="فقط HTTPS. برای حذف وب‌هوک خالی بگذارید." label.default_notification_types="انواع پیش‌فرض اعلان برای کاربران جدید" @@ -4045,3 +4045,24 @@ modules.files.title="فایل‌ها" modules.files.description="کتابخانه فایل‌های پروژه. پیوست‌های کارها و تصاویر درون متن‌ها همچنان در دسترس می‌مانند." modules.reports.title="گزارش‌ها" modules.reports.description="گزارش‌های پروژه و نماهای کلی وضعیت." + +tabs.background = " پس‌زمینه" +menu.background = " پس‌زمینه" +text.set_your_background = "پس‌زمینه صفحه‌هایتان را انتخاب کنید: گرادیان رنگ طرح رنگ شما یا یک تصویر." +label.background_type = "پس‌زمینه فعلی" +label.gradient = "گرادیان رنگ" +label.image = "تصویر" +text.pick_background_image_below = "در زیر یک تصویر انتخاب کنید تا به‌عنوان پس‌زمینه استفاده شود." +label.background_image = "انتخاب تصویر" +label.background_source = "منبع تصویر" +label.unsplash = "Unsplash" +input.search_unsplash = "جستجو در Unsplash" +label.search_for_image = "جستجوی تصویر" +buttons.refresh_images = "نمایش تصاویر دیگر" +buttons.use_image = "استفاده به‌عنوان پس‌زمینه" +text.photo_by_on_unsplash = "عکس از %s در %s" +text.no_images_found = "تصویری یافت نشد" +notifications.background_settings_saved = "پس‌زمینه ذخیره شد" +notifications.background_not_saved = "پس‌زمینه ذخیره نشد" +notifications.background_upload_failed = "پس‌زمینه ذخیره نشد. یک تصویر JPG، PNG، GIF یا WebP بارگذاری کنید." +tabs.webhooks = " وب‌هوک‌ها" diff --git a/app/Language/fr-CH.ini b/app/Language/fr-CH.ini index 9e52e4d75a..ad415ac5e2 100644 --- a/app/Language/fr-CH.ini +++ b/app/Language/fr-CH.ini @@ -1848,7 +1848,7 @@ label.notifications_my_work_description="Uniquement les tâches qui vous sont as label.notifications_muted="En sourdine" label.notifications_muted_description="Aucune notification, sauf les @mentions directes" label.webhook_notifications="Webhook personnel" -label.webhook_notifications_description="Envoyez vos notifications vers votre propre point de terminaison HTTPS. Pour chaque notification de projet à laquelle vous êtes éligible, Leantime envoie une requête HTTP POST avec un corps JSON à cette URL. Les requêtes sont envoyées en arrière-plan et peuvent mettre plusieurs minutes à arriver ; une requête en échec n'est pas relancée. Vos paramètres de réception, de catégories et par projet ci-dessus déterminent les notifications éligibles. Le webhook n'envoie que s'il est activé et qu'une URL est définie. Votre URL reste enregistrée lorsque le webhook est désactivé — videz l'URL pour la supprimer." +label.webhook_notifications_description="Envoyez vos notifications vers votre propre point de terminaison HTTPS. Pour chaque notification de projet à laquelle vous êtes éligible, Leantime envoie une requête HTTP POST avec un corps JSON à cette URL. Les requêtes sont envoyées en arrière-plan et peuvent mettre plusieurs minutes à arriver ; une requête en échec n'est pas relancée. Vos paramètres de l'onglet Notifications (réception, catégories et niveaux par projet) déterminent les notifications éligibles. Le webhook n'envoie que s'il est activé et qu'une URL est définie. Votre URL reste enregistrée lorsque le webhook est désactivé — videz l'URL pour la supprimer." label.enable_webhook_notifications="Activer les notifications par webhook" label.webhook_url_hint="HTTPS uniquement. Laissez vide pour supprimer le webhook." label.default_notification_types="Types de notification par défaut pour les nouveaux utilisateurs" @@ -3923,3 +3923,24 @@ modules.files.title="Fichiers" modules.files.description="La bibliothèque de fichiers du projet. Les pièces jointes des tâches et les images insérées dans les textes restent disponibles." modules.reports.title="Rapports" modules.reports.description="Rapports de projet et aperçus de l'état d'avancement." + +tabs.background = " Arrière-plan" +menu.background = " Arrière-plan" +text.set_your_background = "Choisissez l'arrière-plan de vos pages : le dégradé de votre palette de couleurs ou une image." +label.background_type = "Arrière-plan actuel" +label.gradient = "Dégradé de couleurs" +label.image = "Image" +text.pick_background_image_below = "Choisissez ci-dessous une image à utiliser comme arrière-plan." +label.background_image = "Choisir une image" +label.background_source = "Source de l'image" +label.unsplash = "Unsplash" +input.search_unsplash = "Rechercher sur Unsplash" +label.search_for_image = "Rechercher une image" +buttons.refresh_images = "Afficher d'autres images" +buttons.use_image = "Utiliser comme arrière-plan" +text.photo_by_on_unsplash = "Photo de %s sur %s" +text.no_images_found = "Aucune image trouvée" +notifications.background_settings_saved = "Arrière-plan enregistré" +notifications.background_not_saved = "L'arrière-plan n'a pas pu être enregistré" +notifications.background_upload_failed = "L'arrière-plan n'a pas pu être enregistré. Importez une image JPG, PNG, GIF ou WebP." +tabs.webhooks = " Webhooks" diff --git a/app/Language/fr-FR.ini b/app/Language/fr-FR.ini index 4ed4a1d540..44589718cc 100644 --- a/app/Language/fr-FR.ini +++ b/app/Language/fr-FR.ini @@ -922,7 +922,7 @@ label.notifications_my_work_description="Uniquement les tâches qui vous sont as label.notifications_muted="En sourdine" label.notifications_muted_description="Aucune notification, sauf les @mentions directes" label.webhook_notifications="Webhook personnel" -label.webhook_notifications_description="Envoyez vos notifications vers votre propre point de terminaison HTTPS. Pour chaque notification de projet à laquelle vous êtes éligible, Leantime envoie une requête HTTP POST avec un corps JSON à cette URL. Les requêtes sont envoyées en arrière-plan et peuvent mettre plusieurs minutes à arriver ; une requête en échec n'est pas relancée. Vos paramètres de réception, de catégories et par projet ci-dessus déterminent les notifications éligibles. Le webhook n'envoie que s'il est activé et qu'une URL est définie. Votre URL reste enregistrée lorsque le webhook est désactivé — videz l'URL pour la supprimer." +label.webhook_notifications_description="Envoyez vos notifications vers votre propre point de terminaison HTTPS. Pour chaque notification de projet à laquelle vous êtes éligible, Leantime envoie une requête HTTP POST avec un corps JSON à cette URL. Les requêtes sont envoyées en arrière-plan et peuvent mettre plusieurs minutes à arriver ; une requête en échec n'est pas relancée. Vos paramètres de l'onglet Notifications (réception, catégories et niveaux par projet) déterminent les notifications éligibles. Le webhook n'envoie que s'il est activé et qu'une URL est définie. Votre URL reste enregistrée lorsque le webhook est désactivé — videz l'URL pour la supprimer." label.enable_webhook_notifications="Activer les notifications par webhook" label.webhook_url_hint="HTTPS uniquement. Laissez vide pour supprimer le webhook." label.default_notification_types="Types de notification par défaut pour les nouveaux utilisateurs" @@ -4045,3 +4045,24 @@ modules.files.title="Fichiers" modules.files.description="La bibliothèque de fichiers du projet. Les pièces jointes des tâches et les images insérées dans les textes restent disponibles." modules.reports.title="Rapports" modules.reports.description="Rapports de projet et aperçus de l'état d'avancement." + +tabs.background = " Arrière-plan" +menu.background = " Arrière-plan" +text.set_your_background = "Choisissez l'arrière-plan de vos pages : le dégradé de couleurs de votre palette ou une image." +label.background_type = "Arrière-plan actuel" +label.gradient = "Dégradé de couleurs" +label.image = "Image" +text.pick_background_image_below = "Choisissez ci-dessous une image à utiliser comme arrière-plan." +label.background_image = "Choisir une image" +label.background_source = "Source de l'image" +label.unsplash = "Unsplash" +input.search_unsplash = "Rechercher sur Unsplash" +label.search_for_image = "Rechercher une image" +buttons.refresh_images = "Afficher d'autres images" +buttons.use_image = "Utiliser comme arrière-plan" +text.photo_by_on_unsplash = "Photo de %s sur %s" +text.no_images_found = "Aucune image trouvée" +notifications.background_settings_saved = "Arrière-plan enregistré" +notifications.background_not_saved = "L'arrière-plan n'a pas pu être enregistré" +notifications.background_upload_failed = "L'arrière-plan n'a pas pu être enregistré. Importez une image JPG, PNG, GIF ou WebP." +tabs.webhooks = " Webhooks" diff --git a/app/Language/he-IL.ini b/app/Language/he-IL.ini index 82ef05dde7..148c4613a4 100644 --- a/app/Language/he-IL.ini +++ b/app/Language/he-IL.ini @@ -2623,7 +2623,7 @@ label.notifications_my_work_description="רק משימות שהוקצו לך, ה label.notifications_muted="מושתק" label.notifications_muted_description="ללא התראות מלבד אזכורים (@) ישירים" label.webhook_notifications="Webhook אישי" -label.webhook_notifications_description="שליחת ההתראות שלך לנקודת קצה HTTPS משלך. על כל התראת מיזם שמגיעה לך, Leantime שולח לכתובת הזאת בקשת HTTP POST אחת עם גוף JSON. הבקשות נשלחות ברקע ויכולות להגיע בעיכוב של כמה דקות; בקשה שנכשלת אינה נשלחת שוב. הגדרות קבלת ההתראות, הקטגוריות וההגדרות לפי מיזם שלמעלה קובעות אילו התראות מגיעות לך. ה־webhook שולח רק כשהוא מופעל ומוגדרת כתובת. הכתובת שלך נשארת שמורה גם כשה־webhook מושבת — כדי להסיר אותה יש לרוקן את הכתובת." +label.webhook_notifications_description="שליחת ההתראות שלך לנקודת קצה HTTPS משלך. על כל התראת מיזם שמגיעה לך, Leantime שולח לכתובת הזאת בקשת HTTP POST אחת עם גוף JSON. הבקשות נשלחות ברקע ויכולות להגיע בעיכוב של כמה דקות; בקשה שנכשלת אינה נשלחת שוב. ההגדרות שלך בלשונית התראות (קבלת התראות, קטגוריות ורמות לפי מיזם) קובעות אילו התראות מגיעות לך. ה־webhook שולח רק כשהוא מופעל ומוגדרת כתובת. הכתובת שלך נשארת שמורה גם כשה־webhook מושבת — כדי להסיר אותה יש לרוקן את הכתובת." label.enable_webhook_notifications="הפעלת התראות webhook" label.webhook_url_hint="HTTPS בלבד. יש להשאיר ריק כדי להסיר את ה־webhook." label.default_notification_types="סוגי התראות ברירת מחדל למשתמשים חדשים" @@ -4044,3 +4044,24 @@ modules.files.title="קבצים" modules.files.description="ספריית הקבצים של המיזם. קבצים מצורפים במטלות ותמונות בתוך טקסטים נשארים זמינים." modules.reports.title="דוחות" modules.reports.description="דוחות מיזם וסקירות מצב." + +tabs.background = " רקע" +menu.background = " רקע" +text.set_your_background = "בחרו את הרקע של הדפים שלכם: מעבר הצבעים של ערכת הצבעים או תמונה." +label.background_type = "הרקע הנוכחי" +label.gradient = "מעבר צבעים" +label.image = "תמונה" +text.pick_background_image_below = "בחרו למטה תמונה לשימוש כרקע." +label.background_image = "בחירת תמונה" +label.background_source = "מקור התמונה" +label.unsplash = "Unsplash" +input.search_unsplash = "חיפוש ב-Unsplash" +label.search_for_image = "חיפוש תמונה" +buttons.refresh_images = "הצגת תמונות אחרות" +buttons.use_image = "שימוש כרקע" +text.photo_by_on_unsplash = "צילום: %s ב-%s" +text.no_images_found = "לא נמצאו תמונות" +notifications.background_settings_saved = "הרקע נשמר" +notifications.background_not_saved = "לא ניתן היה לשמור את הרקע" +notifications.background_upload_failed = "לא ניתן היה לשמור את הרקע. העלו תמונת JPG, PNG, GIF או WebP." +tabs.webhooks = " Webhooks" diff --git a/app/Language/hr-HR.ini b/app/Language/hr-HR.ini index 149d3634d0..54b0fdb037 100644 --- a/app/Language/hr-HR.ini +++ b/app/Language/hr-HR.ini @@ -2623,7 +2623,7 @@ label.notifications_my_work_description="Samo zadaci dodijeljeni vama, vaše sta label.notifications_muted="Utišano" label.notifications_muted_description="Bez obavijesti osim izravnih @spominjanja" label.webhook_notifications="Osobni webhook" -label.webhook_notifications_description="Šaljite svoje obavijesti na vlastitu HTTPS krajnju točku. Za svaku obavijest projekta na koju imate pravo Leantime šalje jedan HTTP POST zahtjev s JSON tijelom na ovaj URL. Zahtjevi se šalju u pozadini i mogu stići tek nakon nekoliko minuta; neuspjeli zahtjev ne šalje se ponovno. Vaše postavke primanja obavijesti, kategorija i postavke po projektu iznad određuju na koje obavijesti imate pravo. Webhook šalje samo dok je uključen i dok je URL postavljen. Vaš URL ostaje spremljen dok je webhook isključen — ispraznite URL da biste ga uklonili." +label.webhook_notifications_description="Šaljite svoje obavijesti na vlastitu HTTPS krajnju točku. Za svaku obavijest projekta na koju imate pravo Leantime šalje jedan HTTP POST zahtjev s JSON tijelom na ovaj URL. Zahtjevi se šalju u pozadini i mogu stići tek nakon nekoliko minuta; neuspjeli zahtjev ne šalje se ponovno. Vaše postavke na kartici Obavijesti (primanje obavijesti, kategorije i razine po projektu) određuju na koje obavijesti imate pravo. Webhook šalje samo dok je uključen i dok je URL postavljen. Vaš URL ostaje spremljen dok je webhook isključen — ispraznite URL da biste ga uklonili." label.enable_webhook_notifications="Uključi obavijesti putem webhooka" label.webhook_url_hint="Samo HTTPS. Ostavite prazno da biste uklonili webhook." label.default_notification_types="Zadane vrste obavijesti za nove korisnike" @@ -4044,3 +4044,24 @@ modules.files.title="Datoteke" modules.files.description="Biblioteka datoteka projekta. Privitci na zadacima i slike u tekstovima ostaju dostupni." modules.reports.title="Izvješća" modules.reports.description="Izvješća o projektu i pregledi stanja." + +tabs.background = " Pozadina" +menu.background = " Pozadina" +text.set_your_background = "Odaberite pozadinu svojih stranica: prijelaz boja vaše sheme boja ili sliku." +label.background_type = "Trenutačna pozadina" +label.gradient = "Prijelaz boja" +label.image = "Slika" +text.pick_background_image_below = "Odaberite sliku ispod da je upotrijebite kao pozadinu." +label.background_image = "Odaberite sliku" +label.background_source = "Izvor slike" +label.unsplash = "Unsplash" +input.search_unsplash = "Pretraži Unsplash" +label.search_for_image = "Pretraži slike" +buttons.refresh_images = "Prikaži druge slike" +buttons.use_image = "Upotrijebi kao pozadinu" +text.photo_by_on_unsplash = "Fotografija: %s na %s" +text.no_images_found = "Nema pronađenih slika" +notifications.background_settings_saved = "Pozadina je spremljena" +notifications.background_not_saved = "Pozadinu nije moguće spremiti" +notifications.background_upload_failed = "Pozadinu nije moguće spremiti. Učitajte sliku u formatu JPG, PNG, GIF ili WebP." +tabs.webhooks = " Webhookovi" diff --git a/app/Language/hu-HU.ini b/app/Language/hu-HU.ini index 0ea7a79d3a..29821337ee 100644 --- a/app/Language/hu-HU.ini +++ b/app/Language/hu-HU.ini @@ -2629,7 +2629,7 @@ label.notifications_my_work_description="Csak az Önhöz rendelt feladatok, az label.notifications_muted="Némítva" label.notifications_muted_description="Nincs értesítés, kivéve a közvetlen @említéseket" label.webhook_notifications="Személyes webhook" -label.webhook_notifications_description="Értesítéseit a saját HTTPS-végpontjára is küldheti. A Leantime minden olyan projektértesítésnél, amelyre Ön jogosult, egy HTTP POST kérést küld JSON-törzzsel erre az URL-címre. A kérések a háttérben kerülnek elküldésre, és több percig is eltarthat, mire megérkeznek; a sikertelen kéréseket a rendszer nem küldi újra. A fenti értesítésfogadási, kategória- és projektenkénti beállítások határozzák meg, mely értesítésekre jogosult. A webhook csak akkor küld, ha engedélyezve van, és meg van adva URL-cím. Az URL-cím a webhook letiltása esetén is mentve marad — az eltávolításához ürítse ki az URL mezőt." +label.webhook_notifications_description="Értesítéseit a saját HTTPS-végpontjára is küldheti. A Leantime minden olyan projektértesítésnél, amelyre Ön jogosult, egy HTTP POST kérést küld JSON-törzzsel erre az URL-címre. A kérések a háttérben kerülnek elküldésre, és több percig is eltarthat, mire megérkeznek; a sikertelen kéréseket a rendszer nem küldi újra. Az Értesítések lapon megadott beállításai (értesítésfogadás, kategóriák és projektenkénti szintek) határozzák meg, mely értesítésekre jogosult. A webhook csak akkor küld, ha engedélyezve van, és meg van adva URL-cím. Az URL-cím a webhook letiltása esetén is mentve marad — az eltávolításához ürítse ki az URL mezőt." label.enable_webhook_notifications="Webhook-értesítések engedélyezése" label.webhook_url_hint="Csak HTTPS. Hagyja üresen a webhook eltávolításához." label.default_notification_types="Alapértelmezett értesítéstípusok új felhasználók számára" @@ -4050,3 +4050,24 @@ modules.files.title="Fájlok" modules.files.description="A projekt fájltára. A teendők mellékletei és a szövegekben lévő képek továbbra is elérhetők maradnak." modules.reports.title="Jelentések" modules.reports.description="Projektjelentések és állapotáttekintések." + +tabs.background = " Háttér" +menu.background = " Háttér" +text.set_your_background = "Válassza ki oldalai hátterét: a színséma színátmenetét vagy egy képet." +label.background_type = "Jelenlegi háttér" +label.gradient = "Színátmenet" +label.image = "Kép" +text.pick_background_image_below = "Válasszon ki lent egy képet, hogy háttérként használja." +label.background_image = "Kép kiválasztása" +label.background_source = "Kép forrása" +label.unsplash = "Unsplash" +input.search_unsplash = "Keresés az Unsplashen" +label.search_for_image = "Kép keresése" +buttons.refresh_images = "Más képek megjelenítése" +buttons.use_image = "Használat háttérként" +text.photo_by_on_unsplash = "Fotó: %s, forrás: %s" +text.no_images_found = "Nem található kép" +notifications.background_settings_saved = "A háttér mentve" +notifications.background_not_saved = "A hátteret nem sikerült menteni" +notifications.background_upload_failed = "A hátteret nem sikerült menteni. Töltsön fel JPG, PNG, GIF vagy WebP képet." +tabs.webhooks = " Webhookok" diff --git a/app/Language/id-ID.ini b/app/Language/id-ID.ini index b46dfa54c2..78dc20dd19 100644 --- a/app/Language/id-ID.ini +++ b/app/Language/id-ID.ini @@ -3516,7 +3516,7 @@ text.app_url_mismatch_warning = "Tautan dalam email mengarah ke %s, tetapi Anda text.confirm_delete = "Anda yakin ingin menghapus item ini?" label.chart_metric = "Metrik bagan" label.webhook_notifications = "Webhook Pribadi" -label.webhook_notifications_description = "Kirim notifikasi Anda ke endpoint HTTPS milik Anda sendiri. Untuk setiap notifikasi proyek yang berlaku bagi Anda, Leantime mengirim satu permintaan HTTP POST dengan isi JSON ke URL ini. Permintaan dikirim di latar belakang dan bisa memerlukan beberapa menit untuk tiba; permintaan yang gagal tidak dicoba ulang. Pengaturan terima notifikasi, kategori, dan per proyek di atas menentukan notifikasi mana yang berlaku. Webhook hanya mengirim selama diaktifkan dan URL telah diatur. URL Anda tetap tersimpan selama webhook dinonaktifkan — kosongkan URL untuk menghapusnya." +label.webhook_notifications_description ="Kirim notifikasi Anda ke endpoint HTTPS milik Anda sendiri. Untuk setiap notifikasi proyek yang berlaku bagi Anda, Leantime mengirim satu permintaan HTTP POST dengan isi JSON ke URL ini. Permintaan dikirim di latar belakang dan bisa memerlukan beberapa menit untuk tiba; permintaan yang gagal tidak dicoba ulang. Pengaturan Anda di tab Notifikasi (terima notifikasi, kategori, dan tingkat per proyek) menentukan notifikasi mana yang berlaku. Webhook hanya mengirim selama diaktifkan dan URL telah diatur. URL Anda tetap tersimpan selama webhook dinonaktifkan — kosongkan URL untuk menghapusnya." label.enable_webhook_notifications = "Aktifkan notifikasi webhook" label.webhook_url_hint = "Hanya HTTPS. Biarkan kosong untuk menghapus webhook." label.kanban_view_menu = "Kartu" @@ -4208,3 +4208,24 @@ modules.files.title = "File" modules.files.description = "Pustaka file proyek. Lampiran pada tugas dan gambar dalam teks tetap tersedia." modules.reports.title = "Laporan" modules.reports.description = "Laporan proyek dan ringkasan status." + +tabs.background = " Latar Belakang" +menu.background = " Latar Belakang" +text.set_your_background = "Pilih latar belakang halaman Anda: gradasi warna dari skema warna Anda atau sebuah gambar." +label.background_type = "Latar belakang saat ini" +label.gradient = "Gradasi warna" +label.image = "Gambar" +text.pick_background_image_below = "Pilih gambar di bawah untuk dipakai sebagai latar belakang." +label.background_image = "Pilih gambar" +label.background_source = "Sumber gambar" +label.unsplash = "Unsplash" +input.search_unsplash = "Cari di Unsplash" +label.search_for_image = "Cari gambar" +buttons.refresh_images = "Tampilkan gambar lain" +buttons.use_image = "Pakai sebagai latar belakang" +text.photo_by_on_unsplash = "Foto oleh %s di %s" +text.no_images_found = "Gambar tidak ditemukan" +notifications.background_settings_saved = "Latar belakang disimpan" +notifications.background_not_saved = "Latar belakang tidak dapat disimpan" +notifications.background_upload_failed = "Latar belakang tidak dapat disimpan. Unggah gambar JPG, PNG, GIF, atau WebP." +tabs.webhooks = " Webhook" diff --git a/app/Language/is-IS.ini b/app/Language/is-IS.ini index 4b51cae345..510001243e 100644 --- a/app/Language/is-IS.ini +++ b/app/Language/is-IS.ini @@ -2623,7 +2623,7 @@ label.notifications_my_work_description="Aðeins verk sem eru úthlutuð þér, label.notifications_muted="Þaggað" label.notifications_muted_description="Engar tilkynningar nema beinar @-tilvísanir" label.webhook_notifications="Persónulegur vefkrókur" -label.webhook_notifications_description="Sendu tilkynningarnar þínar á þinn eigin HTTPS-endapunkt. Fyrir hverja verkefnatilkynningu sem á við þig sendir Leantime eina HTTP POST-beiðni með JSON-meginmáli á þessa slóð. Beiðnir eru sendar í bakgrunni og getur tekið nokkrar mínútur fyrir þær að berast; beiðni sem mistekst er ekki reynd aftur. Stillingarnar hér að ofan fyrir móttöku tilkynninga, flokka og einstök verkefni ráða því hvaða tilkynningar eiga við. Vefkrókurinn sendir aðeins þegar hann er virkur og slóð hefur verið stillt. Slóðin helst vistuð þótt vefkrókurinn sé óvirkur — hreinsaðu slóðina til að fjarlægja hana." +label.webhook_notifications_description="Sendu tilkynningarnar þínar á þinn eigin HTTPS-endapunkt. Fyrir hverja verkefnatilkynningu sem á við þig sendir Leantime eina HTTP POST-beiðni með JSON-meginmáli á þessa slóð. Beiðnir eru sendar í bakgrunni og getur tekið nokkrar mínútur fyrir þær að berast; beiðni sem mistekst er ekki reynd aftur. Stillingarnar þínar á flipanum Tilkynningar (móttaka tilkynninga, flokkar og stig fyrir einstök verkefni) ráða því hvaða tilkynningar eiga við. Vefkrókurinn sendir aðeins þegar hann er virkur og slóð hefur verið stillt. Slóðin helst vistuð þótt vefkrókurinn sé óvirkur — hreinsaðu slóðina til að fjarlægja hana." label.enable_webhook_notifications="Virkja tilkynningar í gegnum vefkrók" label.webhook_url_hint="Aðeins HTTPS. Skildu eftir autt til að fjarlægja vefkrókinn." label.default_notification_types="Sjálfgefnar tegundir tilkynninga fyrir nýja notendur" @@ -4044,3 +4044,24 @@ modules.files.title="Skrár" modules.files.description="Skráasafn verkefnisins. Viðhengi á verkum og myndir í texta verða áfram aðgengileg." modules.reports.title="Skýrslur" modules.reports.description="Verkefnaskýrslur og stöðuyfirlit." + +tabs.background = " Bakgrunnur" +menu.background = " Bakgrunnur" +text.set_your_background = "Veldu bakgrunn síðnanna þinna: litaskiptinguna úr litaþemanu þínu eða mynd." +label.background_type = "Núverandi bakgrunnur" +label.gradient = "Litaskipting" +label.image = "Mynd" +text.pick_background_image_below = "Veldu mynd hér að neðan til að nota sem bakgrunn." +label.background_image = "Veldu mynd" +label.background_source = "Uppruni myndar" +label.unsplash = "Unsplash" +input.search_unsplash = "Leita á Unsplash" +label.search_for_image = "Leita að mynd" +buttons.refresh_images = "Sýna aðrar myndir" +buttons.use_image = "Nota sem bakgrunn" +text.photo_by_on_unsplash = "Ljósmynd: %s á %s" +text.no_images_found = "Engar myndir fundust" +notifications.background_settings_saved = "Bakgrunnur vistaður" +notifications.background_not_saved = "Ekki tókst að vista bakgrunninn" +notifications.background_upload_failed = "Ekki tókst að vista bakgrunninn. Hladdu upp JPG, PNG, GIF eða WebP mynd." +tabs.webhooks = " Vefkrókar" diff --git a/app/Language/it-IT.ini b/app/Language/it-IT.ini index b36da64fb8..5d053b20df 100644 --- a/app/Language/it-IT.ini +++ b/app/Language/it-IT.ini @@ -2623,7 +2623,7 @@ label.notifications_my_work_description="Solo le attività assegnate a te, i tuo label.notifications_muted="Silenziato" label.notifications_muted_description="Nessuna notifica, tranne le @menzioni dirette" label.webhook_notifications="Webhook personale" -label.webhook_notifications_description="Invia le tue notifiche al tuo endpoint HTTPS. Per ogni notifica di progetto che puoi ricevere, Leantime invia a questo URL una richiesta HTTP POST con un corpo JSON. Le richieste vengono inviate in background e possono impiegare diversi minuti ad arrivare; una richiesta non riuscita non viene ripetuta. Le impostazioni di ricezione, per categoria e per progetto qui sopra determinano quali notifiche puoi ricevere. Il webhook invia solo quando è attivo e un URL è impostato. Il tuo URL resta salvato anche con il webhook disattivato: svuota l'URL per rimuoverlo." +label.webhook_notifications_description="Invia le tue notifiche al tuo endpoint HTTPS. Per ogni notifica di progetto che puoi ricevere, Leantime invia a questo URL una richiesta HTTP POST con un corpo JSON. Le richieste vengono inviate in background e possono impiegare diversi minuti ad arrivare; una richiesta non riuscita non viene ripetuta. Le tue impostazioni nella scheda Notifiche (ricezione, categorie e livelli per progetto) determinano quali notifiche puoi ricevere. Il webhook invia solo quando è attivo e un URL è impostato. Il tuo URL resta salvato anche con il webhook disattivato: svuota l'URL per rimuoverlo." label.enable_webhook_notifications="Attiva le notifiche webhook" label.webhook_url_hint="Solo HTTPS. Lascia vuoto per rimuovere il webhook." label.default_notification_types="Tipi di notifica predefiniti per i nuovi utenti" @@ -4044,3 +4044,24 @@ modules.files.title="File" modules.files.description="La libreria dei file del progetto. Gli allegati delle To-Do e le immagini nei testi restano disponibili." modules.reports.title="Rapporti" modules.reports.description="Rapporti di progetto e panoramiche dello stato." + +tabs.background = " Sfondo" +menu.background = " Sfondo" +text.set_your_background = "Scegli lo sfondo delle tue pagine: il gradiente di colore della tua combinazione di colori o un'immagine." +label.background_type = "Sfondo attuale" +label.gradient = "Gradiente di colore" +label.image = "Immagine" +text.pick_background_image_below = "Scegli qui sotto un'immagine da usare come sfondo." +label.background_image = "Scegli un'immagine" +label.background_source = "Fonte dell'immagine" +label.unsplash = "Unsplash" +input.search_unsplash = "Cerca su Unsplash" +label.search_for_image = "Cerca un'immagine" +buttons.refresh_images = "Mostra altre immagini" +buttons.use_image = "Usa come sfondo" +text.photo_by_on_unsplash = "Foto di %s su %s" +text.no_images_found = "Nessuna immagine trovata" +notifications.background_settings_saved = "Sfondo salvato" +notifications.background_not_saved = "Non è stato possibile salvare lo sfondo" +notifications.background_upload_failed = "Non è stato possibile salvare lo sfondo. Carica un'immagine JPG, PNG, GIF o WebP." +tabs.webhooks = " Webhook" diff --git a/app/Language/ja-JP.ini b/app/Language/ja-JP.ini index 93a38f6a5f..c2126fd7c5 100644 --- a/app/Language/ja-JP.ini +++ b/app/Language/ja-JP.ini @@ -2623,7 +2623,7 @@ label.notifications_my_work_description="自分に割り当てられたタスク label.notifications_muted="ミュート中" label.notifications_muted_description="直接の@メンション以外は通知しません" label.webhook_notifications="個人用Webhook" -label.webhook_notifications_description="通知を自分のHTTPSエンドポイントに送信します。受信対象となるプロジェクト通知ごとに、LeantimeはJSON本文を含むHTTP POSTリクエストをこのURLに1件送信します。リクエストはバックグラウンドで送信されるため、届くまで数分かかることがあります。失敗したリクエストは再送されません。どの通知が対象になるかは、上記の通知の受信、カテゴリ、プロジェクトごとの設定で決まります。Webhookは有効化されていてURLが設定されている場合にのみ送信されます。Webhookを無効にしてもURLは保存されたままです。削除するにはURLを消去してください。" +label.webhook_notifications_description="通知を自分のHTTPSエンドポイントに送信します。受信対象となるプロジェクト通知ごとに、LeantimeはJSON本文を含むHTTP POSTリクエストをこのURLに1件送信します。リクエストはバックグラウンドで送信されるため、届くまで数分かかることがあります。失敗したリクエストは再送されません。どの通知が対象になるかは、通知タブの設定(通知の受信、カテゴリ、プロジェクトごとのレベル)で決まります。Webhookは有効化されていてURLが設定されている場合にのみ送信されます。Webhookを無効にしてもURLは保存されたままです。削除するにはURLを消去してください。" label.enable_webhook_notifications="Webhook通知を有効にする" label.webhook_url_hint="HTTPSのみ。空欄にするとWebhookを削除します。" label.default_notification_types="新規ユーザーのデフォルト通知種別" @@ -4044,3 +4044,24 @@ modules.files.title="ファイル" modules.files.description="プロジェクトのファイルライブラリ。タスクの添付ファイルやテキスト内の画像は、引き続き利用できます。" modules.reports.title="レポート" modules.reports.description="プロジェクトレポートとステータスの概要。" + +tabs.background = " 背景" +menu.background = " 背景" +text.set_your_background = "ページの背景を選択します。配色のカラーグラデーションまたは画像から選べます。" +label.background_type = "現在の背景" +label.gradient = "カラーグラデーション" +label.image = "画像" +text.pick_background_image_below = "背景として使う画像を下から選択してください。" +label.background_image = "画像を選択" +label.background_source = "画像のソース" +label.unsplash = "Unsplash" +input.search_unsplash = "Unsplashを検索" +label.search_for_image = "画像を検索" +buttons.refresh_images = "他の画像を表示" +buttons.use_image = "背景に使用" +text.photo_by_on_unsplash = "写真: %s / %s" +text.no_images_found = "画像が見つかりません" +notifications.background_settings_saved = "背景を保存しました" +notifications.background_not_saved = "背景を保存できませんでした" +notifications.background_upload_failed = "背景を保存できませんでした。JPG、PNG、GIF、WebPのいずれかの画像をアップロードしてください。" +tabs.webhooks = " Webhook" diff --git a/app/Language/kaa.ini b/app/Language/kaa.ini index bbe7e78696..da8d14c4c4 100644 --- a/app/Language/kaa.ini +++ b/app/Language/kaa.ini @@ -2623,7 +2623,7 @@ label.notifications_my_work_description="Tek sizge tayınlanǵan wazıypalar, si label.notifications_muted="Únsiz" label.notifications_muted_description="Tikkeley @belgilewlerden basqa xabarlandırıwlar joq" label.webhook_notifications="Jeke webhook" -label.webhook_notifications_description="Xabarlandırıwlarıńızdı óz HTTPS mánzilińizge jiberiń. Siz alıwǵa haqılı bolǵan hár bir joba xabarlandırıwı ushın Leantime usı URL mánzilge JSON mazmunlı bir HTTP POST sorawın jiberedi. Sorawlar fonda jiberiledi hám jetip barıwı ushın bir neshe minut ketiwi múmkin; sátsiz bolǵan soraw qaytalanbaydı. Qaysı xabarlandırıwlardıń jiberiletuǵının joqarıdaǵı xabarlandırıwlardı alıw, kategoriya hám jobalar boyınsha sazlawlarıńız anıqlaydı. Webhook tek qosılǵan hám URL kiritilgen waqıtta jiberedi. Webhook óshirilgen waqıtta URL saqlanıp qaladı — onı alıp taslaw ushın URL maydanın tazalań." +label.webhook_notifications_description="Xabarlandırıwlarıńızdı óz HTTPS mánzilińizge jiberiń. Siz alıwǵa haqılı bolǵan hár bir joba xabarlandırıwı ushın Leantime usı URL mánzilge JSON mazmunlı bir HTTP POST sorawın jiberedi. Sorawlar fonda jiberiledi hám jetip barıwı ushın bir neshe minut ketiwi múmkin; sátsiz bolǵan soraw qaytalanbaydı. Qaysı xabarlandırıwlardıń jiberiletuǵının Xabarlandırıwlar yorlıǵındaǵı sazlawlarıńız (xabarlandırıwlardı alıw, kategoriyalar hám jobalar boyınsha dárejeler) anıqlaydı. Webhook tek qosılǵan hám URL kiritilgen waqıtta jiberedi. Webhook óshirilgen waqıtta URL saqlanıp qaladı — onı alıp taslaw ushın URL maydanın tazalań." label.enable_webhook_notifications="Webhook xabarlandırıwların qosıw" label.webhook_url_hint="Tek HTTPS. Webhook baylanısın óshiriw ushın bos qaldırıń." label.default_notification_types="Jańa paydalanıwshılar ushın standart xabarlandırıw túrleri" @@ -4044,3 +4044,24 @@ modules.files.title="Fayllar" modules.files.description="Jobanıń fayllar kitapxanası. Wazıypalarǵa qosılǵan fayllar hám tekstlerdegi súwretler qoljetimli bolıp qala beredi." modules.reports.title="Esabatlar" modules.reports.description="Joba esabatları hám jaǵday boyınsha sholıwlar." + +tabs.background = " Fon" +menu.background = " Fon" +text.set_your_background = "Betlerińizdiń fonın tańlań: reń sxemańızdıń reń gradientin yamasa súwretti." +label.background_type = "Házirgi fon" +label.gradient = "Reń gradienti" +label.image = "Súwret" +text.pick_background_image_below = "Fon retinde paydalanıw ushın tómennen súwret tańlań." +label.background_image = "Súwret tańlaw" +label.background_source = "Súwret deregi" +label.unsplash = "Unsplash" +input.search_unsplash = "Unsplash-tan izlew" +label.search_for_image = "Súwret izlew" +buttons.refresh_images = "Basqa súwretlerdi kórsetiw" +buttons.use_image = "Fon retinde paydalanıw" +text.photo_by_on_unsplash = "Foto: %s, %s" +text.no_images_found = "Súwretler tabılmadı" +notifications.background_settings_saved = "Fon saqlandı" +notifications.background_not_saved = "Fondı saqlaw múmkin bolmadı" +notifications.background_upload_failed = "Fondı saqlaw múmkin bolmadı. JPG, PNG, GIF yamasa WebP súwretin júkleń." +tabs.webhooks = " Webhooklar" diff --git a/app/Language/km-KH.ini b/app/Language/km-KH.ini index f37afae8d5..429456fd2a 100644 --- a/app/Language/km-KH.ini +++ b/app/Language/km-KH.ini @@ -2623,7 +2623,7 @@ label.notifications_my_work_description="តែកិច្ចការដែល label.notifications_muted="បានបិទសំឡេង" label.notifications_muted_description="គ្មានការជូនដំណឹង លើកលែងតែ @mentions ផ្ទាល់" label.webhook_notifications="Webhook ផ្ទាល់ខ្លួន" -label.webhook_notifications_description="ផ្ញើការជូនដំណឹងរបស់អ្នកទៅកាន់ HTTPS endpoint ផ្ទាល់ខ្លួនរបស់អ្នក។ សម្រាប់រាល់ការជូនដំណឹងគម្រោងដែលអ្នកមានសិទ្ធិទទួល Leantime ផ្ញើសំណើ HTTP POST មួយដែលមានតួ JSON ទៅកាន់ URL នេះ។ សំណើត្រូវបានផ្ញើនៅផ្ទៃខាងក្រោយ ហើយអាចចំណាយពេលច្រើននាទីដើម្បីមកដល់។ សំណើដែលបរាជ័យនឹងមិនត្រូវបានព្យាយាមម្តងទៀតទេ។ ការកំណត់ទទួលការជូនដំណឹង ប្រភេទ និងការកំណត់តាមគម្រោងខាងលើរបស់អ្នក សម្រេចថាការជូនដំណឹងណាខ្លះមានសិទ្ធិ។ Webhook ផ្ញើតែនៅពេលវាត្រូវបានបើក ហើយមាន URL កំណត់ប៉ុណ្ណោះ។ URL របស់អ្នកនៅតែរក្សាទុកពេល webhook ត្រូវបានបិទ — សម្អាត URL ដើម្បីដកវាចេញ។" +label.webhook_notifications_description="ផ្ញើការជូនដំណឹងរបស់អ្នកទៅកាន់ HTTPS endpoint ផ្ទាល់ខ្លួនរបស់អ្នក។ សម្រាប់រាល់ការជូនដំណឹងគម្រោងដែលអ្នកមានសិទ្ធិទទួល Leantime ផ្ញើសំណើ HTTP POST មួយដែលមានតួ JSON ទៅកាន់ URL នេះ។ សំណើត្រូវបានផ្ញើនៅផ្ទៃខាងក្រោយ ហើយអាចចំណាយពេលច្រើននាទីដើម្បីមកដល់។ សំណើដែលបរាជ័យនឹងមិនត្រូវបានព្យាយាមម្តងទៀតទេ។ ការកំណត់របស់អ្នកនៅក្នុងផ្ទាំង ការជូនដំណឹងច្រើន (ការទទួលការជូនដំណឹង ប្រភេទ និងកម្រិតតាមគម្រោង) សម្រេចថាការជូនដំណឹងណាខ្លះមានសិទ្ធិ។ Webhook ផ្ញើតែនៅពេលវាត្រូវបានបើក ហើយមាន URL កំណត់ប៉ុណ្ណោះ។ URL របស់អ្នកនៅតែរក្សាទុកពេល webhook ត្រូវបានបិទ — សម្អាត URL ដើម្បីដកវាចេញ។" label.enable_webhook_notifications="បើកការជូនដំណឹងតាម webhook" label.webhook_url_hint="HTTPS តែប៉ុណ្ណោះ។ ទុកទទេដើម្បីដក webhook ចេញ។" label.default_notification_types="ប្រភេទការជូនដំណឹងលំនាំដើមសម្រាប់អ្នកប្រើថ្មី" @@ -4044,3 +4044,24 @@ modules.files.title="ឯកសារច្រើន" modules.files.description="បណ្ណាល័យឯកសាររបស់គម្រោង។ ឯកសារភ្ជាប់លើការងារត្រូវធ្វើ និងរូបភាពក្នុងអត្ថបទនៅតែអាចប្រើបាន។" modules.reports.title="របាយការណ៍" modules.reports.description="របាយការណ៍គម្រោង និងទិដ្ឋភាពទូទៅនៃស្ថានភាព។" + +tabs.background = " ផ្ទៃក្រោយ" +menu.background = " ផ្ទៃក្រោយ" +text.set_your_background = "ជ្រើសរើសផ្ទៃក្រោយនៃទំព័ររបស់អ្នក៖ ពណ៌ជម្រាលនៃគ្រោងពណ៌របស់អ្នក ឬរូបភាព។" +label.background_type = "ផ្ទៃក្រោយបច្ចុប្បន្ន" +label.gradient = "ពណ៌ជម្រាល" +label.image = "រូបភាព" +text.pick_background_image_below = "ជ្រើសរើសរូបភាពខាងក្រោមដើម្បីប្រើជាផ្ទៃក្រោយ។" +label.background_image = "ជ្រើសរើសរូបភាព" +label.background_source = "ប្រភពរូបភាព" +label.unsplash = "Unsplash" +input.search_unsplash = "ស្វែងរកក្នុង Unsplash" +label.search_for_image = "ស្វែងរករូបភាព" +buttons.refresh_images = "បង្ហាញរូបភាពផ្សេងទៀត" +buttons.use_image = "ប្រើជាផ្ទៃក្រោយ" +text.photo_by_on_unsplash = "រូបថតដោយ %s នៅលើ %s" +text.no_images_found = "រកមិនឃើញរូបភាព" +notifications.background_settings_saved = "បានរក្សាទុកផ្ទៃក្រោយ" +notifications.background_not_saved = "មិនអាចរក្សាទុកផ្ទៃក្រោយបានទេ" +notifications.background_upload_failed = "មិនអាចរក្សាទុកផ្ទៃក្រោយបានទេ។ សូមអាប់ឡូតរូបភាព JPG, PNG, GIF ឬ WebP។" +tabs.webhooks = " Webhook" diff --git a/app/Language/ko-KR.ini b/app/Language/ko-KR.ini index 03383aef07..bc320a3f5c 100644 --- a/app/Language/ko-KR.ini +++ b/app/Language/ko-KR.ini @@ -2740,7 +2740,7 @@ label.telegram_topic_id = "토픽 ID" label.open_in_leantime = "Leantime에서 열기" label.assigned_to = "담당자" label.webhook_notifications = "개인 웹훅" -label.webhook_notifications_description = "알림을 내 HTTPS 엔드포인트로 보내요. 내가 받을 수 있는 모든 프로젝트 알림마다 Leantime이 이 URL로 JSON 본문이 담긴 HTTP POST 요청을 하나씩 보내요. 요청은 백그라운드에서 전송되며 도착하기까지 몇 분이 걸릴 수 있고, 실패한 요청은 다시 시도하지 않아요. 받을 수 있는 알림은 위의 알림 수신, 카테고리, 프로젝트별 설정에 따라 결정돼요. 웹훅은 활성화되어 있고 URL이 설정된 경우에만 전송돼요. 웹훅을 비활성화해도 URL은 저장된 상태로 유지되니, 삭제하려면 URL을 지우세요." +label.webhook_notifications_description ="알림을 내 HTTPS 엔드포인트로 보내요. 내가 받을 수 있는 모든 프로젝트 알림마다 Leantime이 이 URL로 JSON 본문이 담긴 HTTP POST 요청을 하나씩 보내요. 요청은 백그라운드에서 전송되며 도착하기까지 몇 분이 걸릴 수 있고, 실패한 요청은 다시 시도하지 않아요. 받을 수 있는 알림은 알림 탭의 설정(알림 수신, 카테고리, 프로젝트별 수준)에 따라 결정돼요. 웹훅은 활성화되어 있고 URL이 설정된 경우에만 전송돼요. 웹훅을 비활성화해도 URL은 저장된 상태로 유지되니, 삭제하려면 URL을 지우세요." label.enable_webhook_notifications = "웹훅 알림 사용" label.webhook_url_hint = "HTTPS만 지원해요. 웹훅을 삭제하려면 비워 두세요." label.kanban_view_menu = "카드" @@ -4109,3 +4109,24 @@ modules.files.title = "파일" modules.files.description = "프로젝트의 파일 라이브러리. 할 일의 첨부 파일과 본문 속 이미지는 계속 사용할 수 있어요." modules.reports.title = "보고서" modules.reports.description = "프로젝트 보고서와 상태 개요." + +tabs.background = " 배경" +menu.background = " 배경" +text.set_your_background = "페이지 배경을 선택하세요. 색 구성표의 색상 그라디언트나 이미지를 쓸 수 있어요." +label.background_type = "현재 배경" +label.gradient = "색상 그라디언트" +label.image = "이미지" +text.pick_background_image_below = "배경으로 쓸 이미지를 아래에서 선택하세요." +label.background_image = "이미지 선택" +label.background_source = "이미지 출처" +label.unsplash = "Unsplash" +input.search_unsplash = "Unsplash 검색" +label.search_for_image = "이미지 검색" +buttons.refresh_images = "다른 이미지 보기" +buttons.use_image = "배경으로 사용" +text.photo_by_on_unsplash = "사진: %s / %s" +text.no_images_found = "이미지를 찾을 수 없어요" +notifications.background_settings_saved = "배경을 저장했어요" +notifications.background_not_saved = "배경을 저장할 수 없어요" +notifications.background_upload_failed = "배경을 저장할 수 없어요. JPG, PNG, GIF 또는 WebP 이미지를 업로드하세요." +tabs.webhooks = " 웹훅" diff --git a/app/Language/ks-IN.ini b/app/Language/ks-IN.ini index 3b60930fa8..42e0edd6a8 100644 --- a/app/Language/ks-IN.ini +++ b/app/Language/ks-IN.ini @@ -2623,7 +2623,7 @@ label.notifications_my_work_description="صِرف تۄہہِ اسائن کٔرم label.notifications_muted="خاموش" label.notifications_muted_description="سیدِ سیود @مینشن ورٲے کانٛہہ نوٹیفکیشن نہٕ" label.webhook_notifications="ذٲتی ویب ہُک" -label.webhook_notifications_description="پننۍ نوٹیفکیشن سوزِو پننِس HTTPS اینڈ پوائنٹس پؠٹھ۔ پرٛتھ تتھ پروجیکٹ نوٹیفکیشنہٕ خٲطرٕ یَمہٕ کِس تۄہہِ حقدار چھِو، چھُ Leantime یتھ URL پؠٹھ JSON باڈی سٟتۍ اکھ HTTP POST ریکویسٹ سوزان۔ ریکویسٹ چھِ پس منظرس منٛز سوزنہٕ یِوان تہٕ واتنس ہیکن واریاہ منٹ لگِتھ؛ ناکام ریکویسٹ چھُ نہٕ دوبارہ سوزنہٕ یِوان۔ ہیٚرِ کنہِ تُہٕنٛزہٕ نوٹیفکیشن حاصل کرنٕچ، کیٹیگری تہٕ پروجیکٹ وار سیٹنگز چھےٚ فیصلہ کران زِ کَم نوٹیفکیشن چھِ اہل۔ ویب ہُک چھُ صِرف تیٚلہِ سوزان ییٚلہِ یہِ فعال آسہِ تہٕ URL سیٹ آسہِ۔ ویب ہُک غیر فعال آسنس دوران چھُ تُہُنٛد URL محفوظ روزان — اَتھ ہٹاونہٕ خٲطرٕ کٔرِو URL صاف۔" +label.webhook_notifications_description="پننۍ نوٹیفکیشن سوزِو پننِس HTTPS اینڈ پوائنٹس پؠٹھ۔ پرٛتھ تتھ پروجیکٹ نوٹیفکیشنہٕ خٲطرٕ یَمہٕ کِس تۄہہِ حقدار چھِو، چھُ Leantime یتھ URL پؠٹھ JSON باڈی سٟتۍ اکھ HTTP POST ریکویسٹ سوزان۔ ریکویسٹ چھِ پس منظرس منٛز سوزنہٕ یِوان تہٕ واتنس ہیکن واریاہ منٹ لگِتھ؛ ناکام ریکویسٹ چھُ نہٕ دوبارہ سوزنہٕ یِوان۔ نوٹیفکیشن ٹیبَس منٛز تُہٕنٛزہٕ سیٹنگز (نوٹیفکیشن حاصل کرنٕچ، کیٹیگری تہٕ پروجیکٹ وار سطحہٕ) چھِ فیصلہ کران زِ کَم نوٹیفکیشن چھِ اہل۔ ویب ہُک چھُ صِرف تیٚلہِ سوزان ییٚلہِ یہِ فعال آسہِ تہٕ URL سیٹ آسہِ۔ ویب ہُک غیر فعال آسنس دوران چھُ تُہُنٛد URL محفوظ روزان — اَتھ ہٹاونہٕ خٲطرٕ کٔرِو URL صاف۔" label.enable_webhook_notifications="ویب ہُک نوٹیفکیشن فعال کٔرِو" label.webhook_url_hint="صِرف HTTPS۔ ویب ہُک ہٹاونہٕ خٲطرٕ ترٲوِو خٲلی۔" label.default_notification_types="نوٚو یوزرن خٲطرٕ ڈیفالٹ نوٹیفکیشن قسم" @@ -4044,3 +4044,24 @@ modules.files.title="فائل" modules.files.description="پروجیکٹ ہٕنٛز فائل لائبریری۔ ٹاسکن سٟتۍ منسلک فائل تہٕ ٹیکسٹ منٛز تصویر چھِ دستیاب روزان۔" modules.reports.title="رپورٹ" modules.reports.description="پروجیکٹ رپورٹ تہٕ سٹیٹس جائزہ۔" + +tabs.background = " پس منظر" +menu.background = " پس منظر" +text.set_your_background = "پننہٕ صفحن ہندِ پس منظر چُنو: پننہٕ رنگ سکیمک رنگ گریڈیینٹ یا تصویر۔" +label.background_type = "موجودہ پس منظر" +label.gradient = "رنگ گریڈیینٹ" +label.image = "تصویر" +text.pick_background_image_below = "پس منظر ہیتۍ استعمال کرنہٕ خاطرٕ تل کانہہ تصویر چُنو۔" +label.background_image = "تصویر چُنو" +label.background_source = "تصویرٕ ہند ماخذ" +label.unsplash = "Unsplash" +input.search_unsplash = "Unsplash منز لٔجو" +label.search_for_image = "تصویر لٔجو" +buttons.refresh_images = "ہور تصویر ہیٹھ" +buttons.use_image = "پس منظر ہیتۍ استعمال کٔرِو" +text.photo_by_on_unsplash = "فوٹو: %s، %s" +text.no_images_found = "کانہہ تصویر ناہ لبنہٕ آمت" +notifications.background_settings_saved = "پس منظر گوٚو محفوظ" +notifications.background_not_saved = "پس منظر ہیوٚک نہٕ محفوظ سپدِتھ" +notifications.background_upload_failed = "پس منظر ہیوٚک نہٕ محفوظ سپدِتھ۔ JPG، PNG، GIF یا WebP تصویر اپلوڈ کٔرِو۔" +tabs.webhooks = " ویب ہُک" diff --git a/app/Language/lv-LV.ini b/app/Language/lv-LV.ini index e96bad81e6..3908d2b1a8 100644 --- a/app/Language/lv-LV.ini +++ b/app/Language/lv-LV.ini @@ -2623,7 +2623,7 @@ label.notifications_my_work_description="Tikai jums piešķirtie uzdevumi, jūsu label.notifications_muted="Apklusināts" label.notifications_muted_description="Nav paziņojumu, izņemot tiešos @pieminējumus" label.webhook_notifications="Personīgā tīmekļa aizķere" -label.webhook_notifications_description="Sūtiet savus paziņojumus uz savu HTTPS galapunktu. Par katru projekta paziņojumu, ko varat saņemt, Leantime nosūta uz šo URL vienu HTTP POST pieprasījumu ar JSON saturu. Pieprasījumi tiek sūtīti fonā, un to piegāde var aizņemt vairākas minūtes; neveiksmīgs pieprasījums netiek atkārtots. Jūsu iepriekš norādītie paziņojumu saņemšanas, kategoriju un projektu iestatījumi nosaka, kuri paziņojumi tiek sūtīti. Tīmekļa aizķere sūta paziņojumus tikai tad, ja tā ir iespējota un ir iestatīts URL. Kamēr tīmekļa aizķere ir atspējota, jūsu URL paliek saglabāts — lai to noņemtu, notīriet URL." +label.webhook_notifications_description="Sūtiet savus paziņojumus uz savu HTTPS galapunktu. Par katru projekta paziņojumu, ko varat saņemt, Leantime nosūta uz šo URL vienu HTTP POST pieprasījumu ar JSON saturu. Pieprasījumi tiek sūtīti fonā, un to piegāde var aizņemt vairākas minūtes; neveiksmīgs pieprasījums netiek atkārtots. Jūsu iestatījumi cilnē Paziņojumi (paziņojumu saņemšana, kategorijas un līmeņi katram projektam) nosaka, kuri paziņojumi tiek sūtīti. Tīmekļa aizķere sūta paziņojumus tikai tad, ja tā ir iespējota un ir iestatīts URL. Kamēr tīmekļa aizķere ir atspējota, jūsu URL paliek saglabāts — lai to noņemtu, notīriet URL." label.enable_webhook_notifications="Iespējot tīmekļa aizķeres paziņojumus" label.webhook_url_hint="Tikai HTTPS. Atstājiet tukšu, lai noņemtu tīmekļa aizķeri." label.default_notification_types="Noklusējuma paziņojumu veidi jauniem lietotājiem" @@ -4044,3 +4044,24 @@ modules.files.title="Faili" modules.files.description="Projekta failu bibliotēka. Uzdevumu pielikumi un attēli tekstā joprojām ir pieejami." modules.reports.title="Pārskati" modules.reports.description="Projekta pārskati un statusa kopsavilkumi." + +tabs.background = " Fons" +menu.background = " Fons" +text.set_your_background = "Izvēlieties savu lapu fonu: savas krāsu shēmas krāsu pāreju vai attēlu." +label.background_type = "Pašreizējais fons" +label.gradient = "Krāsu pāreja" +label.image = "Attēls" +text.pick_background_image_below = "Zemāk izvēlieties attēlu, ko izmantot kā fonu." +label.background_image = "Izvēlēties attēlu" +label.background_source = "Attēla avots" +label.unsplash = "Unsplash" +input.search_unsplash = "Meklēt Unsplash" +label.search_for_image = "Meklēt attēlu" +buttons.refresh_images = "Rādīt citus attēlus" +buttons.use_image = "Izmantot kā fonu" +text.photo_by_on_unsplash = "Foto: %s vietnē %s" +text.no_images_found = "Attēli nav atrasti" +notifications.background_settings_saved = "Fons saglabāts" +notifications.background_not_saved = "Neizdevās saglabāt fonu" +notifications.background_upload_failed = "Neizdevās saglabāt fonu. Augšupielādējiet JPG, PNG, GIF vai WebP attēlu." +tabs.webhooks = " Tīmekļa aizķeres" diff --git a/app/Language/nl-NL.ini b/app/Language/nl-NL.ini index 78a7879072..9dc4e1a1da 100644 --- a/app/Language/nl-NL.ini +++ b/app/Language/nl-NL.ini @@ -2623,7 +2623,7 @@ label.notifications_my_work_description="Alleen taken die aan jou zijn toegeweze label.notifications_muted="Gedempt" label.notifications_muted_description="Geen meldingen behalve directe @vermeldingen" label.webhook_notifications="Persoonlijke webhook" -label.webhook_notifications_description="Stuur je meldingen naar je eigen HTTPS-endpoint. Voor elke projectmelding die voor jou in aanmerking komt, stuurt Leantime één HTTP POST-verzoek met een JSON-body naar deze URL. Verzoeken worden op de achtergrond verzonden en kunnen enkele minuten onderweg zijn; een mislukt verzoek wordt niet opnieuw geprobeerd. Je instellingen hierboven voor het ontvangen van meldingen, per categorie en per project bepalen welke meldingen in aanmerking komen. De webhook verstuurt alleen meldingen als hij is ingeschakeld en er een URL is ingesteld. Je URL blijft bewaard zolang de webhook is uitgeschakeld — maak de URL leeg om hem te verwijderen." +label.webhook_notifications_description="Stuur je meldingen naar je eigen HTTPS-endpoint. Voor elke projectmelding die voor jou in aanmerking komt, stuurt Leantime één HTTP POST-verzoek met een JSON-body naar deze URL. Verzoeken worden op de achtergrond verzonden en kunnen enkele minuten onderweg zijn; een mislukt verzoek wordt niet opnieuw geprobeerd. Je instellingen op het tabblad Notificaties (ontvangen van meldingen, categorieën en niveaus per project) bepalen welke meldingen in aanmerking komen. De webhook verstuurt alleen meldingen als hij is ingeschakeld en er een URL is ingesteld. Je URL blijft bewaard zolang de webhook is uitgeschakeld — maak de URL leeg om hem te verwijderen." label.enable_webhook_notifications="Webhookmeldingen inschakelen" label.webhook_url_hint="Alleen HTTPS. Laat leeg om de webhook te verwijderen." label.default_notification_types="Standaard meldingstypen voor nieuwe gebruikers" @@ -4044,3 +4044,24 @@ modules.files.title="Bestanden" modules.files.description="De bestandsbibliotheek van het project. Bijlagen bij taken en afbeeldingen in tekst blijven beschikbaar." modules.reports.title="Rapporten" modules.reports.description="Projectrapporten en statusoverzichten." + +tabs.background = " Achtergrond" +menu.background = " Achtergrond" +text.set_your_background = "Kies de achtergrond van je pagina's: het kleurverloop van je kleurenschema of een afbeelding." +label.background_type = "Huidige achtergrond" +label.gradient = "Kleurverloop" +label.image = "Afbeelding" +text.pick_background_image_below = "Kies hieronder een afbeelding om als achtergrond te gebruiken." +label.background_image = "Kies een afbeelding" +label.background_source = "Afbeeldingsbron" +label.unsplash = "Unsplash" +input.search_unsplash = "Zoeken op Unsplash" +label.search_for_image = "Zoek een afbeelding" +buttons.refresh_images = "Andere afbeeldingen tonen" +buttons.use_image = "Gebruiken als achtergrond" +text.photo_by_on_unsplash = "Foto van %s op %s" +text.no_images_found = "Geen afbeeldingen gevonden" +notifications.background_settings_saved = "Achtergrond opgeslagen" +notifications.background_not_saved = "De achtergrond kon niet worden opgeslagen" +notifications.background_upload_failed = "De achtergrond kon niet worden opgeslagen. Upload een JPG-, PNG-, GIF- of WebP-afbeelding." +tabs.webhooks = " Webhooks" diff --git a/app/Language/no-NO.ini b/app/Language/no-NO.ini index 602ea9e798..ce0a9b498c 100644 --- a/app/Language/no-NO.ini +++ b/app/Language/no-NO.ini @@ -2623,7 +2623,7 @@ label.notifications_my_work_description="Bare oppgaver tildelt deg, dine element label.notifications_muted="Dempet" label.notifications_muted_description="Ingen varsler unntatt direkte @omtaler" label.webhook_notifications="Personlig webhook" -label.webhook_notifications_description="Send varslene dine til ditt eget HTTPS-endepunkt. For hvert prosjektvarsel du er kvalifisert for, sender Leantime én HTTP POST-forespørsel med en JSON-body til denne URL-en. Forespørsler sendes i bakgrunnen og kan bruke flere minutter på å komme frem; en forespørsel som mislykkes, blir ikke sendt på nytt. Innstillingene for mottak av varsler, kategorier og per prosjekt ovenfor bestemmer hvilke varsler som er kvalifisert. Webhooken sender bare mens den er aktivert og en URL er angitt. URL-en forblir lagret mens webhooken er deaktivert – tøm URL-en for å fjerne den." +label.webhook_notifications_description="Send varslene dine til ditt eget HTTPS-endepunkt. For hvert prosjektvarsel du er kvalifisert for, sender Leantime én HTTP POST-forespørsel med en JSON-body til denne URL-en. Forespørsler sendes i bakgrunnen og kan bruke flere minutter på å komme frem; en forespørsel som mislykkes, blir ikke sendt på nytt. Innstillingene dine i fanen Varsler (mottak av varsler, kategorier og nivåer per prosjekt) bestemmer hvilke varsler som er kvalifisert. Webhooken sender bare mens den er aktivert og en URL er angitt. URL-en forblir lagret mens webhooken er deaktivert – tøm URL-en for å fjerne den." label.enable_webhook_notifications="Aktiver webhook-varsler" label.webhook_url_hint="Bare HTTPS. La stå tomt for å fjerne webhooken." label.default_notification_types="Standard varseltyper for nye brukere" @@ -4044,3 +4044,24 @@ modules.files.title="Filer" modules.files.description="Prosjektets filbibliotek. Vedlegg på oppgaver og bilder i tekst forblir tilgjengelige." modules.reports.title="Rapporter" modules.reports.description="Prosjektrapporter og statusoversikter." + +tabs.background = " Bakgrunn" +menu.background = " Bakgrunn" +text.set_your_background = "Velg bakgrunn for sidene dine: fargegradienten i fargeoppsettet ditt eller et bilde." +label.background_type = "Gjeldende bakgrunn" +label.gradient = "Fargegradient" +label.image = "Bilde" +text.pick_background_image_below = "Velg et bilde nedenfor for å bruke det som bakgrunn." +label.background_image = "Velg et bilde" +label.background_source = "Bildekilde" +label.unsplash = "Unsplash" +input.search_unsplash = "Søk på Unsplash" +label.search_for_image = "Søk etter et bilde" +buttons.refresh_images = "Vis andre bilder" +buttons.use_image = "Bruk som bakgrunn" +text.photo_by_on_unsplash = "Foto av %s på %s" +text.no_images_found = "Fant ingen bilder" +notifications.background_settings_saved = "Bakgrunn lagret" +notifications.background_not_saved = "Bakgrunnen kunne ikke lagres" +notifications.background_upload_failed = "Bakgrunnen kunne ikke lagres. Last opp et JPG-, PNG-, GIF- eller WebP-bilde." +tabs.webhooks = " Webhooks" diff --git a/app/Language/pl-PL.ini b/app/Language/pl-PL.ini index f468a0e26c..77dc8996a9 100644 --- a/app/Language/pl-PL.ini +++ b/app/Language/pl-PL.ini @@ -2808,7 +2808,7 @@ label.notifications_my_work_description = "Tylko zadania przypisane do Ciebie, T label.notifications_muted = "Wyciszone" label.notifications_muted_description = "Brak powiadomień poza bezpośrednimi @wzmiankami" label.webhook_notifications = "Osobisty webhook" -label.webhook_notifications_description = "Wysyłaj powiadomienia do własnego punktu końcowego HTTPS. Dla każdego powiadomienia z projektu, które Ci przysługuje, Leantime wysyła na ten adres URL jedno żądanie HTTP POST z treścią w formacie JSON. Żądania są wysyłane w tle i mogą dotrzeć z kilkuminutowym opóźnieniem; nieudane żądanie nie jest ponawiane. O tym, które powiadomienia są wysyłane, decydują powyższe ustawienia odbierania powiadomień, kategorii i projektów. Webhook działa tylko wtedy, gdy jest włączony i ustawiono adres URL. Adres URL pozostaje zapisany, gdy webhook jest wyłączony — wyczyść go, aby go usunąć." +label.webhook_notifications_description ="Wysyłaj powiadomienia do własnego punktu końcowego HTTPS. Dla każdego powiadomienia z projektu, które Ci przysługuje, Leantime wysyła na ten adres URL jedno żądanie HTTP POST z treścią w formacie JSON. Żądania są wysyłane w tle i mogą dotrzeć z kilkuminutowym opóźnieniem; nieudane żądanie nie jest ponawiane. O tym, które powiadomienia są wysyłane, decydują Twoje ustawienia na karcie Powiadomienia (odbieranie powiadomień, kategorie i poziomy dla poszczególnych projektów). Webhook działa tylko wtedy, gdy jest włączony i ustawiono adres URL. Adres URL pozostaje zapisany, gdy webhook jest wyłączony — wyczyść go, aby go usunąć." label.enable_webhook_notifications = "Włącz powiadomienia przez webhook" label.webhook_url_hint = "Tylko HTTPS. Zostaw puste, aby usunąć webhook." label.default_notification_types = "Domyślne typy powiadomień dla nowych użytkowników" @@ -4224,3 +4224,24 @@ modules.files.title = "Pliki" modules.files.description = "Biblioteka plików projektu. Załączniki do zadań i obrazy w tekście pozostają dostępne." modules.reports.title = "Raporty" modules.reports.description = "Raporty projektu i przeglądy statusu." + +tabs.background = " Tło" +menu.background = " Tło" +text.set_your_background = "Wybierz tło swoich stron: gradient kolorów Twojego schematu kolorów lub obraz." +label.background_type = "Bieżące tło" +label.gradient = "Gradient kolorów" +label.image = "Obraz" +text.pick_background_image_below = "Wybierz poniżej obraz, aby użyć go jako tła." +label.background_image = "Wybierz obraz" +label.background_source = "Źródło obrazu" +label.unsplash = "Unsplash" +input.search_unsplash = "Szukaj w Unsplash" +label.search_for_image = "Szukaj obrazu" +buttons.refresh_images = "Pokaż inne obrazy" +buttons.use_image = "Użyj jako tła" +text.photo_by_on_unsplash = "Zdjęcie: %s na %s" +text.no_images_found = "Nie znaleziono obrazów" +notifications.background_settings_saved = "Tło zapisane" +notifications.background_not_saved = "Nie udało się zapisać tła" +notifications.background_upload_failed = "Nie udało się zapisać tła. Prześlij obraz JPG, PNG, GIF lub WebP." +tabs.webhooks = " Webhooki" diff --git a/app/Language/pt-BR.ini b/app/Language/pt-BR.ini index bdaf179e30..0e46cf87ca 100644 --- a/app/Language/pt-BR.ini +++ b/app/Language/pt-BR.ini @@ -2678,7 +2678,7 @@ label.telegram_topic_id="ID do tópico" label.open_in_leantime="Abrir no Leantime" label.assigned_to="Atribuído a" label.webhook_notifications="Webhook pessoal" -label.webhook_notifications_description="Envie suas notificações para o seu próprio endpoint HTTPS. Para cada notificação de projeto que você pode receber, o Leantime envia uma requisição HTTP POST com um corpo JSON para esta URL. As requisições são enviadas em segundo plano e podem levar vários minutos para chegar; uma requisição que falhar não é repetida. Suas configurações acima de recebimento de notificações, categorias e por projeto determinam quais notificações são enviadas. O webhook só envia enquanto estiver ativado e houver uma URL definida. Sua URL permanece salva enquanto o webhook estiver desativado — limpe a URL para removê-la." +label.webhook_notifications_description="Envie suas notificações para o seu próprio endpoint HTTPS. Para cada notificação de projeto que você pode receber, o Leantime envia uma requisição HTTP POST com um corpo JSON para esta URL. As requisições são enviadas em segundo plano e podem levar vários minutos para chegar; uma requisição que falhar não é repetida. Suas configurações na aba Notificações (recebimento de notificações, categorias e níveis por projeto) determinam quais notificações são enviadas. O webhook só envia enquanto estiver ativado e houver uma URL definida. Sua URL permanece salva enquanto o webhook estiver desativado — limpe a URL para removê-la." label.enable_webhook_notifications="Ativar notificações por webhook" label.webhook_url_hint="Somente HTTPS. Deixe em branco para remover o webhook." label.kanban_view_menu="Cartões" @@ -4044,3 +4044,24 @@ modules.files.title="Arquivos" modules.files.description="A biblioteca de arquivos do projeto. Anexos em tarefas e imagens no texto continuam disponíveis." modules.reports.title="Relatórios" modules.reports.description="Relatórios do projeto e visões gerais de status." + +tabs.background = " Plano de fundo" +menu.background = " Plano de fundo" +text.set_your_background = "Escolha o plano de fundo das suas páginas: o degradê de cores do seu esquema de cores ou uma imagem." +label.background_type = "Plano de fundo atual" +label.gradient = "Degradê de cores" +label.image = "Imagem" +text.pick_background_image_below = "Escolha uma imagem abaixo para usá-la como plano de fundo." +label.background_image = "Escolha uma imagem" +label.background_source = "Origem da imagem" +label.unsplash = "Unsplash" +input.search_unsplash = "Pesquisar no Unsplash" +label.search_for_image = "Pesquisar uma imagem" +buttons.refresh_images = "Mostrar outras imagens" +buttons.use_image = "Usar como plano de fundo" +text.photo_by_on_unsplash = "Foto de %s no %s" +text.no_images_found = "Nenhuma imagem encontrada" +notifications.background_settings_saved = "Plano de fundo salvo" +notifications.background_not_saved = "Não foi possível salvar o plano de fundo" +notifications.background_upload_failed = "Não foi possível salvar o plano de fundo. Envie uma imagem JPG, PNG, GIF ou WebP." +tabs.webhooks = " Webhooks" diff --git a/app/Language/pt-PT.ini b/app/Language/pt-PT.ini index 0aa2af7e83..3901598949 100644 --- a/app/Language/pt-PT.ini +++ b/app/Language/pt-PT.ini @@ -2623,7 +2623,7 @@ label.notifications_my_work_description="Apenas tarefas atribuídas a si, os seu label.notifications_muted="Silenciado" label.notifications_muted_description="Sem notificações, exceto @menções diretas" label.webhook_notifications="Webhook pessoal" -label.webhook_notifications_description="Envie as suas notificações para o seu próprio endpoint HTTPS. Para cada notificação de projeto que lhe seja destinada, o Leantime envia um pedido HTTP POST com um corpo JSON para este URL. Os pedidos são enviados em segundo plano e podem demorar vários minutos a chegar; um pedido que falhe não é repetido. As definições de receção de notificações, de categorias e por projeto acima determinam que notificações lhe são destinadas. O webhook só envia enquanto estiver ativado e houver um URL definido. O seu URL fica guardado enquanto o webhook estiver desativado — limpe o URL para o remover." +label.webhook_notifications_description="Envie as suas notificações para o seu próprio endpoint HTTPS. Para cada notificação de projeto que lhe seja destinada, o Leantime envia um pedido HTTP POST com um corpo JSON para este URL. Os pedidos são enviados em segundo plano e podem demorar vários minutos a chegar; um pedido que falhe não é repetido. As suas definições no separador Notificações (receção de notificações, categorias e níveis por projeto) determinam que notificações lhe são destinadas. O webhook só envia enquanto estiver ativado e houver um URL definido. O seu URL fica guardado enquanto o webhook estiver desativado — limpe o URL para o remover." label.enable_webhook_notifications="Ativar notificações por webhook" label.webhook_url_hint="Apenas HTTPS. Deixe vazio para remover o webhook." label.default_notification_types="Tipos de notificação predefinidos para novos utilizadores" @@ -4044,3 +4044,24 @@ modules.files.title="Ficheiros" modules.files.description="A biblioteca de ficheiros do projeto. Os anexos das tarefas e as imagens no texto continuam disponíveis." modules.reports.title="Relatórios" modules.reports.description="Relatórios do projeto e resumos de estado." + +tabs.background = " Fundo" +menu.background = " Fundo" +text.set_your_background = "Escolha o fundo das suas páginas: o gradiente de cores do seu esquema de cores ou uma imagem." +label.background_type = "Fundo atual" +label.gradient = "Gradiente de cores" +label.image = "Imagem" +text.pick_background_image_below = "Escolha uma imagem abaixo para a usar como fundo." +label.background_image = "Escolher uma imagem" +label.background_source = "Origem da imagem" +label.unsplash = "Unsplash" +input.search_unsplash = "Pesquisar no Unsplash" +label.search_for_image = "Pesquisar uma imagem" +buttons.refresh_images = "Mostrar outras imagens" +buttons.use_image = "Usar como fundo" +text.photo_by_on_unsplash = "Fotografia de %s no %s" +text.no_images_found = "Nenhuma imagem encontrada" +notifications.background_settings_saved = "Fundo guardado" +notifications.background_not_saved = "Não foi possível guardar o fundo" +notifications.background_upload_failed = "Não foi possível guardar o fundo. Carregue uma imagem JPG, PNG, GIF ou WebP." +tabs.webhooks = " Webhooks" diff --git a/app/Language/ro-RO.ini b/app/Language/ro-RO.ini index 3f27ff760c..5b17c8d527 100644 --- a/app/Language/ro-RO.ini +++ b/app/Language/ro-RO.ini @@ -2623,7 +2623,7 @@ label.notifications_my_work_description="Doar sarcinile atribuite dvs., elemente label.notifications_muted="Dezactivate" label.notifications_muted_description="Nicio notificare, cu excepția @mențiunilor directe" label.webhook_notifications="Webhook personal" -label.webhook_notifications_description="Trimiteți notificările către propriul endpoint HTTPS. Pentru fiecare notificare de proiect pentru care sunteți eligibil, Leantime trimite o cerere HTTP POST cu un corp JSON către acest URL. Cererile sunt trimise în fundal și pot ajunge după câteva minute; o cerere eșuată nu este reîncercată. Setările de mai sus pentru primirea notificărilor, categorii și proiecte decid ce notificări sunt eligibile. Webhookul trimite doar cât timp este activat și are un URL setat. URL-ul rămâne salvat cât timp webhookul este dezactivat — goliți URL-ul pentru a-l elimina." +label.webhook_notifications_description="Trimiteți notificările către propriul endpoint HTTPS. Pentru fiecare notificare de proiect pentru care sunteți eligibil, Leantime trimite o cerere HTTP POST cu un corp JSON către acest URL. Cererile sunt trimise în fundal și pot ajunge după câteva minute; o cerere eșuată nu este reîncercată. Setările dumneavoastră din fila Notificări (primirea notificărilor, categorii și niveluri pe proiect) decid ce notificări sunt eligibile. Webhookul trimite doar cât timp este activat și are un URL setat. URL-ul rămâne salvat cât timp webhookul este dezactivat — goliți URL-ul pentru a-l elimina." label.enable_webhook_notifications="Activați notificările prin webhook" label.webhook_url_hint="Doar HTTPS. Lăsați gol pentru a elimina webhookul." label.default_notification_types="Tipuri implicite de notificări pentru utilizatorii noi" @@ -4044,3 +4044,24 @@ modules.files.title="Fișiere" modules.files.description="Biblioteca de fișiere a proiectului. Atașamentele sarcinilor și imaginile din text rămân disponibile." modules.reports.title="Rapoarte" modules.reports.description="Rapoarte de proiect și prezentări generale ale stării." + +tabs.background = " Fundal" +menu.background = " Fundal" +text.set_your_background = "Alegeți fundalul paginilor dvs.: gradientul de culori al schemei de culori sau o imagine." +label.background_type = "Fundal curent" +label.gradient = "Gradient de culori" +label.image = "Imagine" +text.pick_background_image_below = "Alegeți mai jos o imagine pentru a o folosi ca fundal." +label.background_image = "Alegeți o imagine" +label.background_source = "Sursa imaginii" +label.unsplash = "Unsplash" +input.search_unsplash = "Căutați pe Unsplash" +label.search_for_image = "Căutați o imagine" +buttons.refresh_images = "Afișați alte imagini" +buttons.use_image = "Folosiți ca fundal" +text.photo_by_on_unsplash = "Fotografie de %s pe %s" +text.no_images_found = "Nu s-au găsit imagini" +notifications.background_settings_saved = "Fundal salvat" +notifications.background_not_saved = "Fundalul nu a putut fi salvat" +notifications.background_upload_failed = "Fundalul nu a putut fi salvat. Încărcați o imagine JPG, PNG, GIF sau WebP." +tabs.webhooks = " Webhook-uri" diff --git a/app/Language/ru-RU.ini b/app/Language/ru-RU.ini index ba7f6b0308..8f975a8201 100644 --- a/app/Language/ru-RU.ini +++ b/app/Language/ru-RU.ini @@ -2623,7 +2623,7 @@ label.notifications_my_work_description="Только назначенные в label.notifications_muted="Без уведомлений" label.notifications_muted_description="Никаких уведомлений, кроме прямых @упоминаний" label.webhook_notifications="Персональный вебхук" -label.webhook_notifications_description="Отправляйте свои уведомления на собственный HTTPS-адрес. Для каждого уведомления проекта, которое вам положено, Leantime отправляет на этот URL один HTTP POST-запрос с телом в формате JSON. Запросы отправляются в фоновом режиме и могут доходить через несколько минут; неудачный запрос повторно не отправляется. Какие уведомления вам положены, определяют указанные выше настройки получения уведомлений, категорий и проектов. Вебхук работает, только если он включен и указан URL. Пока вебхук отключен, ваш URL остается сохраненным — очистите поле, чтобы удалить его." +label.webhook_notifications_description="Отправляйте свои уведомления на собственный HTTPS-адрес. Для каждого уведомления проекта, которое вам положено, Leantime отправляет на этот URL один HTTP POST-запрос с телом в формате JSON. Запросы отправляются в фоновом режиме и могут доходить через несколько минут; неудачный запрос повторно не отправляется. Какие уведомления вам положены, определяют ваши настройки на вкладке «Уведомления» (получение уведомлений, категории и уровни по проектам). Вебхук работает, только если он включен и указан URL. Пока вебхук отключен, ваш URL остается сохраненным — очистите поле, чтобы удалить его." label.enable_webhook_notifications="Включить уведомления через вебхук" label.webhook_url_hint="Только HTTPS. Оставьте пустым, чтобы удалить вебхук." label.default_notification_types="Типы уведомлений по умолчанию для новых пользователей" @@ -4048,3 +4048,24 @@ modules.files.title="Файлы" modules.files.description="Файловая библиотека проекта. Вложения в задачах и изображения в тексте остаются доступными." modules.reports.title="Отчеты" modules.reports.description="Отчеты по проекту и обзоры статуса." + +tabs.background = " Фон" +menu.background = " Фон" +text.set_your_background = "Выберите фон страниц: цветовой градиент вашей цветовой схемы или изображение." +label.background_type = "Текущий фон" +label.gradient = "Цветовой градиент" +label.image = "Изображение" +text.pick_background_image_below = "Выберите изображение ниже, чтобы использовать его как фон." +label.background_image = "Выберите изображение" +label.background_source = "Источник изображений" +label.unsplash = "Unsplash" +input.search_unsplash = "Поиск в Unsplash" +label.search_for_image = "Поиск изображения" +buttons.refresh_images = "Показать другие изображения" +buttons.use_image = "Использовать как фон" +text.photo_by_on_unsplash = "Фото: %s, %s" +text.no_images_found = "Изображения не найдены" +notifications.background_settings_saved = "Фон сохранён" +notifications.background_not_saved = "Не удалось сохранить фон" +notifications.background_upload_failed = "Не удалось сохранить фон. Загрузите изображение JPG, PNG, GIF или WebP." +tabs.webhooks = " Вебхуки" diff --git a/app/Language/sk-SK.ini b/app/Language/sk-SK.ini index 62e2d23250..1d046b772f 100644 --- a/app/Language/sk-SK.ini +++ b/app/Language/sk-SK.ini @@ -2623,7 +2623,7 @@ label.notifications_my_work_description="Iba úlohy priradené vám, vaše polo label.notifications_muted="Stlmené" label.notifications_muted_description="Žiadne upozornenia okrem priamych @zmienok" label.webhook_notifications="Osobný webhook" -label.webhook_notifications_description="Posielajte svoje upozornenia na vlastný HTTPS endpoint. Pre každé projektové upozornenie, na ktoré máte nárok, odošle Leantime na túto URL adresu jednu požiadavku HTTP POST s telom vo formáte JSON. Požiadavky sa odosielajú na pozadí a ich doručenie môže trvať niekoľko minút; neúspešná požiadavka sa neopakuje. O tom, na ktoré upozornenia máte nárok, rozhodujú vyššie uvedené nastavenia prijímania upozornení, kategórií a jednotlivých projektov. Webhook odosiela, iba keď je zapnutý a je nastavená URL adresa. Kým je webhook vypnutý, vaša URL adresa zostáva uložená — ak ju chcete odstrániť, vymažte ju." +label.webhook_notifications_description="Posielajte svoje upozornenia na vlastný HTTPS endpoint. Pre každé projektové upozornenie, na ktoré máte nárok, odošle Leantime na túto URL adresu jednu požiadavku HTTP POST s telom vo formáte JSON. Požiadavky sa odosielajú na pozadí a ich doručenie môže trvať niekoľko minút; neúspešná požiadavka sa neopakuje. O tom, na ktoré upozornenia máte nárok, rozhodujú vaše nastavenia na karte Oznámenia (prijímanie upozornení, kategórie a úrovne jednotlivých projektov). Webhook odosiela, iba keď je zapnutý a je nastavená URL adresa. Kým je webhook vypnutý, vaša URL adresa zostáva uložená — ak ju chcete odstrániť, vymažte ju." label.enable_webhook_notifications="Zapnúť upozornenia cez webhook" label.webhook_url_hint="Iba HTTPS. Ak chcete webhook odstrániť, ponechajte pole prázdne." label.default_notification_types="Predvolené typy upozornení pre nových používateľov" @@ -4044,3 +4044,24 @@ modules.files.title="Súbory" modules.files.description="Knižnica súborov projektu. Prílohy úloh a obrázky v texte zostanú dostupné." modules.reports.title="Správy" modules.reports.description="Správy o projekte a prehľady stavu." + +tabs.background = " Pozadie" +menu.background = " Pozadie" +text.set_your_background = "Vyberte pozadie svojich stránok: farebný prechod vašej farebnej schémy alebo obrázok." +label.background_type = "Aktuálne pozadie" +label.gradient = "Farebný prechod" +label.image = "Obrázok" +text.pick_background_image_below = "Vyberte nižšie obrázok, ktorý sa použije ako pozadie." +label.background_image = "Vyberte obrázok" +label.background_source = "Zdroj obrázkov" +label.unsplash = "Unsplash" +input.search_unsplash = "Hľadať na Unsplash" +label.search_for_image = "Hľadať obrázok" +buttons.refresh_images = "Zobraziť ďalšie obrázky" +buttons.use_image = "Použiť ako pozadie" +text.photo_by_on_unsplash = "Foto: %s na %s" +text.no_images_found = "Nenašli sa žiadne obrázky" +notifications.background_settings_saved = "Pozadie bolo uložené" +notifications.background_not_saved = "Pozadie sa nepodarilo uložiť" +notifications.background_upload_failed = "Pozadie sa nepodarilo uložiť. Nahrajte obrázok JPG, PNG, GIF alebo WebP." +tabs.webhooks = " Webhooky" diff --git a/app/Language/sl-SI.ini b/app/Language/sl-SI.ini index e9e7d73826..1c212c5a42 100644 --- a/app/Language/sl-SI.ini +++ b/app/Language/sl-SI.ini @@ -2623,7 +2623,7 @@ label.notifications_my_work_description="Samo opravila, dodeljena vam, vaši ele label.notifications_muted="Utišano" label.notifications_muted_description="Brez obvestil, razen neposrednih @omemb" label.webhook_notifications="Osebni webhook" -label.webhook_notifications_description="Pošiljajte svoja obvestila na lastno končno točko HTTPS. Za vsako obvestilo projekta, do katerega ste upravičeni, Leantime na ta URL pošlje eno zahtevo HTTP POST s telesom JSON. Zahteve se pošiljajo v ozadju in lahko prispejo šele po nekaj minutah; neuspela zahteva se ne ponovi. Katera obvestila so upravičena, določajo zgornje nastavitve prejemanja obvestil, kategorij in posameznih projektov. Webhook pošilja samo, ko je omogočen in je URL nastavljen. Vaš URL ostane shranjen, tudi ko je webhook onemogočen — za odstranitev počistite URL." +label.webhook_notifications_description="Pošiljajte svoja obvestila na lastno končno točko HTTPS. Za vsako obvestilo projekta, do katerega ste upravičeni, Leantime na ta URL pošlje eno zahtevo HTTP POST s telesom JSON. Zahteve se pošiljajo v ozadju in lahko prispejo šele po nekaj minutah; neuspela zahteva se ne ponovi. Katera obvestila so upravičena, določajo vaše nastavitve v zavihku Obvestila (prejemanje obvestil, kategorije in ravni posameznih projektov). Webhook pošilja samo, ko je omogočen in je URL nastavljen. Vaš URL ostane shranjen, tudi ko je webhook onemogočen — za odstranitev počistite URL." label.enable_webhook_notifications="Omogoči obvestila prek webhooka" label.webhook_url_hint="Samo HTTPS. Pustite prazno, da odstranite webhook." label.default_notification_types="Privzete vrste obvestil za nove uporabnike" @@ -4044,3 +4044,24 @@ modules.files.title="Datoteke" modules.files.description="Knjižnica datotek projekta. Priloge pri opravilih in slike v besedilu ostanejo na voljo." modules.reports.title="Poročila" modules.reports.description="Poročila o projektu in pregledi stanja." + +tabs.background = " Ozadje" +menu.background = " Ozadje" +text.set_your_background = "Izberite ozadje svojih strani: barvni prelivi vaše barvne sheme ali slika." +label.background_type = "Trenutno ozadje" +label.gradient = "Barvni prelivi" +label.image = "Slika" +text.pick_background_image_below = "Spodaj izberite sliko, ki jo želite uporabiti kot ozadje." +label.background_image = "Izberite sliko" +label.background_source = "Vir slik" +label.unsplash = "Unsplash" +input.search_unsplash = "Išči na Unsplash" +label.search_for_image = "Poišči sliko" +buttons.refresh_images = "Prikaži druge slike" +buttons.use_image = "Uporabi kot ozadje" +text.photo_by_on_unsplash = "Fotografija: %s na %s" +text.no_images_found = "Ni najdenih slik" +notifications.background_settings_saved = "Ozadje je shranjeno" +notifications.background_not_saved = "Ozadja ni bilo mogoče shraniti" +notifications.background_upload_failed = "Ozadja ni bilo mogoče shraniti. Naložite sliko JPG, PNG, GIF ali WebP." +tabs.webhooks = " Webhooki" diff --git a/app/Language/sr-SP.ini b/app/Language/sr-SP.ini index 8f77350927..84ad335a0a 100644 --- a/app/Language/sr-SP.ini +++ b/app/Language/sr-SP.ini @@ -2623,7 +2623,7 @@ label.notifications_my_work_description="Само задаци додељени label.notifications_muted="Утишано" label.notifications_muted_description="Без обавештења, осим директних @помињања" label.webhook_notifications="Лични webhook" -label.webhook_notifications_description="Шаљите своја обавештења на сопствену HTTPS крајњу тачку. За свако обавештење о пројекту које испуњава услове, Leantime шаље један HTTP POST захтев са JSON телом на ову URL адресу. Захтеви се шаљу у позадини и могу стићи и после неколико минута; неуспели захтев се не понавља. Ваша подешавања за примање обавештења, категорије и пројекте изнад одређују која обавештења испуњавају услове. Webhook шаље само док је укључен и док је URL адреса подешена. Ваша URL адреса остаје сачувана док је webhook искључен — испразните URL адресу да бисте је уклонили." +label.webhook_notifications_description="Шаљите своја обавештења на сопствену HTTPS крајњу тачку. За свако обавештење о пројекту које испуњава услове, Leantime шаље један HTTP POST захтев са JSON телом на ову URL адресу. Захтеви се шаљу у позадини и могу стићи и после неколико минута; неуспели захтев се не понавља. Ваша подешавања на картици Обавештења (примање обавештења, категорије и нивои по пројектима) одређују која обавештења испуњавају услове. Webhook шаље само док је укључен и док је URL адреса подешена. Ваша URL адреса остаје сачувана док је webhook искључен — испразните URL адресу да бисте је уклонили." label.enable_webhook_notifications="Укључи webhook обавештења" label.webhook_url_hint="Само HTTPS. Оставите празно да бисте уклонили webhook." label.default_notification_types="Подразумеване врсте обавештења за нове кориснике" @@ -4044,3 +4044,24 @@ modules.files.title="Датотеке" modules.files.description="Библиотека датотека пројекта. Прилози уз задатке и слике у тексту остају доступни." modules.reports.title="Извештаји" modules.reports.description="Извештаји о пројекту и прегледи статуса." + +tabs.background = " Позадина" +menu.background = " Позадина" +text.set_your_background = "Изаберите позадину својих страница: колорни градијент ваше шеме боја или слику." +label.background_type = "Тренутна позадина" +label.gradient = "Колорни градијент" +label.image = "Слика" +text.pick_background_image_below = "Изаберите слику испод да бисте је користили као позадину." +label.background_image = "Изаберите слику" +label.background_source = "Извор слика" +label.unsplash = "Unsplash" +input.search_unsplash = "Претражи Unsplash" +label.search_for_image = "Претражи слику" +buttons.refresh_images = "Прикажи друге слике" +buttons.use_image = "Користи као позадину" +text.photo_by_on_unsplash = "Фотографија: %s на %s" +text.no_images_found = "Нису пронађене слике" +notifications.background_settings_saved = "Позадина је сачувана" +notifications.background_not_saved = "Позадину није могуће сачувати" +notifications.background_upload_failed = "Позадину није могуће сачувати. Отпремите JPG, PNG, GIF или WebP слику." +tabs.webhooks = " Webhooks" diff --git a/app/Language/sv-SE.ini b/app/Language/sv-SE.ini index 8d8e9801ba..9fd19e1197 100644 --- a/app/Language/sv-SE.ini +++ b/app/Language/sv-SE.ini @@ -2623,7 +2623,7 @@ label.notifications_my_work_description="Endast uppgifter som tilldelats dig, di label.notifications_muted="Tystad" label.notifications_muted_description="Inga aviseringar förutom direkta @omnämnanden" label.webhook_notifications="Personlig webhook" -label.webhook_notifications_description="Skicka dina aviseringar till din egen HTTPS-endpoint. För varje projektavisering du har rätt till skickar Leantime en HTTP POST-förfrågan med en JSON-body till den här URL:en. Förfrågningarna skickas i bakgrunden och kan ta flera minuter att komma fram; en förfrågan som misslyckas skickas inte igen. Dina inställningar ovan för att ta emot aviseringar, kategorier och per projekt avgör vilka aviseringar som omfattas. Webhooken skickar endast när den är aktiverad och en URL är angiven. Din URL förblir sparad medan webhooken är inaktiverad — töm URL:en för att ta bort den." +label.webhook_notifications_description="Skicka dina aviseringar till din egen HTTPS-endpoint. För varje projektavisering du har rätt till skickar Leantime en HTTP POST-förfrågan med en JSON-body till den här URL:en. Förfrågningarna skickas i bakgrunden och kan ta flera minuter att komma fram; en förfrågan som misslyckas skickas inte igen. Dina inställningar på fliken Aviseringar (ta emot aviseringar, kategorier och nivåer per projekt) avgör vilka aviseringar som omfattas. Webhooken skickar endast när den är aktiverad och en URL är angiven. Din URL förblir sparad medan webhooken är inaktiverad — töm URL:en för att ta bort den." label.enable_webhook_notifications="Aktivera webhook-aviseringar" label.webhook_url_hint="Endast HTTPS. Lämna tomt för att ta bort webhooken." label.default_notification_types="Standardaviseringstyper för nya användare" @@ -4044,3 +4044,24 @@ modules.files.title="Filer" modules.files.description="Projektets filbibliotek. Bilagor till Att Göra och bilder i text förblir tillgängliga." modules.reports.title="Rapporter" modules.reports.description="Projektrapporter och statusöversikter." + +tabs.background = " Bakgrund" +menu.background = " Bakgrund" +text.set_your_background = "Välj bakgrund för dina sidor: färgtonen i ditt färgschema eller en bild." +label.background_type = "Nuvarande bakgrund" +label.gradient = "Färgövertoning" +label.image = "Bild" +text.pick_background_image_below = "Välj en bild nedan för att använda den som bakgrund." +label.background_image = "Välj en bild" +label.background_source = "Bildkälla" +label.unsplash = "Unsplash" +input.search_unsplash = "Sök på Unsplash" +label.search_for_image = "Sök efter en bild" +buttons.refresh_images = "Visa andra bilder" +buttons.use_image = "Använd som bakgrund" +text.photo_by_on_unsplash = "Foto av %s på %s" +text.no_images_found = "Inga bilder hittades" +notifications.background_settings_saved = "Bakgrunden sparades" +notifications.background_not_saved = "Bakgrunden kunde inte sparas" +notifications.background_upload_failed = "Bakgrunden kunde inte sparas. Ladda upp en JPG-, PNG-, GIF- eller WebP-bild." +tabs.webhooks = " Webhooks" diff --git a/app/Language/tr-TR.ini b/app/Language/tr-TR.ini index 1bd62f27ac..cbcf4fa14c 100644 --- a/app/Language/tr-TR.ini +++ b/app/Language/tr-TR.ini @@ -2623,7 +2623,7 @@ label.notifications_my_work_description="Yalnızca size atanan görevler, öğel label.notifications_muted="Sessizde" label.notifications_muted_description="Doğrudan @bahsetmeler dışında bildirim yok" label.webhook_notifications="Kişisel Webhook" -label.webhook_notifications_description="Bildirimlerinizi kendi HTTPS uç noktanıza gönderin. Alma hakkınız olan her proje bildirimi için Leantime bu URL'ye JSON gövdeli bir HTTP POST isteği gönderir. İstekler arka planda gönderilir ve ulaşması birkaç dakika sürebilir; başarısız olan bir istek yeniden denenmez. Hangi bildirimlerin gönderileceğini yukarıdaki bildirim alma, kategori ve proje bazındaki ayarlarınız belirler. Webhook yalnızca etkinken ve bir URL ayarlanmışken gönderim yapar. Webhook devre dışıyken URL'niz kayıtlı kalır — kaldırmak için URL'yi temizleyin." +label.webhook_notifications_description="Bildirimlerinizi kendi HTTPS uç noktanıza gönderin. Alma hakkınız olan her proje bildirimi için Leantime bu URL'ye JSON gövdeli bir HTTP POST isteği gönderir. İstekler arka planda gönderilir ve ulaşması birkaç dakika sürebilir; başarısız olan bir istek yeniden denenmez. Hangi bildirimlerin gönderileceğini Bildirimler sekmesindeki ayarlarınız (bildirim alma, kategoriler ve proje bazındaki düzeyler) belirler. Webhook yalnızca etkinken ve bir URL ayarlanmışken gönderim yapar. Webhook devre dışıyken URL'niz kayıtlı kalır — kaldırmak için URL'yi temizleyin." label.enable_webhook_notifications="Webhook bildirimlerini etkinleştir" label.webhook_url_hint="Yalnızca HTTPS. Webhook'u kaldırmak için boş bırakın." label.default_notification_types="Yeni kullanıcılar için varsayılan bildirim türleri" @@ -4044,3 +4044,24 @@ modules.files.title="Dosyalar" modules.files.description="Projenin dosya kitaplığı. Yapılacaklardaki ekler ve metinlerdeki görseller erişilebilir kalır." modules.reports.title="Raporlar" modules.reports.description="Proje raporları ve durum özetleri." + +tabs.background = " Arka Plan" +menu.background = " Arka Plan" +text.set_your_background = "Sayfalarınızın arka planını seçin: renk şemanızın renk geçişi veya bir görsel." +label.background_type = "Geçerli arka plan" +label.gradient = "Renk geçişi" +label.image = "Görsel" +text.pick_background_image_below = "Arka plan olarak kullanmak için aşağıdan bir görsel seçin." +label.background_image = "Bir görsel seçin" +label.background_source = "Görsel kaynağı" +label.unsplash = "Unsplash" +input.search_unsplash = "Unsplash'te ara" +label.search_for_image = "Görsel ara" +buttons.refresh_images = "Diğer görselleri göster" +buttons.use_image = "Arka plan olarak kullan" +text.photo_by_on_unsplash = "Fotoğraf: %s / %s" +text.no_images_found = "Görsel bulunamadı" +notifications.background_settings_saved = "Arka plan kaydedildi" +notifications.background_not_saved = "Arka plan kaydedilemedi" +notifications.background_upload_failed = "Arka plan kaydedilemedi. JPG, PNG, GIF veya WebP görseli yükleyin." +tabs.webhooks = " Web Kancaları" diff --git a/app/Language/uk-UA.ini b/app/Language/uk-UA.ini index f071b62eaf..764b2996d2 100644 --- a/app/Language/uk-UA.ini +++ b/app/Language/uk-UA.ini @@ -2620,7 +2620,7 @@ label.notifications_my_work_description="Лише призначені вам з label.notifications_muted="Приглушено" label.notifications_muted_description="Жодних сповіщень, крім прямих @згадок" label.webhook_notifications="Персональний вебхук" -label.webhook_notifications_description="Надсилайте свої сповіщення на власну HTTPS-адресу. Для кожного сповіщення проєкту, яке вам належить отримати, Leantime надсилає на цю URL-адресу один HTTP POST-запит із тілом у форматі JSON. Запити надсилаються у фоновому режимі, і їх доставка може тривати кілька хвилин; невдалий запит не повторюється. Які сповіщення надсилатимуться, визначають наведені вище налаштування отримання сповіщень, категорій і проєктів. Вебхук надсилає дані лише тоді, коли його ввімкнено й задано URL-адресу. Поки вебхук вимкнено, ваша URL-адреса залишається збереженою — очистіть її, щоб видалити." +label.webhook_notifications_description="Надсилайте свої сповіщення на власну HTTPS-адресу. Для кожного сповіщення проєкту, яке вам належить отримати, Leantime надсилає на цю URL-адресу один HTTP POST-запит із тілом у форматі JSON. Запити надсилаються у фоновому режимі, і їх доставка може тривати кілька хвилин; невдалий запит не повторюється. Які сповіщення надсилатимуться, визначають ваші налаштування на вкладці «Сповіщення» (отримання сповіщень, категорії та рівні для проєктів). Вебхук надсилає дані лише тоді, коли його ввімкнено й задано URL-адресу. Поки вебхук вимкнено, ваша URL-адреса залишається збереженою — очистіть її, щоб видалити." label.enable_webhook_notifications="Увімкнути сповіщення через вебхук" label.webhook_url_hint="Лише HTTPS. Залиште порожнім, щоб видалити вебхук." label.default_notification_types="Типи сповіщень за замовчуванням для нових користувачів" @@ -4044,3 +4044,24 @@ modules.files.title="Файли" modules.files.description="Бібліотека файлів проєкту. Вкладення в завданнях і зображення в тексті залишаються доступними." modules.reports.title="Звіти" modules.reports.description="Звіти проєкту та огляди статусу." + +tabs.background = " Тло" +menu.background = " Тло" +text.set_your_background = "Виберіть тло сторінок: колірний градієнт вашої колірної схеми або зображення." +label.background_type = "Поточне тло" +label.gradient = "Колірний градієнт" +label.image = "Зображення" +text.pick_background_image_below = "Виберіть зображення нижче, щоб використати його як тло." +label.background_image = "Виберіть зображення" +label.background_source = "Джерело зображень" +label.unsplash = "Unsplash" +input.search_unsplash = "Пошук в Unsplash" +label.search_for_image = "Пошук зображення" +buttons.refresh_images = "Показати інші зображення" +buttons.use_image = "Використати як тло" +text.photo_by_on_unsplash = "Фото: %s, %s" +text.no_images_found = "Зображень не знайдено" +notifications.background_settings_saved = "Тло збережено" +notifications.background_not_saved = "Не вдалося зберегти тло" +notifications.background_upload_failed = "Не вдалося зберегти тло. Завантажте зображення JPG, PNG, GIF або WebP." +tabs.webhooks = " Вебхуки" diff --git a/app/Language/vi-VN.ini b/app/Language/vi-VN.ini index 89adb8d42a..2e716814e4 100644 --- a/app/Language/vi-VN.ini +++ b/app/Language/vi-VN.ini @@ -2623,7 +2623,7 @@ label.notifications_my_work_description="Chỉ các công việc được giao c label.notifications_muted="Đã tắt tiếng" label.notifications_muted_description="Không có thông báo, trừ các lượt @nhắc đến trực tiếp" label.webhook_notifications="Webhook cá nhân" -label.webhook_notifications_description="Gửi thông báo đến endpoint HTTPS của riêng bạn. Với mỗi thông báo dự án mà bạn đủ điều kiện nhận, Leantime gửi một yêu cầu HTTP POST với nội dung JSON đến URL này. Các yêu cầu được gửi ở chế độ nền và có thể mất vài phút mới đến nơi; yêu cầu bị lỗi sẽ không được gửi lại. Các cài đặt nhận thông báo, danh mục và theo dự án ở trên quyết định thông báo nào đủ điều kiện. Webhook chỉ gửi khi được bật và đã đặt URL. URL của bạn vẫn được lưu khi webhook bị tắt — hãy xóa URL để gỡ bỏ nó." +label.webhook_notifications_description="Gửi thông báo đến endpoint HTTPS của riêng bạn. Với mỗi thông báo dự án mà bạn đủ điều kiện nhận, Leantime gửi một yêu cầu HTTP POST với nội dung JSON đến URL này. Các yêu cầu được gửi ở chế độ nền và có thể mất vài phút mới đến nơi; yêu cầu bị lỗi sẽ không được gửi lại. Các cài đặt của bạn trong tab Thông Báo (nhận thông báo, danh mục và mức theo dự án) quyết định thông báo nào đủ điều kiện. Webhook chỉ gửi khi được bật và đã đặt URL. URL của bạn vẫn được lưu khi webhook bị tắt — hãy xóa URL để gỡ bỏ nó." label.enable_webhook_notifications="Bật thông báo qua webhook" label.webhook_url_hint="Chỉ HTTPS. Để trống để gỡ bỏ webhook." label.default_notification_types="Loại thông báo mặc định cho người dùng mới" @@ -4044,3 +4044,24 @@ modules.files.title="Tệp Tin" modules.files.description="Thư viện tệp của dự án. Tệp đính kèm trong công việc và hình ảnh trong văn bản vẫn có thể sử dụng." modules.reports.title="Báo cáo" modules.reports.description="Báo cáo dự án và tổng quan trạng thái." + +tabs.background = " Hình nền" +menu.background = " Hình nền" +text.set_your_background = "Chọn hình nền cho các trang của bạn: dải màu chuyển sắc của bảng màu hoặc một hình ảnh." +label.background_type = "Hình nền hiện tại" +label.gradient = "Dải màu chuyển sắc" +label.image = "Hình ảnh" +text.pick_background_image_below = "Chọn một hình ảnh bên dưới để dùng làm hình nền." +label.background_image = "Chọn một hình ảnh" +label.background_source = "Nguồn hình ảnh" +label.unsplash = "Unsplash" +input.search_unsplash = "Tìm trên Unsplash" +label.search_for_image = "Tìm hình ảnh" +buttons.refresh_images = "Hiển thị hình ảnh khác" +buttons.use_image = "Dùng làm hình nền" +text.photo_by_on_unsplash = "Ảnh của %s trên %s" +text.no_images_found = "Không tìm thấy hình ảnh" +notifications.background_settings_saved = "Đã lưu hình nền" +notifications.background_not_saved = "Không thể lưu hình nền" +notifications.background_upload_failed = "Không thể lưu hình nền. Hãy tải lên hình ảnh JPG, PNG, GIF hoặc WebP." +tabs.webhooks = " Webhook" diff --git a/app/Language/zh-CN.ini b/app/Language/zh-CN.ini index cb8bf01271..fbe41a1a62 100644 --- a/app/Language/zh-CN.ini +++ b/app/Language/zh-CN.ini @@ -2645,7 +2645,7 @@ label.notifications_my_work_description="仅限分配给你的任务、你的条 label.notifications_muted="已静音" label.notifications_muted_description="除直接 @提及外不接收任何通知" label.webhook_notifications="个人 Webhook" -label.webhook_notifications_description="将你的通知发送到你自己的 HTTPS 端点。对于你有资格接收的每条项目通知,Leantime 都会向此 URL 发送一个带 JSON 正文的 HTTP POST 请求。请求在后台发送,可能需要几分钟才能送达;失败的请求不会重试。上方的接收通知、类别和按项目设置决定哪些通知符合条件。仅当 Webhook 已启用且已设置 URL 时才会发送。禁用 Webhook 后 URL 仍会保留——清空 URL 即可将其删除。" +label.webhook_notifications_description="将你的通知发送到你自己的 HTTPS 端点。对于你有资格接收的每条项目通知,Leantime 都会向此 URL 发送一个带 JSON 正文的 HTTP POST 请求。请求在后台发送,可能需要几分钟才能送达;失败的请求不会重试。你在通知选项卡中的设置(接收通知、类别和按项目的级别)决定哪些通知符合条件。仅当 Webhook 已启用且已设置 URL 时才会发送。禁用 Webhook 后 URL 仍会保留——清空 URL 即可将其删除。" label.enable_webhook_notifications="启用 Webhook 通知" label.webhook_url_hint="仅支持 HTTPS。留空即可删除 Webhook。" label.default_notification_types="新用户的默认通知类型" @@ -4057,3 +4057,24 @@ modules.files.title="文件" modules.files.description="项目的文件库。待办事项中的附件和文本中的图片仍可使用。" modules.reports.title="报告" modules.reports.description="项目报告和状态概览。" + +tabs.background = " 背景" +menu.background = " 背景" +text.set_your_background = "选择页面背景:配色方案的渐变色或一张图片。" +label.background_type = "当前背景" +label.gradient = "渐变色" +label.image = "图片" +text.pick_background_image_below = "在下方选择一张图片作为背景。" +label.background_image = "选择图片" +label.background_source = "图片来源" +label.unsplash = "Unsplash" +input.search_unsplash = "搜索 Unsplash" +label.search_for_image = "搜索图片" +buttons.refresh_images = "显示其他图片" +buttons.use_image = "用作背景" +text.photo_by_on_unsplash = "摄影:%s,来自 %s" +text.no_images_found = "未找到图片" +notifications.background_settings_saved = "背景已保存" +notifications.background_not_saved = "无法保存背景" +notifications.background_upload_failed = "无法保存背景。请上传 JPG、PNG、GIF 或 WebP 图片。" +tabs.webhooks = " Webhook" diff --git a/app/Language/zh-TW.ini b/app/Language/zh-TW.ini index 7d2c17f4ea..1c5fa2d036 100644 --- a/app/Language/zh-TW.ini +++ b/app/Language/zh-TW.ini @@ -2623,7 +2623,7 @@ label.notifications_my_work_description="僅限分配給您的任務、您的項 label.notifications_muted="已靜音" label.notifications_muted_description="除了直接 @提及外,不接收任何通知" label.webhook_notifications="個人 Webhook" -label.webhook_notifications_description="將您的通知傳送到您自己的 HTTPS 端點。對於您有資格接收的每則專案通知,Leantime 都會向此 URL 傳送一個帶有 JSON 內容的 HTTP POST 請求。請求會在背景傳送,可能需要幾分鐘才會送達;失敗的請求不會重試。上方的接收通知、類別和個別專案設定決定哪些通知符合資格。Webhook 只有在啟用且已設定 URL 時才會傳送。停用 Webhook 時,您的 URL 仍會保留 — 清除 URL 即可將其移除。" +label.webhook_notifications_description="將您的通知傳送到您自己的 HTTPS 端點。對於您有資格接收的每則專案通知,Leantime 都會向此 URL 傳送一個帶有 JSON 內容的 HTTP POST 請求。請求會在背景傳送,可能需要幾分鐘才會送達;失敗的請求不會重試。您在通知分頁中的設定(接收通知、類別和個別專案層級)決定哪些通知符合資格。Webhook 只有在啟用且已設定 URL 時才會傳送。停用 Webhook 時,您的 URL 仍會保留 — 清除 URL 即可將其移除。" label.enable_webhook_notifications="啟用 Webhook 通知" label.webhook_url_hint="僅限 HTTPS。留空即可移除 Webhook。" label.default_notification_types="新使用者的預設通知類型" @@ -4044,3 +4044,24 @@ modules.files.title="檔案" modules.files.description="專案的檔案庫。待辦事項中的附件和文字中的圖片仍可使用。" modules.reports.title="報告" modules.reports.description="專案報告與狀態總覽。" + +tabs.background = " 背景" +menu.background = " 背景" +text.set_your_background = "選擇頁面背景:配色的漸層色或一張圖片。" +label.background_type = "目前背景" +label.gradient = "漸層色" +label.image = "圖片" +text.pick_background_image_below = "在下方選擇一張圖片作為背景。" +label.background_image = "選擇圖片" +label.background_source = "圖片來源" +label.unsplash = "Unsplash" +input.search_unsplash = "搜尋 Unsplash" +label.search_for_image = "搜尋圖片" +buttons.refresh_images = "顯示其他圖片" +buttons.use_image = "用作背景" +text.photo_by_on_unsplash = "攝影:%s,來自 %s" +text.no_images_found = "找不到圖片" +notifications.background_settings_saved = "背景已儲存" +notifications.background_not_saved = "無法儲存背景" +notifications.background_upload_failed = "無法儲存背景。請上傳 JPG、PNG、GIF 或 WebP 圖片。" +tabs.webhooks = " Webhook" diff --git a/app/Views/Templates/sections/backgroundStyle.blade.php b/app/Views/Templates/sections/backgroundStyle.blade.php new file mode 100644 index 0000000000..71d7475f88 --- /dev/null +++ b/app/Views/Templates/sections/backgroundStyle.blade.php @@ -0,0 +1,25 @@ +{{-- + The user's page background. Rendered in the page header and swapped out of band + (oob = true) when the user changes their background in their settings. + + Variables: $themeBg (image URL), $themeBgCss (the URL as CSS string, Header::toCssString), + $themeOpacity, $themeType ('gradient' | 'image'). +--}} + diff --git a/app/Views/Templates/sections/header.blade.php b/app/Views/Templates/sections/header.blade.php index 2e4962778c..63ff5f8185 100644 --- a/app/Views/Templates/sections/header.blade.php +++ b/app/Views/Templates/sections/header.blade.php @@ -124,9 +124,26 @@ functions that run on/after DOMContentLoaded (e.g. initCalendar()), never at break; } } + + // Primary buttons put a white label on the first accent. Where that is below AA and dark + // text reads better (light accents, e.g. Forest in dark mode), the label turns dark. + $mainActionLabelColor = null; + if (is_string($accents[0] ?? null) && preg_match('/^#?([0-9a-fA-F]{6})$/', $accents[0], $primaryHex)) { + [$pr, $pg, $pb] = sscanf($primaryHex[1], '%02x%02x%02x'); + $linChannel = static fn ($v) => ($v /= 255) <= 0.03928 ? $v / 12.92 : (($v + 0.055) / 1.055) ** 2.4; + $primaryLum = 0.2126 * $linChannel($pr) + 0.7152 * $linChannel($pg) + 0.0722 * $linChannel($pb); + $whiteLabelContrast = 1.05 / ($primaryLum + 0.05); + $darkLabelContrast = ($primaryLum + 0.05) / (0.0103 + 0.05); // #1a1a1a + if ($whiteLabelContrast < 4.5 && $darkLabelContrast > $whiteLabelContrast) { + $mainActionLabelColor = '#1a1a1a'; + } + } @endphp - +@include('global::sections.backgroundStyle') @dispatchEvent('afterThemeColors') diff --git a/config/sample.env b/config/sample.env index 4ed4d25136..aa3c657801 100644 --- a/config/sample.env +++ b/config/sample.env @@ -66,6 +66,7 @@ LEAN_PRINT_LOGO_URL = '/dist/images/logo.png' # Default logo URL use for pr LEAN_DEFAULT_THEME = 'default' # Default theme LEAN_PRIMARY_COLOR = '#006d9f' # Primary Theme color LEAN_SECONDARY_COLOR =' #00a886' # Secondary Theme Color +#LEAN_UNSPLASH_ACCESS_KEY = '' # Unsplash access key (unsplash.com/developers). Lets users pick Unsplash photos as background ## File Uploads diff --git a/public/assets/css/components/forms.css b/public/assets/css/components/forms.css index 5b99998d40..50e1c6234f 100644 --- a/public/assets/css/components/forms.css +++ b/public/assets/css/components/forms.css @@ -382,7 +382,8 @@ input[type='submit'], line-height:21px; box-shadow:none; text-shadow: none; - color: var(--main-action-color); + /* --main-action-label-color: dark label when white is too faint on the accent (sections/header). */ + color: var(--main-action-label-color, var(--main-action-color)); display: inline-block; word-spacing: 2px; border: 1px solid var(--main-action-bg); @@ -409,7 +410,7 @@ body input[type='submit'].btn-primary .caret, .btn-primary[disabled] { background:var(--main-action-bg); - color: var(--main-action-color); + color: var(--main-action-label-color, var(--main-action-color)); border: 1px solid var(--main-action-bg); cursor:not-allowed; } diff --git a/public/assets/js/app/core/snippets.js b/public/assets/js/app/core/snippets.js index c5254e674a..e400499a82 100644 --- a/public/assets/js/app/core/snippets.js +++ b/public/assets/js/app/core/snippets.js @@ -218,11 +218,43 @@ leantime.snippets = (function () { }; + // Relative luminance (WCAG) of a #rrggbb color, null when it can't be parsed. + var relativeLuminance = function (hexColor) { + var match = /^#?([0-9a-f]{6})$/i.exec(hexColor || ''); + if (!match) { + return null; + } + + var channels = [0, 2, 4].map(function (offset) { + var value = parseInt(match[1].substr(offset, 2), 16) / 255; + return value <= 0.03928 ? value / 12.92 : Math.pow((value + 0.055) / 1.055, 2.4); + }); + + return 0.2126 * channels[0] + 0.7152 * channels[1] + 0.0722 * channels[2]; + }; + + // Live preview of a color scheme. Applies the same contrast rules as sections/header.blade.php: + // a nav scrim when white text is below AA on an accent, and a dark primary-button label when + // white is below AA on accent1 and dark text reads better. var toggleColors = function (accent1, accent2) { - jQuery("#colorSchemeSetter").html(":root { --accent1: "+accent1+"; --accent2: "+accent2+"}") + var navNeedsScrim = [accent1, accent2].some(function (accent) { + var luminance = relativeLuminance(accent); + return luminance === null || 1.05 / (luminance + 0.05) < 4.5; + }); + + var css = ":root { --accent1: " + accent1 + "; --accent2: " + accent2 + "; --nav-scrim: " + (navNeedsScrim ? "0.22" : "0") + ";"; + var primaryLuminance = relativeLuminance(accent1); + if (primaryLuminance !== null) { + var whiteLabelContrast = 1.05 / (primaryLuminance + 0.05); + var darkLabelContrast = (primaryLuminance + 0.05) / (0.0103 + 0.05); + if (whiteLabelContrast < 4.5 && darkLabelContrast > whiteLabelContrast) { + css += " --main-action-label-color: #1a1a1a;"; + } + } + jQuery("#colorSchemeSetter").html(css + " }"); }; diff --git a/tests/Unit/app/Core/UI/ThemeTest.php b/tests/Unit/app/Core/UI/ThemeTest.php index 1d76c30762..eea3c3fd20 100644 --- a/tests/Unit/app/Core/UI/ThemeTest.php +++ b/tests/Unit/app/Core/UI/ThemeTest.php @@ -109,4 +109,19 @@ public function test_get_default_color_scheme_without_env() $this->assertEquals('themeDefault', $colorScheme); } + + public function test_forest_gets_readable_colors_in_dark_mode(): void + { + $forest = ['name' => 'Forest', 'primaryColor' => '#74211c', 'secondaryColor' => '#32251e']; + $schemeForMode = new \ReflectionMethod(Theme::class, 'getColorSchemeForMode'); + + $darkTheme = $this->make(Theme::class, ['getColorMode' => 'dark']); + $lightTheme = $this->make(Theme::class, ['getColorMode' => 'light']); + + $this->assertSame( + ['name' => 'Forest', 'primaryColor' => '#e59a95', 'secondaryColor' => '#c2a89a'], + $schemeForMode->invoke($darkTheme, 'forrest', $forest) + ); + $this->assertSame($forest, $schemeForMode->invoke($lightTheme, 'forrest', $forest)); + } } diff --git a/tests/Unit/app/Domain/Users/Controllers/EditOwnTest.php b/tests/Unit/app/Domain/Users/Controllers/EditOwnTest.php index bbdf8b55f6..a2e2c208b5 100644 --- a/tests/Unit/app/Domain/Users/Controllers/EditOwnTest.php +++ b/tests/Unit/app/Domain/Users/Controllers/EditOwnTest.php @@ -12,9 +12,9 @@ use Unit\TestCase; /** - * The notifications tab of the own-profile form: a rejected webhook URL or an - * unsaved webhook setting must surface as an error, never as the "saved" - * confirmation. + * The notifications and webhooks tabs of the own-profile form: a rejected + * webhook URL or an unsaved webhook setting must surface as an error, never as + * the "saved" confirmation. */ class EditOwnTest extends TestCase { @@ -46,19 +46,16 @@ protected function tearDown(): void } /** - * Submits the notifications tab through the real controller with the given - * (stubbed) user service. + * Submits a tab of the own-profile form through the real controller with the + * given (stubbed) user service and returns the redirect target. + * + * @param array $post The tab's fields, without the form token. */ - private function submitNotificationsTab(UserService $userService): void + private function submitTab(UserService $userService, array $post): string { app()->instance(UserService::class, $userService); - $_POST = [ - 'formToken' => 'expected-token', - 'savenotifications' => '1', - 'webhookUrl' => 'http://insecure.example.com/hook', - 'webhookEnabled' => '1', - ]; + $_POST = ['formToken' => 'expected-token'] + $post; $tpl = $this->make(Template::class, [ 'setNotification' => function (string $message, string $type) { @@ -69,13 +66,24 @@ private function submitNotificationsTab(UserService $userService): void $response = (new EditOwn($this->make(IncomingRequest::class), $tpl, $language))->post(); - $this->assertStringEndsWith('/users/editOwn#notifications', $response->headers->get('Location')); + return (string) $response->headers->get('Location'); + } + + private function submitWebhooksTab(UserService $userService): void + { + $location = $this->submitTab($userService, [ + 'savewebhooks' => '1', + 'webhookUrl' => 'http://insecure.example.com/hook', + 'webhookEnabled' => '1', + ]); + + $this->assertStringEndsWith('/users/editOwn#webhooks', $location); } public function test_rejected_webhook_url_shows_an_error_and_no_success(): void { - $this->submitNotificationsTab($this->make(UserService::class, [ - 'saveOwnNotificationPreferences' => function () { + $this->submitWebhooksTab($this->make(UserService::class, [ + 'saveOwnWebhookSettings' => function () { throw ValidationException::withMessages(['webhookUrl' => ['notification.invalid_webhook_url']]); }, ])); @@ -85,8 +93,8 @@ public function test_rejected_webhook_url_shows_an_error_and_no_success(): void public function test_unsaved_webhook_setting_shows_a_generic_error_and_no_success(): void { - $this->submitNotificationsTab($this->make(UserService::class, [ - 'saveOwnNotificationPreferences' => function () { + $this->submitWebhooksTab($this->make(UserService::class, [ + 'saveOwnWebhookSettings' => function () { throw new WebhookSettingNotSavedException; }, ])); @@ -99,19 +107,35 @@ public function test_unexpected_failures_are_not_swallowed_into_a_notification() // Only the known outcomes are handled; anything else (e.g. a DB error) must reach the exception handler. $this->expectExceptionObject(new \RuntimeException('database went away')); - $this->submitNotificationsTab($this->make(UserService::class, [ - 'saveOwnNotificationPreferences' => function () { + $this->submitWebhooksTab($this->make(UserService::class, [ + 'saveOwnWebhookSettings' => function () { throw new \RuntimeException('database went away'); }, ])); } - public function test_accepted_preferences_show_the_success_confirmation(): void + public function test_accepted_webhook_shows_the_success_confirmation(): void { - $this->submitNotificationsTab($this->make(UserService::class, [ - 'saveOwnNotificationPreferences' => fn () => null, + $this->submitWebhooksTab($this->make(UserService::class, [ + 'saveOwnWebhookSettings' => fn () => null, ])); $this->assertSame([['notifications.changed_profile_settings_successfully', 'success']], $this->notifications); } + + public function test_notification_preferences_are_saved_without_the_webhook(): void + { + $webhookSaves = 0; + + $location = $this->submitTab($this->make(UserService::class, [ + 'saveOwnNotificationPreferences' => fn () => null, + 'saveOwnWebhookSettings' => function () use (&$webhookSaves) { + $webhookSaves++; + }, + ]), ['savenotifications' => '1', 'notifications' => 'on']); + + $this->assertStringEndsWith('/users/editOwn#notifications', $location); + $this->assertSame(0, $webhookSaves); + $this->assertSame([['notifications.changed_profile_settings_successfully', 'success']], $this->notifications); + } } diff --git a/tests/Unit/app/Domain/Users/Hxcontrollers/BackgroundTest.php b/tests/Unit/app/Domain/Users/Hxcontrollers/BackgroundTest.php new file mode 100644 index 0000000000..035af14473 --- /dev/null +++ b/tests/Unit/app/Domain/Users/Hxcontrollers/BackgroundTest.php @@ -0,0 +1,103 @@ +make(Template::class, [ + 'emptyResponse' => fn ($code = 200) => new Response('', $code), + 'assign' => fn () => null, + 'setNotification' => fn () => null, + ]); + + $backgrounds = $this->make(Backgrounds::class, [ + 'setBackgroundType' => function (string $type) use ($changes): bool { + $changes->append(['setBackgroundType', $type]); + + return true; + }, + 'useUnsplashPhoto' => function (string $photoId) use ($changes): bool { + $changes->append(['useUnsplashPhoto', $photoId]); + + return true; + }, + 'useUploadedImage' => function (array $files) use ($changes): bool { + $changes->append(['useUploadedImage', $files]); + + return true; + }, + 'getCurrentBackground' => fn () => ['type' => 'gradient', 'image' => ''], + 'isUnsplashEnabled' => fn () => false, + ]); + + $theme = $this->make(Theme::class, [ + 'getPrimaryColor' => fn () => '#006d9f', + 'getSecondaryColor' => fn () => '#00a886', + ]); + + $controller = (new \ReflectionClass(Background::class))->newInstanceWithoutConstructor(); + foreach (['incomingRequest' => $request, 'tpl' => $tpl] as $property => $value) { + $reflection = new \ReflectionProperty(Background::class, $property); + $reflection->setAccessible(true); + $reflection->setValue($controller, $value); + } + $controller->init($backgrounds, $theme); + + return $controller; + } + + /** + * @dataProvider changeActionProvider + */ + public function test_get_is_rejected_without_changing_the_background(string $action, array $parameters): void + { + $changes = new \ArrayObject; + + $response = $this->controllerFor('GET', $parameters, $changes)->{$action}(); + + $this->assertInstanceOf(Response::class, $response); + $this->assertSame(405, $response->getStatusCode()); + $this->assertCount(0, $changes); + } + + public static function changeActionProvider(): array + { + return [ + 'switch to gradient' => ['setType', ['type' => 'gradient']], + 'use an Unsplash photo' => ['useUnsplashPhoto', ['photoId' => 'abc123']], + 'upload an image' => ['upload', []], + ]; + } + + public function test_post_switches_the_type_from_the_request_body(): void + { + $changes = new \ArrayObject; + + $fragment = $this->controllerFor('POST', ['type' => 'gradient'], $changes)->setType(); + + $this->assertSame('current-background', $fragment); + $this->assertSame([['setBackgroundType', 'gradient']], $changes->getArrayCopy()); + } +} diff --git a/tests/Unit/app/Domain/Users/Services/BackgroundsTest.php b/tests/Unit/app/Domain/Users/Services/BackgroundsTest.php new file mode 100644 index 0000000000..666a50c180 --- /dev/null +++ b/tests/Unit/app/Domain/Users/Services/BackgroundsTest.php @@ -0,0 +1,323 @@ + In-memory user settings. */ + private array $settings = []; + + /** @var int[] File ids the service deleted. */ + private array $deletedFileIds = []; + + /** @var string[] File names the service handed to Files::upload(). */ + private array $uploadedNames = []; + + protected function setUp(): void + { + parent::setUp(); + + $this->http = new HttpClient; + $this->http->preventStrayRequests(); + $this->storedImages = []; + $this->storedTypes = []; + $this->settings = []; + $this->deletedFileIds = []; + $this->uploadedNames = []; + session(['userdata.id' => 1]); + } + + private function makeService(string $accessKey = 'test-key', ?int $uploadedFileId = null): Backgrounds + { + $config = $this->make(Environment::class); + $config->unsplashAccessKey = $accessKey; + + $theme = $this->make(Theme::class, [ + 'setBackgroundImage' => function (string $url) { + $this->storedImages[] = $url; + }, + 'setBackgroundType' => function (string $type) { + $this->storedTypes[] = $type; + }, + ]); + + $files = $this->make(Files::class, [ + 'upload' => function ($files) use ($uploadedFileId) { + $this->uploadedNames[] = $files['file']['name']; + + return ['fileId' => $uploadedFileId, 'encName' => 'enc'.$uploadedFileId, 'extension' => 'png', 'realName' => 'bg.png']; + }, + 'deleteFile' => function ($fileId) { + $this->deletedFileIds[] = (int) $fileId; + + return true; + }, + ]); + + $settings = $this->make(Setting::class, [ + 'getSetting' => fn ($key, $default = false) => $this->settings[$key] ?? $default, + 'saveSetting' => function ($key, $value) { + $this->settings[$key] = $value; + + return true; + }, + 'deleteSetting' => function ($key) { + unset($this->settings[$key]); + }, + ]); + + return new Backgrounds($theme, $config, $files, $this->http, $settings, app('cache')); + } + + /** + * A real PNG upload, as one $_FILES entry. + */ + private function pngUpload(): array + { + $png = tempnam(sys_get_temp_dir(), 'bg'); + $image = imagecreatetruecolor(4, 4); + imagepng($image, $png); + + return ['name' => 'bg.png', 'type' => 'image/png', 'tmp_name' => $png, 'error' => UPLOAD_ERR_OK, 'size' => filesize($png)]; + } + + public function test_a_new_upload_deletes_the_previously_uploaded_background(): void + { + $this->assertTrue($this->makeService(uploadedFileId: 10)->useUploadedImage(['file' => $this->pngUpload()])); + $this->assertSame([], $this->deletedFileIds, 'Nothing to delete on the first upload'); + + $this->assertTrue($this->makeService(uploadedFileId: 11)->useUploadedImage(['file' => $this->pngUpload()])); + + $this->assertSame([10], $this->deletedFileIds); + $this->assertSame(11, $this->settings['usersettings.1.backgroundFileId']); + + // Unique names: with file renaming turned off, two uploads of "bg.png" would otherwise share + // one stored file, and deleting the replaced one would delete the new background. + $this->assertCount(2, array_unique($this->uploadedNames)); + foreach ($this->uploadedNames as $uploadedName) { + $this->assertMatchesRegularExpression('/^background[0-9a-f]{16}\.png$/', $uploadedName); + } + } + + public function test_switching_to_the_gradient_deletes_the_uploaded_background(): void + { + $this->settings['usersettings.1.backgroundFileId'] = 10; + + $this->makeService()->setBackgroundType('gradient'); + + $this->assertSame([10], $this->deletedFileIds); + $this->assertArrayNotHasKey('usersettings.1.backgroundFileId', $this->settings); + } + + public function test_a_change_waits_for_another_change_and_gives_up_without_saving(): void + { + // Two tabs: while one change holds the user's lock, another must not save or clean up, + // and its freshly uploaded file is removed again. + $this->settings['usersettings.1.backgroundFileId'] = 10; + $otherChange = app('cache')->lock('users.background.1', 30); + $this->assertTrue($otherChange->get()); + + $waitSeconds = new \ReflectionClassConstant(Backgrounds::class, 'LOCK_WAIT_SECONDS'); + $this->assertSame(10, $waitSeconds->getValue(), 'The test relies on the wait being bounded'); + + try { + $started = microtime(true); + $saved = $this->makeService(uploadedFileId: 11)->useUploadedImage(['file' => $this->pngUpload()]); + } finally { + $otherChange->release(); + } + + $this->assertFalse($saved); + $this->assertGreaterThanOrEqual(9, microtime(true) - $started, 'It waited for the other change first'); + $this->assertSame([], $this->storedImages, 'Nothing is saved'); + $this->assertSame(10, $this->settings['usersettings.1.backgroundFileId'], 'The tracked upload is unchanged'); + $this->assertSame([11], $this->deletedFileIds, 'Only the new, unused upload is removed'); + } + + public function test_an_unsplash_photo_deletes_the_uploaded_background(): void + { + $this->settings['usersettings.1.backgroundFileId'] = 10; + $this->http->fake([ + 'api.unsplash.com/photos/abc123/download*' => $this->http->response(['url' => 'https://example.test']), + 'api.unsplash.com/photos/abc123' => $this->http->response($this->unsplashPhoto('https://images.unsplash.com/photo-1?ixid=1')), + ]); + + $this->assertTrue($this->makeService()->useUnsplashPhoto('abc123')); + + $this->assertSame([10], $this->deletedFileIds); + $this->assertArrayNotHasKey('usersettings.1.backgroundFileId', $this->settings); + } + + private function unsplashPhoto(string $rawUrl): array + { + return [ + 'id' => 'abc123', + 'alt_description' => 'snowy mountains', + 'urls' => ['raw' => $rawUrl, 'small' => 'https://images.unsplash.com/photo-1?w=400'], + 'user' => ['name' => 'Jane Doe', 'links' => ['html' => 'https://unsplash.com/@jane']], + 'links' => ['download_location' => 'https://api.unsplash.com/photos/abc123/download?ixid=xyz'], + ]; + } + + public function test_unsplash_is_disabled_without_access_key(): void + { + $service = $this->makeService(accessKey: ''); + + $this->assertFalse($service->isUnsplashEnabled()); + $this->assertSame(['photos' => [], 'nextPage' => 1], $service->searchUnsplash('mountains')); + $this->assertFalse($service->useUnsplashPhoto('abc123')); + $this->assertCount(0, $this->http->recorded()); + } + + public function test_search_returns_photos_with_attribution_and_sends_access_key(): void + { + $this->http->fake([ + 'api.unsplash.com/search/photos*' => $this->http->response([ + 'total_pages' => 3, + 'results' => [$this->unsplashPhoto('https://images.unsplash.com/photo-1?ixid=1')], + ]), + ]); + + $searchResult = $this->makeService()->searchUnsplash('mountains'); + + $this->assertSame(2, $searchResult['nextPage']); + $this->assertSame([[ + 'id' => 'abc123', + 'thumbnailUrl' => 'https://images.unsplash.com/photo-1?w=400', + 'description' => 'snowy mountains', + 'photographerName' => 'Jane Doe', + 'photographerUrl' => 'https://unsplash.com/@jane?utm_source=leantime&utm_medium=referral', + ]], $searchResult['photos']); + + $this->http->assertSent(fn (Request $request) => $request->hasHeader('Authorization', 'Client-ID test-key') + && $request['query'] === 'mountains'); + } + + public function test_search_returns_nothing_when_unsplash_fails(): void + { + $this->http->fake(['api.unsplash.com/*' => $this->http->response(['errors' => ['Rate Limit Exceeded']], 403)]); + + $this->assertSame(['photos' => [], 'nextPage' => 1], $this->makeService()->searchUnsplash('mountains')); + } + + public function test_the_next_page_after_the_last_one_starts_over(): void + { + $this->http->fake([ + 'api.unsplash.com/search/photos*' => $this->http->response(['total_pages' => 3, 'results' => []]), + ]); + + $this->assertSame(1, $this->makeService()->searchUnsplash('mountains', 3)['nextPage']); + $this->http->assertSent(fn (Request $request) => $request['page'] === 3); + } + + public function test_using_a_photo_stores_its_resized_url_and_reports_the_download(): void + { + $this->http->fake([ + 'api.unsplash.com/photos/abc123/download*' => $this->http->response(['url' => 'https://example.test']), + 'api.unsplash.com/photos/abc123' => $this->http->response($this->unsplashPhoto('https://images.unsplash.com/photo-1?ixid=1')), + ]); + + $this->assertTrue($this->makeService()->useUnsplashPhoto('abc123')); + + $this->assertSame(['https://images.unsplash.com/photo-1?ixid=1&w=2400&q=80&fm=jpg&fit=max'], $this->storedImages); + $this->http->assertSent(fn (Request $request) => str_starts_with($request->url(), 'https://api.unsplash.com/photos/abc123/download')); + } + + public function test_photo_outside_unsplash_image_host_is_refused(): void + { + $this->http->fake([ + 'api.unsplash.com/photos/abc123' => $this->http->response($this->unsplashPhoto('https://evil.test/x.jpg')), + ]); + + $this->settings['usersettings.1.backgroundFileId'] = 10; + + $this->assertFalse($this->makeService()->useUnsplashPhoto('abc123')); + $this->assertSame([], $this->storedImages); + $this->assertSame([], $this->deletedFileIds, 'A refused photo keeps the uploaded background'); + } + + public function test_photo_id_with_path_characters_is_refused_without_a_request(): void + { + $this->assertFalse($this->makeService()->useUnsplashPhoto('../me')); + $this->assertCount(0, $this->http->recorded()); + } + + public function test_unknown_background_type_is_refused(): void + { + $service = $this->makeService(); + + $service->setBackgroundType('gradient'); + $this->assertSame(['gradient'], $this->storedTypes); + + $this->expectException(ValidationException::class); + $service->setBackgroundType('video'); + } + + public function test_upload_of_an_image_format_browsers_cannot_show_is_refused(): void + { + // A TIFF named .jpg: getimagesize() detects it, but it is not a JPEG/PNG/GIF/WebP. + $tiff = tempnam(sys_get_temp_dir(), 'bg'); + file_put_contents($tiff, "II*\x00\x08\x00\x00\x00\x03\x00\x00\x01\x03\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01\x01\x03\x00\x01\x00\x00\x00\x01\x00\x00\x00\x15\x01\x03\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00"); + $this->assertSame(IMAGETYPE_TIFF_II, getimagesize($tiff)[2], 'The fixture must be a TIFF getimagesize() recognizes'); + + session(['userdata.id' => 1]); + + $stored = $this->makeService()->useUploadedImage(['file' => [ + 'name' => 'background.jpg', + 'type' => 'image/jpeg', + 'tmp_name' => $tiff, + 'error' => UPLOAD_ERR_OK, + 'size' => filesize($tiff), + ]]); + + unlink($tiff); + + $this->assertFalse($stored); + $this->assertSame([], $this->storedImages); + } + + public function test_upload_that_is_not_an_image_is_refused(): void + { + $fakeImage = tempnam(sys_get_temp_dir(), 'bg'); + file_put_contents($fakeImage, ''); + + session(['userdata.id' => 1]); + + $stored = $this->makeService()->useUploadedImage(['file' => [ + 'name' => 'background.jpg', + 'type' => 'image/jpeg', + 'tmp_name' => $fakeImage, + 'error' => UPLOAD_ERR_OK, + 'size' => filesize($fakeImage), + ]]); + + unlink($fakeImage); + + $this->assertFalse($stored); + $this->assertSame([], $this->storedImages); + } +} diff --git a/tests/Unit/app/Domain/Users/Services/UsersServiceTest.php b/tests/Unit/app/Domain/Users/Services/UsersServiceTest.php index 903ebbf009..d6a8389eb5 100644 --- a/tests/Unit/app/Domain/Users/Services/UsersServiceTest.php +++ b/tests/Unit/app/Domain/Users/Services/UsersServiceTest.php @@ -471,7 +471,7 @@ public function test_self_service_methods_ignore_caller_supplied_id_and_pin_to_s } // --------------------------------------------------------------------- - // Personal notification webhook (saveOwnNotificationPreferences / + // Personal notification webhook (saveOwnWebhookSettings / // getNotificationPreferences). // --------------------------------------------------------------------- @@ -526,25 +526,22 @@ private function webhookSetting(string $url, bool $enabled): string return json_encode(['url' => $url, 'enabled' => $enabled]); } - public function test_save_notification_preferences_stores_the_webhook_for_the_session_user_only(): void + public function test_save_webhook_settings_stores_the_webhook_for_the_session_user_only(): void { session(['userdata' => ['id' => 7]]); $saved = []; $this->makeService($this->profileRepo(), ['settingsService' => $this->settingsStore([], $saved)]) - ->saveOwnNotificationPreferences(99, [ + ->saveOwnWebhookSettings(99, [ 'webhookUrl' => ' https://hooks.example.com/services/abc?token=xyz ', 'webhookEnabled' => 'on', ]); + $this->assertSame(['usersettings.7.webhook'], array_keys($saved), 'Only the session user\'s webhook is written'); $this->assertSame( ['url' => 'https://hooks.example.com/services/abc?token=xyz', 'enabled' => true], json_decode($saved['usersettings.7.webhook'], true) ); - $this->assertSame('usersettings.7.webhook', array_key_first($saved), 'The webhook is saved before any other preference'); - foreach (array_keys($saved) as $key) { - $this->assertStringNotContainsString('usersettings.99.', $key, 'A caller-supplied id must never be written to'); - } } public function test_unchecking_the_webhook_keeps_the_url_but_disables_delivery(): void @@ -552,7 +549,7 @@ public function test_unchecking_the_webhook_keeps_the_url_but_disables_delivery( $saved = []; $this->makeService($this->profileRepo(), ['settingsService' => $this->settingsStore([], $saved)]) - ->saveOwnNotificationPreferences(1, ['webhookUrl' => 'https://hooks.example.com/abc']); + ->saveOwnWebhookSettings(1, ['webhookUrl' => 'https://hooks.example.com/abc']); $this->assertSame(['url' => 'https://hooks.example.com/abc', 'enabled' => false], json_decode($saved['usersettings.1.webhook'], true)); } @@ -562,24 +559,39 @@ public function test_an_empty_webhook_url_disables_delivery_even_when_checked(): $saved = []; $this->makeService($this->profileRepo(), ['settingsService' => $this->settingsStore([], $saved)]) - ->saveOwnNotificationPreferences(1, ['webhookUrl' => '', 'webhookEnabled' => '1']); + ->saveOwnWebhookSettings(1, ['webhookUrl' => '', 'webhookEnabled' => '1']); $this->assertSame(['url' => '', 'enabled' => false], json_decode($saved['usersettings.1.webhook'], true)); } - /** - * @dataProvider payloadWithoutWebhookUrlProvider - */ - public function test_omitting_the_webhook_url_leaves_the_stored_webhook_unchanged(array $post): void + public function test_a_cleared_webhook_url_arriving_as_null_disables_delivery(): void { - // API clients built before the webhook existed only send the older preferences. + // ConvertEmptyStringsToNull turns the cleared form field into null. + $saved = []; + + $this->makeService($this->profileRepo(), ['settingsService' => $this->settingsStore([], $saved)]) + ->saveOwnWebhookSettings(1, ['webhookUrl' => null]); + + $this->assertSame(['url' => '', 'enabled' => false], json_decode($saved['usersettings.1.webhook'], true)); + } + + public function test_notification_preferences_never_touch_the_webhook(): void + { + // The webhook has its own tab and save; webhook fields sent along with the + // notification preferences are ignored. $storedWebhook = $this->webhookSetting('https://hooks.example.com/abc', true); $saved = []; $editOwnCalls = 0; $settings = $this->settingsStore(['usersettings.1.webhook' => $storedWebhook], $saved); $this->makeService($this->profileRepo($editOwnCalls), ['settingsService' => $settings]) - ->saveOwnNotificationPreferences(1, $post); + ->saveOwnNotificationPreferences(1, [ + 'notifications' => '1', + 'messagesfrequency' => '60', + 'enabledEventTypes' => ['tasks'], + 'webhookUrl' => '', + 'webhookEnabled' => '0', + ]); $this->assertSame($storedWebhook, $settings->getSetting('usersettings.1.webhook'), 'The stored webhook must be left exactly as it was'); $this->assertArrayNotHasKey('usersettings.1.webhook', $saved, 'Nothing may be written to the webhook setting'); @@ -588,22 +600,11 @@ public function test_omitting_the_webhook_url_leaves_the_stored_webhook_unchange $this->assertSame(json_encode(['tasks']), $saved['usersettings.1.notificationEventTypes']); } - public static function payloadWithoutWebhookUrlProvider(): array - { - $olderPreferences = ['notifications' => '1', 'messagesfrequency' => '60', 'enabledEventTypes' => ['tasks']]; - - return [ - 'older preferences only' => [$olderPreferences], - 'opt-in without a url' => [$olderPreferences + ['webhookEnabled' => '0']], - ]; - } - public function test_a_failed_webhook_disable_is_not_reported_as_saved(): void { // The user unchecks the box, but the write reports false and the store still holds // the enabled webhook — delivery would keep going, so this must not look saved. $saved = []; - $editOwnCalls = 0; $logged = []; Log::shouldReceive('error')->andReturnUsing(function ($message, $context = []) use (&$logged) { $logged[] = $message.' '.json_encode($context); @@ -615,15 +616,13 @@ public function test_a_failed_webhook_disable_is_not_reported_as_saved(): void ); try { - $this->makeService($this->profileRepo($editOwnCalls), ['settingsService' => $settings]) - ->saveOwnNotificationPreferences(1, ['notifications' => '1', 'webhookUrl' => 'https://hooks.example.com/secret-token']); + $this->makeService($this->profileRepo(), ['settingsService' => $settings]) + ->saveOwnWebhookSettings(1, ['webhookUrl' => 'https://hooks.example.com/secret-token']); $this->fail('A webhook setting that did not persist must throw'); } catch (WebhookSettingNotSavedException $e) { $this->assertStringNotContainsString('secret-token', $e->getMessage()); } - $this->assertSame(['usersettings.1.webhook'], array_keys($saved), 'No other preference may be written after the webhook failed'); - $this->assertSame(0, $editOwnCalls); $this->assertCount(1, $logged); $this->assertStringNotContainsString('secret-token', $logged[0], 'The webhook URL must never reach the logs'); } @@ -631,7 +630,6 @@ public function test_a_failed_webhook_disable_is_not_reported_as_saved(): void public function test_a_webhook_write_that_throws_is_reported_without_the_url(): void { $saved = []; - $editOwnCalls = 0; $logged = []; Log::shouldReceive('error')->andReturnUsing(function ($message, $context = []) use (&$logged) { $logged[] = $message.' '.json_encode($context); @@ -642,16 +640,14 @@ public function test_a_webhook_write_that_throws_is_reported_without_the_url(): }); try { - $this->makeService($this->profileRepo($editOwnCalls), ['settingsService' => $settings]) - ->saveOwnNotificationPreferences(1, ['webhookUrl' => 'https://hooks.example.com/secret-token', 'webhookEnabled' => '1']); + $this->makeService($this->profileRepo(), ['settingsService' => $settings]) + ->saveOwnWebhookSettings(1, ['webhookUrl' => 'https://hooks.example.com/secret-token', 'webhookEnabled' => '1']); $this->fail('A webhook setting that could not be written must throw'); } catch (WebhookSettingNotSavedException $e) { $this->assertNull($e->getPrevious(), 'The DB exception (and the URL in its message) must not be chained'); $this->assertStringNotContainsString('secret-token', $e->getMessage()); } - $this->assertSame(['usersettings.1.webhook'], array_keys($saved)); - $this->assertSame(0, $editOwnCalls); $this->assertCount(1, $logged); $this->assertStringContainsString('RuntimeException', $logged[0]); $this->assertStringNotContainsString('secret-token', $logged[0], 'The webhook URL must never reach the logs'); @@ -661,18 +657,16 @@ public function test_an_unchanged_webhook_counts_as_saved_when_the_write_reports { // updateOrInsert returns false when the row already holds the identical value. $saved = []; - $editOwnCalls = 0; $settings = $this->settingsStore( ['usersettings.1.webhook' => $this->webhookSetting('https://hooks.example.com/abc', true)], $saved, fn () => false, ); - $this->makeService($this->profileRepo($editOwnCalls), ['settingsService' => $settings]) - ->saveOwnNotificationPreferences(1, ['webhookUrl' => 'https://hooks.example.com/abc', 'webhookEnabled' => '1', 'messagesfrequency' => '60']); + $this->makeService($this->profileRepo(), ['settingsService' => $settings]) + ->saveOwnWebhookSettings(1, ['webhookUrl' => 'https://hooks.example.com/abc', 'webhookEnabled' => '1']); - $this->assertSame(1, $editOwnCalls, 'The remaining preferences are still saved'); - $this->assertSame(60, $saved['usersettings.1.messageFrequency']); + $this->assertSame(['usersettings.1.webhook'], array_keys($saved), 'No exception: the unchanged webhook counts as saved'); } /** @@ -681,14 +675,10 @@ public function test_an_unchanged_webhook_counts_as_saved_when_the_write_reports public function test_an_invalid_webhook_url_is_rejected_before_anything_is_saved(mixed $webhookUrl): void { $saved = []; - $editOwnCalls = 0; - $service = $this->makeService($this->profileRepo($editOwnCalls), ['settingsService' => $this->settingsStore([], $saved)]); + $service = $this->makeService($this->profileRepo(), ['settingsService' => $this->settingsStore([], $saved)]); try { - $service->saveOwnNotificationPreferences(1, [ - 'notifications' => '1', - 'messagesfrequency' => '60', - 'enabledEventTypes' => ['tasks'], + $service->saveOwnWebhookSettings(1, [ 'webhookUrl' => $webhookUrl, 'webhookEnabled' => '1', ]); @@ -697,8 +687,7 @@ public function test_an_invalid_webhook_url_is_rejected_before_anything_is_saved $this->assertSame(['webhookUrl' => ['notification.invalid_webhook_url']], $e->getErrorData()); } - $this->assertSame([], $saved, 'No preference may be written when the webhook URL is rejected'); - $this->assertSame(0, $editOwnCalls, 'The notifications flag must not be written either'); + $this->assertSame([], $saved, 'Nothing may be written when the webhook URL is rejected'); } public static function invalidWebhookUrlProvider(): array @@ -711,7 +700,6 @@ public static function invalidWebhookUrlProvider(): array 'not a url' => ['hooks.example.com/abc'], 'too long' => ['https://hooks.example.com/'.str_repeat('a', 2048)], 'array instead of string' => [['https://hooks.example.com/abc']], - 'explicit null' => [null], ]; }