diff --git a/oml/changelog.py b/oml/changelog.py index ab7cd2b..6a13f9b 100644 --- a/oml/changelog.py +++ b/oml/changelog.py @@ -20,8 +20,8 @@ class Changelog(db.Model): editlist name {name: newname} orderlists [name, name, name] removelist name - additemtolist listname itemid - removeitemfromlist listname itemid + addlistitems listname [ids] + removelistitems listname [ids] editusername username editcontact string addpeer peerid peername @@ -182,24 +182,17 @@ class Changelog(db.Model): l.remove() return True - def action_addlistitem(self, user, timestamp, name, itemid): - from item.models import Item + def action_addlistitems(self, user, timestamp, name, ids): from user.models import List - l = List.get(user.id, name) - i = Item.get(itemid) - if l and i: - i.lists.append(l) - i.update() + l = List.get_or_create(user.id, name) + l.add_items(ids) return True - def action_removelistitem(self, user, timestamp, name, itemid): - from item.models import Item + def action_removelistitem(self, user, timestamp, name, ids): from user.models import List l = List.get(user.id, name) - i = Item.get(itemid) - if l and i: - i.lists.remove(l) - i.update() + if l: + l.remove_items(ids) return True def action_editusername(self, user, timestamp, username): diff --git a/oml/item/api.py b/oml/item/api.py index 5bc498d..5e42e20 100644 --- a/oml/item/api.py +++ b/oml/item/api.py @@ -6,7 +6,6 @@ from flask import json from oxflask.api import actions from oxflask.shortcuts import returns_json -from oml import utils import query import models @@ -118,6 +117,8 @@ def edit(request): if item and keys and item.json()['mediastate'] == 'available': key = keys[0] print 'update mainid', key, data[key] + if key in ('isbn10', 'isbn13'): + data[key] = utils.normalize_isbn(data[key]) item.update_mainid(key, data[key]) response = item.json() else: diff --git a/oml/item/models.py b/oml/item/models.py index ce5e689..a53931f 100644 --- a/oml/item/models.py +++ b/oml/item/models.py @@ -233,6 +233,12 @@ class Item(db.Model): else: f.value = '%s:' % p.id db.session.add(f) + for l in self.lists: + f = Find() + f.item_id = self.id + f.key = 'list' + f.value = l.find_id + db.session.add(f) def update(self): users = map(str, list(self.users)) diff --git a/oml/localnodes.py b/oml/localnodes.py new file mode 100644 index 0000000..4d7553e --- /dev/null +++ b/oml/localnodes.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +# vi:si:et:sw=4:sts=4:ts=4 + +import socket +import thread +import json +import struct +from threading import Thread + +from settings import preferences, server, USER_ID +from node.utils import get_public_ipv6 + +def can_connect(data): + try: + s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) + s.settimeout(1) + s.connect((data['host'], data['port'])) + s.close() + return True + except: + pass + return False + +class LocalNodes(Thread): + _active = True + _nodes = {} + + _BROADCAST = "ff02::1" + _PORT = 9851 + TTL = 2 + + def __init__(self, app): + self._app = app + Thread.__init__(self) + self.daemon = True + self.start() + self.host = get_public_ipv6() + self.send() + + def send(self): + s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) + + ttl = struct.pack('@i', self.TTL) + s.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, ttl) + message = json.dumps({ + 'id': USER_ID, + 'username': preferences.get('username', 'anonymous'), + 'host': self.host, + 'port': server['node_port'], + }) + s.sendto(message + '\0', (self._BROADCAST, self._PORT)) + + def receive(self): + s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind(('', self._PORT)) + group_bin = socket.inet_pton(socket.AF_INET6, self._BROADCAST) + '\0'*4 + s.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, group_bin) + + while self._active: + data, addr = s.recvfrom(1024) + while data[-1] == '\0': + data = data[:-1] # Strip trailing \0's + data = self.validate(data) + if data: + if data['id'] not in self._nodes: + thread.start_new_thread(self.new_node, (data, )) + else: + print 'UPDATE NODE', data + self._nodes[data['id']] = data + + def validate(self, data): + try: + data = json.loads(data) + except: + return None + for key in ['id', 'username', 'host', 'port']: + if key not in data: + return None + return data + + def get(self, user_id): + if user_id in self._nodes: + if can_connect(self._nodes[user_id]): + return self._nodes[user_id] + + def new_node(self, data): + print 'NEW NODE', data + if can_connect(data): + self._nodes[data['id']] = data + with self._app.app_context(): + import user.models + u = user.models.User.get_or_create(data['id']) + u.info['username'] = data['username'] + u.info['local'] = data + u.save() + self.send() + + def run(self): + self.receive() + + def join(self): + self._active = False + return Thread.join(self) diff --git a/oml/node/server.py b/oml/node/server.py index 05e8296..5dc09a5 100644 --- a/oml/node/server.py +++ b/oml/node/server.py @@ -107,11 +107,8 @@ def start(app): (r"/get/(.*)", ShareHandler, dict(app=app)), (r".*", NodeHandler, dict(app=app)), ]) - - #tr = WSGIContainer(node_app) - #http_server= HTTPServer(tr) http_server.listen(settings.server['node_port'], settings.server['node_address']) - host = utils.get_public_ipv4() + host = utils.get_public_ipv6() state.online = directory.put(settings.sk, { 'host': host, 'port': settings.server['node_port'] diff --git a/oml/node/utils.py b/oml/node/utils.py index 712f401..c9d5e1b 100644 --- a/oml/node/utils.py +++ b/oml/node/utils.py @@ -1,6 +1,7 @@ +# -*- coding: utf-8 -*- +# vi:si:et:sw=4:sts=4:ts=4 + import socket -import requests -from urlparse import urlparse def get_public_ipv6(): host = ('2a01:4f8:120:3201::3', 25519) diff --git a/oml/nodes.py b/oml/nodes.py index 07ff7f4..b92d1d6 100644 --- a/oml/nodes.py +++ b/oml/nodes.py @@ -19,6 +19,7 @@ from changelog import Changelog import directory from websocket import trigger_event +from localnodes import LocalNodes ENCODING='base64' @@ -26,8 +27,9 @@ class Node(object): online = False download_speed = 0 - def __init__(self, app, user): - self._app = app + def __init__(self, nodes, user): + self._nodes = nodes + self._app = nodes._app self.user_id = user.id key = str(user.id) self.vk = ed25519.VerifyingKey(key, encoding=ENCODING) @@ -35,10 +37,15 @@ class Node(object): @property def url(self): - if ':' in self.host: - url = 'http://[%s]:%s' % (self.host, self.port) + local = self.get_local() + if local: + url = 'http://[%s]:%s' % (local['host'], local['port']) + print 'using local peer discovery to access node', url else: - url = 'http://%s:%s' % (self.host, self.port) + if ':' in self.host: + url = 'http://[%s]:%s' % (self.host, self.port) + else: + url = 'http://%s:%s' % (self.host, self.port) return url def resolve_host(self): @@ -51,6 +58,11 @@ class Node(object): self.host = None self.port = 9851 + def get_local(self): + if self._nodes and self._nodes._local: + return self._nodes._local.get(self.user_id) + return None + def request(self, action, *args): if not self.host: self.resolve_host() @@ -211,6 +223,7 @@ class Nodes(Thread): self._app = app self._q = Queue() self._running = True + self._local = LocalNodes(app) Thread.__init__(self) self.daemon = True self.start() @@ -238,7 +251,7 @@ class Nodes(Thread): def _add_node(self, user_id): if user_id not in self._nodes: from user.models import User - self._nodes[user_id] = Node(self._app, User.get_or_create(user_id)) + self._nodes[user_id] = Node(self, User.get_or_create(user_id)) else: self._nodes[user_id].online = True trigger_event('status', { diff --git a/oml/oxflask/query.py b/oml/oxflask/query.py index 77bdec2..97341a4 100644 --- a/oml/oxflask/query.py +++ b/oml/oxflask/query.py @@ -144,6 +144,8 @@ class Parser(object): l = self._list.query.filter_by(user_id=p.id, name=name).first() else: l = None + if l: + v = l.find_id if l and l._query: data = l._query q = self.parse_conditions(data.get('conditions', []), diff --git a/oml/settings.py b/oml/settings.py index d4b5305..944f75e 100644 --- a/oml/settings.py +++ b/oml/settings.py @@ -38,7 +38,7 @@ server_defaults = { 'port': 9842, 'address': '127.0.0.1', 'node_port': 9851, - 'node_address': '::', + 'node_address': '', 'extract_text': True, 'directory_service': 'http://[2a01:4f8:120:3201::3]:25519', 'lookup_service': 'http://data.openmedialibrary.com', diff --git a/oml/user/api.py b/oml/user/api.py index c702eaa..39049f4 100644 --- a/oml/user/api.py +++ b/oml/user/api.py @@ -84,9 +84,9 @@ actions.register(getUsers) @returns_json def getLists(request): - lists = {} + lists = [] for u in models.User.query.filter((models.User.peered==True)|(models.User.id==settings.USER_ID)): - lists[u.id] = u.lists_json() + lists += u.lists_json() return { 'lists': lists } @@ -132,28 +132,24 @@ def editList(request): actions.register(editList, cache=False) @returns_json -def addListItem(request): +def addListItems(request): data = json.loads(request.form['data']) if 'data' in request.form else {} - l = models.List.get_or_create(data['id']) - i = Item.get(data['item']) - if l and i: - l.items.append(i) - models.db.session.add(l) - i.update() + l = models.List.get_or_create(data['list']) + if l: + l.add_items(data['items']) + return l.json() return {} -actions.register(addListItem, cache=False) +actions.register(addListItems, cache=False) @returns_json -def removeListItem(request): +def removeListItems(request): data = json.loads(request.form['data']) if 'data' in request.form else {} - l = models.List.get(data['id']) - i = Item.get(data['item']) - if l and i: - l.items.remove(i) - models.db.session.add(l) - i.update() + l = models.List.get(data['list']) + if l: + l.remove_items(data['items']) + return l.json() return {} -actions.register(removeListItem, cache=False) +actions.register(removeListItems, cache=False) @returns_json def sortLists(request): diff --git a/oml/user/models.py b/oml/user/models.py index 9dafcf1..9ce7379 100644 --- a/oml/user/models.py +++ b/oml/user/models.py @@ -59,7 +59,7 @@ class User(db.Model): return j def check_online(self): - return state.nodes.check_online(self.id) + return state.nodes and state.nodes.check_online(self.id) def lists_json(self): return [l.json() for l in self.lists.order_by('position')] @@ -158,25 +158,39 @@ class List(db.Model): from item.models import Item for item_id in items: i = Item.get(item_id) - self.items.add(i) + self.items.append(i) + i.update() db.session.add(self) db.session.commit() + for item_id in items: + i = Item.get(item_id) + i.update_lists() + db.session.commit() + if self.user_id == settings.USER_ID: + Changelog.record(self.user, 'addlistitems', self.name, items) def remove_items(self, items): from item.models import Item for item_id in items: i = Item.get(item_id) self.items.remove(i) + i.update() db.session.add(self) db.session.commit() + for item_id in items: + i = Item.get(item_id) + i.update_lists() + db.session.commit() + if self.user_id == settings.USER_ID: + Changelog.record(self.user, 'removelistitems', self.name, items) def remove(self): if not self._query: for i in self.items: self.items.remove(i) if not self._query: - print 'record change: removelist', self.user, self.name - Changelog.record(self.user, 'removelist', self.name) + if self.user_id == settings.USER_ID: + Changelog.record(self.user, 'removelist', self.name) db.session.delete(self) db.session.commit() @@ -184,10 +198,21 @@ class List(db.Model): def public_id(self): id = '' if self.user_id != settings.USER_ID: - id += self.user_id - id = '%s:%s' % (id, self.name) + id += self.user.nickname + id = u'%s:%s' % (id, self.name) return id + @property + def find_id(self): + id = '' + if self.user_id != settings.USER_ID: + id += self.user_id + id = u'%s:%s' % (id, self.id) + return id + + def __repr__(self): + return self.public_id.encode('utf-8') + def items_count(self): from item.models import Item if self._query: @@ -199,6 +224,7 @@ class List(db.Model): def json(self): r = { 'id': self.public_id, + 'user': self.user.nickname if self.user_id != settings.USER_ID else settings.preferences['username'], 'name': self.name, 'index': self.position, 'items': self.items_count(), diff --git a/static/js/browser.js b/static/js/browser.js index 1074929..d012e4c 100644 --- a/static/js/browser.js +++ b/static/js/browser.js @@ -9,8 +9,8 @@ oml.ui.browser = function() { defaultRatio: oml.config.coverRatio, draggable: true, item: function(data, sort, size) { - var color = oml.getFileTypeColor(data).map(function(rgb) { - return rgb.concat(0.8) + var color = oml.getFileInfoColor(ui.fileInfo, data).map(function(rgb) { + return rgb.concat(0.8); }), ratio = data.coverRatio || oml.config.coverRatio, width = Math.round(ratio >= 1 ? size : size * ratio), @@ -28,12 +28,14 @@ oml.ui.browser = function() { height: Math.round(size / 12.8) + 'px', borderWidth: Math.round(size / 64) + 'px 0', borderStyle: 'solid', - borderColor: 'rgba(' + color[1].join(', ') + ')', + borderColor: 'rgba(' + color[2].join(', ') + ')', margin: Math.round(size / 18) + 'px ' + Math.round(width / 3) + 'px', fontSize: Math.round(size / 16) + 'px', textAlign: 'center', - color: 'rgba(' + color[1].join(', ') + ')', - backgroundColor: 'rgba(' + color[0].join(', ') + ')', + color: 'rgba(' + color[2].join(', ') + ')', + backgroundImage: '-webkit-linear-gradient(top, ' + color.slice(0, 2).map(function(rgba) { + return 'rgba(' + rgba.join(', ') + ')'; + }).join(', ') + ')', WebkitTransform: 'rotate(45deg)' }) .html( @@ -55,8 +57,8 @@ oml.ui.browser = function() { }), callback); }, keys: [ - 'author', 'coverRatio', 'extension', - 'id', 'size', 'textsize', 'title' + 'author', 'coverRatio', 'extension', 'id', + 'mediastate', 'size', 'textsize', 'title' ], max: 1, min: 1, @@ -101,6 +103,8 @@ oml.ui.browser = function() { } }); + oml.enableDragAndDrop(that); + return that; }; \ No newline at end of file diff --git a/static/js/folders.js b/static/js/folders.js index 5c4467a..00957c6 100644 --- a/static/js/folders.js +++ b/static/js/folders.js @@ -58,200 +58,187 @@ oml.ui.folders = function() { oml.$ui.libraryList = []; oml.$ui.folderList = []; - oml.api.getUsers(function(result) { + oml.getUsersAndLists(function(users, lists) { - var peers = result.data.users.filter(function(user) { - return user.peered; - }); + Ox.print('GOT USERS AND LISTS', users, lists); - oml.api.getLists(function(result) { + users.forEach(function(user, index) { - Ox.print('GOT LISTS', result.data); + var $content, + items = lists.filter(function(list) { + return list.user == user.nickname + && list.type != 'library'; + }), + libraryId = (!index ? '' : user.nickname) + ':' - var users = [ - { - id: oml.user.id, - nickname: oml.user.preferences.username, - online: oml.user.online - } - ].concat(peers), + userIndex[user.nickname] = index; - lists = result.data.lists; - - users.forEach(function(user, index) { - - var $content, - libraryId = (!index ? '' : user.nickname) + ':'; - - userIndex[user.nickname] = index; - - oml.$ui.folder[index] = Ox.CollapsePanel({ - collapsed: false, - extras: [ - oml.ui.statusIcon( - !oml.user.online ? 'unknown' - : user.online ? 'connected' - : 'disconnected' - ), - {}, - Ox.Button({ - style: 'symbol', - title: 'info', - tooltip: Ox._(!index ? 'Preferences' : 'Profile'), - type: 'image' - }) - .bindEvent({ - click: function() { - if (!index) { - oml.UI.set({ - page: 'preferences', - 'part.preferences': 'account' - }); - } else { - oml.UI.set({page: 'users'}) - } + oml.$ui.folder[index] = Ox.CollapsePanel({ + collapsed: false, + extras: [ + oml.ui.statusIcon( + !oml.user.online ? 'unknown' + : user.online ? 'connected' + : 'disconnected' + ), + {}, + Ox.Button({ + style: 'symbol', + title: 'info', + tooltip: Ox._(!index ? 'Preferences' : 'Profile'), + type: 'image' + }) + .bindEvent({ + click: function() { + if (!index) { + oml.UI.set({ + page: 'preferences', + 'part.preferences': 'account' + }); + } else { + oml.UI.set({page: 'users'}) } - }) - ], - title: Ox.encodeHTMLEntities(user.nickname) - }) - .css({ - width: ui.sidebarSize + } + }) + ], + title: Ox.encodeHTMLEntities(user.nickname) + }) + .css({ + width: ui.sidebarSize + }) + .bindEvent({ + toggle: function(data) { + oml.UI.set('showFolder.' + user.nickname, !data.collapsed); + } + }) + .bindEvent( + 'oml_showfolder.' + user.nickname.toLowerCase(), + function(data) { + oml.$ui.folder[index].options({collapsed: !data.value}); + } + ) + .appendTo(that); + + $content = oml.$ui.folder[index].$content + .css({ + height: (1 + items.length) * 16 + 'px' + }); + + $lists.push( + oml.$ui.libraryList[index] = oml.ui.folderList({ + items: [ + { + id: libraryId, + name: Ox._('Library'), + type: 'library', + items: -1 + } + ] }) .bindEvent({ - toggle: function(data) { - oml.UI.set('showFolder.' + user.nickname, !data.collapsed); + add: function() { + !index && oml.addList(); + }, + load: function() { + oml.api.find({ + query: getFind(libraryId) + }, function(result) { + oml.$ui.libraryList[index].value( + libraryId, 'items', result.data.items + ); + }); + }, + open: function() { + oml.$ui.listDialog = oml.ui.listDialog().open(); + }, + select: function(data) { + oml.UI.set({find: getFind(data.ids[0])}); + }, + selectnext: function() { + oml.UI.set({find: getFind(lists[user.id][0].id)}); + }, + selectprevious: function() { + var userId = !index ? null : users[index - 1].id, + set = { + find: getFind( + !index + ? '' + : Ox.last(lists[userId]).id + ) + }; + if (userId) { + Ox.extend(set, 'showFolder.' + userId, true); + } + oml.UI.set(set); } }) - .bindEvent( - 'oml_showfolder.' + user.nickname.toLowerCase(), - function(data) { - oml.$ui.folder[index].options({collapsed: !data.value}); - } - ) - .appendTo(that); + .appendTo($content) + ); - $content = oml.$ui.folder[index].$content - .css({ - height: (1 + lists[user.id].length) * 16 + 'px' - }); - - $lists.push( - oml.$ui.libraryList[index] = oml.ui.folderList({ - items: [ - { - id: libraryId, - name: Ox._('Library'), - type: 'library', - items: -1 - } - ] - }) - .bindEvent({ - add: function() { - !index && oml.addList(); - }, - load: function() { - oml.api.find({ - query: getFind(libraryId) - }, function(result) { - oml.$ui.libraryList[index].value( - libraryId, 'items', result.data.items - ); - }); - }, - open: function() { - oml.$ui.listDialog = oml.ui.listDialog().open(); - }, - select: function(data) { - oml.UI.set({find: getFind(data.ids[0])}); - }, - selectnext: function() { - oml.UI.set({find: getFind(lists[user.id][0].id)}); - }, - selectprevious: function() { - var userId = !index ? null : users[index - 1].id, - set = { - find: getFind( - !index - ? '' - : Ox.last(lists[userId]).id - ) - }; - if (userId) { - Ox.extend(set, 'showFolder.' + userId, true); - } - oml.UI.set(set); - } - }) - .appendTo($content) - ); - - $lists.push( - oml.$ui.folderList[index] = oml.ui.folderList({ - draggable: !!index, - items: lists[user.id], - sortable: true - }) - .bindEvent({ - add: function() { - !index && oml.addList(); - }, - 'delete': function() { - !index && oml.deleteList(); - }, - key_control_d: function() { - oml.addList(ui._list); - }, - load: function() { + $lists.push( + oml.$ui.folderList[index] = oml.ui.folderList({ + draggable: !!index, + items: items, + sortable: true + }) + .bindEvent({ + add: function() { + !index && oml.addList(); + }, + 'delete': function() { + !index && oml.deleteList(); + }, + key_control_d: function() { + oml.addList(ui._list); + }, + load: function() { + // ... + }, + move: function(data) { + lists[user.id] = data.ids.map(function(listId) { + return Ox.getObjectById(lists[user.id], listId); + }); + oml.api.sortLists({ + ids: data.ids, + user: user.id + }, function(result) { // ... - }, - move: function(data) { - lists[user.id] = data.ids.map(function(listId) { - return Ox.getObjectById(lists[user.id], listId); - }); - oml.api.sortLists({ - ids: data.ids, - user: user.id - }, function(result) { - // ... - }); - }, - open: function() { - oml.ui.listDialog().open(); - }, - select: function(data) { - oml.UI.set({find: getFind(data.ids[0])}); - }, - selectnext: function() { - if (index < users.length - 1) { - oml.UI.set(Ox.extend( - {find: getFind(users[index + 1].nickname + ':')}, - 'showFolder.' + users[index + 1].nickname, - true - )); - } - }, - selectprevious: function() { - oml.UI.set({find: getFind(libraryId)}); + }); + }, + open: function() { + oml.ui.listDialog().open(); + }, + select: function(data) { + oml.UI.set({find: getFind(data.ids[0])}); + }, + selectnext: function() { + if (index < users.length - 1) { + oml.UI.set(Ox.extend( + {find: getFind(users[index + 1].nickname + ':')}, + 'showFolder.' + users[index + 1].nickname, + true + )); } - }) - .bindEvent(function(data, event) { - if (!index) { - Ox.print('LIST EVENT', event, data); - } - }) - .css({height: lists[user.id].length * 16 + 'px'}) - .appendTo($content) - ); + }, + selectprevious: function() { + oml.UI.set({find: getFind(libraryId)}); + } + }) + .bindEvent(function(data, event) { + if (!index) { + Ox.print('LIST EVENT', event, data); + } + }) + .css({height: items.length * 16 + 'px'}) + .appendTo($content) + ); - oml.$ui.folderList[index].$body.css({top: '16px'}); - - }); - - selectList(); + oml.$ui.folderList[index].$body.css({top: '16px'}); }); + + selectList(); + }); function getFind(list) { diff --git a/static/js/gridView.js b/static/js/gridView.js index e0ca368..f0fce27 100644 --- a/static/js/gridView.js +++ b/static/js/gridView.js @@ -8,8 +8,8 @@ oml.ui.gridView = function() { defaultRatio: oml.config.coverRatio, draggable: true, item: function(data, sort, size) { - var color = oml.getFileTypeColor(data).map(function(rgb) { - return rgb.concat(0.8) + var color = oml.getFileInfoColor(ui.fileInfo, data).map(function(rgb) { + return rgb.concat(0.8); }), ratio = data.coverRatio || oml.config.coverRatio, width = Math.round(ratio >= 1 ? size : size * ratio), @@ -20,6 +20,9 @@ oml.ui.gridView = function() { ? (data.author || '') : data[sortKey] ); size = size || 128; + Ox.print('WTF', '-webkit-linear-gradient(top, ' + color.slice(2).map(function(rgba) { + return 'rgba(' + rgba.join(', ') + ')'; + }).join(', ') + ')'); return { extra: ui.showFileInfo ? $('
') .css({ @@ -27,12 +30,14 @@ oml.ui.gridView = function() { height: Math.round(size / 12.8) + 'px', borderWidth: Math.round(size / 64) + 'px 0', borderStyle: 'solid', - borderColor: 'rgba(' + color[1].join(', ') + ')', + borderColor: 'rgba(' + color[2].join(', ') + ')', margin: Math.round(size / 18) + 'px ' + Math.round(width / 3) + 'px', fontSize: Math.round(size / 16) + 'px', textAlign: 'center', - color: 'rgba(' + color[1].join(', ') + ')', - backgroundColor: 'rgba(' + color[0].join(', ') + ')', + color: 'rgba(' + color[2].join(', ') + ')', + backgroundImage: '-webkit-linear-gradient(top, ' + color.slice(0, 2).map(function(rgba) { + return 'rgba(' + rgba.join(', ') + ')'; + }).join(', ') + ')', WebkitTransform: 'rotate(45deg)' }) .html( @@ -54,8 +59,8 @@ oml.ui.gridView = function() { }), callback); }, keys: [ - 'author', 'coverRatio', 'extension', - 'id', 'size', 'textsize', 'title' + 'author', 'coverRatio', 'extension', 'id', + 'mediastate', 'size', 'textsize', 'title' ], selected: ui.listSelection, size: 128, diff --git a/static/js/infoView.js b/static/js/infoView.js index 0d1473e..6546d49 100644 --- a/static/js/infoView.js +++ b/static/js/infoView.js @@ -162,7 +162,40 @@ oml.ui.infoView = function() { } function renderMediaButton(data) { - return data.mediastate == 'transferring' + return data.mediastate == 'unavailable' + ? Ox.FormElementGroup({ + elements: [ + Ox.Button({ + title: Ox._('Download Book'), + width: 112 + }) + .bindEvent({ + click: function() { + data.mediastate = 'transferring'; + that.update(data, $data); + oml.api.download({id: ui.item}, function(result) { + // ... + }); + } + }), + Ox.MenuButton({ + items: [ + {id: '', title: Ox._('Library')} + ], + overlap: 'left', + title: 'list', + tooltip: Ox._('Download Book to a List'), + type: 'image' + }) + .bindEvent({ + click: function() { + // ... + } + }) + ], + float: 'right' + }) + : data.mediastate == 'transferring' ? Ox.FormElementGroup({ elements: [ Ox.Button({ @@ -192,29 +225,13 @@ oml.ui.infoView = function() { ], float: 'right' }) - .css({ - marginTop: '8px' - }) : Ox.Button({ - title: Ox._( - data.mediastate == 'available' ? 'Read Book' : 'Download Book' - ), + title: Ox._('Read Book'), width: 128 }) - .css({ - marginTop: '8px' - }) .bindEvent({ click: function() { - if (data.mediastate == 'available') { - oml.UI.set({itemView: 'book'}); - } else { - data.mediastate = 'transferring'; - that.update(data, $data); - oml.api.download({id: ui.item}, function(result) { - // ... - }); - } + oml.UI.set({itemView: 'book'}); } }); } @@ -339,10 +356,50 @@ oml.ui.infoView = function() { } else if ($element == $data) { + $mediaButton = renderMediaButton(data) + .appendTo($data); + + $('
') + .addClass('OxSelectable') + .css({ + marginTop: '8px', + }) + .text( + [ + data.extension.toUpperCase(), + Ox.formatValue(data.size, 'B') + ].join(', ') + ) + .appendTo($data); + + ['accessed', 'modified', 'added', 'created'].forEach(function(id) { + var title; + if (data[id]) { + title = Ox.getObjectById(oml.config.itemKeys, id).title; + $('
') + .css({ + marginTop: '8px', + fontWeight: 'bold' + }) + .text(title) + .appendTo($data); + Ox.EditableContent({ + editable: false, + format: function(value) { + return value ? Ox.formatDate(value, '%b %e, %Y') : ''; + }, + placeholder: Ox._('unknown'), + value: data[id] || '' + }) + .appendTo($data); + } + }); + Ox.Button({ title: Ox._('Identify Book...'), width: 128 }) + .css({marginTop: '16px'}) .bindEvent({ click: function() { identify(data); @@ -385,42 +442,6 @@ oml.ui.infoView = function() { }); - $mediaButton = renderMediaButton(data) - .appendTo($data); - - $('
') - .addClass('OxSelectable') - .css({ - marginTop: '8px', - }) - .text( - [ - data.extension.toUpperCase(), - Ox.formatValue(data.size, 'B') - ].join(', ') - ) - .appendTo($data); - - ['accessed', 'modified', 'added', 'created'].forEach(function(id) { - var title = Ox.getObjectById(oml.config.itemKeys, id).title; - $('
') - .css({ - marginTop: '8px', - fontWeight: 'bold' - }) - .text(title) - .appendTo($data); - Ox.EditableContent({ - editable: false, - format: function(value) { - return value ? Ox.formatDate(value, '%b %e, %Y') : ''; - }, - placeholder: Ox._('unknown'), - value: data[id] || '' - }) - .appendTo($data); - }); - $('
').css({height: '16px'}).appendTo($data); } diff --git a/static/js/leftPanel.js b/static/js/leftPanel.js index ba77a6e..d4bad02 100644 --- a/static/js/leftPanel.js +++ b/static/js/leftPanel.js @@ -29,7 +29,7 @@ oml.ui.leftPanel = function() { resize: function(data) { ui.sidebarSize = data.size; oml.resizeListFolders(); - that.size(2, data.size); + that.size(2, oml.getInfoHeight()); if (!ui.showInfo) { that.css({bottom: -data.size + 'px'}); } diff --git a/static/js/list.js b/static/js/list.js index 84f78a8..dec2c98 100644 --- a/static/js/list.js +++ b/static/js/list.js @@ -63,6 +63,8 @@ oml.ui.list = function() { } }); + oml.enableDragAndDrop(that); + return that; }; \ No newline at end of file diff --git a/static/js/mainMenu.js b/static/js/mainMenu.js index 64f8575..00e825b 100644 --- a/static/js/mainMenu.js +++ b/static/js/mainMenu.js @@ -46,151 +46,8 @@ oml.ui.mainMenu = function() { } ] }, - { - id: 'listMenu', - title: Ox._('List'), - items: [ - { - id: 'newlist', - title: Ox._('New List'), - keyboard: 'control n' - }, - { - id: 'newlistfromselection', - title: Ox._('New List from Selection'), - keyboard: 'shift control n', - disabled: true - }, - { - id: 'newsmartlist', - title: Ox._('New Smart List'), - keyboard: 'alt control n' - }, - { - id: 'newsmartlistfromresults', - title: Ox._('New Smart List from Results'), - keyboard: 'shift alt control n', - disabled: true - }, - {}, - { - id: 'duplicatelist', - title: Ox._('Duplicate List'), - keyboard: 'control d', - disabled: true - }, - { - id: 'editlist', - title: Ox._('Edit List...'), - keyboard: 'return', - disabled: true - }, - { - id: 'deletelist', - title: Ox._('Delete List...'), - keyboard: 'delete', - disabled: true - } - ] - }, - { - id: 'editMenu', - title: Ox._('Edit'), - items: [ - { - id: 'importitems', - title: Ox._('Import Books...') - }, - { - id: 'exportitems', - title: Ox._('Export Books...') - }, - {}, - { - id: 'download', - title: Ox._('Download Items'), - disabled: true, - keyboard: 'control d' - }, - {}, - { - id: 'selectall', - title: Ox._('Select All'), - keyboard: 'control a' - }, - { - id: 'selectnone', - title: Ox._('Select None'), - keyboard: 'shift control a' - }, - { - id: 'invertselection', - title: Ox._('Invert Selection'), - keyboard: 'alt control a' - }, - {}, - { - id: 'cut', - title: Ox._('Cut Items'), - disabled: true, - keyboard: 'control x' - }, - { - id: 'cutadd', - title: Ox._('Cut and Add to Clipboard'), - disabled: true, - keyboard: 'shift control x' - }, - { - id: 'copy', - title: Ox._('Copy Items'), - disabled: true, - keyboard: 'control c' - }, - { - id: 'copyadd', - title: Ox._('Copy and Add to Clipboard'), - disabled: true, - keyboard: 'shift control c' - }, - { - id: 'paste', - title: Ox._('Paste Items'), - disabled: true, - keyboard: 'control v' - }, - { - id: 'clearclipboard', - title: Ox._('Clear Clipboard'), - disabled: true - }, - {}, - { - id: 'delete', - title: Ox._('Delete Items from List'), - disabled: true, - keyboard: 'delete' - }, - {}, - { - id: 'undo', - title: Ox._('Undo'), - disabled: true, - keyboard: 'control z' - }, - { - id: 'redo', - title: Ox._('Redo'), - disabled: true, - keyboard: 'shift control z' - }, - { - id: 'clearhistory', - title: Ox._('Clear History'), - disabled: true, - } - ] - }, + getListMenu(), + getEditMenu(), { id: 'viewMenu', title: Ox._('View'), @@ -529,6 +386,9 @@ oml.ui.mainMenu = function() { oml.UI.set({showSidebar: !ui.showSidebar}); }, oml_find: function() { + that.replaceMenu('listMenu', getListMenu()); + that.replaceMenu('editMenu', getEditMenu()); + /* var action = Ox.startsWith(ui._list, ':') && ui._list != ':' ? 'enableItem' : 'disableItem'; that[ @@ -537,6 +397,7 @@ oml.ui.mainMenu = function() { ]('duplicatelist'); that[action]('editlist'); that[action]('deletelist'); + */ }, oml_item: function(data) { if (!!data.value != !!data.previousValue) { @@ -544,6 +405,9 @@ oml.ui.mainMenu = function() { that[data.value ? 'enableItem' : 'disableItem']('showbrowser'); } }, + oml_listselection: function(data) { + that.replaceMenu('editMenu', getEditMenu()); + }, oml_showbrowser: function(data) { that.setItemTitle('showbrowser', Ox._((data.value ? 'Hide' : 'Show') + ' Browser')); }, @@ -559,10 +423,206 @@ oml.ui.mainMenu = function() { }, }); - function getItemMenu() { - // ... + function getEditMenu() { + var listData = oml.getListData(), + username = oml.user.preferences.username, + selectionItems = ui.listSelection.length, + selectionItemName = ( + selectionItems > 1 ? Ox.formatNumber(selectionItems) + ' ' : '' + ) + Ox._(clipboardItems == 1 ? 'Book' : 'Books'), + clipboardItems = oml.clipboard.items(), + clipboardType = oml.clipboard.type(), + clipboardItemName = !clipboardItems ? '' + : ( + clipboardItems > 1 ? Ox.formatNumber(clipboardItems) + ' ' : '' + ) + Ox._(clipboardItems == 1 ? 'Book' : 'Books'), + canSelect = !ui.item, + canCopy = canSelect && selectionItems, + canCut = canCopy && listData.editable, + canPaste = listData.editable && clipboardItems, + canAdd = canCopy && clipboardItems && clipboardItemType == ui.section, + canDownload = listData.user != username && selectionItems, + historyItems = oml.history.items(), + undoText = oml.history.undoText(), + redoText = oml.history.redoText(); + return { + id: 'editMenu', + title: Ox._('Edit'), + items: [ + { + id: 'importitems', + title: Ox._('Import Books...') + }, + { + id: 'exportitems', + title: Ox._('Export Books...') + }, + {}, + { + id: 'download', + title: Ox._('Download {0}', [selectionItemName]), + disabled: !canDownload, + keyboard: 'control d' + }, + {}, + { + id: 'selectall', + title: Ox._('Select All'), + disabled: !canSelect, + keyboard: 'control a' + }, + { + id: 'selectnone', + title: Ox._('Select None'), + disabled: !canSelect, + keyboard: 'shift control a' + }, + { + id: 'invertselection', + title: Ox._('Invert Selection'), + disabled: !canSelect, + keyboard: 'alt control a' + }, + {}, + { + id: 'cut', + title: Ox._('Cut {0}', [selectionItemName]), + disabled: !canCut, + keyboard: 'control x' + }, + { + id: 'cutadd', + title: Ox._('Cut and Add to Clipboard'), + disabled: !canCut || !canAdd, + keyboard: 'shift control x' + }, + { + id: 'copy', + title: Ox._('Copy {0}', [selectionItemName]), + disabled: !canCopy, + keyboard: 'control c' + }, + { + id: 'copyadd', + title: Ox._('Copy and Add to Clipboard'), + disabled: !canCopy || !canAdd, + keyboard: 'shift control c' + }, + { + id: 'paste', + title: !clipboardItems ? Ox._('Paste') : Ox._('Paste {0}', [clipboardItemName]), + disabled: !canPaste, + keyboard: 'control v' + }, + { + id: 'clearclipboard', + title: Ox._('Clear Clipboard'), + disabled: !clipboardItems + }, + {}, + { + id: 'delete', + title: Ox._('Delete {0} from List', [selectionItemName]), + disabled: !canCut, + keyboard: 'delete' + }, + {}, + { + id: 'undo', + title: undoText ? Ox._('Undo {0}', [undoText]) : Ox._('Undo'), + disabled: !undoText, + keyboard: 'control z' + }, + { + id: 'redo', + title: redoText ? Ox._('Redo {0}', [redoText]) : Ox._('Redo'), + disabled: !redoText, + keyboard: 'shift control z' + }, + { + id: 'clearhistory', + title: Ox._('Clear History'), + disabled: !historyItems, + } + ] + }; } + function getListMenu() { + var isLibraries = !ui._list, + isLibrary = Ox.endsWith(ui._list, ':'), + isList = !isLibraries && !isLibrary, + isOwnList = ui._list[0] == ':'; + return { + id: 'listMenu', + title: Ox._('List'), + items: [ + { + id: 'libraries', + title: Ox._('All Libraries'), + keyboard: 'shift control w' + }, + { + id: 'library', + title: Ox._('This Library'), + disabled: isLibraries, + keyboard: isLibrary ? 'control w' : '' + }, + { + id: 'list', + title: Ox._('This List'), + disabled: isLibrary, + keyboard: isLibrary ? '' : 'control w' + }, + {}, + { + id: 'newlist', + title: Ox._('New List'), + keyboard: 'control n' + }, + { + id: 'newlistfromselection', + title: Ox._('New List from Selection'), + keyboard: 'shift control n', + disabled: !ui.listSelection.length + }, + { + id: 'newsmartlist', + title: Ox._('New Smart List'), + keyboard: 'alt control n' + }, + { + id: 'newsmartlistfromresults', + title: Ox._('New Smart List from Results'), + keyboard: 'shift alt control n' + }, + {}, + { + id: 'duplicatelist', + title: Ox._('Duplicate List'), + disabled: !isList + }, + { + id: 'editlist', + title: Ox._('Edit List...'), + keyboard: 'return', + disabled: !isOwnList + }, + { + id: 'deletelist', + title: Ox._('Delete List...'), + keyboard: 'delete', + disabled: !isOwnList + } + ] + }; + } + + that.update = function() { + return that.updateMenu('listMenu', getListMenu()) + .updateMenu('editMenu', getEditMenu()); + }; + return that; }; diff --git a/static/js/usersDialog.js b/static/js/usersDialog.js index ee62d31..31f271c 100644 --- a/static/js/usersDialog.js +++ b/static/js/usersDialog.js @@ -453,12 +453,12 @@ oml.ui.usersDialog = function() { }) .bindEvent({ select: function(data) { - $lists.forEach(function($element) { - if ($element != $list) { - $element.options({selected: []}); - } - }); - renderUser(Ox.getObjectById(users, data.ids[0])); + if (data.ids.length) { + selectItem($list); + renderUser(Ox.getObjectById(users, data.ids[0])); + } else { + renderUser(); + } } }); diff --git a/static/js/utils.js b/static/js/utils.js index c05482e..9d0689b 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -1,3 +1,564 @@ +oml.addList = function() { + // addList(isSmart, isFrom) or addList(list) [=dupicate] + var args = arguments, + isDuplicate = args.length == 1, + isSmart, isFrom, list, listData, data; + oml.api.getLists(function(result) { + var lists = result.data.lists, + listNames = lists[oml.user.id].map(function(list) { + return list.name; + }), + query; + if (!isDuplicate) { + isSmart = args[0]; + isFrom = args[1]; + data = { + name: oml.validateName(Ox._('Untitled'), listNames), + type: !isSmart ? 'static' : 'smart' + }; + if (isFrom) { + if (!isSmart) { + data.items = ui.listSelection; + } else { + data.query = ui.find; + } + } + addList(); + } else { + list = args[0]; + listData = Ox.getObjectById(Ox.flatten(Ox.values(lists)), list); + data = Ox.extend({ + name: oml.validateName(listData.name, listNames), + type: listData.type + }, listData.query ? { + query: listData.query + } : {}); + if (!data.query) { + var query = { + conditions: [{key: 'list', operator: '==', value: list}], + operator: '&' + }; + oml.api.find({query: query}, function(result) { + if (result.data.items) { + oml.api.find({ + query: query, + keys: ['id'], + sort: [{key: 'id', operator: '+'}], + range: [0, result.data.items] + }, function(result) { + data.items = result.data.items.map(function(item) { + return item.id; + }); + addList(); + }); + } else { + addList(); + } + }); + } else { + addList(); + } + } + }); + function addList() { + oml.api.addList(data, function(result) { + var list = result.data.id, + $folderList = oml.$ui.folderList[0]; + oml.$ui.folder[0].options({collapsed: false}); // FIXME: SET UI! + // FIXME: DOESN'T WORK + $folderList + .bindEventOnce({ + load: function() { + $folderList + .gainFocus() + .options({selected: [list]}); + oml.UI.set({ + find: { + conditions: [{ + key: 'list', + operator: '==', + value: list + }], + operator: '&' + } + }); + oml.$ui.listDialog = oml.ui.listDialog().open(); + } + }); + oml.updateLists(); + }); + } +}; + +oml.clearFilters = function() { + var ui = oml.user.ui, + find = Ox.clone(ui.find, true), + indices = ui._filterState.map(function(filterState) { + return filterState.index; + }).filter(function(index) { + return index > -1; + }); + find.conditions = find.conditions.filter(function(condition, index) { + return !Ox.contains(indices, index); + }); + oml.UI.set({find: find}); +}; + +oml.deleteList = function() { + var ui = oml.user.ui; + oml.ui.confirmDialog({ + buttons: [ + Ox.Button({ + title: Ox._('No, Keep List') + }), + Ox.Button({ + title: Ox._('Yes, Delete List') + }) + ], + content: Ox._('Are you sure you want to delete this list?'), + title: Ox._('Delete List') + }, function() { + oml.api.removeList({ + id: ui._list + }, function() { + oml.UI.set({ + find: { + conditions: [{ + key: 'list', + operator: '==', + value: ':' + }], + operator: '&' + } + }); + oml.updateLists(); + }); + }); +}; + +(function() { + + oml.doHistory = function(action, items, targets, callback) { + items = Ox.makeArray(items); + targets = Ox.makeArray(targets); + if (action == 'copy' || action == 'paste') { + addItems(items, targets[0], addToHistory); + } else if (action == 'cut' || action == 'delete') { + removeItems(items, targets[0], addToHistory); + } else if (action == 'move') { + removeItems(items, targets[0], function() { + addItems(items, targets[1], addToHistory); + }); + } + function addToHistory(result, addedItems) { + var actions = { + copy: 'Copying', + cut: 'Cutting', + 'delete': 'Deleting', + move: 'Moving', + paste: 'Pasting' + }, + length = items.length, + text = Ox._(actions[action]) + ' ' + ( + length == 1 ? 'Book' : 'Books' + ); + oml.history.add({ + action: action, + items: action == 'cut' || action == 'delete' ? [items] + : action == 'copy' || action == 'paste' ? [addedItems] + : [items, addedItems], // move + positions: [], + targets: targets, + text: text + }); + callback(result); + } + }; + + oml.redoHistory = function(callback) { + var object = oml.history.redo(); + if (object) { + if (object.action == 'copy' || object.action == 'paste') { + addItems(object.items[0], object.targets[0], done); + } else if (object.action == 'cut' || object.action == 'delete') { + removeItems(object.items[0], object.targets[0], done); + } else if (object.action == 'move') { + removeItems(object.items[0], object.targets[0], function() { + addItems(object.items[1], object.targets[1], done); + }); + } + } + function done() { + doneHistory(object, callback); + } + }; + + oml.undoHistory = function(callback) { + var object = oml.history.undo(); + if (object) { + if (object.action == 'copy' || object.action == 'paste') { + removeItems(object.items[0], object.targets[0], done); + } else if (object.action == 'cut' || object.action == 'delete') { + addItems(object.items[0], object.targets[0], done); + } else if (object.action == 'move') { + removeItems(object.items[1], object.targets[1], function() { + addItems(object.items[0], object.targets[0], done); + }); + } + } + function done() { + doneHistory(object, callback); + } + }; + + function addItems(items, target, callback) { + oml.api.find({ + query: { + conditions: [{ + key: 'list', + operator: '==', + value: target + }], + operator: '&' + }, + positions: items + }, function(result) { + var existingItems = Object.keys(result.data.positions), + addedItems = items.filter(function(item) { + return !Ox.contains(existingItems, item); + }); + if (addedItems.length) { + oml.api.addListItems({ + items: addedItems, + list: target + }, function(result) { + callback(result, addedItems); + }); + } else { + callback(null, []); + } + }); + } + + function doneHistory(object, callback) { + var list, listData, ui = oml.user.ui; + Ox.Request.clearCache('find'); + object.targets.filter(function(list) { + return list != ui._list; + }).forEach(function(list) { + listData = oml.getListData(list); + oml.api.find({ + query: { + conditions: [{ + key: 'list', + operator: '==', + value: list + }], + operator: '&' + } + }, function(result) { + oml.$ui.folderList[listData.folder].value( + list, 'items', result.data.items + ); + }); + }); + if (Ox.contains(object.targets, ui._list)) { + // FIXME: Why is this timeout needed? + setTimeout(oml.reloadList, 250); + } + callback && callback(); + } + + function removeItems(items, target, callback) { + oml.api.removeListItems({ + items: items, + list: target + }, callback); + } + +}()); + +oml.enableDragAndDrop = function($list, canMove) { + + var $tooltip = Ox.Tooltip({ + animate: false + }), + drag = {}, + scrollInterval, + ui = oml.user.ui, + username = oml.user.preferences.username; + + $list.bindEvent({ + draganddropstart: function(data) { + Ox.print('DND START', data); + var $lists = oml.$ui.libraryList.concat(oml.$ui.folderList); + drag.action = 'copy'; + drag.ids = $list.options('selected'); + drag.item = drag.ids.length == 1 + ? $list.value(drag.ids[0], 'title') + : drag.ids.length; + drag.source = oml.getListData(); + drag.targets = {}; + $lists.forEach(function($list) { + $list.addClass('OxDroppable').find('.OxItem').each(function() { + var $item = $(this), + id = $item.data('id'), + data = oml.getListData(id); + drag.targets[id] = Ox.extend(data, { + editable: data.editable || ( + data.type == 'library' + && drag.source.user != username + && data.user == 'username' + ), + selected: data.id == ui._list + }, data); + if (!drag.targets[id].selected && drag.targets[id].editable) { + $item.addClass('OxDroppable'); + } + }); + }); + $tooltip.options({title: getTitle()}).show(data.event); + Ox.UI.$window.on({ + keydown: keydown, + keyup: keyup + }); + }, + draganddrop: function(data) { + var event = data.event; + $tooltip.options({ + title: getTitle(event) + }).show(event); + if (scrollInterval && !isAtListsTop(event) && !isAtListsBottom(event)) { + clearInterval(scrollInterval); + scrollInterval = 0; + } + }, + draganddroppause: function(data) { + var event = data.event, scroll, title, + ui = oml.user.ui, + $parent, $grandparent, $panel; + if (!ui.showSidebar) { + if (event.clientX < 16 && event.clientY >= 44 + && event.clientY < window.innerHeight - 16 + ) { + oml.$ui.mainPanel.toggle(0); + } + } else { + $parent = $(event.target).parent(); + $grandparent = $parent.parent(); + $panel = $parent.is('.OxCollapsePanel') ? $parent + : $grandparent.is('.OxCollapsePanel') ? $grandparent + : null; + if ($panel) { + title = $panel.children('.OxBar').children('.OxTitle') + .html().split(' ')[0].toLowerCase(); + if (!ui.showFolder[title]) { + Ox.UI.elements[$panel.data('oxid')].options({ + collapsed: false + }); + } + } + if (!scrollInterval) { + scroll = isAtListsTop(event) ? -16 + : isAtListsBottom(event) ? 16 + : 0 + if (scroll) { + scrollInterval = setInterval(function() { + oml.$ui.folders.scrollTop( + oml.$ui.folders.scrollTop() + scroll + ); + }, 100); + } + } + } + }, + draganddropenter: function(data) { + var $parent = $(data.event.target).parent(), + $item = $parent.is('.OxItem') ? $parent : $parent.parent(), + $list = $item.parent().parent().parent().parent(); + if ($list.is('.OxDroppable')) { + $item.addClass('OxDrop'); + drag.target = drag.targets[$item.data('id')]; + } else { + drag.target = null; + } + }, + draganddropleave: function(data) { + var $parent = $(data.event.target).parent(), + $item = $parent.is('.OxItem') ? $parent : $parent.parent(); + if ($item.is('.OxDroppable')) { + $item.removeClass('OxDrop'); + drag.target = null; + } + }, + draganddropend: function(data) { + var targets; + Ox.UI.$window.off({ + keydown: keydown, + keyup: keyup + }); + if ( + drag.target && drag.target.editable && !drag.target.selected + && (drag.action == 'copy' || drag.source.editable) + ) { + var targets = drag.action == 'copy' ? drag.target.id + : [oml.user.ui._list, drag.target.id]; + oml.doHistory(drag.action, data.ids, targets, function() { + Ox.Request.clearCache('find'); + oml.api.find({ + query: { + conditions: [{ + key: 'list', + operator: '==', + value: drag.target.id + }], + operator: '&' + } + }, function(result) { + oml.$ui.folderList[drag.target.folder].value( + drag.target.id, 'items', result.data.items + ); + cleanup(250); + }); + drag.action == 'move' && oml.reloadList(); + }); + } else { + cleanup() + } + } + }); + + function cleanup(ms) { + ms = ms || 0; + drag = {}; + clearInterval(scrollInterval); + scrollInterval = 0; + setTimeout(function() { + $('.OxDroppable').removeClass('OxDroppable'); + $('.OxDrop').removeClass('OxDrop'); + $tooltip.hide(); + }, ms); + } + + function getTitle() { + var image, text, + actionText = drag.action == 'copy' ? ( + drag.source.user == username ? 'copy' : 'download' + ) : 'move', + itemText = Ox.isString(drag.item) + ? '"' + Ox.encodeHTMLEntities(Ox.truncate(drag.item, 32)) + '"' + : Ox._('{0} books', [drag.item]), + targetText; + if (drag.action == 'move') { + if (drag.source.user != username) { + text = Ox._('You can only remove books
from your own lists.'); + } else if (drag.source.type == 'library') { + text = Ox._('You cannot move books
out of your library.'); + } else if (drag.source.type == 'smart') { + text = Ox._('You cannot move books
out of a smart list.'); + } + } else if (drag.target) { + targetText = drag.target.type == 'libraries' ? Ox._('a library') + : drag.target.type == 'library' ? Ox._('your library') + : Ox._('the list "{0}"', [Ox.encodeHTMLEntities(Ox.truncate(drag.target.name, 32))]); + if ( + ( + drag.target.type == 'library' + && drag.source.user == username + && drag.target.user == username + ) + || drag.target.selected + ) { + text = Ox._('{0}
is already in {1}.', [ + Ox._(itemText[0] == '"' ? '' : 'These ') + itemText, + targetText + ]); + } else if (drag.target.user != username) { + text = Ox._( + 'You can only {0} books
to your own {1}.', + [actionText, drag.target.type == 'library' ? 'library' : 'lists'] + ); + } else if (drag.target.type == 'smart') { + text = Ox._('You cannot {0} books
to a smart list.', [actionText]); + } + } + if (text) { + image = 'symbolClose' + } else { + image = drag.action == 'copy' ? ( + drag.source.user == username ? 'symbolAdd' : 'symbolDownload' + ) : 'symbolRemove', + text = Ox._(Ox.toTitleCase(actionText)) + ' ' + ( + Ox.isString(drag.item) + ? '"' + Ox.encodeHTMLEntities(Ox.truncate(drag.item, 32)) + '"' + : drag.item + ' ' + 'books' + ) + '
' + ( + drag.target && drag.target.editable && !drag.target.selected + ? Ox._('to {0}.', [targetText]) + : drag.source.user == username + ? Ox._('to {0} list.', [ui._list == ':' ? 'a' : 'another']) + : Ox._('to your library or to one of your lists.') + ); + } + return $('
') + .append( + $('
') + .css({ + float: 'left', + width: '16px', + height: '16px', + padding: '1px', + border: '3px solid rgb(' + Ox.Theme.getThemeData().symbolDefaultColor.join(', ') + ')', + borderRadius: '12px', + margin: '3px 2px 2px 2px' + }) + .append( + $('') + .attr({src: Ox.UI.getImageURL(image)}) + .css({width: '16px', height: '16px'}) + ) + ) + .append( + $('
') + .css({ + float: 'left', + margin: '1px 2px 2px 2px', + fontSize: '11px', + whiteSpace: 'nowrap' + }) + .html(text) + ); + } + + function isAtListsTop(e) { + return ui.showSidebar + && e.clientX < ui.sidebarSize + && e.clientY >= 44 && e.clientY < 60; + } + + function isAtListsBottom(e) { + var listsBottom = window.innerHeight - oml.getInfoHeight(); + return ui.showSidebar + && e.clientX < ui.sidebarSize + && e.clientY >= listsBottom - 16 && e.clientY < listsBottom; + } + + function keydown(e) { + if (e.metaKey) { + drag.action = 'move'; + $tooltip.options({title: getTitle()}).show(); + } + } + + function keyup(e) { + if (drag.action == 'move') { + drag.action = 'copy'; + $tooltip.options({title: getTitle()}).show(); + } + } + +}; + (function() { // Note: getFindState has to run after getListState and getFilterState @@ -140,146 +701,64 @@ }()); -oml.addList = function() { - // addList(isSmart, isFrom) or addList(list) [=dupicate] - var args = arguments, - isDuplicate = args.length == 1, - isSmart, isFrom, list, listData, data; - oml.api.getLists(function(result) { - var lists = result.data.lists, - listNames = lists[oml.user.id].map(function(list) { - return list.name; - }), - query; - if (!isDuplicate) { - isSmart = args[0]; - isFrom = args[1]; - data = { - name: oml.validateName(Ox._('Untitled'), listNames), - type: !isSmart ? 'static' : 'smart' - }; - if (isFrom) { - if (!isSmart) { - data.items = ui.listSelection; - } else { - data.query = ui.find; - } - } - addList(); - } else { - list = args[0]; - listData = Ox.getObjectById(Ox.flatten(Ox.values(lists)), list); - Ox.print('LISTDATA,', listData) - data = Ox.extend({ - name: oml.validateName(listData.name, listNames), - type: listData.type - }, listData.query ? { - query: listData.query - } : {}); - if (!data.query) { - var query = { - conditions: [{key: 'list', operator: '==', value: list}], - operator: '&' - }; - oml.api.find({query: query}, function(result) { - if (result.data.items) { - oml.api.find({ - query: query, - keys: ['id'], - sort: [{key: 'id', operator: '+'}], - range: [0, result.data.items] - }, function(result) { - data.items = result.data.items.map(function(item) { - return item.id; - }); - addList(); - }); - } else { - addList(); - } - }); - } else { - addList(); - } - } - }); - function addList() { - Ox.print('DATA, ', data); - oml.api.addList(data, function(result) { - Ox.print('LIST ADDED', result.data); - var list = result.data.id, - $folderList = oml.$ui.folderList[0]; - oml.$ui.folder[0].options({collapsed: false}); // FIXME: SET UI! - // FIXME: DOESN'T WORK - $folderList - .bindEventOnce({ - load: function() { - $folderList - .gainFocus() - .options({selected: [list]}); - oml.UI.set({ - find: { - conditions: [{ - key: 'list', - operator: '==', - value: list - }], - operator: '&' - } - }); - oml.$ui.listDialog = oml.ui.listDialog().open(); - } - }); - oml.updateLists(); - }); - } +oml.getFileInfoColor = function(type, data) { + return type == 'extension' ? ( + data.extension == 'epub' ? [[32, 160, 32], [0, 128, 0], [128, 255, 128]] + : data.extension == 'pdf' ? ( + data.textsize + ? [[224, 32, 32], [192, 0, 0], [255, 192, 192]] + : [[224, 128, 32], [192, 96, 0], [255, 192, 128]] + ) + : data.extension == 'txt' ? [[255, 255, 255], [224, 224, 224], [0, 0, 0]] + : [[96, 96, 96], [64, 64, 64], [192, 192, 192]] + ) : data.mediastate == 'available' ? [[32, 160, 32], [0, 128, 0], [128, 255, 128]] + : data.mediastate == 'transferring' ? [[160, 160, 32], [128, 128, 0], [255, 255, 128]] + : [[224, 32, 32], [192, 0, 0], [255, 192, 192]]; }; -oml.clearFilters = function() { - var ui = oml.user.ui, - find = Ox.clone(ui.find, true), - indices = ui._filterState.map(function(filterState) { - return filterState.index; - }).filter(function(index) { - return index > -1; - }); - find.conditions = find.conditions.filter(function(condition, index) { - return !Ox.contains(indices, index); - }); - oml.UI.set({find: find}); -}; - -oml.deleteList = function() { +oml.getFilterSizes = function() { var ui = oml.user.ui; - oml.ui.confirmDialog({ - buttons: [ - Ox.Button({ - title: Ox._('No, Keep List') - }), - Ox.Button({ - title: Ox._('Yes, Delete List') - }) - ], - content: Ox._('Are you sure you want to delete this list?'), - title: Ox._('Delete List') - }, function() { - oml.api.removeList({ - id: ui._list - }, function() { - oml.UI.set({ - find: { - conditions: [{ - key: 'list', - operator: '==', - value: ':' - }], - operator: '&' - } - }); - oml.updateLists(); - }); - }); -} + return Ox.splitInt( + window.innerWidth - ui.showSidebar * ui.sidebarSize - 1, + 5 + ); +}; + +oml.getInfoHeight = function() { + return Math.min( + oml.user.ui.sidebarSize, + window.innerHeight - 20 - 24 - 16 - 1 + ); +}; + +oml.getListData = function(list) { + var data = {}, ui = oml.user.ui; + if (Ox.isUndefined(list)) { + list = ui._list; + } + if (ui._lists) { + data = ui._lists[list]; + } + return data; +}; + +oml.getListFoldersHeight = function() { + var ui = oml.user.ui; + return Object.keys(ui.showFolder).reduce(function(value, id, index) { + var items = oml.$ui.folderList[index].options('items').length; + return value + 16 + ui.showFolder[id] * (1 + items) * 16; + }, 16); +}; + +oml.getListFoldersWidth = function() { + var ui = oml.user.ui; + return ui.sidebarSize - ( + oml.$ui.appPanel + && oml.getListFoldersHeight() + > window.innerHeight - 20 - 24 - 1 - ui.showInfo * oml.getInfoHeight() + ? Ox.UI.SCROLLBAR_SIZE : 0 + ); +}; oml.getPageTitle = function(stateOrURL) { var page = Ox.getObjectById( @@ -298,44 +777,47 @@ oml.getSortOperator = function(key) { ) ? '+' : '-'; }; -oml.getFileTypeColor = function(data) { - return data.extension == 'epub' ? [[0, 128, 0], [128, 255, 128]] - : data.extension == 'pdf' ? ( - data.textsize ? [[192, 0, 0], [255, 192, 192]] - : [[192, 96, 0], [255, 192, 128]] - ) - : data.extension == 'txt' ? [[255, 255, 255], [0, 0, 0]] - : [[64, 64, 64], [192, 192, 192]]; -}; - -oml.getFilterSizes = function() { - var ui = oml.user.ui; - return Ox.splitInt( - window.innerWidth - ui.showSidebar * ui.sidebarSize - 1, - 5 - ); -}; - -oml.getListFoldersHeight = function() { - var ui = oml.user.ui; - return Object.keys(ui.showFolder).reduce(function(value, id, index) { - var items = oml.$ui.folderList[index].options('items').length; - Ox.print('REDUCE', value, id, index, '...', items) - return value + 16 + ui.showFolder[id] * (1 + items) * 16; - }, 16); -}; - -oml.getListFoldersWidth = function() { - var ui = oml.user.ui; - Ox.print('HEIGHT::::', oml.getListFoldersHeight(), 'SCROLLBAR????', oml.$ui.appPanel - && oml.getListFoldersHeight() - > window.innerHeight - 20 - 24 - 1 - ui.showInfo * ui.sidebarSize) - return ui.sidebarSize - ( - oml.$ui.appPanel - && oml.getListFoldersHeight() - > window.innerHeight - 20 - 24 - 1 - ui.showInfo * ui.sidebarSize - ? Ox.UI.SCROLLBAR_SIZE : 0 - ); +oml.getUsersAndLists = function(callback) { + var lists = [{ + id: '', + name: Ox._('All Libraries'), + type: 'libraries' + }], + ui = oml.user.ui, + username = oml.user.preferences.username, + users = [{ + id: oml.user.id, + nickname: username, + online: oml.user.online + }]; + oml.api.getUsers(function(result) { + users = users.concat( + result.data.users.filter(function(user) { + return user.peered; + }) + ); + users.forEach(function(user) { + lists.push({ + id: (user.nickname == username ? '' : user.nickname) + ':', + name: Ox._('Library'), + type: 'library', + user: user.nickname + }); + }); + oml.api.getLists(function(result) { + lists = lists.concat(result.data.lists); + if (!ui.lists) { + oml.$ui.mainMenu.update(); + } + ui._lists = {}; + Ox.forEach(lists, function(list) { + ui._lists[list.id] = Ox.extend(list, { + editable: list.user == username && list.type == 'static', + }); + }); + callback(users, lists); + }); + }) }; oml.hasDialogOrScreen = function() { @@ -344,6 +826,11 @@ oml.hasDialogOrScreen = function() { || !!$('.OxScreen').length; }; +oml.reloadList = function() { + Ox.print('RELOAD LIST NOT IMPLEMENTED') + // ... +}; + oml.resizeFilters = function() { // ... };