Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resolution overlay and search support #502

Merged
merged 4 commits into from
Oct 1, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions common/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ export const firefoxExtensionUrl =

export const RECURSIVE_DIR_WATCH_DEPTH = 16;

/** No technical limitation, just for preventing infinite loops due to bugs */
export const MAX_TAG_DEPTH = 32;

export const thumbnailFormat = 'webp';

const isRenderer = process.type === 'renderer';
Expand Down
36 changes: 27 additions & 9 deletions resources/style/content.scss
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,7 @@
height: inherit;
}

// Note: this feature has an ugly exception, when filename is also shown. See the #gallery-item[data-show-filename] rule below
// Note: this feature has an ugly exception, when filename is also shown. See the #gallery-item[data-show-overlay] rule below
&:hover .thumbnail-tags {
max-height: 100%;
overflow-y: overlay; // overlay scrollbar so it doesn't push tags away
Expand Down Expand Up @@ -496,8 +496,11 @@
}
}

.thumbnail-filename {
display: block;
.thumbnail-overlay {
display: flex;
justify-content: space-between;
column-gap: 0.25em;

background-color: rgba(0, 0, 0, 0.5);
color: rgb(
200,
Expand All @@ -510,18 +513,33 @@
bottom: 0;
padding: 0.25rem;

overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;

opacity: 0.75;
transition: opacity 0.25s $pt-transition-cubic2, background-color 0.25s $pt-transition-cubic2;

// Resolution is always visible if enabled by truncating the filename
> :first-child {
white-space: nowrap;
overflow: hidden visible;
text-overflow: ellipsis;

// Only here to avoid vertical scrollbar due to overflow-x: hidden
// It won't take up more than 1 line due to no-wrap
height: 2em;
}
}

&:hover {
.thumbnail-filename {
.thumbnail-overlay {
opacity: 1;
background-color: rgba(0, 0, 0, 0.75);

&:hover {
// Expand filename on hover if it was truncated
> :first-child {
overflow: inherit;
text-overflow: inherit;
}
}
}
}
}
Expand All @@ -535,7 +553,7 @@

// Exception when both showing filename and tags in thumbnail overlay:
// move the tags up by the height of the filename element (23.59px)
#gallery-content[data-show-filename='true'] .masonry {
#gallery-content[data-show-overlay='true'] .masonry {
[data-masonrycell] {
&:hover .thumbnail-tags {
max-height: calc(100% - 1.5em - 0.25rem);
Expand Down
22 changes: 15 additions & 7 deletions src/entities/Tag.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { MAX_TAG_DEPTH } from 'common/config';
import { IReactionDisposer, observable, reaction, computed, action, makeObservable } from 'mobx';

import TagStore from 'src/frontend/stores/TagStore';
Expand Down Expand Up @@ -71,24 +72,31 @@ export class ClientTag {

/** Returns this tag and all of its sub-tags ordered depth-first */
@action getSubTree(): Generator<ClientTag> {
function* tree(tag: ClientTag): Generator<ClientTag> {
function* tree(tag: ClientTag, iteration: number): Generator<ClientTag> {
RvanderLaan marked this conversation as resolved.
Show resolved Hide resolved
if (iteration > MAX_TAG_DEPTH) {
console.error('Subtree has too many tags. Is there a cycle in the tag tree?', tag);
return;
}

yield tag;
for (const subTag of tag.subTags) {
yield* tree(subTag);
yield* tree(subTag, iteration + 1);
}
}
return tree(this);
return tree(this, 0);
}

/** Returns this tag and all its ancestors (excluding root tag). */
@action getAncestors(): Generator<ClientTag> {
function* ancestors(tag: ClientTag): Generator<ClientTag> {
if (tag.id !== ROOT_TAG_ID) {
function* ancestors(tag: ClientTag, iteration: number): Generator<ClientTag> {
if (iteration > MAX_TAG_DEPTH) {
console.error('Tag has too many ancestors. Is there a cycle in the tag tree?', tag);
} else if (tag.id !== ROOT_TAG_ID) {
yield tag;
yield* ancestors(tag.parent);
yield* ancestors(tag.parent, iteration + 1);
}
}
return ancestors(this);
return ancestors(this, 0);
}

/** Returns the tags up the hierarchy from this tag, excluding the root tag */
Expand Down
14 changes: 10 additions & 4 deletions src/frontend/containers/AdvancedSearch/Inputs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ export const KeySelector = forwardRef(function KeySelector(
<option key="size" value="size">
File Size (MB)
</option>
<option key="width" value="width">
Width
</option>
<option key="height" value="height">
Height
</option>
<option key="dateAdded" value="dateAdded">
Date Added
</option>
Expand Down Expand Up @@ -100,8 +106,8 @@ export const ValueInput = ({ labelledby, keyValue, value, dispatch }: FieldInput
return <TagInput labelledby={labelledby} value={value as TagValue} dispatch={dispatch} />;
} else if (keyValue === 'extension') {
return <ExtensionInput labelledby={labelledby} value={value as string} dispatch={dispatch} />;
} else if (keyValue === 'size') {
return <SizeInput labelledby={labelledby} value={value as number} dispatch={dispatch} />;
} else if (['size', 'width', 'height'].includes(keyValue)) {
return <NumberInput labelledby={labelledby} value={value as number} dispatch={dispatch} />;
} else if (keyValue === 'dateAdded') {
return <DateAddedInput labelledby={labelledby} value={value as Date} dispatch={dispatch} />;
}
Expand Down Expand Up @@ -250,7 +256,7 @@ const ExtensionInput = ({ labelledby, value, dispatch }: ValueInput<string>) =>
</select>
);

const SizeInput = ({ value, labelledby, dispatch }: ValueInput<number>) => {
const NumberInput = ({ value, labelledby, dispatch }: ValueInput<number>) => {
return (
<input
aria-labelledby={labelledby}
Expand Down Expand Up @@ -286,7 +292,7 @@ const DateAddedInput = ({ value, labelledby, dispatch }: ValueInput<Date>) => {
};

function getOperatorOptions(key: Key) {
if (key === 'dateAdded' || key === 'size') {
if (['dateAdded', 'size', 'width', 'height'].includes(key)) {
return NumberOperators.map((op) => toOperatorOption(op, NumberOperatorSymbols));
} else if (key === 'extension') {
return BinaryOperators.map((op) => toOperatorOption(op));
Expand Down
17 changes: 13 additions & 4 deletions src/frontend/containers/AdvancedSearch/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export type Criteria =
| Field<'tags', TagOperatorType, TagValue>
| Field<'extension', BinaryOperatorType, string>
| Field<'size', NumberOperatorType, number>
| Field<'width' | 'height', NumberOperatorType, number>
| Field<'dateAdded', NumberOperatorType, Date>;

interface Field<K extends Key, O extends Operator, V extends Value> {
Expand All @@ -31,7 +32,7 @@ interface Field<K extends Key, O extends Operator, V extends Value> {

export type Key = keyof Pick<
FileDTO,
'name' | 'absolutePath' | 'tags' | 'extension' | 'size' | 'dateAdded'
'name' | 'absolutePath' | 'tags' | 'extension' | 'size' | 'width' | 'height' | 'dateAdded'
>;
export type Operator = OperatorType;
export type Value = string | number | Date | TagValue;
Expand All @@ -48,21 +49,22 @@ export function defaultQuery(key: Key): Criteria {
operator: 'equals',
value: IMG_EXTENSIONS[0],
};
} else if (key === 'size') {
return { key, operator: 'greaterThanOrEquals', value: 0 };
} else {
} else if (key === 'dateAdded') {
return {
key,
operator: 'equals',
value: new Date(),
};
} else {
return { key, operator: 'greaterThanOrEquals', value: 0 };
}
}

const BYTES_IN_MB = 1024 * 1024;

export function fromCriteria(criteria: ClientFileSearchCriteria): [ID, Criteria] {
const query = defaultQuery('tags');
// Preserve the value when the criteria has the same type of value
if (
criteria instanceof ClientStringSearchCriteria &&
(criteria.key === 'name' || criteria.key === 'absolutePath' || criteria.key === 'extension')
Expand All @@ -75,6 +77,11 @@ export function fromCriteria(criteria: ClientFileSearchCriteria): [ID, Criteria]
} else if (criteria instanceof ClientTagSearchCriteria && criteria.key === 'tags') {
const id = criteria.value;
query.value = id;
} else if (
criteria instanceof ClientNumberSearchCriteria &&
(criteria.key === 'width' || criteria.key === 'height')
) {
query.value = criteria.value;
} else {
return [generateCriteriaId(), query];
}
Expand All @@ -90,6 +97,8 @@ export function intoCriteria(query: Criteria, tagStore: TagStore): ClientFileSea
return new ClientDateSearchCriteria(query.key, query.value, query.operator);
} else if (query.key === 'size') {
return new ClientNumberSearchCriteria(query.key, query.value * BYTES_IN_MB, query.operator);
} else if (query.key === 'width' || query.key === 'height') {
return new ClientNumberSearchCriteria(query.key, query.value, query.operator);
} else if (query.key === 'tags') {
const tag = query.value !== undefined ? tagStore.get(query.value) : undefined;
return new ClientTagSearchCriteria('tags', tag?.id, query.operator);
Expand Down
33 changes: 29 additions & 4 deletions src/frontend/containers/ContentView/GalleryItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,14 @@ export const MasonryCell = observer(
/>
)}

{uiStore.isThumbnailFilenameOverlayEnabled && <ThumbnailFilename file={file} />}
{(uiStore.isThumbnailFilenameOverlayEnabled ||
uiStore.isThumbnailResolutionOverlayEnabled) && (
<ThumbnailOverlay
file={file}
showFilename={uiStore.isThumbnailFilenameOverlayEnabled}
showResolution={uiStore.isThumbnailResolutionOverlayEnabled}
/>
)}

{/* Show tags when the option is enabled, or when the file is selected */}
{(uiStore.isThumbnailTagOverlayEnabled || uiStore.fileSelection.has(file)) &&
Expand Down Expand Up @@ -204,14 +211,32 @@ const TagWithHint = observer(
},
);

const ThumbnailFilename = ({ file }: { file: ClientFile }) => {
const ThumbnailOverlay = ({
file,
showFilename,
showResolution,
}: {
file: ClientFile;
showFilename: boolean;
showResolution: boolean;
}) => {
const title = `${ellipsize(file.absolutePath, 80, 'middle')}, ${file.width}x${
file.height
}, ${humanFileSize(file.size)}`;

return (
<div className="thumbnail-filename" data-tooltip={title}>
{file.name}
<div className="thumbnail-overlay" data-tooltip={title}>
{showFilename && (
<div className="thumbnail-filename" data-tooltip={title}>
{file.name}
</div>
)}

{showResolution && (
<div className="thumbnail-resolution" data-tooltip={title}>
{file.width}⨯{file.height}
</div>
)}
</div>
);
};
4 changes: 3 additions & 1 deletion src/frontend/containers/ContentView/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,9 @@ const Content = observer(() => {
id="gallery-content"
className={isMaximized ? '' : 'unmaximized'}
tabIndex={-1}
data-show-filename={uiStore.isThumbnailFilenameOverlayEnabled}
data-show-overlay={
uiStore.isThumbnailFilenameOverlayEnabled || uiStore.isThumbnailResolutionOverlayEnabled
}
data-selected-file-dropping={isDroppingTagOnSelection}
onContextMenu={handleContextMenu}
// Clear selection when clicking on the background, unless in slide mode: always needs an active image
Expand Down
Loading