Skip to content

Commit

Permalink
Small screen optimizations
Browse files Browse the repository at this point in the history
This application is still _very_ far from mobile optimized, but the
following changes make it slightly more manageable:

* Add a global "onresize" handler that fires an event when the screen
  size passes the mobile/desktop width threshold.
* Add "dynamic" buttons that show an icon and text when the screen is
  wide enough, otherwise only shows the icon (using the original text as
  the tooltip).
* Make thumbnail show/hide and chapter mode marker edit buttons dynamic.
* Make marker delete confirmation buttons dynamic.
* Shrink thumbnails on small-width devices
  • Loading branch information
danrahn committed Jan 27, 2024
1 parent 06ef0bd commit e367674
Show file tree
Hide file tree
Showing 9 changed files with 189 additions and 26 deletions.
62 changes: 59 additions & 3 deletions Client/Script/ButtonCreator.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { $$, appendChildren, buildNode } from './Common.js';
import { $, $$, appendChildren, buildNode } from './Common.js';
import { ContextualLog } from '../../Shared/ConsoleLog.js';

import { getSvgIcon } from './SVGHelper.js';
import Icons from './Icons.js';
import { PlexUI } from './PlexUI.js';
import { ThemeColors } from './ThemeColors.js';
import Tooltip from './Tooltip.js';

