oml.addList = function() { // addList(isSmart, isFrom[, name[, callback]]) // or addList(list) [=duplicate] var args = arguments, isDuplicate = args.length == 1, isSmart, isFrom, name, callback, list, listData, data, ui = oml.user.ui, username = oml.user.preferences.username; Ox.Request.clearCache('getLists'); oml.api.getLists(function(result) { var lists = result.data.lists, listNames = lists.filter(function(list) { return list.user = username; }).map(function(list) { return list.name; }), query; if (!isDuplicate) { isSmart = args[0]; isFrom = args[1]; name = args[2] || Ox._('Untitled'); callback = args[3]; data = { name: oml.getValidName(name, listNames), type: !isSmart ? 'static' : 'smart' }; if (!isSmart) { if (isFrom) { data.items = ui.listSelection; } } else { if (!isFrom) { data.query = oml.config.user.ui.find; } else { data.query = ui.find; } } addList(); } else { list = args[0]; listData = Ox.getObjectById(Ox.flatten(Ox.values(lists)), list); data = Ox.extend({ name: oml.getValidName(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 if ( !oml.$ui.importExportDialog || !oml.$ui.importExportDialog.is(':visible') ) { $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.$ui.folders.updateOwnLists(); callback && callback(); }); } }; 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.clickLink = function(e) { if ( e.target.hostname == document.location.hostname && !Ox.startsWith(e.target.pathname, '/static') ) { oml.URL.push(e.target.pathname, true); } else { oml.openLink(e.target.href); } }; oml.createLinks = function($element) { function isExternalLink(target) { return target.hostname != document.location.hostname || Ox.startsWith(target.pathname, '/static'); } $element.on({ click: function(e) { var $target = $(e.target); if ($target.is('a')) { e.preventDefault(); if (isExternalLink(e.target)) { oml.openLink(e.target.href); } else { oml.clickLink(e); } } return false; } }); }; (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) { Ox.Request.clearCache(); 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; $list.bindEvent({ draganddropstart: function(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 != '' && data.user == '' ), 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.$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.toggleElement(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.$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.$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) { /* FIXME oml.$ui.folderList[drag.target.folder].value( drag.target.id, 'items', result.data.items ); */ cleanup(250); }); oml.$ui.folders.updateItems(); if (drag.action == 'move') { oml.$ui.list.updateElement(); } }); } 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 == '' ? '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 != '') { 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 == '' && drag.target.user == '' ) || drag.target.selected ) { text = Ox._('{0}
is already in {1}.', [ Ox._(itemText[0] == '"' ? '' : 'These ') + itemText, targetText ]); } else if (drag.target.user != '') { 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 == '' ? '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 == '' ? 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(); } } }; oml.getEditTooltip = function(title) { return function(e) { var $target = $(e.target); return ( $target.is('a') || $target.parents('a').length ? Ox._('Shift+doubleclick to edit') : Ox._('Doubleclick to edit') ) + (title ? ' ' + Ox._(title) : ''); } }; (function() { // Note: getFindState has to run after getListState and getFilterState function everyCondition(conditions, key, operator) { // If every condition has the given key and operator // (excluding conditions where all subconditions match) // returns true, otherwise false return Ox.every(conditions, function(condition) { return condition.key == key && condition.operator == operator; }); } function oneCondition(conditions, key, operator, includeSubconditions) { // If exactly one condition has the given key and operator // (including or excluding conditions where all subconditions match) // returns the corresponding index, otherwise returns -1 var indices = Ox.indicesOf(conditions, function(condition) { return ( condition.conditions ? includeSubconditions && everyCondition(condition.conditions, key, operator) : condition.key == key && condition.operator == operator ); }); return indices.length == 1 ? indices[0] : -1; } oml.getFindState = function(find) { // The find element is populated if exactly one condition in an & query // has a findKey as key and "=" as operator (and all other conditions // are either list or filters), or if all conditions in an | query have // the same filter id as key and "==" as operator Ox.Log('Find', 'getFindState', find) // FIXME: this is still incorrect when you select a lot of filter items // and reload the page (will be advanced) var conditions, indices, state = {index: -1, key: '*', value: ''}, ui = oml.user.ui; if (find.operator == '&') { // number of conditions that are not list or filters conditions = find.conditions.length - !!ui._list - ui._filterState.filter(function(filter) { return filter.index > -1; }).length; // indices of non-advanced find queries indices = oml.config.findKeys.map(function(findKey) { return oneCondition(find.conditions, findKey.id, '='); }).filter(function(index) { return index > -1; }); state = conditions == 1 && indices.length == 1 ? { index: indices[0], key: find.conditions[indices[0]].key, value: Ox.decodeURIComponent(find.conditions[indices[0]].value) } : { index: -1, key: conditions == 0 && indices.length == 0 ? '*' : 'advanced', value: '' }; } else { state = { index: -1, key: 'advanced', value: '' }; Ox.forEach(ui.filters, function(key) { if (everyCondition(find.conditions, key, '==')) { state.key = '*'; return false; } }); } return state; } oml.getFilterState = function(find) { // A filter is selected if exactly one condition in an & query or every // condition in an | query has the filter id as key and "==" as operator var ui = oml.user.ui; return ui.filters.map(function(filter) { // FIXME: cant index be an empty array, instead of -1? var key = filter.id, state = {index: -1, find: Ox.clone(find, true), selected: []}; if (find.operator == '&') { // include conditions where all subconditions match state.index = oneCondition(find.conditions, key, '==', true); if (state.index > -1) { state.selected = find.conditions[state.index].conditions ? find.conditions[state.index].conditions.map(function(condition) { return condition.value; }) : [find.conditions[state.index].value]; } } else { if (everyCondition(find.conditions, key, '==')) { state.index = Ox.range(find.conditions.length); state.selected = find.conditions.map(function(condition) { return condition.value; }); } } if (state.selected.length) { if (Ox.isArray(state.index)) { // every condition in an | query matches this filter state.find = {conditions: [], operator: ''}; } else { // one condition in an & query matches this filter state.find.conditions.splice(state.index, 1); if ( state.find.conditions.length == 1 && state.find.conditions[0].conditions ) { // unwrap single remaining bracketed query state.find = { conditions: state.find.conditions[0].conditions, operator: state.find.conditions[0].operator }; } } } return state; }); } oml.getListState = function(find) { // A list is selected if exactly one condition in an & query has "list" // as key and "==" as operator var index, state = ''; if (find.operator == '&') { index = oneCondition(find.conditions, 'list', '=='); if (index > -1) { state = find.conditions[index].value; } } return state; }; }()); oml.getFilterSizes = function() { var ui = oml.user.ui; return Ox.splitInt( window.innerWidth - ui.showSidebar * ui.sidebarSize - 1, 5 ); }; oml.getIconInfoColor = 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.getInfoHeight = function() { return Math.min( oml.user.ui.sidebarSize, window.innerHeight - 20 - 24 - 16 - 1 ); }; oml.getListData = function(list) { var ui = oml.user.ui; list = Ox.isUndefined(list) ? ui._list : list; return ui._lists ? Ox.getObjectById(ui._lists, list) : {}; }; oml.getListFoldersHeight = function() { var ui = oml.user.ui; return oml.$ui.folder.reduce(function(value, $folder, index) { var items = oml.$ui.folderList[index].options('items').length; return value + 16 + !$folder.options('collapsed') * (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.getLists = function(callback) { var ui = oml.user.ui; Ox.Request.clearCache('getLists'); oml.api.getLists(function(result) { var username = oml.user.preferences.username; ui._lists = result.data.lists.map(function(list) { // FIXME: 'editable' is notoriously vague list.name = list.type == 'libraries' ? Ox._('Libraries') : list.type == 'library' ? Ox._('Library') : list.name; return Ox.extend(list, { editable: list.user == '' && list.type == 'static', own: list.user == '', title: (list.user ? list.user + ': ' : '') + list.name }); }); callback(ui._lists); }); }; oml.getOwnListNames = function() { var ui = oml.user.ui, username = oml.user.preferences.username; return ui._lists.filter(function(list) { return list.user == username; }).filter(function(list) { return list.name; }); } oml.getPageTitle = function(stateOrURL) { var page = Ox.getObjectById( oml.config.pages, Ox.isObject(stateOrURL) ? stateOrURL.page : stateOrURL.slice(1) ); return (page ? page.title + ' – ' : '') + 'Open Media Library'; }; oml.getSortOperator = function(key) { var itemKey = Ox.getObjectById(oml.config.itemKeys, key); return itemKey.sortOperator || Ox.contains( ['string', 'text'], Ox.isArray(itemKey.type) ? itemKey.type[0] : itemKey.type ) ? '+' : '-'; }; oml.getUsers = function(callback) { var ui = oml.user.ui; Ox.Request.clearCache('getUsers'); oml.api.getUsers(function(result) { var users = [{ id: oml.user.id, name: '', online: oml.user.online }].concat( Ox.sortBy(result.data.users.filter(function(user) { return user.peered; }), 'index') ); ui._users = users; callback(users); }); }; oml.getValidName = function(value, names, chars) { var index = 1, length = 256, suffix; if (chars) { value = value.replace( new RegExp('[' + Ox.escapeRegExp(chars) + ']', 'g'), '' ); } value = Ox.clean(Ox.clean(value).slice(0, length)); names = names || []; while (Ox.contains(names, value)) { suffix = ' [' + (++index) + ']'; value = value.replace(/ \[\d+\]$/, '') .slice(0, length - suffix.length) + suffix; }; return value; }; oml.hasDialogOrScreen = function() { return !!$('.OxDialog:visible').length || !!$('.OxFullscreen').length || !!$('.OxScreen').length; }; oml.openLink = function(url) { if (Ox.startsWith(url, 'mailto:')) { window.open(url); } else { window.open('/url=' + encodeURIComponent(url), '_blank'); } }; oml.reloadList = function() { oml.$ui.list.updateElement(); }; oml.renameUser = function(data) { var ui = oml.user.ui, index = Ox.getIndexById(ui._users, data.id), name = ui._users[index].name, set = {}, oldFind = Ox.clone(ui.find, true), newFind = Ox.clone(ui.find, true); ui._users[index].name = data.name; ui._users[index].nickname = data.nickname; set['showFolder.' + name] = null; set['showFolder.' + data.name] = ui.showFolder[name]; Ox.forEach(ui.lists, function(value, key) { var split = key.split(':'), username = split[0], listname = split.slice(1).join(':'); if (username == name) { set['lists.' + key] = null; set['lists.' + data.name + ':' + listname] = value; } }); ui._lists.filter(function(list) { return list.user === '' && list.type == 'smart'; }).forEach(function(list) { updateConditions(list.query); }); oml.UI.set(set, false); updateConditions(newFind); if (!Ox.isEqual(oldFind, newFind)) { oml.replaceURL = true; oml.UI.set({find: newFind}, false); } oml.$ui.folders.updateUser(index); function updateCondition(condition) { if (condition.key == 'list') { condition.value = condition.value.replace( new RegExp('^' + Ox.escapeRegExp(name) + ':'), data.name + ':' ); } } function updateConditions(query) { query.conditions.forEach(function(condition) { if (!condition.conditions) { updateCondition(condition); } else { condition.conditions.forEach(updateCondition); } }); } }; oml.resizeFilters = function() { // ... }; oml.resizeListFolders = function() { // FIXME: does this have to be here? var width = oml.getListFoldersWidth(), columnWidth = width - 58; oml.$ui.librariesList .css({width: width + 'px'}) .resizeColumn('name', columnWidth); Ox.forEach(oml.$ui.folder, function($folder, index) { $folder.css({width: width + 'px'}); oml.$ui.libraryList[index] .css({width: width + 'px'}) .resizeColumn('name', columnWidth); oml.$ui.folderList[index] .css({width: width + 'px'}) .resizeColumn('name', columnWidth); }); }; oml.resizeWindow = function() { oml.$ui.leftPanel && oml.$ui.leftPanel.size(2, oml.getInfoHeight()); oml.resizeListFolders(); oml.$ui.rightPanel && oml.$ui.rightPanel.updateElement(); oml.$ui.list && oml.$ui.list.size(); }; oml.updateFilterMenus = function() { // FIXME: does this have to be a utils function? var selected = oml.$ui.filters.map(function($filter) { return !Ox.isEmpty($filter.options('selected')); }), filtersHaveSelection = !!Ox.sum(selected); oml.$ui.filters.forEach(function($filter, index) { $filter[ selected[index] ? 'enableMenuItem' : 'disableMenuItem' ]('clearFilter'); $filter[ filtersHaveSelection ? 'enableMenuItem' : 'disableMenuItem' ]('clearFilters'); }); }; oml.validatePublicKey = function(value) { return /^[A-Za-z0-9+\/]{43}$/.test(value); };