diff --git a/README.md b/README.md index 7745eca..f033c97 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ npm run deploy - `/timer help` – Timer-Hilfe ### Stoppuhr (Hochzählender Timer) -- `/stopwatch start ` – Startet eine neue Stoppuhr mit Namen +- `/stopwatch start [startzeit]` – Startet eine neue Stoppuhr mit Namen (optional mit vergangener Startzeit z.B. `14:30`, `2h`) - `/stopwatch status [name]` – Abgelaufene Zeit abfragen - `/stopwatch stop ` – Stoppuhr beenden & Laufzeit ausgeben - `/stopwatch list` – Aktive Stoppuhren auflisten diff --git a/src/commands/utility/stopwatch.ts b/src/commands/utility/stopwatch.ts index a8e94e5..b2693bf 100644 --- a/src/commands/utility/stopwatch.ts +++ b/src/commands/utility/stopwatch.ts @@ -18,6 +18,73 @@ function formatDuration(ms: number): string { return parts.join(' '); } +function parseCustomStartTime(input: string): Date | null { + const now = new Date(); + const cleanInput = input.trim(); + + // Relative duration offset, e.g. "30m", "2h", "1d" (meaning X ago) + const durationRegex = /^(\d+)\s*([a-z]+)$/i; + const durMatch = durationRegex.exec(cleanInput); + if (durMatch) { + const val = parseInt(durMatch[1]); + const unit = durMatch[2].toLowerCase(); + const unitMs: Record = { + s: 1000, sec: 1000, sek: 1000, + m: 60000, min: 60000, + h: 3600000, std: 3600000, hour: 3600000, + d: 86400000, tag: 86400000, tage: 86400000, day: 86400000 + }; + if (unitMs[unit]) { + return new Date(now.getTime() - val * unitMs[unit]); + } + } + + // Time only HH:MM (assumes today's date, or yesterday if HH:MM is in future today) + const timeOnlyRegex = /^(\d{1,2}):(\d{2})$/; + const timeMatch = timeOnlyRegex.exec(cleanInput); + if (timeMatch) { + const h = parseInt(timeMatch[1]); + const m = parseInt(timeMatch[2]); + const date = new Date(now.getFullYear(), now.getMonth(), now.getDate(), h, m, 0); + if (date > now) { + date.setDate(date.getDate() - 1); + } + return date; + } + + // German Date format: DD.MM.YYYY HH:MM or DD.MM.YYYY + const germanDateRegex = /^(\d{1,2})\.(\d{1,2})\.(\d{4})(?:\s+(\d{1,2}):(\d{2}))?$/; + const germanMatch = germanDateRegex.exec(cleanInput); + if (germanMatch) { + const day = parseInt(germanMatch[1]); + const month = parseInt(germanMatch[2]) - 1; + const year = parseInt(germanMatch[3]); + const hour = germanMatch[4] ? parseInt(germanMatch[4]) : 0; + const min = germanMatch[5] ? parseInt(germanMatch[5]) : 0; + + const date = new Date(year, month, day, hour, min, 0); + if (isNaN(date.getTime())) return null; + return date; + } + + // ISO Date format: YYYY-MM-DD HH:MM + const isoDateRegex = /^(\d{4})-(\d{1,2})-(\d{1,2})(?:[\sT](\d{1,2}):(\d{2}))?$/; + const isoMatch = isoDateRegex.exec(cleanInput); + if (isoMatch) { + const year = parseInt(isoMatch[1]); + const month = parseInt(isoMatch[2]) - 1; + const day = parseInt(isoMatch[3]); + const hour = isoMatch[4] ? parseInt(isoMatch[4]) : 0; + const min = isoMatch[5] ? parseInt(isoMatch[5]) : 0; + + const date = new Date(year, month, day, hour, min, 0); + if (isNaN(date.getTime())) return null; + return date; + } + + return null; +} + const command: Command = { data: new SlashCommandBuilder() .setName('stopwatch') @@ -29,7 +96,11 @@ const command: Command = { .addStringOption(option => option.setName('name') .setDescription('Der Name der Stoppuhr (z.B. "Gaming", "Lernen")') - .setRequired(true))) + .setRequired(true)) + .addStringOption(option => + option.setName('startzeit') + .setDescription('Optionale Startzeit (z.B. "14:30", "03.08.2026 14:30", "2h")') + .setRequired(false))) .addSubcommand(subcommand => subcommand .setName('status') @@ -70,11 +141,11 @@ const command: Command = { .setColor(0x3498db) .setDescription('Verwalte hochzählende Timer (Stoppuhren).') .addFields( - { name: '`/stopwatch start `', value: 'Startet eine neue Stoppuhr mit dem angegebenen Namen.', inline: false }, + { name: '`/stopwatch start [startzeit]`', value: 'Startet eine neue Stoppuhr mit Namen.\nOptional kann eine vergangene Startzeit angegeben werden (z.B. `14:30`, `03.08.2026 14:30` oder `2h`).', inline: false }, { name: '`/stopwatch status [name]`', value: 'Zeigt die aktuelle Laufzeit einer oder aller Stoppuhren an.', inline: false }, { name: '`/stopwatch stop `', value: 'Stoppt eine Stoppuhr und gibt die finale Laufzeit aus.', inline: false }, { name: '`/stopwatch list`', value: 'Listet alle deine aktiven Stoppuhren auf diesem Server auf.', inline: false }, - { name: 'Beispiele', value: '`/stopwatch start name: Zocksession`\n`/stopwatch status name: Zocksession`\n`/stopwatch stop name: Zocksession`', inline: false } + { name: 'Beispiele', value: '`/stopwatch start name: Zocksession`\n`/stopwatch start name: Lernen startzeit: 14:00`\n`/stopwatch start name: Workout startzeit: 30m`\n`/stopwatch status name: Zocksession`\n`/stopwatch stop name: Zocksession`', inline: false } ) .setTimestamp(); @@ -84,40 +155,66 @@ const command: Command = { if (subcommand === 'start') { const name = interaction.options.getString('name')!.trim(); + const startInput = interaction.options.getString('startzeit')?.trim(); + // Duplicate check (case-insensitive) const existing: any = DB.get( - 'SELECT * FROM stopwatches WHERE guild_id = ? AND user_id = ? AND name = ?', + 'SELECT * FROM stopwatches WHERE guild_id = ? AND user_id = ? AND LOWER(name) = ?', guildId, userId, - name + name.toLowerCase() ); if (existing) { const startTime = new Date(existing.start_time); const unixTs = Math.floor(startTime.getTime() / 1000); await interaction.reply({ - content: `⚠️ Du hast bereits eine Stoppuhr namens **"${name}"** laufen (gestartet ).\nNutze \`/stopwatch status name:${name}\` oder \`/stopwatch stop name:${name}\`.`, + content: `⚠️ Du hast bereits eine Stoppuhr namens **"${existing.name}"** laufen (gestartet ).\nNutze \`/stopwatch status name:${existing.name}\` oder \`/stopwatch stop name:${existing.name}\`.`, ephemeral: true }); return; } + let startTimeDate = new Date(); + + if (startInput) { + const parsed = parseCustomStartTime(startInput); + if (!parsed) { + await interaction.reply({ + content: '❌ Ungültige Startzeit. Gültige Formate: `"14:30"`, `"03.08.2026 14:30"`, `"2026-08-03 14:30"` oder relativer Abstand wie `"2h"`, `"30m"`.', + ephemeral: true + }); + return; + } + if (parsed.getTime() > Date.now()) { + await interaction.reply({ + content: '❌ Die Startzeit einer Stoppuhr darf nicht in der Zukunft liegen.', + ephemeral: true + }); + return; + } + startTimeDate = parsed; + } + DB.run( - 'INSERT INTO stopwatches (guild_id, channel_id, user_id, name, start_time) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)', + 'INSERT INTO stopwatches (guild_id, channel_id, user_id, name, start_time) VALUES (?, ?, ?, ?, ?)', guildId, channelId, userId, - name + name, + startTimeDate.toISOString() ); - const nowUnix = Math.floor(Date.now() / 1000); + const startUnix = Math.floor(startTimeDate.getTime() / 1000); + const elapsedMs = Date.now() - startTimeDate.getTime(); const embed = new EmbedBuilder() .setTitle(`⏱️ Stoppuhr "${name}" gestartet`) .setColor(0x2ecc71) .addFields( { name: 'Name', value: name, inline: true }, - { name: 'Gestartet um', value: ` ()`, inline: true } + { name: 'Gestartet um', value: ` ()`, inline: true }, + { name: 'Laufzeit bisher', value: `**${formatDuration(elapsedMs)}**`, inline: false } ) .setTimestamp(); @@ -130,10 +227,10 @@ const command: Command = { if (name) { const sw: any = DB.get( - 'SELECT * FROM stopwatches WHERE guild_id = ? AND user_id = ? AND name = ?', + 'SELECT * FROM stopwatches WHERE guild_id = ? AND user_id = ? AND LOWER(name) = ?', guildId, userId, - name + name.toLowerCase() ); if (!sw) { @@ -198,10 +295,10 @@ const command: Command = { const name = interaction.options.getString('name')!.trim(); const sw: any = DB.get( - 'SELECT * FROM stopwatches WHERE guild_id = ? AND user_id = ? AND name = ?', + 'SELECT * FROM stopwatches WHERE guild_id = ? AND user_id = ? AND LOWER(name) = ?', guildId, userId, - name + name.toLowerCase() ); if (!sw) { @@ -220,7 +317,7 @@ const command: Command = { DB.run('DELETE FROM stopwatches WHERE id = ?', sw.id); const embed = new EmbedBuilder() - .setTitle(`⏹️ Stoppuhr "${name}" beendet`) + .setTitle(`⏹️ Stoppuhr "${sw.name}" beendet`) .setColor(0xe74c3c) .addFields( { name: 'Gesamtlaufzeit', value: `**${formatDuration(elapsedMs)}**`, inline: true },