mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:01:09 +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,
|
FilterDropdown,
|
||||||
type FilterDropdownOption,
|
type FilterDropdownOption,
|
||||||
} from '@manacore/shared-ui';
|
} from '@manacore/shared-ui';
|
||||||
|
import { APP_VERSION } from '$lib/version';
|
||||||
import type { CalendarViewType, Calendar } from '@calendar/shared';
|
import type { CalendarViewType, Calendar } from '@calendar/shared';
|
||||||
|
|
||||||
// Calendar management state
|
// Calendar management state
|
||||||
|
|
@ -651,6 +652,8 @@
|
||||||
</div>
|
</div>
|
||||||
</SettingsCard>
|
</SettingsCard>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
|
||||||
|
<p class="mt-8 pb-4 text-center text-xs text-gray-400 dark:text-gray-600">v{APP_VERSION}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
import { authStore } from '$lib/stores/auth.svelte';
|
import { authStore } from '$lib/stores/auth.svelte';
|
||||||
import AppSlider from '$lib/components/AppSlider.svelte';
|
import AppSlider from '$lib/components/AppSlider.svelte';
|
||||||
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
|
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
|
||||||
|
import { APP_VERSION } from '$lib/version';
|
||||||
import '$lib/i18n';
|
import '$lib/i18n';
|
||||||
|
|
||||||
// Get redirect URL from query params or sessionStorage (set by AuthGateModal)
|
// Get redirect URL from query params or sessionStorage (set by AuthGateModal)
|
||||||
|
|
@ -66,6 +67,7 @@
|
||||||
{translations}
|
{translations}
|
||||||
{verified}
|
{verified}
|
||||||
{initialEmail}
|
{initialEmail}
|
||||||
|
version={APP_VERSION}
|
||||||
>
|
>
|
||||||
{#snippet headerControls()}
|
{#snippet headerControls()}
|
||||||
<LanguageSelector />
|
<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 { authStore } from '$lib/stores/auth.svelte';
|
||||||
import AppSlider from '$lib/components/AppSlider.svelte';
|
import AppSlider from '$lib/components/AppSlider.svelte';
|
||||||
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
|
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
|
||||||
|
import { APP_VERSION } from '$lib/version';
|
||||||
import '$lib/i18n';
|
import '$lib/i18n';
|
||||||
|
|
||||||
// Dev credentials - pre-filled in development mode
|
// Dev credentials - pre-filled in development mode
|
||||||
|
|
@ -74,6 +75,7 @@
|
||||||
{verified}
|
{verified}
|
||||||
{initialEmail}
|
{initialEmail}
|
||||||
{initialPassword}
|
{initialPassword}
|
||||||
|
version={APP_VERSION}
|
||||||
>
|
>
|
||||||
{#snippet headerControls()}
|
{#snippet headerControls()}
|
||||||
<LanguageSelector />
|
<LanguageSelector />
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { userSettings } from '$lib/stores/user-settings.svelte';
|
import { userSettings } from '$lib/stores/user-settings.svelte';
|
||||||
|
import { APP_VERSION } from '$lib/version';
|
||||||
import {
|
import {
|
||||||
SettingsPage,
|
SettingsPage,
|
||||||
SettingsSection,
|
SettingsSection,
|
||||||
|
|
@ -171,4 +172,6 @@
|
||||||
<a href="#" class="text-[hsl(var(--primary))] hover:underline">Hilfe & Support</a>
|
<a href="#" class="text-[hsl(var(--primary))] hover:underline">Hilfe & Support</a>
|
||||||
</div>
|
</div>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
|
||||||
|
<p class="mt-8 pb-4 text-center text-xs text-gray-400 dark:text-gray-600">v{APP_VERSION}</p>
|
||||||
</SettingsPage>
|
</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 { _ } from 'svelte-i18n';
|
||||||
import { userSettings } from '$lib/stores/user-settings.svelte';
|
import { userSettings } from '$lib/stores/user-settings.svelte';
|
||||||
import { GlobalSettingsSection } from '@manacore/shared-ui';
|
import { GlobalSettingsSection } from '@manacore/shared-ui';
|
||||||
|
import { APP_VERSION } from '$lib/version';
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await userSettings.load();
|
await userSettings.load();
|
||||||
|
|
@ -102,4 +103,6 @@
|
||||||
Töne können für einzelne Wecker und Timer in deren Einstellungen angepasst werden.
|
Töne können für einzelne Wecker und Timer in deren Einstellungen angepasst werden.
|
||||||
</p>
|
</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>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
import { getLoginTranslations } from '@manacore/shared-i18n';
|
import { getLoginTranslations } from '@manacore/shared-i18n';
|
||||||
import { ClockLogo } from '@manacore/shared-branding';
|
import { ClockLogo } from '@manacore/shared-branding';
|
||||||
import { authStore } from '$lib/stores/auth.svelte';
|
import { authStore } from '$lib/stores/auth.svelte';
|
||||||
|
import { APP_VERSION } from '$lib/version';
|
||||||
import '$lib/i18n';
|
import '$lib/i18n';
|
||||||
|
|
||||||
// Read verification status from query params (set after email verification)
|
// Read verification status from query params (set after email verification)
|
||||||
|
|
@ -64,4 +65,5 @@
|
||||||
{translations}
|
{translations}
|
||||||
{verified}
|
{verified}
|
||||||
{initialEmail}
|
{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 { goto } from '$app/navigation';
|
||||||
import { authStore } from '$lib/stores/auth.svelte';
|
import { authStore } from '$lib/stores/auth.svelte';
|
||||||
import { userSettings } from '$lib/stores/user-settings.svelte';
|
import { userSettings } from '$lib/stores/user-settings.svelte';
|
||||||
|
import { APP_VERSION } from '$lib/version';
|
||||||
import {
|
import {
|
||||||
contactsSettings,
|
contactsSettings,
|
||||||
type ContactView,
|
type ContactView,
|
||||||
|
|
@ -638,4 +639,6 @@
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</SettingsDangerButton>
|
</SettingsDangerButton>
|
||||||
</SettingsDangerZone>
|
</SettingsDangerZone>
|
||||||
|
|
||||||
|
<p class="mt-8 pb-4 text-center text-xs text-gray-400 dark:text-gray-600">v{APP_VERSION}</p>
|
||||||
</SettingsPage>
|
</SettingsPage>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
import { authStore } from '$lib/stores/auth.svelte';
|
import { authStore } from '$lib/stores/auth.svelte';
|
||||||
import AppSlider from '$lib/components/AppSlider.svelte';
|
import AppSlider from '$lib/components/AppSlider.svelte';
|
||||||
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
|
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
|
||||||
|
import { APP_VERSION } from '$lib/version';
|
||||||
import '$lib/i18n';
|
import '$lib/i18n';
|
||||||
|
|
||||||
// Get redirect URL from query params or sessionStorage
|
// Get redirect URL from query params or sessionStorage
|
||||||
|
|
@ -63,6 +64,7 @@
|
||||||
{translations}
|
{translations}
|
||||||
{verified}
|
{verified}
|
||||||
{initialEmail}
|
{initialEmail}
|
||||||
|
version={APP_VERSION}
|
||||||
>
|
>
|
||||||
{#snippet headerControls()}
|
{#snippet headerControls()}
|
||||||
<LanguageSelector />
|
<LanguageSelector />
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ scores:
|
||||||
security: 82
|
security: 82
|
||||||
ux: 88
|
ux: 88
|
||||||
status: 'production'
|
status: 'production'
|
||||||
|
version: '1.0.0'
|
||||||
stats:
|
stats:
|
||||||
backendModules: 13
|
backendModules: 13
|
||||||
webRoutes: 19
|
webRoutes: 19
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ scores:
|
||||||
security: 82
|
security: 82
|
||||||
ux: 80
|
ux: 80
|
||||||
status: 'production'
|
status: 'production'
|
||||||
|
version: '0.3.0'
|
||||||
stats:
|
stats:
|
||||||
backendModules: 9
|
backendModules: 9
|
||||||
webRoutes: 24
|
webRoutes: 24
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ scores:
|
||||||
security: 60
|
security: 60
|
||||||
ux: 55
|
ux: 55
|
||||||
status: 'beta'
|
status: 'beta'
|
||||||
|
version: '0.2.0'
|
||||||
stats:
|
stats:
|
||||||
backendModules: 7
|
backendModules: 7
|
||||||
webRoutes: 17
|
webRoutes: 17
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ scores:
|
||||||
security: 85
|
security: 85
|
||||||
ux: 85
|
ux: 85
|
||||||
status: 'production'
|
status: 'production'
|
||||||
|
version: '1.0.0'
|
||||||
stats:
|
stats:
|
||||||
backendModules: 14
|
backendModules: 14
|
||||||
webRoutes: 20
|
webRoutes: 20
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ scores:
|
||||||
security: 68
|
security: 68
|
||||||
ux: 65
|
ux: 65
|
||||||
status: 'beta'
|
status: 'beta'
|
||||||
|
version: '0.1.0'
|
||||||
stats:
|
stats:
|
||||||
backendModules: 5
|
backendModules: 5
|
||||||
webRoutes: 15
|
webRoutes: 15
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ scores:
|
||||||
security: 72
|
security: 72
|
||||||
ux: 75
|
ux: 75
|
||||||
status: 'beta'
|
status: 'beta'
|
||||||
|
version: '0.2.0'
|
||||||
stats:
|
stats:
|
||||||
backendModules: 0
|
backendModules: 0
|
||||||
webRoutes: 33
|
webRoutes: 33
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ scores:
|
||||||
security: 55
|
security: 55
|
||||||
ux: 68
|
ux: 68
|
||||||
status: 'alpha'
|
status: 'alpha'
|
||||||
|
version: '0.2.0'
|
||||||
stats:
|
stats:
|
||||||
backendModules: 2
|
backendModules: 2
|
||||||
webRoutes: 19
|
webRoutes: 19
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ scores:
|
||||||
security: 88
|
security: 88
|
||||||
ux: 82
|
ux: 82
|
||||||
status: 'production'
|
status: 'production'
|
||||||
|
version: '0.2.0'
|
||||||
stats:
|
stats:
|
||||||
backendModules: 0
|
backendModules: 0
|
||||||
webRoutes: 7
|
webRoutes: 7
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ scores:
|
||||||
security: 78
|
security: 78
|
||||||
ux: 60
|
ux: 60
|
||||||
status: 'beta'
|
status: 'beta'
|
||||||
|
version: '0.2.0'
|
||||||
stats:
|
stats:
|
||||||
backendModules: 11
|
backendModules: 11
|
||||||
webRoutes: 16
|
webRoutes: 16
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ scores:
|
||||||
security: 68
|
security: 68
|
||||||
ux: 55
|
ux: 55
|
||||||
status: 'beta'
|
status: 'beta'
|
||||||
|
version: '0.2.0'
|
||||||
stats:
|
stats:
|
||||||
backendModules: 8
|
backendModules: 8
|
||||||
webRoutes: 10
|
webRoutes: 10
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ scores:
|
||||||
security: 65
|
security: 65
|
||||||
ux: 55
|
ux: 55
|
||||||
status: 'beta'
|
status: 'beta'
|
||||||
|
version: '0.2.0'
|
||||||
stats:
|
stats:
|
||||||
backendModules: 7
|
backendModules: 7
|
||||||
webRoutes: 12
|
webRoutes: 12
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ scores:
|
||||||
security: 80
|
security: 80
|
||||||
ux: 78
|
ux: 78
|
||||||
status: 'production'
|
status: 'production'
|
||||||
|
version: '0.3.0'
|
||||||
stats:
|
stats:
|
||||||
backendModules: 11
|
backendModules: 11
|
||||||
webRoutes: 19
|
webRoutes: 19
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ scores:
|
||||||
security: 55
|
security: 55
|
||||||
ux: 50
|
ux: 50
|
||||||
status: 'alpha'
|
status: 'alpha'
|
||||||
|
version: '0.1.0'
|
||||||
stats:
|
stats:
|
||||||
backendModules: 6
|
backendModules: 6
|
||||||
webRoutes: 12
|
webRoutes: 12
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ scores:
|
||||||
security: 55
|
security: 55
|
||||||
ux: 68
|
ux: 68
|
||||||
status: 'beta'
|
status: 'beta'
|
||||||
|
version: '0.2.0'
|
||||||
stats:
|
stats:
|
||||||
backendModules: 7
|
backendModules: 7
|
||||||
webRoutes: 16
|
webRoutes: 16
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ scores:
|
||||||
security: 55
|
security: 55
|
||||||
ux: 55
|
ux: 55
|
||||||
status: 'alpha'
|
status: 'alpha'
|
||||||
|
version: '0.1.0'
|
||||||
stats:
|
stats:
|
||||||
backendModules: 8
|
backendModules: 8
|
||||||
webRoutes: 12
|
webRoutes: 12
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ scores:
|
||||||
security: 65
|
security: 65
|
||||||
ux: 72
|
ux: 72
|
||||||
status: 'beta'
|
status: 'beta'
|
||||||
|
version: '0.2.0'
|
||||||
stats:
|
stats:
|
||||||
backendModules: 4
|
backendModules: 4
|
||||||
webRoutes: 6
|
webRoutes: 6
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ scores:
|
||||||
security: 72
|
security: 72
|
||||||
ux: 55
|
ux: 55
|
||||||
status: 'beta'
|
status: 'beta'
|
||||||
|
version: '0.2.0'
|
||||||
stats:
|
stats:
|
||||||
backendModules: 10
|
backendModules: 10
|
||||||
webRoutes: 17
|
webRoutes: 17
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ scores:
|
||||||
security: 82
|
security: 82
|
||||||
ux: 85
|
ux: 85
|
||||||
status: 'production'
|
status: 'production'
|
||||||
|
version: '1.0.0'
|
||||||
stats:
|
stats:
|
||||||
backendModules: 7
|
backendModules: 7
|
||||||
webRoutes: 13
|
webRoutes: 13
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ scores:
|
||||||
security: 55
|
security: 55
|
||||||
ux: 35
|
ux: 35
|
||||||
status: 'alpha'
|
status: 'alpha'
|
||||||
|
version: '0.0.1'
|
||||||
stats:
|
stats:
|
||||||
backendModules: 7
|
backendModules: 7
|
||||||
webRoutes: 0
|
webRoutes: 0
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ scores:
|
||||||
security: 70
|
security: 70
|
||||||
ux: 75
|
ux: 75
|
||||||
status: 'beta'
|
status: 'beta'
|
||||||
|
version: '0.2.0'
|
||||||
stats:
|
stats:
|
||||||
backendModules: 5
|
backendModules: 5
|
||||||
webRoutes: 13
|
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 { creditsService } from '$lib/api/credits';
|
||||||
import type { CreditBalance } from '$lib/api/credits';
|
import type { CreditBalance } from '$lib/api/credits';
|
||||||
import { userSettings } from '$lib/stores/user-settings.svelte';
|
import { userSettings } from '$lib/stores/user-settings.svelte';
|
||||||
|
import { APP_VERSION } from '$lib/version';
|
||||||
|
|
||||||
let loading = $state(true);
|
let loading = $state(true);
|
||||||
let savingProfile = $state(false);
|
let savingProfile = $state(false);
|
||||||
|
|
@ -333,5 +334,7 @@
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<p class="mt-8 pb-4 text-center text-xs text-gray-400 dark:text-gray-600">v{APP_VERSION}</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
import AppSlider from '$lib/components/AppSlider.svelte';
|
import AppSlider from '$lib/components/AppSlider.svelte';
|
||||||
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
|
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
|
||||||
import { authStore } from '$lib/stores/auth.svelte';
|
import { authStore } from '$lib/stores/auth.svelte';
|
||||||
|
import { APP_VERSION } from '$lib/version';
|
||||||
|
|
||||||
// Get translations based on current locale
|
// Get translations based on current locale
|
||||||
const translations = $derived(getLoginTranslations($locale || 'de'));
|
const translations = $derived(getLoginTranslations($locale || 'de'));
|
||||||
|
|
@ -42,6 +43,7 @@
|
||||||
{translations}
|
{translations}
|
||||||
{verified}
|
{verified}
|
||||||
{initialEmail}
|
{initialEmail}
|
||||||
|
version={APP_VERSION}
|
||||||
>
|
>
|
||||||
{#snippet headerControls()}
|
{#snippet headerControls()}
|
||||||
<LanguageSelector />
|
<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">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { userSettings } from '$lib/stores/user-settings.svelte';
|
import { userSettings } from '$lib/stores/user-settings.svelte';
|
||||||
|
import { APP_VERSION } from '$lib/version';
|
||||||
import {
|
import {
|
||||||
SettingsPage,
|
SettingsPage,
|
||||||
SettingsSection,
|
SettingsSection,
|
||||||
|
|
@ -41,4 +42,6 @@
|
||||||
</SettingsRow>
|
</SettingsRow>
|
||||||
</SettingsCard>
|
</SettingsCard>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
|
||||||
|
<p class="mt-8 pb-4 text-center text-xs text-gray-400 dark:text-gray-600">v{APP_VERSION}</p>
|
||||||
</SettingsPage>
|
</SettingsPage>
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
import AppSlider from '$lib/components/AppSlider.svelte';
|
import AppSlider from '$lib/components/AppSlider.svelte';
|
||||||
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
|
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
|
||||||
import { authStore } from '$lib/stores/auth.svelte';
|
import { authStore } from '$lib/stores/auth.svelte';
|
||||||
|
import { APP_VERSION } from '$lib/version';
|
||||||
|
|
||||||
// Get translations based on current locale
|
// Get translations based on current locale
|
||||||
const translations = $derived(getLoginTranslations($locale || 'de'));
|
const translations = $derived(getLoginTranslations($locale || 'de'));
|
||||||
|
|
@ -42,6 +43,7 @@
|
||||||
{translations}
|
{translations}
|
||||||
{verified}
|
{verified}
|
||||||
{initialEmail}
|
{initialEmail}
|
||||||
|
version={APP_VERSION}
|
||||||
>
|
>
|
||||||
{#snippet headerControls()}
|
{#snippet headerControls()}
|
||||||
<LanguageSelector />
|
<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">
|
<script lang="ts">
|
||||||
import { matrixStore } from '$lib/matrix';
|
import { matrixStore } from '$lib/matrix';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
import { APP_VERSION } from '$lib/version';
|
||||||
import {
|
import {
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
User,
|
User,
|
||||||
|
|
@ -228,8 +229,8 @@
|
||||||
<button
|
<button
|
||||||
class="flex flex-col items-center gap-2 rounded-xl p-4 transition-all
|
class="flex flex-col items-center gap-2 rounded-xl p-4 transition-all
|
||||||
{theme.mode === 'light'
|
{theme.mode === 'light'
|
||||||
? 'bg-primary text-primary-foreground ring-2 ring-primary'
|
? 'bg-primary text-primary-foreground ring-2 ring-primary'
|
||||||
: 'bg-muted hover:bg-muted/80'}"
|
: 'bg-muted hover:bg-muted/80'}"
|
||||||
onclick={() => theme.setMode('light')}
|
onclick={() => theme.setMode('light')}
|
||||||
>
|
>
|
||||||
<Sun class="h-6 w-6" />
|
<Sun class="h-6 w-6" />
|
||||||
|
|
@ -239,8 +240,8 @@
|
||||||
<button
|
<button
|
||||||
class="flex flex-col items-center gap-2 rounded-xl p-4 transition-all
|
class="flex flex-col items-center gap-2 rounded-xl p-4 transition-all
|
||||||
{theme.mode === 'dark'
|
{theme.mode === 'dark'
|
||||||
? 'bg-primary text-primary-foreground ring-2 ring-primary'
|
? 'bg-primary text-primary-foreground ring-2 ring-primary'
|
||||||
: 'bg-muted hover:bg-muted/80'}"
|
: 'bg-muted hover:bg-muted/80'}"
|
||||||
onclick={() => theme.setMode('dark')}
|
onclick={() => theme.setMode('dark')}
|
||||||
>
|
>
|
||||||
<Moon class="h-6 w-6" />
|
<Moon class="h-6 w-6" />
|
||||||
|
|
@ -250,8 +251,8 @@
|
||||||
<button
|
<button
|
||||||
class="flex flex-col items-center gap-2 rounded-xl p-4 transition-all
|
class="flex flex-col items-center gap-2 rounded-xl p-4 transition-all
|
||||||
{theme.mode === 'system'
|
{theme.mode === 'system'
|
||||||
? 'bg-primary text-primary-foreground ring-2 ring-primary'
|
? 'bg-primary text-primary-foreground ring-2 ring-primary'
|
||||||
: 'bg-muted hover:bg-muted/80'}"
|
: 'bg-muted hover:bg-muted/80'}"
|
||||||
onclick={() => theme.setMode('system')}
|
onclick={() => theme.setMode('system')}
|
||||||
>
|
>
|
||||||
<Desktop class="h-6 w-6" />
|
<Desktop class="h-6 w-6" />
|
||||||
|
|
@ -309,7 +310,9 @@
|
||||||
{:else}
|
{:else}
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<!-- Enable/Disable Toggle -->
|
<!-- 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">
|
<div class="flex items-center gap-3">
|
||||||
<BellRinging class="h-6 w-6" />
|
<BellRinging class="h-6 w-6" />
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -330,7 +333,9 @@
|
||||||
|
|
||||||
<!-- Sound Toggle -->
|
<!-- Sound Toggle -->
|
||||||
<label
|
<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">
|
<div class="flex items-center gap-3">
|
||||||
<SpeakerHigh class="h-6 w-6" />
|
<SpeakerHigh class="h-6 w-6" />
|
||||||
|
|
@ -350,7 +355,9 @@
|
||||||
|
|
||||||
<!-- Preview Toggle -->
|
<!-- Preview Toggle -->
|
||||||
<label
|
<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">
|
<div class="flex items-center gap-3">
|
||||||
<Eye class="h-6 w-6" />
|
<Eye class="h-6 w-6" />
|
||||||
|
|
@ -386,6 +393,8 @@
|
||||||
</button>
|
</button>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<p class="mt-8 pb-4 text-center text-xs text-gray-400 dark:text-gray-600">v{APP_VERSION}</p>
|
||||||
</div>
|
</div>
|
||||||
</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 { JwtAuthGuard, CurrentUser, CurrentUserData } from '@manacore/shared-nestjs-auth';
|
||||||
import { LibraryService } from './library.service';
|
import { LibraryService } from './library.service';
|
||||||
|
|
||||||
|
|
@ -7,6 +7,12 @@ import { LibraryService } from './library.service';
|
||||||
export class LibraryController {
|
export class LibraryController {
|
||||||
constructor(private readonly libraryService: LibraryService) {}
|
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')
|
@Get('albums')
|
||||||
async getAlbums(@CurrentUser() user: CurrentUserData) {
|
async getAlbums(@CurrentUser() user: CurrentUserData) {
|
||||||
const albums = await this.libraryService.getAlbums(user.userId);
|
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_CONNECTION } from '../db/database.module';
|
||||||
import { Database } from '../db/connection';
|
import { Database } from '../db/connection';
|
||||||
import { songs } from '../db/schema';
|
import { songs } from '../db/schema';
|
||||||
|
import { createMukkeStorage, type StorageClient } from '@manacore/shared-storage';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LibraryService {
|
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) {
|
async getAlbums(userId: string) {
|
||||||
const result = await this.db.execute<{
|
const result = await this.db.execute<{
|
||||||
|
|
@ -93,4 +98,26 @@ export class LibraryService {
|
||||||
.where(and(eq(songs.userId, userId), eq(songs.genre, genreName)))
|
.where(and(eq(songs.userId, userId), eq(songs.genre, genreName)))
|
||||||
.orderBy(asc(songs.title));
|
.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[];
|
artists: Artist[];
|
||||||
genres: Genre[];
|
genres: Genre[];
|
||||||
stats: LibraryStats | null;
|
stats: LibraryStats | null;
|
||||||
|
coverUrls: Record<string, string>;
|
||||||
activeTab: 'songs' | 'albums' | 'artists' | 'genres';
|
activeTab: 'songs' | 'albums' | 'artists' | 'genres';
|
||||||
sortField: SortField;
|
sortField: SortField;
|
||||||
sortDirection: SortDirection;
|
sortDirection: SortDirection;
|
||||||
|
|
@ -40,6 +41,7 @@ function createLibraryStore() {
|
||||||
artists: [],
|
artists: [],
|
||||||
genres: [],
|
genres: [],
|
||||||
stats: null,
|
stats: null,
|
||||||
|
coverUrls: {},
|
||||||
activeTab: 'songs',
|
activeTab: 'songs',
|
||||||
sortField: 'addedAt' as SortField,
|
sortField: 'addedAt' as SortField,
|
||||||
sortDirection: 'desc' as SortDirection,
|
sortDirection: 'desc' as SortDirection,
|
||||||
|
|
@ -94,10 +96,27 @@ function createLibraryStore() {
|
||||||
get isLoading() {
|
get isLoading() {
|
||||||
return state.isLoading;
|
return state.isLoading;
|
||||||
},
|
},
|
||||||
|
get coverUrls() {
|
||||||
|
return state.coverUrls;
|
||||||
|
},
|
||||||
get error() {
|
get error() {
|
||||||
return state.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() {
|
async loadSongs() {
|
||||||
state.isLoading = true;
|
state.isLoading = true;
|
||||||
state.error = null;
|
state.error = null;
|
||||||
|
|
@ -106,6 +125,8 @@ function createLibraryStore() {
|
||||||
`/songs?sort=${state.sortField}&direction=${state.sortDirection}`
|
`/songs?sort=${state.sortField}&direction=${state.sortDirection}`
|
||||||
);
|
);
|
||||||
state.songs = data.songs;
|
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) {
|
} catch (e) {
|
||||||
state.error = e instanceof Error ? e.message : 'Failed to load songs';
|
state.error = e instanceof Error ? e.message : 'Failed to load songs';
|
||||||
}
|
}
|
||||||
|
|
@ -118,6 +139,8 @@ function createLibraryStore() {
|
||||||
try {
|
try {
|
||||||
const data = await fetchApi<{ albums: Album[] }>('/library/albums');
|
const data = await fetchApi<{ albums: Album[] }>('/library/albums');
|
||||||
state.albums = data.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) {
|
} catch (e) {
|
||||||
state.error = e instanceof Error ? e.message : 'Failed to load albums';
|
state.error = e instanceof Error ? e.message : 'Failed to load albums';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { authStore } from './auth.svelte';
|
||||||
interface PlaylistState {
|
interface PlaylistState {
|
||||||
playlists: Playlist[];
|
playlists: Playlist[];
|
||||||
currentPlaylist: PlaylistWithSongs | null;
|
currentPlaylist: PlaylistWithSongs | null;
|
||||||
|
coverUrls: Record<string, string>;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
}
|
}
|
||||||
|
|
@ -23,6 +24,7 @@ function createPlaylistStore() {
|
||||||
let state = $state<PlaylistState>({
|
let state = $state<PlaylistState>({
|
||||||
playlists: [],
|
playlists: [],
|
||||||
currentPlaylist: null,
|
currentPlaylist: null,
|
||||||
|
coverUrls: {},
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: null,
|
error: null,
|
||||||
});
|
});
|
||||||
|
|
@ -56,16 +58,37 @@ function createPlaylistStore() {
|
||||||
get isLoading() {
|
get isLoading() {
|
||||||
return state.isLoading;
|
return state.isLoading;
|
||||||
},
|
},
|
||||||
|
get coverUrls() {
|
||||||
|
return state.coverUrls;
|
||||||
|
},
|
||||||
get error() {
|
get error() {
|
||||||
return state.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() {
|
async loadPlaylists() {
|
||||||
state.isLoading = true;
|
state.isLoading = true;
|
||||||
state.error = null;
|
state.error = null;
|
||||||
try {
|
try {
|
||||||
const data = await fetchApi<{ playlists: Playlist[] }>('/playlists');
|
const data = await fetchApi<{ playlists: Playlist[] }>('/playlists');
|
||||||
state.playlists = data.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) {
|
} catch (e) {
|
||||||
state.error = e instanceof Error ? e.message : 'Failed to load playlists';
|
state.error = e instanceof Error ? e.message : 'Failed to load playlists';
|
||||||
}
|
}
|
||||||
|
|
@ -78,6 +101,10 @@ function createPlaylistStore() {
|
||||||
try {
|
try {
|
||||||
const data = await fetchApi<{ playlist: PlaylistWithSongs }>(`/playlists/${id}`);
|
const data = await fetchApi<{ playlist: PlaylistWithSongs }>(`/playlists/${id}`);
|
||||||
state.currentPlaylist = data.playlist;
|
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) {
|
} catch (e) {
|
||||||
state.error = e instanceof Error ? e.message : 'Failed to load playlist';
|
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">
|
<div class="bg-surface rounded-lg overflow-hidden">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div
|
<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>Title</span>
|
||||||
<span>Artist</span>
|
<span>Artist</span>
|
||||||
<span>Album</span>
|
<span>Album</span>
|
||||||
|
|
@ -149,8 +150,33 @@
|
||||||
<!-- Song rows -->
|
<!-- Song rows -->
|
||||||
{#each libraryStore.songs as song}
|
{#each libraryStore.songs as song}
|
||||||
<div
|
<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 font-medium">{song.title}</span>
|
||||||
<span class="truncate text-foreground-secondary">{song.artist ?? 'Unknown'}</span>
|
<span class="truncate text-foreground-secondary">{song.artist ?? 'Unknown'}</span>
|
||||||
<span class="truncate text-foreground-secondary">{song.album ?? '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"
|
class="bg-surface rounded-lg p-4 hover:bg-surface-hover transition-colors group"
|
||||||
>
|
>
|
||||||
<div
|
<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
|
{#if album.coverArtPath && libraryStore.coverUrls[album.coverArtPath]}
|
||||||
class="w-12 h-12 text-foreground-secondary"
|
<img
|
||||||
fill="none"
|
src={libraryStore.coverUrls[album.coverArtPath]}
|
||||||
stroke="currentColor"
|
alt={album.album}
|
||||||
viewBox="0 0 24 24"
|
class="w-full h-full object-cover"
|
||||||
>
|
|
||||||
<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>
|
{: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>
|
</div>
|
||||||
<h3 class="font-medium truncate group-hover:text-primary transition-colors">
|
<h3 class="font-medium truncate group-hover:text-primary transition-colors">
|
||||||
{album.album}
|
{album.album}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@
|
||||||
let songs = $state<Song[]>([]);
|
let songs = $state<Song[]>([]);
|
||||||
let isLoading = $state(true);
|
let isLoading = $state(true);
|
||||||
let error = $state<string | null>(null);
|
let error = $state<string | null>(null);
|
||||||
|
let coverUrl = $state<string | null>(null);
|
||||||
|
|
||||||
let albumName = $derived(decodeURIComponent($page.params.name ?? ''));
|
let albumName = $derived(decodeURIComponent($page.params.name ?? ''));
|
||||||
let albumArtist = $derived(
|
let albumArtist = $derived(
|
||||||
|
|
@ -42,10 +43,17 @@
|
||||||
|
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
error = null;
|
error = null;
|
||||||
|
coverUrl = null;
|
||||||
|
|
||||||
fetchApi<{ songs: Song[] }>(`/library/albums/${encodeURIComponent(decodeURIComponent(name))}`)
|
fetchApi<{ songs: Song[] }>(`/library/albums/${encodeURIComponent(decodeURIComponent(name))}`)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
songs = data.songs;
|
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) => {
|
.catch((e) => {
|
||||||
error = e instanceof Error ? e.message : 'Failed to load album';
|
error = e instanceof Error ? e.message : 'Failed to load album';
|
||||||
|
|
@ -100,20 +108,26 @@
|
||||||
{:else}
|
{:else}
|
||||||
<!-- Album header -->
|
<!-- Album header -->
|
||||||
<div class="flex items-end gap-6 mb-8">
|
<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">
|
<div
|
||||||
<svg
|
class="w-48 h-48 bg-surface rounded-lg flex items-center justify-center flex-shrink-0 overflow-hidden"
|
||||||
class="w-16 h-16 text-foreground-secondary"
|
>
|
||||||
fill="none"
|
{#if coverUrl}
|
||||||
stroke="currentColor"
|
<img src={coverUrl} alt={albumName} class="w-full h-full object-cover" />
|
||||||
viewBox="0 0 24 24"
|
{:else}
|
||||||
>
|
<svg
|
||||||
<path
|
class="w-16 h-16 text-foreground-secondary"
|
||||||
stroke-linecap="round"
|
fill="none"
|
||||||
stroke-linejoin="round"
|
stroke="currentColor"
|
||||||
stroke-width="2"
|
viewBox="0 0 24 24"
|
||||||
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"
|
>
|
||||||
/>
|
<path
|
||||||
</svg>
|
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>
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-3xl font-bold mb-1">{albumName}</h1>
|
<h1 class="text-3xl font-bold mb-1">{albumName}</h1>
|
||||||
|
|
|
||||||
|
|
@ -99,20 +99,30 @@
|
||||||
href="/playlists/{playlist.id}"
|
href="/playlists/{playlist.id}"
|
||||||
class="bg-surface rounded-lg p-4 hover:bg-surface-hover transition-colors group relative"
|
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">
|
<div
|
||||||
<svg
|
class="aspect-square bg-background rounded-lg mb-3 flex items-center justify-center overflow-hidden"
|
||||||
class="w-12 h-12 text-foreground-secondary"
|
>
|
||||||
fill="none"
|
{#if playlist.coverArtPath && playlistStore.coverUrls[playlist.coverArtPath]}
|
||||||
stroke="currentColor"
|
<img
|
||||||
viewBox="0 0 24 24"
|
src={playlistStore.coverUrls[playlist.coverArtPath]}
|
||||||
>
|
alt={playlist.name}
|
||||||
<path
|
class="w-full h-full object-cover"
|
||||||
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>
|
{: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>
|
</div>
|
||||||
<h3 class="font-medium truncate group-hover:text-primary transition-colors">
|
<h3 class="font-medium truncate group-hover:text-primary transition-colors">
|
||||||
{playlist.name}
|
{playlist.name}
|
||||||
|
|
|
||||||
|
|
@ -205,8 +205,9 @@
|
||||||
<div class="bg-surface rounded-lg overflow-hidden">
|
<div class="bg-surface rounded-lg overflow-hidden">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div
|
<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>Title</span>
|
||||||
<span>Artist</span>
|
<span>Artist</span>
|
||||||
<span>Album</span>
|
<span>Album</span>
|
||||||
|
|
@ -216,7 +217,7 @@
|
||||||
<!-- Song rows -->
|
<!-- Song rows -->
|
||||||
{#each playlistStore.currentPlaylist.songs as song, index}
|
{#each playlistStore.currentPlaylist.songs as song, index}
|
||||||
<div
|
<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)}
|
onclick={() => handlePlaySong(song, index)}
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
|
|
@ -224,6 +225,31 @@
|
||||||
if (e.key === 'Enter') handlePlaySong(song, index);
|
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 font-medium">{song.title}</span>
|
||||||
<span class="truncate text-foreground-secondary">{song.artist ?? 'Unknown'}</span>
|
<span class="truncate text-foreground-secondary">{song.artist ?? 'Unknown'}</span>
|
||||||
<span class="truncate text-foreground-secondary">{song.album ?? 'Unknown'}</span>
|
<span class="truncate text-foreground-secondary">{song.album ?? 'Unknown'}</span>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { authStore } from '$lib/stores/auth.svelte';
|
import { authStore } from '$lib/stores/auth.svelte';
|
||||||
|
import { APP_VERSION } from '$lib/version';
|
||||||
import {
|
import {
|
||||||
SettingsPage,
|
SettingsPage,
|
||||||
SettingsSection,
|
SettingsSection,
|
||||||
|
|
@ -60,4 +61,6 @@
|
||||||
onclick={handleLogout}
|
onclick={handleLogout}
|
||||||
/>
|
/>
|
||||||
</SettingsDangerZone>
|
</SettingsDangerZone>
|
||||||
|
|
||||||
|
<p class="mt-8 pb-4 text-center text-xs text-gray-400 dark:text-gray-600">v{APP_VERSION}</p>
|
||||||
</SettingsPage>
|
</SettingsPage>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
import { getLoginTranslations } from '@manacore/shared-i18n';
|
import { getLoginTranslations } from '@manacore/shared-i18n';
|
||||||
import { MukkeLogo } from '@manacore/shared-branding';
|
import { MukkeLogo } from '@manacore/shared-branding';
|
||||||
import { authStore } from '$lib/stores/auth.svelte';
|
import { authStore } from '$lib/stores/auth.svelte';
|
||||||
|
import { APP_VERSION } from '$lib/version';
|
||||||
|
|
||||||
// Get redirect URL from query params or sessionStorage
|
// Get redirect URL from query params or sessionStorage
|
||||||
const redirectTo = $derived.by(() => {
|
const redirectTo = $derived.by(() => {
|
||||||
|
|
@ -60,4 +61,5 @@
|
||||||
{translations}
|
{translations}
|
||||||
{verified}
|
{verified}
|
||||||
{initialEmail}
|
{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 { locale } from 'svelte-i18n';
|
||||||
import { NutriPhiLogo } from '@manacore/shared-branding';
|
import { NutriPhiLogo } from '@manacore/shared-branding';
|
||||||
import { authStore } from '$lib/stores/auth.svelte';
|
import { authStore } from '$lib/stores/auth.svelte';
|
||||||
|
import { APP_VERSION } from '$lib/version';
|
||||||
import '$lib/i18n';
|
import '$lib/i18n';
|
||||||
|
|
||||||
// Get redirect URL from query params or sessionStorage
|
// Get redirect URL from query params or sessionStorage
|
||||||
|
|
@ -62,4 +63,5 @@
|
||||||
{translations}
|
{translations}
|
||||||
{verified}
|
{verified}
|
||||||
{initialEmail}
|
{initialEmail}
|
||||||
|
version={APP_VERSION}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
import { authStore } from '$lib/stores/auth.svelte';
|
import { authStore } from '$lib/stores/auth.svelte';
|
||||||
import { apiClient } from '$lib/api/client';
|
import { apiClient } from '$lib/api/client';
|
||||||
import { DEFAULT_DAILY_VALUES } from '@nutriphi/shared';
|
import { DEFAULT_DAILY_VALUES } from '@nutriphi/shared';
|
||||||
|
import { APP_VERSION } from '$lib/version';
|
||||||
import {
|
import {
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
FloppyDisk,
|
FloppyDisk,
|
||||||
|
|
@ -278,8 +279,10 @@
|
||||||
|
|
||||||
<!-- App Info -->
|
<!-- App Info -->
|
||||||
<section class="text-center text-sm text-[var(--color-text-muted)] py-4">
|
<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>
|
<p class="mt-1">KI-gestützte Ernährungsanalyse</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<p class="mt-8 pb-4 text-center text-xs text-gray-400 dark:text-gray-600">v{APP_VERSION}</p>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</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 { theme } from '$lib/stores/theme';
|
||||||
import { setLocale, supportedLocales, type SupportedLocale } from '$lib/i18n';
|
import { setLocale, supportedLocales, type SupportedLocale } from '$lib/i18n';
|
||||||
import { THEME_DEFINITIONS, DEFAULT_THEME_VARIANTS } from '@manacore/shared-theme';
|
import { THEME_DEFINITIONS, DEFAULT_THEME_VARIANTS } from '@manacore/shared-theme';
|
||||||
|
import { APP_VERSION } from '$lib/version';
|
||||||
|
|
||||||
let selectedLocale = $state<SupportedLocale>('de');
|
let selectedLocale = $state<SupportedLocale>('de');
|
||||||
|
|
||||||
|
|
@ -135,6 +136,8 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
import { LoginPage } from '@manacore/shared-auth-ui';
|
import { LoginPage } from '@manacore/shared-auth-ui';
|
||||||
import { getLoginTranslations } from '@manacore/shared-i18n';
|
import { getLoginTranslations } from '@manacore/shared-i18n';
|
||||||
import { authStore } from '$lib/stores/auth.svelte';
|
import { authStore } from '$lib/stores/auth.svelte';
|
||||||
|
import { APP_VERSION } from '$lib/version';
|
||||||
import '$lib/i18n';
|
import '$lib/i18n';
|
||||||
|
|
||||||
let redirectTo = $state('/');
|
let redirectTo = $state('/');
|
||||||
|
|
@ -53,4 +54,5 @@
|
||||||
{translations}
|
{translations}
|
||||||
{verified}
|
{verified}
|
||||||
{initialEmail}
|
{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">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { userSettings } from '$lib/stores/user-settings.svelte';
|
import { userSettings } from '$lib/stores/user-settings.svelte';
|
||||||
|
import { APP_VERSION } from '$lib/version';
|
||||||
import {
|
import {
|
||||||
SettingsPage,
|
SettingsPage,
|
||||||
SettingsSection,
|
SettingsSection,
|
||||||
|
|
@ -41,4 +42,6 @@
|
||||||
</SettingsRow>
|
</SettingsRow>
|
||||||
</SettingsCard>
|
</SettingsCard>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
|
||||||
|
<p class="mt-8 pb-4 text-center text-xs text-gray-400 dark:text-gray-600">v{APP_VERSION}</p>
|
||||||
</SettingsPage>
|
</SettingsPage>
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
import AppSlider from '$lib/components/AppSlider.svelte';
|
import AppSlider from '$lib/components/AppSlider.svelte';
|
||||||
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
|
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
|
||||||
import { authStore } from '$lib/stores/auth.svelte';
|
import { authStore } from '$lib/stores/auth.svelte';
|
||||||
|
import { APP_VERSION } from '$lib/version';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { PUBLIC_GOOGLE_CLIENT_ID, PUBLIC_APPLE_CLIENT_ID } from '$env/static/public';
|
import { PUBLIC_GOOGLE_CLIENT_ID, PUBLIC_APPLE_CLIENT_ID } from '$env/static/public';
|
||||||
|
|
||||||
|
|
@ -66,6 +67,7 @@
|
||||||
{translations}
|
{translations}
|
||||||
{verified}
|
{verified}
|
||||||
{initialEmail}
|
{initialEmail}
|
||||||
|
version={APP_VERSION}
|
||||||
>
|
>
|
||||||
{#snippet headerControls()}
|
{#snippet headerControls()}
|
||||||
<LanguageSelector />
|
<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">
|
<script lang="ts">
|
||||||
import { theme } from '$lib/stores/theme';
|
import { theme } from '$lib/stores/theme';
|
||||||
import { authStore } from '$lib/stores/auth.svelte';
|
import { authStore } from '$lib/stores/auth.svelte';
|
||||||
|
import { APP_VERSION } from '$lib/version';
|
||||||
|
|
||||||
let isDark = $derived(theme.isDark);
|
let isDark = $derived(theme.isDark);
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -54,6 +55,8 @@
|
||||||
Planta hilft dir, deine Pflanzen zu dokumentieren und zu pflegen. Mache ein Foto und die KI
|
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.
|
erstellt automatisch einen Steckbrief mit Pflegehinweisen und Gießvorschlägen.
|
||||||
</p>
|
</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>
|
</div>
|
||||||
|
|
||||||
|
<p class="mt-8 pb-4 text-center text-xs text-gray-400 dark:text-gray-600">v{APP_VERSION}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
import { locale } from 'svelte-i18n';
|
import { locale } from 'svelte-i18n';
|
||||||
import { PlantaLogo } from '@manacore/shared-branding';
|
import { PlantaLogo } from '@manacore/shared-branding';
|
||||||
import { authStore } from '$lib/stores/auth.svelte';
|
import { authStore } from '$lib/stores/auth.svelte';
|
||||||
|
import { APP_VERSION } from '$lib/version';
|
||||||
import '$lib/i18n';
|
import '$lib/i18n';
|
||||||
|
|
||||||
// Get redirect URL from query params or sessionStorage
|
// Get redirect URL from query params or sessionStorage
|
||||||
|
|
@ -62,4 +63,5 @@
|
||||||
{translations}
|
{translations}
|
||||||
{verified}
|
{verified}
|
||||||
{initialEmail}
|
{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 { auth } from '$lib/stores/auth.svelte';
|
||||||
import { theme } from '$lib/stores/theme';
|
import { theme } from '$lib/stores/theme';
|
||||||
import { userSettings } from '$lib/stores/user-settings.svelte';
|
import { userSettings } from '$lib/stores/user-settings.svelte';
|
||||||
|
import { APP_VERSION } from '$lib/version';
|
||||||
import {
|
import {
|
||||||
SettingsPage,
|
SettingsPage,
|
||||||
SettingsSection,
|
SettingsSection,
|
||||||
|
|
@ -191,4 +192,6 @@
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</SettingsDangerButton>
|
</SettingsDangerButton>
|
||||||
</SettingsDangerZone>
|
</SettingsDangerZone>
|
||||||
|
|
||||||
|
<p class="mt-8 pb-4 text-center text-xs text-gray-400 dark:text-gray-600">v{APP_VERSION}</p>
|
||||||
</SettingsPage>
|
</SettingsPage>
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
import { auth } from '$lib/stores/auth.svelte';
|
import { auth } from '$lib/stores/auth.svelte';
|
||||||
import AppSlider from '$lib/components/AppSlider.svelte';
|
import AppSlider from '$lib/components/AppSlider.svelte';
|
||||||
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
|
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
|
||||||
|
import { APP_VERSION } from '$lib/version';
|
||||||
import '$lib/i18n';
|
import '$lib/i18n';
|
||||||
|
|
||||||
// Get redirect URL from query params
|
// Get redirect URL from query params
|
||||||
|
|
@ -50,6 +51,7 @@
|
||||||
{translations}
|
{translations}
|
||||||
{verified}
|
{verified}
|
||||||
{initialEmail}
|
{initialEmail}
|
||||||
|
version={APP_VERSION}
|
||||||
>
|
>
|
||||||
{#snippet headerControls()}
|
{#snippet headerControls()}
|
||||||
<LanguageSelector />
|
<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">
|
<script lang="ts">
|
||||||
import { authStore } from '$lib/stores/auth.svelte';
|
import { authStore } from '$lib/stores/auth.svelte';
|
||||||
import { theme } from '$lib/stores/theme';
|
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';
|
import { ArrowLeft, User, Moon, Sun, Desktop, Bell, Shield, Trash } from '@manacore/shared-icons';
|
||||||
|
|
||||||
let currentTheme = $state(theme.current);
|
let currentTheme = $state(theme.current);
|
||||||
|
|
@ -169,10 +170,12 @@
|
||||||
<section class="mb-8">
|
<section class="mb-8">
|
||||||
<div class="rounded-xl border border-border bg-card p-6 text-center">
|
<div class="rounded-xl border border-border bg-card p-6 text-center">
|
||||||
<p class="text-sm text-muted-foreground">
|
<p class="text-sm text-muted-foreground">
|
||||||
Questions App v1.0.0
|
Questions App v{APP_VERSION}
|
||||||
<br />
|
<br />
|
||||||
Powered by mana-search
|
Powered by mana-search
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<p class="mt-8 pb-4 text-center text-xs text-gray-400 dark:text-gray-600">v{APP_VERSION}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
import { QuestionsLogo } from '@manacore/shared-branding';
|
import { QuestionsLogo } from '@manacore/shared-branding';
|
||||||
import { authStore } from '$lib/stores/auth.svelte';
|
import { authStore } from '$lib/stores/auth.svelte';
|
||||||
import { apiClient } from '$lib/api/client';
|
import { apiClient } from '$lib/api/client';
|
||||||
|
import { APP_VERSION } from '$lib/version';
|
||||||
import '$lib/i18n';
|
import '$lib/i18n';
|
||||||
|
|
||||||
// Get redirect URL from query params or sessionStorage
|
// Get redirect URL from query params or sessionStorage
|
||||||
|
|
@ -68,4 +69,5 @@
|
||||||
{translations}
|
{translations}
|
||||||
{verified}
|
{verified}
|
||||||
{initialEmail}
|
{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 { SkillTreeLogo } from '@manacore/shared-branding';
|
||||||
import { authStore } from '$lib/stores/auth.svelte';
|
import { authStore } from '$lib/stores/auth.svelte';
|
||||||
import { apiClient } from '$lib/api/client';
|
import { apiClient } from '$lib/api/client';
|
||||||
|
import { APP_VERSION } from '$lib/version';
|
||||||
import '$lib/i18n';
|
import '$lib/i18n';
|
||||||
|
|
||||||
// Get redirect URL from query params or sessionStorage
|
// Get redirect URL from query params or sessionStorage
|
||||||
|
|
@ -68,4 +69,5 @@
|
||||||
{translations}
|
{translations}
|
||||||
{verified}
|
{verified}
|
||||||
{initialEmail}
|
{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 { getLoginTranslations } from '@manacore/shared-i18n';
|
||||||
import { authStore } from '$lib/stores/auth.svelte';
|
import { authStore } from '$lib/stores/auth.svelte';
|
||||||
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
|
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
|
||||||
|
import { APP_VERSION } from '$lib/version';
|
||||||
import '$lib/i18n';
|
import '$lib/i18n';
|
||||||
|
|
||||||
const translations = $derived(getLoginTranslations($locale || 'de'));
|
const translations = $derived(getLoginTranslations($locale || 'de'));
|
||||||
|
|
@ -43,6 +44,7 @@
|
||||||
{translations}
|
{translations}
|
||||||
{verified}
|
{verified}
|
||||||
{initialEmail}
|
{initialEmail}
|
||||||
|
version={APP_VERSION}
|
||||||
>
|
>
|
||||||
{#snippet headerControls()}
|
{#snippet headerControls()}
|
||||||
<LanguageSelector />
|
<LanguageSelector />
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import { theme } from '$lib/stores/theme.svelte';
|
import { theme } from '$lib/stores/theme.svelte';
|
||||||
import { userSettings } from '$lib/stores/user-settings.svelte';
|
import { userSettings } from '$lib/stores/user-settings.svelte';
|
||||||
import { THEME_DEFINITIONS } from '@manacore/shared-theme';
|
import { THEME_DEFINITIONS } from '@manacore/shared-theme';
|
||||||
|
import { APP_VERSION } from '$lib/version';
|
||||||
import {
|
import {
|
||||||
SettingsPage,
|
SettingsPage,
|
||||||
SettingsSection,
|
SettingsSection,
|
||||||
|
|
@ -121,4 +122,6 @@
|
||||||
</SettingsRow>
|
</SettingsRow>
|
||||||
</SettingsCard>
|
</SettingsCard>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
|
||||||
|
<p class="mt-8 pb-4 text-center text-xs text-gray-400 dark:text-gray-600">v{APP_VERSION}</p>
|
||||||
</SettingsPage>
|
</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 { goto } from '$app/navigation';
|
||||||
import { authStore } from '$lib/stores/auth.svelte';
|
import { authStore } from '$lib/stores/auth.svelte';
|
||||||
import { userSettings } from '$lib/stores/user-settings.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 { todoSettings, type TodoView, type KanbanCardSize } from '$lib/stores/settings.svelte';
|
||||||
import { projectsStore } from '$lib/stores/projects.svelte';
|
import { projectsStore } from '$lib/stores/projects.svelte';
|
||||||
import type { TaskPriority } from '@todo/shared';
|
import type { TaskPriority } from '@todo/shared';
|
||||||
|
|
@ -698,4 +699,6 @@
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</SettingsDangerButton>
|
</SettingsDangerButton>
|
||||||
</SettingsDangerZone>
|
</SettingsDangerZone>
|
||||||
|
|
||||||
|
<p class="mt-8 pb-4 text-center text-xs text-gray-400 dark:text-gray-600">v{APP_VERSION}</p>
|
||||||
</SettingsPage>
|
</SettingsPage>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
import { authStore } from '$lib/stores/auth.svelte';
|
import { authStore } from '$lib/stores/auth.svelte';
|
||||||
import AppSlider from '$lib/components/AppSlider.svelte';
|
import AppSlider from '$lib/components/AppSlider.svelte';
|
||||||
import LanguageSelector from '$lib/components/LanguageSelector.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)
|
// Get redirect URL from query params or sessionStorage (set by AuthGateModal)
|
||||||
const redirectTo = $derived.by(() => {
|
const redirectTo = $derived.by(() => {
|
||||||
|
|
@ -65,6 +66,7 @@
|
||||||
{translations}
|
{translations}
|
||||||
{verified}
|
{verified}
|
||||||
{initialEmail}
|
{initialEmail}
|
||||||
|
version={APP_VERSION}
|
||||||
>
|
>
|
||||||
{#snippet headerControls()}
|
{#snippet headerControls()}
|
||||||
<LanguageSelector />
|
<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 { _ } from 'svelte-i18n';
|
||||||
import { quotesStore } from '$lib/stores/quotes.svelte';
|
import { quotesStore } from '$lib/stores/quotes.svelte';
|
||||||
import type { SupportedLanguage } from '@zitare/content';
|
import type { SupportedLanguage } from '@zitare/content';
|
||||||
|
import { APP_VERSION } from '$lib/version';
|
||||||
|
|
||||||
// Language options for quotes
|
// Language options for quotes
|
||||||
const languageOptions: { value: SupportedLanguage; label: string }[] = [
|
const languageOptions: { value: SupportedLanguage; label: string }[] = [
|
||||||
|
|
@ -56,4 +57,6 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
import { ZitareLogo } from '@manacore/shared-branding';
|
import { ZitareLogo } from '@manacore/shared-branding';
|
||||||
import { authStore } from '$lib/stores/auth.svelte';
|
import { authStore } from '$lib/stores/auth.svelte';
|
||||||
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
|
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
|
||||||
|
import { APP_VERSION } from '$lib/version';
|
||||||
import '$lib/i18n';
|
import '$lib/i18n';
|
||||||
|
|
||||||
// Get redirect URL from query params or sessionStorage
|
// Get redirect URL from query params or sessionStorage
|
||||||
|
|
@ -63,6 +64,7 @@
|
||||||
{translations}
|
{translations}
|
||||||
{verified}
|
{verified}
|
||||||
{initialEmail}
|
{initialEmail}
|
||||||
|
version={APP_VERSION}
|
||||||
>
|
>
|
||||||
{#snippet headerControls()}
|
{#snippet headerControls()}
|
||||||
<LanguageSelector />
|
<LanguageSelector />
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,8 @@
|
||||||
initialEmail?: string;
|
initialEmail?: string;
|
||||||
/** Pre-fill password field (for dev mode) */
|
/** Pre-fill password field (for dev mode) */
|
||||||
initialPassword?: string;
|
initialPassword?: string;
|
||||||
|
/** App version string to display */
|
||||||
|
version?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
|
|
@ -115,6 +117,7 @@
|
||||||
verified = false,
|
verified = false,
|
||||||
initialEmail = '',
|
initialEmail = '',
|
||||||
initialPassword = '',
|
initialPassword = '',
|
||||||
|
version = '',
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
const t = $derived({ ...defaultTranslations, ...translations });
|
const t = $derived({ ...defaultTranslations, ...translations });
|
||||||
|
|
@ -542,6 +545,10 @@
|
||||||
{@render appSlider()}
|
{@render appSlider()}
|
||||||
</footer>
|
</footer>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if version}
|
||||||
|
<p class="version-label">v{version}</p>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
@ -1012,6 +1019,21 @@
|
||||||
padding: 0 0 1rem;
|
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 */
|
/* Entrance Animations */
|
||||||
@keyframes fadeInUp {
|
@keyframes fadeInUp {
|
||||||
from {
|
from {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue