Compare commits

...

9 Commits

Author SHA1 Message Date
j e0cba14d6a store annotations in db and sync with peers 2019-02-10 17:58:05 +05:30
j 131a6a3215 fix enter event 2019-02-10 17:49:15 +05:30
j dad9b53b54 hide peers 2019-02-10 17:49:15 +05:30
j ad2d763fbb more logging 2019-02-10 17:49:15 +05:30
j 497c2bb6be add position 2019-02-10 17:49:15 +05:30
j 81e943625d deselect annotation 2019-02-10 17:49:15 +05:30
j 46e679f2b9 cleanup 2019-02-10 17:49:15 +05:30
j 925967ddf0 add delete quote button 2019-02-10 17:49:15 +05:30
j 0c2ad46e71 limit logging 2019-02-10 17:49:15 +05:30
24 changed files with 550 additions and 78 deletions

View File

@ -241,6 +241,18 @@
"format": {"type": "boolean", "args": []}, "format": {"type": "boolean", "args": []},
"sort": true "sort": true
}, },
{
"id": "quotes",
"title": "Quotes",
"find": true,
"type": "text"
},
{
"id": "notes",
"title": "Notes",
"find": true,
"type": "text"
},
{ {
"id": "fulltext", "id": "fulltext",
"title": "Full Text", "title": "Full Text",
@ -368,6 +380,7 @@
"showFilters": true, "showFilters": true,
"showIconInfo": true, "showIconInfo": true,
"showInfo": true, "showInfo": true,
"showPeers": true,
"showSection": { "showSection": {
"notifications": { "notifications": {
"received": true, "received": true,

4
ctl
View File

@ -180,14 +180,10 @@ if [ "$1" == "open" ]; then
if ps -p `cat "$PID"` > /dev/null; then if ps -p `cat "$PID"` > /dev/null; then
open_linux open_linux
else else
#$PYTHON "${NAME}/oml/gtkstatus.py" $@
#exit $?
open_linux open_linux
"$0" start & "$0" start &
fi fi
else else
#$PYTHON "$NAME/oml/gtkstatus.py" $@
#exit $?
open_linux open_linux
"$0" start & "$0" start &
fi fi

48
oml/annotation/api.py Normal file
View File

@ -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)

124
oml/annotation/models.py Normal file
View File

@ -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)

View File

@ -12,6 +12,7 @@ from oxtornado import actions
import item.api import item.api
import user.api import user.api
import annotation.api
import update import update
import utils import utils

View File

@ -1,8 +1,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import os
from datetime import datetime from datetime import datetime
import json import json
import os
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy.sql.expression import text from sqlalchemy.sql.expression import text
@ -76,6 +76,10 @@ class Changelog(db.Model):
addpeer peerid peername addpeer peerid peername
removepeer peerid peername removepeer peerid peername
editpeer peerid {username: string, contact: string} editpeer peerid {username: string, contact: string}
addannotation itemid data
editannotation itemid annotationid data
removeannotation itemid annotationid
''' '''
__tablename__ = 'changelog' __tablename__ = 'changelog'
id = sa.Column(sa.Integer(), primary_key=True) id = sa.Column(sa.Integer(), primary_key=True)
@ -327,6 +331,27 @@ class Changelog(db.Model):
peer.save() peer.save()
return True 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 @classmethod
def aggregated_changes(cls, since=None, user_id=None): def aggregated_changes(cls, since=None, user_id=None):
from item.models import Item from item.models import Item

View File

@ -190,6 +190,33 @@ class Peer(object):
self.info['username'] = args[0] self.info['username'] = args[0]
elif action == 'editcontact': elif action == 'editcontact':
self.info['contact'] = args[0] 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: else:
logger.debug('UNKNOWN ACTION:', action) logger.debug('UNKNOWN ACTION:', action)
self.info['revision'] = revision self.info['revision'] = revision

View File

@ -152,8 +152,8 @@ class LocalNodes(dict):
if state.tasks: if state.tasks:
state.tasks.queue('removelocalinfo', id) state.tasks.queue('removelocalinfo', id)
def get(self, user_id): def get_data(self, user_id):
data = super().get(user_id) data = self.get(user_id)
if data and can_connect(data): if data and can_connect(data):
return data return data
return None return None

View File

@ -124,9 +124,12 @@ class Node(Thread):
self.local = None self.local = None
self.port = 9851 self.port = 9851
def is_local(self):
return self._nodes and self.user_id in self._nodes.local
def get_local(self): def get_local(self):
if self._nodes and self._nodes.local: 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 return None
def request(self, action, *args): def request(self, action, *args):
@ -216,7 +219,7 @@ class Node(Thread):
return False return False
def is_online(self): 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): def send_response(self):
self._q.put('send_response') self._q.put('send_response')
@ -523,7 +526,8 @@ class Nodes(Thread):
while not state.shutdown: while not state.shutdown:
args = self._q.get() args = self._q.get()
if args: 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': if args[0] == 'add':
self._add(*args[1:]) self._add(*args[1:])
elif args[0] == 'pull': elif args[0] == 'pull':
@ -532,7 +536,7 @@ class Nodes(Thread):
self._call(*args) self._call(*args)
def queue(self, *args): def queue(self, *args):
if args: if args and DEBUG_NODES:
logger.debug('queue "%s", %s entries in queue', args, self._q.qsize()) logger.debug('queue "%s", %s entries in queue', args, self._q.qsize())
self._q.put(list(args)) self._q.put(list(args))

View File

@ -116,7 +116,18 @@ class Parser(object):
elif k == 'fulltext': elif k == 'fulltext':
ids = find_fulltext(v) ids = find_fulltext(v)
return self.in_ids(ids, exclude) 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"): elif key_type in ("string", "text"):
if isinstance(v, str): if isinstance(v, str):
v = unicodedata.normalize('NFKD', v).lower() v = unicodedata.normalize('NFKD', v).lower()

View File

@ -170,8 +170,8 @@ def run():
import bandwidth import bandwidth
state.bandwidth = bandwidth.Bandwidth() state.bandwidth = bandwidth.Bandwidth()
state.tor = tor.Tor() state.tor = tor.Tor()
state.node = node.server.start()
state.nodes = nodes.Nodes() state.nodes = nodes.Nodes()
state.node = node.server.start()
def publish(): def publish():
if not state.tor.is_online(): if not state.tor.is_online():

View File

@ -82,7 +82,7 @@ if 'modules' in release and 'openmedialibrary' in release['modules']:
else: else:
MINOR_VERSION = 'git' MINOR_VERSION = 'git'
NODE_PROTOCOL = "0.8" NODE_PROTOCOL = "0.9"
VERSION = "%s.%s" % (NODE_PROTOCOL, MINOR_VERSION) VERSION = "%s.%s" % (NODE_PROTOCOL, MINOR_VERSION)
USER_AGENT = 'OpenMediaLibrary/%s' % VERSION USER_AGENT = 'OpenMediaLibrary/%s' % VERSION
@ -95,4 +95,4 @@ FULLTEXT_SUPPORT = fulltext.platform_supported()
if not FULLTEXT_SUPPORT: if not FULLTEXT_SUPPORT:
config['itemKeys'] = [k for k in config['itemKeys'] if k['id'] != 'fulltext'] config['itemKeys'] = [k for k in config['itemKeys'] if k['id'] != 'fulltext']
DB_VERSION = 17 DB_VERSION = 18

View File

@ -151,6 +151,22 @@ CREATE TABLE listitem (
FOREIGN KEY(item_id) REFERENCES item (id), FOREIGN KEY(item_id) REFERENCES item (id),
FOREIGN KEY(list_id) REFERENCES list (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 PRAGMA journal_mode=WAL
''' '''
for statement in sql.split(';'): for statement in sql.split(';'):

View File

@ -377,6 +377,8 @@ class Update(Thread):
db_version = migrate_16() db_version = migrate_16()
if db_version < 17: if db_version < 17:
db_version = migrate_17() db_version = migrate_17()
if db_version < 18:
db_version = migrate_18()
settings.server['db_version'] = db_version settings.server['db_version'] = db_version
def run(self): def run(self):
@ -674,3 +676,25 @@ def migrate_17():
lists.append(l.name) lists.append(l.name)
add_record('orderlists', lists) add_record('orderlists', lists)
return 17 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

View File

@ -474,7 +474,7 @@ def removePeering(data):
if len(data.get('id', '')) not in (16, 43): if len(data.get('id', '')) not in (16, 43):
logger.debug('invalid user id') logger.debug('invalid user id')
return {} return {}
u = models.User.get(data['id'], for_udpate=True) u = models.User.get(data['id'], for_update=True)
if u: if u:
u.info['message'] = data.get('message', '') u.info['message'] = data.get('message', '')
u.update_peering(False) u.update_peering(False)

View File

@ -187,7 +187,9 @@ class User(db.Model):
def cleanup(self): def cleanup(self):
from item.models import user_items, Item from item.models import user_items, Item
from annotation.models import Annotation
List.query.filter_by(user_id=self.id).delete() 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'] c_user_id = user_items.columns['user_id']
q = user_items.delete().where(c_user_id.is_(self.id)) q = user_items.delete().where(c_user_id.is_(self.id))
state.db.session.execute(q) state.db.session.execute(q)
@ -197,6 +199,7 @@ class User(db.Model):
state.peers[self.id].remove() state.peers[self.id].remove()
del state.peers[self.id] del state.peers[self.id]
def update_name(self): def update_name(self):
if self.id == settings.USER_ID: if self.id == settings.USER_ID:
name = settings.preferences.get('username', 'anonymous') name = settings.preferences.get('username', 'anonymous')

View File

@ -6,9 +6,16 @@ oml.ui.annotation = function(annotation, $iframe) {
.html(Ox.encodeHTMLEntities(annotation.text).replace(/\n/g, '<br/>')) .html(Ox.encodeHTMLEntities(annotation.text).replace(/\n/g, '<br/>'))
.on({ .on({
click: function(event) { click: function(event) {
that.select() var id
if (event.ctrlKey) {
that.deselect()
id = null
} else {
that.select()
id = annotation.id
}
$iframe.postMessage('selectAnnotation', { $iframe.postMessage('selectAnnotation', {
id: annotation.id id: id
}) })
} }
}); });
@ -39,14 +46,22 @@ oml.ui.annotation = function(annotation, $iframe) {
note.value = data.value note.value = data.value
note.modified = (new Date).toISOString() note.modified = (new Date).toISOString()
} else { } else {
annotation.notes.push({ annotation.notes.push(note = {
created: data.created || (new Date).toISOString(), created: data.created || (new Date).toISOString(),
modified: (new Date).toISOString(), modified: (new Date).toISOString(),
id: data.id, id: data.id,
user: '',
value: data.value 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') that.triggerEvent('change')
} }
}); });
@ -82,6 +97,11 @@ oml.ui.annotation = function(annotation, $iframe) {
addNote() addNote()
} }
} }
that.delete = function() {
that.triggerEvent('delete', {
id: annotation.id
})
}
that.deselect = function() { that.deselect = function() {
that.removeClass('selected') that.removeClass('selected')
that.loseFocus() that.loseFocus()
@ -94,7 +114,8 @@ oml.ui.annotation = function(annotation, $iframe) {
selected && selected.classList.remove('selected') selected && selected.classList.remove('selected')
that.addClass('selected') that.addClass('selected')
that.gainFocus() that.gainFocus()
that[0].scrollIntoViewIfNeeded() oml.$ui.annotationPanel.updateSelection(false)
that[0].scrollIntoViewIfNeeded && that[0].scrollIntoViewIfNeeded()
} }
return that; return that;
}; };

View File

@ -1,12 +1,13 @@
'use strict'; 'use strict';
oml.ui.annotationPanel = function() { oml.ui.annotationPanel = function() {
var ui = oml.user.ui;
var ui = oml.user.ui; var ui = oml.user.ui;
var $bar = Ox.Bar({size: 16}); var $bar = Ox.Bar({size: 16});
var $button = Ox.Button({ var $addQuote = Ox.Button({
disabled: true, disabled: true,
style: 'symbol', style: 'symbol',
title: 'add', title: 'add',
@ -18,6 +19,20 @@ oml.ui.annotationPanel = function() {
} }
}).appendTo($bar); }).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({ var $menuButton = Ox.MenuButton({
items: [ items: [
{id: 'addAnnotation', title: 'Add Annotation', disabled: true}, {id: 'addAnnotation', title: 'Add Annotation', disabled: true},
@ -67,6 +82,21 @@ oml.ui.annotationPanel = function() {
}, function(result) { }, function(result) {
oml.ui.exportAnnotationsDialog(result.data).open() 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); }).appendTo($bar);
@ -86,9 +116,13 @@ oml.ui.annotationPanel = function() {
}); });
that.updateSelection = function(selection) { that.updateSelection = function(selection) {
$button.options({ $addQuote.options({
disabled: !selection disabled: !selection
}) })
var $annotation = oml.$ui.annotationFolder.find('.OMLAnnotation.selected')
$deleteQuote.options({
disabled: !$annotation.length
})
} }
return that; return that;

View File

@ -47,7 +47,8 @@ oml.ui.exportAnnotationsDialog = function(data) {
var annotations = oml.$ui.viewer.getAnnotations() var annotations = oml.$ui.viewer.getAnnotations()
var text = 'Annotations for ' + data.title + ' (' + data.author.join(', ') + ')\n\n\n\n' var text = 'Annotations for ' + data.title + ' (' + data.author.join(', ') + ')\n\n\n\n'
text += annotations.map(function(annotation) { 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) { if (annotation.notes.length) {
text += '\n\nNotes:\n' + annotation.notes.map(function(note) { text += '\n\nNotes:\n' + annotation.notes.map(function(note) {
return note.value return note.value

View File

@ -20,6 +20,9 @@ oml.ui.folders = function() {
oml_showfolder: function() { oml_showfolder: function() {
oml.resizeListFolders(); oml.resizeListFolders();
}, },
oml_showpeers: function() {
that.updateElement();
},
oml_showinfo: function() { oml_showinfo: function() {
oml.resizeListFolders(); oml.resizeListFolders();
} }
@ -48,9 +51,15 @@ oml.ui.folders = function() {
function getUsersAndLists(callback) { function getUsersAndLists(callback) {
oml.getUsers(function(users_) { oml.getUsers(function(users_) {
users = users_.filter(function(user) { if (ui.showPeers) {
return user.id == oml.user.id || user.peered; 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) { oml.getLists(function(lists) {
callback(users, lists); callback(users, lists);
}); });

View File

@ -214,6 +214,12 @@ oml.ui.mainMenu = function() {
title: Ox._((ui.showSidebar ? 'Hide' : 'Show') + ' Sidebar'), title: Ox._((ui.showSidebar ? 'Hide' : 'Show') + ' Sidebar'),
keyboard: 'shift s' keyboard: 'shift s'
}, },
{
id: 'showpeers',
title: Ox._((ui.showPeers ? 'Hide' : 'Show') + ' Peers'),
keyboard: 'shift p',
disabled: !ui.showSidebar
},
{ {
id: 'showinfo', id: 'showinfo',
title: Ox._((ui.showInfo ? 'Hide' : 'Show') + ' Info'), title: Ox._((ui.showInfo ? 'Hide' : 'Show') + ' Info'),
@ -539,6 +545,8 @@ oml.ui.mainMenu = function() {
oml.history.clear(); oml.history.clear();
} else if (id == 'showsidebar') { } else if (id == 'showsidebar') {
oml.UI.set({showSidebar: !ui.showSidebar}); oml.UI.set({showSidebar: !ui.showSidebar});
} else if (id == 'showpeers') {
oml.UI.set({showPeers: !ui.showPeers});
} else if (id == 'showinfo') { } else if (id == 'showinfo') {
oml.UI.set({showInfo: !ui.showInfo}); oml.UI.set({showInfo: !ui.showInfo});
} else if (id == 'showfilters') { } else if (id == 'showfilters') {
@ -636,12 +644,16 @@ oml.ui.mainMenu = function() {
that[action]('viewMenu_iconsSubmenu_extension'); that[action]('viewMenu_iconsSubmenu_extension');
that[action]('viewMenu_iconsSubmenu_size'); that[action]('viewMenu_iconsSubmenu_size');
}, },
oml_showpeers: function(data) {
that.setItemTitle('showpeers', Ox._((data.value ? 'Hide' : 'Show') + ' Peers'));
},
oml_showinfo: function(data) { oml_showinfo: function(data) {
that.setItemTitle('showinfo', Ox._((data.value ? 'Hide' : 'Show') + ' Info')); that.setItemTitle('showinfo', Ox._((data.value ? 'Hide' : 'Show') + ' Info'));
}, },
oml_showsidebar: function(data) { oml_showsidebar: function(data) {
that.setItemTitle('showsidebar', Ox._((data.value ? 'Hide' : 'Show') + ' Sidebar')); that.setItemTitle('showsidebar', Ox._((data.value ? 'Hide' : 'Show') + ' Sidebar'));
that[data.value ? 'enableItem' : 'disableItem']('showinfo'); that[data.value ? 'enableItem' : 'disableItem']('showinfo');
that[data.value ? 'enableItem' : 'disableItem']('showpeers');
}, },
}); });
Ox.Event.bind({ Ox.Event.bind({
@ -726,8 +738,17 @@ oml.ui.mainMenu = function() {
key_shift_f: function() { key_shift_f: function() {
!ui.item && oml.UI.set({showFilters: !ui.showFilters}); !ui.item && oml.UI.set({showFilters: !ui.showFilters});
}, },
key_shift_i: function() { key_shift_p: function(event, name, target) {
ui.showSidebar && oml.UI.set({showInfo: !ui.showInfo}); // 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() { key_shift_s: function() {
oml.UI.set({showSidebar: !ui.showSidebar}); oml.UI.set({showSidebar: !ui.showSidebar});

View File

@ -15,6 +15,12 @@ oml.ui.viewer = function() {
}, },
oml_showannotations: function() { oml_showannotations: function() {
panel.toggleElement(1); panel.toggleElement(1);
},
oml_sortannotations: function(data) {
that.renderAnnotations()
},
oml_annotationusers: function(data) {
that.renderAnnotations()
} }
}), }),
panel = Ox.SplitPanel({ panel = Ox.SplitPanel({
@ -40,38 +46,81 @@ oml.ui.viewer = function() {
$iframe, item; $iframe, item;
function loadAnnotations(callback) { function loadAnnotations(callback) {
annotations = JSON.parse(localStorage[item + '.annotations'] || '[]') if (localStorage[item + '.annotations']) {
annotations.forEach(function(data) { annotations = JSON.parse(localStorage[item + '.annotations'] || '[]')
if (data.comments && !data.notes) { var ids = []
data.notes = data.comments annotations.forEach(function(data) {
delete data.comments if (data.comments && !data.notes) {
} data.notes = data.comments
data.notes = data.notes || []; delete data.comments
}) }
callback && callback(annotations) if (!Ox.contains(ids, data.id)) {
} ids.push(data.id)
function saveAnnotations(data) { var note
if (data) { if (data.notes && data.notes.length) {
data.created = data.created || (new Date).toISOString(); note = data.notes[0]
if (data.comments && !data.notes) { delete data.notes
data.notes = data.comments }
delete data.comments addAnnotation(data, false)
} if (note) {
data.notes = data.notes || []; data.notes = [note]
annotations.push(data); } 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) { function removeAnnotation(id) {
annotations = annotations.filter(function(annotation) { oml.api.removeAnnotation({
return annotation.id != id item: ui.item,
annotation: id
}, function(result) {
annotations = annotations.filter(function(annotation) {
return annotation.id != id
})
}) })
saveAnnotations()
} }
var annotationEvents = { var annotationEvents = {
change: function() { change: function(data) {
saveAnnotations() // console.log('change', data)
}, },
'delete': function(data) { 'delete': function(data) {
oml.$ui.annotationFolder.find('#a-' + data.id).remove() oml.$ui.annotationFolder.find('#a-' + data.id).remove()
@ -93,14 +142,16 @@ oml.ui.viewer = function() {
height: '100%', height: '100%',
border: 0 border: 0
}).onMessage(function(data, event) { }).onMessage(function(data, event) {
console.log('got', event, data)
if (event == 'addAnnotation') { if (event == 'addAnnotation') {
saveAnnotations(data); addAnnotation(data);
var $annotation = oml.ui.annotation(data, $iframe).bindEvent(annotationEvents) var $annotation = oml.ui.annotation(data, $iframe).bindEvent(annotationEvents)
oml.$ui.annotationFolder.append($annotation); oml.$ui.annotationFolder.append($annotation);
$annotation.annotate(); $annotation.annotate();
oml.$ui.annotationPanel.updateSelection(false)
} else if (event == 'removeAnnotation') { } else if (event == 'removeAnnotation') {
oml.$ui.annotationFolder.find('#a-' + data.id).remove() oml.$ui.annotationFolder.find('#a-' + data.id).remove()
removeAnnotation(data.id) data.id && removeAnnotation(data.id)
} else if (event == 'selectAnnotation') { } else if (event == 'selectAnnotation') {
if (data.id) { if (data.id) {
var $annotation = oml.$ui.annotationFolder.find('#a-' + 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') var $annotation = oml.$ui.annotationFolder.find('.OMLAnnotation.selected')
$annotation.length && $annotation.deselect() $annotation.length && $annotation.deselect()
} }
oml.$ui.annotationPanel.updateSelection(false)
} else if (event == 'selectText') { } else if (event == 'selectText') {
oml.$ui.annotationPanel.updateSelection(data) oml.$ui.annotationPanel.updateSelection(data)
} else { } else {
console.log('got', event, data) console.log('trigger unknwon event', event, data)
that.triggerEvent(event, data); that.triggerEvent(event, data);
} }
}).bindEvent({ }).bindEvent({
init: function() { init: function() {
loadAnnotations(function(annotations) { loadAnnotations(function(annotations) {
that.renderAnnotations() that.renderAnnotations()
// fixme: trigger loaded event from reader instead?
setTimeout(function() {
that.postMessage('addAnnotations', {
annotations: annotations
})
}, 500)
}) })
} }
}).appendTo(frame); }).appendTo(frame);
@ -143,12 +189,34 @@ oml.ui.viewer = function() {
return annotations; return annotations;
} }
that.renderAnnotations = function() { 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(); oml.$ui.annotationFolder.empty();
Ox.sortBy(annotations, ui.sortAnnotations); var visibleAnnotations = [];
annotations.forEach(function(data) { annotations.forEach(function(data) {
var $annotation = oml.ui.annotation(data, $iframe).bindEvent(annotationEvents) //that.postMessage('removeAnnotation', {id: data.id})
oml.$ui.annotationFolder.append($annotation); 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(); return that.updateElement();
}; };

View File

@ -4,6 +4,7 @@ var id = document.location.pathname.split('/')[1];
var annotations = []; var annotations = [];
var currentSelection; var currentSelection;
var fontSize = parseInt(localStorage.epubFontSize || '100', 10) var fontSize = parseInt(localStorage.epubFontSize || '100', 10)
var justSelected = false;
Ox.load({ Ox.load({
'UI': { 'UI': {
@ -21,6 +22,12 @@ Ox.load({
} else if (event == 'addAnnotation') { } else if (event == 'addAnnotation') {
createAnnotation() createAnnotation()
} else if (event == 'addAnnotations') { } else if (event == 'addAnnotations') {
if (data.replace) {
annotations.forEach(function(a) {
reader.rendition.annotations.remove(a.cfiRange)
})
annotations = []
}
data.annotations.forEach(function(annotation) { data.annotations.forEach(function(annotation) {
annotations.push(annotation) annotations.push(annotation)
renderAnnotation(annotation) renderAnnotation(annotation)
@ -50,6 +57,7 @@ function createAnnotation() {
document.querySelectorAll('.epubjs-hl.selected').forEach(function(other) { document.querySelectorAll('.epubjs-hl.selected').forEach(function(other) {
other.classList.remove('selected') other.classList.remove('selected')
}) })
console.log('create annot')
currentSelection = null currentSelection = null
} }
} }
@ -144,6 +152,8 @@ document.onreadystatechange = function () {
} }
if (event.key == 'n' || event.keyCode == 13) { if (event.key == 'n' || event.keyCode == 13) {
var selected = document.querySelector('.epubjs-hl.selected') var selected = document.querySelector('.epubjs-hl.selected')
console.log('!!', currentSelection, selected)
if (currentSelection) { if (currentSelection) {
if (selected) { if (selected) {
deselectAllAnnotations() deselectAllAnnotations()
@ -151,7 +161,6 @@ document.onreadystatechange = function () {
createAnnotation() createAnnotation()
} else if (selected) { } else if (selected) {
console.log('editNote?', selected.dataset.id) console.log('editNote?', selected.dataset.id)
} }
} }
if (event.keyCode == 61 && event.shiftKey) { if (event.keyCode == 61 && event.shiftKey) {
@ -171,7 +180,7 @@ document.onreadystatechange = function () {
event.preventDefault() event.preventDefault()
} }
}).on('mouseup', function(event) { }).on('mouseup', function(event) {
if (currentSelection) { if (!justSelected) {
var selection = window.getSelection() var selection = window.getSelection()
if (selection.isCollapsed) { if (selection.isCollapsed) {
currentSelection = null currentSelection = null
@ -180,15 +189,20 @@ document.onreadystatechange = function () {
Ox.$parent.postMessage('selectText', false) Ox.$parent.postMessage('selectText', false)
} }
} }
deselectAllAnnotations()
justSelected = false
}) })
rendition.on("mark", function(cfiRange, contents) { rendition.on("mark", function(cfiRange, contents) {
console.log('!! mark', cfiRange) console.log('!! mark', cfiRange)
}) })
rendition.on("selected", function(cfiRange, contents) { rendition.on("selected", function(cfiRange, contents) {
justSelected = true
getText(book, cfiRange, function(text) { getText(book, cfiRange, function(text) {
var position = cfiRange;
currentSelection = { currentSelection = {
id: Ox.SHA1(cfiRange), id: Ox.SHA1(cfiRange),
cfiRange: cfiRange, cfiRange: cfiRange,
position: position,
text: text, text: text,
contents: contents contents: contents
} }

View File

@ -8,7 +8,6 @@ Ox.load({
} }
}, function() { }, function() {
Ox.$parent.bindMessage(function(data, event) { Ox.$parent.bindMessage(function(data, event) {
console.log('got', event, 'data', data)
if (event == 'selectAnnotation') { if (event == 'selectAnnotation') {
var annotation = annotations.filter(function(a) { return a.id == data.id })[0] var annotation = annotations.filter(function(a) { return a.id == data.id })[0]
var delay = 0 var delay = 0
@ -20,7 +19,7 @@ Ox.load({
PDFViewerApplication.pdfViewer.currentPageNumber = annotation.page; PDFViewerApplication.pdfViewer.currentPageNumber = annotation.page;
delay = 250 delay = 250
} }
setTimeout(function() { annotation && setTimeout(function() {
var el = document.querySelector('.a' + annotation.id); var el = document.querySelector('.a' + annotation.id);
if (el && !isInView(el)) { if (el && !isInView(el)) {
document.querySelector('#viewerContainer').scrollTop = el.offsetTop + el.parentElement.offsetTop - 64; document.querySelector('#viewerContainer').scrollTop = el.offsetTop + el.parentElement.offsetTop - 64;
@ -30,12 +29,20 @@ Ox.load({
} else if (event == 'addAnnotation') { } else if (event == 'addAnnotation') {
createAnnotation() createAnnotation()
} else if (event == 'addAnnotations') { } else if (event == 'addAnnotations') {
if (data.replace) {
document.querySelectorAll('.oml-annotation').forEach(function(a) {
a.remove()
})
annotations = []
}
data.annotations.forEach(function(annotation) { data.annotations.forEach(function(annotation) {
annotations.push(annotation) annotations.push(annotation)
renderAnnotation(annotation) renderAnnotation(annotation)
}) })
} else if (event == 'removeAnnotation') { } else if (event == 'removeAnnotation') {
removeAnnotation(data.id) removeAnnotation(data.id)
} else {
console.log('got', event, 'data', data)
} }
}) })
}) })
@ -47,14 +54,16 @@ window.addEventListener('keydown', function(event) {
removeAnnotation(selected.dataset.id) removeAnnotation(selected.dataset.id)
} }
} else if (event.key == 'n' || event.keyCode == 13) { } else if (event.key == 'n' || event.keyCode == 13) {
var selected = document.querySelector('.oml-annotation.selected') if (event.target.nodeName != 'INPUT') {
if (!window.getSelection().isCollapsed) { var selected = document.querySelector('.oml-annotation.selected')
createAnnotation() if (!window.getSelection().isCollapsed) {
} else if (selected) { createAnnotation()
console.log('editNote?', selected.dataset.id) } else if (selected) {
console.log('editNote?', selected.dataset.id)
}
event.stopPropagation()
event.preventDefault()
} }
event.stopPropagation()
event.preventDefault()
} }
}) })
window.addEventListener('mouseup', function(event) { window.addEventListener('mouseup', function(event) {
@ -91,8 +100,11 @@ function getHighlight() {
viewport.convertToPdfPoint(r.right - pageRect.x, r.bottom - pageRect.y)); viewport.convertToPdfPoint(r.right - pageRect.x, r.bottom - pageRect.y));
}); });
var text = selection.toString(); var text = selection.toString();
var position = [pageNumber].concat(Ox.sort(selected.map(function(c) { return [c[1], c[0]]}))[0]);
return { return {
page: pageNumber, page: pageNumber,
pageLabel: PDFViewerApplication.pdfViewer.currentPageLabel,
position: position,
coords: selected, coords: selected,
text: text, text: text,
id: Ox.SHA1(pageNumber.toString() + JSON.stringify(selected)) id: Ox.SHA1(pageNumber.toString() + JSON.stringify(selected))
@ -172,7 +184,7 @@ function deselectAllAnnotations() {
g.classList.remove('selected') g.classList.remove('selected')
g.style.backgroundColor = 'yellow' g.style.backgroundColor = 'yellow'
var id = $(g).parents('.oml-annotation').data('id') var id = $(g).parents('.oml-annotation').data('id')
console.log('deselect', g, id) //console.log('deselect', g, id)
if (!Ox.contains(ids, id)) { if (!Ox.contains(ids, id)) {
ids.push(id) ids.push(id)
Ox.$parent.postMessage('selectAnnotation', {id: null}) Ox.$parent.postMessage('selectAnnotation', {id: null})