feat: add optional custom starttime and duplicate name check to /stopwatch
All checks were successful
Auto Build and Push Docker Image / build (push) Successful in 6s
All checks were successful
Auto Build and Push Docker Image / build (push) Successful in 6s
This commit is contained in:
@@ -90,7 +90,7 @@ npm run deploy
|
|||||||
- `/timer help` – Timer-Hilfe
|
- `/timer help` – Timer-Hilfe
|
||||||
|
|
||||||
### Stoppuhr (Hochzählender Timer)
|
### Stoppuhr (Hochzählender Timer)
|
||||||
- `/stopwatch start <name>` – Startet eine neue Stoppuhr mit Namen
|
- `/stopwatch start <name> [startzeit]` – Startet eine neue Stoppuhr mit Namen (optional mit vergangener Startzeit z.B. `14:30`, `2h`)
|
||||||
- `/stopwatch status [name]` – Abgelaufene Zeit abfragen
|
- `/stopwatch status [name]` – Abgelaufene Zeit abfragen
|
||||||
- `/stopwatch stop <name>` – Stoppuhr beenden & Laufzeit ausgeben
|
- `/stopwatch stop <name>` – Stoppuhr beenden & Laufzeit ausgeben
|
||||||
- `/stopwatch list` – Aktive Stoppuhren auflisten
|
- `/stopwatch list` – Aktive Stoppuhren auflisten
|
||||||
|
|||||||
@@ -18,6 +18,73 @@ function formatDuration(ms: number): string {
|
|||||||
return parts.join(' ');
|
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<string, number> = {
|
||||||
|
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 = {
|
const command: Command = {
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
.setName('stopwatch')
|
.setName('stopwatch')
|
||||||
@@ -29,7 +96,11 @@ const command: Command = {
|
|||||||
.addStringOption(option =>
|
.addStringOption(option =>
|
||||||
option.setName('name')
|
option.setName('name')
|
||||||
.setDescription('Der Name der Stoppuhr (z.B. "Gaming", "Lernen")')
|
.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 =>
|
.addSubcommand(subcommand =>
|
||||||
subcommand
|
subcommand
|
||||||
.setName('status')
|
.setName('status')
|
||||||
@@ -70,11 +141,11 @@ const command: Command = {
|
|||||||
.setColor(0x3498db)
|
.setColor(0x3498db)
|
||||||
.setDescription('Verwalte hochzählende Timer (Stoppuhren).')
|
.setDescription('Verwalte hochzählende Timer (Stoppuhren).')
|
||||||
.addFields(
|
.addFields(
|
||||||
{ name: '`/stopwatch start <name>`', value: 'Startet eine neue Stoppuhr mit dem angegebenen Namen.', inline: false },
|
{ name: '`/stopwatch start <name> [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 status [name]`', value: 'Zeigt die aktuelle Laufzeit einer oder aller Stoppuhren an.', inline: false },
|
||||||
{ name: '`/stopwatch stop <name>`', value: 'Stoppt eine Stoppuhr und gibt die finale Laufzeit aus.', inline: false },
|
{ name: '`/stopwatch stop <name>`', 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: '`/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();
|
.setTimestamp();
|
||||||
|
|
||||||
@@ -84,40 +155,66 @@ const command: Command = {
|
|||||||
|
|
||||||
if (subcommand === 'start') {
|
if (subcommand === 'start') {
|
||||||
const name = interaction.options.getString('name')!.trim();
|
const name = interaction.options.getString('name')!.trim();
|
||||||
|
const startInput = interaction.options.getString('startzeit')?.trim();
|
||||||
|
|
||||||
|
// Duplicate check (case-insensitive)
|
||||||
const existing: any = DB.get(
|
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,
|
guildId,
|
||||||
userId,
|
userId,
|
||||||
name
|
name.toLowerCase()
|
||||||
);
|
);
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
const startTime = new Date(existing.start_time);
|
const startTime = new Date(existing.start_time);
|
||||||
const unixTs = Math.floor(startTime.getTime() / 1000);
|
const unixTs = Math.floor(startTime.getTime() / 1000);
|
||||||
await interaction.reply({
|
await interaction.reply({
|
||||||
content: `⚠️ Du hast bereits eine Stoppuhr namens **"${name}"** laufen (gestartet <t:${unixTs}:R>).\nNutze \`/stopwatch status name:${name}\` oder \`/stopwatch stop name:${name}\`.`,
|
content: `⚠️ Du hast bereits eine Stoppuhr namens **"${existing.name}"** laufen (gestartet <t:${unixTs}:R>).\nNutze \`/stopwatch status name:${existing.name}\` oder \`/stopwatch stop name:${existing.name}\`.`,
|
||||||
ephemeral: true
|
ephemeral: true
|
||||||
});
|
});
|
||||||
return;
|
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(
|
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,
|
guildId,
|
||||||
channelId,
|
channelId,
|
||||||
userId,
|
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()
|
const embed = new EmbedBuilder()
|
||||||
.setTitle(`⏱️ Stoppuhr "${name}" gestartet`)
|
.setTitle(`⏱️ Stoppuhr "${name}" gestartet`)
|
||||||
.setColor(0x2ecc71)
|
.setColor(0x2ecc71)
|
||||||
.addFields(
|
.addFields(
|
||||||
{ name: 'Name', value: name, inline: true },
|
{ name: 'Name', value: name, inline: true },
|
||||||
{ name: 'Gestartet um', value: `<t:${nowUnix}:F> (<t:${nowUnix}:R>)`, inline: true }
|
{ name: 'Gestartet um', value: `<t:${startUnix}:F> (<t:${startUnix}:R>)`, inline: true },
|
||||||
|
{ name: 'Laufzeit bisher', value: `**${formatDuration(elapsedMs)}**`, inline: false }
|
||||||
)
|
)
|
||||||
.setTimestamp();
|
.setTimestamp();
|
||||||
|
|
||||||
@@ -130,10 +227,10 @@ const command: Command = {
|
|||||||
|
|
||||||
if (name) {
|
if (name) {
|
||||||
const sw: any = DB.get(
|
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,
|
guildId,
|
||||||
userId,
|
userId,
|
||||||
name
|
name.toLowerCase()
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!sw) {
|
if (!sw) {
|
||||||
@@ -198,10 +295,10 @@ const command: Command = {
|
|||||||
const name = interaction.options.getString('name')!.trim();
|
const name = interaction.options.getString('name')!.trim();
|
||||||
|
|
||||||
const sw: any = DB.get(
|
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,
|
guildId,
|
||||||
userId,
|
userId,
|
||||||
name
|
name.toLowerCase()
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!sw) {
|
if (!sw) {
|
||||||
@@ -220,7 +317,7 @@ const command: Command = {
|
|||||||
DB.run('DELETE FROM stopwatches WHERE id = ?', sw.id);
|
DB.run('DELETE FROM stopwatches WHERE id = ?', sw.id);
|
||||||
|
|
||||||
const embed = new EmbedBuilder()
|
const embed = new EmbedBuilder()
|
||||||
.setTitle(`⏹️ Stoppuhr "${name}" beendet`)
|
.setTitle(`⏹️ Stoppuhr "${sw.name}" beendet`)
|
||||||
.setColor(0xe74c3c)
|
.setColor(0xe74c3c)
|
||||||
.addFields(
|
.addFields(
|
||||||
{ name: 'Gesamtlaufzeit', value: `**${formatDuration(elapsedMs)}**`, inline: true },
|
{ name: 'Gesamtlaufzeit', value: `**${formatDuration(elapsedMs)}**`, inline: true },
|
||||||
|
|||||||
Reference in New Issue
Block a user