mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 18:41:08 +02:00
feat(mukke): display album cover art in library, playlists, and song lists
Add batch cover-url endpoint (POST /library/cover-urls) to efficiently resolve multiple cover art presigned URLs in a single request. Integrate cover art display across all UI surfaces: album grid, album detail header, song list thumbnails, playlist grid, and playlist detail song list. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
789ce0a435
commit
e848fa5342
81 changed files with 376 additions and 58 deletions
1
apps/calendar/apps/web/src/lib/version.ts
Normal file
1
apps/calendar/apps/web/src/lib/version.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const APP_VERSION = '1.0.0';
|
||||
|
|
@ -14,6 +14,7 @@
|
|||
FilterDropdown,
|
||||
type FilterDropdownOption,
|
||||
} from '@manacore/shared-ui';
|
||||
import { APP_VERSION } from '$lib/version';
|
||||
import type { CalendarViewType, Calendar } from '@calendar/shared';
|
||||
|
||||
// Calendar management state
|
||||
|
|
@ -651,6 +652,8 @@
|
|||
</div>
|
||||
</SettingsCard>
|
||||
</SettingsSection>
|
||||
|
||||
<p class="mt-8 pb-4 text-center text-xs text-gray-400 dark:text-gray-600">v{APP_VERSION}</p>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import AppSlider from '$lib/components/AppSlider.svelte';
|
||||
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
|
||||
import { APP_VERSION } from '$lib/version';
|
||||
import '$lib/i18n';
|
||||
|
||||
// Get redirect URL from query params or sessionStorage (set by AuthGateModal)
|
||||
|
|
@ -66,6 +67,7 @@
|
|||
{translations}
|
||||
{verified}
|
||||
{initialEmail}
|
||||
version={APP_VERSION}
|
||||
>
|
||||
{#snippet headerControls()}
|
||||
<LanguageSelector />
|
||||
|
|
|
|||
1
apps/chat/apps/web/src/lib/version.ts
Normal file
1
apps/chat/apps/web/src/lib/version.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const APP_VERSION = '0.3.0';
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import AppSlider from '$lib/components/AppSlider.svelte';
|
||||
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
|
||||
import { APP_VERSION } from '$lib/version';
|
||||
import '$lib/i18n';
|
||||
|
||||
// Dev credentials - pre-filled in development mode
|
||||
|
|
@ -74,6 +75,7 @@
|
|||
{verified}
|
||||
{initialEmail}
|
||||
{initialPassword}
|
||||
version={APP_VERSION}
|
||||
>
|
||||
{#snippet headerControls()}
|
||||
<LanguageSelector />
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { userSettings } from '$lib/stores/user-settings.svelte';
|
||||
import { APP_VERSION } from '$lib/version';
|
||||
import {
|
||||
SettingsPage,
|
||||
SettingsSection,
|
||||
|
|
@ -171,4 +172,6 @@
|
|||
<a href="#" class="text-[hsl(var(--primary))] hover:underline">Hilfe & Support</a>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
|
||||
<p class="mt-8 pb-4 text-center text-xs text-gray-400 dark:text-gray-600">v{APP_VERSION}</p>
|
||||
</SettingsPage>
|
||||
|
|
|
|||
1
apps/clock/apps/web/src/lib/version.ts
Normal file
1
apps/clock/apps/web/src/lib/version.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const APP_VERSION = '0.2.0';
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
import { _ } from 'svelte-i18n';
|
||||
import { userSettings } from '$lib/stores/user-settings.svelte';
|
||||
import { GlobalSettingsSection } from '@manacore/shared-ui';
|
||||
import { APP_VERSION } from '$lib/version';
|
||||
|
||||
onMount(async () => {
|
||||
await userSettings.load();
|
||||
|
|
@ -102,4 +103,6 @@
|
|||
Töne können für einzelne Wecker und Timer in deren Einstellungen angepasst werden.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p class="mt-8 pb-4 text-center text-xs text-gray-400 dark:text-gray-600">v{APP_VERSION}</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
import { getLoginTranslations } from '@manacore/shared-i18n';
|
||||
import { ClockLogo } from '@manacore/shared-branding';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { APP_VERSION } from '$lib/version';
|
||||
import '$lib/i18n';
|
||||
|
||||
// Read verification status from query params (set after email verification)
|
||||
|
|
@ -64,4 +65,5 @@
|
|||
{translations}
|
||||
{verified}
|
||||
{initialEmail}
|
||||
version={APP_VERSION}
|
||||
/>
|
||||
|
|
|
|||
1
apps/contacts/apps/web/src/lib/version.ts
Normal file
1
apps/contacts/apps/web/src/lib/version.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const APP_VERSION = '1.0.0';
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
import { goto } from '$app/navigation';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { userSettings } from '$lib/stores/user-settings.svelte';
|
||||
import { APP_VERSION } from '$lib/version';
|
||||
import {
|
||||
contactsSettings,
|
||||
type ContactView,
|
||||
|
|
@ -638,4 +639,6 @@
|
|||
{/snippet}
|
||||
</SettingsDangerButton>
|
||||
</SettingsDangerZone>
|
||||
|
||||
<p class="mt-8 pb-4 text-center text-xs text-gray-400 dark:text-gray-600">v{APP_VERSION}</p>
|
||||
</SettingsPage>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import AppSlider from '$lib/components/AppSlider.svelte';
|
||||
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
|
||||
import { APP_VERSION } from '$lib/version';
|
||||
import '$lib/i18n';
|
||||
|
||||
// Get redirect URL from query params or sessionStorage
|
||||
|
|
@ -63,6 +64,7 @@
|
|||
{translations}
|
||||
{verified}
|
||||
{initialEmail}
|
||||
version={APP_VERSION}
|
||||
>
|
||||
{#snippet headerControls()}
|
||||
<LanguageSelector />
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ scores:
|
|||
security: 82
|
||||
ux: 88
|
||||
status: 'production'
|
||||
version: '1.0.0'
|
||||
stats:
|
||||
backendModules: 13
|
||||
webRoutes: 19
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ scores:
|
|||
security: 82
|
||||
ux: 80
|
||||
status: 'production'
|
||||
version: '0.3.0'
|
||||
stats:
|
||||
backendModules: 9
|
||||
webRoutes: 24
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ scores:
|
|||
security: 60
|
||||
ux: 55
|
||||
status: 'beta'
|
||||
version: '0.2.0'
|
||||
stats:
|
||||
backendModules: 7
|
||||
webRoutes: 17
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ scores:
|
|||
security: 85
|
||||
ux: 85
|
||||
status: 'production'
|
||||
version: '1.0.0'
|
||||
stats:
|
||||
backendModules: 14
|
||||
webRoutes: 20
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ scores:
|
|||
security: 68
|
||||
ux: 65
|
||||
status: 'beta'
|
||||
version: '0.1.0'
|
||||
stats:
|
||||
backendModules: 5
|
||||
webRoutes: 15
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ scores:
|
|||
security: 72
|
||||
ux: 75
|
||||
status: 'beta'
|
||||
version: '0.2.0'
|
||||
stats:
|
||||
backendModules: 0
|
||||
webRoutes: 33
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ scores:
|
|||
security: 55
|
||||
ux: 68
|
||||
status: 'alpha'
|
||||
version: '0.2.0'
|
||||
stats:
|
||||
backendModules: 2
|
||||
webRoutes: 19
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ scores:
|
|||
security: 88
|
||||
ux: 82
|
||||
status: 'production'
|
||||
version: '0.2.0'
|
||||
stats:
|
||||
backendModules: 0
|
||||
webRoutes: 7
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ scores:
|
|||
security: 78
|
||||
ux: 60
|
||||
status: 'beta'
|
||||
version: '0.2.0'
|
||||
stats:
|
||||
backendModules: 11
|
||||
webRoutes: 16
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ scores:
|
|||
security: 68
|
||||
ux: 55
|
||||
status: 'beta'
|
||||
version: '0.2.0'
|
||||
stats:
|
||||
backendModules: 8
|
||||
webRoutes: 10
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ scores:
|
|||
security: 65
|
||||
ux: 55
|
||||
status: 'beta'
|
||||
version: '0.2.0'
|
||||
stats:
|
||||
backendModules: 7
|
||||
webRoutes: 12
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ scores:
|
|||
security: 80
|
||||
ux: 78
|
||||
status: 'production'
|
||||
version: '0.3.0'
|
||||
stats:
|
||||
backendModules: 11
|
||||
webRoutes: 19
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ scores:
|
|||
security: 55
|
||||
ux: 50
|
||||
status: 'alpha'
|
||||
version: '0.1.0'
|
||||
stats:
|
||||
backendModules: 6
|
||||
webRoutes: 12
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ scores:
|
|||
security: 55
|
||||
ux: 68
|
||||
status: 'beta'
|
||||
version: '0.2.0'
|
||||
stats:
|
||||
backendModules: 7
|
||||
webRoutes: 16
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ scores:
|
|||
security: 55
|
||||
ux: 55
|
||||
status: 'alpha'
|
||||
version: '0.1.0'
|
||||
stats:
|
||||
backendModules: 8
|
||||
webRoutes: 12
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ scores:
|
|||
security: 65
|
||||
ux: 72
|
||||
status: 'beta'
|
||||
version: '0.2.0'
|
||||
stats:
|
||||
backendModules: 4
|
||||
webRoutes: 6
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ scores:
|
|||
security: 72
|
||||
ux: 55
|
||||
status: 'beta'
|
||||
version: '0.2.0'
|
||||
stats:
|
||||
backendModules: 10
|
||||
webRoutes: 17
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ scores:
|
|||
security: 82
|
||||
ux: 85
|
||||
status: 'production'
|
||||
version: '1.0.0'
|
||||
stats:
|
||||
backendModules: 7
|
||||
webRoutes: 13
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ scores:
|
|||
security: 55
|
||||
ux: 35
|
||||
status: 'alpha'
|
||||
version: '0.0.1'
|
||||
stats:
|
||||
backendModules: 7
|
||||
webRoutes: 0
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ scores:
|
|||
security: 70
|
||||
ux: 75
|
||||
status: 'beta'
|
||||
version: '0.2.0'
|
||||
stats:
|
||||
backendModules: 5
|
||||
webRoutes: 13
|
||||
|
|
|
|||
1
apps/manacore/apps/web/src/lib/version.ts
Normal file
1
apps/manacore/apps/web/src/lib/version.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const APP_VERSION = '0.2.0';
|
||||
|
|
@ -5,6 +5,7 @@
|
|||
import { creditsService } from '$lib/api/credits';
|
||||
import type { CreditBalance } from '$lib/api/credits';
|
||||
import { userSettings } from '$lib/stores/user-settings.svelte';
|
||||
import { APP_VERSION } from '$lib/version';
|
||||
|
||||
let loading = $state(true);
|
||||
let savingProfile = $state(false);
|
||||
|
|
@ -333,5 +334,7 @@
|
|||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<p class="mt-8 pb-4 text-center text-xs text-gray-400 dark:text-gray-600">v{APP_VERSION}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
import AppSlider from '$lib/components/AppSlider.svelte';
|
||||
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { APP_VERSION } from '$lib/version';
|
||||
|
||||
// Get translations based on current locale
|
||||
const translations = $derived(getLoginTranslations($locale || 'de'));
|
||||
|
|
@ -42,6 +43,7 @@
|
|||
{translations}
|
||||
{verified}
|
||||
{initialEmail}
|
||||
version={APP_VERSION}
|
||||
>
|
||||
{#snippet headerControls()}
|
||||
<LanguageSelector />
|
||||
|
|
|
|||
1
apps/manadeck/apps/web/src/lib/version.ts
Normal file
1
apps/manadeck/apps/web/src/lib/version.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const APP_VERSION = '0.2.0';
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { userSettings } from '$lib/stores/user-settings.svelte';
|
||||
import { APP_VERSION } from '$lib/version';
|
||||
import {
|
||||
SettingsPage,
|
||||
SettingsSection,
|
||||
|
|
@ -41,4 +42,6 @@
|
|||
</SettingsRow>
|
||||
</SettingsCard>
|
||||
</SettingsSection>
|
||||
|
||||
<p class="mt-8 pb-4 text-center text-xs text-gray-400 dark:text-gray-600">v{APP_VERSION}</p>
|
||||
</SettingsPage>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
import AppSlider from '$lib/components/AppSlider.svelte';
|
||||
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { APP_VERSION } from '$lib/version';
|
||||
|
||||
// Get translations based on current locale
|
||||
const translations = $derived(getLoginTranslations($locale || 'de'));
|
||||
|
|
@ -42,6 +43,7 @@
|
|||
{translations}
|
||||
{verified}
|
||||
{initialEmail}
|
||||
version={APP_VERSION}
|
||||
>
|
||||
{#snippet headerControls()}
|
||||
<LanguageSelector />
|
||||
|
|
|
|||
1
apps/matrix/apps/web/src/lib/version.ts
Normal file
1
apps/matrix/apps/web/src/lib/version.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const APP_VERSION = '0.2.0';
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { matrixStore } from '$lib/matrix';
|
||||
import { goto } from '$app/navigation';
|
||||
import { APP_VERSION } from '$lib/version';
|
||||
import {
|
||||
ArrowLeft,
|
||||
User,
|
||||
|
|
@ -228,8 +229,8 @@
|
|||
<button
|
||||
class="flex flex-col items-center gap-2 rounded-xl p-4 transition-all
|
||||
{theme.mode === 'light'
|
||||
? 'bg-primary text-primary-foreground ring-2 ring-primary'
|
||||
: 'bg-muted hover:bg-muted/80'}"
|
||||
? 'bg-primary text-primary-foreground ring-2 ring-primary'
|
||||
: 'bg-muted hover:bg-muted/80'}"
|
||||
onclick={() => theme.setMode('light')}
|
||||
>
|
||||
<Sun class="h-6 w-6" />
|
||||
|
|
@ -239,8 +240,8 @@
|
|||
<button
|
||||
class="flex flex-col items-center gap-2 rounded-xl p-4 transition-all
|
||||
{theme.mode === 'dark'
|
||||
? 'bg-primary text-primary-foreground ring-2 ring-primary'
|
||||
: 'bg-muted hover:bg-muted/80'}"
|
||||
? 'bg-primary text-primary-foreground ring-2 ring-primary'
|
||||
: 'bg-muted hover:bg-muted/80'}"
|
||||
onclick={() => theme.setMode('dark')}
|
||||
>
|
||||
<Moon class="h-6 w-6" />
|
||||
|
|
@ -250,8 +251,8 @@
|
|||
<button
|
||||
class="flex flex-col items-center gap-2 rounded-xl p-4 transition-all
|
||||
{theme.mode === 'system'
|
||||
? 'bg-primary text-primary-foreground ring-2 ring-primary'
|
||||
: 'bg-muted hover:bg-muted/80'}"
|
||||
? 'bg-primary text-primary-foreground ring-2 ring-primary'
|
||||
: 'bg-muted hover:bg-muted/80'}"
|
||||
onclick={() => theme.setMode('system')}
|
||||
>
|
||||
<Desktop class="h-6 w-6" />
|
||||
|
|
@ -309,7 +310,9 @@
|
|||
{:else}
|
||||
<div class="space-y-3">
|
||||
<!-- Enable/Disable Toggle -->
|
||||
<label class="flex items-center justify-between rounded-lg bg-muted p-4 cursor-pointer">
|
||||
<label
|
||||
class="flex items-center justify-between rounded-lg bg-muted p-4 cursor-pointer"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<BellRinging class="h-6 w-6" />
|
||||
<div>
|
||||
|
|
@ -330,7 +333,9 @@
|
|||
|
||||
<!-- Sound Toggle -->
|
||||
<label
|
||||
class="flex items-center justify-between rounded-lg bg-muted p-4 cursor-pointer {!notificationSettings.enabled ? 'opacity-50' : ''}"
|
||||
class="flex items-center justify-between rounded-lg bg-muted p-4 cursor-pointer {!notificationSettings.enabled
|
||||
? 'opacity-50'
|
||||
: ''}"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<SpeakerHigh class="h-6 w-6" />
|
||||
|
|
@ -350,7 +355,9 @@
|
|||
|
||||
<!-- Preview Toggle -->
|
||||
<label
|
||||
class="flex items-center justify-between rounded-lg bg-muted p-4 cursor-pointer {!notificationSettings.enabled ? 'opacity-50' : ''}"
|
||||
class="flex items-center justify-between rounded-lg bg-muted p-4 cursor-pointer {!notificationSettings.enabled
|
||||
? 'opacity-50'
|
||||
: ''}"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<Eye class="h-6 w-6" />
|
||||
|
|
@ -386,6 +393,8 @@
|
|||
</button>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<p class="mt-8 pb-4 text-center text-xs text-gray-400 dark:text-gray-600">v{APP_VERSION}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Controller, Get, Param, UseGuards } from '@nestjs/common';
|
||||
import { Controller, Get, Post, Param, Body, UseGuards } from '@nestjs/common';
|
||||
import { JwtAuthGuard, CurrentUser, CurrentUserData } from '@manacore/shared-nestjs-auth';
|
||||
import { LibraryService } from './library.service';
|
||||
|
||||
|
|
@ -7,6 +7,12 @@ import { LibraryService } from './library.service';
|
|||
export class LibraryController {
|
||||
constructor(private readonly libraryService: LibraryService) {}
|
||||
|
||||
@Post('cover-urls')
|
||||
async getCoverUrls(@Body() body: { paths: string[] }) {
|
||||
const urls = await this.libraryService.getCoverUrls(body.paths ?? []);
|
||||
return { urls };
|
||||
}
|
||||
|
||||
@Get('albums')
|
||||
async getAlbums(@CurrentUser() user: CurrentUserData) {
|
||||
const albums = await this.libraryService.getAlbums(user.userId);
|
||||
|
|
|
|||
|
|
@ -3,10 +3,15 @@ import { eq, and, asc, sql } from 'drizzle-orm';
|
|||
import { DATABASE_CONNECTION } from '../db/database.module';
|
||||
import { Database } from '../db/connection';
|
||||
import { songs } from '../db/schema';
|
||||
import { createMukkeStorage, type StorageClient } from '@manacore/shared-storage';
|
||||
|
||||
@Injectable()
|
||||
export class LibraryService {
|
||||
constructor(@Inject(DATABASE_CONNECTION) private db: Database) {}
|
||||
private storage: StorageClient;
|
||||
|
||||
constructor(@Inject(DATABASE_CONNECTION) private db: Database) {
|
||||
this.storage = createMukkeStorage();
|
||||
}
|
||||
|
||||
async getAlbums(userId: string) {
|
||||
const result = await this.db.execute<{
|
||||
|
|
@ -93,4 +98,26 @@ export class LibraryService {
|
|||
.where(and(eq(songs.userId, userId), eq(songs.genre, genreName)))
|
||||
.orderBy(asc(songs.title));
|
||||
}
|
||||
|
||||
async getCoverUrls(paths: string[]): Promise<Record<string, string>> {
|
||||
const uniquePaths = [...new Set(paths.filter(Boolean))];
|
||||
if (uniquePaths.length === 0) return {};
|
||||
|
||||
const entries = await Promise.all(
|
||||
uniquePaths.map(async (path) => {
|
||||
try {
|
||||
const url = await this.storage.getDownloadUrl(path, { expiresIn: 3600 });
|
||||
return [path, url] as const;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const result: Record<string, string> = {};
|
||||
for (const entry of entries) {
|
||||
if (entry) result[entry[0]] = entry[1];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ interface LibraryState {
|
|||
artists: Artist[];
|
||||
genres: Genre[];
|
||||
stats: LibraryStats | null;
|
||||
coverUrls: Record<string, string>;
|
||||
activeTab: 'songs' | 'albums' | 'artists' | 'genres';
|
||||
sortField: SortField;
|
||||
sortDirection: SortDirection;
|
||||
|
|
@ -40,6 +41,7 @@ function createLibraryStore() {
|
|||
artists: [],
|
||||
genres: [],
|
||||
stats: null,
|
||||
coverUrls: {},
|
||||
activeTab: 'songs',
|
||||
sortField: 'addedAt' as SortField,
|
||||
sortDirection: 'desc' as SortDirection,
|
||||
|
|
@ -94,10 +96,27 @@ function createLibraryStore() {
|
|||
get isLoading() {
|
||||
return state.isLoading;
|
||||
},
|
||||
get coverUrls() {
|
||||
return state.coverUrls;
|
||||
},
|
||||
get error() {
|
||||
return state.error;
|
||||
},
|
||||
|
||||
async loadCoverUrls(paths: string[]) {
|
||||
const uncached = paths.filter((p) => p && !state.coverUrls[p]);
|
||||
if (uncached.length === 0) return;
|
||||
try {
|
||||
const data = await fetchApi<{ urls: Record<string, string> }>('/library/cover-urls', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ paths: uncached }),
|
||||
});
|
||||
state.coverUrls = { ...state.coverUrls, ...data.urls };
|
||||
} catch {
|
||||
// Cover URLs are non-critical, don't set error
|
||||
}
|
||||
},
|
||||
|
||||
async loadSongs() {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
|
|
@ -106,6 +125,8 @@ function createLibraryStore() {
|
|||
`/songs?sort=${state.sortField}&direction=${state.sortDirection}`
|
||||
);
|
||||
state.songs = data.songs;
|
||||
const coverPaths = data.songs.map((s) => s.coverArtPath).filter((p): p is string => !!p);
|
||||
if (coverPaths.length > 0) this.loadCoverUrls(coverPaths);
|
||||
} catch (e) {
|
||||
state.error = e instanceof Error ? e.message : 'Failed to load songs';
|
||||
}
|
||||
|
|
@ -118,6 +139,8 @@ function createLibraryStore() {
|
|||
try {
|
||||
const data = await fetchApi<{ albums: Album[] }>('/library/albums');
|
||||
state.albums = data.albums;
|
||||
const coverPaths = data.albums.map((a) => a.coverArtPath).filter((p): p is string => !!p);
|
||||
if (coverPaths.length > 0) this.loadCoverUrls(coverPaths);
|
||||
} catch (e) {
|
||||
state.error = e instanceof Error ? e.message : 'Failed to load albums';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { authStore } from './auth.svelte';
|
|||
interface PlaylistState {
|
||||
playlists: Playlist[];
|
||||
currentPlaylist: PlaylistWithSongs | null;
|
||||
coverUrls: Record<string, string>;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
|
@ -23,6 +24,7 @@ function createPlaylistStore() {
|
|||
let state = $state<PlaylistState>({
|
||||
playlists: [],
|
||||
currentPlaylist: null,
|
||||
coverUrls: {},
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
|
|
@ -56,16 +58,37 @@ function createPlaylistStore() {
|
|||
get isLoading() {
|
||||
return state.isLoading;
|
||||
},
|
||||
get coverUrls() {
|
||||
return state.coverUrls;
|
||||
},
|
||||
get error() {
|
||||
return state.error;
|
||||
},
|
||||
|
||||
async loadCoverUrls(paths: string[]) {
|
||||
const uncached = paths.filter((p) => p && !state.coverUrls[p]);
|
||||
if (uncached.length === 0) return;
|
||||
try {
|
||||
const data = await fetchApi<{ urls: Record<string, string> }>('/library/cover-urls', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ paths: uncached }),
|
||||
});
|
||||
state.coverUrls = { ...state.coverUrls, ...data.urls };
|
||||
} catch {
|
||||
// Cover URLs are non-critical
|
||||
}
|
||||
},
|
||||
|
||||
async loadPlaylists() {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
try {
|
||||
const data = await fetchApi<{ playlists: Playlist[] }>('/playlists');
|
||||
state.playlists = data.playlists;
|
||||
const coverPaths = data.playlists
|
||||
.map((p) => p.coverArtPath)
|
||||
.filter((p): p is string => !!p);
|
||||
if (coverPaths.length > 0) this.loadCoverUrls(coverPaths);
|
||||
} catch (e) {
|
||||
state.error = e instanceof Error ? e.message : 'Failed to load playlists';
|
||||
}
|
||||
|
|
@ -78,6 +101,10 @@ function createPlaylistStore() {
|
|||
try {
|
||||
const data = await fetchApi<{ playlist: PlaylistWithSongs }>(`/playlists/${id}`);
|
||||
state.currentPlaylist = data.playlist;
|
||||
const coverPaths = data.playlist.songs
|
||||
.map((s) => s.coverArtPath)
|
||||
.filter((p): p is string => !!p);
|
||||
if (coverPaths.length > 0) this.loadCoverUrls(coverPaths);
|
||||
} catch (e) {
|
||||
state.error = e instanceof Error ? e.message : 'Failed to load playlist';
|
||||
}
|
||||
|
|
|
|||
1
apps/mukke/apps/web/src/lib/version.ts
Normal file
1
apps/mukke/apps/web/src/lib/version.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const APP_VERSION = '0.2.0';
|
||||
|
|
@ -136,8 +136,9 @@
|
|||
<div class="bg-surface rounded-lg overflow-hidden">
|
||||
<!-- Header -->
|
||||
<div
|
||||
class="grid grid-cols-[1fr_1fr_1fr_80px_40px_40px_40px] gap-4 px-4 py-3 text-xs font-medium text-foreground-secondary uppercase tracking-wide border-b border-border"
|
||||
class="grid grid-cols-[40px_1fr_1fr_1fr_80px_40px_40px_40px] gap-4 px-4 py-3 text-xs font-medium text-foreground-secondary uppercase tracking-wide border-b border-border"
|
||||
>
|
||||
<span></span>
|
||||
<span>Title</span>
|
||||
<span>Artist</span>
|
||||
<span>Album</span>
|
||||
|
|
@ -149,8 +150,33 @@
|
|||
<!-- Song rows -->
|
||||
{#each libraryStore.songs as song}
|
||||
<div
|
||||
class="grid grid-cols-[1fr_1fr_1fr_80px_40px_40px_40px] gap-4 px-4 py-3 hover:bg-background transition-colors items-center"
|
||||
class="grid grid-cols-[40px_1fr_1fr_1fr_80px_40px_40px_40px] gap-4 px-4 py-3 hover:bg-background transition-colors items-center"
|
||||
>
|
||||
<div
|
||||
class="w-10 h-10 rounded bg-background flex items-center justify-center overflow-hidden flex-shrink-0"
|
||||
>
|
||||
{#if song.coverArtPath && libraryStore.coverUrls[song.coverArtPath]}
|
||||
<img
|
||||
src={libraryStore.coverUrls[song.coverArtPath]}
|
||||
alt=""
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
{:else}
|
||||
<svg
|
||||
class="w-5 h-5 text-foreground-secondary"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</div>
|
||||
<span class="truncate font-medium">{song.title}</span>
|
||||
<span class="truncate text-foreground-secondary">{song.artist ?? 'Unknown'}</span>
|
||||
<span class="truncate text-foreground-secondary">{song.album ?? 'Unknown'}</span>
|
||||
|
|
@ -226,21 +252,29 @@
|
|||
class="bg-surface rounded-lg p-4 hover:bg-surface-hover transition-colors group"
|
||||
>
|
||||
<div
|
||||
class="aspect-square bg-background rounded-lg mb-3 flex items-center justify-center"
|
||||
class="aspect-square bg-background rounded-lg mb-3 flex items-center justify-center overflow-hidden"
|
||||
>
|
||||
<svg
|
||||
class="w-12 h-12 text-foreground-secondary"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3"
|
||||
{#if album.coverArtPath && libraryStore.coverUrls[album.coverArtPath]}
|
||||
<img
|
||||
src={libraryStore.coverUrls[album.coverArtPath]}
|
||||
alt={album.album}
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
</svg>
|
||||
{:else}
|
||||
<svg
|
||||
class="w-12 h-12 text-foreground-secondary"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</div>
|
||||
<h3 class="font-medium truncate group-hover:text-primary transition-colors">
|
||||
{album.album}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@
|
|||
let songs = $state<Song[]>([]);
|
||||
let isLoading = $state(true);
|
||||
let error = $state<string | null>(null);
|
||||
let coverUrl = $state<string | null>(null);
|
||||
|
||||
let albumName = $derived(decodeURIComponent($page.params.name ?? ''));
|
||||
let albumArtist = $derived(
|
||||
|
|
@ -42,10 +43,17 @@
|
|||
|
||||
isLoading = true;
|
||||
error = null;
|
||||
coverUrl = null;
|
||||
|
||||
fetchApi<{ songs: Song[] }>(`/library/albums/${encodeURIComponent(decodeURIComponent(name))}`)
|
||||
.then((data) => {
|
||||
songs = data.songs;
|
||||
const songWithCover = data.songs.find((s) => s.coverArtPath);
|
||||
if (songWithCover) {
|
||||
fetchApi<{ url: string | null }>(`/songs/${songWithCover.id}/cover-url`).then((res) => {
|
||||
coverUrl = res.url;
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
error = e instanceof Error ? e.message : 'Failed to load album';
|
||||
|
|
@ -100,20 +108,26 @@
|
|||
{:else}
|
||||
<!-- Album header -->
|
||||
<div class="flex items-end gap-6 mb-8">
|
||||
<div class="w-48 h-48 bg-surface rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<svg
|
||||
class="w-16 h-16 text-foreground-secondary"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3"
|
||||
/>
|
||||
</svg>
|
||||
<div
|
||||
class="w-48 h-48 bg-surface rounded-lg flex items-center justify-center flex-shrink-0 overflow-hidden"
|
||||
>
|
||||
{#if coverUrl}
|
||||
<img src={coverUrl} alt={albumName} class="w-full h-full object-cover" />
|
||||
{:else}
|
||||
<svg
|
||||
class="w-16 h-16 text-foreground-secondary"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold mb-1">{albumName}</h1>
|
||||
|
|
|
|||
|
|
@ -99,20 +99,30 @@
|
|||
href="/playlists/{playlist.id}"
|
||||
class="bg-surface rounded-lg p-4 hover:bg-surface-hover transition-colors group relative"
|
||||
>
|
||||
<div class="aspect-square bg-background rounded-lg mb-3 flex items-center justify-center">
|
||||
<svg
|
||||
class="w-12 h-12 text-foreground-secondary"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3"
|
||||
<div
|
||||
class="aspect-square bg-background rounded-lg mb-3 flex items-center justify-center overflow-hidden"
|
||||
>
|
||||
{#if playlist.coverArtPath && playlistStore.coverUrls[playlist.coverArtPath]}
|
||||
<img
|
||||
src={playlistStore.coverUrls[playlist.coverArtPath]}
|
||||
alt={playlist.name}
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
</svg>
|
||||
{:else}
|
||||
<svg
|
||||
class="w-12 h-12 text-foreground-secondary"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</div>
|
||||
<h3 class="font-medium truncate group-hover:text-primary transition-colors">
|
||||
{playlist.name}
|
||||
|
|
|
|||
|
|
@ -205,8 +205,9 @@
|
|||
<div class="bg-surface rounded-lg overflow-hidden">
|
||||
<!-- Header -->
|
||||
<div
|
||||
class="grid grid-cols-[1fr_1fr_1fr_80px_40px] gap-4 px-4 py-3 text-xs font-medium text-foreground-secondary uppercase tracking-wide border-b border-border"
|
||||
class="grid grid-cols-[40px_1fr_1fr_1fr_80px_40px] gap-4 px-4 py-3 text-xs font-medium text-foreground-secondary uppercase tracking-wide border-b border-border"
|
||||
>
|
||||
<span></span>
|
||||
<span>Title</span>
|
||||
<span>Artist</span>
|
||||
<span>Album</span>
|
||||
|
|
@ -216,7 +217,7 @@
|
|||
<!-- Song rows -->
|
||||
{#each playlistStore.currentPlaylist.songs as song, index}
|
||||
<div
|
||||
class="grid grid-cols-[1fr_1fr_1fr_80px_40px] gap-4 px-4 py-3 hover:bg-background transition-colors items-center cursor-pointer"
|
||||
class="grid grid-cols-[40px_1fr_1fr_1fr_80px_40px] gap-4 px-4 py-3 hover:bg-background transition-colors items-center cursor-pointer"
|
||||
onclick={() => handlePlaySong(song, index)}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
|
|
@ -224,6 +225,31 @@
|
|||
if (e.key === 'Enter') handlePlaySong(song, index);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="w-10 h-10 rounded bg-background flex items-center justify-center overflow-hidden flex-shrink-0"
|
||||
>
|
||||
{#if song.coverArtPath && playlistStore.coverUrls[song.coverArtPath]}
|
||||
<img
|
||||
src={playlistStore.coverUrls[song.coverArtPath]}
|
||||
alt=""
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
{:else}
|
||||
<svg
|
||||
class="w-5 h-5 text-foreground-secondary"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</div>
|
||||
<span class="truncate font-medium">{song.title}</span>
|
||||
<span class="truncate text-foreground-secondary">{song.artist ?? 'Unknown'}</span>
|
||||
<span class="truncate text-foreground-secondary">{song.album ?? 'Unknown'}</span>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { APP_VERSION } from '$lib/version';
|
||||
import {
|
||||
SettingsPage,
|
||||
SettingsSection,
|
||||
|
|
@ -60,4 +61,6 @@
|
|||
onclick={handleLogout}
|
||||
/>
|
||||
</SettingsDangerZone>
|
||||
|
||||
<p class="mt-8 pb-4 text-center text-xs text-gray-400 dark:text-gray-600">v{APP_VERSION}</p>
|
||||
</SettingsPage>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
import { getLoginTranslations } from '@manacore/shared-i18n';
|
||||
import { MukkeLogo } from '@manacore/shared-branding';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { APP_VERSION } from '$lib/version';
|
||||
|
||||
// Get redirect URL from query params or sessionStorage
|
||||
const redirectTo = $derived.by(() => {
|
||||
|
|
@ -60,4 +61,5 @@
|
|||
{translations}
|
||||
{verified}
|
||||
{initialEmail}
|
||||
version={APP_VERSION}
|
||||
/>
|
||||
|
|
|
|||
1
apps/nutriphi/apps/web/src/lib/version.ts
Normal file
1
apps/nutriphi/apps/web/src/lib/version.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const APP_VERSION = '0.2.0';
|
||||
|
|
@ -7,6 +7,7 @@
|
|||
import { locale } from 'svelte-i18n';
|
||||
import { NutriPhiLogo } from '@manacore/shared-branding';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { APP_VERSION } from '$lib/version';
|
||||
import '$lib/i18n';
|
||||
|
||||
// Get redirect URL from query params or sessionStorage
|
||||
|
|
@ -62,4 +63,5 @@
|
|||
{translations}
|
||||
{verified}
|
||||
{initialEmail}
|
||||
version={APP_VERSION}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { apiClient } from '$lib/api/client';
|
||||
import { DEFAULT_DAILY_VALUES } from '@nutriphi/shared';
|
||||
import { APP_VERSION } from '$lib/version';
|
||||
import {
|
||||
ArrowLeft,
|
||||
FloppyDisk,
|
||||
|
|
@ -278,8 +279,10 @@
|
|||
|
||||
<!-- App Info -->
|
||||
<section class="text-center text-sm text-[var(--color-text-muted)] py-4">
|
||||
<p>NutriPhi v1.0.0</p>
|
||||
<p>NutriPhi v{APP_VERSION}</p>
|
||||
<p class="mt-1">KI-gestützte Ernährungsanalyse</p>
|
||||
</section>
|
||||
|
||||
<p class="mt-8 pb-4 text-center text-xs text-gray-400 dark:text-gray-600">v{APP_VERSION}</p>
|
||||
</main>
|
||||
</div>
|
||||
|
|
|
|||
1
apps/photos/apps/web/src/lib/version.ts
Normal file
1
apps/photos/apps/web/src/lib/version.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const APP_VERSION = '0.2.0';
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
import { theme } from '$lib/stores/theme';
|
||||
import { setLocale, supportedLocales, type SupportedLocale } from '$lib/i18n';
|
||||
import { THEME_DEFINITIONS, DEFAULT_THEME_VARIANTS } from '@manacore/shared-theme';
|
||||
import { APP_VERSION } from '$lib/version';
|
||||
|
||||
let selectedLocale = $state<SupportedLocale>('de');
|
||||
|
||||
|
|
@ -135,6 +136,8 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="mt-8 pb-4 text-center text-xs text-gray-400 dark:text-gray-600">v{APP_VERSION}</p>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
import { LoginPage } from '@manacore/shared-auth-ui';
|
||||
import { getLoginTranslations } from '@manacore/shared-i18n';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { APP_VERSION } from '$lib/version';
|
||||
import '$lib/i18n';
|
||||
|
||||
let redirectTo = $state('/');
|
||||
|
|
@ -53,4 +54,5 @@
|
|||
{translations}
|
||||
{verified}
|
||||
{initialEmail}
|
||||
version={APP_VERSION}
|
||||
/>
|
||||
|
|
|
|||
1
apps/picture/apps/web/src/lib/version.ts
Normal file
1
apps/picture/apps/web/src/lib/version.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const APP_VERSION = '0.3.0';
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { userSettings } from '$lib/stores/user-settings.svelte';
|
||||
import { APP_VERSION } from '$lib/version';
|
||||
import {
|
||||
SettingsPage,
|
||||
SettingsSection,
|
||||
|
|
@ -41,4 +42,6 @@
|
|||
</SettingsRow>
|
||||
</SettingsCard>
|
||||
</SettingsSection>
|
||||
|
||||
<p class="mt-8 pb-4 text-center text-xs text-gray-400 dark:text-gray-600">v{APP_VERSION}</p>
|
||||
</SettingsPage>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
import AppSlider from '$lib/components/AppSlider.svelte';
|
||||
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { APP_VERSION } from '$lib/version';
|
||||
import { onMount } from 'svelte';
|
||||
import { PUBLIC_GOOGLE_CLIENT_ID, PUBLIC_APPLE_CLIENT_ID } from '$env/static/public';
|
||||
|
||||
|
|
@ -66,6 +67,7 @@
|
|||
{translations}
|
||||
{verified}
|
||||
{initialEmail}
|
||||
version={APP_VERSION}
|
||||
>
|
||||
{#snippet headerControls()}
|
||||
<LanguageSelector />
|
||||
|
|
|
|||
1
apps/planta/apps/web/src/lib/version.ts
Normal file
1
apps/planta/apps/web/src/lib/version.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const APP_VERSION = '0.1.0';
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { theme } from '$lib/stores/theme';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { APP_VERSION } from '$lib/version';
|
||||
|
||||
let isDark = $derived(theme.isDark);
|
||||
</script>
|
||||
|
|
@ -54,6 +55,8 @@
|
|||
Planta hilft dir, deine Pflanzen zu dokumentieren und zu pflegen. Mache ein Foto und die KI
|
||||
erstellt automatisch einen Steckbrief mit Pflegehinweisen und Gießvorschlägen.
|
||||
</p>
|
||||
<p class="text-sm text-muted-foreground mt-2">Version 1.0.0</p>
|
||||
<p class="text-sm text-muted-foreground mt-2">Version {APP_VERSION}</p>
|
||||
</div>
|
||||
|
||||
<p class="mt-8 pb-4 text-center text-xs text-gray-400 dark:text-gray-600">v{APP_VERSION}</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
import { locale } from 'svelte-i18n';
|
||||
import { PlantaLogo } from '@manacore/shared-branding';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { APP_VERSION } from '$lib/version';
|
||||
import '$lib/i18n';
|
||||
|
||||
// Get redirect URL from query params or sessionStorage
|
||||
|
|
@ -62,4 +63,5 @@
|
|||
{translations}
|
||||
{verified}
|
||||
{initialEmail}
|
||||
version={APP_VERSION}
|
||||
/>
|
||||
|
|
|
|||
1
apps/presi/apps/web/src/lib/version.ts
Normal file
1
apps/presi/apps/web/src/lib/version.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const APP_VERSION = '0.2.0';
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
import { auth } from '$lib/stores/auth.svelte';
|
||||
import { theme } from '$lib/stores/theme';
|
||||
import { userSettings } from '$lib/stores/user-settings.svelte';
|
||||
import { APP_VERSION } from '$lib/version';
|
||||
import {
|
||||
SettingsPage,
|
||||
SettingsSection,
|
||||
|
|
@ -191,4 +192,6 @@
|
|||
{/snippet}
|
||||
</SettingsDangerButton>
|
||||
</SettingsDangerZone>
|
||||
|
||||
<p class="mt-8 pb-4 text-center text-xs text-gray-400 dark:text-gray-600">v{APP_VERSION}</p>
|
||||
</SettingsPage>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
import { auth } from '$lib/stores/auth.svelte';
|
||||
import AppSlider from '$lib/components/AppSlider.svelte';
|
||||
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
|
||||
import { APP_VERSION } from '$lib/version';
|
||||
import '$lib/i18n';
|
||||
|
||||
// Get redirect URL from query params
|
||||
|
|
@ -50,6 +51,7 @@
|
|||
{translations}
|
||||
{verified}
|
||||
{initialEmail}
|
||||
version={APP_VERSION}
|
||||
>
|
||||
{#snippet headerControls()}
|
||||
<LanguageSelector />
|
||||
|
|
|
|||
1
apps/questions/apps/web/src/lib/version.ts
Normal file
1
apps/questions/apps/web/src/lib/version.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const APP_VERSION = '0.1.0';
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { theme } from '$lib/stores/theme';
|
||||
import { APP_VERSION } from '$lib/version';
|
||||
import { ArrowLeft, User, Moon, Sun, Desktop, Bell, Shield, Trash } from '@manacore/shared-icons';
|
||||
|
||||
let currentTheme = $state(theme.current);
|
||||
|
|
@ -169,10 +170,12 @@
|
|||
<section class="mb-8">
|
||||
<div class="rounded-xl border border-border bg-card p-6 text-center">
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Questions App v1.0.0
|
||||
Questions App v{APP_VERSION}
|
||||
<br />
|
||||
Powered by mana-search
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<p class="mt-8 pb-4 text-center text-xs text-gray-400 dark:text-gray-600">v{APP_VERSION}</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
import { QuestionsLogo } from '@manacore/shared-branding';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { apiClient } from '$lib/api/client';
|
||||
import { APP_VERSION } from '$lib/version';
|
||||
import '$lib/i18n';
|
||||
|
||||
// Get redirect URL from query params or sessionStorage
|
||||
|
|
@ -68,4 +69,5 @@
|
|||
{translations}
|
||||
{verified}
|
||||
{initialEmail}
|
||||
version={APP_VERSION}
|
||||
/>
|
||||
|
|
|
|||
1
apps/skilltree/apps/web/src/lib/version.ts
Normal file
1
apps/skilltree/apps/web/src/lib/version.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const APP_VERSION = '0.2.0';
|
||||
|
|
@ -8,6 +8,7 @@
|
|||
import { SkillTreeLogo } from '@manacore/shared-branding';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { apiClient } from '$lib/api/client';
|
||||
import { APP_VERSION } from '$lib/version';
|
||||
import '$lib/i18n';
|
||||
|
||||
// Get redirect URL from query params or sessionStorage
|
||||
|
|
@ -68,4 +69,5 @@
|
|||
{translations}
|
||||
{verified}
|
||||
{initialEmail}
|
||||
version={APP_VERSION}
|
||||
/>
|
||||
|
|
|
|||
1
apps/storage/apps/web/src/lib/version.ts
Normal file
1
apps/storage/apps/web/src/lib/version.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const APP_VERSION = '0.2.0';
|
||||
|
|
@ -7,6 +7,7 @@
|
|||
import { getLoginTranslations } from '@manacore/shared-i18n';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
|
||||
import { APP_VERSION } from '$lib/version';
|
||||
import '$lib/i18n';
|
||||
|
||||
const translations = $derived(getLoginTranslations($locale || 'de'));
|
||||
|
|
@ -43,6 +44,7 @@
|
|||
{translations}
|
||||
{verified}
|
||||
{initialEmail}
|
||||
version={APP_VERSION}
|
||||
>
|
||||
{#snippet headerControls()}
|
||||
<LanguageSelector />
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { theme } from '$lib/stores/theme.svelte';
|
||||
import { userSettings } from '$lib/stores/user-settings.svelte';
|
||||
import { THEME_DEFINITIONS } from '@manacore/shared-theme';
|
||||
import { APP_VERSION } from '$lib/version';
|
||||
import {
|
||||
SettingsPage,
|
||||
SettingsSection,
|
||||
|
|
@ -121,4 +122,6 @@
|
|||
</SettingsRow>
|
||||
</SettingsCard>
|
||||
</SettingsSection>
|
||||
|
||||
<p class="mt-8 pb-4 text-center text-xs text-gray-400 dark:text-gray-600">v{APP_VERSION}</p>
|
||||
</SettingsPage>
|
||||
|
|
|
|||
1
apps/todo/apps/web/src/lib/version.ts
Normal file
1
apps/todo/apps/web/src/lib/version.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const APP_VERSION = '1.0.0';
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
import { goto } from '$app/navigation';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { userSettings } from '$lib/stores/user-settings.svelte';
|
||||
import { APP_VERSION } from '$lib/version';
|
||||
import { todoSettings, type TodoView, type KanbanCardSize } from '$lib/stores/settings.svelte';
|
||||
import { projectsStore } from '$lib/stores/projects.svelte';
|
||||
import type { TaskPriority } from '@todo/shared';
|
||||
|
|
@ -698,4 +699,6 @@
|
|||
{/snippet}
|
||||
</SettingsDangerButton>
|
||||
</SettingsDangerZone>
|
||||
|
||||
<p class="mt-8 pb-4 text-center text-xs text-gray-400 dark:text-gray-600">v{APP_VERSION}</p>
|
||||
</SettingsPage>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import AppSlider from '$lib/components/AppSlider.svelte';
|
||||
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
|
||||
import { APP_VERSION } from '$lib/version';
|
||||
|
||||
// Get redirect URL from query params or sessionStorage (set by AuthGateModal)
|
||||
const redirectTo = $derived.by(() => {
|
||||
|
|
@ -65,6 +66,7 @@
|
|||
{translations}
|
||||
{verified}
|
||||
{initialEmail}
|
||||
version={APP_VERSION}
|
||||
>
|
||||
{#snippet headerControls()}
|
||||
<LanguageSelector />
|
||||
|
|
|
|||
1
apps/zitare/apps/web/src/lib/version.ts
Normal file
1
apps/zitare/apps/web/src/lib/version.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const APP_VERSION = '0.2.0';
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
import { _ } from 'svelte-i18n';
|
||||
import { quotesStore } from '$lib/stores/quotes.svelte';
|
||||
import type { SupportedLanguage } from '@zitare/content';
|
||||
import { APP_VERSION } from '$lib/version';
|
||||
|
||||
// Language options for quotes
|
||||
const languageOptions: { value: SupportedLanguage; label: string }[] = [
|
||||
|
|
@ -56,4 +57,6 @@
|
|||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="mt-8 pb-4 text-center text-xs text-gray-400 dark:text-gray-600">v{APP_VERSION}</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
import { ZitareLogo } from '@manacore/shared-branding';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
|
||||
import { APP_VERSION } from '$lib/version';
|
||||
import '$lib/i18n';
|
||||
|
||||
// Get redirect URL from query params or sessionStorage
|
||||
|
|
@ -63,6 +64,7 @@
|
|||
{translations}
|
||||
{verified}
|
||||
{initialEmail}
|
||||
version={APP_VERSION}
|
||||
>
|
||||
{#snippet headerControls()}
|
||||
<LanguageSelector />
|
||||
|
|
|
|||
|
|
@ -91,6 +91,8 @@
|
|||
initialEmail?: string;
|
||||
/** Pre-fill password field (for dev mode) */
|
||||
initialPassword?: string;
|
||||
/** App version string to display */
|
||||
version?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
|
|
@ -115,6 +117,7 @@
|
|||
verified = false,
|
||||
initialEmail = '',
|
||||
initialPassword = '',
|
||||
version = '',
|
||||
}: Props = $props();
|
||||
|
||||
const t = $derived({ ...defaultTranslations, ...translations });
|
||||
|
|
@ -542,6 +545,10 @@
|
|||
{@render appSlider()}
|
||||
</footer>
|
||||
{/if}
|
||||
|
||||
{#if version}
|
||||
<p class="version-label">v{version}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
|
@ -1012,6 +1019,21 @@
|
|||
padding: 0 0 1rem;
|
||||
}
|
||||
|
||||
.version-label {
|
||||
position: fixed;
|
||||
bottom: 0.5rem;
|
||||
right: 0.75rem;
|
||||
font-size: 10px;
|
||||
color: rgba(156, 163, 175, 0.6);
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.light .version-label {
|
||||
color: rgba(156, 163, 175, 0.6);
|
||||
}
|
||||
|
||||
/* Entrance Animations */
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue