From 0bd00f267cc93f31906ea076927a4a0b3fbf6084 Mon Sep 17 00:00:00 2001 From: Mars-Marcelyn Date: Thu, 18 Jun 2026 19:51:48 +0200 Subject: [PATCH] 1.0.4 --- lang/en.json | 20 ++- lang/es.json | 20 ++- lang/gl.json | 20 ++- main.js | 420 +++++++++++++++++++++++++++++++++++++++++++++++--- manifest.json | 2 +- styles.css | 52 +++++++ 6 files changed, 505 insertions(+), 29 deletions(-) diff --git a/lang/en.json b/lang/en.json index d903a91..813073c 100644 --- a/lang/en.json +++ b/lang/en.json @@ -29,7 +29,24 @@ "customDurationName": "Custom duration", "customDurationDesc": "Enter duration in seconds", "totalTimeName": "Total session time", - "totalTimeDesc": "Calculated from images × duration", + "totalTimeDesc": "Calculated from images × duration plus breaks", + "breakEnabledName": "Enable breaks", + "breakEnabledDesc": "Pause the session periodically", + "breakEnabledInfiniteDesc": "Not available with Infinite duration", + "noticeBreakInfinite": "Breaks cannot be used with Infinite duration", + "breakDurationName": "Break duration", + "breakDurationDesc": "How long each break lasts", + "breakDurationPresets": { + "15s": "15s", + "30s": "30s", + "1min": "1 min", + "3min": "3 min", + "custom": "Custom" + }, + "breakEveryName": "Break frequency", + "breakEveryDesc": "Number of images between breaks", + "breakTitle": "Break time!", + "breakRemaining": "{0} remaining", "loggingName": "Session logging", "loggingDesc": "Log shown images to prioritize unseen ones next session", "clearLogName": "Clear session log", @@ -46,6 +63,7 @@ "60s": "60s", "2min": "2 min", "5min": "5 min", + "infinite": "Infinite", "custom": "Custom" }, diff --git a/lang/es.json b/lang/es.json index 5c6e94d..aaa1687 100644 --- a/lang/es.json +++ b/lang/es.json @@ -29,7 +29,24 @@ "customDurationName": "Duración personalizada", "customDurationDesc": "Introduce la duración en segundos", "totalTimeName": "Tiempo total de sesión", - "totalTimeDesc": "Calculado de imágenes × duración", + "totalTimeDesc": "Calculado de imágenes × duración más descansos", + "breakEnabledName": "Activar descansos", + "breakEnabledDesc": "Pausa la sesión periódicamente", + "breakEnabledInfiniteDesc": "No disponible con duración Infinita", + "noticeBreakInfinite": "Los descansos no pueden usarse con duración Infinita", + "breakDurationName": "Duración del descanso", + "breakDurationDesc": "Cuánto dura cada descanso", + "breakDurationPresets": { + "15s": "15s", + "30s": "30s", + "1min": "1 min", + "3min": "3 min", + "custom": "Personalizado" + }, + "breakEveryName": "Frecuencia de descansos", + "breakEveryDesc": "Número de imágenes entre descansos", + "breakTitle": "¡Descanso!", + "breakRemaining": "{0} restante", "loggingName": "Registro de sesión", "loggingDesc": "Registra las imágenes mostradas para priorizar las no vistas en la próxima sesión", "clearLogName": "Borrar registro de sesión", @@ -46,6 +63,7 @@ "60s": "60s", "2min": "2 min", "5min": "5 min", + "infinite": "Infinito", "custom": "Personalizado" }, diff --git a/lang/gl.json b/lang/gl.json index bd27bcf..935f750 100644 --- a/lang/gl.json +++ b/lang/gl.json @@ -29,7 +29,24 @@ "customDurationName": "Duración personalizada", "customDurationDesc": "Introduce a duración en segundos", "totalTimeName": "Tempo total de sesión", - "totalTimeDesc": "Calculado de imaxes × duración", + "totalTimeDesc": "Calculado de imaxes × duración máis descansos", + "breakEnabledName": "Activar descansos", + "breakEnabledDesc": "Pausa a sesión periódicamente", + "breakEnabledInfiniteDesc": "Non dispoñible con duración Infinita", + "noticeBreakInfinite": "Os descansos non poden usarse con duración Infinita", + "breakDurationName": "Duración do descanso", + "breakDurationDesc": "Canto dura cada descanso", + "breakDurationPresets": { + "15s": "15s", + "30s": "30s", + "1min": "1 min", + "3min": "3 min", + "custom": "Personalizado" + }, + "breakEveryName": "Frecuencia de descansos", + "breakEveryDesc": "Número de imaxes entre descansos", + "breakTitle": "¡Descanso!", + "breakRemaining": "{0} restante", "loggingName": "Rexistro de sesión", "loggingDesc": "Rexistra as imaxes mostradas para priorizar as non vistas na seguinte sesión", "clearLogName": "Borrar rexistro de sesión", @@ -46,6 +63,7 @@ "60s": "60s", "2min": "2 min", "5min": "5 min", + "infinite": "Infinito", "custom": "Personalizado" }, diff --git a/main.js b/main.js index 7584983..d92495a 100644 --- a/main.js +++ b/main.js @@ -30,7 +30,24 @@ const FALLBACK_LOCALE = { "customDurationName": "Custom duration", "customDurationDesc": "Enter duration in seconds", "totalTimeName": "Total session time", - "totalTimeDesc": "Calculated from images \u00d7 duration", + "totalTimeDesc": "Calculated from images \u00d7 duration plus breaks", + "breakEnabledName": "Enable breaks", + "breakEnabledDesc": "Pause the session periodically", + "breakEnabledInfiniteDesc": "Not available with Infinite duration", + "noticeBreakInfinite": "Breaks cannot be used with Infinite duration", + "breakDurationName": "Break duration", + "breakDurationDesc": "How long each break lasts", + "breakDurationPresets": { + "15s": "15s", + "30s": "30s", + "1min": "1 min", + "3min": "3 min", + "custom": "Custom" + }, + "breakEveryName": "Break frequency", + "breakEveryDesc": "Number of images between breaks", + "breakTitle": "Break time!", + "breakRemaining": "{0} remaining", "loggingName": "Session logging", "loggingDesc": "Log shown images to prioritize unseen ones next session", "clearLogName": "Clear session log", @@ -47,6 +64,7 @@ const FALLBACK_LOCALE = { "60s": "60s", "2min": "2 min", "5min": "5 min", + "infinite": "Infinite", "custom": "Custom" }, "introStartBtn": "Start", @@ -73,7 +91,11 @@ const DEFAULT_SETTINGS = { numPictures: 5, durationPerImage: 30, loop: true, + breakEnabled: false, + breakDuration: 60, + breakEvery: 5, language: 'en', + folderSettings: {}, }; module.exports = class VisualConceptLab extends Plugin { @@ -110,10 +132,38 @@ module.exports = class VisualConceptLab extends Plugin { async loadSettings() { this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); + if (this.settings.durationPerImage === -2) this.settings.durationPerImage = Infinity; + if (this.settings.folderSettings) { + for (const path of Object.keys(this.settings.folderSettings)) { + if (this.settings.folderSettings[path].durationPerImage === -2) { + this.settings.folderSettings[path].durationPerImage = Infinity; + } + } + } } async saveSettings() { - await this.saveData(this.settings); + const toSave = { ...this.settings }; + if (toSave.durationPerImage === Infinity) toSave.durationPerImage = -2; + if (toSave.folderSettings) { + toSave.folderSettings = { ...toSave.folderSettings }; + for (const path of Object.keys(toSave.folderSettings)) { + if (toSave.folderSettings[path].durationPerImage === Infinity) { + toSave.folderSettings[path] = { ...toSave.folderSettings[path], durationPerImage: -2 }; + } + } + } + await this.saveData(toSave); + } + + getFolderSettings(folderPath) { + return this.settings.folderSettings?.[folderPath] || null; + } + + saveFolderSettings(folderPath, settings) { + if (!this.settings.folderSettings) this.settings.folderSettings = {}; + this.settings.folderSettings[folderPath] = { ...settings }; + this.saveSettings(); } async loadLocale() { @@ -242,8 +292,51 @@ class SessionSetupModal extends Modal { this.numPictures = plugin.settings.numPictures; this.durationPerImage = plugin.settings.durationPerImage; this.loop = plugin.settings.loop; + this.breakEnabled = plugin.settings.breakEnabled; + this.breakDuration = plugin.settings.breakDuration; + this.breakEvery = plugin.settings.breakEvery; this.enableLogging = true; this.sliderComponent = null; + this.loadFolderSettings(); + } + + loadFolderSettings() { + const saved = this.selectedFolder ? this.plugin.getFolderSettings(this.selectedFolder) : null; + if (saved) { + this.numPictures = saved.numPictures; + this.durationPerImage = saved.durationPerImage; + this.loop = saved.loop; + if (saved.breakEnabled !== undefined) this.breakEnabled = saved.breakEnabled; + if (saved.breakDuration !== undefined) this.breakDuration = saved.breakDuration; + if (saved.breakEvery !== undefined) this.breakEvery = saved.breakEvery; + } + this.syncUI(); + } + + syncUI() { + this.updateSliderMax(); + if (this.durationDropdown) { + const matched = [30, 60, 120, 300].includes(this.durationPerImage); + const isInfinite = this.durationPerImage === Infinity; + this.durationDropdown.setValue(matched ? String(this.durationPerImage) : isInfinite ? 'Infinity' : '-1'); + if (this.customDurationSetting) { + this.customDurationSetting.classList.toggle('shuffle-lab-is-hidden', matched || isInfinite); + } + } + if (this.breakDurationDropdown) { + const matched = [15, 30, 60, 180].includes(this.breakDuration); + this.breakDurationDropdown.setValue(matched ? String(this.breakDuration) : '-1'); + if (this.customBreakDurationSetting) { + this.customBreakDurationSetting.classList.toggle('shuffle-lab-is-hidden', matched); + } + } + if (this.breakEverySlider) { + this.breakEverySlider.setValue(this.breakEvery); + } + if (this.loopToggle) { + this.loopToggle.setValue(this.loop); + } + this.updateBreakDisabledState(); } onOpen() { @@ -257,6 +350,8 @@ class SessionSetupModal extends Modal { contentEl.createEl('h2', { text: this.plugin.t('sessionHeading') }); + contentEl.createEl('hr'); + const folders = this.getFolders(); if (folders.length === 0) { @@ -278,11 +373,13 @@ class SessionSetupModal extends Modal { dropdown.setValue(current); dropdown.onChange(value => { this.selectedFolder = value; - this.updateSliderMax(); + this.loadFolderSettings(); this.updatePromptBtn(); }); }); + contentEl.createEl('hr'); + new Setting(contentEl) .setName(this.plugin.t('numImagesName')) .setDesc(this.plugin.t('numImagesDesc')) @@ -310,6 +407,7 @@ class SessionSetupModal extends Modal { .setName(this.plugin.t('loopName')) .setDesc(this.plugin.t('loopDesc')) .addToggle(toggle => { + this.loopToggle = toggle; toggle.setValue(this.loop); toggle.onChange(value => { this.loop = value; @@ -321,6 +419,7 @@ class SessionSetupModal extends Modal { { label: '60s', value: 60, localeKey: '60s' }, { label: '2 min', value: 120, localeKey: '2min' }, { label: '5 min', value: 300, localeKey: '5min' }, + { label: 'Infinite', value: Infinity, localeKey: 'infinite' }, { label: 'Custom', value: -1, localeKey: 'custom' }, ]; @@ -330,34 +429,136 @@ class SessionSetupModal extends Modal { .setName(this.plugin.t('durationName')) .setDesc(this.plugin.t('durationDesc')) .addDropdown(dropdown => { + this.durationDropdown = dropdown; DURATION_PRESETS.forEach(p => dropdown.addOption(String(p.value), this.plugin.t('durationPresets.' + p.localeKey))); dropdown.setValue(matchedPreset ? String(matchedPreset.value) : '-1'); dropdown.onChange(value => { - const num = parseInt(value); - if (num === -1) { - customDurationSetting.classList.remove('shuffle-lab-is-hidden'); + if (value === 'Infinity') { + this.durationPerImage = Infinity; + this.customDurationSetting.classList.add('shuffle-lab-is-hidden'); } else { - this.durationPerImage = num; - customDurationSetting.classList.add('shuffle-lab-is-hidden'); + const num = parseInt(value); + if (num === -1) { + this.customDurationSetting.classList.remove('shuffle-lab-is-hidden'); + } else { + this.durationPerImage = num; + this.customDurationSetting.classList.add('shuffle-lab-is-hidden'); + } } + this.updateBreakDisabledState(); + this.updateTotalTime(); }); }); - const customDurationSetting = new Setting(contentEl) + this.customDurationSetting = new Setting(contentEl) .setName(this.plugin.t('customDurationName')) .setDesc(this.plugin.t('customDurationDesc')) .addText(text => { text.setValue(String(this.durationPerImage)); + text.inputEl.addEventListener('input', () => { + text.inputEl.value = text.inputEl.value.replace(/\D/g, ''); + }); text.onChange(value => { const num = parseInt(value); if (!isNaN(num) && num > 0) { - this.durationPerImage = num; + this.durationPerImage = Math.min(num, 9999); + text.setValue(String(this.durationPerImage)); this.updateTotalTime(); } }); }).settingEl; - customDurationSetting.classList.toggle('shuffle-lab-is-hidden', !!matchedPreset); + this.customDurationSetting.classList.toggle('shuffle-lab-is-hidden', !!matchedPreset); + + contentEl.createEl('hr'); + + this.breakToggleSetting = new Setting(contentEl) + .setName(this.plugin.t('breakEnabledName')) + .setDesc(this.plugin.t('breakEnabledDesc')) + .addToggle(toggle => { + this.breakToggle = toggle; + toggle.setValue(this.breakEnabled); + toggle.onChange(value => { + if (value && this.durationPerImage === Infinity) { + new Notice(this.plugin.t('noticeBreakInfinite')); + toggle.setValue(false); + return; + } + this.breakEnabled = value; + this.breakSettingsEl.classList.toggle('shuffle-lab-is-hidden', !value); + this.updateTotalTime(); + }); + }); + + this.breakSettingsEl = contentEl.createDiv(); + this.breakSettingsEl.classList.toggle('shuffle-lab-is-hidden', !this.breakEnabled); + this.updateBreakDisabledState(); + + new Setting(this.breakSettingsEl) + .setName(this.plugin.t('breakEveryName')) + .setDesc(this.plugin.t('breakEveryDesc')) + .addSlider(slider => { + this.breakEverySlider = slider; + const max = Math.max(1, this.countImagesInTopic(this.selectedFolder)); + slider.setLimits(1, max, 1); + slider.setValue(this.breakEvery); + slider.setDynamicTooltip(); + slider.onChange(value => { + this.breakEvery = value; + this.updateTotalTime(); + }); + }); + + const BREAK_DURATION_PRESETS = [ + { label: '15s', value: 15, localeKey: '15s' }, + { label: '30s', value: 30, localeKey: '30s' }, + { label: '1 min', value: 60, localeKey: '1min' }, + { label: '3 min', value: 180, localeKey: '3min' }, + { label: 'Custom', value: -1, localeKey: 'custom' }, + ]; + + const matchedBreakPreset = BREAK_DURATION_PRESETS.find(p => p.value === this.breakDuration); + + new Setting(this.breakSettingsEl) + .setName(this.plugin.t('breakDurationName')) + .setDesc(this.plugin.t('breakDurationDesc')) + .addDropdown(dropdown => { + this.breakDurationDropdown = dropdown; + BREAK_DURATION_PRESETS.forEach(p => dropdown.addOption(String(p.value), this.plugin.t('breakDurationPresets.' + p.localeKey))); + dropdown.setValue(matchedBreakPreset ? String(matchedBreakPreset.value) : '-1'); + dropdown.onChange(value => { + const num = parseInt(value); + if (num === -1) { + this.customBreakDurationSetting.classList.remove('shuffle-lab-is-hidden'); + } else { + this.breakDuration = num; + this.customBreakDurationSetting.classList.add('shuffle-lab-is-hidden'); + this.updateTotalTime(); + } + }); + }); + + this.customBreakDurationSetting = new Setting(this.breakSettingsEl) + .setName(this.plugin.t('customDurationName')) + .setDesc(this.plugin.t('customDurationDesc')) + .addText(text => { + text.setValue(String(this.breakDuration)); + text.inputEl.addEventListener('input', () => { + text.inputEl.value = text.inputEl.value.replace(/\D/g, ''); + }); + text.onChange(value => { + const num = parseInt(value); + if (!isNaN(num) && num > 0) { + this.breakDuration = Math.min(num, 9999); + text.setValue(String(this.breakDuration)); + this.updateTotalTime(); + } + }); + }).settingEl; + + this.customBreakDurationSetting.classList.toggle('shuffle-lab-is-hidden', !!matchedBreakPreset); + + contentEl.createEl('hr'); this.totalTimeSetting = new Setting(contentEl) .setName(this.plugin.t('totalTimeName')) @@ -365,6 +566,8 @@ class SessionSetupModal extends Modal { this.totalTimeEl = this.totalTimeSetting.controlEl.createEl('span', { cls: 'drawing-session-total-time' }); this.updateTotalTime(); + contentEl.createEl('hr'); + new Setting(contentEl) .setName(this.plugin.t('loggingName')) .setDesc(this.plugin.t('loggingDesc')) @@ -380,6 +583,8 @@ class SessionSetupModal extends Modal { .setButtonText(this.plugin.t('clearLogBtn')) .onClick(() => this.clearLog())); + contentEl.createEl('hr'); + const promptSetting = new Setting(contentEl) .setName(this.plugin.t('promptFileName')) .setDesc(this.plugin.t('promptFileDesc')) @@ -394,6 +599,8 @@ class SessionSetupModal extends Modal { }); this.updatePromptBtn(); + contentEl.createEl('hr'); + new Setting(contentEl) .addButton(button => button .setButtonText(this.plugin.t('startBtn')) @@ -433,6 +640,20 @@ class SessionSetupModal extends Modal { return count; } + updateBreakDisabledState() { + const isInfinite = this.durationPerImage === Infinity; + if (this.breakToggle) { + if (this.breakToggleSetting) { + this.breakToggleSetting.setDesc(this.plugin.t(isInfinite ? 'breakEnabledInfiniteDesc' : 'breakEnabledDesc')); + } + if (isInfinite) { + this.breakEnabled = false; + this.breakToggle.setValue(false); + this.breakSettingsEl.classList.add('shuffle-lab-is-hidden'); + } + } + } + updateSliderMax() { if (!this.sliderComponent) return; const count = this.countImagesInTopic(this.selectedFolder); @@ -442,14 +663,29 @@ class SessionSetupModal extends Modal { this.numPictures = max; this.sliderComponent.setValue(max); } + if (this.breakEverySlider) { + this.breakEverySlider.setLimits(1, max, 1); + if (this.breakEvery > max) { + this.breakEvery = max; + this.breakEverySlider.setValue(max); + } + } this.warningEl.classList.toggle('shuffle-lab-is-hidden', count !== 0); this.updateTotalTime(); } updateTotalTime() { if (!this.totalTimeEl) return; - const totalSeconds = this.numPictures * this.durationPerImage; - this.totalTimeEl.textContent = formatTimeLong(totalSeconds); + if (this.durationPerImage === Infinity) { + this.totalTimeEl.textContent = '\u221E'; + } else { + let totalSeconds = this.numPictures * this.durationPerImage; + if (this.breakEnabled && this.breakEvery > 0) { + const numBreaks = Math.floor(this.numPictures / this.breakEvery); + totalSeconds += numBreaks * this.breakDuration; + } + this.totalTimeEl.textContent = formatTimeLong(totalSeconds); + } } startSession() { @@ -461,9 +697,20 @@ class SessionSetupModal extends Modal { this.plugin.settings.numPictures = this.numPictures; this.plugin.settings.durationPerImage = this.durationPerImage; this.plugin.settings.loop = this.loop; + this.plugin.settings.breakEnabled = this.breakEnabled; + this.plugin.settings.breakDuration = this.breakDuration; + this.plugin.settings.breakEvery = this.breakEvery; + this.plugin.saveFolderSettings(this.selectedFolder, { + numPictures: this.numPictures, + durationPerImage: this.durationPerImage, + loop: this.loop, + breakEnabled: this.breakEnabled, + breakDuration: this.breakDuration, + breakEvery: this.breakEvery, + }); this.plugin.saveSettings(); this.close(); - new SlideshowModal(this.app, this.plugin, this.selectedFolder, this.numPictures, this.durationPerImage, this.loop, this.enableLogging).open(); + new SlideshowModal(this.app, this.plugin, this.selectedFolder, this.numPictures, this.durationPerImage, this.loop, this.breakEnabled, this.breakDuration, this.breakEvery, this.enableLogging).open(); } async clearLog() { @@ -529,13 +776,16 @@ class SessionSetupModal extends Modal { } class SlideshowModal extends Modal { - constructor(app, plugin, sessionFolder, numPictures, durationPerImage, loop, enableLogging) { + constructor(app, plugin, sessionFolder, numPictures, durationPerImage, loop, breakEnabled, breakDuration, breakEvery, enableLogging) { super(app); this.plugin = plugin; this.sessionFolder = sessionFolder; this.numPictures = numPictures; this.durationPerImage = durationPerImage; this.loop = loop; + this.breakEnabled = breakEnabled; + this.breakDuration = breakDuration; + this.breakEvery = breakEvery; this.enableLogging = enableLogging; this.imageFiles = []; this.currentIndex = 0; @@ -547,6 +797,11 @@ class SlideshowModal extends Modal { this.shownImageNames = []; this.sessionInterval = null; this.imageInterval = null; + this.breakInterval = null; + this.isBreakTime = false; + this.imagesSinceBreak = 0; + this.pendingBreak = false; + this.breakTimeRemaining = 0; this.scope.register([], 'Escape', (e) => { this.stopSession(); return false; }); } @@ -759,6 +1014,12 @@ class SlideshowModal extends Modal { this.endOfDeckEl.createEl('p', { cls: 'drawing-session-endofdeck-hint', text: this.plugin.t('endOfDeckHint') }); this.endOfDeckEl.style.display = 'none'; + this.breakOverlayEl = contentEl.createDiv({ cls: 'drawing-session-break' }); + this.breakOverlayEl.createEl('div', { cls: 'drawing-session-break-icon', text: '\u2615' }); + this.breakOverlayEl.createEl('h2', { cls: 'drawing-session-break-title', text: this.plugin.t('breakTitle') }); + this.breakCountdownEl = this.breakOverlayEl.createEl('div', { cls: 'drawing-session-break-countdown' }); + this.breakOverlayEl.style.display = 'none'; + this.scope.register([], ' ', (e) => { e.preventDefault(); this.togglePause(); return false; }); } @@ -796,25 +1057,38 @@ class SlideshowModal extends Modal { this.imageTimeRemaining = this.durationPerImage; this.updateNavState(); this.updateTimeDisplay(); + if (this.breakEnabled && !this.isBreakTime) { + this.imagesSinceBreak++; + if (this.imagesSinceBreak >= this.breakEvery) { + this.imagesSinceBreak = 0; + if (this.loop || this.currentIndex < this.imageFiles.length - 1) { + this.pendingBreak = true; + } + } + } } startTimers() { this.sessionInterval = window.setInterval(() => { if (this.isPaused) return; - this.sessionTimeRemaining--; + if (this.sessionTimeRemaining !== Infinity) { + this.sessionTimeRemaining--; + } this.updateTimeDisplay(); if (this.sessionTimeRemaining <= 0) { this.sessionComplete(); } }, 1000); - this.imageInterval = window.setInterval(() => { - if (this.isPaused) return; - this.imageTimeRemaining--; - if (this.imageTimeRemaining <= 0) { - this.nextImage(); - } - }, 1000); + if (this.durationPerImage !== Infinity) { + this.imageInterval = window.setInterval(() => { + if (this.isPaused) return; + this.imageTimeRemaining--; + if (this.imageTimeRemaining <= 0) { + this.nextImage(); + } + }, 1000); + } } updateNavState() { @@ -841,15 +1115,65 @@ class SlideshowModal extends Modal { clearInterval(this.imageInterval); this.imageInterval = null; } + if (this.breakInterval !== null) { + clearInterval(this.breakInterval); + this.breakInterval = null; + } } showEndOfDeck() { this.showingEndOfDeck = true; this.endOfDeckEl.style.display = ''; - this.imageTimeRemaining = this.durationPerImage; + if (this.imageInterval !== null) { + clearInterval(this.imageInterval); + this.imageInterval = null; + } this.updateNavState(); } + showBreak() { + this.isBreakTime = true; + this.breakTimeRemaining = this.breakDuration; + if (this.imageInterval !== null) { + clearInterval(this.imageInterval); + this.imageInterval = null; + } + this.imageContainerEl.style.display = 'none'; + this.breakOverlayEl.style.display = ''; + this.updateBreakDisplay(); + this.breakInterval = window.setInterval(() => { + this.breakTimeRemaining--; + this.updateBreakDisplay(); + if (this.breakTimeRemaining <= 0) { + this.hideBreak(); + this.nextImage(); + } + }, 1000); + } + + hideBreak() { + clearInterval(this.breakInterval); + this.breakInterval = null; + this.isBreakTime = false; + this.breakOverlayEl.style.display = 'none'; + this.imageContainerEl.style.display = ''; + if (this.durationPerImage !== Infinity && this.imageInterval === null) { + this.imageInterval = window.setInterval(() => { + if (this.isPaused) return; + this.imageTimeRemaining--; + if (this.imageTimeRemaining <= 0) { + this.nextImage(); + } + }, 1000); + } + } + + updateBreakDisplay() { + this.breakCountdownEl.textContent = this.plugin.t('breakRemaining', formatBreakTime(this.breakTimeRemaining)); + const willExpire = this.sessionTimeRemaining !== Infinity && this.sessionTimeRemaining <= this.breakTimeRemaining; + this.breakCountdownEl.classList.toggle('drawing-session-break-countdown-warning', willExpire); + } + nextImage() { if (this.sessionEnded) { this.stopSession(); @@ -859,6 +1183,26 @@ class SlideshowModal extends Modal { this.sessionComplete(); return; } + if (this.isBreakTime) { + this.hideBreak(); + const next = this.currentIndex + 1; + if (next >= this.imageFiles.length) { + if (this.loop) { + this.imageFiles = shuffleArray(this.imageFiles); + this.showImage(0); + } else { + this.showEndOfDeck(); + } + } else { + this.showImage(next); + } + return; + } + if (this.pendingBreak) { + this.pendingBreak = false; + this.showBreak(); + return; + } const next = this.currentIndex + 1; if (next >= this.imageFiles.length) { if (this.loop) { @@ -874,6 +1218,15 @@ class SlideshowModal extends Modal { prevImage() { if (this.sessionEnded) return; + if (this.isBreakTime) { + this.hideBreak(); + if (this.currentIndex > 0) { + this.showImage(this.currentIndex - 1); + } else { + this.showImage(0); + } + return; + } if (this.showingEndOfDeck) { this.showImage(this.imageFiles.length - 1); return; @@ -915,7 +1268,11 @@ class SlideshowModal extends Modal { updateTimeDisplay() { if (this.timeDisplayEl) { - this.timeDisplayEl.textContent = '\u23F1 ' + formatTimeShort(this.sessionTimeRemaining); + if (this.sessionTimeRemaining === Infinity) { + this.timeDisplayEl.textContent = '\u23F1 \u221E'; + } else { + this.timeDisplayEl.textContent = '\u23F1 ' + formatBreakTime(this.sessionTimeRemaining); + } } if (this.counterDisplayEl) { this.counterDisplayEl.textContent = (this.currentIndex + 1) + ' / ' + this.imageFiles.length; @@ -943,6 +1300,19 @@ function formatTimeShort(seconds) { return String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0'); } +function formatBreakTime(seconds) { + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + const s = seconds % 60; + if (h > 0) { + return h + ':' + String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0'); + } + if (m > 0) { + return m + ':' + String(s).padStart(2, '0'); + } + return String(s) + 's'; +} + function formatTimeLong(seconds) { const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); diff --git a/manifest.json b/manifest.json index 498dc84..618e0dd 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "shuffle-lab", "name": "Shuffle Lab", - "version": "1.0.3", + "version": "1.0.4", "minAppVersion": "1.0.0", "description": "Timed, randomized fullscreen image slideshows for presentations, games, drawing sessions, and more.", "author": "Mars Marcelyn", diff --git a/styles.css b/styles.css index 5fe14b3..303cad5 100644 --- a/styles.css +++ b/styles.css @@ -2,6 +2,17 @@ padding: 0 8px; } +.session-setup-modal .setting-item { + border-bottom: none !important; + border-top: none !important; +} + +.session-setup-modal hr { + border: none; + border-top: 1px solid var(--background-modifier-border); + margin: 16px 0; +} + .drawing-session-modal { width: 100%; height: 100%; @@ -48,6 +59,7 @@ gap: 24px; padding: 20px; background: rgba(0, 0, 0, 0.6); + margin-top: auto; } .drawing-session-btn { @@ -234,6 +246,46 @@ margin: 8px 0 0; } +.drawing-session-break { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 100px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background: rgba(0, 0, 0, 0.92); + color: #fff; + z-index: 30; + gap: 16px; + padding: 40px; +} + +.drawing-session-break-icon { + font-size: 80px; + line-height: 1; +} + +.drawing-session-break-title { + font-size: 36px; + font-weight: 700; + margin: 0; + color: #fff; +} + +.drawing-session-break-countdown { + font-size: 48px; + font-weight: 700; + font-variant-numeric: tabular-nums; + color: #4caf50; +} + +.drawing-session-break-countdown-warning { + color: #e5c07b; +} + .shuffle-lab-modal { width: 100vw; height: 100vh;