1
0
mirror of https://github.com/pocket-id/pocket-id.git synced 2026-03-22 19:05:08 +00:00

Merge branch 'main' into chore/depot

This commit is contained in:
Kyle Mendell
2026-03-08 14:52:02 -05:00
committed by GitHub
5 changed files with 81 additions and 24 deletions

View File

@@ -2,7 +2,9 @@ package controller
import (
"net/http"
"slices"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
@@ -34,6 +36,7 @@ func NewAppImagesController(
group.PUT("/application-images/favicon", authMiddleware.Add(), controller.updateFaviconHandler)
group.PUT("/application-images/default-profile-picture", authMiddleware.Add(), controller.updateDefaultProfilePicture)
group.DELETE("/application-images/background", authMiddleware.Add(), controller.deleteBackgroundImageHandler)
group.DELETE("/application-images/default-profile-picture", authMiddleware.Add(), controller.deleteDefaultProfilePicture)
}
@@ -192,12 +195,27 @@ func (c *AppImagesController) updateBackgroundImageHandler(ctx *gin.Context) {
ctx.Status(http.StatusNoContent)
}
// deleteBackgroundImageHandler godoc
// @Summary Delete background image
// @Description Delete the application background image
// @Tags Application Images
// @Success 204 "No Content"
// @Router /api/application-images/background [delete]
func (c *AppImagesController) deleteBackgroundImageHandler(ctx *gin.Context) {
if err := c.appImagesService.DeleteImage(ctx.Request.Context(), "background"); err != nil {
_ = ctx.Error(err)
return
}
ctx.Status(http.StatusNoContent)
}
// updateFaviconHandler godoc
// @Summary Update favicon
// @Description Update the application favicon
// @Tags Application Images
// @Accept multipart/form-data
// @Param file formData file true "Favicon file (.ico)"
// @Param file formData file true "Favicon file (.svg/.png/.ico)"
// @Success 204 "No Content"
// @Router /api/application-images/favicon [put]
func (c *AppImagesController) updateFaviconHandler(ctx *gin.Context) {
@@ -208,8 +226,9 @@ func (c *AppImagesController) updateFaviconHandler(ctx *gin.Context) {
}
fileType := utils.GetFileExtension(file.Filename)
if fileType != "ico" {
_ = ctx.Error(&common.WrongFileTypeError{ExpectedFileType: ".ico"})
mimeType := utils.GetImageMimeType(strings.ToLower(fileType))
if !slices.Contains([]string{"image/svg+xml", "image/png", "image/x-icon"}, mimeType) {
_ = ctx.Error(&common.WrongFileTypeError{ExpectedFileType: ".svg or .png or .ico"})
return
}

View File

@@ -1,3 +1,9 @@
<script module lang="ts">
// Persist the last failing background image URL across route remounts so
// login pages without a background do not briefly retry the image and stutter.
let persistedMissingBackgroundImageUrl: string | undefined;
</script>
<script lang="ts">
import { afterNavigate } from '$app/navigation';
import { page } from '$app/state';
@@ -17,13 +23,32 @@
showAlternativeSignInMethodButton?: boolean;
} = $props();
let missingBackgroundImageUrl = $state<string | undefined>(persistedMissingBackgroundImageUrl);
let loadedBackgroundImageUrl = $state<string | undefined>();
let isInitialLoad = $state(false);
let animate = $derived(isInitialLoad && !$appConfigStore.disableAnimations);
let backgroundImageUrl = $derived(cachedBackgroundImage.getUrl());
let imageError = $derived(missingBackgroundImageUrl === backgroundImageUrl);
let imageLoaded = $derived(loadedBackgroundImageUrl === backgroundImageUrl);
let animate = $derived(isInitialLoad && imageLoaded && !$appConfigStore.disableAnimations);
afterNavigate((e) => {
isInitialLoad = !e?.from?.url;
});
function onBackgroundImageLoad() {
loadedBackgroundImageUrl = backgroundImageUrl;
if (persistedMissingBackgroundImageUrl === backgroundImageUrl) {
persistedMissingBackgroundImageUrl = undefined;
missingBackgroundImageUrl = undefined;
}
}
function onBackgroundImageError() {
loadedBackgroundImageUrl = undefined;
persistedMissingBackgroundImageUrl = backgroundImageUrl;
missingBackgroundImageUrl = backgroundImageUrl;
}
const isDesktop = new MediaQuery('min-width: 1024px');
let alternativeSignInButton = $state({
href: '/login/alternative',
@@ -46,9 +71,9 @@
</script>
{#if isDesktop.current}
<div class="h-screen items-center overflow-hidden text-center">
<div class="h-screen items-center overflow-hidden text-center flex justify-center">
<div
class="relative z-10 flex h-full w-[650px] 2xl:w-[800px] p-16 {cn(
class="flex h-full w-[650px] 2xl:w-[800px] p-16 {cn(
showAlternativeSignInMethodButton && 'pb-0'
)}"
>
@@ -69,16 +94,18 @@
</div>
</div>
<!-- Background image -->
<div class="absolute top-0 right-0 left-500px bottom-0 z-0 overflow-hidden rounded-[40px] m-6">
<img
src={cachedBackgroundImage.getUrl()}
class="{cn(
animate && 'animate-bg-zoom'
)} h-screen object-cover w-[calc(100vw-650px)] 2xl:w-[calc(100vw-800px)]"
alt={m.login_background()}
/>
</div>
{#if !imageError}
<!-- Background image -->
<div class="m-6 flex h-[calc(100vh-3rem)] overflow-hidden rounded-[40px]">
<img
src={backgroundImageUrl}
class="h-full object-cover {cn(animate && 'animate-bg-zoom')}"
alt={m.login_background()}
onload={onBackgroundImageLoad}
onerror={onBackgroundImageError}
/>
</div>
{/if}
</div>
{:else}
<div

View File

@@ -71,6 +71,11 @@ export default class AppConfigService extends APIService {
cachedBackgroundImage.bustCache();
};
deleteBackgroundImage = async () => {
await this.api.delete(`/application-images/background`);
cachedBackgroundImage.bustCache();
};
deleteDefaultProfilePicture = async () => {
await this.api.delete('/application-images/default-profile-picture');
cachedDefaultProfilePicture.bustCache();

View File

@@ -44,7 +44,7 @@
logoDark: File | undefined,
logoEmail: File | undefined,
defaultProfilePicture: File | null | undefined,
backgroundImage: File | undefined,
backgroundImage: File | null | undefined,
favicon: File | undefined
) {
const faviconPromise = favicon ? appConfigService.updateFavicon(favicon) : Promise.resolve();
@@ -68,9 +68,12 @@
? appConfigService.updateDefaultProfilePicture(defaultProfilePicture)
: Promise.resolve();
const backgroundImagePromise = backgroundImage
? appConfigService.updateBackgroundImage(backgroundImage)
: Promise.resolve();
const backgroundImagePromise =
backgroundImage === null
? appConfigService.deleteBackgroundImage()
: backgroundImage
? appConfigService.updateBackgroundImage(backgroundImage)
: Promise.resolve();
await Promise.all([
lightLogoPromise,

View File

@@ -17,7 +17,7 @@
logoDark: File | undefined,
logoEmail: File | undefined,
defaultProfilePicture: File | null | undefined,
backgroundImage: File | undefined,
backgroundImage: File | null | undefined,
favicon: File | undefined
) => void;
} = $props();
@@ -26,10 +26,11 @@
let logoDark = $state<File | undefined>();
let logoEmail = $state<File | undefined>();
let defaultProfilePicture = $state<File | null | undefined>();
let backgroundImage = $state<File | undefined>();
let backgroundImage = $state<File | null | undefined>();
let favicon = $state<File | undefined>();
let defaultProfilePictureSet = $state(true);
let backgroundImageSet = $state(true);
</script>
<div class="flex flex-col gap-8">
@@ -39,7 +40,7 @@
label={m.favicon()}
bind:image={favicon}
imageURL="/api/application-images/favicon"
accept="image/x-icon"
accept="image/svg+xml, image/png, image/x-icon"
/>
<ApplicationImage
id="logo-light"
@@ -77,10 +78,12 @@
/>
<ApplicationImage
id="background-image"
imageClass="h-[350px] max-w-[500px]"
imageClass="max-h-[350px] max-w-[500px]"
label={m.background_image()}
isResetable
bind:image={backgroundImage}
imageURL={cachedBackgroundImage.getUrl()}
isImageSet={backgroundImageSet}
/>
</div>
<div class="flex justify-end">