mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
merge: remote-tracking branch 'origin/main' into feat/integrity-checks-izzy
This commit is contained in:
+1
-1
@@ -1 +1 @@
|
|||||||
24.12.0
|
24.13.0
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
24.12.0
|
24.13.0
|
||||||
|
|||||||
+1
-1
@@ -69,6 +69,6 @@
|
|||||||
"micromatch": "^4.0.8"
|
"micromatch": "^4.0.8"
|
||||||
},
|
},
|
||||||
"volta": {
|
"volta": {
|
||||||
"node": "24.12.0"
|
"node": "24.13.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -1 +1 @@
|
|||||||
24.12.0
|
24.13.0
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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);
|
||||||
|
|||||||
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
|||||||
24.12.0
|
24.13.0
|
||||||
|
|||||||
+1
-1
@@ -52,6 +52,6 @@
|
|||||||
"vitest": "^3.0.0"
|
"vitest": "^3.0.0"
|
||||||
},
|
},
|
||||||
"volta": {
|
"volta": {
|
||||||
"node": "24.12.0"
|
"node": "24.13.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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')];
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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"),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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"),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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
@@ -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 @@
|
|||||||
24.12.0
|
24.13.0
|
||||||
|
|||||||
@@ -28,6 +28,6 @@
|
|||||||
"directory": "open-api/typescript-sdk"
|
"directory": "open-api/typescript-sdk"
|
||||||
},
|
},
|
||||||
"volta": {
|
"volta": {
|
||||||
"node": "24.12.0"
|
"node": "24.13.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+13564
-4515
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -1 +1 @@
|
|||||||
24.12.0
|
24.13.0
|
||||||
|
|||||||
+2
-2
@@ -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,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
@@ -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
@@ -1 +1 @@
|
|||||||
24.12.0
|
24.13.0
|
||||||
|
|||||||
+2
-2
@@ -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
@@ -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);
|
||||||
|
|||||||
@@ -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.
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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user