diff --git a/apps/mana/apps/web/src/lib/components/dashboard/widgets/ActivityFeedWidget.svelte b/apps/mana/apps/web/src/lib/components/dashboard/widgets/ActivityFeedWidget.svelte index 11e590db2..d563fee7f 100644 --- a/apps/mana/apps/web/src/lib/components/dashboard/widgets/ActivityFeedWidget.svelte +++ b/apps/mana/apps/web/src/lib/components/dashboard/widgets/ActivityFeedWidget.svelte @@ -29,6 +29,9 @@ Compass, MapPin, BookOpen, + MusicNote, + SunHorizon, + Presentation, } from '@mana/shared-icons'; import { getIconComponent } from '@mana/shared-icons'; import { formatDistanceToNow } from 'date-fns'; @@ -63,6 +66,9 @@ guide: Compass, visit: MapPin, study: BookOpen, + listening: MusicNote, + mood: SunHorizon, + rehearsal: Presentation, }; function timeAgo(iso: string): string { diff --git a/apps/mana/apps/web/src/lib/components/dashboard/widgets/DayTimelineWidget.svelte b/apps/mana/apps/web/src/lib/components/dashboard/widgets/DayTimelineWidget.svelte index 584433df8..cf632c8d6 100644 --- a/apps/mana/apps/web/src/lib/components/dashboard/widgets/DayTimelineWidget.svelte +++ b/apps/mana/apps/web/src/lib/components/dashboard/widgets/DayTimelineWidget.svelte @@ -28,6 +28,9 @@ Compass, MapPin, BookOpen, + MusicNote, + SunHorizon, + Presentation, } from '@mana/shared-icons'; import { getIconComponent } from '@mana/shared-icons'; import { format } from 'date-fns'; @@ -78,6 +81,9 @@ guide: { icon: Compass, label: 'Guide' }, visit: { icon: MapPin, label: 'Besuch' }, study: { icon: BookOpen, label: 'Lernen' }, + listening: { icon: MusicNote, label: 'Musik' }, + mood: { icon: SunHorizon, label: 'Stimmung' }, + rehearsal: { icon: Presentation, label: 'Probe' }, }; function formatBlockTime(block: TimeBlock): string { diff --git a/apps/mana/apps/web/src/lib/data/time-blocks/analytics.ts b/apps/mana/apps/web/src/lib/data/time-blocks/analytics.ts index c8146dd08..01bcbf356 100644 --- a/apps/mana/apps/web/src/lib/data/time-blocks/analytics.ts +++ b/apps/mana/apps/web/src/lib/data/time-blocks/analytics.ts @@ -33,6 +33,9 @@ const TYPE_COLORS: Record = { guide: '#14b8a6', visit: '#a855f7', study: '#0ea5e9', + listening: '#d946ef', + mood: '#fb923c', + rehearsal: '#84cc16', }; const TYPE_LABELS: Record = { @@ -50,6 +53,9 @@ const TYPE_LABELS: Record = { guide: 'Guides', visit: 'Besuche', study: 'Lernen', + listening: 'Musik', + mood: 'Stimmung', + rehearsal: 'Probe', }; function blockDuration(b: TimeBlock): number { diff --git a/apps/mana/apps/web/src/lib/data/time-blocks/types.ts b/apps/mana/apps/web/src/lib/data/time-blocks/types.ts index 09bee167a..7c340c6e4 100644 --- a/apps/mana/apps/web/src/lib/data/time-blocks/types.ts +++ b/apps/mana/apps/web/src/lib/data/time-blocks/types.ts @@ -26,7 +26,10 @@ export type TimeBlockType = | 'cycle' | 'guide' | 'visit' - | 'study'; + | 'study' + | 'listening' + | 'mood' + | 'rehearsal'; export type TimeBlockSourceModule = | 'calendar' @@ -41,7 +44,10 @@ export type TimeBlockSourceModule = | 'cycles' | 'guides' | 'places' - | 'cards'; + | 'cards' + | 'music' + | 'moodlit' + | 'presi'; // ─── Local Record Types (Dexie) ────────────────────────── diff --git a/apps/mana/apps/web/src/lib/modules/calendar/components/CalendarHeader.svelte b/apps/mana/apps/web/src/lib/modules/calendar/components/CalendarHeader.svelte index 9fd922d70..ec876e861 100644 --- a/apps/mana/apps/web/src/lib/modules/calendar/components/CalendarHeader.svelte +++ b/apps/mana/apps/web/src/lib/modules/calendar/components/CalendarHeader.svelte @@ -20,6 +20,9 @@ Compass, MapPin, BookOpen, + MusicNote, + SunHorizon, + Presentation, } from '@mana/shared-icons'; import { db } from '$lib/data/database'; import { decryptRecords } from '$lib/data/crypto'; @@ -50,6 +53,9 @@ { type: 'guide', label: 'Guides', icon: Compass }, { type: 'visit', label: 'Besuche', icon: MapPin }, { type: 'study', label: 'Lernen', icon: BookOpen }, + { type: 'listening', label: 'Musik', icon: MusicNote }, + { type: 'mood', label: 'Stimmung', icon: SunHorizon }, + { type: 'rehearsal', label: 'Probe', icon: Presentation }, ]; let allActive = $derived( diff --git a/apps/mana/apps/web/src/lib/modules/calendar/stores/view.svelte.ts b/apps/mana/apps/web/src/lib/modules/calendar/stores/view.svelte.ts index 9b60f735d..83709c290 100644 --- a/apps/mana/apps/web/src/lib/modules/calendar/stores/view.svelte.ts +++ b/apps/mana/apps/web/src/lib/modules/calendar/stores/view.svelte.ts @@ -39,6 +39,9 @@ let visibleBlockTypes = $state>( 'guide', 'visit', 'study', + 'listening', + 'mood', + 'rehearsal', ]) ); diff --git a/apps/mana/apps/web/src/lib/modules/moodlit/stores/moods.svelte.ts b/apps/mana/apps/web/src/lib/modules/moodlit/stores/moods.svelte.ts index 3cffa3252..02458f5c3 100644 --- a/apps/mana/apps/web/src/lib/modules/moodlit/stores/moods.svelte.ts +++ b/apps/mana/apps/web/src/lib/modules/moodlit/stores/moods.svelte.ts @@ -4,6 +4,7 @@ import { db } from '$lib/data/database'; import { MoodlitEvents } from '@mana/shared-utils/analytics'; +import { createBlock, updateBlock } from '$lib/data/time-blocks/service'; import type { LocalMood } from '../types'; import type { Mood, MoodSettings } from '../types'; @@ -21,6 +22,7 @@ function createMoodsStore() { let favoriteIds = $state([]); let settings = $state({ ...DEFAULT_SETTINGS }); let activeMood = $state(null); + let activeSessionBlockId = $state(null); // Load from localStorage on init if (typeof window !== 'undefined') { @@ -65,6 +67,34 @@ function createMoodsStore() { activeMood = mood; }, + async startMoodSession(mood: Mood): Promise { + if (activeSessionBlockId) { + await updateBlock(activeSessionBlockId, { + endDate: new Date().toISOString(), + }); + } + activeSessionBlockId = await createBlock({ + startDate: new Date().toISOString(), + endDate: null, + isLive: true, + kind: 'logged', + type: 'mood', + sourceModule: 'moodlit', + sourceId: mood.id, + title: mood.name, + color: mood.colors?.[0] ?? '#fb923c', + }); + }, + + async endMoodSession(): Promise { + if (!activeSessionBlockId) return; + await updateBlock(activeSessionBlockId, { + endDate: new Date().toISOString(), + isLive: false, + }); + activeSessionBlockId = null; + }, + addMood(mood: Mood) { customMoods = [...customMoods, mood]; persist(); diff --git a/apps/mana/apps/web/src/lib/modules/music/stores/library.svelte.ts b/apps/mana/apps/web/src/lib/modules/music/stores/library.svelte.ts index 1a783d1e9..344a89cab 100644 --- a/apps/mana/apps/web/src/lib/modules/music/stores/library.svelte.ts +++ b/apps/mana/apps/web/src/lib/modules/music/stores/library.svelte.ts @@ -6,7 +6,8 @@ */ import { songTable } from '../collections'; -import { encryptRecord } from '$lib/data/crypto'; +import { encryptRecord, decryptRecord } from '$lib/data/crypto'; +import { createBlock } from '$lib/data/time-blocks/service'; import { MusicEvents } from '@mana/shared-utils/analytics'; import type { LocalSong } from '../types'; @@ -24,15 +25,35 @@ export const libraryStore = { } }, - /** Increment play count. */ + /** Increment play count and create a listening TimeBlock. */ async incrementPlayCount(id: string) { const local = await songTable.get(id); if (local) { + const now = new Date().toISOString(); await songTable.update(id, { playCount: (local.playCount || 0) + 1, - lastPlayedAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), + lastPlayedAt: now, + updatedAt: now, }); + + const decrypted = await decryptRecord('songs', { ...local }); + const title = decrypted?.title ?? 'Song'; + const artist = decrypted?.artist; + const endDate = local.duration + ? new Date(Date.now() + local.duration * 1000).toISOString() + : now; + + await createBlock({ + startDate: now, + endDate, + kind: 'logged', + type: 'listening', + sourceModule: 'music', + sourceId: id, + title: artist ? `${title} — ${artist}` : title, + color: '#d946ef', + }); + MusicEvents.songPlayed(); } }, diff --git a/apps/mana/apps/web/src/lib/modules/presi/stores/decks.svelte.ts b/apps/mana/apps/web/src/lib/modules/presi/stores/decks.svelte.ts index 71568f5d1..876e4a71b 100644 --- a/apps/mana/apps/web/src/lib/modules/presi/stores/decks.svelte.ts +++ b/apps/mana/apps/web/src/lib/modules/presi/stores/decks.svelte.ts @@ -9,7 +9,8 @@ import { db } from '$lib/data/database'; import { presiDeckTable, slideTable } from '../collections'; import { toDeck, toSlide } from '../queries'; import { PresiEvents } from '@mana/shared-utils/analytics'; -import { encryptRecord } from '$lib/data/crypto'; +import { encryptRecord, decryptRecord } from '$lib/data/crypto'; +import { createBlock, updateBlock } from '$lib/data/time-blocks/service'; import type { LocalDeck, LocalSlide, @@ -164,6 +165,51 @@ function createDecksStore() { } } + async function startRehearsal(deckId: string): Promise { + const deck = await presiDeckTable.get(deckId); + if (!deck) return null; + if (deck.activeRehearsalBlockId) return deck.activeRehearsalBlockId; + + const decrypted = await decryptRecord('presiDecks', { ...deck }); + const deckTitle = decrypted?.title ?? 'Präsentation'; + const now = new Date().toISOString(); + + const timeBlockId = await createBlock({ + startDate: now, + endDate: null, + isLive: true, + kind: 'logged', + type: 'rehearsal', + sourceModule: 'presi', + sourceId: deckId, + title: `${deckTitle} — Probe`, + color: '#84cc16', + }); + + await presiDeckTable.update(deckId, { + activeRehearsalBlockId: timeBlockId, + updatedAt: now, + }); + + return timeBlockId; + } + + async function endRehearsal(deckId: string): Promise { + const deck = await presiDeckTable.get(deckId); + if (!deck?.activeRehearsalBlockId) return; + + const now = new Date().toISOString(); + await updateBlock(deck.activeRehearsalBlockId, { + endDate: now, + isLive: false, + }); + + await presiDeckTable.update(deckId, { + activeRehearsalBlockId: null, + updatedAt: now, + }); + } + return { get isLoading() { return isLoading; @@ -178,6 +224,8 @@ function createDecksStore() { updateSlide, deleteSlide, reorderSlides, + startRehearsal, + endRehearsal, }; } diff --git a/apps/mana/apps/web/src/lib/modules/presi/types.ts b/apps/mana/apps/web/src/lib/modules/presi/types.ts index 188a4c395..21389c413 100644 --- a/apps/mana/apps/web/src/lib/modules/presi/types.ts +++ b/apps/mana/apps/web/src/lib/modules/presi/types.ts @@ -9,6 +9,7 @@ export interface LocalDeck extends BaseRecord { description?: string | null; themeId?: string | null; isPublic: boolean; + activeRehearsalBlockId?: string | null; } export interface LocalSlide extends BaseRecord {