Skip to content

Commit

Permalink
Expand the concept of sticky settings
Browse files Browse the repository at this point in the history
Generalize and expand on the idea of remembering the state of some
settings/toggles:
* Add a new client setting that allows specific actions to 'stick'
  across the session, across all sessions, or not at all. Session in
  this case means "until the window is closed or refreshed."
* Create StickySettingsBase class that handles all the "what should the
  default be" logic, as well as saving/restoring session data.
* Make bulk add/shift/delete options sticky, as well as individual
  marker adds.

Tangential changes:
* Adjust inline marker type dropdown margins
* Add a tiny help icon next to client settings that have tooltips to
  indicate help is available on hover.
  • Loading branch information
danrahn committed Feb 19, 2024
1 parent a4c21c6 commit a163250
Show file tree
Hide file tree
Showing 16 changed files with 710 additions and 96 deletions.
21 changes: 13 additions & 8 deletions Client/Script/BulkActionCommon.js
Original file line number Diff line number Diff line change
Expand Up @@ -521,16 +521,21 @@ class BulkActionCommon {
/**
* Common UI to select specific marker type(s) for bulk operations.
* @param {string} label The label for the dropdown
* @param {() => void} callback The function to call when the value changes. */
static markerSelectType(label, callback) {
* @param {() => void} callback The function to call when the value changes.
* @param {number} initialValue The initial value to select in the dropdown. Defaults to 'All'. */
static markerSelectType(label, callback, initialValue=MarkerEnum.All) {
const select = appendChildren(
buildNode('select', { id : 'markerTypeSelect' }, 0, { change : callback }),
buildNode('option', { value : MarkerEnum.All }, 'All'),
buildNode('option', { value : MarkerEnum.Intro }, 'Intro'),
buildNode('option', { value : MarkerEnum.Credits }, 'Credits')
);

select.value = initialValue;

return appendChildren(buildNode('div'),
buildNode('label', { for : 'markerTypeSelect' }, label),
appendChildren(
buildNode('select', { id : 'markerTypeSelect' }, 0, { change : callback }),
buildNode('option', { value : MarkerEnum.All, selected : 'selected' }, 'All'),
buildNode('option', { value : MarkerEnum.Intro }, 'Intro'),
buildNode('option', { value : MarkerEnum.Credits }, 'Credits')
)
select
);
}
}
Expand Down
57 changes: 24 additions & 33 deletions Client/Script/BulkAddOverlay.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ import {

import { BulkActionCommon, BulkActionRow, BulkActionTable, BulkActionType } from './BulkActionCommon.js';
import { BulkMarkerResolveType, MarkerData } from '../../Shared/PlexTypes.js';
import BulkAddStickySettings from './StickySettings/BulkAddStickySettings.js';
import ButtonCreator from './ButtonCreator.js';
import { ClientSettings } from './ClientSettings.js';
import { ContextualLog } from '../../Shared/ConsoleLog.js';
import { getSvgIcon } from './SVGHelper.js';
import Icons from './Icons.js';
import { MarkerType } from '../../Shared/MarkerType.js';
import Overlay from './Overlay.js';
import { PlexClientState } from './PlexClientState.js';
import TableElements from './TableElements.js';
Expand All @@ -39,8 +40,6 @@ import Tooltip from './Tooltip.js';

const Log = new ContextualLog('BulkAddOverlay');

/** @typedef {{ chapterMode : boolean, chapterIndexMode : boolean }} BulkAddSettings */

/**
* UI for bulk adding markers to a given show/season
*/
Expand All @@ -61,8 +60,6 @@ class BulkAddOverlay {
#cachedStart = NaN;
/** @type {number} Cached ms of the current end input to prevent repeated calculations. */
#cachedEnd = NaN;
/** @type {number} The current resolution type. */
#cachedApplyType = BulkMarkerResolveType.Fail;
/** @type {HTMLElement} Cached chapter/manual mode toggle. */
#inputMode;
/** @type {ChapterMap} Chapter data for all individual episodes in this overlay. */
Expand All @@ -71,8 +68,8 @@ class BulkAddOverlay {
#cachedChapterStart;
/** @type {ChapterData} Cached baseline end chapter data. */
#cachedChapterEnd;
/** @type {boolean} Determines whether to favor chapter indexes or timestamps for fuzzy matching. */
#chapterIndexMode = false;
/** @type {BulkAddStickySettings} Applicable settings that might "stick" depending on client settings. */
#stickySettings = new BulkAddStickySettings();

/**
* List of descriptions for the various bulk marker resolution actions. */
Expand All @@ -89,8 +86,6 @@ class BulkAddOverlay {
"When an exact chapter name match isn't available, " +
"use the chapter's index to find matching chapters, not the closest timestamp");

static #settingsKey = 'bulkAdd';

/**
* Construct a new bulk add overlay.
* @param {ShowData|SeasonData} mediaItem */
Expand Down Expand Up @@ -169,15 +164,15 @@ class BulkAddOverlay {
),
appendChildren(buildNode('div', { id : 'bulkAddMarkerType' }),
buildNode('label', { for : 'markerTypeSelect' }, 'Marker Type: '),
appendChildren(buildNode('select', { id : 'markerTypeSelect' }),
buildNode('option', { value : 'intro', selected : 'selected' }, 'Intro'),
buildNode('option', { value : 'credits' }, 'Credits'))
appendChildren(buildNode('select', { id : 'markerTypeSelect' }, 0, { change : this.#onMarkerTypeChanged.bind(this) }),
buildNode('option', { value : MarkerType.Intro }, 'Intro'),
buildNode('option', { value : MarkerType.Credits }, 'Credits'))
),
appendChildren(buildNode('div', { id : 'bulkAddApplyType' }),
buildNode('label', { for : 'applyTypeSelect' }, 'Apply Action: '),
appendChildren(
buildNode('select', { id : 'applyTypeSelect' }, 0, { change : this.#onApplyTypeChange.bind(this) }),
buildNode('option', { value : 1, selected : 'selected' }, 'Fail'),
buildNode('option', { value : 1 }, 'Fail'),
buildNode('option', { value : 4 }, 'Overwrite'),
buildNode('option', { value : 2 }, 'Merge'),
buildNode('option', { value : 3 }, 'Ignore')),
Expand All @@ -196,6 +191,8 @@ class BulkAddOverlay {

this.#inputMode = $('#switchInputMethod', container);
Tooltip.setTooltip($('#chapterIndexModeHelp', container), BulkAddOverlay.#indexMatchingTooltip);
$('#markerTypeSelect', container).value = this.#stickySettings.markerType();
$('#applyTypeSelect', container).value = this.#stickySettings.applyType();

Overlay.build({
dismissible : true,
Expand Down Expand Up @@ -240,18 +237,24 @@ class BulkAddOverlay {
const tz = $('#timeZone');
tz.classList.toggle('hidden');
$('#chapterZone').classList.toggle('hidden');
if (tz.classList.contains('hidden')) {
const chapterMode = tz.classList.contains('hidden');
if (chapterMode) {
ButtonCreator.setText(this.#inputMode, 'Manual Mode');
ButtonCreator.setIcon(this.#inputMode, Icons.Cursor, ThemeColors.Primary);
} else {
ButtonCreator.setText(this.#inputMode, 'Chapter Mode');
ButtonCreator.setIcon(this.#inputMode, Icons.Chapter, ThemeColors.Primary);
}

this.#saveSessionSettings();
this.#stickySettings.setChapterMode(chapterMode);
this.#updateTableStats();
}

/** Update the type of marker to create. */
#onMarkerTypeChanged() {
this.#stickySettings.setMarkerType($('#markerTypeSelect').value);
}

/**
* When the baseline episode changes, populate the chapter dropdown with the new chapters
* and update the customization table. */
Expand Down Expand Up @@ -313,8 +316,7 @@ class BulkAddOverlay {
/**
* Update chapter index mode, i.e. whether chapter indexes or timestamps take precedence for fuzzy matching. */
#onChapterIndexModeChanged() {
this.#chapterIndexMode = $('#chapterIndexMode').checked;
this.#saveSessionSettings();
this.#stickySettings.setChapterIndexMode($('#chapterIndexMode').checked);
this.#updateTableStats();
}

Expand Down Expand Up @@ -357,12 +359,8 @@ class BulkAddOverlay {

if (!allEmpty) {
this.#inputMode.classList.remove('disabled');

/** @type {BulkAddSettings?} */
const sessionSettings = ClientSettings.getSessionSetting(BulkAddOverlay.#settingsKey);
if (sessionSettings?.chapterMode) {
if (sessionSettings.chapterIndexMode) {
this.#chapterIndexMode = true;
if (this.#stickySettings.chapterMode()) {
if (this.#stickySettings.chapterIndexMode()) {
$('#chapterIndexMode').checked = true;
}

Expand All @@ -371,13 +369,6 @@ class BulkAddOverlay {
}
}

#saveSessionSettings() {
ClientSettings.saveSessionSetting(BulkAddOverlay.#settingsKey, {
chapterMode : this.chapterMode(),
chapterIndexMode : this.chapterIndexMode()
});
}

/**
* Populate the baseline episode dropdown with all relevant episodes that have chapter data available. */
#populateChapterInfoDropdown() {
Expand Down Expand Up @@ -429,7 +420,7 @@ class BulkAddOverlay {
if (!sel) { return; }

const val = parseInt(sel.value);
this.#cachedApplyType = val;
this.#stickySettings.setApplyType(val);
$('#applyTypeDescription').innerText = BulkAddOverlay.#descriptions[val];
this.#updateTableStats();
}
Expand Down Expand Up @@ -647,14 +638,14 @@ class BulkAddOverlay {

startTime() { return this.#cachedStart; }
endTime() { return this.#cachedEnd; }
resolveType() { return this.#cachedApplyType; }
resolveType() { return this.#stickySettings.applyType(); }
/** @returns {string} */
markerType() { return $('#markerTypeSelect').value; }
/** @returns {boolean} */
chapterMode() { return $('#timeZone').classList.contains('hidden'); }
chapterStart() { return this.#cachedChapterStart; }
chapterEnd() { return this.#cachedChapterEnd; }
chapterIndexMode() { return this.#chapterIndexMode; }
chapterIndexMode() { return this.#stickySettings.chapterIndexMode(); }

/** Update all items in the customization table, if present. */
#updateTableStats() {
Expand Down
7 changes: 6 additions & 1 deletion Client/Script/BulkDeleteOverlay.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { $, appendChildren, buildNode, errorResponseOverlay, pad0, ServerCommand } from './Common.js';

import { BulkActionCommon, BulkActionRow, BulkActionTable, BulkActionType } from './BulkActionCommon.js';
import BulkDeleteStickySettings from './StickySettings/BulkDeleteStickySettings.js';
import ButtonCreator from './ButtonCreator.js';
import Icons from './Icons.js';
import { MarkerEnum } from '../../Shared/MarkerType.js';
Expand All @@ -26,6 +27,9 @@ class BulkDeleteOverlay {
/** @type {HTMLSelectElement} */
#appliesToDropdown;

/** @type {BulkDeleteStickySettings} */
#stickySettings = new BulkDeleteStickySettings();

/**
* Construct a new bulk delete overlay.
* @param {ShowData|SeasonData} mediaItem */
Expand All @@ -43,7 +47,7 @@ class BulkDeleteOverlay {
title,
buildNode('hr'),
buildNode('h4', {}, `Are you sure you want to bulk delete markers for ${this.#mediaItem.title}?<br>This cannot be undone.`),
BulkActionCommon.markerSelectType('Delete Marker Type(s): ', this.#onApplyToChanged.bind(this)),
BulkActionCommon.markerSelectType('Delete Marker Type(s): ', this.#onApplyToChanged.bind(this), this.#stickySettings.applyTo()),
appendChildren(buildNode('div', { id : 'bulkActionButtons' }),
ButtonCreator.fullButton('Delete All',
Icons.Confirm,
Expand Down Expand Up @@ -76,6 +80,7 @@ class BulkDeleteOverlay {
/** Adjusts the customization table (if visible) after the marker apply type is changed. */
#onApplyToChanged() {
const applyTo = this.#applyTo();
this.#stickySettings.setApplyTo(applyTo);
this.#table?.rows().forEach(row => {
if (!(row instanceof BulkDeleteRow)) {
return;
Expand Down
23 changes: 13 additions & 10 deletions Client/Script/BulkShiftOverlay.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { $, appendChildren, buildNode, msToHms, pad0, ServerCommand, timeInputSh
import { ContextualLog } from '../../Shared/ConsoleLog.js';

import { BulkActionCommon, BulkActionRow, BulkActionTable, BulkActionType } from './BulkActionCommon.js';
import BulkShiftStickySettings from './StickySettings/BulkShiftStickySettings.js';
import ButtonCreator from './ButtonCreator.js';
import Icons from './Icons.js';
import { MarkerEnum } from '../../Shared/MarkerType.js';
Expand Down Expand Up @@ -42,9 +43,6 @@ class BulkShiftOverlay {
/** @type {HTMLInputElement} */
#endTimeInput;

/** @type {boolean} */
#separateShift = false;

/** @type {HTMLSelectElement} */
#appliesToDropdown;

Expand All @@ -68,6 +66,8 @@ class BulkShiftOverlay {
/** @type {number} Cached end shift time, in milliseconds */
#endShiftMs;

#stickySettings = new BulkShiftStickySettings();

/**
* Construct a new shift overlay.
* @param {ShowData|SeasonData} mediaItem */
Expand All @@ -93,12 +93,13 @@ class BulkShiftOverlay {
keydown : timeInputShortcutHandler
});

const endVisible = this.#stickySettings.separateShift();
this.#endTimeInput = buildNode('input',
{ type : 'text',
placeholder : 'ms or mm:ss[.000]',
name : 'shiftEndTime',
id : 'shiftEndTime',
class : 'hidden' },
class : endVisible ? '' : 'hidden' },
0,
{ keyup : this.#onTimeShiftChange.bind(this),
keydown : timeInputShortcutHandler
Expand All @@ -112,18 +113,19 @@ class BulkShiftOverlay {
});

separateShiftCheck.addEventListener('change', this.#onSeparateShiftChange.bind(this, separateShiftCheck));
separateShiftCheck.checked = endVisible;
appendChildren(container,
title,
buildNode('hr'),
appendChildren(buildNode('div', { id : 'shiftZone' }),
buildNode('label', { for : 'shiftStartTime', id : 'shiftStartTimeLabel' }, 'Time shift: '),
this.#startTimeInput,
buildNode('label', { for : 'shiftEndTime', class : 'hidden', id : 'shiftEndTimeLabel' }, 'End shift: '),
buildNode('label', { for : 'shiftEndTime', class : endVisible ? '' : 'hidden', id : 'shiftEndTimeLabel' }, 'End shift: '),
this.#endTimeInput),
appendChildren(buildNode('div', { id : 'expandShrinkCheck' }),
buildNode('label', { for : 'separateShiftCheck' }, 'Shift start and end times separately: '),
separateShiftCheck),
BulkActionCommon.markerSelectType('Shift Marker Type(s): ', this.#onApplyToChanged.bind(this)),
BulkActionCommon.markerSelectType('Shift Marker Type(s): ', this.#onApplyToChanged.bind(this), this.#stickySettings.applyTo()),
appendChildren(buildNode('div', { id : 'bulkActionButtons' }),
ButtonCreator.fullButton('Apply',
Icons.Confirm,
Expand Down Expand Up @@ -182,8 +184,8 @@ class BulkShiftOverlay {
* Update UI when the user enables/disables the 'separate start/end' checkbox
* @param {HTMLInputElement} checkbox */
#onSeparateShiftChange(checkbox) {
this.#separateShift = checkbox.checked;
if (this.#separateShift) {
this.#stickySettings.setSeparateShift(checkbox.checked);
if (this.#stickySettings.separateShift()) {
$('#shiftStartTimeLabel').innerText = 'Start shift: ';
$('#shiftEndTimeLabel').classList.remove('hidden');
this.#endTimeInput.classList.remove('hidden');
Expand All @@ -209,6 +211,7 @@ class BulkShiftOverlay {
/**
* Recreate the marker table if it's showing and the marker apply type was changed. */
#onApplyToChanged() {
this.#stickySettings.setApplyTo(this.#applyTo());
if (this.#table) {
this.#check();
}
Expand Down Expand Up @@ -417,7 +420,7 @@ class BulkShiftOverlay {
/**
* Retrieve the current ms time of the end shift input, or the start time if we're not separating the shift.
* @returns {number} */
shiftEndValue() { return this.#separateShift ? this.#endShiftMs : this.#startShiftMs; }
shiftEndValue() { return this.#stickySettings.separateShift() ? this.#endShiftMs : this.#startShiftMs; }

table() { return this.#table; }

Expand All @@ -433,7 +436,7 @@ class BulkShiftOverlay {

this.#startShiftMs = timeToMs(this.#startTimeInput.value, true /*allowNegative*/);
checkTime(this.#startShiftMs, this.#startTimeInput);
if (this.#separateShift) {
if (this.#stickySettings.separateShift()) {
this.#endShiftMs = timeToMs(this.#endTimeInput.value, true /*allowNegative*/);
// If end is less than or equal to start, mark it invalid.
checkTime(this.#endShiftMs, this.#endTimeInput);
Expand Down
Loading

0 comments on commit a163250

Please sign in to comment.