feat: pnpm-monorepo (#236)

This commit is contained in:
Jason Rasmussen
2025-10-14 17:04:39 -04:00
committed by GitHub
parent 9c0db648d8
commit e32cd9b218
234 changed files with 6784 additions and 9941 deletions
+42 -28
View File
@@ -22,41 +22,43 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
app_name: ['api', 'root', 'my', 'buy', 'get', 'datasets']
app_name: ['root', 'api', 'my', 'get', 'buy', 'datasets']
defaults:
run:
working-directory: apps/${{ matrix.app_name }}.immich.app
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version-file: '.nvmrc'
node-version-file: './.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Set app
run: |
touch .env
echo IMMICH_APP="${{ matrix.app_name }}.immich.app" >> .env
- name: Install
run: pnpm install
- name: Use futopay-test
if: ${{ env.ENVIRONMENT == 'dev' }}
run: |
echo PUBLIC_IMMICH_PAY_HOST=https://futopay-test.azurewebsites.net >> .env
- name: Run npm install
run: npm ci
- name: Run build
- name: Build
env:
PUBLIC_IMMICH_PAY_HOST: ${{ env.ENVIRONMENT == 'dev' && 'https://futopay-test.azurewebsites.net' || 'https://pay.futo.org' }}
PUBLIC_CF_TURNSTILE_SITE: ${{ secrets.CF_TURNSTILE_DEFAULT_INVISIBLE_SITE_KEY }}
run: npm run build
run: pnpm build
- name: Upload build output
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: build-frontend-output-${{ matrix.app_name }}
include-hidden-files: true
path: build
path: apps/${{ matrix.app_name }}.immich.app/build
retention-days: 1
build-backend:
@@ -65,22 +67,32 @@ jobs:
strategy:
matrix:
app_name: ['datasets']
defaults:
run:
working-directory: apps/${{ matrix.app_name }}.immich.app/backend
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version-file: '.nvmrc'
node-version-file: './.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Run npm install
run: npm ci
working-directory: 'apps/${{ matrix.app_name }}.immich.app/backend'
- name: Run pnpm install
run: pnpm install
- name: Build backend
run: npx wrangler build
working-directory: 'apps/${{ matrix.app_name }}.immich.app/backend'
- name: Upload build output
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
@@ -96,7 +108,7 @@ jobs:
strategy:
fail-fast: false
matrix:
app_name: ['api', 'root', 'my', 'buy', 'get', 'datasets']
app_name: ['root', 'api', 'my', 'get', 'buy', 'datasets']
env:
TF_VAR_app_name: ${{ matrix.app_name }}
TF_VAR_stage: ${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.number) || '' }}
@@ -104,7 +116,9 @@ jobs:
OP_SERVICE_ACCOUNT_TOKEN: ${{ github.ref == 'refs/heads/main' && secrets.OP_TF_PROD_ENV || secrets.OP_TF_DEV_ENV }}
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: Get build artifact
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
+2 -2
View File
@@ -14,7 +14,7 @@ jobs:
fail-fast: false
matrix:
environment: ['dev', 'prod']
name: ['api', 'root', 'my', 'buy', 'get', 'datasets']
app_name: ['root', 'api', 'my', 'get', 'buy', 'datasets']
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
@@ -37,7 +37,7 @@ jobs:
working-directory: ${{ github.workspace }}/deployment
env:
ENVIRONMENT: ${{ matrix.environment }}
TF_VAR_app_name: ${{ matrix.name }}
TF_VAR_app_name: ${{ matrix.app_name }}
TF_VAR_dist_dir: ${{ github.workspace }}/dist
TF_VAR_stage: pr-${{ github.event.number }}
OP_SERVICE_ACCOUNT_TOKEN: ${{ matrix.environment == 'prod' && secrets.OP_TF_PROD_ENV || secrets.OP_TF_DEV_ENV }}
+28 -22
View File
@@ -15,45 +15,51 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
app_name: ['api', 'root', 'my', 'buy', 'get', 'datasets']
app_name: ['root', 'api', 'my', 'get', 'buy', 'datasets']
defaults:
run:
working-directory: apps/${{ matrix.app_name }}.immich.app
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Setup Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version-file: '.nvmrc'
node-version-file: './.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Set app
run: |
touch .env
echo IMMICH_APP="${{ matrix.app_name }}.immich.app" >> .env
- name: Install
run: pnpm install
- name: Run npm install
run: npm ci
- name: Run linter
run: npm run lint
- name: Lint
run: pnpm lint
if: ${{ !cancelled() }}
- name: Run formatter
run: npm run format
- name: Format
run: pnpm format
if: ${{ !cancelled() }}
- name: Run tsc
run: npm run check
- name: Type check
run: pnpm check
if: ${{ !cancelled() }}
- name: Run unit tests
run: npm test
- name: Test
run: pnpm test
if: ${{ !cancelled() }}
- name: Check for app backend
id: check-backend
run: |
if [ -d "apps/${{ matrix.app_name }}.immich.app/backend" ]; then
if [ -d "backend" ]; then
echo "has_backend=true" >> $GITHUB_ENV
else
echo "has_backend=false" >> $GITHUB_ENV
@@ -63,5 +69,5 @@ jobs:
if: ${{ env.has_backend == 'true' }}
working-directory: 'apps/${{ matrix.app_name }}.immich.app/backend'
run: |
npm ci
npm test
pnpm install
pnpm test
+2 -2
View File
@@ -3,8 +3,8 @@ node_modules
# Output
.output
.vercel
/.svelte-kit
/build
apps/*/.svelte-kit
apps/*/build
# OS
.DS_Store
-7
View File
@@ -1,7 +0,0 @@
# Package Managers
package-lock.json
pnpm-lock.yaml
yarn.lock
# wrangler generated files
worker-configuration.d.ts
+1 -1
View File
@@ -2,6 +2,6 @@
"singleQuote": true,
"trailingComma": "all",
"printWidth": 120,
"plugins": ["prettier-plugin-svelte"],
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}
+1 -38
View File
@@ -1,38 +1 @@
# create-svelte
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```bash
# create a new project in the current directory
npm create svelte@latest
# create a new project in my-app
npm create svelte@latest my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```bash
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
# Static sites
+15
View File
@@ -0,0 +1,15 @@
# Package Managers
package-lock.json
pnpm-lock.yaml
yarn.lock
bun.lock
bun.lockb
# Miscellaneous
/static/
# wrangler generated files
worker-configuration.d.ts
/.svelte-kit
/build
+3
View File
@@ -0,0 +1,3 @@
# https://api.immich.app/
Immich link proxy to redirect to your personal instance
@@ -4,13 +4,13 @@ import svelte from 'eslint-plugin-svelte';
import globals from 'globals';
import ts from 'typescript-eslint';
/** @type {import('eslint').Linter.FlatConfig[]} */
export default [
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs['flat/recommended'],
prettier,
...svelte.configs['flat/prettier'],
...svelte.configs.prettier,
{
languageOptions: {
globals: {
@@ -32,7 +32,6 @@ export default [
},
},
{
// idk why svelte.ts files are not being parsed correctly
ignores: ['build/', '.svelte-kit/', 'dist/', 'apps/**/backend/worker-configuration.d.ts', '**/*.svelte.ts'],
ignores: ['build/', '.svelte-kit/', 'dist/'],
},
];
+51
View File
@@ -0,0 +1,51 @@
{
"name": "api.immich.app",
"private": true,
"version": "0.0.1",
"license": "GNU Affero General Public License version 3",
"type": "module",
"scripts": {
"start": "vite dev",
"build": "npm run sync && vite build && node ../../bin/postbuild.js",
"preview": "vite preview",
"check": "npm run sync && svelte-check --tsconfig ./tsconfig.json",
"sync": "svelte-kit sync",
"test": "vitest",
"lint": "eslint . --max-warnings 0",
"lint:fix": "npm run lint -- --fix",
"format": "prettier --check .",
"format:fix": "prettier --write ."
},
"devDependencies": {
"@eslint/compat": "^1.4.0",
"@eslint/js": "^9.36.0",
"@sveltejs/adapter-static": "^3.0.9",
"@sveltejs/kit": "^2.43.2",
"@sveltejs/vite-plugin-svelte": "^6.2.0",
"@tailwindcss/vite": "^4.1.13",
"@types/node": "^22",
"@vitest/browser": "^3.2.4",
"dotenv": "^17.2.3",
"eslint": "^9.36.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-svelte": "^3.12.4",
"globals": "^16.4.0",
"playwright": "^1.55.1",
"prettier": "^3.6.2",
"prettier-plugin-svelte": "^3.4.0",
"prettier-plugin-tailwindcss": "^0.6.14",
"svelte": "^5.39.5",
"svelte-check": "^4.3.2",
"tailwindcss": "^4.1.13",
"typescript": "^5.9.2",
"typescript-eslint": "^8.44.1",
"vite": "^7.1.7",
"vitest": "^3.2.4",
"vitest-browser-svelte": "^1.1.0"
},
"dependencies": {
"@immich/ui": "^0.35.0",
"@mdi/js": "^7.4.47",
"svelte-highlight": "^7.8.4"
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
// See https://kit.svelte.dev/docs/types#app
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
@@ -21,7 +21,7 @@
<meta name="twitter:image" content="/img/social-preview.png" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover" class="dark bg-light text-dark">
<body data-sveltekit-preload-data="hover" class="bg-light text-dark dark">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
+7
View File
@@ -0,0 +1,7 @@
import { describe, it, expect } from 'vitest';
describe('sum test', () => {
it('adds 1 + 2 to equal 3', () => {
expect(1 + 2).toBe(3);
});
});
+5
View File
@@ -0,0 +1,5 @@
@import 'tailwindcss';
@import '@immich/ui/theme/default.css';
@import '$common/theme/theme.css';
@source '../../node_modules/@immich/ui/dist';
@source '../../../../common';

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

@@ -0,0 +1,4 @@
<span
class="text-dark rounded-lg bg-teal-200 px-2 py-1 dark:bg-teal-900 dark:text-teal-50"
title="Endpoint can only be used by Administrators">Admin Endpoint</span
>
@@ -1,5 +1,5 @@
<script lang="ts">
import { playgroundManager } from '$lib/api/services/playground-manager.svelte';
import { playgroundManager } from '$lib/services/playground-manager.svelte';
import { Button, Code, Field, HStack, Input, Modal, ModalBody, ModalFooter, Stack, Text } from '@immich/ui';
import { mdiConnection } from '@mdi/js';
@@ -59,14 +59,14 @@
<form autocomplete="off" novalidate id="configure-form" {onsubmit}>
<Stack gap={4}>
{#if playgroundManager.connected}
<div class="flex gap-1 items-center">
<div class="flex items-center gap-1">
<Text>
Connected to <Code>{playgroundManager.serverUrl}</Code>
</Text>
</div>
{#if playgroundManager.serverVersion}
<div class="flex gap-1 items-center">
<div class="flex items-center gap-1">
<Text>
Server version <Code>{playgroundManager.serverVersion.value}</Code>
</Text>
@@ -74,7 +74,7 @@
{/if}
{:else}
{#if errorMessage}
<Text class="pb-4 bg-red-100 dark:bg-red-900 border border-red-900 dark:border-red-100 rounded-lg p-4"
<Text class="rounded-lg border border-red-900 bg-red-100 p-4 pb-4 dark:border-red-100 dark:bg-red-900"
>{errorMessage}</Text
>
{/if}
@@ -95,7 +95,7 @@
<hr class="my-1 border border-gray-500" />
<Text
class="pb-4 bg-yellow-100 dark:bg-yellow-900 border border-yellow-900 dark:border-yellow-100 rounded-lg p-4"
class="rounded-lg border border-yellow-900 bg-yellow-100 p-4 pb-4 dark:border-yellow-100 dark:bg-yellow-900"
>Note: In order to connect, CORS has to be enabled on the remote server.</Text
>
{/if}
@@ -0,0 +1,3 @@
<span class="rounded-lg bg-red-200 px-2 py-1 dark:bg-red-900 dark:text-red-50" title="Endpoint is deprecated"
>Deprecated</span
>
@@ -1,5 +1,5 @@
<script lang="ts">
import ApiNextStepCard from '$lib/api/components/ApiNextStepCard.svelte';
import ApiNextStepCard from '$lib/components/ApiNextStepCard.svelte';
import { Heading, Stack } from '@immich/ui';
type Props = {
@@ -11,7 +11,7 @@
<Stack gap={4}>
<Heading>Next Steps</Heading>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
{#each nextSteps as nextStep (nextStep.title)}
<ApiNextStepCard {...nextStep} />
{/each}
@@ -1,5 +1,5 @@
<script lang="ts">
import ApiNextSteps from '$lib/api/components/ApiNextSteps.svelte';
import ApiNextSteps from '$lib/components/ApiNextSteps.svelte';
import { Heading, Stack, Text } from '@immich/ui';
import type { Snippet } from 'svelte';
@@ -14,14 +14,14 @@
</script>
<Stack gap={8}>
<div class="flex justify-between place-items-end">
<div class="flex place-items-end justify-between">
<div class="flex flex-col gap-2">
<Heading size="giant" tag="h1">{title}</Heading>
<Text color="muted" size="giant" fontWeight="bold">{description}</Text>
</div>
</div>
<hr class="border border-subtle" />
<hr class="border-subtle border" />
{@render children?.()}
@@ -1,5 +1,5 @@
<script lang="ts">
import type { ParameterObject } from '$lib/api/services/open-api.d';
import type { ParameterObject } from '$lib/services/open-api.d';
import ApiParamsInternal from './ApiParamsInternal.svelte';
type Props = {
@@ -1,6 +1,6 @@
<script lang="ts">
import ApiSchemaType from '$lib/api/components/ApiSchemaType.svelte';
import type { ReferenceObject, SchemaObject } from '$lib/api/services/open-api.d';
import ApiSchemaType from '$lib/components/ApiSchemaType.svelte';
import type { ReferenceObject, SchemaObject } from '$lib/services/open-api.d';
import { Card, CardBody, Text } from '@immich/ui';
type InternalParam = {
@@ -20,9 +20,9 @@
<div class="flex flex-col gap-2 lg:hidden">
{#each params as param, i (i)}
<Card color="secondary">
<CardBody class="py-2 flex flex-col gap-2">
<CardBody class="flex flex-col gap-2 py-2">
<div class="flex gap-2">
<div class="flex justify-between w-full place-items-center">
<div class="flex w-full place-items-center justify-between">
<Text>
<code>{param.name} </code>
{#if param.required}
@@ -44,14 +44,14 @@
<div class="hidden lg:block">
<Card color="secondary">
<div class="grid grid-cols-12 py-2 px-4 font-bold bg-subtle text-dark mb-2 border-b">
<div class="bg-subtle text-dark mb-2 grid grid-cols-12 border-b px-4 py-2 font-bold">
<div class="col-span-3">Property</div>
<div class="col-span-3">Type</div>
<div class="col-span-3">Required</div>
<div class="col-span-3">Description</div>
</div>
{#each params as param, i (i)}
<div class="grid grid-cols-12 py-2 px-4">
<div class="grid grid-cols-12 px-4 py-2">
<div class="col-span-3">
<code>{param.name}</code>
</div>
@@ -62,7 +62,7 @@
</div>
<div class="col-span-3">
{#if param.required}
<span class="py-1 px-2 dark:bg-red-900 dark:text-red-50 bg-red-200 rounded-lg" title="Param is required"
<span class="rounded-lg bg-red-200 px-2 py-1 dark:bg-red-900 dark:text-red-50" title="Param is required"
>Required</span
>
{/if}
@@ -7,7 +7,7 @@
</script>
<span
class="py-1 px-2 dark:bg-purple-900 dark:text-purple-50 bg-purple-200 text-dark rounded-lg"
class="text-dark rounded-lg bg-purple-200 px-2 py-1 dark:bg-purple-900 dark:text-purple-50"
title="The {value} permission"
>
<span class="font-mono">{value}</span>
@@ -1,10 +1,10 @@
<script lang="ts">
import { type ApiEndpoint } from '$lib/api/services/open-api';
import { playgroundManager } from '$lib/api/services/playground-manager.svelte';
import CodeBlock from '$lib/components/CodeBlock.svelte';
import { type ApiEndpoint } from '$lib/services/open-api';
import { playgroundManager } from '$lib/services/playground-manager.svelte';
import {
Button,
Code,
CodeBlock,
Field,
Heading,
Icon,
@@ -63,7 +63,7 @@
</script>
<section class="flex flex-col gap-2">
<div class="flex justify-between place-items-center gap-2">
<div class="flex place-items-center justify-between gap-2">
<Heading size="small" tag="h2" class="flex items-center gap-1">
Live Response
<Icon icon={mdiLightningBolt} size="1.5rem" class="text-yellow-600 dark:text-yellow-400" />
@@ -1,4 +1,4 @@
<span
class="py-1 px-2 dark:bg-blue-900 dark:text-blue-50 bg-blue-200 text-dark rounded-lg"
class="text-dark rounded-lg bg-blue-200 px-2 py-1 dark:bg-blue-900 dark:text-blue-50"
title="Endpoint can be accessed without any authentication">Public Endpoint</span
>
@@ -1,10 +1,10 @@
<script lang="ts">
import ApiSchemaEnum from '$lib/api/components/ApiSchemaEnum.svelte';
import ApiSchemaObject from '$lib/api/components/ApiSchemaObject.svelte';
import ApiSchemaRef from '$lib/api/components/ApiSchemaRef.svelte';
import ApiSchemaType from '$lib/api/components/ApiSchemaType.svelte';
import { isRef } from '$lib/api/services/open-api';
import type { ReferenceObject, SchemaObject } from '$lib/api/services/open-api.d';
import ApiSchemaEnum from '$lib/components/ApiSchemaEnum.svelte';
import ApiSchemaObject from '$lib/components/ApiSchemaObject.svelte';
import ApiSchemaRef from '$lib/components/ApiSchemaRef.svelte';
import ApiSchemaType from '$lib/components/ApiSchemaType.svelte';
import { isRef } from '$lib/services/open-api';
import type { ReferenceObject, SchemaObject } from '$lib/services/open-api.d';
type Props = {
schema?: SchemaObject | ReferenceObject;
@@ -1,6 +1,6 @@
<script lang="ts">
import type { ReferenceObject, SchemaObject } from '$lib/api/services/open-api.d';
import { getRefHref, getRefName, isRef } from '$lib/api/services/open-api';
import type { ReferenceObject, SchemaObject } from '$lib/services/open-api.d';
import { getRefHref, getRefName, isRef } from '$lib/services/open-api';
import { Link, Text } from '@immich/ui';
type Props = {
@@ -11,7 +11,7 @@
</script>
{#if isRef(schema)}
<span class="py-1 px-2 dark:bg-pink-900 dark:text-pink-50 bg-pink-200 rounded-lg">
<span class="rounded-lg bg-pink-200 px-2 py-1 dark:bg-pink-900 dark:text-pink-50">
<Link href={getRefHref(schema)}>{getRefName(schema)}[]</Link>
</span>
{:else}
@@ -1,6 +1,6 @@
<script lang="ts">
import type { SchemaObject } from '$lib/api/services/open-api.d';
import ApiParamsInternal from './ApiParamsInternal.svelte';
import ApiParamsInternal from '$lib/components/ApiParamsInternal.svelte';
import type { SchemaObject } from '$lib/services/open-api.d';
type Props = {
schema: SchemaObject;
@@ -1,6 +1,6 @@
<script lang="ts">
import type { ReferenceObject } from '$lib/api/services/open-api.d';
import { getOpenApi, getRefHref, getRefName } from '$lib/api/services/open-api';
import type { ReferenceObject } from '$lib/services/open-api.d';
import { getOpenApi, getRefHref, getRefName } from '$lib/services/open-api';
import { Link, Text } from '@immich/ui';
type Props = {
@@ -13,7 +13,7 @@
</script>
{#if fromRef(ref)}
<span class="py-1 px-2 dark:bg-pink-900 dark:text-pink-50 bg-pink-200 rounded-lg">
<span class="rounded-lg bg-pink-200 px-2 py-1 dark:bg-pink-900 dark:text-pink-50">
<Link href={getRefHref(ref)}>{getRefName(ref)}</Link>
</span>
{:else}
@@ -1,8 +1,8 @@
<script lang="ts">
import ApiSchemaType from '$lib/api/components/ApiSchemaType.svelte';
import { getOpenApi, getRefHref, isRef } from '$lib/api/services/open-api';
import type { ReferenceObject, SchemaObject } from '$lib/api/services/open-api.d';
import { cleanClass } from '$lib/utils';
import { cleanClass } from '$lib';
import ApiSchemaType from '$lib/components/ApiSchemaType.svelte';
import { getOpenApi, getRefHref, isRef } from '$lib/services/open-api';
import type { ReferenceObject, SchemaObject } from '$lib/services/open-api.d';
import { Link, Text } from '@immich/ui';
type Props = {
@@ -65,7 +65,7 @@
const schema = $derived(schemaOrRef as SchemaObject);
</script>
<div class="flex gap-1 place-items-center">
<div class="flex place-items-center gap-1">
{#if isRef(schema)}
{@const ref = fromRef(schema)}
{#if ref}
@@ -1,4 +1,4 @@
<span
class="py-1 px-2 dark:bg-yellow-900 dark:text-yellow-50 bg-yellow-200 text-dark rounded-lg"
class="text-dark rounded-lg bg-yellow-200 px-2 py-1 dark:bg-yellow-900 dark:text-yellow-50"
title="Endpoint can be accessed by shared link authentication">Shared Link Route</span
>
@@ -1,5 +1,5 @@
<script lang="ts">
import { getEndpointColor, type ApiEndpointTag } from '$lib/api/services/open-api';
import { getEndpointColor, type ApiEndpointTag } from '$lib/services/open-api';
import { Button, Card, CardTitle, Heading, Text } from '@immich/ui';
type Props = {
@@ -10,15 +10,15 @@
</script>
<div>
<Heading class="flex gap-2 py-4 items-center">
<Heading class="flex items-center gap-2 py-4">
<a href={tag.href}>{tag.name}</a>
</Heading>
<Card color="secondary">
{#each tag.endpoints as endpoint, i (i)}
<Button href={endpoint.href} shape="rectangle" color="secondary" fullWidth variant="ghost" class="p-4">
<div class="flex flex-col w-full">
<div class="flex w-full flex-col">
<CardTitle>
<span class="group flex gap-2 justify-between {endpoint.deprecated ? 'text-gray-500 italic' : ''}">
<span class="group flex justify-between gap-2 {endpoint.deprecated ? 'italic text-gray-500' : ''}">
<span class="flex gap-2">
<span class={getEndpointColor(endpoint)}>{endpoint.method}</span>
<span>{endpoint.route}</span>
@@ -1,8 +1,7 @@
<script lang="ts">
import { cleanClass } from '$lib/utils';
import { cleanClass } from '$lib';
import { Heading } from '@immich/ui';
import type { ComponentProps } from 'svelte';
import type { Snippet } from 'svelte';
import type { ComponentProps, Snippet } from 'svelte';
import type { HTMLAnchorAttributes } from 'svelte/elements';
type Props = {
@@ -26,6 +26,16 @@ import {
mdiTrashCanOutline,
} from '@mdi/js';
export enum ApiPage {
Introduction = '/introduction',
GettingStarted = '/getting-started',
Authentication = '/authentication',
Permissions = '/permissions',
Sdk = '/sdk',
Endpoints = '/endpoints',
Models = '/models',
}
const icons: Record<string, string> = {
'API Keys': mdiKeyOutline,
Activities: mdiForumOutline,
@@ -58,3 +68,15 @@ const icons: Record<string, string> = {
};
export const getIcon = (name: string) => icons[name] || mdiCodeJson;
export const cleanClass = (...classNames: unknown[]) => {
return classNames
.filter((className) => {
if (!className || typeof className === 'boolean') {
return false;
}
return typeof className === 'string';
})
.join(' ');
};
@@ -1,13 +1,13 @@
import type { Pathname } from '$app/types';
import { PUBLIC_IMMICH_SPEC_URL } from '$env/static/public';
import { ApiPage } from '$lib';
import type {
OpenAPIObject,
ParameterObject,
ReferenceObject,
RequestBodyObject,
SchemaObject,
} from '$lib/api/services/open-api.d';
import { ApiPage } from '$lib/utils/api';
} from '$lib/services/open-api.d';
export type ApiMethod = 'GET' | 'PUT' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'PATCH';
export type AuthenticationMethod = 'ApiKey' | 'Cookie' | 'Bearer';
@@ -1,4 +1,4 @@
import ApiConfigureModal from '$lib/api/modals/ApiConfigureModal.svelte';
import ApiConfigureModal from '$lib/components/ApiConfigureModal.svelte';
import { modalManager } from '@immich/ui';
const LOCAL_STORAGE_KEY = 'immich-api:connection';
@@ -1,11 +1,10 @@
<script lang="ts">
import { beforeNavigate } from '$app/navigation';
import { page } from '$app/state';
import { getOpenApi } from '$lib/api/services/open-api';
import Header from '$lib/components/Header.svelte';
import PageContent from '$lib/components/PageContent.svelte';
import { ApiPage } from '$lib/utils/api';
import { getIcon } from '$lib/utils/icons';
import Header from '$common/components/Header.svelte';
import PageContent from '$common/components/PageContent.svelte';
import { ApiPage, getIcon } from '$lib';
import { getOpenApi } from '$lib/services/open-api';
import { AppShell, AppShellHeader, AppShellSidebar, NavbarGroup, NavbarItem } from '@immich/ui';
import {
mdiApi,
@@ -48,7 +47,7 @@
</AppShellHeader>
<AppShellSidebar bind:open class="border-e">
<div class="mt-8 pe-6 flex flex-col gap-4">
<div class="mt-8 flex flex-col gap-4 pe-6">
<div>
<NavbarItem title="Introduction" href={ApiPage.Introduction} icon={mdiNoteOutline} activeIcon={mdiNote} />
<NavbarItem
@@ -83,7 +82,7 @@
</div>
</AppShellSidebar>
<PageContent class="mx-auto w-full max-w-(--breakpoint-lg)">
<PageContent class="max-w-(--breakpoint-lg) mx-auto w-full">
{@render children?.()}
</PageContent>
</AppShell>
@@ -1,7 +1,7 @@
<script lang="ts">
import ApiPageContent from '$lib/api/components/ApiPageContent.svelte';
import ApiPermission from '$lib/api/components/ApiPermission.svelte';
import { ApiPage } from '$lib/utils/api';
import { ApiPage } from '$lib';
import ApiPageContent from '$lib/components/ApiPageContent.svelte';
import ApiPermission from '$lib/components/ApiPermission.svelte';
import { Alert, Card, CardBody, CardHeader, Code, CodeBlock, Heading, Link, Text } from '@immich/ui';
import { mdiLightbulbOnOutline } from '@mdi/js';
import { plaintext } from 'svelte-highlight/languages';
@@ -34,7 +34,7 @@
</Text>
<Card class="mt-2">
<CardHeader class="py-1 bg-subtle">
<CardHeader class="bg-subtle py-1">
<div class="grid grid-cols-3">
<div class="font-bold">Type</div>
<div class="font-bold">Value</div>
@@ -1,7 +1,7 @@
<script lang="ts">
import ApiPageContent from '$lib/api/components/ApiPageContent.svelte';
import ApiTagSummary from '$lib/api/components/ApiTagSummary.svelte';
import { getOpenApi } from '$lib/api/services/open-api';
import ApiPageContent from '$lib/components/ApiPageContent.svelte';
import ApiTagSummary from '$lib/components/ApiTagSummary.svelte';
import { getOpenApi } from '$lib/services/open-api';
const { tags } = getOpenApi();
</script>
@@ -1,9 +1,9 @@
<script lang="ts">
import ApiTagSummary from '$lib/api/components/ApiTagSummary.svelte';
import { Text, Button, Heading, Stack, Icon } from '@immich/ui';
import { type PageData } from './$types';
import { ApiPage } from '$lib/utils/api';
import { ApiPage } from '$lib';
import ApiTagSummary from '$lib/components/ApiTagSummary.svelte';
import { Button, Heading, Icon, Stack, Text } from '@immich/ui';
import { mdiArrowLeft, mdiArrowRight } from '@mdi/js';
import { type PageData } from './$types';
type Props = {
data: PageData;
@@ -15,7 +15,7 @@
</script>
<Stack gap={4}>
<div class="flex justify-between items-center">
<div class="flex items-center justify-between">
<Heading size="giant">API Endpoints</Heading>
<Button href={ApiPage.Endpoints} color="secondary">View All</Button>
</div>
@@ -23,7 +23,7 @@
<ApiTagSummary {tag} />
<Stack gap={4}>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
{#if tag.previous}
<Button href={tag.previous.href} size="giant" fullWidth color="secondary" variant="outline">
<Stack gap={2} class="w-full">
@@ -40,7 +40,7 @@
{#if tag.next}
<Button href={tag.next.href} size="giant" fullWidth color="secondary" variant="outline">
<div class="w-full flex flex-col place-items-end">
<div class="flex w-full flex-col place-items-end">
<Text size="small">Next</Text>
<div class="flex items-center gap-2">
<Text>{tag.next.name}</Text>
@@ -1,7 +1,7 @@
import { getOpenApi } from '$lib/api/services/open-api';
import { ApiPage } from '$lib';
import { getOpenApi } from '$lib/services/open-api';
import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';
import { ApiPage } from '$lib/utils/api';
export const load = (async ({ params, parent }) => {
await parent();
@@ -1,13 +1,13 @@
<script lang="ts">
import ApiAdminRouteBadge from '$lib/api/components/ApiAdminRouteBadge.svelte';
import ApiDeprecatedBadge from '$lib/api/components/ApiDeprecatedBadge.svelte';
import ApiParams from '$lib/api/components/ApiParams.svelte';
import ApiPermission from '$lib/api/components/ApiPermission.svelte';
import ApiPlayground from '$lib/api/components/ApiPlayground.svelte';
import ApiPublicRouteBadge from '$lib/api/components/ApiPublicRouteBadge.svelte';
import ApiSchema from '$lib/api/components/ApiSchema.svelte';
import ApiSharedLinkRouteBadge from '$lib/api/components/ApiSharedLinkRouteBadge.svelte';
import { getEndpointColor, getOpenApi, isRef } from '$lib/api/services/open-api';
import ApiAdminRouteBadge from '$lib/components/ApiAdminRouteBadge.svelte';
import ApiDeprecatedBadge from '$lib/components/ApiDeprecatedBadge.svelte';
import ApiParams from '$lib/components/ApiParams.svelte';
import ApiPermission from '$lib/components/ApiPermission.svelte';
import ApiPlayground from '$lib/components/ApiPlayground.svelte';
import ApiPublicRouteBadge from '$lib/components/ApiPublicRouteBadge.svelte';
import ApiSchema from '$lib/components/ApiSchema.svelte';
import ApiSharedLinkRouteBadge from '$lib/components/ApiSharedLinkRouteBadge.svelte';
import { getEndpointColor, getOpenApi, isRef } from '$lib/services/open-api';
import LinkableHeading from '$lib/components/LinkableHeading.svelte';
import { Button, Heading, Icon, Stack, Text } from '@immich/ui';
import { mdiArrowLeft, mdiArrowRight } from '@mdi/js';
@@ -28,8 +28,8 @@
<Stack gap={8}>
<section>
<div class="flex flex-col gap-2">
<Heading size="large" tag="h1" class="flex flex-col md:flex-row md:justify-between md:items-center">
<span class="group flex items-center gap-1 {endpoint.deprecated ? 'text-gray-500 italic' : ''}">
<Heading size="large" tag="h1" class="flex flex-col md:flex-row md:items-center md:justify-between">
<span class="group flex items-center gap-1 {endpoint.deprecated ? 'italic text-gray-500' : ''}">
<span class="flex gap-2">
<span class={getEndpointColor(endpoint)}>{endpoint.method}</span>
<span>{endpoint.route}</span>
@@ -40,7 +40,7 @@
{#if endpoint.description}
<Text color="muted" fontWeight="bold">{endpoint.description}</Text>
{/if}
<div class="flex flex-col lg:flex-row gap-4 justify-between">
<div class="flex flex-col justify-between gap-4 lg:flex-row">
<div>
{#if endpoint.deprecated}
<span>
@@ -66,7 +66,7 @@
</div>
</div>
</div>
<hr class="mt-4 border border-subtle" />
<hr class="border-subtle mt-4 border" />
</section>
{#if endpoint.params.length > 0}
@@ -97,7 +97,7 @@
<ApiSchema schema={fromRef(endpoint.responses[0].schema)} />
{:else}
<Heading tag="h2" size="small">Responses</Heading>
<div class="px-4 flex gap-1">
<div class="flex gap-1 px-4">
{#each endpoint.responses as response, i (i)}
<span>{response.status}</span>
{#if response.status === 204 || !response.contentType}
@@ -115,7 +115,7 @@
<ApiPlayground {endpoint} />
<Stack gap={4}>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
{#if endpoint.previous}
<Button href={endpoint.previous.href} size="giant" fullWidth color="secondary" variant="outline">
<Stack gap={2} class="w-full">
@@ -132,7 +132,7 @@
{#if endpoint.next}
<Button href={endpoint.next.href} size="giant" fullWidth color="secondary" variant="outline">
<div class="w-full flex flex-col place-items-end">
<div class="flex w-full flex-col place-items-end">
<Text size="small">Next</Text>
<div class="flex items-center gap-2">
<Text>{tag.name}.{endpoint.next.operationId}</Text>
@@ -1,7 +1,7 @@
import { getOpenApi } from '$lib/api/services/open-api';
import { ApiPage } from '$lib';
import { getOpenApi } from '$lib/services/open-api';
import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';
import { ApiPage } from '$lib/utils/api';
export const load = (async ({ params, parent }) => {
await parent();
@@ -1,7 +1,7 @@
<script lang="ts">
import ApiPageContent from '$lib/api/components/ApiPageContent.svelte';
import ApiPermission from '$lib/api/components/ApiPermission.svelte';
import { ApiPage } from '$lib/utils/api';
import { ApiPage } from '$lib';
import ApiPageContent from '$lib/components/ApiPageContent.svelte';
import ApiPermission from '$lib/components/ApiPermission.svelte';
import { Alert, Heading, Link, Text } from '@immich/ui';
import { mdiLightbulbOnOutline } from '@mdi/js';
</script>
@@ -1,6 +1,6 @@
<script lang="ts">
import ApiPageContent from '$lib/api/components/ApiPageContent.svelte';
import { ApiPage } from '$lib/utils/api';
import { ApiPage } from '$lib';
import ApiPageContent from '$lib/components/ApiPageContent.svelte';
import { Heading, Link, Text } from '@immich/ui';
</script>
@@ -1,13 +1,13 @@
<script lang="ts">
import ApiPageContent from '$lib/api/components/ApiPageContent.svelte';
import { getOpenApi } from '$lib/api/services/open-api';
import ApiPageContent from '$lib/components/ApiPageContent.svelte';
import { getOpenApi } from '$lib/services/open-api';
import { Card, CardTitle } from '@immich/ui';
const { models } = getOpenApi();
</script>
<ApiPageContent title="API Models" description="An overview of all the available API models">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-2">
<div class="grid grid-cols-1 gap-2 lg:grid-cols-3">
{#each models as model (model.href)}
<a href={model.href}>
<Card color="secondary" class="">
@@ -1,6 +1,6 @@
<script lang="ts">
import ApiSchema from '$lib/api/components/ApiSchema.svelte';
import type { ApiModel } from '$lib/api/services/open-api';
import ApiSchema from '$lib/components/ApiSchema.svelte';
import type { ApiModel } from '$lib/services/open-api';
import { Code, Heading } from '@immich/ui';
import type { PageData } from './$types';
@@ -24,7 +24,7 @@
</script>
<Heading size="large" class="mb-2" tag="h1">
<a href="#{model.name}" id={model.name} class="scroll-m-8 flex gap-2">
<a href="#{model.name}" id={model.name} class="flex scroll-m-8 gap-2">
<span>{model.name}</span>
</a>
</Heading>
@@ -1,4 +1,4 @@
import { getOpenApi } from '$lib/api/services/open-api';
import { getOpenApi } from '$lib/services/open-api';
import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';
@@ -1,6 +1,6 @@
<script lang="ts">
import ApiPageContent from '$lib/api/components/ApiPageContent.svelte';
import { ApiPage } from '$lib/utils/api';
import { ApiPage } from '$lib';
import ApiPageContent from '$lib/components/ApiPageContent.svelte';
import { Link, Text } from '@immich/ui';
</script>
@@ -1,6 +1,6 @@
<script lang="ts">
import ApiPageContent from '$lib/api/components/ApiPageContent.svelte';
import { ApiPage } from '$lib/utils/api';
import { ApiPage } from '$lib';
import ApiPageContent from '$lib/components/ApiPageContent.svelte';
import { Code, CodeBlock, Constants, Heading, Link, Text } from '@immich/ui';
import { bash } from 'svelte-highlight/languages';
@@ -1,9 +1,9 @@
<script lang="ts">
import { afterNavigate, beforeNavigate } from '$app/navigation';
import { page } from '$app/state';
import { getOpenApi } from '$lib/api/services/open-api';
import { ApiPage } from '$lib';
import '$lib/app.css';
import { ApiPage } from '$lib/utils/api';
import { getOpenApi } from '$lib/services/open-api';
import {
asText,
CommandPalette,
@@ -1,5 +1,5 @@
import { goto } from '$app/navigation';
import { loadOpenApi } from '$lib/api/services/open-api';
import { loadOpenApi } from '$lib/services/open-api';
import type { LayoutLoad } from './$types';
export const ssr = false;
@@ -1,7 +1,7 @@
<script lang="ts">
import Header from '$common/components/Header.svelte';
import PageContent from '$common/components/PageContent.svelte';
import stackedBlocksUrl from '$lib/assets/img/stacked-blocks.svg';
import Header from '$lib/components/Header.svelte';
import PageContent from '$lib/components/PageContent.svelte';
import { AppShell, AppShellHeader, Button, Constants, Heading, Link, Logo, Text, VStack } from '@immich/ui';
import { mdiCubeOutline, mdiOpenInNew } from '@mdi/js';
</script>
@@ -21,8 +21,8 @@
/>
</AppShellHeader>
<PageContent class="mx-auto w-full max-w-(--breakpoint-lg)">
<VStack gap={8} class="mt-4 lg:mt-16 text-center">
<PageContent class="max-w-(--breakpoint-lg) mx-auto w-full">
<VStack gap={8} class="mt-4 text-center lg:mt-16">
<Logo size="giant" variant="logo" class="border" />
<div class="flex flex-col gap-2">
<Heading size="title" tag="h1">Immich API</Heading>
+3
View File
@@ -0,0 +1,3 @@
# allow crawling everything by default
User-agent: *
Disallow:
+28
View File
@@ -0,0 +1,28 @@
import adapter from '@sveltejs/adapter-static';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
import dotenv from 'dotenv';
import { resolve } from 'node:path';
dotenv.config({ path: '../../.env' });
process.env.PUBLIC_IMMICH_SPEC_URL = process.env.PUBLIC_IMMICH_SPEC_URL || 'https://docs.immich.app/openapi.json';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://svelte.dev/docs/kit/integrations
// for more information about preprocessors
preprocess: vitePreprocess(),
kit: {
adapter: adapter({
fallback: 'index.html',
}),
alias: {
$common: '../common',
'$common/*': '../../common/*',
'@immich/ui': resolve('./node_modules/@immich/ui/dist'),
'@mdi/js': resolve('./node_modules/@mdi/js'),
},
},
};
export default config;
@@ -11,9 +11,9 @@
"strict": true,
"moduleResolution": "bundler"
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
// except $lib which is handled by https://kit.svelte.dev/docs/configuration#files
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
// To make changes to top-level options such as include and exclude, we recommend extending
// the generated config; see https://svelte.dev/docs/kit/configuration#typescript
}
+19
View File
@@ -0,0 +1,19 @@
import tailwindcss from '@tailwindcss/vite';
import { defineConfig } from 'vitest/config';
import { sveltekit } from '@sveltejs/kit/vite';
export default defineConfig({
plugins: [tailwindcss(), sveltekit()],
server: {
fs: {
// Allow serving files from one level up to the project root
allow: ['../../common'],
},
},
test: {
expect: { requireAssertions: true },
environment: 'node',
include: ['src/**/*.{test,spec}.{js,ts}'],
exclude: ['src/**/*.svelte.{test,spec}.{js,ts}'],
},
});
+15
View File
@@ -0,0 +1,15 @@
# Package Managers
package-lock.json
pnpm-lock.yaml
yarn.lock
bun.lock
bun.lockb
# Miscellaneous
/static/
# wrangler generated files
worker-configuration.d.ts
/.svelte-kit
/build
+3
View File
@@ -0,0 +1,3 @@
# https://buy.immich.app/
Support Immich by buying a product key.
+37
View File
@@ -0,0 +1,37 @@
import js from '@eslint/js';
import prettier from 'eslint-config-prettier';
import svelte from 'eslint-plugin-svelte';
import globals from 'globals';
import ts from 'typescript-eslint';
export default [
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs['flat/recommended'],
prettier,
...svelte.configs['flat/prettier'],
...svelte.configs.prettier,
{
languageOptions: {
globals: {
...globals.browser,
...globals.node,
},
},
},
{
files: ['**/*.svelte', '**/*.svelte.ts'],
languageOptions: {
parserOptions: {
parser: ts.parser,
},
},
rules: {
'svelte/no-navigation-without-resolve': 'off',
},
},
{
ignores: ['build/', '.svelte-kit/', 'dist/'],
},
];
+51
View File
@@ -0,0 +1,51 @@
{
"name": "buy.immich.app",
"private": true,
"version": "0.0.1",
"license": "GNU Affero General Public License version 3",
"type": "module",
"scripts": {
"start": "vite dev",
"build": "npm run sync && vite build && node ../../bin/postbuild.js",
"preview": "vite preview",
"check": "npm run sync && svelte-check --tsconfig ./tsconfig.json",
"sync": "svelte-kit sync",
"test": "vitest",
"lint": "eslint . --max-warnings 0",
"lint:fix": "npm run lint -- --fix",
"format": "prettier --check .",
"format:fix": "prettier --write ."
},
"devDependencies": {
"@eslint/compat": "^1.4.0",
"@eslint/js": "^9.36.0",
"@sveltejs/adapter-static": "^3.0.9",
"@sveltejs/kit": "^2.43.2",
"@sveltejs/vite-plugin-svelte": "^6.2.0",
"@tailwindcss/vite": "^4.1.13",
"@types/node": "^22",
"@vitest/browser": "^3.2.4",
"dotenv": "^17.2.3",
"eslint": "^9.36.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-svelte": "^3.12.4",
"globals": "^16.4.0",
"playwright": "^1.55.1",
"prettier": "^3.6.2",
"prettier-plugin-svelte": "^3.4.0",
"prettier-plugin-tailwindcss": "^0.6.14",
"svelte": "^5.39.5",
"svelte-check": "^4.3.2",
"tailwindcss": "^4.1.13",
"typescript": "^5.9.2",
"typescript-eslint": "^8.44.1",
"vite": "^7.1.7",
"vitest": "^3.2.4",
"vitest-browser-svelte": "^1.1.0"
},
"dependencies": {
"@immich/ui": "^0.35.0",
"@mdi/js": "^7.4.47",
"svelte-highlight": "^7.8.4"
}
}
+13
View File
@@ -0,0 +1,13 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};
@@ -21,7 +21,7 @@
<meta name="twitter:image" content="/img/social-preview.png" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover" class="dark bg-light text-dark">
<body data-sveltekit-preload-data="hover" class="bg-light text-dark dark">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
+7
View File
@@ -0,0 +1,7 @@
import { describe, it, expect } from 'vitest';
describe('sum test', () => {
it('adds 1 + 2 to equal 3', () => {
expect(1 + 2).toBe(3);
});
});
+5
View File
@@ -0,0 +1,5 @@
@import 'tailwindcss';
@import '@immich/ui/theme/default.css';
@import '$common/theme/theme.css';
@source '../../node_modules/@immich/ui/dist';
@source '../../../../common';
@@ -1,5 +1,5 @@
<script lang="ts">
import { getRedirectUrl } from '$lib/utils/license';
import { getRedirectUrl } from '$lib';
import { Button, Card, CardBody, CardFooter, CardHeader, CardTitle, Stack, Text } from '@immich/ui';
import { mdiCheckCircleOutline, mdiContentCopy } from '@mdi/js';
@@ -33,16 +33,16 @@
<CardBody>
<Stack gap={4}>
<div>
<div class="bg-primary/10 py-2 px-6 rounded-lg">
<div class="bg-primary/10 rounded-lg px-6 py-2">
<Text color="primary">{productKey}</Text>
</div>
</div>
</Stack>
</CardBody>
<CardFooter>
<div class="w-full grid grid-cols-1 md:grid-cols-2 gap-2">
<div class="grid w-full grid-cols-1 gap-2 md:grid-cols-2">
<Button href={getRedirectUrl(productKey, 'https://my.immich.app')} fullWidth size="medium">Activate</Button>
<div class="flex justify-center items-center">
<div class="flex items-center justify-center">
{#if clipboardStatus === 'error'}
<Text color="danger">Unable to copy to clipboard</Text>
{:else if clipboardStatus === 'success'}
@@ -1,6 +1,6 @@
<script lang="ts">
import FullPageLayout from '$lib/layouts/FullPageLayout.svelte';
import { getCallbackUrl, ImmichLicense } from '$lib/utils/license';
import FullPageLayout from '$common/components/FullPageLayout.svelte';
import { getCallbackUrl, ImmichLicense } from '$lib';
import { Button, Heading, Icon, Logo, Stack, SupporterBadge, Text } from '@immich/ui';
import { mdiAccount, mdiCheckCircleOutline, mdiServer } from '@mdi/js';
@@ -22,19 +22,19 @@
<section>
<Stack gap={4}>
<div
class="gradiant-bg dark:bg-gray-900 border border-gray-300 dark:border-gray-800 rounded-2xl w-full hover:bg-immich-primary/10 dark:hover:bg-immich-primary/20 p-6"
class="gradiant-bg hover:bg-immich-primary/10 dark:hover:bg-immich-primary/20 w-full rounded-2xl border border-gray-300 p-6 dark:border-gray-800 dark:bg-gray-900"
>
<div class="">
<div class="w-full flex place-items-center place-content-center">
<div class="flex w-full place-content-center place-items-center">
<img
src="https://imgproxy.fourthwall.com/DuRc1wOto_bXVrM-sqTsf17VxnxBXUFEQ5F_PT6sphQ/w:1920/sm:1/enc/CqJpqXXfIjg43qs5/dYp-7fXLhSxgi23i/vXvGKj_Wr12gyskx/XpH5YYXnecOLkEZD/Df2Y8QRXm5WNGa0M/tTxj3yhuyZNfwLfh/HIAL4muGjJJgH77a/m4-0PI8wYVzKDLfL/aKzTzjksdRXbrDIs/C3FH6IfFkB64QwBV/I4q06GTK8g4kyH_x/R_0WQk6JhX0eicl8/yzswcthmPd9IwLY-/daD-bA"
alt="Immich Retro Disk"
class="w-full md:w-1/2 aspect-square object-cover rounded-lg"
class="aspect-square w-full rounded-lg object-cover md:w-1/2"
/>
</div>
<div
class="flex-1 flex flex-col sm:flex-row gap-4 sm:justify-between sm:min-w-0 mt-6 sm:place-items-center"
class="mt-6 flex flex-1 flex-col gap-4 sm:min-w-0 sm:flex-row sm:place-items-center sm:justify-between"
>
<div class="flex flex-col gap-2 sm:min-w-0 sm:flex-1">
<p class="text-primary retro-font text-4xl">Retro Disk</p>
@@ -48,7 +48,7 @@
</Stack>
</div>
<div class="flex flex-col sm:items-end sm:justify-start sm:flex-shrink-0">
<div class="flex flex-col sm:flex-shrink-0 sm:items-end sm:justify-start">
<Heading size="giant" class="text-6xl sm:whitespace-nowrap">$25</Heading>
<Text class="sm:whitespace-nowrap">+ shipping</Text>
</div>
@@ -61,21 +61,21 @@
</div>
<hr />
<div class="flex gap-4 justify-between flex-wrap lg:flex-nowrap">
<div class="flex flex-wrap justify-between gap-4 lg:flex-nowrap">
<!-- SERVER -->
<div
class="flex flex-col gap-4 border border-gray-300 dark:border-gray-800 w-full p-4 md:p-8 rounded-2xl bg-gray-100 dark:bg-gray-900 hover:bg-immich-primary/10 dark:hover:bg-immich-primary/20 transition-all"
class="hover:bg-immich-primary/10 dark:hover:bg-immich-primary/20 flex w-full flex-col gap-4 rounded-2xl border border-gray-300 bg-gray-100 p-4 transition-all md:p-8 dark:border-gray-800 dark:bg-gray-900"
>
<div class="flex flex-col sm:flex-row gap-4 sm:items-start lg:flex-col lg:items-start">
<div class="flex-shrink-0 w-full sm:w-32 md:w-48 lg:w-full">
<div class="flex flex-col gap-1 text-primary items-start">
<div class="flex flex-col gap-4 sm:flex-row sm:items-start lg:flex-col lg:items-start">
<div class="w-full flex-shrink-0 sm:w-32 md:w-48 lg:w-full">
<div class="text-primary flex flex-col items-start gap-1">
<Icon icon={mdiServer} size="56" />
<Text fontWeight="bold" size="large">Server</Text>
</div>
</div>
<div class="flex-1 flex flex-col sm:flex-row gap-4 sm:justify-between sm:min-w-0 lg:flex-col lg:gap-8">
<Stack gap={1} class="sm:flex-1 sm:min-w-0">
<div class="flex flex-1 flex-col gap-4 sm:min-w-0 sm:flex-row sm:justify-between lg:flex-col lg:gap-8">
<Stack gap={1} class="sm:min-w-0 sm:flex-1">
{#each serverFeatures as feature, index (index)}
<div class="grid grid-cols-[36px_auto] items-center">
<Icon icon={mdiCheckCircleOutline} size="24" class="text-success" />
@@ -84,7 +84,7 @@
{/each}
</Stack>
<div class="flex flex-col sm:items-end sm:justify-start sm:flex-shrink-0 lg:items-start">
<div class="flex flex-col sm:flex-shrink-0 sm:items-end sm:justify-start lg:items-start">
<Heading size="giant" class="text-6xl sm:whitespace-nowrap">$100</Heading>
<Text class="sm:whitespace-nowrap">per server</Text>
</div>
@@ -96,18 +96,18 @@
<!-- INDIVIDUAL -->
<div
class="flex flex-col gap-4 border border-gray-300 dark:border-gray-800 w-full p-4 md:p-8 rounded-2xl bg-gray-100 dark:bg-gray-900 hover:bg-immich-primary/10 dark:hover:bg-immich-primary/20 transition-all"
class="hover:bg-immich-primary/10 dark:hover:bg-immich-primary/20 flex w-full flex-col gap-4 rounded-2xl border border-gray-300 bg-gray-100 p-4 transition-all md:p-8 dark:border-gray-800 dark:bg-gray-900"
>
<div class="flex flex-col sm:flex-row gap-4 sm:items-start lg:flex-col lg:items-start">
<div class="flex-shrink-0 w-full sm:w-32 md:w-48 lg:w-full">
<div class="flex flex-col gap-1 text-primary items-start">
<div class="flex flex-col gap-4 sm:flex-row sm:items-start lg:flex-col lg:items-start">
<div class="w-full flex-shrink-0 sm:w-32 md:w-48 lg:w-full">
<div class="text-primary flex flex-col items-start gap-1">
<Icon icon={mdiAccount} size="56" />
<Text fontWeight="bold" size="large">Individual</Text>
</div>
</div>
<div class="flex-1 flex flex-col sm:flex-row gap-4 sm:justify-between sm:min-w-0 lg:flex-col lg:gap-8">
<Stack gap={1} class="sm:flex-1 sm:min-w-0">
<div class="flex flex-1 flex-col gap-4 sm:min-w-0 sm:flex-row sm:justify-between lg:flex-col lg:gap-8">
<Stack gap={1} class="sm:min-w-0 sm:flex-1">
{#each userFeatures as feature, index (index)}
<div class="grid grid-cols-[36px_auto] items-center">
<Icon icon={mdiCheckCircleOutline} size="24" class="text-success" />
@@ -116,7 +116,7 @@
{/each}
</Stack>
<div class="flex flex-col sm:items-end sm:justify-start sm:flex-shrink-0 lg:items-start">
<div class="flex flex-col sm:flex-shrink-0 sm:items-end sm:justify-start lg:items-start">
<Heading size="giant" class="text-6xl sm:whitespace-nowrap">$25</Heading>
<Text class="sm:whitespace-nowrap">per user</Text>
</div>
@@ -1,4 +1,4 @@
import { getCallbackUrl, ImmichLicense } from '$lib/utils/license';
import { getCallbackUrl, ImmichLicense } from '$lib';
import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';
@@ -1,7 +1,7 @@
<script lang="ts">
import DefaultPageLayout from '$common/components/DefaultPageLayout.svelte';
import { getPaymentStatus, getRedirectUrl, PurchaseStatus, type PaymentStatusResponseDto } from '$lib';
import LicenseKey from '$lib/components/LicenseKey.svelte';
import DefaultPageLayout from '$lib/layouts/DefaultPageLayout.svelte';
import { getPaymentStatus, getRedirectUrl, PurchaseStatus, type PaymentStatusResponseDto } from '$lib/utils/license';
import { Alert, Button, Card, CardBody, Heading, LoadingSpinner, Stack, Text } from '@immich/ui';
import { onMount } from 'svelte';
import type { PageData } from './$types';
+3
View File
@@ -0,0 +1,3 @@
# allow crawling everything by default
User-agent: *
Disallow:
+28
View File
@@ -0,0 +1,28 @@
import adapter from '@sveltejs/adapter-static';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
import dotenv from 'dotenv';
import { resolve } from 'node:path';
dotenv.config({ path: '../../.env' });
process.env.PUBLIC_IMMICH_PAY_HOST = process.env.PUBLIC_IMMICH_PAY_HOST || 'https://pay.futo.org';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://svelte.dev/docs/kit/integrations
// for more information about preprocessors
preprocess: vitePreprocess(),
kit: {
adapter: adapter({
fallback: 'index.html',
}),
alias: {
$common: '../common',
'$common/*': '../../common/*',
'@immich/ui': resolve('./node_modules/@immich/ui/dist'),
'@mdi/js': resolve('./node_modules/@mdi/js'),
},
},
};
export default config;
+19
View File
@@ -0,0 +1,19 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
//
// To make changes to top-level options such as include and exclude, we recommend extending
// the generated config; see https://svelte.dev/docs/kit/configuration#typescript
}
+19
View File
@@ -0,0 +1,19 @@
import tailwindcss from '@tailwindcss/vite';
import { defineConfig } from 'vitest/config';
import { sveltekit } from '@sveltejs/kit/vite';
export default defineConfig({
plugins: [tailwindcss(), sveltekit()],
server: {
fs: {
// Allow serving files from one level up to the project root
allow: ['../../common'],
},
},
test: {
expect: { requireAssertions: true },
environment: 'node',
include: ['src/**/*.{test,spec}.{js,ts}'],
exclude: ['src/**/*.svelte.{test,spec}.{js,ts}'],
},
});
+15
View File
@@ -0,0 +1,15 @@
# Package Managers
package-lock.json
pnpm-lock.yaml
yarn.lock
bun.lock
bun.lockb
# Miscellaneous
/static/
# wrangler generated files
worker-configuration.d.ts
/.svelte-kit
/build
+3
View File
@@ -0,0 +1,3 @@
# https://datasets.immich.app/
Asset collection website
File diff suppressed because it is too large Load Diff
+37
View File
@@ -0,0 +1,37 @@
import js from '@eslint/js';
import prettier from 'eslint-config-prettier';
import svelte from 'eslint-plugin-svelte';
import globals from 'globals';
import ts from 'typescript-eslint';
export default [
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs['flat/recommended'],
prettier,
...svelte.configs['flat/prettier'],
...svelte.configs.prettier,
{
languageOptions: {
globals: {
...globals.browser,
...globals.node,
},
},
},
{
files: ['**/*.svelte', '**/*.svelte.ts'],
languageOptions: {
parserOptions: {
parser: ts.parser,
},
},
rules: {
'svelte/no-navigation-without-resolve': 'off',
},
},
{
ignores: ['build/', '.svelte-kit/', 'dist/', 'backend/worker-configuration.d.ts'],
},
];
+54
View File
@@ -0,0 +1,54 @@
{
"name": "datasets.immich.app",
"private": true,
"version": "0.0.1",
"license": "GNU Affero General Public License version 3",
"type": "module",
"scripts": {
"start": "vite dev",
"build": "npm run sync && vite build && node ../../bin/postbuild.js",
"preview": "vite preview",
"check": "npm run sync && svelte-check --tsconfig ./tsconfig.json",
"sync": "svelte-kit sync",
"test": "vitest",
"lint": "eslint . --max-warnings 0",
"lint:fix": "npm run lint -- --fix",
"format": "prettier --check .",
"format:fix": "prettier --write ."
},
"devDependencies": {
"@eslint/compat": "^1.4.0",
"@eslint/js": "^9.36.0",
"@sveltejs/adapter-static": "^3.0.9",
"@sveltejs/kit": "^2.43.2",
"@sveltejs/vite-plugin-svelte": "^6.2.0",
"@tailwindcss/vite": "^4.1.13",
"@types/node": "^22",
"@vitest/browser": "^3.2.4",
"dotenv": "^17.2.3",
"eslint": "^9.36.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-svelte": "^3.12.4",
"globals": "^16.4.0",
"playwright": "^1.55.1",
"prettier": "^3.6.2",
"prettier-plugin-svelte": "^3.4.0",
"prettier-plugin-tailwindcss": "^0.6.14",
"svelte": "^5.39.5",
"svelte-check": "^4.3.2",
"tailwindcss": "^4.1.13",
"typescript": "^5.9.2",
"typescript-eslint": "^8.44.1",
"vite": "^7.1.7",
"vitest": "^3.2.4",
"vitest-browser-svelte": "^1.1.0"
},
"dependencies": {
"@immich/ui": "^0.35.0",
"@mdi/js": "^7.4.47",
"exifreader": "^4.32.0",
"p-limit": "^7.1.1",
"svelte-highlight": "^7.8.4",
"svelte-turnstile": "^0.11.0"
}
}
+13
View File
@@ -0,0 +1,13 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};
@@ -21,7 +21,7 @@
<meta name="twitter:image" content="/img/social-preview.png" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover" class="dark bg-light text-dark">
<body data-sveltekit-preload-data="hover" class="bg-light text-dark dark">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
@@ -0,0 +1,7 @@
import { describe, it, expect } from 'vitest';
describe('sum test', () => {
it('adds 1 + 2 to equal 3', () => {
expect(1 + 2).toBe(3);
});
});
+5
View File
@@ -0,0 +1,5 @@
@import 'tailwindcss';
@import '@immich/ui/theme/default.css';
@import '$common/theme/theme.css';
@source '../../node_modules/@immich/ui/dist';
@source '../../../../common';
@@ -157,7 +157,7 @@
{#if dragStartTarget}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="fixed inset-0 flex h-full w-full flex-col items-center justify-center bg-gray-100/90 text-immich-dark-gray dark:bg-neutral-900/90 dark:text-immich-gray z-10"
class="text-immich-dark-gray dark:text-immich-gray fixed inset-0 z-10 flex h-full w-full flex-col items-center justify-center bg-gray-100/90 dark:bg-neutral-900/90"
transition:fade={{ duration: 250 }}
ondragover={onDragOver}
>
@@ -1,5 +1,5 @@
<script lang="ts">
import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { Button, Modal, ModalBody, ModalFooter, Text } from '@immich/ui';
type Props = { onClose: () => void };
@@ -8,7 +8,7 @@
<Modal title="Upload failed" size="medium" {onClose}>
<ModalBody>
<p>There was an error while trying to upload your dataset. Please try again later.</p>
<Text>There was an error while trying to upload your dataset. Please try again later.</Text>
</ModalBody>
<ModalFooter>
<Button onclick={onClose} shape="round">Close</Button>
@@ -3,7 +3,7 @@
import { Button, Checkbox, Field, HelperText, Input, Modal, ModalBody, ModalFooter, Stack } from '@immich/ui';
import pLimit from 'p-limit';
import { Turnstile } from 'svelte-turnstile';
import type { UploadableAssets } from '../../../apps/datasets.immich.app/types/upload-manager';
import type { UploadableAssets } from '../../../types/upload-manager';
interface Props {
onClose: (event: { cancelled: boolean; failedIds: string[] }) => void;
@@ -1,7 +1,7 @@
import { mdiAccount, mdiCameraBurst, mdiImage, mdiPanoramaVariant, mdiPencil, mdiSphere } from '@mdi/js';
import EXIFReader from 'exifreader';
import type { ExifDatasetMetadata } from '../../../types/metadata';
import type { UploadableAssets } from '../../../types/upload-manager';
import type { ExifDatasetMetadata } from '../../types/metadata';
import type { UploadableAssets } from '../../types/upload-manager';
export type AssetType = ExifDatasetMetadata['captureType'];
export const AssetTypeNames: Record<AssetType, string> = {
@@ -3,9 +3,9 @@
import { initializeTheme } from '@immich/ui';
import type { Snippet } from 'svelte';
interface Props {
type Props = {
children?: Snippet;
}
};
let { children }: Props = $props();

Some files were not shown because too many files have changed in this diff Show More