Interface is fully DB-driven
While there are still events for things like maintaining indexes, those may be unnecessary and may go away.
This commit is contained in:
parent
937713de53
commit
a9a252f8b0
@ -14,6 +14,7 @@
|
||||
"dependencies": {
|
||||
"domvm": "~3.2.0",
|
||||
"exif-parser": "~0.1.9",
|
||||
"frptools": "1.1.0",
|
||||
"pica": "~2.0.8",
|
||||
"pouchdb-adapter-http": "~6.3.4",
|
||||
"pouchdb-adapter-idb": "~6.3.4",
|
||||
|
||||
@ -1,50 +1,22 @@
|
||||
import { createView } from 'domvm/dist/dev/domvm.dev.js';
|
||||
// import { createView } from 'domvm/dist/dev/domvm.dev.js';
|
||||
import { createView } from 'domvm/dist/full/domvm.full.js';
|
||||
|
||||
import * as image from './data/image.js';
|
||||
import * as index from './data/indexType.js';
|
||||
import { getDatabase } from './services/db.js';
|
||||
import * as imageTag from './context/manageImageTags.js';
|
||||
import generateThumbnails from './contextLoaders/generateThumbnails.js';
|
||||
import { GalleryView } from './interface/gallery.js';
|
||||
import { router, routeChanged } from './services/router.js';
|
||||
import { router } from './services/router.js';
|
||||
|
||||
import { getDatabase } from './services/db.js';
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
EventEmitter.defaultMaxListeners = 1000; // https://github.com/pouchdb/pouchdb/issues/6123
|
||||
window.db = getDatabase();
|
||||
|
||||
const NAV_OPTIONS = {
|
||||
images: {
|
||||
model: image,
|
||||
title: 'Images'
|
||||
},
|
||||
albums: {
|
||||
model: index,
|
||||
title: 'Albums'
|
||||
}
|
||||
};
|
||||
|
||||
async function update(route) {
|
||||
const o = NAV_OPTIONS[route.name];
|
||||
gallery.update({
|
||||
title: o.title,
|
||||
members: (await o.model.find({ attachments: true })).rows
|
||||
});
|
||||
}
|
||||
function redraw() {
|
||||
update(router.current());
|
||||
}
|
||||
function onRouteChange(router, route) {
|
||||
update(route);
|
||||
}
|
||||
|
||||
// Watch for new images, generate thumbnails if they need them.
|
||||
image.watcher(generateThumbnails);
|
||||
image.imported.subscribe(redraw);
|
||||
image.removed.subscribe(redraw);
|
||||
index.added.subscribe(redraw);
|
||||
index.removed.subscribe(redraw);
|
||||
routeChanged.subscribe(onRouteChange);
|
||||
|
||||
const gallery = createView(GalleryView, {
|
||||
title: '',
|
||||
members: []
|
||||
}).mount(document.querySelector('#app'));
|
||||
// Attach our root view to the DOM
|
||||
createView(GalleryView, {}).mount(document.querySelector('#app'));
|
||||
|
||||
// Start the router
|
||||
router.start('home');
|
||||
|
||||
@ -8,7 +8,7 @@ import { Watcher } from '../utils/watcher.js';
|
||||
const db = getDatabase();
|
||||
const PROCESS_PREFIX = 'importing';
|
||||
const PREFIX = 'image';
|
||||
const SELECTOR = {
|
||||
export const SELECTOR = {
|
||||
_id: {
|
||||
$gt: `${PREFIX}_`,
|
||||
$lt: `${PREFIX}_\ufff0`
|
||||
@ -45,6 +45,10 @@ export async function find(keys, options = {}) {
|
||||
return await db.allDocs(opts);
|
||||
}
|
||||
|
||||
export async function getAttachment(id, attName) {
|
||||
return await db.getAttachment(id, attName);
|
||||
}
|
||||
|
||||
export async function add(imageFileList) {
|
||||
const docs = Array.prototype.map.call(imageFileList, f => ({
|
||||
_id: `${PROCESS_PREFIX}_${f.name}`,
|
||||
|
||||
@ -4,6 +4,12 @@ import { Event } from '../utils/event.js';
|
||||
|
||||
const db = getDatabase();
|
||||
const PREFIX = 'index';
|
||||
export const SELECTOR = {
|
||||
_id: {
|
||||
$gt: `${PREFIX}_`,
|
||||
$lt: `${PREFIX}_\ufff0`
|
||||
}
|
||||
};
|
||||
|
||||
// Events
|
||||
export const added = new Event('Index.added');
|
||||
|
||||
@ -1,31 +1,75 @@
|
||||
import { defineView, defineElement as el } from 'domvm';
|
||||
import * as image from '../data/image.js';
|
||||
import { ImageView } from './image.js';
|
||||
import { LiveArray } from '../utils/livearray.js';
|
||||
|
||||
// Warn if overriding existing method
|
||||
if (Array.prototype.equals)
|
||||
console.warn(
|
||||
"Overriding existing Array.prototype.equals. Possible causes: New API defines the method, there's a framework conflict or you've got double inclusions in your code."
|
||||
);
|
||||
// attach the .equals method to Array's prototype to call it on any array
|
||||
Array.prototype.equals = function(array) {
|
||||
// if the other array is a falsy value, return
|
||||
if (!array) return false;
|
||||
|
||||
// compare lengths - can save a lot of time
|
||||
if (this.length != array.length) return false;
|
||||
|
||||
for (var i = 0, l = this.length; i < l; i++) {
|
||||
// Check if we have nested arrays
|
||||
if (this[i] instanceof Array && array[i] instanceof Array) {
|
||||
// recurse into the nested arrays
|
||||
if (!this[i].equals(array[i])) return false;
|
||||
} else if (this[i] != array[i]) {
|
||||
// Warning - two different object instances will never be equal: {x:20} != {x:20}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
// Hide method from for-in loops
|
||||
Object.defineProperty(Array.prototype, 'equals', { enumerable: false });
|
||||
|
||||
export function AlbumView(vm, model) {
|
||||
const { albumRow, remove } = model;
|
||||
const { props, members } = albumRow.doc;
|
||||
const title = props.title;
|
||||
let images = [];
|
||||
|
||||
// FIXME - If the album is updated, this does not properly refresh.
|
||||
image.find(members, { attachments: true }).then(res => {
|
||||
images = res.rows.filter(i => i.doc);
|
||||
vm.redraw();
|
||||
});
|
||||
const { remove } = model;
|
||||
let data = null;
|
||||
let currentMembers = [];
|
||||
let title = null;
|
||||
|
||||
function removeImageFromAlbum(id, rev) {
|
||||
remove(title, id);
|
||||
}
|
||||
|
||||
return function(vm, model, key, opts) {
|
||||
const { doc, remove } = model;
|
||||
const { props, members } = doc;
|
||||
|
||||
if (title !== props.title || currentMembers.length !== members.length) {
|
||||
if (data) {
|
||||
data.cleanup();
|
||||
}
|
||||
title = props.title;
|
||||
currentMembers = members;
|
||||
const SELECTOR = {
|
||||
$or: [
|
||||
Object.assign({ [`tags.${title}`]: { $eq: true } }, image.SELECTOR),
|
||||
{ _id: { $in: members } }
|
||||
]
|
||||
};
|
||||
|
||||
data = LiveArray(db, SELECTOR);
|
||||
data.subscribe(() => vm.redraw());
|
||||
}
|
||||
const images = data();
|
||||
|
||||
return el('.album', [
|
||||
el('h2', [title]),
|
||||
...images.map(i => {
|
||||
return defineView(
|
||||
ImageView,
|
||||
{
|
||||
imageRow: i,
|
||||
doc: i,
|
||||
showTags: false,
|
||||
remove: removeImageFromAlbum
|
||||
},
|
||||
|
||||
@ -1,17 +1,47 @@
|
||||
import { defineView, defineElement as el } from 'domvm';
|
||||
import * as image from '../data/image.js';
|
||||
import * as index from '../data/indexType.js';
|
||||
import * as imageTag from '../context/manageImageTags.js';
|
||||
import { ImageView } from './image.js';
|
||||
import { AlbumView } from './album.js';
|
||||
import { router } from '../services/router.js';
|
||||
import { router, routeChanged } from '../services/router.js';
|
||||
import { LiveArray } from '../utils/livearray.js';
|
||||
|
||||
const NAV_OPTIONS = {
|
||||
images: {
|
||||
selector: image.SELECTOR,
|
||||
title: 'Images'
|
||||
},
|
||||
albums: {
|
||||
selector: index.SELECTOR,
|
||||
title: 'Albums'
|
||||
}
|
||||
};
|
||||
|
||||
function uploadImages(evt) {
|
||||
image.add(evt.currentTarget.files);
|
||||
}
|
||||
|
||||
export function GalleryView(vm, model) {
|
||||
function uploadImages(evt) {
|
||||
image.add(evt.currentTarget.files);
|
||||
}
|
||||
let data = null;
|
||||
let title = '';
|
||||
|
||||
routeChanged.subscribe(function onRouteChange(router, route) {
|
||||
if (data) {
|
||||
data.cleanup();
|
||||
}
|
||||
const o = NAV_OPTIONS[route.name];
|
||||
data = LiveArray(db, o.selector);
|
||||
title = o.title;
|
||||
data.subscribe(() => vm.redraw());
|
||||
});
|
||||
|
||||
return function(vm, model, key, opts) {
|
||||
const { title, members } = model;
|
||||
if (!data || !data.ready()) {
|
||||
return el('h1', 'Loading...');
|
||||
}
|
||||
|
||||
const members = data();
|
||||
|
||||
return el('.gallery', [
|
||||
el('input#fInput', {
|
||||
@ -28,7 +58,7 @@ export function GalleryView(vm, model) {
|
||||
return defineView(
|
||||
ImageView,
|
||||
{
|
||||
imageRow: i,
|
||||
doc: i,
|
||||
showTags: true,
|
||||
addTag: imageTag.add,
|
||||
remove: image.remove,
|
||||
@ -41,7 +71,7 @@ export function GalleryView(vm, model) {
|
||||
return defineView(
|
||||
AlbumView,
|
||||
{
|
||||
albumRow: a,
|
||||
doc: a,
|
||||
addTag: imageTag.add,
|
||||
remove: imageTag.remove
|
||||
},
|
||||
|
||||
@ -1,27 +1,46 @@
|
||||
import { defineView, defineElement as el } from 'domvm';
|
||||
import { observable, computed } from 'frptools';
|
||||
|
||||
import * as image from '../data/image.js';
|
||||
|
||||
export function ImageView(vm, model) {
|
||||
const { addTag } = model;
|
||||
const imageData = observable(null);
|
||||
let imageId = null;
|
||||
|
||||
function onAddTag(image_id) {
|
||||
addTag(prompt('Tag Name'), image_id);
|
||||
}
|
||||
|
||||
return function(vm, model, key, opts) {
|
||||
const { imageRow, showTags, remove, addTag, removeTag } = model;
|
||||
const { doc } = imageRow;
|
||||
const { doc, showTags, remove, removeTag } = model;
|
||||
const { _id: id, _rev: rev, tags } = doc;
|
||||
const { thumbnail } = doc._attachments;
|
||||
const _showTags = showTags !== undefined ? showTags : true;
|
||||
const filteredTags = _showTags ? Object.entries(doc.tags).filter(([_, visible]) => visible) : [];
|
||||
if (imageId !== id) {
|
||||
image
|
||||
.getAttachment(id, 'thumbnail')
|
||||
.then(thumbnail => {
|
||||
if (imageData()) {
|
||||
URL.revokeObjectURL(imageData());
|
||||
}
|
||||
imageData(URL.createObjectURL(thumbnail));
|
||||
vm.redraw();
|
||||
})
|
||||
.catch(err => {
|
||||
// Probably hasn't created the thumbnail yet.
|
||||
console.log("Probably hasn't created the thumbnail yet.", err);
|
||||
imageId = null;
|
||||
});
|
||||
imageId = id;
|
||||
}
|
||||
|
||||
if (thumbnail) {
|
||||
return el('div', [
|
||||
if (imageData()) {
|
||||
return el('div', { _key: id }, [
|
||||
el(`figure#${doc._id}.image`, [
|
||||
el('img', {
|
||||
src: `data:${thumbnail.content_type};base64,${thumbnail.data}`,
|
||||
src: imageData(),
|
||||
title: `${id} ${name}`,
|
||||
'data-id': id,
|
||||
onclick: [remove, id, rev]
|
||||
}),
|
||||
filteredTags.length
|
||||
|
||||
35
packages/gallery/src/utils/livearray.js
Normal file
35
packages/gallery/src/utils/livearray.js
Normal file
@ -0,0 +1,35 @@
|
||||
import { observable, computed } from 'frptools';
|
||||
import { group, groupEnd, log } from '../services/console.js';
|
||||
import { Watcher } from './watcher.js';
|
||||
|
||||
export function LiveArray(db, selector) {
|
||||
const watcher = Watcher(db, selector);
|
||||
const data = observable({ docs: [] });
|
||||
const docs = computed(r => r.docs, [data]);
|
||||
let changeSub = null;
|
||||
|
||||
const accessor = docs;
|
||||
accessor.ready = observable(false);
|
||||
accessor.cleanup = () => {
|
||||
docs.detach();
|
||||
if (changeSub) {
|
||||
changeSub();
|
||||
}
|
||||
accessor.ready.unsubscribeAll();
|
||||
data({ docs: [] });
|
||||
};
|
||||
|
||||
async function refresh() {
|
||||
group('LiveArray Refreshing');
|
||||
log(selector);
|
||||
data(await db.find({ selector }));
|
||||
log(data());
|
||||
groupEnd('LiveArray Refreshing');
|
||||
}
|
||||
|
||||
refresh().then(() => {
|
||||
changeSub = watcher(refresh);
|
||||
accessor.ready(true);
|
||||
});
|
||||
return accessor;
|
||||
}
|
||||
@ -25,7 +25,7 @@ export function Watcher(db, selector) {
|
||||
});
|
||||
}
|
||||
return () => {
|
||||
this.subscribers.delete(fn);
|
||||
subscribers.delete(fn);
|
||||
if (subscribers.size === 0 && changes) {
|
||||
log('Unwatching:', db, selector);
|
||||
changes.cancel();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user