This commit is contained in:
Mars-Marcelyn 2026-06-18 19:51:48 +02:00
parent 13a27af1c6
commit 0bd00f267c
6 changed files with 505 additions and 29 deletions

View file

@ -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"
},

View file

@ -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"
},

View file

@ -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"
},

420
main.js
View file

@ -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);

View file

@ -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",

View file

@ -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;