Skip to content

Commit

Permalink
State gallery: Title box with loading status indicator
Browse files Browse the repository at this point in the history
  • Loading branch information
midlik committed Aug 6, 2024
1 parent 66dd17e commit e887bb1
Show file tree
Hide file tree
Showing 3 changed files with 42 additions and 12 deletions.
4 changes: 3 additions & 1 deletion src/app/extensions/state-gallery/behavior.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { PluginBehavior } from 'molstar/lib/mol-plugin/behavior';
import { BehaviorSubject } from 'rxjs';
import { clearExtensionCustomState, extensionCustomStateGetter } from '../../plugin-custom-state';
import { StateGalleryManager } from './manager';
import { LoadingStatus, StateGalleryManager } from './manager';
import { StateGalleryControls } from './ui';


Expand All @@ -15,6 +15,7 @@ export const StateGalleryExtensionFunctions = {
export type StateGalleryCustomState = {
title: BehaviorSubject<string | undefined>,
manager: BehaviorSubject<StateGalleryManager | undefined>,
status: BehaviorSubject<LoadingStatus>,
}
export const StateGalleryCustomState = extensionCustomStateGetter<StateGalleryCustomState>(StateGalleryExtensionName);

Expand Down Expand Up @@ -42,6 +43,7 @@ export const StateGallery = PluginBehavior.create<{ autoAttach: boolean }>({
// });
StateGalleryCustomState(this.ctx).title = new BehaviorSubject<string | undefined>(undefined);
StateGalleryCustomState(this.ctx).manager = new BehaviorSubject<StateGalleryManager | undefined>(undefined);
StateGalleryCustomState(this.ctx).status = new BehaviorSubject<LoadingStatus>('ready');
this.ctx.customStructureControls.set(StateGalleryExtensionName, StateGalleryControls as any);
// this.ctx.builders.structure.representation.registerPreset(AssemblySymmetryPreset);
}
Expand Down
18 changes: 14 additions & 4 deletions src/app/extensions/state-gallery/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ export interface Image {
}


export type LoadingStatus = 'ready' | 'loading' | 'error'


export class StateGalleryManager {
public readonly images: Image[];
/** Maps filename to its index within `this.images` */
Expand All @@ -83,7 +86,7 @@ export class StateGalleryManager {
/** Image that has been successfully loaded most recently. Undefined if another state has been requested since. */
loadedImage: new BehaviorSubject<Image | undefined>(undefined),
/** Loading status. */
status: new BehaviorSubject<'ready' | 'loading' | 'error'>('ready'),
status: new BehaviorSubject<LoadingStatus>('ready'),
};
/** True if at least one image has been loaded (this is to skip animation on the first load) */
private firstLoaded = false;
Expand All @@ -97,6 +100,16 @@ export class StateGalleryManager {
const allImages = listImages(data, true);
this.images = removeWithSuffixes(allImages, ['_side', '_top']); // removing images in different orientation than 'front'
this.filenameIndex = createIndex(this.images.map(img => img.filename));
this.events.status.subscribe(status => {
const customState = StateGalleryCustomState(this.plugin);
customState.status?.next(status);
if (customState.manager?.value !== this) customState.manager?.next(this);
});
this.events.requestedImage.subscribe(img => {
const customState = StateGalleryCustomState(this.plugin);
customState.title?.next(img?.simple_title ?? img?.filename);
if (customState.manager?.value !== this) customState.manager?.next(this);
});
}

static async create(plugin: PluginContext, entryId: string, options?: Partial<StateGalleryConfigValues>) {
Expand Down Expand Up @@ -139,9 +152,6 @@ export class StateGalleryManager {
this.events.requestedImage.next(img);
this.events.loadedImage.next(undefined);
this.events.status.next('loading');
const customState = StateGalleryCustomState(this.plugin);
customState.title?.next(img.simple_title ?? img.filename);
if (customState.manager?.value !== this) customState.manager?.next(this);
let result;
try {
result = await this.loader.requestRun(img.filename);
Expand Down
32 changes: 25 additions & 7 deletions src/app/extensions/state-gallery/ui.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { CollapsableControls, CollapsableState, PluginReactContext } from 'molstar/lib/mol-plugin-ui/base';
import { Button, ExpandGroup } from 'molstar/lib/mol-plugin-ui/controls/common';
import { CheckSvg, ErrorSvg } from 'molstar/lib/mol-plugin-ui/controls/icons';
import { CheckSvg, ErrorSvg, Icon } from 'molstar/lib/mol-plugin-ui/controls/icons';
import { ParameterControls } from 'molstar/lib/mol-plugin-ui/controls/parameters';
import { ParamDefinition as PD } from 'molstar/lib/mol-util/param-definition';
import React from 'react';
import React, { useRef } from 'react';
import { groupElements } from '../../helpers';
import { ChevronLeftSvg, ChevronRightSvg, CollectionsOutlinedSvg, EmptyIconSvg, HourglassBottomSvg } from '../../ui/icons';
import { StateGalleryCustomState } from './behavior';
import { Image, StateGalleryManager } from './manager';
import { Image, LoadingStatus, StateGalleryManager } from './manager';


interface StateGalleryControlsState {
Expand Down Expand Up @@ -109,7 +109,7 @@ function ManagerControls(props: { manager: StateGalleryManager }) {
const nImages = images.length;
const categories = React.useMemo(() => groupElements(images, img => img.category ?? 'Miscellaneous'), [images]);
const [selected, setSelected] = React.useState<Image | undefined>(undefined);
const [status, setStatus] = React.useState<'ready' | 'loading' | 'error'>('ready');
const [status, setStatus] = React.useState<LoadingStatus>('ready');

React.useEffect(() => {
if (images.length > 0) {
Expand Down Expand Up @@ -160,7 +160,7 @@ function ManagerControls(props: { manager: StateGalleryManager }) {
}


function StateButton(props: { img: Image, isSelected: boolean, status: 'ready' | 'loading' | 'error', onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void }) {
function StateButton(props: { img: Image, isSelected: boolean, status: LoadingStatus, onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void }) {
const { img, isSelected, status, onClick } = props;
const icon = !isSelected ? EmptyIconSvg : (status === 'loading') ? HourglassBottomSvg : (status === 'error') ? ErrorSvg : CheckSvg;
const title = img.simple_title ?? img.filename;
Expand All @@ -179,15 +179,27 @@ export function StateGalleryTitleBox() {
const plugin = React.useContext(PluginReactContext);
const [title, setTitle] = React.useState<string | undefined>(undefined);
const [manager, setManager] = React.useState<StateGalleryManager | undefined>(undefined);
const [status, setStatus] = React.useState<LoadingStatus>('ready');
const loadingCounter = useRef<number>(0);
React.useEffect(() => {
const customState = StateGalleryCustomState(plugin);
const subs = [
customState.title?.subscribe(x => setTitle(x)),
customState.manager?.subscribe(x => setManager(x)),
customState.status?.subscribe(status => {
const counter = ++loadingCounter.current;
if (status === 'loading') {
setTimeout(() => { if (loadingCounter.current === counter) setStatus('loading'); }, 250);
} else {
setStatus(status);
}
}),
];
return () => subs.forEach(sub => sub?.unsubscribe());
}, [plugin]);

const IconWidth = '1.2em'; // width of msp-material-icon

if (title === undefined) return null;

return <div style={{ backgroundColor: '#99999930', position: 'absolute', top: 42, width: 400 }}>
Expand All @@ -199,8 +211,14 @@ export function StateGalleryTitleBox() {
onClick={() => manager.loadPrevious()} />
</div>
}
<div style={{ padding: 8, textAlign: 'center', fontWeight: 'bold' }}>
{title}
<div style={{ padding: 8, textAlign: 'center', fontWeight: 'bold', display: 'flex', flexDirection: 'row' }}
title={status === 'error' ? `${title} (failed to load)` : status === 'loading' ? `${title} (loading)` : title} >
<div style={{ width: IconWidth }}>
<Icon svg={status === 'error' ? ErrorSvg : status === 'loading' ? HourglassBottomSvg : EmptyIconSvg} />
</div>
<div style={{ marginRight: IconWidth, paddingInline: 4 }}>
{title}
</div>
</div>
{manager &&
<div>
Expand Down

0 comments on commit e887bb1

Please sign in to comment.