Add back albums

Removing an image from an album does not delete it.  Deleteing an images does not remove its link from the album.
This commit is contained in:
Timothy Farrell 2017-11-20 21:57:37 -06:00
parent 9b07868edd
commit 3206345999
12 changed files with 184 additions and 168 deletions

View File

@ -16,7 +16,7 @@
"domvm": "~3.2.1", "domvm": "~3.2.1",
"exif-parser": "~0.1.9", "exif-parser": "~0.1.9",
"extract-text-webpack-plugin": "^3.0.2", "extract-text-webpack-plugin": "^3.0.2",
"frptools": "2.0.0", "frptools": "2.1.0",
"pica": "~2.0.8", "pica": "~2.0.8",
"pouchdb-adapter-http": "~6.3.4", "pouchdb-adapter-http": "~6.3.4",
"pouchdb-adapter-idb": "~6.3.4", "pouchdb-adapter-idb": "~6.3.4",

View File

@ -1,20 +0,0 @@
import * as image from '../data/image.js';
import * as index from '../data/indexType.js';
export async function add(title, imageId, visible = true) {
const trimmedTitle = title.trim();
await index.add(trimmedTitle, { title: trimmedTitle }, [imageId]);
return image.update(imageId, {
tags: { [trimmedTitle]: visible }
});
}
export async function remove(title, imageId) {
const id = index.hashString(title);
await image.update(imageId, { tags: { [title]: undefined } });
await index.removeMember(title, imageId);
}
image.removed.subscribe(image => {
Object.keys(image.tags).forEach(t => index.removeMember(t, image._id));
});

View File