Expand All @@ -16,11 +17,37 @@ const Log = new ContextualLog('ButtonCreator');
* A static class that creates various buttons used throughout the app.
*/
class ButtonCreator {

/**
* One-time setup that initializes the window resize event listener that determines whether to show
* text labels for dynamic buttons. */
static Setup() {
PlexUI.addResizeListener(() => {
const small = PlexUI.isSmallScreen();
$('.button.resizable').forEach((button) => {
const buttonText = $$('.buttonText', button);
buttonText.classList[small ? 'add' : 'remove']('hidden');

// Don't override the tooltip if it was user-set.
if (!button.hasAttribute('data-default-tooltip')) {
return;
}

if (small) {
Tooltip.setTooltip(button, buttonText.innerText);
} else {
Tooltip.removeTooltip(button);
}

});
});
}

/**
* Creates a tabbable button with an associated icon.
* Creates a tabbable button with an associated icon and text.
* @param {string} text The text of the button.
* @param {keyof Icons} icon The icon to use.
* @param {keyof ThemeColors} color The color of the icon as a hex string (without the leading '#')
* @param {keyof ThemeColors} color The theme color of the icon.
* @param {EventListener} clickHandler The callback to invoke when the button is clicked.
* @param {AttributeMap} attributes Additional attributes to set on the button. */
static fullButton(text, icon, color, clickHandler, attributes={}) {
Expand All @@ -30,6 +57,35 @@ class ButtonCreator {
buildNode('span', { class : 'buttonText' }, text));
}

/**
* Creates a tabbable button with the associated icon and text. On small-width devices, hides the text.
* @param {string} text The text of the button.
* @param {keyof Icons} icon The icon to use.
* @param {keyof ThemeColors} color The theme color of the icon.
* @param {EventListener} clickHandler The callback to invoke when the button is clicked.
* @param {AttributeMap} attributes Additional attributes to set on the button. */
static dynamicButton(text, icon, color, clickHandler, attributes={}) {
if (attributes.class) {
attributes.class += ' resizable';
} else {
attributes.class = 'resizable';
}

const button = ButtonCreator.fullButton(text, icon, color, clickHandler, attributes);
if (!attributes.tooltip) {
button.setAttribute('data-default-tooltip', 1);
}

if (PlexUI.isSmallScreen()) {
$$('.buttonText', button).classList.add('hidden');
if (!attributes.tooltip) {
Tooltip.setTooltip(button, text);
}
}

return button;
}

/**
* Creates a button with only an icon, no associated label text.
* @param {keyof Icons} icon The name of the icon to add to the button.
Expand Down
31 changes: 25 additions & 6 deletions Client/Script/MarkerEdit.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import Icons from './Icons.js';
import { MarkerData } from '../../Shared/PlexTypes.js';
import { MarkerType } from '../../Shared/MarkerType.js';
import Overlay from './Overlay.js';
import { PlexUI } from './PlexUI.js';
import { ThemeColors } from './ThemeColors.js';
import Tooltip from './Tooltip.js';

Expand Down Expand Up @@ -374,7 +375,7 @@ class MarkerEdit {
child.classList.add('hidden');
}

const btn = ButtonCreator.fullButton(
const btn = ButtonCreator.dynamicButton(
'Chapters',
Icons.Chapter,
ThemeColors.Primary,
Expand Down Expand Up @@ -569,6 +570,17 @@ class MarkerEdit {
* An extension of MarkerEdit that handles showing/hiding thumbnails associated with the input timestamps.
*/
class ThumbnailMarkerEdit extends MarkerEdit {

/**
* One-time initialization to set up the window resize listener that adjusts the size of preview thumbnails. */
static Setup() {
PlexUI.addResizeListener(() => {
const width = PlexUI.isSmallScreen() ? 180 : 240;
$('.inputThumb').forEach(thumb => {
thumb.width = width;
});
});
}
/**
* Whether we ran into an error when loading a start/end thumbnail.
* @type {boolean[]} */
Expand Down Expand Up @@ -599,13 +611,15 @@ class ThumbnailMarkerEdit extends MarkerEdit {
const timestamp = (isEnd ? this.markerRow.endTime() : this.markerRow.startTime());
input.addEventListener('keyup', this.#onTimeInputKeyup.bind(this, input));
const src = `t/${this.markerRow.parent().mediaItem().metadataId}/${timestamp}`;
// Not dynamic, but good enough for government work.
const width = document.body.clientWidth < 768 ? '180px' : '240px';
const thumbnail = buildNode(
'img',
{
src : src,
class : `inputThumb loading thumb${isEnd ? 'End' : 'Start' }`,
alt : 'Timestamp thumbnail',
width : '240px',
width : width,
style : 'height: 0'
},
0,
Expand All @@ -630,11 +644,12 @@ class ThumbnailMarkerEdit extends MarkerEdit {

this.#thumbnailsCollapsed = ClientSettings.collapseThumbnails();
const startText = this.#thumbnailsCollapsed ? 'Show' : 'Hide';
const btn = ButtonCreator.fullButton(
const btn = ButtonCreator.dynamicButton(
startText,
Icons.Img,
ThemeColors.Primary,
this.#expandContractThumbnails.bind(this));
this.#expandContractThumbnails.bind(this),
{ tooltip : startText + ' thumbnails' });

btn.classList.add('thumbnailShowHide');
const options = this.markerRow.row().children[4];
Expand Down Expand Up @@ -783,7 +798,7 @@ class ThumbnailMarkerEdit extends MarkerEdit {
this.#cachedHeight = realHeight;
thumb.setAttribute('realheight', realHeight);
if (!this.#thumbnailsCollapsed && parseInt(thumb.style.height) === 0) {
slideDown(thumb, `${realHeight}px`, { duration : 250, noReset : true });
slideDown(thumb, `${realHeight}px`, { duration : 250, noReset : true }, () => thumb.style.removeProperty('height'));
}
}

Expand All @@ -806,7 +821,11 @@ class ThumbnailMarkerEdit extends MarkerEdit {
}
}));

if (button) { $$('span', button).innerText = hidden ? 'Show' : 'Hide'; }
if (button) {
const text = hidden ? 'Show' : 'Hide';
$$('span', button).innerText = text;
Tooltip.setText(button, text + ' thumbnails');
}

if (!this.#thumbnailsCollapsed) {
this.#refreshImage(thumb.parentNode);
Expand Down
13 changes: 9 additions & 4 deletions Client/Script/MarkerTableRow.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,8 @@ class ExistingMarkerRow extends MarkerRow {
* @returns {HTMLElement} */
#buildOptionButtons() {
return appendChildren(buildNode('div', { class : 'markerOptionsHolder' }),
ButtonCreator.fullButton('Edit', Icons.Edit, ThemeColors.Primary, e => this.editor().onEdit(e.shiftKey)),
ButtonCreator.fullButton('Delete', Icons.Delete, ThemeColors.Red,
ButtonCreator.dynamicButton('Edit', Icons.Edit, ThemeColors.Primary, e => this.editor().onEdit(e.shiftKey)),
ButtonCreator.dynamicButton('Delete', Icons.Delete, ThemeColors.Red,
this.#confirmMarkerDelete.bind(this), { class : 'deleteMarkerBtn' })
);
}
Expand All @@ -205,10 +205,15 @@ class ExistingMarkerRow extends MarkerRow {
dateAdded.appendChild(text);

options.children[0].style.display = 'none';
const cancel = ButtonCreator.fullButton('No', Icons.Cancel, ThemeColors.Red, this.#onMarkerDeleteCancel.bind(this));
const cancel = ButtonCreator.dynamicButton('No', Icons.Cancel, ThemeColors.Red, this.#onMarkerDeleteCancel.bind(this));
const delOptions = appendChildren(
buildNode('div', { class : 'markerOptionsHolder inlineMarkerDeleteButtons' }),
ButtonCreator.fullButton('Yes', Icons.Confirm, ThemeColors.Green, this.#onMarkerDelete.bind(this), { class : 'confirmDelete' }),
ButtonCreator.dynamicButton('Yes',
Icons.Confirm,
ThemeColors.Green,
this.#onMarkerDelete.bind(this),
{ class : 'confirmDelete' }
),
cancel,
);

Expand Down
16 changes: 16 additions & 0 deletions Client/Script/PlexClientState.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,22 @@ class PlexClientStateManager {
}

Instance = new PlexClientStateManager();

// When the window resizes, let search results know so they can
// adjust their UI accordingly.
PlexUI.addResizeListener(() => {
if (Instance.#activeSeason) {
Instance.#activeSeason.notifyWindowResize();
}

if (Instance.#activeSectionType === SectionType.Movie) {
/** @type {MovieResultRow} */
let searchRow;
for (searchRow of PlexUI.getActiveSearchRows()) {
searchRow.updateMarkerBreakdown();
}
}
});
}

constructor() {
Expand Down
23 changes: 23 additions & 0 deletions Client/Script/PlexUI.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ class PlexUIManager {
order : SortOrder.Ascending
};

/**
* Determines whether the window is classified as "small".
* Cached to avoid unnecessary queries if a resize event doesn't change our window size classification. */
#smallScreen = {};

/** Creates the singleton PlexUI for this session. */
static CreateInstance() {
if (Instance) {
Expand Down Expand Up @@ -552,6 +557,24 @@ class PlexUIManager {
// they both are UI-related.
PlexClientState.onFilterApplied();
}

/**
* Adds an listener to the window resize event.
* Ensures the event is only triggered when the small/large screen threshold is crossed.
* @param {(e: Event) => void} callback */
addResizeListener(callback) {
this.#smallScreen[callback] = document.body.clientWidth < 768;
window.addEventListener('resize', () => {
if (this.#smallScreen[callback] === document.body.clientWidth < 768) {
return;
}

this.#smallScreen[callback] = !this.#smallScreen[callback];
callback();
});
}

isSmallScreen() { return document.body.clientWidth < 768; }
}

export { PlexUIManager, UISection, Instance as PlexUI };
29 changes: 26 additions & 3 deletions Client/Script/ResultRow.js
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,8 @@ class ResultRow {
tooltipText += this.hasPurgedMarkers() ? '<hr>' : '</span>';
}

const percent = (atLeastOne / mediaItem.episodeCount * 100).toFixed(2);
// TODO: Is it worth making toFixed dynamic? I don't think so.
const percent = (atLeastOne / mediaItem.episodeCount * 100).toFixed(PlexUI.isSmallScreen() ? 0 : 2);
const innerText = buildNode('span', {}, `${atLeastOne}/${mediaItem.episodeCount} (${percent}%)`);

if (this.hasPurgedMarkers()) {
Expand Down Expand Up @@ -1135,6 +1136,18 @@ class SeasonResultRow extends ResultRow {

super.updateMarkerBreakdown();
}

/** Ensure all episode results marker counts are expanded/minified when the
* window is resized. */
notifyWindowResize() {
if (!this.#episodes) {
return;
}

for (const episode of Object.values(this.#episodes)) {
episode.updateMarkerBreakdown();
}
}
}

/**
Expand Down Expand Up @@ -1384,7 +1397,13 @@ class EpisodeResultRow extends BaseItemResultRow {
#buildMarkerText() {
const episode = this.episode();
const hasPurges = this.hasPurgedMarkers();
const text = buildNode('span', {}, plural(episode.markerTable().markerCount(), 'Marker'));
const smallScreen = PlexUI.isSmallScreen();
const markerCount = episode.markerTable().markerCount();
const text = buildNode('span', {}, smallScreen ? markerCount.toString() : plural(markerCount, 'Marker'));
if (smallScreen) {
text.classList.add('smallScreenMarkerCount');
}

if (hasPurges) {
text.appendChild(purgeIcon());
}
Expand Down Expand Up @@ -1546,7 +1565,11 @@ class MovieResultRow extends BaseItemResultRow {
markerCount = movie.markerTable().markerCount();
}

text = buildNode('span', {}, plural(markerCount, 'Marker'));
const smallScreen = PlexUI.isSmallScreen();
text = buildNode('span', {}, smallScreen ? markerCount.toString() : plural(markerCount, 'Marker'));
if (smallScreen) {
text.classList.add('smallScreenMarkerCount');
}
}

if (hasPurges) {
Expand Down
6 changes: 5 additions & 1 deletion Client/Script/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import { BaseLog } from '../../Shared/ConsoleLog.js';

import { ClientSettings, SettingsManager } from './ClientSettings.js';
import { PlexUI, PlexUIManager } from './PlexUI.js';
import ButtonCreator from './ButtonCreator.js';
import HelpOverlay from './HelpOverlay.js';
import MarkerBreakdownManager from './MarkerBreakdownChart.js';
import { PlexClientStateManager } from './PlexClientState.js';
import { PurgedMarkerManager } from './PurgedMarkerManager.js';
import { ThumbnailMarkerEdit } from './MarkerEdit.js';
import Tooltip from './Tooltip.js';
import VersionManager from './VersionManager.js';

Expand All @@ -18,9 +20,11 @@ window.addEventListener('load', setup);
function setup() {
HelpOverlay.SetupHelperListeners();
SettingsManager.CreateInstance();
PlexClientStateManager.CreateInstance();
PlexUIManager.CreateInstance();
PlexClientStateManager.CreateInstance();
Tooltip.Setup();
ButtonCreator.Setup();
ThumbnailMarkerEdit.Setup();

// MarkerBreakdownManager is self-contained - we don't need anything from it,
// and it doesn't need anything from us, so no need to keep a reference to it.
Expand Down

0 comments on commit e367674

Please sign in to comment.