diff --git a/config.json b/config.json index cfd416b..0a7072a 100644 --- a/config.json +++ b/config.json @@ -241,6 +241,18 @@ "format": {"type": "boolean", "args": []}, "sort": true }, + { + "id": "quotes", + "title": "Quotes", + "find": true, + "type": "text" + }, + { + "id": "notes", + "title": "Notes", + "find": true, + "type": "text" + }, { "id": "fulltext", "title": "Full Text", @@ -368,6 +380,7 @@ "showFilters": true, "showIconInfo": true, "showInfo": true, + "showPeers": true, "showSection": { "notifications": { "received": true, diff --git a/ctl b/ctl index 8195061..74d9d76 100755 --- a/ctl +++ b/ctl @@ -180,14 +180,10 @@ if [ "$1" == "open" ]; then if ps -p `cat "$PID"` > /dev/null; then open_linux else - #$PYTHON "${NAME}/oml/gtkstatus.py" $@ - #exit $? open_linux "$0" start & fi else - #$PYTHON "$NAME/oml/gtkstatus.py" $@ - #exit $? open_linux "$0" start & fi diff --git a/oml/annotation/api.py b/oml/annotation/api.py new file mode 100644 index 0000000..005127b --- /dev/null +++ b/oml/annotation/api.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +from oxtornado import actions +from . import models +import settings +import state +from changelog import add_record + +import logging +logger = logging.getLogger(__name__) + + +def getAnnotations(data): + response = {} + response['annotations'] = models.Annotation.get_by_item(data['id']) + return response +actions.register(getAnnotations) + + +def addAnnotation(data): + item_id = data.pop('item') + a = models.Annotation.create(item_id, settings.USER_ID, data) + a.add_record('addannotation') + response = a.json() + return response +actions.register(addAnnotation) + + +def editNote(data): + a = models.Annotation.get(state.user(), data['item'], data['annotation']) + if a: + a.data['notes'] = data['notes'] + a.add_record('editannotation') + a.save() + response = a.json() + else: + response = {} + return response +actions.register(editNote) + + +def removeAnnotation(data): + a = models.Annotation.get(state.user(), data['item'], data['annotation']) + if a: + a.add_record('removeannotation') + a.delete() + response = {} + return response +actions.register(removeAnnotation) diff --git a/oml/annotation/models.py b/oml/annotation/models.py new file mode 100644 index 0000000..1b144f9 --- /dev/null +++ b/oml/annotation/models.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- +from datetime import datetime +import json +import logging +import os +import shutil +import unicodedata + +from sqlalchemy.orm import load_only +import ox +import sqlalchemy as sa + +from changelog import add_record +from db import MutableDict +import db +import json_pickler +import settings +import state +import utils +import media +from websocket import trigger_event + + +logger = logging.getLogger(__name__) + + +class Annotation(db.Model): + __tablename__ = 'annotation' + + _id = sa.Column(sa.Integer(), primary_key=True) + id = sa.Column(sa.String(43)) + created = sa.Column(sa.DateTime()) + modified = sa.Column(sa.DateTime()) + + user_id = sa.Column(sa.String(43), sa.ForeignKey('user.id')) + user = sa.orm.relationship('User', backref=sa.orm.backref('annotations', lazy='dynamic')) + item_id = sa.Column(sa.String(43), sa.ForeignKey('item.id')) + item = sa.orm.relationship('Item', backref=sa.orm.backref('items', lazy='dynamic')) + data = sa.Column(MutableDict.as_mutable(sa.PickleType(pickler=json_pickler))) + + findquotes = sa.Column(sa.Text(), index=True) + findnotes = sa.Column(sa.Text(), index=True) + + def __init__(self, item_id, user_id, data): + self.created = datetime.utcnow() + self.modified = datetime.utcnow() + self.item_id = item_id + self.user_id = user_id + self.data = data + + @classmethod + def create(cls, item_id, user_id, data): + a = cls(item_id, user_id, data) + a.save() + return a + + def delete(self): + state.db.session.delete(self) + state.db.session.commit() + + @classmethod + def get(cls, user, item_id, annotation_id): + if isinstance(user, str): + qs = cls.query.filter_by(item_id=item_id, user_id=user, id=annotation_id) + else: + qs = cls.query.filter_by(item_id=item_id, user=user, id=annotation_id) + for a in qs: + return a + + @classmethod + def get_by_item(cls, item_id): + annotations = [] + for a in cls.query.filter_by(item_id=item_id): + annotations.append(a.json()) + return annotations + + def save(self): + id = self.data.get('id') + if id: + self.id = id + self.findquotes = unicodedata.normalize('NFKD', self.data.get('text', '')).lower() + note = self.data.get('notes', {}) + if isinstance(note, list) and note: + note = note[0] + if isinstance(note, dict): + note = note.get('value', '') + else: + note = '' + self.findnotes = unicodedata.normalize('NFKD', note).lower() + state.db.session.add(self) + state.db.session.commit() + + def json(self): + data = self.data.copy() + data['created'] = self.created + data['modified'] = self.modified + data['user'] = self.user_id + data['_id'] = ox.toAZ(self._id) + if isinstance(data.get('notes'), dict): + note = data['notes'] + if self.user_id != settings.USER_ID: + note['user'] = self.user_id + if not note.get('id'): + note['id'] = 'A' + data['notes'] = [data['notes']] + if 'notes' not in data: + data['notes'] = [] + return data + + def add_record(self, action): + args = [self.item_id] + if action == 'addannotation': + args.append(self.data) + elif action == 'editannotation': + args.append(self.id) + args.append({ + 'notes': self.data['notes'] + }) + elif action == 'removeannotation': + args.append(self.id) + else: + raise Exception('unknown action') + add_record(action, *args) + diff --git a/oml/api.py b/oml/api.py index 1975c7d..8cd4eb8 100644 --- a/oml/api.py +++ b/oml/api.py @@ -12,6 +12,7 @@ from oxtornado import actions import item.api import user.api +import annotation.api import update import utils diff --git a/oml/changelog.py b/oml/changelog.py index c16834c..3f89f47 100644 --- a/oml/changelog.py +++ b/oml/changelog.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- -import os from datetime import datetime import json +import os import sqlalchemy as sa from sqlalchemy.sql.expression import text @@ -76,6 +76,10 @@ class Changelog(db.Model): addpeer peerid peername removepeer peerid peername editpeer peerid {username: string, contact: string} + + addannotation itemid data + editannotation itemid annotationid data + removeannotation itemid annotationid ''' __tablename__ = 'changelog' id = sa.Column(sa.Integer(), primary_key=True) @@ -327,6 +331,27 @@ class Changelog(db.Model): peer.save() return True + def action_addannotation(self, user, timestamp, itemid, data): + from annotation.models import Annotation + Annotation.create(item_id=itemid, user_id=user.id, data=data) + return True + + def action_editannotation(self, user, timestamp, itemid, annotationid, data): + from annotation.models import Annotation + a = Annotation.get(user, itemid, annotationid) + if a: + for key in data: + a.data[key] = data[key] + a.save() + return True + + def action_removeannotation(self, user, timestamp, itemid, annotationid): + from annotation.models import Annotation + a = Annotation.get(user, itemid, annotationid) + if a: + a.delete() + return True + @classmethod def aggregated_changes(cls, since=None, user_id=None): from item.models import Item diff --git a/oml/library.py b/oml/library.py index 362826d..3c17ed0 100644 --- a/oml/library.py +++ b/oml/library.py @@ -190,6 +190,33 @@ class Peer(object): self.info['username'] = args[0] elif action == 'editcontact': self.info['contact'] = args[0] + elif action == 'addannotation': + from annotation.models import Annotation + if len(args) == 2: + itemid, data = args + Annotation.create(item_id=itemid, user_id=self.id, data=data) + else: + logger.error('invalid entry %s %s', action, args) + elif action == 'editannotation': + from annotation.models import Annotation + if len(args) == 3: + itemid, annotationid, data = args + a = Annotation.get(self.id, itemid, annotationid) + if a: + for key in data: + a.data[key] = data[key] + a.save() + else: + logger.error('invalid entry %s %s', action, args) + elif action == 'removeannotation': + from annotation.models import Annotation + if len(args) == 2: + itemid, annotationid = args + a = Annotation.get(self.id, itemid, annotationid) + if a: + a.delete() + else: + logger.error('invalid entry %s %s', action, args) else: logger.debug('UNKNOWN ACTION:', action) self.info['revision'] = revision diff --git a/oml/localnodes.py b/oml/localnodes.py index 20562e7..5178f0a 100644 --- a/oml/localnodes.py +++ b/oml/localnodes.py @@ -152,8 +152,8 @@ class LocalNodes(dict): if state.tasks: state.tasks.queue('removelocalinfo', id) - def get(self, user_id): - data = super().get(user_id) + def get_data(self, user_id): + data = self.get(user_id) if data and can_connect(data): return data return None diff --git a/oml/nodes.py b/oml/nodes.py index c5cfbfe..3e4c575 100644 --- a/oml/nodes.py +++ b/oml/nodes.py @@ -124,9 +124,12 @@ class Node(Thread): self.local = None self.port = 9851 + def is_local(self): + return self._nodes and self.user_id in self._nodes.local + def get_local(self): if self._nodes and self._nodes.local: - return self._nodes.local.get(self.user_id) + return self._nodes.local.get_data(self.user_id) return None def request(self, action, *args): @@ -216,7 +219,7 @@ class Node(Thread): return False def is_online(self): - return self.online or self.get_local() is not None + return self.online or self.is_local() def send_response(self): self._q.put('send_response') @@ -523,7 +526,8 @@ class Nodes(Thread): while not state.shutdown: args = self._q.get() if args: - logger.debug('processing nodes queue: next: "%s", %s entries in queue', args[0], self._q.qsize()) + if DEBUG_NODES: + logger.debug('processing nodes queue: next: "%s", %s entries in queue', args[0], self._q.qsize()) if args[0] == 'add': self._add(*args[1:]) elif args[0] == 'pull': @@ -532,7 +536,7 @@ class Nodes(Thread): self._call(*args) def queue(self, *args): - if args: + if args and DEBUG_NODES: logger.debug('queue "%s", %s entries in queue', args, self._q.qsize()) self._q.put(list(args)) diff --git a/oml/queryparser.py b/oml/queryparser.py index 939611b..8562218 100644 --- a/oml/queryparser.py +++ b/oml/queryparser.py @@ -116,7 +116,18 @@ class Parser(object): elif k == 'fulltext': ids = find_fulltext(v) return self.in_ids(ids, exclude) - + elif k in ('notes', 'quotes'): + from annotation.models import Annotation + if isinstance(v, str): + v = unicodedata.normalize('NFKD', v).lower() + ids = set() + if k == 'notes': + qs = Annotation.query.filter(get_operator('=')(Annotation.findnotes, v)) + elif k == 'quotes': + qs = Annotation.query.filter(get_operator('=')(Annotation.findquotes, v)) + for a in qs: + ids.add(a.item_id) + return self.in_ids(ids, exclude) elif key_type in ("string", "text"): if isinstance(v, str): v = unicodedata.normalize('NFKD', v).lower() diff --git a/oml/server.py b/oml/server.py index b01d598..fbae2fc 100644 --- a/oml/server.py +++ b/oml/server.py @@ -170,8 +170,8 @@ def run(): import bandwidth state.bandwidth = bandwidth.Bandwidth() state.tor = tor.Tor() - state.node = node.server.start() state.nodes = nodes.Nodes() + state.node = node.server.start() def publish(): if not state.tor.is_online(): diff --git a/oml/settings.py b/oml/settings.py index 1d5d9ca..39df3a2 100644 --- a/oml/settings.py +++ b/oml/settings.py @@ -82,7 +82,7 @@ if 'modules' in release and 'openmedialibrary' in release['modules']: else: MINOR_VERSION = 'git' -NODE_PROTOCOL = "0.8" +NODE_PROTOCOL = "0.9" VERSION = "%s.%s" % (NODE_PROTOCOL, MINOR_VERSION) USER_AGENT = 'OpenMediaLibrary/%s' % VERSION @@ -95,4 +95,4 @@ FULLTEXT_SUPPORT = fulltext.platform_supported() if not FULLTEXT_SUPPORT: config['itemKeys'] = [k for k in config['itemKeys'] if k['id'] != 'fulltext'] -DB_VERSION = 17 +DB_VERSION = 18 diff --git a/oml/setup.py b/oml/setup.py index 69024d0..1581e83 100644 --- a/oml/setup.py +++ b/oml/setup.py @@ -151,6 +151,22 @@ CREATE TABLE listitem ( FOREIGN KEY(item_id) REFERENCES item (id), FOREIGN KEY(list_id) REFERENCES list (id) ); +CREATE TABLE annotation ( + _id INTEGER NOT NULL, + id VARCHAR(43), + created DATETIME, + modified DATETIME, + user_id VARCHAR(43), + item_id VARCHAR(43), + data BLOB, + findquotes TEXT, + findnotes TEXT, + PRIMARY KEY (_id), + FOREIGN KEY(user_id) REFERENCES user (id), + FOREIGN KEY(item_id) REFERENCES item (id) +); +CREATE INDEX ix_annotation_findquotes ON annotation (findquotes); +CREATE INDEX ix_annotation_findnotes ON annotation (findnotes); PRAGMA journal_mode=WAL ''' for statement in sql.split(';'): diff --git a/oml/update.py b/oml/update.py index e51ed08..3ae467a 100644 --- a/oml/update.py +++ b/oml/update.py @@ -377,6 +377,8 @@ class Update(Thread): db_version = migrate_16() if db_version < 17: db_version = migrate_17() + if db_version < 18: + db_version = migrate_18() settings.server['db_version'] = db_version def run(self): @@ -674,3 +676,25 @@ def migrate_17(): lists.append(l.name) add_record('orderlists', lists) return 17 + +def migrate_18(): + db.run_sql([ + '''CREATE TABLE annotation ( + _id INTEGER NOT NULL, + id VARCHAR(43), + created DATETIME, + modified DATETIME, + user_id VARCHAR(43), + item_id VARCHAR(43), + data BLOB, + findquotes TEXT, + findnotes TEXT, + PRIMARY KEY (_id), + FOREIGN KEY(user_id) REFERENCES user (id), + FOREIGN KEY(item_id) REFERENCES item (id) +)''']) + db.run_sql([ + 'CREATE INDEX ix_annotation_findquotes ON annotation (findquotes)', + 'CREATE INDEX ix_annotation_findnotes ON annotation (findnotes)' + ]) + return 18 diff --git a/oml/user/api.py b/oml/user/api.py index b3aff56..53448b3 100644 --- a/oml/user/api.py +++ b/oml/user/api.py @@ -474,7 +474,7 @@ def removePeering(data): if len(data.get('id', '')) not in (16, 43): logger.debug('invalid user id') return {} - u = models.User.get(data['id'], for_udpate=True) + u = models.User.get(data['id'], for_update=True) if u: u.info['message'] = data.get('message', '') u.update_peering(False) diff --git a/oml/user/models.py b/oml/user/models.py index 77ff81e..ffe60f5 100644 --- a/oml/user/models.py +++ b/oml/user/models.py @@ -187,7 +187,9 @@ class User(db.Model): def cleanup(self): from item.models import user_items, Item + from annotation.models import Annotation List.query.filter_by(user_id=self.id).delete() + Annotation.query.filter_by(user_id=self.id).delete() c_user_id = user_items.columns['user_id'] q = user_items.delete().where(c_user_id.is_(self.id)) state.db.session.execute(q) @@ -197,6 +199,7 @@ class User(db.Model): state.peers[self.id].remove() del state.peers[self.id] + def update_name(self): if self.id == settings.USER_ID: name = settings.preferences.get('username', 'anonymous') diff --git a/static/js/annotation.js b/static/js/annotation.js index b6e9368..13887c6 100644 --- a/static/js/annotation.js +++ b/static/js/annotation.js @@ -6,9 +6,16 @@ oml.ui.annotation = function(annotation, $iframe) { .html(Ox.encodeHTMLEntities(annotation.text).replace(/\n/g, '
')) .on({ click: function(event) { - that.select() + var id + if (event.ctrlKey) { + that.deselect() + id = null + } else { + that.select() + id = annotation.id + } $iframe.postMessage('selectAnnotation', { - id: annotation.id + id: id }) } }); @@ -39,14 +46,22 @@ oml.ui.annotation = function(annotation, $iframe) { note.value = data.value note.modified = (new Date).toISOString() } else { - annotation.notes.push({ + annotation.notes.push(note = { created: data.created || (new Date).toISOString(), modified: (new Date).toISOString(), id: data.id, - user: '', value: data.value }) } + oml.api.editNote({ + item: oml.user.ui.item, + annotation: annotation.id, + notes: { + created: note.created, + modified: note.modified, + value: note.value + } + }) that.triggerEvent('change') } }); @@ -82,6 +97,11 @@ oml.ui.annotation = function(annotation, $iframe) { addNote() } } + that.delete = function() { + that.triggerEvent('delete', { + id: annotation.id + }) + } that.deselect = function() { that.removeClass('selected') that.loseFocus() @@ -94,7 +114,8 @@ oml.ui.annotation = function(annotation, $iframe) { selected && selected.classList.remove('selected') that.addClass('selected') that.gainFocus() - that[0].scrollIntoViewIfNeeded() + oml.$ui.annotationPanel.updateSelection(false) + that[0].scrollIntoViewIfNeeded && that[0].scrollIntoViewIfNeeded() } return that; }; diff --git a/static/js/annotationPanel.js b/static/js/annotationPanel.js index 0860c0d..c189abc 100644 --- a/static/js/annotationPanel.js +++ b/static/js/annotationPanel.js @@ -1,12 +1,13 @@ 'use strict'; oml.ui.annotationPanel = function() { + var ui = oml.user.ui; var ui = oml.user.ui; var $bar = Ox.Bar({size: 16}); - var $button = Ox.Button({ + var $addQuote = Ox.Button({ disabled: true, style: 'symbol', title: 'add', @@ -18,6 +19,20 @@ oml.ui.annotationPanel = function() { } }).appendTo($bar); + var $deleteQuote = Ox.Button({ + disabled: true, + style: 'symbol', + title: 'remove', + tooltip: Ox._('Delete Quote'), + type: 'image' + }).bindEvent({ + click: function() { + var $annotation = oml.$ui.annotationFolder.find('.OMLAnnotation.selected') + $annotation.length && $annotation.delete() + $deleteQuote.options({disabled: true}) + } + }).appendTo($bar); + var $menuButton = Ox.MenuButton({ items: [ {id: 'addAnnotation', title: 'Add Annotation', disabled: true}, @@ -67,6 +82,21 @@ oml.ui.annotationPanel = function() { }, function(result) { oml.ui.exportAnnotationsDialog(result.data).open() }) + } else { + console.log('click', id, data) + } + }, + change: function(data) { + var id = data.id; + console.log('change', data) + if (id == 'show') { + console.log('show', data) + oml.UI.set({annotationsShow: data.checked[0].id}); + } else if (id == 'sort') { + console.log('sort', data) + oml.UI.set({annotationsSort: data.checked[0].id}); + } else { + console.log('change', id, data) } } }).appendTo($bar); @@ -86,9 +116,13 @@ oml.ui.annotationPanel = function() { }); that.updateSelection = function(selection) { - $button.options({ + $addQuote.options({ disabled: !selection }) + var $annotation = oml.$ui.annotationFolder.find('.OMLAnnotation.selected') + $deleteQuote.options({ + disabled: !$annotation.length + }) } return that; diff --git a/static/js/exportAnnotationsDialog.js b/static/js/exportAnnotationsDialog.js index f891efa..6078e19 100644 --- a/static/js/exportAnnotationsDialog.js +++ b/static/js/exportAnnotationsDialog.js @@ -47,7 +47,8 @@ oml.ui.exportAnnotationsDialog = function(data) { var annotations = oml.$ui.viewer.getAnnotations() var text = 'Annotations for ' + data.title + ' (' + data.author.join(', ') + ')\n\n\n\n' text += annotations.map(function(annotation) { - var text = 'Quote:\n\n' + annotation.text + var page = annotation.pageLabel || annotation.page + var text = 'Quote' + (page ? ' Page ' + page : '' )+ ':\n\n' + annotation.text if (annotation.notes.length) { text += '\n\nNotes:\n' + annotation.notes.map(function(note) { return note.value diff --git a/static/js/folders.js b/static/js/folders.js index 17775b9..b96e9c7 100644 --- a/static/js/folders.js +++ b/static/js/folders.js @@ -20,6 +20,9 @@ oml.ui.folders = function() { oml_showfolder: function() { oml.resizeListFolders(); }, + oml_showpeers: function() { + that.updateElement(); + }, oml_showinfo: function() { oml.resizeListFolders(); } @@ -48,9 +51,15 @@ oml.ui.folders = function() { function getUsersAndLists(callback) { oml.getUsers(function(users_) { - users = users_.filter(function(user) { - return user.id == oml.user.id || user.peered; - }); + if (ui.showPeers) { + users = users_.filter(function(user) { + return user.id == oml.user.id || user.peered; + }); + } else { + users = users_.filter(function(user) { + return user.id == oml.user.id; + }); + } oml.getLists(function(lists) { callback(users, lists); }); diff --git a/static/js/mainMenu.js b/static/js/mainMenu.js index 501cc03..039595a 100644 --- a/static/js/mainMenu.js +++ b/static/js/mainMenu.js @@ -214,6 +214,12 @@ oml.ui.mainMenu = function() { title: Ox._((ui.showSidebar ? 'Hide' : 'Show') + ' Sidebar'), keyboard: 'shift s' }, + { + id: 'showpeers', + title: Ox._((ui.showPeers ? 'Hide' : 'Show') + ' Peers'), + keyboard: 'shift p', + disabled: !ui.showSidebar + }, { id: 'showinfo', title: Ox._((ui.showInfo ? 'Hide' : 'Show') + ' Info'), @@ -539,6 +545,8 @@ oml.ui.mainMenu = function() { oml.history.clear(); } else if (id == 'showsidebar') { oml.UI.set({showSidebar: !ui.showSidebar}); + } else if (id == 'showpeers') { + oml.UI.set({showPeers: !ui.showPeers}); } else if (id == 'showinfo') { oml.UI.set({showInfo: !ui.showInfo}); } else if (id == 'showfilters') { @@ -636,12 +644,16 @@ oml.ui.mainMenu = function() { that[action]('viewMenu_iconsSubmenu_extension'); that[action]('viewMenu_iconsSubmenu_size'); }, + oml_showpeers: function(data) { + that.setItemTitle('showpeers', Ox._((data.value ? 'Hide' : 'Show') + ' Peers')); + }, oml_showinfo: function(data) { that.setItemTitle('showinfo', Ox._((data.value ? 'Hide' : 'Show') + ' Info')); }, oml_showsidebar: function(data) { that.setItemTitle('showsidebar', Ox._((data.value ? 'Hide' : 'Show') + ' Sidebar')); that[data.value ? 'enableItem' : 'disableItem']('showinfo'); + that[data.value ? 'enableItem' : 'disableItem']('showpeers'); }, }); Ox.Event.bind({ @@ -726,8 +738,17 @@ oml.ui.mainMenu = function() { key_shift_f: function() { !ui.item && oml.UI.set({showFilters: !ui.showFilters}); }, - key_shift_i: function() { - ui.showSidebar && oml.UI.set({showInfo: !ui.showInfo}); + key_shift_p: function(event, name, target) { + // FIXME: event triggers twice + if (target.hasClass('OxFocus')) { + ui.showSidebar && oml.UI.set({showPeers: !ui.showPeers}); + } + }, + key_shift_i: function(event, name, target) { + // FIXME: event triggers twice + if (target.hasClass('OxFocus')) { + ui.showSidebar && oml.UI.set({showInfo: !ui.showInfo}); + } }, key_shift_s: function() { oml.UI.set({showSidebar: !ui.showSidebar}); diff --git a/static/js/viewer.js b/static/js/viewer.js index 1eb49ca..362e178 100644 --- a/static/js/viewer.js +++ b/static/js/viewer.js @@ -15,6 +15,12 @@ oml.ui.viewer = function() { }, oml_showannotations: function() { panel.toggleElement(1); + }, + oml_sortannotations: function(data) { + that.renderAnnotations() + }, + oml_annotationusers: function(data) { + that.renderAnnotations() } }), panel = Ox.SplitPanel({ @@ -40,38 +46,81 @@ oml.ui.viewer = function() { $iframe, item; function loadAnnotations(callback) { - annotations = JSON.parse(localStorage[item + '.annotations'] || '[]') - annotations.forEach(function(data) { - if (data.comments && !data.notes) { - data.notes = data.comments - delete data.comments - } - data.notes = data.notes || []; - }) - callback && callback(annotations) - } - function saveAnnotations(data) { - if (data) { - data.created = data.created || (new Date).toISOString(); - if (data.comments && !data.notes) { - data.notes = data.comments - delete data.comments - } - data.notes = data.notes || []; - annotations.push(data); + if (localStorage[item + '.annotations']) { + annotations = JSON.parse(localStorage[item + '.annotations'] || '[]') + var ids = [] + annotations.forEach(function(data) { + if (data.comments && !data.notes) { + data.notes = data.comments + delete data.comments + } + if (!Ox.contains(ids, data.id)) { + ids.push(data.id) + var note + if (data.notes && data.notes.length) { + note = data.notes[0] + delete data.notes + } + addAnnotation(data, false) + if (note) { + data.notes = [note] + } else { + data.notes = [] + } + if (data.notes.length) { + var note = data.notes[0] + oml.api.editNote({ + item: ui.item, + annotation: data.id, + notes: { + created: note.created, + modified: note.modified, + value: note.value + } + }) + } + } else { + console.log('ignore second time', data) + } + }) + localStorage[oml.user.ui.item + '.annotations_'] = localStorage[oml.user.ui.item + '.annotations'] + delete localStorage[oml.user.ui.item + '.annotations'] + callback && callback(annotations) + } else { + oml.api.getAnnotations({ + id: ui.item + }, function(result) { + console.log(result) + annotations = result.data.annotations + callback && callback(annotations) + }) } - localStorage[item + '.annotations'] = JSON.stringify(annotations) } + function addAnnotation(data, save) { + var a = Ox.extend({}, data) + a.created = a.created || (new Date).toISOString(); + a.item = ui.item + if (save !== false) { + oml.api.addAnnotation(a) + } + data.notes = data.notes || []; + annotations.push(data); + } + function removeAnnotation(id) { - annotations = annotations.filter(function(annotation) { - return annotation.id != id + oml.api.removeAnnotation({ + item: ui.item, + annotation: id + }, function(result) { + annotations = annotations.filter(function(annotation) { + return annotation.id != id + }) }) - saveAnnotations() } var annotationEvents = { - change: function() { - saveAnnotations() + change: function(data) { + // console.log('change', data) }, 'delete': function(data) { oml.$ui.annotationFolder.find('#a-' + data.id).remove() @@ -93,14 +142,16 @@ oml.ui.viewer = function() { height: '100%', border: 0 }).onMessage(function(data, event) { + console.log('got', event, data) if (event == 'addAnnotation') { - saveAnnotations(data); + addAnnotation(data); var $annotation = oml.ui.annotation(data, $iframe).bindEvent(annotationEvents) oml.$ui.annotationFolder.append($annotation); $annotation.annotate(); + oml.$ui.annotationPanel.updateSelection(false) } else if (event == 'removeAnnotation') { oml.$ui.annotationFolder.find('#a-' + data.id).remove() - removeAnnotation(data.id) + data.id && removeAnnotation(data.id) } else if (event == 'selectAnnotation') { if (data.id) { var $annotation = oml.$ui.annotationFolder.find('#a-' + data.id) @@ -109,22 +160,17 @@ oml.ui.viewer = function() { var $annotation = oml.$ui.annotationFolder.find('.OMLAnnotation.selected') $annotation.length && $annotation.deselect() } + oml.$ui.annotationPanel.updateSelection(false) } else if (event == 'selectText') { oml.$ui.annotationPanel.updateSelection(data) } else { - console.log('got', event, data) + console.log('trigger unknwon event', event, data) that.triggerEvent(event, data); } }).bindEvent({ init: function() { loadAnnotations(function(annotations) { that.renderAnnotations() - // fixme: trigger loaded event from reader instead? - setTimeout(function() { - that.postMessage('addAnnotations', { - annotations: annotations - }) - }, 500) }) } }).appendTo(frame); @@ -143,12 +189,34 @@ oml.ui.viewer = function() { return annotations; } that.renderAnnotations = function() { + var sortKey = ui.sortAnnotations + if (sortKey == 'date') { + sortKey = 'created' + } + if (sortKey == 'date') { + sortKey = 'created' + } + if (sortKey == 'quote') { + sortKey = 'text' + } + annotations = Ox.sortBy(annotations, sortKey) oml.$ui.annotationFolder.empty(); - Ox.sortBy(annotations, ui.sortAnnotations); + var visibleAnnotations = []; annotations.forEach(function(data) { - var $annotation = oml.ui.annotation(data, $iframe).bindEvent(annotationEvents) - oml.$ui.annotationFolder.append($annotation); + //that.postMessage('removeAnnotation', {id: data.id}) + if (ui.showAnnotationUsers == 'all' || data.user == oml.user.id) { + var $annotation = oml.ui.annotation(data, $iframe).bindEvent(annotationEvents) + oml.$ui.annotationFolder.append($annotation); + visibleAnnotations.push(data) + } }) + // fixme: trigger loaded event from reader instead? + setTimeout(function() { + that.postMessage('addAnnotations', { + annotations: visibleAnnotations, + replace: true + }) + }, 500) } return that.updateElement(); }; diff --git a/static/reader/epub.js b/static/reader/epub.js index 9aca10f..57fd996 100644 --- a/static/reader/epub.js +++ b/static/reader/epub.js @@ -4,6 +4,7 @@ var id = document.location.pathname.split('/')[1]; var annotations = []; var currentSelection; var fontSize = parseInt(localStorage.epubFontSize || '100', 10) +var justSelected = false; Ox.load({ 'UI': { @@ -21,6 +22,12 @@ Ox.load({ } else if (event == 'addAnnotation') { createAnnotation() } else if (event == 'addAnnotations') { + if (data.replace) { + annotations.forEach(function(a) { + reader.rendition.annotations.remove(a.cfiRange) + }) + annotations = [] + } data.annotations.forEach(function(annotation) { annotations.push(annotation) renderAnnotation(annotation) @@ -50,6 +57,7 @@ function createAnnotation() { document.querySelectorAll('.epubjs-hl.selected').forEach(function(other) { other.classList.remove('selected') }) + console.log('create annot') currentSelection = null } } @@ -144,6 +152,8 @@ document.onreadystatechange = function () { } if (event.key == 'n' || event.keyCode == 13) { var selected = document.querySelector('.epubjs-hl.selected') + console.log('!!', currentSelection, selected) + if (currentSelection) { if (selected) { deselectAllAnnotations() @@ -151,7 +161,6 @@ document.onreadystatechange = function () { createAnnotation() } else if (selected) { console.log('editNote?', selected.dataset.id) - } } if (event.keyCode == 61 && event.shiftKey) { @@ -171,7 +180,7 @@ document.onreadystatechange = function () { event.preventDefault() } }).on('mouseup', function(event) { - if (currentSelection) { + if (!justSelected) { var selection = window.getSelection() if (selection.isCollapsed) { currentSelection = null @@ -180,15 +189,20 @@ document.onreadystatechange = function () { Ox.$parent.postMessage('selectText', false) } } + deselectAllAnnotations() + justSelected = false }) rendition.on("mark", function(cfiRange, contents) { console.log('!! mark', cfiRange) }) rendition.on("selected", function(cfiRange, contents) { + justSelected = true getText(book, cfiRange, function(text) { + var position = cfiRange; currentSelection = { id: Ox.SHA1(cfiRange), cfiRange: cfiRange, + position: position, text: text, contents: contents } diff --git a/static/reader/pdf.js b/static/reader/pdf.js index 1e4b003..0a0b50b 100644 --- a/static/reader/pdf.js +++ b/static/reader/pdf.js @@ -8,7 +8,6 @@ Ox.load({ } }, function() { Ox.$parent.bindMessage(function(data, event) { - console.log('got', event, 'data', data) if (event == 'selectAnnotation') { var annotation = annotations.filter(function(a) { return a.id == data.id })[0] var delay = 0 @@ -20,7 +19,7 @@ Ox.load({ PDFViewerApplication.pdfViewer.currentPageNumber = annotation.page; delay = 250 } - setTimeout(function() { + annotation && setTimeout(function() { var el = document.querySelector('.a' + annotation.id); if (el && !isInView(el)) { document.querySelector('#viewerContainer').scrollTop = el.offsetTop + el.parentElement.offsetTop - 64; @@ -30,12 +29,20 @@ Ox.load({ } else if (event == 'addAnnotation') { createAnnotation() } else if (event == 'addAnnotations') { + if (data.replace) { + document.querySelectorAll('.oml-annotation').forEach(function(a) { + a.remove() + }) + annotations = [] + } data.annotations.forEach(function(annotation) { annotations.push(annotation) renderAnnotation(annotation) }) } else if (event == 'removeAnnotation') { removeAnnotation(data.id) + } else { + console.log('got', event, 'data', data) } }) }) @@ -47,14 +54,16 @@ window.addEventListener('keydown', function(event) { removeAnnotation(selected.dataset.id) } } else if (event.key == 'n' || event.keyCode == 13) { - var selected = document.querySelector('.oml-annotation.selected') - if (!window.getSelection().isCollapsed) { - createAnnotation() - } else if (selected) { - console.log('editNote?', selected.dataset.id) + if (event.target.nodeName != 'INPUT') { + var selected = document.querySelector('.oml-annotation.selected') + if (!window.getSelection().isCollapsed) { + createAnnotation() + } else if (selected) { + console.log('editNote?', selected.dataset.id) + } + event.stopPropagation() + event.preventDefault() } - event.stopPropagation() - event.preventDefault() } }) window.addEventListener('mouseup', function(event) { @@ -91,8 +100,11 @@ function getHighlight() { viewport.convertToPdfPoint(r.right - pageRect.x, r.bottom - pageRect.y)); }); var text = selection.toString(); + var position = [pageNumber].concat(Ox.sort(selected.map(function(c) { return [c[1], c[0]]}))[0]); return { page: pageNumber, + pageLabel: PDFViewerApplication.pdfViewer.currentPageLabel, + position: position, coords: selected, text: text, id: Ox.SHA1(pageNumber.toString() + JSON.stringify(selected)) @@ -172,7 +184,7 @@ function deselectAllAnnotations() { g.classList.remove('selected') g.style.backgroundColor = 'yellow' var id = $(g).parents('.oml-annotation').data('id') - console.log('deselect', g, id) + //console.log('deselect', g, id) if (!Ox.contains(ids, id)) { ids.push(id) Ox.$parent.postMessage('selectAnnotation', {id: null})