@ -0,0 +1,53 @@
import { PouchDB, TypeSpec } from '../services/db.js';
import { log } from '../services/console.js';
class AlbumSpec extends TypeSpec {
static getUniqueID(doc) {
return doc.title
.trim()
.replace(/[ \-~!@#$%^&]/g, '_')
.toLowerCase();
}
async addMember(member, position) {
const currentPosition = this.members.indexOf(member);
const newPosition = position ? position : this.members.length;
if (currentPosition !== -1) {
this.members.splice(currentPosition, 1);
}
this.members.splice(newPosition, 0, member);
await this.save();
}
async removeMember(member) {
const currentPosition = this.members.indexOf(member);
if (currentPosition !== -1) {
this.members.splice(currentPosition, 1);
await this.save();
}
}
//
// static validate(doc) {
// // TODO actually validate perhaps against a JSON schema
//
// const schema = {
// title: t.REQUIRED_STRING,
// members: {
// type: "array",
// items: t.STRING
// }
// };
// }
}
export const AlbumType = PouchDB.registerType('Album', AlbumSpec);
// ImageType.watch({_deleted: true}, true)
// .then(la => {
// la.subscribe() );
//
// image.removed.subscribe(image => {
// Object.keys(image.tags).forEach(t => index.removeMember(t, image._id));
// })

View File

@ -1,5 +1,4 @@
import { PouchDB, TypeSpec } from '../services/db.js'; import { PouchDB, TypeSpec } from '../services/db.js';
import { log } from '../services/console.js';
import { sha256 } from '../utils/crypto.js'; import { sha256 } from '../utils/crypto.js';
import { blobToArrayBuffer } from '../utils/conversion.js'; import { blobToArrayBuffer } from '../utils/conversion.js';

View File

@ -1,82 +0,0 @@
import { log, error } from '../services/console.js';
import { getDatabase, getOrCreate } from '../services/db.js';
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');
export const removed = new Event('Index.removed');
// Methods
export const hashString = name =>
name
.trim()
.replace(/[ \-~!@#$%^&]/g, '_')
.toLowerCase();
const getId = id => (id.startsWith(PREFIX) ? id : `${PREFIX}_${hashString(id)}`);
export async function find(keys, options = {}) {
let opts = { include_docs: true };
if (Array.isArray(keys)) {
Object.assign(opts, options);
opts.keys = keys.map(getId);
} else {
Object.assign(opts, keys);
opts.startkey = `${PREFIX}_`;
opts.endkey = `${PREFIX}_\ufff0`;
}
return await db.allDocs(opts);
}
export async function add(id, props = {}, members = []) {
const _id = getId(id);
const [results, created] = await getOrCreate({
_id,
props,
members: []
});
if (members.length) {
members.forEach(async m => await addMember(_id, m));
}
return created || results.ok;
}
export async function addMember(id, member) {
const results = await find([id]);
const doc = results.rows[0].doc;
if (doc.members.indexOf(member) === -1) {
doc.members.push(member);
await db.put(doc);
added.fire(doc._id, member);
}
return doc;
}
export async function removeMember(id, member) {
const results = await find([id]);
const doc = results.rows[0].doc;
const idx = doc.members.indexOf(member);
if (idx !== -1) {
if (doc.members.length > 1) {
doc.members.splice(idx, 1);
await db.put(doc);
removed.fire(doc._id, member);
} else {
await db.remove(doc);
removed.fire(doc._id, member);
}
}
}

View File

@ -1,41 +1,70 @@
import { defineView, defineElement as el } from 'domvm'; import { defineView, defineElement as el } from 'domvm';
import * as image from '../data/image.js';
import { ImageType } from '../data/image.js';
import { FileType } from '../data/file.js';
import { pouchDocArrayHash, pouchDocHash } from '../utils/conversion.js';
import { ThumbnailView } from './thumbnail.js'; import { ThumbnailView } from './thumbnail.js';
import { LiveArray } from '../utils/livearray.js'; import { prop, computed, bundle } from 'frptools';
export function AlbumView(vm, model) { export function AlbumView(vm, params) {
const { remove, db } = model; const model = prop({}, pouchDocHash);
let data = null; const images = prop([], pouchDocArrayHash);
let title = null;
function removeImageFromAlbum(id, rev) { const id = computed(pouchDocHash, [model]);
remove(title, id); const members = computed(d => d.members, [model]); // always update
const title = computed(d => d.title, [model]); // always update
let laCleanup = null;
id.subscribe(async () => {
const la = await ImageType.find(
{
_id: { $in: members() }
},
true
);
function refresh() {
images(la());
vm.redraw();
}
if (laCleanup) {
laCleanup();
}
laCleanup = la.subscribe(refresh);
la.ready.subscribe(refresh);
});
function removeImageFromAlbum(image) {
model().removeMember(image._id);
} }
return function(vm, model, key, opts) { function removeAlbum() {
const { doc, remove } = model; model().delete();
const { props } = doc; }
if (title !== props.title) { function uploadImages(album, evt) {
if (data) { Promise.all(Array.from(evt.currentTarget.files).map(ImageType.upload)).then(images => {
data.cleanup(); images.forEach(i => album.addMember(i._id));
} });
title = props.title; }
const SELECTOR = Object.assign(
{
[`tags.${title}`]: { $eq: true }
},
image.SELECTOR
);
data = LiveArray(db, SELECTOR); model(params.doc);
data.subscribe(() => vm.redraw());
} return function(vm, params, key, opts) {
const images = data(); model(params.doc);
return el('.album', [ return el('.album', [
el('h2', [title]), el('h2', [title(), el('button', { onclick: removeAlbum }, 'X')]),
...images.map(i => { el('input#fInput', {
type: 'file',
multiple: true,
accept: 'image/jpeg',
onchange: [uploadImages, model()]
}),
...images().map(i => {
return defineView( return defineView(
ThumbnailView, ThumbnailView,
{ {
@ -43,7 +72,7 @@ export function AlbumView(vm, model) {
showTags: false, showTags: false,
remove: removeImageFromAlbum remove: removeImageFromAlbum
}, },
i._id id()
); );
}) })
]); ]);

View File

@ -3,28 +3,32 @@ import { prop, computed, bundle } from 'frptools';
import { ImageType } from '../data/image.js'; import { ImageType } from '../data/image.js';
import { FileType } from '../data/file.js'; import { FileType } from '../data/file.js';
import { pouchDocComparator } from '../utils/comparators.js'; import { pouchDocArrayHash, pouchDocHash } from '../utils/conversion.js';
export function AttachmentImageView(vm, doc) { export function AttachmentImageView(vm, image) {
const model = bundle({ const model = prop(image, pouchDocHash);
_id: prop(doc._id), const id = computed(pouchDocHash, [model]);
_rev: prop(doc._rev), const sizes = computed(d => d.sizes, [model]); // always update
sizes: prop(doc.sizes)
});
const blobURL = prop(''); const blobURL = prop('');
const imageURL = computed((sizes, bURL) => bURL || sizes.thumbnail || sizes.full, [ const imageURL = computed((sizes, bURL) => bURL || sizes.thumbnail || sizes.full, [
model.sizes, sizes,
blobURL blobURL
]); ]);
const _key = computed((id, rev) => id + rev, [model._id, model._rev]);
model.subscribe(() => {
if (blobURL()) {
URL.revokeObjectURL(blobURL());
blobURL('');
}
});
async function loadImageFromBlob() { async function loadImageFromBlob() {
const options = ['thumbnail', 'full'].filter(o => model.sizes().hasOwnProperty(o)); const options = ['thumbnail', 'full'].filter(o => sizes().hasOwnProperty(o));
for (let attempt of options) { for (let attempt of options) {
try { try {
const data = await FileType.getFromURL(model.sizes()[attempt]); const data = await FileType.getFromURL(sizes()[attempt]);
if (blobURL()) { if (blobURL()) {
URL.revokeObjectURL(blobURL()); URL.revokeObjectURL(blobURL());
@ -45,16 +49,12 @@ export function AttachmentImageView(vm, doc) {
const redrawOff = imageURL.subscribe(() => vm.redraw()); const redrawOff = imageURL.subscribe(() => vm.redraw());
return function render(vm, doc) { return function render(vm, doc) {
if (!pouchDocComparator(doc, { _id: model._id(), _rev: model._rev() })) {
URL.revokeObjectURL(blobURL());
blobURL('');
}
model(doc); model(doc);
return el('img', { return el('img', {
src: imageURL(), src: imageURL(),
onerror: loadImageFromBlob, onerror: loadImageFromBlob,
_key: _key(), _key: id(),
_hooks: { _hooks: {
didRemove: cleanup didRemove: cleanup
} }

View File

@ -1,7 +1,6 @@
import { defineView as vw } from 'domvm'; import { defineView as vw } from 'domvm';
import { ImageType } from '../data/image.js'; import { ImageType } from '../data/image.js';
// import * as index from '../data/indexType.js'; import { AlbumType } from '../data/album.js';
// import * as imageTag from '../context/manageImageTags.js';
import { ThumbnailView } from './thumbnail.js'; import { ThumbnailView } from './thumbnail.js';
import { AlbumView } from './album.js'; import { AlbumView } from './album.js';
import { router, routeChanged } from '../services/router.js'; import { router, routeChanged } from '../services/router.js';
@ -18,11 +17,11 @@ export function GalleryView(vm, model) {
true true
), ),
title: 'Images' title: 'Images'
},
albums: {
data: AlbumType.find({}, true),
title: 'Albums'
} }
// albums: {
// selector: index.SELECTOR,
// title: 'Albums'
// }
}; };
let data = null; let data = null;
@ -33,12 +32,26 @@ export function GalleryView(vm, model) {
Array.from(evt.currentTarget.files).forEach(ImageType.upload); Array.from(evt.currentTarget.files).forEach(ImageType.upload);
} }
function deleteImage(i) {
ImageType.delete(i._id);
}
function addAlbum() {
const a = new AlbumType({
title: prompt('Album Name'),
members: []
});
a.save();
}
routeChanged.subscribe(function onRouteChange(router, route) { routeChanged.subscribe(function onRouteChange(router, route) {
if (laCleanup) { if (laCleanup) {
laCleanup(); laCleanup();
} }
const o = NAV_OPTIONS[route.name]; const o = NAV_OPTIONS[route.name];
title = o.title; title = o.title;
vm.redraw();
return o.data.then(la => { return o.data.then(la => {
data = la; data = la;
laCleanup = data.subscribe(() => { laCleanup = data.subscribe(() => {
@ -52,6 +65,7 @@ export function GalleryView(vm, model) {
return el('.gallery', [ return el('.gallery', [
header([ header([
el('div', { css: { fontSize: '20pt' } }, 'Gallery'), el('div', { css: { fontSize: '20pt' } }, 'Gallery'),
el('button', { onclick: addAlbum }, 'Add Album'),
el('input#fInput', { el('input#fInput', {
type: 'file', type: 'file',
multiple: true, multiple: true,
@ -73,7 +87,7 @@ export function GalleryView(vm, model) {
doc: i, doc: i,
showTags: true, showTags: true,
// addTag: imageTag.add, // addTag: imageTag.add,
remove: i.delete.bind(i) remove: deleteImage
// removeTag: imageTag.remove // removeTag: imageTag.remove
}, },
i._id + i._rev i._id + i._rev
@ -83,8 +97,7 @@ export function GalleryView(vm, model) {
return vw( return vw(
AlbumView, AlbumView,
{ {
doc: a, doc: a
db
// addTag: imageTag.add, // addTag: imageTag.add,
// remove: imageTag.remove // remove: imageTag.remove
}, },

View File

@ -24,7 +24,7 @@ export function ThumbnailView(vm, model) {
el( el(
`figure#${doc._id}.image`, `figure#${doc._id}.image`,
{ {
onclick: { img: [remove, id, rev] } onclick: { img: [remove, doc] }
}, },
[ [
vw(AttachmentImageView, doc, doc._id + doc._rev), vw(AttachmentImageView, doc, doc._id + doc._rev),

View File

@ -95,19 +95,21 @@ export function PouchORM(PouchDB) {
const instantiate = doc => new cls(doc); const instantiate = doc => new cls(doc);
async function find(idOrQuery, live = false) { async function find(idOrSelector, live = false) {
if (typeof idOrQuery === 'string') { if (typeof idOrSelector === 'string') {
return instantiate(await _db.get(idOrQuery)); return instantiate(await _db.get(idOrSelector));
} }
const isSelector = isObject(idOrSelector);
const selector = Object.assign( const selector = Object.assign(
{ _deleted: { exists: false } }, isSelector && idOrSelector._deleted ? { _deleted: true } : { _deleted: { exists: false } },
isObject(idOrQuery) ? idOrQuery : { _id: { $gt: `${prefix}_0`, $lt: `${prefix}_\ufff0` } } isSelector ? idOrSelector : { _id: { $gt: `${prefix}_0`, $lt: `${prefix}_\ufff0` } }
); );
if (live) { if (live) {
return LiveArray(_db, idOrQuery, instantiate); return LiveArray(_db, idOrSelector, instantiate);
} }
return (await _db.find({ selector: idOrQuery })).docs.map(instantiate); return (await _db.find({ selector: idOrSelector })).docs.map(instantiate);
} }
async function getOrCreate(props) { async function getOrCreate(props) {
@ -123,6 +125,18 @@ export function PouchORM(PouchDB) {
return doc; return doc;
} }
async function _delete(id) {
try {
const doc = await find(id);
doc._deleted = true;
await _db.put(doc);
} catch (e) {
if (e.status !== 404) {
throw e;
}
}
}
Object.defineProperties(cls.prototype, { Object.defineProperties(cls.prototype, {
_name: { value: name }, _name: { value: name },
_prefix: { value: prefix }, _prefix: { value: prefix },
@ -133,6 +147,7 @@ export function PouchORM(PouchDB) {
Object.defineProperties(cls, { Object.defineProperties(cls, {
getOrCreate: { value: getOrCreate }, getOrCreate: { value: getOrCreate },
find: { value: find }, find: { value: find },
delete: { value: _delete },
db: { value: _db }, db: { value: _db },
name: { value: name } name: { value: name }
}); });

View File

@ -1,4 +1,5 @@
import { readAsArrayBuffer } from 'pouchdb-binary-utils'; import { readAsArrayBuffer } from 'pouchdb-binary-utils';
import { isObject } from './comparators';
export function bufferToHexString(buffer) { export function bufferToHexString(buffer) {
const hexCodes = []; const hexCodes = [];
@ -20,6 +21,14 @@ export function blobToArrayBuffer(blob) {
return new Promise(resolve => readAsArrayBuffer(blob, resolve)); return new Promise(resolve => readAsArrayBuffer(blob, resolve));
} }
export const arrayHashWrapper = hash => arr => (Array.isArray(arr) ? arr.map(hash).join('?') : arr);
export function pouchDocHash(d) {
return isObject(d) ? `${d._id}:${d._rev}` : d;
}
export const pouchDocArrayHash = arrayHashWrapper(pouchDocHash);
export function deepAssign(to, ...rest) { export function deepAssign(to, ...rest) {
for (let src of rest) { for (let src of rest) {
for (let prop in src) { for (let prop in src) {

View File

@ -1,7 +1,7 @@
import { prop, computed } from 'frptools'; import { prop, computed } from 'frptools';
import { Watcher } from './watcher.js'; import { Watcher } from './watcher.js';
import { pouchDocArrayComparator } from './comparators.js'; import { pouchDocArrayHash } from './conversion.js';
// LiveArray is a subscribable property function that always returns the db results that match the provided selector and calls subscribers when the results change. // LiveArray is a subscribable property function that always returns the db results that match the provided selector and calls subscribers when the results change.
export function LiveArray(db, selector, mapper) { export function LiveArray(db, selector, mapper) {
@ -11,7 +11,7 @@ export function LiveArray(db, selector, mapper) {
const ready = prop(false); const ready = prop(false);
const data = prop({ docs: [] }); const data = prop({ docs: [] });
const docs = computed(r => r.docs.map(_mapper), [data], pouchDocArrayComparator); const docs = computed(r => r.docs.map(_mapper), [data], pouchDocArrayHash);
const cleanup = () => { const cleanup = () => {
docs.unsubscribeAll(); docs.unsubscribeAll();