Add image selection
This commit is contained in:
parent
cfdc78ab4c
commit
213d2b1559
@ -16,7 +16,7 @@
|
||||
"domvm": "~3.2.1",
|
||||
"exif-parser": "~0.1.9",
|
||||
"extract-text-webpack-plugin": "^3.0.2",
|
||||
"frptools": "2.1.0",
|
||||
"frptools": "2.2.0",
|
||||
"pica": "~2.0.8",
|
||||
"pouchdb-adapter-http": "~6.3.4",
|
||||
"pouchdb-adapter-idb": "~6.3.4",
|
||||
|
||||
@ -1,9 +1,15 @@
|
||||
import { prop, computed } from 'frptools';
|
||||
import { prop, computed, container } from 'frptools';
|
||||
|
||||
import { subscribeToRender, defineView, defineElement as el } from '../utils/domvm.js';
|
||||
import {
|
||||
subscribeToRender,
|
||||
defineView,
|
||||
subscribeToRender,
|
||||
defineElement as el
|
||||
} from '../utils/domvm.js';
|
||||
|
||||
import { error } from '../services/console.js';
|
||||
import { ImageType } from '../data/image.js';
|
||||
import { pouchDocArrayHash, pouchDocHash } from '../utils/conversion.js';
|
||||
import { pouchDocArrayHash, pouchDocHash, hashSet, extractID } from '../utils/conversion.js';
|
||||
import { AlbumTemplate } from './components/albumTemplate.js';
|
||||
import { injectStyle, styled } from '../services/style.js';
|
||||
|
||||
@ -17,7 +23,11 @@ export function uploadImages(evt, files) {
|
||||
|
||||
export function AllImagesView(vm, params, key, opts) {
|
||||
const model = prop({}, pouchDocHash);
|
||||
const images = prop([], pouchDocArrayHash);
|
||||
const images = container([], pouchDocArrayHash);
|
||||
const visibleIds = computed(arr => arr.map(extractID), [images]);
|
||||
const hoverId = prop(null);
|
||||
const selectedIds = container(new Set(), hashSet);
|
||||
const mode = computed(sIds => (sIds.size > 0 ? 'select' : 'view'), [selectedIds]);
|
||||
|
||||
ImageType.find(
|
||||
{
|
||||
@ -26,7 +36,7 @@ export function AllImagesView(vm, params, key, opts) {
|
||||
true
|
||||
).then(la => {
|
||||
opts.appbar.renderButtons(renderAppBarButtons);
|
||||
subscribeToRender(vm, [images], [la.subscribe(images)]);
|
||||
subscribeToRender(vm, [images], [la.subscribe(res => images.splice(0, images.length, ...res))]);
|
||||
});
|
||||
|
||||
function renderAppBarButtons() {
|
||||
@ -43,27 +53,74 @@ export function AllImagesView(vm, params, key, opts) {
|
||||
})
|
||||
];
|
||||
}
|
||||
//
|
||||
// function deleteImage(i) {
|
||||
// ImageType.delete(i._id);
|
||||
// }
|
||||
//
|
||||
// function addAlbum() {
|
||||
// const albumName = prompt("Album Name");
|
||||
// if (albumName && albumName.trim()) {
|
||||
// const a = new AlbumType({
|
||||
// title: albumName.trim(),
|
||||
// count: 0
|
||||
// });
|
||||
// a.save();
|
||||
// }
|
||||
// }
|
||||
|
||||
function deleteImage(i) {
|
||||
ImageType.delete(i._id);
|
||||
function nodeParentWithType(node, type) {
|
||||
let parentNode = node;
|
||||
while (parentNode && (!parentNode.data || parentNode.data.type !== type)) {
|
||||
parentNode = parentNode.parent;
|
||||
}
|
||||
if (!parentNode) {
|
||||
error(`Could not find {"type": "${type}"} parent.`);
|
||||
return;
|
||||
}
|
||||
return parentNode;
|
||||
}
|
||||
|
||||
function addAlbum() {
|
||||
const albumName = prompt('Album Name');
|
||||
if (albumName && albumName.trim()) {
|
||||
const a = new AlbumType({
|
||||
title: albumName.trim(),
|
||||
count: 0
|
||||
});
|
||||
a.save();
|
||||
function toggleSelect(evt, node, vm) {
|
||||
const imageNode = nodeParentWithType(node, 'image');
|
||||
const id = imageNode.data._id;
|
||||
if (selectedIds.has(id)) {
|
||||
selectedIds.delete(id);
|
||||
} else {
|
||||
selectedIds.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAll(evt, node, vm) {
|
||||
if (images.length === selectedIds.size) {
|
||||
selectedIds.clear();
|
||||
} else {
|
||||
images.map(extractID).forEach(i => selectedIds.add(i));
|
||||
}
|
||||
}
|
||||
|
||||
subscribeToRender(vm, [selectedIds, images, hoverId, mode]);
|
||||
|
||||
return function() {
|
||||
return AlbumTemplate({
|
||||
return el(
|
||||
'.eventSnarfer',
|
||||
{
|
||||
onclick: {
|
||||
'.photoSelect .icon svg path': toggleSelect,
|
||||
'.photoSelect .icon': toggleSelect,
|
||||
'.albumSelectButton .icon': toggleAll,
|
||||
'.albumSelectButton .icon svg path': toggleAll
|
||||
}
|
||||
},
|
||||
[
|
||||
AlbumTemplate({
|
||||
title: 'Test',
|
||||
id: 1,
|
||||
photos: images()
|
||||
});
|
||||
photos: images,
|
||||
selectedIds,
|
||||
mode: mode()
|
||||
})
|
||||
]
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,66 +1,77 @@
|
||||
import { prop } from 'frptools';
|
||||
|
||||
import {
|
||||
defineView as vw,
|
||||
defineElement as el,
|
||||
patchRefStyleMap,
|
||||
patchNodeStyle
|
||||
patchNodeStyle,
|
||||
subscribeToRender
|
||||
} from '../../utils/domvm.js';
|
||||
import { injectStyle, styled } from '../../services/style.js';
|
||||
|
||||
import { DEFAULT_TRANSITION, CSS_FULL_SIZE, IMAGE_MARGIN } from '../styles.js';
|
||||
import { Icon } from './icon.js';
|
||||
import { AttachmentImageView } from './attachmentImage.js';
|
||||
|
||||
export function AlbumPhotoTemplate(doc, isSelected, selectMode) {
|
||||
const _imageHover = false;
|
||||
const dim = 'opacity: 0.7;';
|
||||
const off = 'opacity: 0;';
|
||||
const full = 'opacity: 1;';
|
||||
|
||||
export function AlbumPhotoTemplate(vm, { doc }) {
|
||||
const photoSelectButtonRef = `pSB${doc._id}`;
|
||||
const photoBackgroundRef = `pBkd${doc._id}`;
|
||||
const hover = prop(false);
|
||||
const hoverSelectButton = prop(false);
|
||||
|
||||
subscribeToRender(vm, [hover, hoverSelectButton]);
|
||||
|
||||
return function render(vm, { isSelected, selectMode }) {
|
||||
return photoContainer(
|
||||
{
|
||||
onmouseenter: [
|
||||
patchRefStyleMap,
|
||||
{ [photoSelectButtonRef]: 'opacity: 0.7;', [photoBackgroundRef]: 'opacity: 0.7;' }
|
||||
],
|
||||
onmouseleave: [
|
||||
patchRefStyleMap,
|
||||
{ [photoSelectButtonRef]: 'opacity: 0;', [photoBackgroundRef]: 'opacity: 0;' }
|
||||
]
|
||||
class: 'photoContainer',
|
||||
onmouseenter: [hover, true],
|
||||
onmouseleave: [hover, false],
|
||||
css: {
|
||||
cursor: selectMode ? 'pointer' : 'zoom-in'
|
||||
}
|
||||
},
|
||||
[
|
||||
vw(AttachmentImageView, doc, doc._hash()),
|
||||
AttachmentImageView(doc, {
|
||||
css: {
|
||||
transform: isSelected ? 'translateZ(-50px)' : null
|
||||
}
|
||||
}),
|
||||
photoSelectButton(
|
||||
{
|
||||
_ref: photoSelectButtonRef,
|
||||
_data: doc,
|
||||
class: 'photoSelect',
|
||||
css: {
|
||||
// backgroundColor: isSelected ? 'white' : 'transparent',
|
||||
// opacity: isSelected ? 1 : selectMode || _imageHover ? 0.7 : 0,
|
||||
backgroundColor: isSelected ? 'white' : 'transparent',
|
||||
opacity: isSelected || hoverSelectButton() ? 1 : selectMode || hover() ? 0.7 : 0
|
||||
},
|
||||
onmouseenter: [patchNodeStyle, 'opacity: 1;'],
|
||||
onmouseleave: [patchNodeStyle, 'opacity: 0.7;']
|
||||
onmouseenter: [hoverSelectButton, true],
|
||||
onmouseleave: [hoverSelectButton, false]
|
||||
},
|
||||
[
|
||||
Icon({
|
||||
name: 'check_circle',
|
||||
name: selectMode && !isSelected ? 'circle_o' : 'check_circle',
|
||||
size: 0.75,
|
||||
fill: '#fff' // isSelected ? '#00C800' : '#fff'
|
||||
fill: isSelected ? '#00C800' : '#fff'
|
||||
})
|
||||
]
|
||||
),
|
||||
photoBackdrop({
|
||||
_ref: photoBackgroundRef,
|
||||
css: {
|
||||
// transform: isSelected ? 'translateZ(-50px)' : null,
|
||||
// opacity: selectMode || _imageHover ? 0.7 : 0,
|
||||
transform: isSelected ? 'translateZ(-50px)' : null,
|
||||
opacity: selectMode || hover() ? 0.7 : 0
|
||||
}
|
||||
})
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
const IMAGE_MARGIN = 2;
|
||||
|
||||
const CSS_FULL_SIZE = {
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
};
|
||||
}
|
||||
|
||||
const photoContainer = styled({
|
||||
position: 'relative',
|
||||
@ -70,21 +81,18 @@ const photoContainer = styled({
|
||||
cursor: 'zoom-in'
|
||||
});
|
||||
|
||||
const image = styled('img', CSS_FULL_SIZE, {
|
||||
const image = styled('img', CSS_FULL_SIZE, DEFAULT_TRANSITION, {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
transition: 'transform .135s cubic-bezier(0.0,0.0,0.2,1)'
|
||||
left: 0
|
||||
});
|
||||
|
||||
const photoSelectButton = styled({
|
||||
const photoSelectButton = styled(DEFAULT_TRANSITION, {
|
||||
position: 'absolute',
|
||||
top: '4%',
|
||||
left: '4%',
|
||||
zIndex: 2,
|
||||
display: 'flex',
|
||||
transition:
|
||||
'transform .135s cubic-bezier(0.0,0.0,0.2,1), opacity .135s cubic-bezier(0.0,0.0,0.2,1)',
|
||||
borderRadius: '50%',
|
||||
padding: '2px',
|
||||
backgroundColor: 'transparent',
|
||||
@ -92,11 +100,10 @@ const photoSelectButton = styled({
|
||||
cursor: 'pointer'
|
||||
});
|
||||
|
||||
const photoBackdrop = styled(CSS_FULL_SIZE, {
|
||||
const photoBackdrop = styled(CSS_FULL_SIZE, DEFAULT_TRANSITION, {
|
||||
position: 'absolute', // Unnecessary but helps with a rendering bug in Chrome. https://gitlab.com/explorigin/gallery/issues/1
|
||||
top: '0px',
|
||||
left: '0px',
|
||||
zIndex: 1,
|
||||
transition:
|
||||
'transform .135s cubic-bezier(0.0,0.0,0.2,1), opacity .135s cubic-bezier(0.0,0.0,0.2,1)',
|
||||
backgroundImage: 'linear-gradient(to bottom,rgba(0,0,0,0.26),transparent 56px,transparent)'
|
||||
});
|
||||
|
||||
@ -5,13 +5,26 @@ import {
|
||||
patchNodeStyle
|
||||
} from '../../utils/domvm.js';
|
||||
import { injectStyle, styled } from '../../services/style.js';
|
||||
|
||||
import { DEFAULT_TRANSITION } from '../styles.js';
|
||||
import { Icon } from './icon.js';
|
||||
import { AlbumPhotoTemplate } from './albumPhotoTemplate.js';
|
||||
|
||||
export function AlbumTemplate(params) {
|
||||
const { id, title, photos } = params;
|
||||
const { id, title, photos, selectedIds, mode } = params;
|
||||
const albumSelectButtonRef = `albSel${id}`;
|
||||
const selectMode = mode === 'select';
|
||||
|
||||
function photoMap(doc) {
|
||||
return vw(
|
||||
AlbumPhotoTemplate,
|
||||
{
|
||||
doc,
|
||||
isSelected: selectedIds.has(doc._id),
|
||||
selectMode
|
||||
},
|
||||
doc._hash()
|
||||
);
|
||||
}
|
||||
|
||||
return Album(
|
||||
{
|
||||
@ -25,12 +38,16 @@ export function AlbumTemplate(params) {
|
||||
{
|
||||
_ref: albumSelectButtonRef,
|
||||
onmouseenter: [patchNodeStyle, 'opacity: 1;'],
|
||||
onmouseleave: [patchNodeStyle, 'opacity: 0.7;']
|
||||
onmouseleave: [patchNodeStyle, 'opacity: 0.7;'],
|
||||
css: {
|
||||
opacity: selectMode ? 0.7 : 0
|
||||
},
|
||||
class: 'albumSelectButton'
|
||||
},
|
||||
[Icon({ name: 'check_circle', size: 0.25 })]
|
||||
)
|
||||
]),
|
||||
albumContent(photos.map(AlbumPhotoTemplate))
|
||||
albumContent(photos.map(photoMap))
|
||||
]
|
||||
);
|
||||
}
|
||||
@ -50,10 +67,8 @@ const albumContent = styled({
|
||||
userSelect: 'none'
|
||||
});
|
||||
|
||||
const albumSelectButton = styled({
|
||||
const albumSelectButton = styled(DEFAULT_TRANSITION, {
|
||||
paddingLeft: '0.5em',
|
||||
cursor: 'pointer',
|
||||
opacity: 0, // TODO onhover 0.7
|
||||
transition:
|
||||
'transform 0.135s cubic-bezier(0, 0, 0.2, 1), opacity 0.135s cubic-bezier(0, 0, 0.2, 1)'
|
||||
opacity: 0
|
||||
});
|
||||
|
||||
@ -4,55 +4,58 @@ import { defineElement as el } from '../../utils/domvm.js';
|
||||
import { ImageType } from '../../data/image.js';
|
||||
import { FileType } from '../../data/file.js';
|
||||
import { pouchDocHash } from '../../utils/conversion.js';
|
||||
import { styled } from '../../services/style.js';
|
||||
import { DEFAULT_TRANSITION } from '../styles.js';
|
||||
|
||||
export function AttachmentImageView(vm, params) {
|
||||
const model = prop(params, pouchDocHash);
|
||||
const id = computed(pouchDocHash, [model]);
|
||||
const sizes = computed(d => d.sizes, [model]); // always update
|
||||
const srcMap = new Map();
|
||||
|
||||
const blobURL = prop('');
|
||||
const imageURL = computed((sizes, bURL) => bURL || sizes.thumbnail || sizes.full, [
|
||||
sizes,
|
||||
blobURL
|
||||
]);
|
||||
|
||||
model.subscribe(() => {
|
||||
if (blobURL()) {
|
||||
URL.revokeObjectURL(blobURL());
|
||||
blobURL('');
|
||||
}
|
||||
});
|
||||
|
||||
async function loadImageFromBlob() {
|
||||
const options = ['thumbnail', 'preview', 'full'].filter(o => sizes().hasOwnProperty(o));
|
||||
async function loadImageFromBlob(doc, evt, node, vm) {
|
||||
const { sizes, _id } = doc;
|
||||
const options = ['thumbnail', 'preview', 'full'].filter(o => sizes.hasOwnProperty(o));
|
||||
|
||||
for (let attempt of options) {
|
||||
try {
|
||||
const data = await FileType.getFromURL(sizes()[attempt]);
|
||||
|
||||
if (blobURL()) {
|
||||
URL.revokeObjectURL(blobURL());
|
||||
const data = await FileType.getFromURL(sizes[attempt]);
|
||||
let src = evt.target.src;
|
||||
if (src.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(src);
|
||||
}
|
||||
blobURL(URL.createObjectURL(data));
|
||||
return;
|
||||
src = URL.createObjectURL(data);
|
||||
node.patch({ src });
|
||||
srcMap.set(_id, src);
|
||||
// node.data = attempt;
|
||||
break;
|
||||
} catch (err) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
URL.revokeObjectURL(blobURL());
|
||||
function cleanup(id, evt) {
|
||||
const { src } = evt.target;
|
||||
if (src.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(s);
|
||||
srcMap.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
return function render() {
|
||||
return el('img', {
|
||||
src: imageURL,
|
||||
onerror: loadImageFromBlob,
|
||||
_key: id(),
|
||||
export function AttachmentImageView(doc, props) {
|
||||
const { sizes, _id } = doc;
|
||||
const src = srcMap.get(_id) || sizes.thumbnail || sizes.preview || sizes.full;
|
||||
|
||||
return image(
|
||||
Object.assign(
|
||||
{
|
||||
src,
|
||||
onerror: [loadImageFromBlob, doc],
|
||||
_key: _id,
|
||||
_hooks: {
|
||||
didRemove: cleanup
|
||||
didRemove: [cleanup, _id]
|
||||
}
|
||||
});
|
||||
};
|
||||
},
|
||||
props || {}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const image = styled('img', DEFAULT_TRANSITION);
|
||||
|
||||
@ -8,7 +8,7 @@ export function ThumbnailTemplate(doc, remove, key) {
|
||||
{
|
||||
onclick: { img: [remove, doc] }
|
||||
},
|
||||
[vw(AttachmentImageView, doc, key)]
|
||||
[AttachmentImageView(doc)]
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
11
packages/gallery/src/interface/styles.js
Normal file
11
packages/gallery/src/interface/styles.js
Normal file
@ -0,0 +1,11 @@
|
||||
export const IMAGE_MARGIN = 2;
|
||||
|
||||
export const CSS_FULL_SIZE = {
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
};
|
||||
|
||||
export const DEFAULT_TRANSITION = {
|
||||
transition:
|
||||
'transform 0.135s cubic-bezier(0, 0, 0.2, 1), opacity 0.135s cubic-bezier(0, 0, 0.2, 1)'
|
||||
};
|
||||
@ -16,6 +16,6 @@ export function isObject(obj) {
|
||||
return typeof obj === 'object' && !Array.isArray(obj);
|
||||
}
|
||||
|
||||
export function isString(str) {
|
||||
export function isString(obj) {
|
||||
return typeof obj === 'string';
|
||||
}
|
||||
|
||||
@ -49,3 +49,13 @@ export const pick = id => doc => doc[id];
|
||||
|
||||
export const extractID = pick('_id');
|
||||
export const extractREV = pick('_rev');
|
||||
|
||||
export function hashSet(_a) {
|
||||
if (_a instanceof Set) {
|
||||
return Array.from(_a.keys())
|
||||
.sort()
|
||||
.map(k => `${(typeof k).substr(0, 1)}:${encodeURIComponent(k)}/`)
|
||||
.join('?');
|
||||
}
|
||||
return _a;
|
||||
}
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
// export * from 'domvm/dist/dev/domvm.dev.js';
|
||||
export * from 'domvm/dist/mini/domvm.mini.js';
|
||||
import { deepAssign } from './conversion.js';
|
||||
|
||||
export function subscribeToRender(vm, subscribables, subscriptions) {
|
||||
const redraw = () => vm.redraw();
|
||||
const redraw = (...args) => vm.redraw();
|
||||
const subList = subscribables.map(s => s.subscribe(redraw)).concat(subscriptions);
|
||||
|
||||
vm.config({ hooks: { willUnmount: () => subList.forEach(s => s()) } });
|
||||
@ -12,10 +13,16 @@ export function patchRefStyle(ref, style, evt, node, vm) {
|
||||
vm.refs[ref].patch({ style });
|
||||
}
|
||||
|
||||
export function patchRefStyleMap(refStylemap, ...args) {
|
||||
Object.entries(refStylemap).forEach(([r, s]) => patchRefStyle(r, s, ...args));
|
||||
export function patchRefStyleMap(refStyleMap, ...args) {
|
||||
Object.entries(refStyleMap).forEach(([r, s]) => patchRefStyle(r, s, ...args));
|
||||
}
|
||||
|
||||
export function patchNodeStyle(style, evt, node) {
|
||||
node.patch({ style });
|
||||
}
|
||||
|
||||
export function changeElementStateMap(refStateMap, evt, node, vm) {
|
||||
Object.entries(refStateMap).forEach(([r, state]) => {
|
||||
deepAssign(vm.refs[ref]._data, state);
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user