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:
parent
9b07868edd
commit
3206345999
@ -16,7 +16,7 @@
|
||||
"domvm": "~3.2.1",
|
||||
"exif-parser": "~0.1.9",
|
||||
"extract-text-webpack-plugin": "^3.0.2",
|
||||
"frptools": "2.0.0",
|
||||
"frptools": "2.1.0",
|
||||
"pica": "~2.0.8",
|
||||
"pouchdb-adapter-http": "~6.3.4",
|
||||
"pouchdb-adapter-idb": "~6.3.4",
|
||||
|
||||
@ -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));
|
||||
});
|
||||
53
packages/gallery/src/data/album.js
Normal file
53
packages/gallery/src/data/album.js
Normal 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));
|
||||
// })
|
||||
@ -1,5 +1,4 @@
|
||||
import { PouchDB, TypeSpec } from '../services/db.js';
|
||||
import { log } from '../services/console.js';
|
||||
import { sha256 } from '../utils/crypto.js';
|
||||
import { blobToArrayBuffer } from '../utils/conversion.js';
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,41 +1,70 @@
|
||||
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 { LiveArray } from '../utils/livearray.js';
|
||||
import { prop, computed, bundle } from 'frptools';
|
||||
|
||||
export function AlbumView(vm, model) {
|
||||
const { remove, db } = model;
|
||||
let data = null;
|
||||
let title = null;
|
||||
export function AlbumView(vm, params) {
|
||||
const model = prop({}, pouchDocHash);
|
||||
const images = prop([], pouchDocArrayHash);
|
||||
|
||||
function removeImageFromAlbum(id, rev) {
|
||||
remove(title, id);
|
||||
const id = computed(pouchDocHash, [model]);
|
||||
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) {
|
||||
const { doc, remove } = model;
|
||||
const { props } = doc;
|
||||
function removeAlbum() {
|
||||
model().delete();
|
||||
}
|
||||
|
||||
if (title !== props.title) {
|
||||
if (data) {
|
||||
data.cleanup();
|
||||
}
|
||||
title = props.title;
|
||||
const SELECTOR = Object.assign(
|
||||
{
|
||||
[`tags.${title}`]: { $eq: true }
|
||||
},
|
||||
image.SELECTOR
|
||||
);
|
||||
function uploadImages(album, evt) {
|
||||
Promise.all(Array.from(evt.currentTarget.files).map(ImageType.upload)).then(images => {
|
||||
images.forEach(i => album.addMember(i._id));
|
||||
});
|
||||
}
|
||||
|
||||
data = LiveArray(db, SELECTOR);
|
||||
data.subscribe(() => vm.redraw());
|
||||
}
|
||||
const images = data();
|
||||
model(params.doc);
|
||||
|
||||
return function(vm, params, key, opts) {
|
||||
model(params.doc);
|
||||
|
||||
return el('.album', [
|
||||
el('h2', [title]),
|
||||
...images.map(i => {
|
||||
el('h2', [title(), el('button', { onclick: removeAlbum }, 'X')]),
|
||||
el('input#fInput', {
|
||||
type: 'file',
|
||||
multiple: true,
|
||||
accept: 'image/jpeg',
|
||||
onchange: [uploadImages, model()]
|
||||
}),
|
||||
...images().map(i => {
|
||||
return defineView(
|
||||
ThumbnailView,
|
||||
{
|
||||
@ -43,7 +72,7 @@ export function AlbumView(vm, model) {
|
||||
showTags: false,
|
||||
remove: removeImageFromAlbum
|
||||
},
|
||||
i._id
|
||||
id()
|
||||
);
|
||||
})
|
||||
]);
|
||||
|
||||
@ -3,28 +3,32 @@ import { prop, computed, bundle } from 'frptools';
|
||||
|
||||
import { ImageType } from '../data/image.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) {
|
||||
const model = bundle({
|
||||
_id: prop(doc._id),
|
||||
_rev: prop(doc._rev),
|
||||
sizes: prop(doc.sizes)
|
||||
});
|
||||
export function AttachmentImageView(vm, image) {
|
||||
const model = prop(image, pouchDocHash);
|
||||
const id = computed(pouchDocHash, [model]);
|
||||
const sizes = computed(d => d.sizes, [model]); // always update
|
||||
|
||||
const blobURL = prop('');
|
||||
const imageURL = computed((sizes, bURL) => bURL || sizes.thumbnail || sizes.full, [
|
||||
model.sizes,
|
||||
sizes,
|
||||
blobURL
|
||||
]);
|
||||
const _key = computed((id, rev) => id + rev, [model._id, model._rev]);
|
||||
|
||||
model.subscribe(() => {
|
||||
if (blobURL()) {
|
||||
URL.revokeObjectURL(blobURL());
|
||||
blobURL('');
|
||||
}
|
||||
});
|
||||
|
||||
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) {
|
||||
try {
|
||||
const data = await FileType.getFromURL(model.sizes()[attempt]);
|
||||
const data = await FileType.getFromURL(sizes()[attempt]);
|
||||
|
||||
if (blobURL()) {
|
||||
URL.revokeObjectURL(blobURL());
|
||||
@ -45,16 +49,12 @@ export function AttachmentImageView(vm, doc) {
|
||||
const redrawOff = imageURL.subscribe(() => vm.redraw());
|
||||
|
||||
return function render(vm, doc) {
|
||||
if (!pouchDocComparator(doc, { _id: model._id(), _rev: model._rev() })) {
|
||||
URL.revokeObjectURL(blobURL());
|
||||
blobURL('');
|
||||
}
|
||||
model(doc);
|
||||
|
||||
return el('img', {
|
||||
src: imageURL(),
|
||||
onerror: loadImageFromBlob,
|
||||
_key: _key(),
|
||||
_key: id(),
|
||||
_hooks: {
|
||||
didRemove: cleanup
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { defineView as vw } from 'domvm';
|
||||
import { ImageType } from '../data/image.js';
|
||||
// import * as index from '../data/indexType.js';
|
||||
// import * as imageTag from '../context/manageImageTags.js';
|
||||
import { AlbumType } from '../data/album.js';
|
||||
import { ThumbnailView } from './thumbnail.js';
|
||||
import { AlbumView } from './album.js';
|
||||
import { router, routeChanged } from '../services/router.js';
|
||||
@ -18,11 +17,11 @@ export function GalleryView(vm, model) {
|
||||
true
|
||||
),
|
||||
title: 'Images'
|
||||
},
|
||||
albums: {
|
||||
data: AlbumType.find({}, true),
|
||||
title: 'Albums'
|
||||
}
|
||||
// albums: {
|
||||
// selector: index.SELECTOR,
|
||||
// title: 'Albums'
|
||||
// }
|
||||
};
|
||||
|
||||
let data = null;
|
||||
@ -33,12 +32,26 @@ export function GalleryView(vm, model) {
|
||||
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) {
|
||||
if (laCleanup) {
|
||||
laCleanup();
|
||||
}
|
||||
const o = NAV_OPTIONS[route.name];
|
||||
title = o.title;
|
||||
vm.redraw();
|
||||
|
||||
return o.data.then(la => {
|
||||
data = la;
|
||||
laCleanup = data.subscribe(() => {
|
||||
@ -52,6 +65,7 @@ export function GalleryView(vm, model) {
|
||||
return el('.gallery', [
|
||||
header([
|
||||
el('div', { css: { fontSize: '20pt' } }, 'Gallery'),
|
||||
el('button', { onclick: addAlbum }, 'Add Album'),
|
||||
el('input#fInput', {
|
||||
type: 'file',
|
||||
multiple: true,
|
||||
@ -73,7 +87,7 @@ export function GalleryView(vm, model) {
|
||||
doc: i,
|
||||
showTags: true,
|
||||
// addTag: imageTag.add,
|
||||
remove: i.delete.bind(i)
|
||||
remove: deleteImage
|
||||
// removeTag: imageTag.remove
|
||||
},
|
||||
i._id + i._rev
|
||||
@ -83,8 +97,7 @@ export function GalleryView(vm, model) {
|
||||
return vw(
|
||||
AlbumView,
|
||||
{
|
||||
doc: a,
|
||||
db
|
||||
doc: a
|
||||
// addTag: imageTag.add,
|
||||
// remove: imageTag.remove
|
||||
},
|
||||
|
||||
@ -24,7 +24,7 @@ export function ThumbnailView(vm, model) {
|
||||
el(
|
||||
`figure#${doc._id}.image`,
|
||||
{
|
||||
onclick: { img: [remove, id, rev] }
|
||||
onclick: { img: [remove, doc] }
|
||||
},
|
||||
[
|
||||
vw(AttachmentImageView, doc, doc._id + doc._rev),
|
||||
|
||||
@ -95,19 +95,21 @@ export function PouchORM(PouchDB) {
|
||||
|
||||
const instantiate = doc => new cls(doc);
|
||||
|
||||
async function find(idOrQuery, live = false) {
|
||||
if (typeof idOrQuery === 'string') {
|
||||
return instantiate(await _db.get(idOrQuery));
|
||||
async function find(idOrSelector, live = false) {
|
||||
if (typeof idOrSelector === 'string') {
|
||||
return instantiate(await _db.get(idOrSelector));
|
||||
}
|
||||
|
||||
const isSelector = isObject(idOrSelector);
|
||||
|
||||
const selector = Object.assign(
|
||||
{ _deleted: { exists: false } },
|
||||
isObject(idOrQuery) ? idOrQuery : { _id: { $gt: `${prefix}_0`, $lt: `${prefix}_\ufff0` } }
|
||||
isSelector && idOrSelector._deleted ? { _deleted: true } : { _deleted: { exists: false } },
|
||||
isSelector ? idOrSelector : { _id: { $gt: `${prefix}_0`, $lt: `${prefix}_\ufff0` } }
|
||||
);
|
||||
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) {
|
||||
@ -123,6 +125,18 @@ export function PouchORM(PouchDB) {
|
||||
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, {
|
||||
_name: { value: name },
|
||||
_prefix: { value: prefix },
|
||||
@ -133,6 +147,7 @@ export function PouchORM(PouchDB) {
|
||||
Object.defineProperties(cls, {
|
||||
getOrCreate: { value: getOrCreate },
|
||||
find: { value: find },
|
||||
delete: { value: _delete },
|
||||
db: { value: _db },
|
||||
name: { value: name }
|
||||
});
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { readAsArrayBuffer } from 'pouchdb-binary-utils';
|
||||
import { isObject } from './comparators';
|
||||
|
||||
export function bufferToHexString(buffer) {
|
||||
const hexCodes = [];
|
||||
@ -20,6 +21,14 @@ export function blobToArrayBuffer(blob) {
|
||||
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) {
|
||||
for (let src of rest) {
|
||||
for (let prop in src) {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { prop, computed } from 'frptools';
|
||||
|
||||
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.
|
||||
export function LiveArray(db, selector, mapper) {
|
||||
@ -11,7 +11,7 @@ export function LiveArray(db, selector, mapper) {
|
||||
|
||||
const ready = prop(false);
|
||||
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 = () => {
|
||||
docs.unsubscribeAll();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user