mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:41:09 +02:00
feat(timeblocks): integrate music, moodlit, presi modules
Extend the unified TimeBlock system to 3 more modules. New TimeBlockTypes: listening, mood, rehearsal New SourceModules: music, moodlit, presi - music: incrementPlayCount() creates 'listening' block with song title, artist, and duration-based endDate - moodlit: add startMoodSession/endMoodSession with live 'mood' blocks using the mood's primary color - presi: add startRehearsal/endRehearsal with live 'rehearsal' blocks for presentation practice sessions - Update analytics colors/labels, calendar filters, dashboard widgets Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e068335dd4
commit
cbfe995f7b
10 changed files with 140 additions and 7 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -33,6 +33,9 @@ const TYPE_COLORS: Record<TimeBlockType, string> = {
|
|||
guide: '#14b8a6',
|
||||
visit: '#a855f7',
|
||||
study: '#0ea5e9',
|
||||
listening: '#d946ef',
|
||||
mood: '#fb923c',
|
||||
rehearsal: '#84cc16',
|
||||
};
|
||||
|
||||
const TYPE_LABELS: Record<TimeBlockType, string> = {
|
||||
|
|
@ -50,6 +53,9 @@ const TYPE_LABELS: Record<TimeBlockType, string> = {
|
|||
guide: 'Guides',
|
||||
visit: 'Besuche',
|
||||
study: 'Lernen',
|
||||
listening: 'Musik',
|
||||
mood: 'Stimmung',
|
||||
rehearsal: 'Probe',
|
||||
};
|
||||
|
||||
function blockDuration(b: TimeBlock): number {
|
||||
|
|
|
|||
|
|
@ -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) ──────────────────────────
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -39,6 +39,9 @@ let visibleBlockTypes = $state<Set<TimeBlockType>>(
|
|||
'guide',
|
||||
'visit',
|
||||
'study',
|
||||
'listening',
|
||||
'mood',
|
||||
'rehearsal',
|
||||
])
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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<string[]>([]);
|
||||
let settings = $state<MoodSettings>({ ...DEFAULT_SETTINGS });
|
||||
let activeMood = $state<Mood | null>(null);
|
||||
let activeSessionBlockId = $state<string | null>(null);
|
||||
|
||||
// Load from localStorage on init
|
||||
if (typeof window !== 'undefined') {
|
||||
|
|
@ -65,6 +67,34 @@ function createMoodsStore() {
|
|||
activeMood = mood;
|
||||
},
|
||||
|
||||
async startMoodSession(mood: Mood): Promise<void> {
|
||||
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<void> {
|
||||
if (!activeSessionBlockId) return;
|
||||
await updateBlock(activeSessionBlockId, {
|
||||
endDate: new Date().toISOString(),
|
||||
isLive: false,
|
||||
});
|
||||
activeSessionBlockId = null;
|
||||
},
|
||||
|
||||
addMood(mood: Mood) {
|
||||
customMoods = [...customMoods, mood];
|
||||
persist();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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<string | null> {
|
||||
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<void> {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue