mirror of
https://github.com/mars-marcelyn/Shuffle-Lab.git
synced 2026-07-22 07:44:45 +00:00
1.0.4
This commit is contained in:
parent
13a27af1c6
commit
0bd00f267c
6 changed files with 505 additions and 29 deletions
20
lang/en.json
20
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"
|
||||
},
|
||||
|
||||
|
|
|
|||
20
lang/es.json
20
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"
|
||||
},
|
||||
|
||||
|
|
|
|||
20
lang/gl.json
20
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"
|
||||
},
|
||||
|
||||
|
|
|
|||
420
main.js
420
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);
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
52
styles.css
52
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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue