merge: remote-tracking branch 'origin/main' into feat/integrity-checks-izzy

This commit is contained in:
izzy
2026-01-14 12:25:28 +00:00
89 changed files with 13815 additions and 4655 deletions
+1 -1
View File
@@ -1 +1 @@
24.12.0 24.13.0
+1 -1
View File
@@ -1 +1 @@
24.12.0 24.13.0
+1 -1
View File
@@ -69,6 +69,6 @@
"micromatch": "^4.0.8" "micromatch": "^4.0.8"
}, },
"volta": { "volta": {
"node": "24.12.0" "node": "24.13.0"
} }
} }
+1 -1
View File
@@ -85,7 +85,7 @@ services:
container_name: immich_prometheus container_name: immich_prometheus
ports: ports:
- 9090:9090 - 9090:9090
image: prom/prometheus@sha256:2b6f734e372c1b4717008f7d0a0152316aedd4d13ae17ef1e3268dbfaf68041b image: prom/prometheus@sha256:1f0f50f06acaceb0f5670d2c8a658a599affe7b0d8e78b898c1035653849a702
volumes: volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml - ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus - prometheus-data:/prometheus
+1 -1
View File
@@ -1 +1 @@
24.12.0 24.13.0
+6
View File
@@ -26,6 +26,12 @@ const config = {
locales: ['en'], locales: ['en'],
}, },
// Mermaid diagrams
markdown: {
mermaid: true,
},
themes: ['@docusaurus/theme-mermaid'],
plugins: [ plugins: [
async function myPlugin(context, options) { async function myPlugin(context, options) {
return { return {
+2 -1
View File
@@ -20,6 +20,7 @@
"@docusaurus/core": "~3.9.0", "@docusaurus/core": "~3.9.0",
"@docusaurus/preset-classic": "~3.9.0", "@docusaurus/preset-classic": "~3.9.0",
"@docusaurus/theme-common": "~3.9.0", "@docusaurus/theme-common": "~3.9.0",
"@docusaurus/theme-mermaid": "~3.9.0",
"@mdi/js": "^7.3.67", "@mdi/js": "^7.3.67",
"@mdi/react": "^1.6.1", "@mdi/react": "^1.6.1",
"@mdx-js/react": "^3.0.0", "@mdx-js/react": "^3.0.0",
@@ -57,6 +58,6 @@
"node": ">=20" "node": ">=20"
}, },
"volta": { "volta": {
"node": "24.12.0" "node": "24.13.0"
} }
} }
+30 -11
View File
@@ -8,19 +8,19 @@
@tailwind utilities; @tailwind utilities;
@font-face { @font-face {
font-family: 'Overpass'; font-family: 'GoogleSans';
src: url('/fonts/overpass/Overpass.ttf') format('truetype-variations'); src: url('/fonts/GoogleSans/GoogleSans.ttf') format('truetype-variations');
font-weight: 1 999; font-weight: 410 900;
font-style: normal; font-style: normal;
ascent-override: 106.25%; ascent-override: 106.25%;
size-adjust: 106.25%; size-adjust: 106.25%;
} }
@font-face { @font-face {
font-family: 'Overpass Mono'; font-family: 'GoogleSansCode';
src: url('/fonts/overpass/OverpassMono.ttf') format('truetype-variations'); src: url('/fonts/GoogleSansCode/GoogleSansCode.ttf') format('truetype-variations');
font-weight: 1 999; font-weight: 1 900;
font-style: normal; font-style: monospace;
ascent-override: 106.25%; ascent-override: 106.25%;
size-adjust: 106.25%; size-adjust: 106.25%;
} }
@@ -37,7 +37,8 @@ img {
/* You can override the default Infima variables here. */ /* You can override the default Infima variables here. */
:root { :root {
font-family: 'Overpass', sans-serif; font-family: 'GoogleSans', sans-serif;
letter-spacing: 0.1px;
--ifm-color-primary: #4250af; --ifm-color-primary: #4250af;
--ifm-color-primary-dark: #4250af; --ifm-color-primary-dark: #4250af;
--ifm-color-primary-darker: #4250af; --ifm-color-primary-darker: #4250af;
@@ -48,6 +49,16 @@ img {
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
} }
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: 'GoogleSans', sans-serif;
letter-spacing: 0.1px;
}
/* For readability concerns, you should choose a lighter palette in dark mode. */ /* For readability concerns, you should choose a lighter palette in dark mode. */
[data-theme='dark'] { [data-theme='dark'] {
--ifm-color-primary: #adcbfa; --ifm-color-primary: #adcbfa;
@@ -71,15 +82,22 @@ div[class^='announcementBar_'] {
padding: 10px 10px 10px 16px; padding: 10px 10px 10px 16px;
border-radius: 24px; border-radius: 24px;
margin-right: 16px; margin-right: 16px;
font-weight: 500;
} }
.menu__list-item-collapsible { .menu__list-item-collapsible {
margin-right: 16px; margin-right: 16px;
border-radius: 24px; border-radius: 24px;
font-weight: 500;
} }
.menu__link--active { .menu__link--active {
font-weight: 500; font-weight: 600;
}
.table-of-contents__link {
font-size: 14px;
font-weight: 450;
} }
/* workaround for version switcher PR 15894 */ /* workaround for version switcher PR 15894 */
@@ -88,13 +106,14 @@ div[class*='navbar__items'] > li:has(a[class*='version-switcher-34ab39']) {
} }
code { code {
font-weight: 600; font-weight: 500;
font-family: 'GoogleSansCode';
} }
.buy-button { .buy-button {
padding: 8px 14px; padding: 8px 14px;
border: 1px solid transparent; border: 1px solid transparent;
font-family: 'Overpass', sans-serif; font-family: 'GoogleSans', sans-serif;
font-weight: 500; font-weight: 500;
cursor: pointer; cursor: pointer;
box-shadow: 0 0 5px 2px rgba(181, 206, 254, 0.4); box-shadow: 0 0 5px 2px rgba(181, 206, 254, 0.4);
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
24.12.0 24.13.0
+1 -1
View File
@@ -52,6 +52,6 @@
"vitest": "^3.0.0" "vitest": "^3.0.0"
}, },
"volta": { "volta": {
"node": "24.12.0" "node": "24.13.0"
} }
} }
+1 -1
View File
@@ -1,7 +1,7 @@
experimental_monorepo_root = true experimental_monorepo_root = true
[tools] [tools]
node = "24.12.0" node = "24.13.0"
flutter = "3.35.7" flutter = "3.35.7"
pnpm = "10.27.0" pnpm = "10.27.0"
terragrunt = "0.93.10" terragrunt = "0.93.10"
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -51,4 +51,4 @@ const Map<String, Locale> locales = {
const String translationsPath = 'assets/i18n'; const String translationsPath = 'assets/i18n';
const List<Locale> localesNotSupportedByOverpass = [Locale('el', 'GR'), Locale('sr', 'Cyrl')]; const List<Locale> localesNotSupportedByAppFont = [Locale('el', 'GR'), Locale('sr', 'Cyrl')];
+1 -1
View File
@@ -100,7 +100,7 @@ class AppLogPage extends HookConsumerWidget {
minLeadingWidth: 10, minLeadingWidth: 10,
title: Text( title: Text(
truncateLogMessage(logMessage.message, 4), truncateLogMessage(logMessage.message, 4),
style: TextStyle(fontSize: 14.0, color: context.colorScheme.onSurface, fontFamily: "Inconsolata"), style: TextStyle(fontSize: 14.0, color: context.colorScheme.onSurface, fontFamily: "GoogleSansCode"),
), ),
subtitle: Text( subtitle: Text(
"at ${DateFormat("HH:mm:ss.SSS").format(logMessage.createdAt)} in ${logMessage.logger}", "at ${DateFormat("HH:mm:ss.SSS").format(logMessage.createdAt)} in ${logMessage.logger}",
@@ -57,7 +57,7 @@ class AppLogDetailPage extends HookConsumerWidget {
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: SelectableText( child: SelectableText(
text, text,
style: const TextStyle(fontSize: 12.0, fontWeight: FontWeight.bold, fontFamily: "Inconsolata"), style: const TextStyle(fontSize: 12.0, fontWeight: FontWeight.bold, fontFamily: "GoogleSansCode"),
), ),
), ),
), ),
@@ -88,7 +88,7 @@ class AppLogDetailPage extends HookConsumerWidget {
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: SelectableText( child: SelectableText(
logger.toString(), logger.toString(),
style: const TextStyle(fontSize: 12.0, fontWeight: FontWeight.bold, fontFamily: "Inconsolata"), style: const TextStyle(fontSize: 12.0, fontWeight: FontWeight.bold, fontFamily: "GoogleSansCode"),
), ),
), ),
), ),
@@ -234,7 +234,7 @@ class FolderPath extends StatelessWidget {
Text( Text(
currentFolder.path, currentFolder.path,
style: TextStyle( style: TextStyle(
fontFamily: 'Inconsolata', fontFamily: 'GoogleSansCode',
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 14, fontSize: 14,
color: context.colorScheme.onSurface.withAlpha(175), color: context.colorScheme.onSurface.withAlpha(175),
+2 -2
View File
@@ -41,7 +41,7 @@ class LoginPage extends HookConsumerWidget {
style: TextStyle( style: TextStyle(
color: context.colorScheme.onSurfaceSecondary, color: context.colorScheme.onSurfaceSecondary,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontFamily: "Inconsolata", fontFamily: "GoogleSansCode",
), ),
), ),
const Text(' '), const Text(' '),
@@ -51,7 +51,7 @@ class LoginPage extends HookConsumerWidget {
style: TextStyle( style: TextStyle(
color: context.primaryColor, color: context.primaryColor,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontFamily: "Inconsolata", fontFamily: "GoogleSansCode",
), ),
), ),
onTap: () { onTap: () {
@@ -527,7 +527,9 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
void _onScaleStateChanged(PhotoViewScaleState scaleState) { void _onScaleStateChanged(PhotoViewScaleState scaleState) {
if (scaleState != PhotoViewScaleState.initial) { if (scaleState != PhotoViewScaleState.initial) {
ref.read(assetViewerProvider.notifier).setControls(false); if (!dragInProgress) {
ref.read(assetViewerProvider.notifier).setControls(false);
}
ref.read(videoPlayerControlsProvider.notifier).pause(); ref.read(videoPlayerControlsProvider.notifier).pause();
return; return;
} }
@@ -450,7 +450,7 @@ class _SegmentWidget extends StatelessWidget {
alignment: Alignment.center, alignment: Alignment.center,
child: Text( child: Text(
_segment.date.year.toString(), _segment.date.year.toString(),
style: context.textTheme.labelMedium?.copyWith(fontFamily: "OverpassMono", fontWeight: FontWeight.w600), style: context.textTheme.labelMedium?.copyWith(fontFamily: "GoogleSansCode", fontWeight: FontWeight.w600),
), ),
), ),
), ),
+2 -2
View File
@@ -147,9 +147,9 @@ ImmichTheme decolorizeSurfaces({required ImmichTheme theme}) {
} }
String? _getFontFamilyFromLocale(Locale locale) { String? _getFontFamilyFromLocale(Locale locale) {
if (localesNotSupportedByOverpass.contains(locale)) { if (localesNotSupportedByAppFont.contains(locale)) {
// Let Flutter use the default font // Let Flutter use the default font
return null; return null;
} }
return 'Overpass'; return 'GoogleSans';
} }
@@ -58,7 +58,7 @@ class AdvancedBottomSheet extends HookConsumerWidget {
style: const TextStyle( style: const TextStyle(
fontSize: 12.0, fontSize: 12.0,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontFamily: "Inconsolata", fontFamily: "GoogleSansCode",
), ),
showCursor: true, showCursor: true,
), ),
@@ -36,7 +36,7 @@ class BackupUploadProgressBar extends ConsumerWidget {
), ),
Text( Text(
" ${uploadProgress.toStringAsFixed(0)}%", " ${uploadProgress.toStringAsFixed(0)}%",
style: const TextStyle(fontSize: 12, fontFamily: "OverpassMono"), style: const TextStyle(fontSize: 12, fontFamily: "GoogleSansCode"),
), ),
], ],
), ),
+2 -2
View File
@@ -26,10 +26,10 @@ class BackupUploadStats extends ConsumerWidget {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text(uploadFileProgress, style: const TextStyle(fontSize: 10, fontFamily: "OverpassMono")), Text(uploadFileProgress, style: const TextStyle(fontSize: 10, fontFamily: "GoogleSansCode")),
Text( Text(
_formatUploadFileSpeed(uploadFileSpeed), _formatUploadFileSpeed(uploadFileSpeed),
style: const TextStyle(fontSize: 10, fontFamily: "OverpassMono"), style: const TextStyle(fontSize: 10, fontFamily: "GoogleSansCode"),
), ),
], ],
), ),
+1 -1
View File
@@ -43,7 +43,7 @@ class PinInput extends StatelessWidget {
final defaultPinTheme = PinTheme( final defaultPinTheme = PinTheme(
width: getPinSize().width, width: getPinSize().width,
height: getPinSize().height, height: getPinSize().height,
textStyle: TextStyle(fontSize: 24, color: context.colorScheme.onSurface, fontFamily: 'Overpass Mono'), textStyle: TextStyle(fontSize: 24, color: context.colorScheme.onSurface, fontFamily: 'GoogleSansCode'),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(19)), borderRadius: const BorderRadius.all(Radius.circular(19)),
border: Border.all(color: context.colorScheme.surfaceBright), border: Border.all(color: context.colorScheme.surfaceBright),
@@ -50,7 +50,7 @@ class EntityCountTile extends StatelessWidget {
const Spacer(), const Spacer(),
RichText( RichText(
text: TextSpan( text: TextSpan(
style: const TextStyle(fontSize: 18, fontFamily: 'OverpassMono', fontWeight: FontWeight.w600), style: const TextStyle(fontSize: 18, fontFamily: 'GoogleSansCode', fontWeight: FontWeight.w600),
children: [ children: [
TextSpan( TextSpan(
text: zeroPadding(count, maxDigits), text: zeroPadding(count, maxDigits),
@@ -117,7 +117,7 @@ class EndpointInputState extends ConsumerState<EndpointInput> {
autovalidateMode: AutovalidateMode.onUserInteraction, autovalidateMode: AutovalidateMode.onUserInteraction,
validator: validateUrl, validator: validateUrl,
keyboardType: TextInputType.url, keyboardType: TextInputType.url,
style: const TextStyle(fontFamily: 'Inconsolata', fontWeight: FontWeight.w600, fontSize: 14), style: const TextStyle(fontFamily: 'GoogleSansCode', fontWeight: FontWeight.w600, fontSize: 14),
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'http(s)://immich.domain.com', hintText: 'http(s)://immich.domain.com',
contentPadding: const EdgeInsets.all(16), contentPadding: const EdgeInsets.all(16),
@@ -155,7 +155,7 @@ class LocalNetworkPreference extends HookConsumerWidget {
style: context.textTheme.labelLarge?.copyWith( style: context.textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: enabled ? context.primaryColor : context.colorScheme.onSurface.withAlpha(100), color: enabled ? context.primaryColor : context.colorScheme.onSurface.withAlpha(100),
fontFamily: 'Inconsolata', fontFamily: 'GoogleSansCode',
), ),
), ),
trailing: IconButton( trailing: IconButton(
@@ -175,7 +175,7 @@ class LocalNetworkPreference extends HookConsumerWidget {
style: context.textTheme.labelLarge?.copyWith( style: context.textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: enabled ? context.primaryColor : context.colorScheme.onSurface.withAlpha(100), color: enabled ? context.primaryColor : context.colorScheme.onSurface.withAlpha(100),
fontFamily: 'Inconsolata', fontFamily: 'GoogleSansCode',
), ),
), ),
trailing: IconButton( trailing: IconButton(
@@ -110,7 +110,7 @@ class NetworkingSettings extends HookConsumerWidget {
currentEndpoint ?? "--", currentEndpoint ?? "--",
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontFamily: 'Inconsolata', fontFamily: 'GoogleSansCode',
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: context.primaryColor, color: context.primaryColor,
), ),
+13 -11
View File
@@ -127,24 +127,26 @@ flutter:
assets: assets:
- assets/ - assets/
fonts: fonts:
- family: Inconsolata - family: GoogleSans
fonts: fonts:
- asset: fonts/Inconsolata-Regular.ttf - asset: fonts/GoogleSans/GoogleSans-Regular.ttf
- family: Overpass
fonts:
- asset: fonts/overpass/Overpass-Regular.ttf
weight: 400 weight: 400
- asset: fonts/overpass/Overpass-Italic.ttf - asset: fonts/GoogleSans/GoogleSans-Italic.ttf
style: italic style: italic
- asset: fonts/overpass/Overpass-Medium.ttf - asset: fonts/GoogleSans/GoogleSans-Medium.ttf
weight: 500 weight: 500
- asset: fonts/overpass/Overpass-SemiBold.ttf - asset: fonts/GoogleSans/GoogleSans-SemiBold.ttf
weight: 600 weight: 600
- asset: fonts/overpass/Overpass-Bold.ttf - asset: fonts/GoogleSans/GoogleSans-Bold.ttf
weight: 700 weight: 700
- family: OverpassMono - family: GoogleSansCode
fonts: fonts:
- asset: fonts/overpass/OverpassMono.ttf - asset: fonts/GoogleSansCode/GoogleSansCode-Regular.ttf
weight: 400
- asset: fonts/GoogleSansCode/GoogleSansCode-Medium.ttf
weight: 500
- asset: fonts/GoogleSansCode/GoogleSansCode-SemiBold.ttf
weight: 600
flutter_launcher_icons: flutter_launcher_icons:
image_path_android: 'assets/immich-logo.png' image_path_android: 'assets/immich-logo.png'
adaptive_icon_background: '#ffffff' adaptive_icon_background: '#ffffff'
+1 -1
View File
@@ -1 +1 @@
24.12.0 24.13.0
+1 -1
View File
@@ -28,6 +28,6 @@
"directory": "open-api/typescript-sdk" "directory": "open-api/typescript-sdk"
}, },
"volta": { "volta": {
"node": "24.12.0" "node": "24.13.0"
} }
} }
+13564 -4515
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1 +1 @@
24.12.0 24.13.0
+2 -2
View File
@@ -1,4 +1,4 @@
FROM ghcr.io/immich-app/base-server-dev:202511261514@sha256:cbcca5851fd11042463f09797e6d6068d94adbb108749e62aa69159df59c0591 AS builder FROM ghcr.io/immich-app/base-server-dev:202601131104@sha256:8d907eb3fe10dba4a1e034fd0060ea68c01854d92fcc9debc6b868b98f888ba7 AS builder
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \ ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
CI=1 \ CI=1 \
COREPACK_HOME=/tmp \ COREPACK_HOME=/tmp \
@@ -71,7 +71,7 @@ RUN --mount=type=cache,id=pnpm-plugins,target=/buildcache/pnpm-store \
--mount=type=cache,id=mise-tools-${TARGETPLATFORM},target=/buildcache/mise \ --mount=type=cache,id=mise-tools-${TARGETPLATFORM},target=/buildcache/mise \
cd plugins && mise run build cd plugins && mise run build
FROM ghcr.io/immich-app/base-server-prod:202511261514@sha256:c04c1c38dd90e53455b180aedf93c3c63474c8d20ffe2c6d7a3a61a2181e6d29 FROM ghcr.io/immich-app/base-server-prod:202601131104@sha256:c649c5838b6348836d27db6d49cadbbc6157feae7a1a237180c3dec03577ba8f
WORKDIR /usr/src/app WORKDIR /usr/src/app
ENV NODE_ENV=production \ ENV NODE_ENV=production \
+1 -1
View File
@@ -1,5 +1,5 @@
# dev build # dev build
FROM ghcr.io/immich-app/base-server-dev:202511261514@sha256:cbcca5851fd11042463f09797e6d6068d94adbb108749e62aa69159df59c0591 AS dev FROM ghcr.io/immich-app/base-server-dev:202601131104@sha256:8d907eb3fe10dba4a1e034fd0060ea68c01854d92fcc9debc6b868b98f888ba7 AS dev
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \ ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
CI=1 \ CI=1 \
+2 -2
View File
@@ -96,7 +96,7 @@
"pg": "^8.11.3", "pg": "^8.11.3",
"pg-connection-string": "^2.9.1", "pg-connection-string": "^2.9.1",
"picomatch": "^4.0.2", "picomatch": "^4.0.2",
"postgres": "3.4.7", "postgres": "3.4.8",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-email": "^4.0.0", "react-email": "^4.0.0",
@@ -167,7 +167,7 @@
"vitest": "^3.0.0" "vitest": "^3.0.0"
}, },
"volta": { "volta": {
"node": "24.12.0" "node": "24.13.0"
}, },
"overrides": { "overrides": {
"sharp": "^0.34.5" "sharp": "^0.34.5"
+1 -1
View File
@@ -1 +1 @@
24.12.0 24.13.0
+2 -2
View File
@@ -71,7 +71,7 @@
"@sveltejs/adapter-static": "^3.0.8", "@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/enhanced-img": "^0.9.0", "@sveltejs/enhanced-img": "^0.9.0",
"@sveltejs/kit": "^2.27.1", "@sveltejs/kit": "^2.27.1",
"@sveltejs/vite-plugin-svelte": "6.2.1", "@sveltejs/vite-plugin-svelte": "6.2.3",
"@tailwindcss/vite": "^4.1.7", "@tailwindcss/vite": "^4.1.7",
"@testing-library/jest-dom": "^6.4.2", "@testing-library/jest-dom": "^6.4.2",
"@testing-library/svelte": "^5.2.8", "@testing-library/svelte": "^5.2.8",
@@ -107,6 +107,6 @@
"vitest": "^3.0.0" "vitest": "^3.0.0"
}, },
"volta": { "volta": {
"node": "24.12.0" "node": "24.13.0"
} }
} }
+10 -10
View File
@@ -49,7 +49,7 @@
} }
@theme { @theme {
--font-immich-mono: Overpass Mono, monospace; --font-immich-mono: GoogleSansCode, monospace;
--spacing-18: 4.5rem; --spacing-18: 4.5rem;
@@ -84,25 +84,25 @@
@layer utilities { @layer utilities {
@font-face { @font-face {
font-family: 'Overpass'; font-family: 'GoogleSans';
src: url('$lib/assets/fonts/overpass/Overpass.ttf') format('truetype-variations'); src: url('$lib/assets/fonts/GoogleSans/GoogleSans.ttf') format('truetype-variations');
font-weight: 1 999; font-weight: 410 900;
font-style: normal; font-style: normal;
ascent-override: 106.25%; ascent-override: 106.25%;
size-adjust: 106.25%; size-adjust: 106.25%;
} }
@font-face { @font-face {
font-family: 'Overpass Mono'; font-family: 'GoogleSansCode';
src: url('$lib/assets/fonts/overpass/OverpassMono.ttf') format('truetype-variations'); src: url('$lib/assets/fonts/GoogleSansCode/GoogleSansCode.ttf') format('truetype-variations');
font-weight: 1 999; font-weight: 1 900;
font-style: monospace; font-style: monospace;
ascent-override: 106.25%;
size-adjust: 106.25%;
} }
:root { :root {
font-family: 'Overpass', sans-serif; font-family: 'GoogleSans', sans-serif;
letter-spacing: 0.1px;
/* Used by layouts to ensure proper spacing between navbar and content */ /* Used by layouts to ensure proper spacing between navbar and content */
--navbar-height: calc(4.5rem + 4px); --navbar-height: calc(4.5rem + 4px);
--navbar-height-md: calc(4.5rem + 4px - 14px); --navbar-height-md: calc(4.5rem + 4px - 14px);
+3 -3
View File
@@ -1,12 +1,12 @@
import overpass from '$lib/assets/fonts/overpass/Overpass.ttf?url'; import GoogleSans from '$lib/assets/fonts/GoogleSans/GoogleSans.ttf?url';
import overpassMono from '$lib/assets/fonts/overpass/OverpassMono.ttf?url'; import GoogleSansCode from '$lib/assets/fonts/GoogleSansCode/GoogleSansCode.ttf?url';
import type { Handle } from '@sveltejs/kit'; import type { Handle } from '@sveltejs/kit';
// only used during the build to replace the variables from app.html // only used during the build to replace the variables from app.html
export const handle = (async ({ event, resolve }) => { export const handle = (async ({ event, resolve }) => {
return resolve(event, { return resolve(event, {
transformPageChunk: ({ html }) => { transformPageChunk: ({ html }) => {
return html.replace('%app.font%', overpass).replace('%app.monofont%', overpassMono); return html.replace('%app.font%', GoogleSans).replace('%app.monofont%', GoogleSansCode);
}, },
}); });
}) satisfies Handle; }) satisfies Handle;
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -31,7 +31,7 @@
{#if isOwned} {#if isOwned}
<Textarea <Textarea
bind:value={description} bind:value={description}
class="outline-none border-b border-gray-500 bg-transparent ring-0 focus:ring-0 resize-none focus:border-b-2 focus:border-immich-primary dark:focus:border-immich-dark-primary dark:bg-transparent" class="outline-none border-b max-h-32 border-transparent pl-0 bg-transparent ring-0 focus:ring-0 resize-none focus:border-b-2 focus:border-immich-primary dark:focus:border-immich-dark-primary dark:bg-transparent"
rows={1} rows={1}
grow grow
shape="rectangle" shape="rectangle"
@@ -44,7 +44,7 @@
}))} }))}
/> />
{:else if description} {:else if description}
<p class="break-words whitespace-pre-line w-full text-black dark:text-white text-base"> <p class="wrap-break-words whitespace-pre-line w-full text-black dark:text-white text-base">
{description} {description}
</p> </p>
{/if} {/if}
@@ -138,7 +138,11 @@
const normalizedSearchQuery = $derived(normalizeSearchString(searchQuery)); const normalizedSearchQuery = $derived(normalizeSearchString(searchQuery));
let filteredAlbums = $derived( let filteredAlbums = $derived(
normalizedSearchQuery normalizedSearchQuery
? albums.filter(({ albumName }) => normalizeSearchString(albumName).includes(normalizedSearchQuery)) ? albums.filter(
({ albumName, description }) =>
normalizeSearchString(albumName).includes(normalizedSearchQuery) ||
normalizeSearchString(description).includes(normalizedSearchQuery),
)
: albums, : albums,
); );
@@ -566,7 +566,7 @@
<div <div
transition:fly={{ duration: 150 }} transition:fly={{ duration: 150 }}
id="detail-panel" id="detail-panel"
class="row-start-1 row-span-4 w-[360px] overflow-y-auto transition-all dark:border-l dark:border-s-immich-dark-gray bg-light" class="row-start-1 row-span-4 w-90 overflow-y-auto transition-all dark:border-l dark:border-s-immich-dark-gray bg-light"
translate="yes" translate="yes"
> >
<DetailPanel {asset} currentAlbum={album} albums={appearsInAlbums} /> <DetailPanel {asset} currentAlbum={album} albums={appearsInAlbums} />
@@ -577,7 +577,7 @@
<div <div
transition:fly={{ duration: 150 }} transition:fly={{ duration: 150 }}
id="editor-panel" id="editor-panel"
class="row-start-1 row-span-4 w-[400px] overflow-y-auto transition-all dark:border-l dark:border-s-immich-dark-gray" class="row-start-1 row-span-4 w-100 overflow-y-auto transition-all dark:border-l dark:border-s-immich-dark-gray"
translate="yes" translate="yes"
> >
<EditorPanel {asset} onClose={closeEditor} /> <EditorPanel {asset} onClose={closeEditor} />
@@ -624,7 +624,7 @@
<div <div
transition:fly={{ duration: 150 }} transition:fly={{ duration: 150 }}
id="activity-panel" id="activity-panel"
class="row-start-1 row-span-5 w-[360px] md:w-[460px] overflow-y-auto transition-all dark:border-l dark:border-s-immich-dark-gray" class="row-start-1 row-span-5 w-90 md:w-115 overflow-y-auto transition-all dark:border-l dark:border-s-immich-dark-gray"
translate="yes" translate="yes"
> >
<ActivityViewer <ActivityViewer
@@ -33,7 +33,7 @@
<section class="px-4 mt-10"> <section class="px-4 mt-10">
<Textarea <Textarea
bind:value={description} bind:value={description}
class="max-h-40 outline-none border-b border-gray-500 bg-transparent ring-0 focus:ring-0 resize-none focus:border-b-2 focus:border-immich-primary dark:focus:border-immich-dark-primary dark:bg-transparent" class="max-h-40 pl-0 outline-none border-b border-gray-500 bg-transparent ring-0 focus:ring-0 resize-none focus:border-b-2 focus:border-immich-primary dark:focus:border-immich-dark-primary dark:bg-transparent"
rows={1} rows={1}
grow grow
shape="rectangle" shape="rectangle"
@@ -79,7 +79,7 @@
<div class="absolute flex h-16 w-full place-items-center justify-between border-b p-2 text-dark"> <div class="absolute flex h-16 w-full place-items-center justify-between border-b p-2 text-dark">
<div class="flex gap-2 items-center"> <div class="flex gap-2 items-center">
{#if title} {#if title}
<div class="font-medium outline-none pe-8" tabindex="-1" id={headerId}>{title}</div> <div class="outline-none pe-8" tabindex="-1" id={headerId}>{title}</div>
{/if} {/if}
{#if description} {#if description}
<p class="text-sm text-gray-400 dark:text-gray-600">{description}</p> <p class="text-sm text-gray-400 dark:text-gray-600">{description}</p>
@@ -27,13 +27,13 @@
{#if icon} {#if icon}
<Icon {icon} size="40" /> <Icon {icon} size="40" />
{/if} {/if}
<Text size="large" fontWeight="bold" class="uppercase">{title}</Text> <Text size="giant" class="font-medium">{title}</Text>
</div> </div>
<div class="relative mx-auto font-mono text-2xl font-semibold"> <div class="relative mx-auto font-immich-mono text-2xl font-medium">
<span class="text-gray-400 dark:text-gray-600">{zeros()}</span><span>{value}</span> <span class="text-gray-300 dark:text-gray-600">{zeros()}</span><span>{value}</span>
{#if unit} {#if unit}
<Code color="muted" class="absolute -top-5 end-1 font-light p-0">{unit}</Code> <Code color="muted" class="font-immich-mono absolute -top-5 end-1 font-light p-0">{unit}</Code>
{/if} {/if}
</div> </div>
@@ -3,7 +3,7 @@
import { locale } from '$lib/stores/preferences.store'; import { locale } from '$lib/stores/preferences.store';
import { getByteUnitString, getBytesWithUnit } from '$lib/utils/byte-units'; import { getByteUnitString, getBytesWithUnit } from '$lib/utils/byte-units';
import type { ServerStatsResponseDto } from '@immich/sdk'; import type { ServerStatsResponseDto } from '@immich/sdk';
import { Code, Heading, Icon, Text } from '@immich/ui'; import { Code, Icon, Text } from '@immich/ui';
import { mdiCameraIris, mdiChartPie, mdiPlayCircle } from '@mdi/js'; import { mdiCameraIris, mdiChartPie, mdiPlayCircle } from '@mdi/js';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
@@ -13,8 +13,7 @@
const { stats }: Props = $props(); const { stats }: Props = $props();
const zeros = (value: number) => { const zeros = (value: number, maxLength = 13) => {
const maxLength = 13;
const valueLength = value.toString().length; const valueLength = value.toString().length;
const zeroLength = maxLength - valueLength; const zeroLength = maxLength - valueLength;
@@ -27,7 +26,7 @@
<div class="flex flex-col gap-5 my-4"> <div class="flex flex-col gap-5 my-4">
<div> <div>
<Heading size="tiny" class="mb-2">{$t('total_usage')}</Heading> <Text class="mb-2 font-medium">{$t('total_usage')}</Text>
<div class="hidden justify-between lg:flex gap-4"> <div class="hidden justify-between lg:flex gap-4">
<StatsCard icon={mdiCameraIris} title={$t('photos')} value={stats.photos} /> <StatsCard icon={mdiCameraIris} title={$t('photos')} value={stats.photos} />
@@ -40,38 +39,35 @@
<div class="flex flex-wrap gap-x-12"> <div class="flex flex-wrap gap-x-12">
<div class="flex flex-1 place-items-center gap-4 text-primary"> <div class="flex flex-1 place-items-center gap-4 text-primary">
<Icon icon={mdiCameraIris} size="25" /> <Icon icon={mdiCameraIris} size="25" />
<Text fontWeight="bold" class="uppercase">{$t('photos')}</Text> <Text class="font-medium" size="medium">{$t('photos')}</Text>
</div> </div>
<div class="relative text-center font-mono text-2xl font-semibold"> <div class="relative text-center font-immich-mono text-2xl font-medium">
<span class="text-gray-400 dark:text-gray-600">{zeros(stats.photos)}</span><span class="text-primary" <span class="text-light-300">{zeros(stats.photos)}</span><span class="text-primary">{stats.photos}</span>
>{stats.photos}</span
>
</div> </div>
</div> </div>
<div class="flex flex-wrap gap-x-12"> <div class="flex flex-wrap gap-x-12">
<div class="flex flex-1 place-items-center gap-4 text-primary"> <div class="flex flex-1 place-items-center gap-4 text-primary">
<Icon icon={mdiPlayCircle} size="25" /> <Icon icon={mdiPlayCircle} size="25" />
<Text fontWeight="bold" class="uppercase">{$t('videos')}</Text> <Text class="font-medium" size="medium">{$t('videos')}</Text>
</div> </div>
<div class="relative text-center font-mono text-2xl font-semibold"> <div class="relative text-center font-immich-mono text-2xl font-medium">
<span class="text-gray-400 dark:text-gray-600">{zeros(stats.videos)}</span><span class="text-primary" <span class="text-light-300">{zeros(stats.videos)}</span><span class="text-primary">{stats.videos}</span>
>{stats.videos}</span
>
</div> </div>
</div> </div>
<div class="flex flex-wrap gap-x-5"> <div class="flex flex-wrap gap-x-5">
<div class="flex flex-1 flex-nowrap place-items-center gap-4 text-primary"> <div class="flex flex-1 flex-nowrap place-items-center gap-4 text-primary">
<Icon icon={mdiChartPie} size="25" /> <Icon icon={mdiChartPie} size="25" />
<Text fontWeight="bold" class="uppercase">{$t('storage')}</Text> <Text class="font-medium" size="medium">{$t('storage')}</Text>
</div> </div>
<div class="relative flex text-center font-mono text-2xl font-semibold"> <div class="relative flex text-center font-immich-mono text-2xl font-medium">
<span class="text-gray-400 dark:text-gray-600">{zeros(statsUsage)}</span><span class="text-primary" <span class="text-light-300">{zeros(statsUsage)}</span><span class="text-primary">{statsUsage}</span>
>{statsUsage}</span
> <div class="absolute -right-1.5 -bottom-4">
<Code color="muted" class="font-light">{statsUsageUnit}</Code> <Code color="muted" class="text-xs font-light font-immich-mono">{statsUsageUnit}</Code>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -79,7 +75,7 @@
</div> </div>
<div> <div>
<Heading size="tiny" class="mb-2">{$t('user_usage_detail')}</Heading> <Text class="mt-6 mb-2 font-medium">{$t('user_usage_detail')}</Text>
<table class="mt-5 w-full text-start"> <table class="mt-5 w-full text-start">
<thead <thead
class="mb-4 flex h-12 w-full rounded-md border bg-gray-50 text-primary dark:border-immich-dark-gray dark:bg-immich-dark-gray" class="mb-4 flex h-12 w-full rounded-md border bg-gray-50 text-primary dark:border-immich-dark-gray dark:bg-immich-dark-gray"
@@ -467,7 +467,7 @@
/> />
{#if showAssetName && !isTimelineAsset(asset)} {#if showAssetName && !isTimelineAsset(asset)}
<div <div
class="absolute text-center p-1 text-xs font-mono font-semibold w-full bottom-0 bg-linear-to-t bg-slate-50/75 dark:bg-slate-800/75 overflow-clip text-ellipsis whitespace-pre-wrap" class="absolute text-center p-1 text-xs font-immich-mono font-semibold w-full bottom-0 bg-linear-to-t bg-slate-50/75 dark:bg-slate-800/75 overflow-clip text-ellipsis whitespace-pre-wrap"
> >
{asset.originalFileName} {asset.originalFileName}
</div> </div>
@@ -361,7 +361,7 @@
> >
{#snippet children({ feature })} {#snippet children({ feature })}
<div <div
class="rounded-full w-10 h-10 bg-immich-primary text-white flex justify-center items-center font-mono font-bold shadow-lg hover:bg-immich-dark-primary transition-all duration-200 hover:text-immich-dark-bg opacity-90" class="rounded-full w-10 h-10 bg-immich-primary text-white flex justify-center items-center font-immich-mono font-bold shadow-lg hover:bg-immich-dark-primary transition-all duration-200 hover:text-immich-dark-bg opacity-90"
> >
{feature.properties?.point_count?.toLocaleString()} {feature.properties?.point_count?.toLocaleString()}
</div> </div>
@@ -64,7 +64,7 @@
</script> </script>
<div <div
class="border-2 rounded-2xl border-primary/20 my-4 px-6 py-4 transition-all {isOpen class="border-2 rounded-2xl border-primary/20 mt-4 px-6 py-4 transition-all {isOpen
? 'border-primary/60 shadow-md' ? 'border-primary/60 shadow-md'
: ''}" : ''}"
bind:this={accordionElement} bind:this={accordionElement}
@@ -35,11 +35,11 @@
let closestLanguage = $derived(getClosestAvailableLocale([$lang], langCodes)); let closestLanguage = $derived(getClosestAvailableLocale([$lang], langCodes));
</script> </script>
<div class="max-w-[300px]"> <div class="max-w-75">
{#if showSettingDescription} {#if showSettingDescription}
<div> <div>
<div class="flex h-6.5 place-items-center gap-1"> <div class="flex h-6.5 place-items-center gap-1">
<Label>{$t('language')}</Label> <Label size="small">{$t('language')}</Label>
</div> </div>
<Text size="small" color="muted">{$t('language_setting_description')}</Text> <Text size="small" color="muted">{$t('language_setting_description')}</Text>
@@ -62,12 +62,12 @@
> >
{#if $connected} {#if $connected}
<div class="flex gap-2 place-items-center place-content-center"> <div class="flex gap-2 place-items-center place-content-center">
<div class="w-[7px] h-[7px] bg-green-500 rounded-full"></div> <div class="w-1.75 h-1.75 bg-green-500 rounded-full"></div>
<p class="dark:text-immich-gray">{$t('server_online')}</p> <p class="dark:text-immich-gray">{$t('server_online')}</p>
</div> </div>
{:else} {:else}
<div class="flex gap-2 place-items-center place-content-center"> <div class="flex gap-2 place-items-center place-content-center">
<div class="w-[7px] h-[7px] bg-red-500 rounded-full"></div> <div class="w-1.75 h-1.75 bg-red-500 rounded-full"></div>
<p class="text-red-500">{$t('server_offline')}</p> <p class="text-red-500">{$t('server_offline')}</p>
</div> </div>
{/if} {/if}
@@ -66,8 +66,8 @@
})} })}
</p> </p>
<div class="mt-4 h-[7px] w-full rounded-full bg-gray-200 dark:bg-gray-700"> <div class="mt-4 h-1.75 w-full rounded-full bg-gray-200 dark:bg-gray-700">
<div class="h-[7px] rounded-full {usageClasses}" style="width: {usedPercentage}%"></div> <div class="h-1.75 rounded-full {usageClasses}" style="width: {usedPercentage}%"></div>
</div> </div>
{:else} {:else}
<div class="mt-2"> <div class="mt-2">
@@ -51,7 +51,7 @@
/> />
</li> </li>
{#each parents as parent (parent)} {#each parents as parent (parent)}
<li class="flex gap-2 items-center font-mono text-sm text-nowrap text-primary"> <li class="flex gap-2 items-center font-immich-mono text-sm text-nowrap text-primary">
<Icon icon={mdiChevronRight} class="text-gray-500 dark:text-gray-300" size="16" aria-hidden /> <Icon icon={mdiChevronRight} class="text-gray-500 dark:text-gray-300" size="16" aria-hidden />
<a class="underline hover:font-semibold whitespace-pre-wrap" href={getLink(parent.path)}> <a class="underline hover:font-semibold whitespace-pre-wrap" href={getLink(parent.path)}>
{parent.value} {parent.value}
@@ -59,7 +59,7 @@
</li> </li>
{/each} {/each}
<li class="flex gap-2 items-center font-mono text-sm text-nowrap text-primary"> <li class="flex gap-2 items-center font-immich-mono text-sm text-nowrap text-primary">
<Icon icon={mdiChevronRight} class="text-gray-500 dark:text-gray-300" size="16" aria-hidden /> <Icon icon={mdiChevronRight} class="text-gray-500 dark:text-gray-300" size="16" aria-hidden />
<p class="cursor-default whitespace-pre-wrap">{node.value}</p> <p class="cursor-default whitespace-pre-wrap">{node.value}</p>
</li> </li>
@@ -42,7 +42,9 @@
size="20" size="20"
/> />
</div> </div>
<span class="text-nowrap overflow-hidden text-ellipsis font-mono ps-1 pt-1 whitespace-pre-wrap">{node.value}</span> <span class="text-nowrap overflow-hidden text-ellipsis font-immich-mono ps-1 pt-1 whitespace-pre-wrap"
>{node.value}</span
>
</a> </a>
{#if isOpen} {#if isOpen}
@@ -543,7 +543,7 @@
in:fade={{ duration: 200 }} in:fade={{ duration: 200 }}
out:fade={{ duration: 200 }} out:fade={{ duration: 200 }}
> >
<Icon icon={mdiPlay} size="20" class="-rotate-90 relative top-[9px] -end-0.5" /> <Icon icon={mdiPlay} size="20" class="-rotate-90 relative top-2.25 -end-0.5" />
<Icon icon={mdiPlay} size="20" class="rotate-90 relative top-px -end-0.5" /> <Icon icon={mdiPlay} size="20" class="rotate-90 relative top-px -end-0.5" />
{#if (timelineManager.scrolling && scrollHoverLabel) || isHover || isDragging} {#if (timelineManager.scrolling && scrollHoverLabel) || isHover || isDragging}
<p <p
@@ -588,7 +588,7 @@
> >
{#if !usingMobileDevice} {#if !usingMobileDevice}
{#if segment.hasLabel} {#if segment.hasLabel}
<div class="absolute end-5 text-[12px] dark:text-immich-dark-fg font-immich-mono bottom-0"> <div class="absolute end-5 text-[13px] dark:text-immich-dark-fg font-immich-mono bottom-0">
{segment.year} {segment.year}
</div> </div>
{/if} {/if}
@@ -128,7 +128,7 @@
maxlength="1" maxlength="1"
bind:this={pinCodeInputElements[index]} bind:this={pinCodeInputElements[index]}
id="pin-code-{index}" id="pin-code-{index}"
class="h-12 w-10 rounded-xl border-2 border-suble dark:border-gray-700 text-center text-lg font-medium focus:border-immich-primary focus:ring-primary dark:focus:border-primary font-mono bg-white dark:bg-light" class="h-12 w-10 rounded-xl border-2 border-suble dark:border-gray-700 text-center text-lg font-medium focus:border-immich-primary focus:ring-primary dark:focus:border-primary font-immich-mono bg-white dark:bg-light"
bind:value={pinValues[index]} bind:value={pinValues[index]}
onkeydown={handleKeydown} onkeydown={handleKeydown}
oninput={(event) => handleInput(event, index)} oninput={(event) => handleInput(event, index)}
@@ -59,7 +59,7 @@
<section class="my-4"> <section class="my-4">
<div in:fade={{ duration: 500 }}> <div in:fade={{ duration: 500 }}>
<div class="ms-8 mt-4 flex flex-col gap-4"> <div class="ms-8 mt-4 flex flex-col gap-6">
<Field label={$t('theme_selection')} description={$t('theme_selection_description')}> <Field label={$t('theme_selection')} description={$t('theme_selection_description')}>
<Switch checked={themeManager.theme.system} onCheckedChange={(checked) => themeManager.setSystem(checked)} /> <Switch checked={themeManager.theme.system} onCheckedChange={(checked) => themeManager.setSystem(checked)} />
</Field> </Field>
@@ -68,7 +68,7 @@
<Field label={$t('default_locale')} description={$t('default_locale_description')}> <Field label={$t('default_locale')} description={$t('default_locale_description')}>
<Switch checked={$locale == 'default'} onCheckedChange={handleToggleLocaleBrowser} /> <Switch checked={$locale == 'default'} onCheckedChange={handleToggleLocaleBrowser} />
<Text size="small" class="mt-2">{selectedDate}</Text> <Text size="small" class="mt-2 font-immich-mono text-sm">{selectedDate}</Text>
</Field> </Field>
{#if $locale !== 'default'} {#if $locale !== 'default'}
@@ -42,7 +42,7 @@
<section class="my-4"> <section class="my-4">
<div in:fade={{ duration: 500 }}> <div in:fade={{ duration: 500 }}>
<form autocomplete="off" {onsubmit}> <form autocomplete="off" {onsubmit}>
<div class="ms-4 mt-4 flex flex-col gap-4"> <div class="ms-4 mt-4 flex flex-col gap-6">
<Field label={$t('enable')} description={$t('notification_toggle_setting_description')}> <Field label={$t('enable')} description={$t('notification_toggle_setting_description')}>
<Switch bind:checked={emailNotificationsEnabled} /> <Switch bind:checked={emailNotificationsEnabled} />
</Field> </Field>
@@ -39,7 +39,7 @@
checked={selectAllSubItems} checked={selectAllSubItems}
onCheckedChange={handleSelectAllSubItems} onCheckedChange={handleSelectAllSubItems}
/> />
<Label label={title} for="permission-{title}" class="font-mono text-primary text-lg" /> <Label label={title} for="permission-{title}" class="font-immich-mono text-primary text-lg" />
</div> </div>
<div class="mx-6 mt-3 grid grid-cols-3 gap-2"> <div class="mx-6 mt-3 grid grid-cols-3 gap-2">
{#each subItems as item (item)} {#each subItems as item (item)}
@@ -50,7 +50,7 @@
checked={selectedItems.includes(item)} checked={selectedItems.includes(item)}
onCheckedChange={() => handleToggleItem(item)} onCheckedChange={() => handleToggleItem(item)}
/> />
<Label label={item} for="permission-{item}" class="text-sm font-mono" /> <Label label={item} for="permission-{item}" class="text-sm font-immich-mono" />
</div> </div>
{/each} {/each}
</div> </div>
@@ -64,7 +64,7 @@
> >
<td class="w-1/4 text-ellipsis px-4 text-sm overflow-hidden">{key.name}</td> <td class="w-1/4 text-ellipsis px-4 text-sm overflow-hidden">{key.name}</td>
<td <td
class="w-1/4 text-ellipsis px-4 text-xs overflow-hidden line-clamp-3 break-all font-mono" class="w-1/4 text-ellipsis px-4 text-xs overflow-hidden line-clamp-3 break-all font-immich-mono"
title={JSON.stringify(key.permissions, undefined, 2)}>{key.permissions}</td title={JSON.stringify(key.permissions, undefined, 2)}>{key.permissions}</td
> >
<td class="w-1/4 text-ellipsis px-4 text-sm overflow-hidden" <td class="w-1/4 text-ellipsis px-4 text-sm overflow-hidden"
+1 -1
View File
@@ -15,7 +15,7 @@
<Modal title={$t('api_key')} icon={mdiKeyVariant} {onClose} size="small"> <Modal title={$t('api_key')} icon={mdiKeyVariant} {onClose} size="small">
<ModalBody> <ModalBody>
<Text size="small" class="mb-4">{$t('api_key_description')}</Text> <Text size="small" class="mb-4">{$t('api_key_description')}</Text>
<Textarea bind:value={secret} readonly class="font-mono" /> <Textarea bind:value={secret} readonly class="font-immich-mono" />
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
+1 -1
View File
@@ -11,7 +11,7 @@ import { AppRoute } from '../constants';
export interface AuthOptions { export interface AuthOptions {
admin?: true; admin?: true;
public?: true; public?: boolean;
} }
export const loadUser = async () => { export const loadUser = async () => {
+73
View File
@@ -0,0 +1,73 @@
import { uploadManager } from '$lib/managers/upload-manager.svelte';
import { uploadAssetsStore } from '$lib/stores/upload';
import { resetSavedUser, user } from '$lib/stores/user.store';
import { UploadState } from '$lib/types';
import * as utils from '$lib/utils';
import { AssetMediaStatus, type AssetMediaResponseDto, type UserAdminResponseDto } from '@immich/sdk';
import { get } from 'svelte/store';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { fileUploadHandler } from './file-uploader';
describe('fileUploader error handling', () => {
const mockFile = new File(['content'], 'test.jpg', { type: 'image/jpeg' });
const mockUserObject = { id: 'user-123', email: 'test@example.com' } as UserAdminResponseDto;
const mockError = new Error('Upload failed');
const mockUploadResponse = { id: 'mock-id', status: AssetMediaStatus.Created } as AssetMediaResponseDto;
beforeEach(() => {
vi.clearAllMocks();
vi.spyOn(uploadManager, 'getExtensions').mockReturnValue(['.jpg']);
uploadAssetsStore.reset();
resetSavedUser();
// Stub out crypto to avoid that branch
vi.stubGlobal('crypto', undefined);
});
for (const [name, mockUser] of [
['logged-in users', true],
['anonymous users', false],
] as const) {
describe(`for ${name}`, () => {
beforeEach(() => {
if (mockUser) {
user.set(mockUserObject);
}
});
it(`should transition successful uploads to done`, async () => {
vi.spyOn(utils, 'uploadRequest').mockResolvedValue({ status: 200, data: mockUploadResponse });
await fileUploadHandler({ files: [mockFile] });
const items = get(uploadAssetsStore);
expect(items.length).toBe(1);
expect(items[0].state).toBe(UploadState.DONE);
});
it('should capture errors', async () => {
vi.spyOn(utils, 'uploadRequest').mockRejectedValue(mockError);
await fileUploadHandler({ files: [mockFile] });
const items = get(uploadAssetsStore);
expect(items.length).toBe(1);
expect(items[0].state).toBe(UploadState.ERROR);
});
});
}
it('should suppress errors on logout', async () => {
user.set(mockUserObject);
vi.spyOn(utils, 'uploadRequest').mockImplementationOnce(() => {
resetSavedUser();
return Promise.reject(mockError);
});
await fileUploadHandler({ files: [mockFile] });
const items = get(uploadAssetsStore);
expect(items.length).toBe(1);
expect(items[0].state).toBe(UploadState.STARTED);
});
});
+4 -2
View File
@@ -125,6 +125,7 @@ async function fileUploader({
}: FileUploaderParams): Promise<string | undefined> { }: FileUploaderParams): Promise<string | undefined> {
const fileCreatedAt = new Date(assetFile.lastModified).toISOString(); const fileCreatedAt = new Date(assetFile.lastModified).toISOString();
const $t = get(t); const $t = get(t);
const wasInitiallyLoggedIn = !!get(user);
uploadAssetsStore.markStarted(deviceAssetId); uploadAssetsStore.markStarted(deviceAssetId);
@@ -215,8 +216,9 @@ async function fileUploader({
return responseData.id; return responseData.id;
} catch (error) { } catch (error) {
// ignore errors if the user logs out during uploads // If the user store no longer holds a user, it means they have logged out
if (!get(user)) { // In this case don't bother reporting any errors.
if (wasInitiallyLoggedIn && !get(user)) {
return; return;
} }
+4 -2
View File
@@ -1,7 +1,9 @@
import { getAssetInfoFromParam } from '$lib/utils/navigation'; import { authenticate } from '$lib/utils/auth';
import { getAssetInfoFromParam, isSharedLinkRoute } from '$lib/utils/navigation';
import type { LayoutLoad } from './$types'; import type { LayoutLoad } from './$types';
export const load = (async ({ params }) => { export const load = (async ({ url, params, route }) => {
await authenticate(url, { public: isSharedLinkRoute(route.id) });
const asset = await getAssetInfoFromParam(params); const asset = await getAssetInfoFromParam(params);
return { return {
@@ -141,12 +141,12 @@
<div class="flex gap-2 justify-end place-items-center"> <div class="flex gap-2 justify-end place-items-center">
<Text class="hidden md:block text-xs mr-4 text-dark/50">{$t('geolocation_instruction_location')}</Text> <Text class="hidden md:block text-xs mr-4 text-dark/50">{$t('geolocation_instruction_location')}</Text>
<div class="border flex place-items-center place-content-center px-2 py-1 bg-primary/10 rounded-2xl"> <div class="border flex place-items-center place-content-center px-2 py-1 bg-primary/10 rounded-2xl">
<Text class="hidden md:inline-block text-xs text-gray-500 font-mono mr-5 ml-2 uppercase"> <Text class="hidden md:inline-block text-xs text-gray-500 font-immich-mono mr-5 ml-2 uppercase">
{$t('selected_gps_coordinates')} {$t('selected_gps_coordinates')}
</Text> </Text>
<Text <Text
title="latitude, longitude" title="latitude, longitude"
class="rounded-3xl font-mono text-sm text-primary px-2 py-1 transition-all duration-100 ease-in-out {locationUpdated class="rounded-3xl font-immich-mono text-sm text-primary px-2 py-1 transition-all duration-100 ease-in-out {locationUpdated
? 'bg-primary/90 text-light font-semibold scale-105' ? 'bg-primary/90 text-light font-semibold scale-105'
: ''}">{location.latitude.toFixed(3)}, {location.longitude.toFixed(3)}</Text : ''}">{location.latitude.toFixed(3)}, {location.longitude.toFixed(3)}</Text
> >
@@ -326,7 +326,7 @@
{#snippet cardOrder(index: number)} {#snippet cardOrder(index: number)}
<div class="h-8 w-8 rounded-lg flex place-items-center place-content-center shrink-0 border bg-light-50"> <div class="h-8 w-8 rounded-lg flex place-items-center place-content-center shrink-0 border bg-light-50">
<Text size="small" class="font-mono font-bold"> <Text size="small" class="font-immich-mono font-bold">
{index + 1} {index + 1}
</Text> </Text>
</div> </div>
+3 -1
View File
@@ -1,6 +1,8 @@
import { systemConfigManager } from '$lib/managers/system-config-manager.svelte'; import { systemConfigManager } from '$lib/managers/system-config-manager.svelte';
import { authenticate } from '$lib/utils/auth';
import type { LayoutLoad } from './$types'; import type { LayoutLoad } from './$types';
export const load = (async () => { export const load = (async ({ url }) => {
await authenticate(url, { admin: true });
await systemConfigManager.init(); await systemConfigManager.init();
}) satisfies LayoutLoad; }) satisfies LayoutLoad;