Open Media Library
This commit is contained in:
commit
2ee2bc178a
|
@ -0,0 +1,9 @@
|
||||||
|
env
|
||||||
|
*.pyc
|
||||||
|
*.gz
|
||||||
|
*.swp
|
||||||
|
*.min.js
|
||||||
|
static/oxjs
|
||||||
|
*~
|
||||||
|
*.db
|
||||||
|
._*
|
|
@ -0,0 +1,19 @@
|
||||||
|
Open Media Library
|
||||||
|
|
||||||
|
== Install ==
|
||||||
|
|
||||||
|
soon
|
||||||
|
|
||||||
|
== Development ==
|
||||||
|
|
||||||
|
mkdir client
|
||||||
|
cd client
|
||||||
|
git clone https://git.0x2620.org/openmedialibrary.git
|
||||||
|
git clone https://git.0x2620.org/openmedialibrary_platform.git platform
|
||||||
|
ln -s openmedialibrary/ctl ctl
|
||||||
|
./ctl update_static
|
||||||
|
./ctl db upgrade
|
||||||
|
./ctl setup
|
||||||
|
|
||||||
|
# and start it
|
||||||
|
./ctl debug
|
|
@ -0,0 +1,366 @@
|
||||||
|
{
|
||||||
|
"coverRatio": 0.75,
|
||||||
|
"itemKeys": [
|
||||||
|
{
|
||||||
|
"id": "*",
|
||||||
|
"title": "All",
|
||||||
|
"type": "text",
|
||||||
|
"find": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "title",
|
||||||
|
"title": "Title",
|
||||||
|
"type": "string",
|
||||||
|
"autocomplete": true,
|
||||||
|
"columnRequired": true,
|
||||||
|
"columnWidth": 256,
|
||||||
|
"find": true,
|
||||||
|
"sort": true,
|
||||||
|
"sortType": "title"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "author",
|
||||||
|
"title": "Author",
|
||||||
|
"type": ["string"],
|
||||||
|
"autocomplete": true,
|
||||||
|
"columnRequired": true,
|
||||||
|
"columnWidth": 192,
|
||||||
|
"filter": true,
|
||||||
|
"sort": true,
|
||||||
|
"sortType": "person"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "edition",
|
||||||
|
"title": "Edition",
|
||||||
|
"type": "string",
|
||||||
|
"columnWidth": 128,
|
||||||
|
"find": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "publisher",
|
||||||
|
"title": "Publisher",
|
||||||
|
"type": "string",
|
||||||
|
"autocomplete": true,
|
||||||
|
"columnWidth": 128,
|
||||||
|
"filter": true,
|
||||||
|
"find": true,
|
||||||
|
"sort": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "place",
|
||||||
|
"title": "Place",
|
||||||
|
"type": ["string"],
|
||||||
|
"columnWidth": 128,
|
||||||
|
"sort": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "country",
|
||||||
|
"title": "Country",
|
||||||
|
"type": "string",
|
||||||
|
"columnWidth": 128,
|
||||||
|
"filter": true,
|
||||||
|
"find": true,
|
||||||
|
"sort": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "date",
|
||||||
|
"title": "Date",
|
||||||
|
"type": "string",
|
||||||
|
"columnWidth": 96,
|
||||||
|
"filter": true,
|
||||||
|
"filterMap": "(-?\\d+)",
|
||||||
|
"sort": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "language",
|
||||||
|
"title": "Language",
|
||||||
|
"type": "string",
|
||||||
|
"columnWidth": 128,
|
||||||
|
"filter": true,
|
||||||
|
"find": true,
|
||||||
|
"sort": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pages",
|
||||||
|
"title": "Pages",
|
||||||
|
"type": "integer",
|
||||||
|
"columnWidth": 96,
|
||||||
|
"format": {"type": "unit", "args": ["pages"]},
|
||||||
|
"sort": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "classification",
|
||||||
|
"title": "Classification",
|
||||||
|
"type": "string",
|
||||||
|
"autocomplete": true,
|
||||||
|
"columnWidth": 192,
|
||||||
|
"find": true,
|
||||||
|
"filter": true,
|
||||||
|
"sort": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "description",
|
||||||
|
"title": "Description",
|
||||||
|
"type": "text",
|
||||||
|
"find": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "extension",
|
||||||
|
"title": "Extension",
|
||||||
|
"type": "string",
|
||||||
|
"columnWidth": 80,
|
||||||
|
"sort": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "size",
|
||||||
|
"title": "Size",
|
||||||
|
"type": "integer",
|
||||||
|
"columnWidth": 64,
|
||||||
|
"format": {"type": "value", "args": ["B"]},
|
||||||
|
"sort": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "created",
|
||||||
|
"title": "First Seen",
|
||||||
|
"type": "date",
|
||||||
|
"columnWidth": 144,
|
||||||
|
"format": {"type": "date", "args": ["%Y-%m-%d %H:%M:%S"]},
|
||||||
|
"sort": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "added",
|
||||||
|
"title": "Date Added",
|
||||||
|
"type": "date",
|
||||||
|
"columnWidth": 144,
|
||||||
|
"format": {"type": "date", "args": ["%Y-%m-%d %H:%M:%S"]},
|
||||||
|
"sort": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "modified",
|
||||||
|
"title": "Last Modified",
|
||||||
|
"type": "date",
|
||||||
|
"columnWidth": 144,
|
||||||
|
"format": {"type": "date", "args": ["%Y-%m-%d %H:%M:%S"]},
|
||||||
|
"sort": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "accessed",
|
||||||
|
"title": "Last Read",
|
||||||
|
"type": "date",
|
||||||
|
"columnWidth": 144,
|
||||||
|
"format": {"type": "date", "args": ["%Y-%m-%d %H:%M:%S"]},
|
||||||
|
"sort": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "timesaccessed",
|
||||||
|
"title": "Times Accessed",
|
||||||
|
"type": "integer",
|
||||||
|
"columnWidth": 64,
|
||||||
|
"format": {"type": "number", "args": []},
|
||||||
|
"sort": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "mediastate",
|
||||||
|
"title": "Media State",
|
||||||
|
"type": "string",
|
||||||
|
"find": true,
|
||||||
|
"sort": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "transferadded",
|
||||||
|
"title": "Added",
|
||||||
|
"type": "date",
|
||||||
|
"format": {"type": "date", "args": ["%Y-%m-%d %H:%M:%S"]},
|
||||||
|
"sort": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "transferprogress",
|
||||||
|
"title": "Progress",
|
||||||
|
"type": "float",
|
||||||
|
"format": {"type": "percent", "args": [1, 0]},
|
||||||
|
"sort": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "id",
|
||||||
|
"title": "ID",
|
||||||
|
"type": "string",
|
||||||
|
"columnWidth": 96,
|
||||||
|
"sort": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "isbn10",
|
||||||
|
"title": "ISBN-10",
|
||||||
|
"type": "string",
|
||||||
|
"columnWidth": 96,
|
||||||
|
"sort": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "isbn13",
|
||||||
|
"title": "ISBN-13",
|
||||||
|
"type": "string",
|
||||||
|
"columnWidth": 96,
|
||||||
|
"sort": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "lccn",
|
||||||
|
"title": "LCCN",
|
||||||
|
"type": "string",
|
||||||
|
"columnWidth": 96,
|
||||||
|
"sort": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "olid",
|
||||||
|
"title": "OLID",
|
||||||
|
"type": "string",
|
||||||
|
"columnWidth": 96,
|
||||||
|
"sort": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "oclc",
|
||||||
|
"title": "OCLC",
|
||||||
|
"type": "string",
|
||||||
|
"columnWidth": 96,
|
||||||
|
"sort": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "mainid",
|
||||||
|
"title": "Main ID",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "random",
|
||||||
|
"title": "Random",
|
||||||
|
"type": "integer",
|
||||||
|
"sort": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"itemViews": [
|
||||||
|
{"id": "info", "title": "Info"},
|
||||||
|
{"id": "book", "title": "Book"}
|
||||||
|
],
|
||||||
|
"lists": [
|
||||||
|
{"title": "Reading List"},
|
||||||
|
{"title": "1968", "query": {
|
||||||
|
"conditions": [{"key": "*", "operator": "=", "value": "1968"}],
|
||||||
|
"operator": "&"
|
||||||
|
}}
|
||||||
|
],
|
||||||
|
"listViews": [
|
||||||
|
{"id": "list", "title": "List"},
|
||||||
|
{"id": "grid", "title": "Grid"}
|
||||||
|
],
|
||||||
|
"locales": ["en", "ar", "hi"],
|
||||||
|
"pages": [
|
||||||
|
{"id": "welcome", "title": "Welcome"},
|
||||||
|
{"id": "app", "title": "Application", "parts": [
|
||||||
|
{"id": "about", "title": "About Open Media Library"},
|
||||||
|
{"id": "faq", "title": "Frequently Asked Questions"},
|
||||||
|
{"id": "terms", "title": "Terms and Conditions"},
|
||||||
|
{"id": "development", "title": "Software Development"},
|
||||||
|
{"id": "contact", "title": "Send Feedback"},
|
||||||
|
{"id": "update", "title": "Software Update"}
|
||||||
|
]},
|
||||||
|
{"id": "preferences", "title": "Preferences", "parts": [
|
||||||
|
{"id": "account", "title": "Account"},
|
||||||
|
{"id": "library", "title": "Library"},
|
||||||
|
{"id": "peering", "title": "Peering"},
|
||||||
|
{"id": "network", "title": "Network"},
|
||||||
|
{"id": "appearance", "title": "Appearance"},
|
||||||
|
{"id": "extensions", "title": "Extensions"},
|
||||||
|
{"id": "advanced", "title": "Advanced"}
|
||||||
|
]},
|
||||||
|
{"id": "users", "title": "Users"},
|
||||||
|
{"id": "devices", "title": "Devices"},
|
||||||
|
{"id": "notifications", "title": "Notifications"},
|
||||||
|
{"id": "transfers", "title": "Transfers"},
|
||||||
|
{"id": "gettingstarted", "title": "Getting Started"},
|
||||||
|
{"id": "help", "title": "Help", "parts": [
|
||||||
|
{"id": "introduction", "title": "Introduction"},
|
||||||
|
{"id": "accounts", "title": "Accounts"},
|
||||||
|
{"id": "navigaion", "title": "Navigation"},
|
||||||
|
{"id": "views", "title": "Views"},
|
||||||
|
{"id": "lists", "title": "Lists"}
|
||||||
|
]},
|
||||||
|
{"id": "documentation", "title": "Documentation"}
|
||||||
|
],
|
||||||
|
"themes": ["oxlight", "oxmedium", "oxdark"],
|
||||||
|
"totals": [
|
||||||
|
{"id": "items"},
|
||||||
|
{"id": "size"}
|
||||||
|
],
|
||||||
|
"user": {
|
||||||
|
"preferences": {
|
||||||
|
"acceptMessage": "",
|
||||||
|
"contact": "",
|
||||||
|
"downloadRate": null,
|
||||||
|
"extensions": "",
|
||||||
|
"importPath": "~/Documents/Open Media Library/Import/",
|
||||||
|
"libraryPath": "~/Documents/Open Media Library/",
|
||||||
|
"receivedRequests": "notify",
|
||||||
|
"rejectMessage": "",
|
||||||
|
"sendDiagnostics": false,
|
||||||
|
"sendRequests": "manually",
|
||||||
|
"uploadRate": null,
|
||||||
|
"username": ""
|
||||||
|
},
|
||||||
|
"ui": {
|
||||||
|
"fileInfo": "extension",
|
||||||
|
"filters": [
|
||||||
|
{"id": "author", "sort": [{"key": "items", "operator": "-"}]},
|
||||||
|
{"id": "publisher", "sort": [{"key": "items", "operator": "-"}]},
|
||||||
|
{"id": "date", "sort": [{"key": "name", "operator": "-"}]},
|
||||||
|
{"id": "language", "sort": [{"key": "items", "operator": "-"}]},
|
||||||
|
{"id": "classification", "sort": [{"key": "items", "operator": "-"}]}
|
||||||
|
],
|
||||||
|
"filtersSize": 176,
|
||||||
|
"find": {"conditions": [], "operator": "&"},
|
||||||
|
"icons": "cover",
|
||||||
|
"item": "",
|
||||||
|
"itemView": "info",
|
||||||
|
"listColumns": ["title", "author", "publisher", "date"],
|
||||||
|
"listColumnWidth": {},
|
||||||
|
"lists": {},
|
||||||
|
"listSelection": [],
|
||||||
|
"listSort": [
|
||||||
|
{"key": "author", "operator": "+"},
|
||||||
|
{"key": "date", "operator": "+"},
|
||||||
|
{"key": "title", "operator": "+"}
|
||||||
|
],
|
||||||
|
"listView": "grid",
|
||||||
|
"locale": "en",
|
||||||
|
"mediaState": {},
|
||||||
|
"page": "welcome",
|
||||||
|
"part": {
|
||||||
|
"app": "about",
|
||||||
|
"preferences": "account",
|
||||||
|
"help": "introduction"
|
||||||
|
},
|
||||||
|
"section": "books",
|
||||||
|
"showBrowser": true,
|
||||||
|
"showDebugMenu": false,
|
||||||
|
"showFileInfo": true,
|
||||||
|
"showFolder": {},
|
||||||
|
"showFilters": true,
|
||||||
|
"showInfo": true,
|
||||||
|
"showSection": {
|
||||||
|
"notifications": {
|
||||||
|
"received": true,
|
||||||
|
"sent": true
|
||||||
|
},
|
||||||
|
"transfers": {
|
||||||
|
"active": true,
|
||||||
|
"queued": true
|
||||||
|
},
|
||||||
|
"users": {
|
||||||
|
"peers": true,
|
||||||
|
"pending": true,
|
||||||
|
"others": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"showSidebar": true,
|
||||||
|
"sidebarSize": 256,
|
||||||
|
"theme": "oxlight",
|
||||||
|
"usersSelection": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
HOST="127.0.0.1:9842"
|
||||||
|
NAME="openmedialibrary"
|
||||||
|
PID="/tmp/$NAME.pid"
|
||||||
|
|
||||||
|
cd `dirname "$0"`
|
||||||
|
if [ -e oml ]; then
|
||||||
|
cd ..
|
||||||
|
fi
|
||||||
|
BASE=`pwd`
|
||||||
|
SYSTEM=`uname -s`
|
||||||
|
|
||||||
|
export PLATFORM_ENV="$BASE/platform/$SYSTEM"
|
||||||
|
if [ $SYSTEM == "Darwin" ]; then
|
||||||
|
export DYLD_FALLBACK_LIBRARY_PATH="$PLATFORM_ENV/lib"
|
||||||
|
fi
|
||||||
|
PATH="$PLATFORM_ENV/bin:$PATH"
|
||||||
|
|
||||||
|
SHARED_ENV="$BASE/platform/Shared"
|
||||||
|
export SHARED_ENV
|
||||||
|
|
||||||
|
PATH="$SHARED_ENV/bin:$PATH"
|
||||||
|
export PATH
|
||||||
|
|
||||||
|
PYTHONPATH="$PLATFORM_ENV/lib/python2.7/site-packages:$SHARED_ENV/lib/python2.7/site-packages:$BASE/$NAME"
|
||||||
|
export PYTHONPATH
|
||||||
|
|
||||||
|
#must be called to update commands in $PATH
|
||||||
|
hash -r 2>/dev/null
|
||||||
|
|
||||||
|
if [ "$1" == "start" ]; then
|
||||||
|
cd $BASE/$NAME
|
||||||
|
if [ -e $PID ]; then
|
||||||
|
echo openmedialibrary already running
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
python oml server PID &
|
||||||
|
exit $?
|
||||||
|
fi
|
||||||
|
if [ "$1" == "debug" ]; then
|
||||||
|
cd $BASE/$NAME
|
||||||
|
if [ -e $PID ]; then
|
||||||
|
echo openmedialibrary already running
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
shift
|
||||||
|
echo Open browser at http://$HOST
|
||||||
|
python oml server $@
|
||||||
|
exit $?
|
||||||
|
fi
|
||||||
|
if [ "$1" == "stop" ]; then
|
||||||
|
test -e $PID && kill `cat $PID`
|
||||||
|
test -e $PID && rm $PID
|
||||||
|
exit $?
|
||||||
|
fi
|
||||||
|
if [ "$1" == "restart" ]; then
|
||||||
|
if [ -e $PID ]; then
|
||||||
|
$0 stop
|
||||||
|
$0 start
|
||||||
|
exit $?
|
||||||
|
else
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [ "$1" == "open" ]; then
|
||||||
|
#time to switch to python and use webbrowser.open_tab?
|
||||||
|
if [ $SYSTEM == "Darwin" ]; then
|
||||||
|
open http://$HOST/
|
||||||
|
else
|
||||||
|
xdg-open http://$HOST/
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd $BASE/$NAME
|
||||||
|
python oml $@
|
||||||
|
exit $?
|
|
@ -0,0 +1,93 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
from __future__ import with_statement
|
||||||
|
|
||||||
|
from contextlib import closing
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tarfile
|
||||||
|
import urllib2
|
||||||
|
|
||||||
|
|
||||||
|
release_url = "http://downloads.openmedialibrary.com/release.json"
|
||||||
|
release_url = "http://c.local/oml/release.json"
|
||||||
|
|
||||||
|
def get_release():
|
||||||
|
with closing(urllib2.urlopen(release_url)) as u:
|
||||||
|
data = json.load(u)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def download(url, filename):
|
||||||
|
dirname = os.path.dirname(filename)
|
||||||
|
if dirname and not os.path.exists(dirname):
|
||||||
|
os.makedirs(dirname)
|
||||||
|
print url, filename
|
||||||
|
with open(filename, 'w') as f:
|
||||||
|
with closing(urllib2.urlopen(url)) as u:
|
||||||
|
data = u.read(4096)
|
||||||
|
while data:
|
||||||
|
f.write(data)
|
||||||
|
data = u.read(4096)
|
||||||
|
|
||||||
|
def install_launchd(base):
|
||||||
|
plist = os.path.expanduser('~/Library/LaunchAgents/com.openmedialibrary.loginscript.plist')
|
||||||
|
with open(plist, 'w') as f:
|
||||||
|
f.write('''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>Label</key>
|
||||||
|
<string>com.openmedialibrary.loginscript</string>
|
||||||
|
<key>ProgramArguments</key>
|
||||||
|
<array>
|
||||||
|
<string>%s/ctl</string>
|
||||||
|
<string>start</string>
|
||||||
|
</array>
|
||||||
|
<key>RunAtLoad</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>''' % base)
|
||||||
|
|
||||||
|
os.system('launchctl load "%s"' % plist)
|
||||||
|
os.system('launchctl start com.openmedialibrary.loginscript')
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
if len(sys.argv) == 1:
|
||||||
|
target = os.path.join(os.curdir, 'openmedialibrary')
|
||||||
|
elif len(sys.argv) != 2:
|
||||||
|
print "usage: %s target" % sys.argv[0]
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
target = sys.argv[1]
|
||||||
|
target = os.path.normpath(os.path.join(os.path.abspath(target)))
|
||||||
|
if not os.path.exists(target):
|
||||||
|
os.makedirs(target)
|
||||||
|
os.chdir(target)
|
||||||
|
release = get_release()
|
||||||
|
packages = ['contrib', 'openmedialibrary']
|
||||||
|
if sys.platform == 'darwin':
|
||||||
|
packages.append('platform')
|
||||||
|
for package in packages:
|
||||||
|
package_tar = '%s.tar.bz2' % package
|
||||||
|
download(release[package]['url'], package_tar)
|
||||||
|
tar = tarfile.open(package_tar)
|
||||||
|
tar.extractall()
|
||||||
|
tar.close()
|
||||||
|
os.unlink(package_tar)
|
||||||
|
os.symlink('openmedialibrary/ctl', 'ctl')
|
||||||
|
with open('release.json', 'w') as fd:
|
||||||
|
json.dump(release, fd, indent=2)
|
||||||
|
|
||||||
|
if sys.platform == 'darwin':
|
||||||
|
cmd = 'Open OpenMediaLibrary.command'
|
||||||
|
with open(cmd, 'w') as fd:
|
||||||
|
fd.write('''#!/bin/sh
|
||||||
|
cd `dirname "$0"`
|
||||||
|
./ctl start
|
||||||
|
./ctl open
|
||||||
|
''')
|
||||||
|
os.chmod(cmd, 0755)
|
||||||
|
install_launchd(target)
|
||||||
|
elif sys.platform == 'linux2':
|
||||||
|
#fixme, do only if on debian/ubuntu
|
||||||
|
os.sysrem('sudo apt-get install python-imaging python-setproctitle python-simplejson')
|
|
@ -0,0 +1 @@
|
||||||
|
Generic single-database configuration.
|
|
@ -0,0 +1,45 @@
|
||||||
|
# A generic, single database configuration.
|
||||||
|
|
||||||
|
[alembic]
|
||||||
|
# template used to generate migration files
|
||||||
|
# file_template = %%(rev)s_%%(slug)s
|
||||||
|
|
||||||
|
# set to 'true' to run the environment during
|
||||||
|
# the 'revision' command, regardless of autogenerate
|
||||||
|
# revision_environment = false
|
||||||
|
|
||||||
|
|
||||||
|
# Logging configuration
|
||||||
|
[loggers]
|
||||||
|
keys = root,sqlalchemy,alembic
|
||||||
|
|
||||||
|
[handlers]
|
||||||
|
keys = console
|
||||||
|
|
||||||
|
[formatters]
|
||||||
|
keys = generic
|
||||||
|
|
||||||
|
[logger_root]
|
||||||
|
level = WARN
|
||||||
|
handlers = console
|
||||||
|
qualname =
|
||||||
|
|
||||||
|
[logger_sqlalchemy]
|
||||||
|
level = WARN
|
||||||
|
handlers =
|
||||||
|
qualname = sqlalchemy.engine
|
||||||
|
|
||||||
|
[logger_alembic]
|
||||||
|
level = INFO
|
||||||
|
handlers =
|
||||||
|
qualname = alembic
|
||||||
|
|
||||||
|
[handler_console]
|
||||||
|
class = StreamHandler
|
||||||
|
args = (sys.stderr,)
|
||||||
|
level = NOTSET
|
||||||
|
formatter = generic
|
||||||
|
|
||||||
|
[formatter_generic]
|
||||||
|
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||||
|
datefmt = %H:%M:%S
|
|
@ -0,0 +1,73 @@
|
||||||
|
from __future__ import with_statement
|
||||||
|
from alembic import context
|
||||||
|
from sqlalchemy import engine_from_config, pool
|
||||||
|
from logging.config import fileConfig
|
||||||
|
|
||||||
|
# this is the Alembic Config object, which provides
|
||||||
|
# access to the values within the .ini file in use.
|
||||||
|
config = context.config
|
||||||
|
|
||||||
|
# Interpret the config file for Python logging.
|
||||||
|
# This line sets up loggers basically.
|
||||||
|
fileConfig(config.config_file_name)
|
||||||
|
|
||||||
|
# add your model's MetaData object here
|
||||||
|
# for 'autogenerate' support
|
||||||
|
# from myapp import mymodel
|
||||||
|
# target_metadata = mymodel.Base.metadata
|
||||||
|
from flask import current_app
|
||||||
|
config.set_main_option('sqlalchemy.url', current_app.config.get('SQLALCHEMY_DATABASE_URI'))
|
||||||
|
target_metadata = current_app.extensions['migrate'].db.metadata
|
||||||
|
|
||||||
|
# other values from the config, defined by the needs of env.py,
|
||||||
|
# can be acquired:
|
||||||
|
# my_important_option = config.get_main_option("my_important_option")
|
||||||
|
# ... etc.
|
||||||
|
|
||||||
|
def run_migrations_offline():
|
||||||
|
"""Run migrations in 'offline' mode.
|
||||||
|
|
||||||
|
This configures the context with just a URL
|
||||||
|
and not an Engine, though an Engine is acceptable
|
||||||
|
here as well. By skipping the Engine creation
|
||||||
|
we don't even need a DBAPI to be available.
|
||||||
|
|
||||||
|
Calls to context.execute() here emit the given string to the
|
||||||
|
script output.
|
||||||
|
|
||||||
|
"""
|
||||||
|
url = config.get_main_option("sqlalchemy.url")
|
||||||
|
context.configure(url=url)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
def run_migrations_online():
|
||||||
|
"""Run migrations in 'online' mode.
|
||||||
|
|
||||||
|
In this scenario we need to create an Engine
|
||||||
|
and associate a connection with the context.
|
||||||
|
|
||||||
|
"""
|
||||||
|
engine = engine_from_config(
|
||||||
|
config.get_section(config.config_ini_section),
|
||||||
|
prefix='sqlalchemy.',
|
||||||
|
poolclass=pool.NullPool)
|
||||||
|
|
||||||
|
connection = engine.connect()
|
||||||
|
context.configure(
|
||||||
|
connection=connection,
|
||||||
|
target_metadata=target_metadata
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
finally:
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
if context.is_offline_mode():
|
||||||
|
run_migrations_offline()
|
||||||
|
else:
|
||||||
|
run_migrations_online()
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
"""${message}
|
||||||
|
|
||||||
|
Revision ID: ${up_revision}
|
||||||
|
Revises: ${down_revision}
|
||||||
|
Create Date: ${create_date}
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = ${repr(up_revision)}
|
||||||
|
down_revision = ${repr(down_revision)}
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
${imports if imports else ""}
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
${upgrades if upgrades else "pass"}
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
${downgrades if downgrades else "pass"}
|
|
@ -0,0 +1,26 @@
|
||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 1ead68a53597
|
||||||
|
Revises: 348720abe06e
|
||||||
|
Create Date: 2014-05-11 17:12:04.427336
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '1ead68a53597'
|
||||||
|
down_revision = '348720abe06e'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
### commands auto generated by Alembic - please adjust! ###
|
||||||
|
pass
|
||||||
|
### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
### commands auto generated by Alembic - please adjust! ###
|
||||||
|
pass
|
||||||
|
### end Alembic commands ###
|
|
@ -0,0 +1,214 @@
|
||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 348720abe06e
|
||||||
|
Revises: None
|
||||||
|
Create Date: 2014-05-11 12:24:57.346130
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '348720abe06e'
|
||||||
|
down_revision = None
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('user',
|
||||||
|
sa.Column('created', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('modified', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('id', sa.String(length=43), nullable=False),
|
||||||
|
sa.Column('info', sa.PickleType(), nullable=True),
|
||||||
|
sa.Column('nickname', sa.String(length=256), nullable=True),
|
||||||
|
sa.Column('pending', sa.String(length=64), nullable=True),
|
||||||
|
sa.Column('peered', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('online', sa.Boolean(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('person',
|
||||||
|
sa.Column('name', sa.String(length=1024), nullable=False),
|
||||||
|
sa.Column('sortname', sa.String(), nullable=True),
|
||||||
|
sa.Column('numberofnames', sa.Integer(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('name')
|
||||||
|
)
|
||||||
|
op.create_table('work',
|
||||||
|
sa.Column('created', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('modified', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('id', sa.String(length=32), nullable=False),
|
||||||
|
sa.Column('meta', sa.PickleType(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('changelog',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('created', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('user_id', sa.String(length=43), nullable=True),
|
||||||
|
sa.Column('revision', sa.BigInteger(), nullable=True),
|
||||||
|
sa.Column('data', sa.Text(), nullable=True),
|
||||||
|
sa.Column('sig', sa.String(length=96), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('list',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('name', sa.String(), nullable=True),
|
||||||
|
sa.Column('position', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('type', sa.String(length=64), nullable=True),
|
||||||
|
sa.Column('query', sa.PickleType(), nullable=True),
|
||||||
|
sa.Column('user_id', sa.String(length=43), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('edition',
|
||||||
|
sa.Column('created', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('modified', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('id', sa.String(length=32), nullable=False),
|
||||||
|
sa.Column('meta', sa.PickleType(), nullable=True),
|
||||||
|
sa.Column('work_id', sa.String(length=32), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['work_id'], ['work.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('item',
|
||||||
|
sa.Column('created', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('modified', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('id', sa.String(length=32), nullable=False),
|
||||||
|
sa.Column('info', sa.PickleType(), nullable=True),
|
||||||
|
sa.Column('meta', sa.PickleType(), nullable=True),
|
||||||
|
sa.Column('added', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('accessed', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('timesaccessed', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('transferadded', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('transferprogress', sa.Float(), nullable=True),
|
||||||
|
sa.Column('edition_id', sa.String(length=32), nullable=True),
|
||||||
|
sa.Column('work_id', sa.String(length=32), nullable=True),
|
||||||
|
sa.Column('sort_title', sa.String(length=1000), nullable=True),
|
||||||
|
sa.Column('sort_author', sa.String(length=1000), nullable=True),
|
||||||
|
sa.Column('sort_language', sa.String(length=1000), nullable=True),
|
||||||
|
sa.Column('sort_publisher', sa.String(length=1000), nullable=True),
|
||||||
|
sa.Column('sort_place', sa.String(length=1000), nullable=True),
|
||||||
|
sa.Column('sort_country', sa.String(length=1000), nullable=True),
|
||||||
|
sa.Column('sort_date', sa.String(length=1000), nullable=True),
|
||||||
|
sa.Column('sort_pages', sa.BigInteger(), nullable=True),
|
||||||
|
sa.Column('sort_classification', sa.String(length=1000), nullable=True),
|
||||||
|
sa.Column('sort_id', sa.String(length=1000), nullable=True),
|
||||||
|
sa.Column('sort_isbn10', sa.String(length=1000), nullable=True),
|
||||||
|
sa.Column('sort_isbn13', sa.String(length=1000), nullable=True),
|
||||||
|
sa.Column('sort_lccn', sa.String(length=1000), nullable=True),
|
||||||
|
sa.Column('sort_olid', sa.String(length=1000), nullable=True),
|
||||||
|
sa.Column('sort_oclc', sa.String(length=1000), nullable=True),
|
||||||
|
sa.Column('sort_extension', sa.String(length=1000), nullable=True),
|
||||||
|
sa.Column('sort_size', sa.BigInteger(), nullable=True),
|
||||||
|
sa.Column('sort_created', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('sort_added', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('sort_modified', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('sort_accessed', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('sort_timesaccessed', sa.BigInteger(), nullable=True),
|
||||||
|
sa.Column('sort_mediastate', sa.String(length=1000), nullable=True),
|
||||||
|
sa.Column('sort_transferadded', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('sort_transferprogress', sa.Float(), nullable=True),
|
||||||
|
sa.Column('sort_random', sa.BigInteger(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['edition_id'], ['edition.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['work_id'], ['work.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_item_sort_accessed'), 'item', ['sort_accessed'], unique=False)
|
||||||
|
op.create_index(op.f('ix_item_sort_added'), 'item', ['sort_added'], unique=False)
|
||||||
|
op.create_index(op.f('ix_item_sort_author'), 'item', ['sort_author'], unique=False)
|
||||||
|
op.create_index(op.f('ix_item_sort_classification'), 'item', ['sort_classification'], unique=False)
|
||||||
|
op.create_index(op.f('ix_item_sort_country'), 'item', ['sort_country'], unique=False)
|
||||||
|
op.create_index(op.f('ix_item_sort_created'), 'item', ['sort_created'], unique=False)
|
||||||
|
op.create_index(op.f('ix_item_sort_date'), 'item', ['sort_date'], unique=False)
|
||||||
|
op.create_index(op.f('ix_item_sort_extension'), 'item', ['sort_extension'], unique=False)
|
||||||
|
op.create_index(op.f('ix_item_sort_id'), 'item', ['sort_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_item_sort_isbn10'), 'item', ['sort_isbn10'], unique=False)
|
||||||
|
op.create_index(op.f('ix_item_sort_isbn13'), 'item', ['sort_isbn13'], unique=False)
|
||||||
|
op.create_index(op.f('ix_item_sort_language'), 'item', ['sort_language'], unique=False)
|
||||||
|
op.create_index(op.f('ix_item_sort_lccn'), 'item', ['sort_lccn'], unique=False)
|
||||||
|
op.create_index(op.f('ix_item_sort_mediastate'), 'item', ['sort_mediastate'], unique=False)
|
||||||
|
op.create_index(op.f('ix_item_sort_modified'), 'item', ['sort_modified'], unique=False)
|
||||||
|
op.create_index(op.f('ix_item_sort_oclc'), 'item', ['sort_oclc'], unique=False)
|
||||||
|
op.create_index(op.f('ix_item_sort_olid'), 'item', ['sort_olid'], unique=False)
|
||||||
|
op.create_index(op.f('ix_item_sort_pages'), 'item', ['sort_pages'], unique=False)
|
||||||
|
op.create_index(op.f('ix_item_sort_place'), 'item', ['sort_place'], unique=False)
|
||||||
|
op.create_index(op.f('ix_item_sort_publisher'), 'item', ['sort_publisher'], unique=False)
|
||||||
|
op.create_index(op.f('ix_item_sort_random'), 'item', ['sort_random'], unique=False)
|
||||||
|
op.create_index(op.f('ix_item_sort_size'), 'item', ['sort_size'], unique=False)
|
||||||
|
op.create_index(op.f('ix_item_sort_timesaccessed'), 'item', ['sort_timesaccessed'], unique=False)
|
||||||
|
op.create_index(op.f('ix_item_sort_title'), 'item', ['sort_title'], unique=False)
|
||||||
|
op.create_index(op.f('ix_item_sort_transferadded'), 'item', ['sort_transferadded'], unique=False)
|
||||||
|
op.create_index(op.f('ix_item_sort_transferprogress'), 'item', ['sort_transferprogress'], unique=False)
|
||||||
|
op.create_table('useritem',
|
||||||
|
sa.Column('user_id', sa.String(length=43), nullable=True),
|
||||||
|
sa.Column('item_id', sa.String(length=32), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['item_id'], ['item.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['user.id'], )
|
||||||
|
)
|
||||||
|
op.create_table('find',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('item_id', sa.String(length=32), nullable=True),
|
||||||
|
sa.Column('key', sa.String(length=200), nullable=True),
|
||||||
|
sa.Column('value', sa.Text(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['item_id'], ['item.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_find_key'), 'find', ['key'], unique=False)
|
||||||
|
op.create_table('file',
|
||||||
|
sa.Column('created', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('modified', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('sha1', sa.String(length=32), nullable=False),
|
||||||
|
sa.Column('path', sa.String(length=2048), nullable=True),
|
||||||
|
sa.Column('info', sa.PickleType(), nullable=True),
|
||||||
|
sa.Column('item_id', sa.String(length=32), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['item_id'], ['item.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('sha1')
|
||||||
|
)
|
||||||
|
op.create_table('listitem',
|
||||||
|
sa.Column('list_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('item_id', sa.String(length=32), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['item_id'], ['item.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['list_id'], ['list.id'], )
|
||||||
|
)
|
||||||
|
### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('listitem')
|
||||||
|
op.drop_table('file')
|
||||||
|
op.drop_index(op.f('ix_find_key'), table_name='find')
|
||||||
|
op.drop_table('find')
|
||||||
|
op.drop_table('useritem')
|
||||||
|
op.drop_index(op.f('ix_item_sort_transferprogress'), table_name='item')
|
||||||
|
op.drop_index(op.f('ix_item_sort_transferadded'), table_name='item')
|
||||||
|
op.drop_index(op.f('ix_item_sort_title'), table_name='item')
|
||||||
|
op.drop_index(op.f('ix_item_sort_timesaccessed'), table_name='item')
|
||||||
|
op.drop_index(op.f('ix_item_sort_size'), table_name='item')
|
||||||
|
op.drop_index(op.f('ix_item_sort_random'), table_name='item')
|
||||||
|
op.drop_index(op.f('ix_item_sort_publisher'), table_name='item')
|
||||||
|
op.drop_index(op.f('ix_item_sort_place'), table_name='item')
|
||||||
|
op.drop_index(op.f('ix_item_sort_pages'), table_name='item')
|
||||||
|
op.drop_index(op.f('ix_item_sort_olid'), table_name='item')
|
||||||
|
op.drop_index(op.f('ix_item_sort_oclc'), table_name='item')
|
||||||
|
op.drop_index(op.f('ix_item_sort_modified'), table_name='item')
|
||||||
|
op.drop_index(op.f('ix_item_sort_mediastate'), table_name='item')
|
||||||
|
op.drop_index(op.f('ix_item_sort_lccn'), table_name='item')
|
||||||
|
op.drop_index(op.f('ix_item_sort_language'), table_name='item')
|
||||||
|
op.drop_index(op.f('ix_item_sort_isbn13'), table_name='item')
|
||||||
|
op.drop_index(op.f('ix_item_sort_isbn10'), table_name='item')
|
||||||
|
op.drop_index(op.f('ix_item_sort_id'), table_name='item')
|
||||||
|
op.drop_index(op.f('ix_item_sort_extension'), table_name='item')
|
||||||
|
op.drop_index(op.f('ix_item_sort_date'), table_name='item')
|
||||||
|
op.drop_index(op.f('ix_item_sort_created'), table_name='item')
|
||||||
|
op.drop_index(op.f('ix_item_sort_country'), table_name='item')
|
||||||
|
op.drop_index(op.f('ix_item_sort_classification'), table_name='item')
|
||||||
|
op.drop_index(op.f('ix_item_sort_author'), table_name='item')
|
||||||
|
op.drop_index(op.f('ix_item_sort_added'), table_name='item')
|
||||||
|
op.drop_index(op.f('ix_item_sort_accessed'), table_name='item')
|
||||||
|
op.drop_table('item')
|
||||||
|
op.drop_table('edition')
|
||||||
|
op.drop_table('list')
|
||||||
|
op.drop_table('changelog')
|
||||||
|
op.drop_table('work')
|
||||||
|
op.drop_table('person')
|
||||||
|
op.drop_table('user')
|
||||||
|
### end Alembic commands ###
|
|
@ -0,0 +1,16 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vi:si:et:sw=4:sts=4:ts=4
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
import app
|
||||||
|
import server
|
||||||
|
|
||||||
|
if len(sys.argv) > 1 and sys.argv[1] == 'server':
|
||||||
|
import server
|
||||||
|
server.run()
|
||||||
|
else:
|
||||||
|
app.manager.run()
|
|
@ -0,0 +1,6 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vi:si:et:sw=4:sts=4:ts=4
|
||||||
|
|
||||||
|
import item.api
|
||||||
|
import user.api
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vi:si:et:sw=4:sts=4:ts=4
|
||||||
|
|
||||||
|
from flask import Flask
|
||||||
|
from flask.ext.script import Manager
|
||||||
|
from flask.ext.migrate import Migrate, MigrateCommand
|
||||||
|
|
||||||
|
|
||||||
|
import oxflask.api
|
||||||
|
|
||||||
|
import settings
|
||||||
|
from settings import db
|
||||||
|
|
||||||
|
import changelog
|
||||||
|
import item.models
|
||||||
|
import user.models
|
||||||
|
import item.person
|
||||||
|
|
||||||
|
import item.api
|
||||||
|
import user.api
|
||||||
|
|
||||||
|
import item.views
|
||||||
|
import commands
|
||||||
|
|
||||||
|
app = Flask('openmedialibrary', static_folder=settings.static_path)
|
||||||
|
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////%s' % settings.db_path
|
||||||
|
app.register_blueprint(oxflask.api.app)
|
||||||
|
app.register_blueprint(item.views.app)
|
||||||
|
db.init_app(app)
|
||||||
|
|
||||||
|
migrate = Migrate(app, db)
|
||||||
|
|
||||||
|
manager = Manager(app)
|
||||||
|
manager.add_command('db', MigrateCommand)
|
||||||
|
manager.add_command('setup', commands.Setup)
|
||||||
|
manager.add_command('update_static', commands.UpdateStatic)
|
||||||
|
manager.add_command('release', commands.Release)
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
@app.route('/<path:path>')
|
||||||
|
def main(path=None):
|
||||||
|
return app.send_static_file('html/oml.html')
|
||||||
|
|
|
@ -0,0 +1,230 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vi:si:et:sw=4:sts=4:ts=4
|
||||||
|
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from ed25519_utils import valid
|
||||||
|
|
||||||
|
import settings
|
||||||
|
from settings import db
|
||||||
|
import state
|
||||||
|
from websocket import trigger_event
|
||||||
|
|
||||||
|
class Changelog(db.Model):
|
||||||
|
'''
|
||||||
|
additem itemid metadata from file (info) + OLID
|
||||||
|
edititem itemid name->id (i.e. olid-> OL...M)
|
||||||
|
removeitem itemid
|
||||||
|
addlist name
|
||||||
|
editlist name {name: newname}
|
||||||
|
orderlists [name, name, name]
|
||||||
|
removelist name
|
||||||
|
additemtolist listname itemid
|
||||||
|
removeitemfromlist listname itemid
|
||||||
|
editusername username
|
||||||
|
editcontact string
|
||||||
|
addpeer peerid peername
|
||||||
|
removepeer peerid peername
|
||||||
|
'''
|
||||||
|
id = db.Column(db.Integer(), primary_key=True)
|
||||||
|
|
||||||
|
created = db.Column(db.DateTime())
|
||||||
|
|
||||||
|
user_id = db.Column(db.String(43))
|
||||||
|
revision = db.Column(db.BigInteger())
|
||||||
|
data = db.Column(db.Text())
|
||||||
|
sig = db.Column(db.String(96))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def record(cls, user, action, *args):
|
||||||
|
c = cls()
|
||||||
|
c.created = datetime.now()
|
||||||
|
c.user_id = user.id
|
||||||
|
c.revision = cls.query.filter_by(user_id=user.id).count()
|
||||||
|
c.data = json.dumps([action, args])
|
||||||
|
timestamp = c.timestamp
|
||||||
|
_data = str(c.revision) + str(timestamp) + c.data
|
||||||
|
c.sig = settings.sk.sign(_data, encoding='base64')
|
||||||
|
db.session.add(c)
|
||||||
|
db.session.commit()
|
||||||
|
if state.online:
|
||||||
|
state.nodes.queue('online', 'pushChanges', [c.json()])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def timestamp(self):
|
||||||
|
return self.created.strftime('%s')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def apply_change(cls, user, change, rebuild=False):
|
||||||
|
revision, timestamp, sig, data = change
|
||||||
|
last = Changelog.query.filter_by(user_id=user.id).order_by('-revision').first()
|
||||||
|
next_revision = last.revision + 1 if last else 0
|
||||||
|
if revision == next_revision:
|
||||||
|
_data = str(revision) + str(timestamp) + data
|
||||||
|
if rebuild:
|
||||||
|
sig = settings.sk.sign(_data, encoding='base64')
|
||||||
|
if valid(user.id, _data, sig):
|
||||||
|
c = cls()
|
||||||
|
c.created = datetime.now()
|
||||||
|
c.user_id = user.id
|
||||||
|
c.revision = revision
|
||||||
|
c.data = data
|
||||||
|
c.sig = sig
|
||||||
|
action, args = json.loads(data)
|
||||||
|
print 'apply change', action
|
||||||
|
if getattr(c, 'action_' + action)(user, timestamp, *args):
|
||||||
|
print 'change applied'
|
||||||
|
db.session.add(c)
|
||||||
|
db.session.commit()
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print 'INVLAID SIGNATURE ON CHANGE', change
|
||||||
|
raise Exception, 'invalid signature'
|
||||||
|
else:
|
||||||
|
print 'revsion does not match! got', revision, 'expecting', next_revision
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return self.data
|
||||||
|
|
||||||
|
def verify(self):
|
||||||
|
_data = str(self.revision) + str(self.timestamp) + self.data
|
||||||
|
return valid(self.user_id, _data, self.sig)
|
||||||
|
|
||||||
|
def json(self):
|
||||||
|
return [self.revision, self.timestamp, self.sig, self.data]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def restore(cls, user_id, path=None):
|
||||||
|
from user.models import User
|
||||||
|
user = User.get_or_create(user_id)
|
||||||
|
if not path:
|
||||||
|
path = '/tmp/oml_changelog_%s.json' % user_id
|
||||||
|
with open(path, 'r') as fd:
|
||||||
|
for change in fd:
|
||||||
|
change = json.loads(change)
|
||||||
|
cls.apply_change(user, change, user_id == settings.USER_ID)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def export(cls, user_id, path=None):
|
||||||
|
if not path:
|
||||||
|
path = '/tmp/oml_changelog_%s.json' % user_id
|
||||||
|
with open(path, 'w') as fd:
|
||||||
|
for c in cls.query.filter_by(user_id=user_id).order_by('revision'):
|
||||||
|
fd.write(json.dumps(c.json()) + '\n')
|
||||||
|
|
||||||
|
def action_additem(self, user, timestamp, itemid, info):
|
||||||
|
from item.models import Item
|
||||||
|
i = Item.get(itemid)
|
||||||
|
if i and i.timestamp > timestamp:
|
||||||
|
return True
|
||||||
|
if not i:
|
||||||
|
i = Item.get_or_create(itemid, info)
|
||||||
|
i.users.append(user)
|
||||||
|
i.update()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def action_edititem(self, user, timestamp, itemid, meta):
|
||||||
|
from item.models import Item
|
||||||
|
i = Item.get(itemid)
|
||||||
|
if i.timestamp > timestamp:
|
||||||
|
return True
|
||||||
|
key = meta.keys()[0]
|
||||||
|
if not meta[key] and i.meta.get('mainid') == key:
|
||||||
|
print 'remove id mapping', key, meta[key], 'currently', i.meta[key]
|
||||||
|
i.update_mainid(key, meta[key])
|
||||||
|
elif meta[key] and (i.meta.get('mainid') != key or meta[key] != i.meta.get(key)):
|
||||||
|
print 'new mapping', key, meta[key], 'currently', i.meta.get('mainid'), i.meta.get(i.meta.get('mainid'))
|
||||||
|
i.update_mainid(key, meta[key])
|
||||||
|
return True
|
||||||
|
|
||||||
|
def action_removeitem(self, user, timestamp, itemid):
|
||||||
|
from item.models import Item
|
||||||
|
i = Item.get(itemid)
|
||||||
|
if not i or i.timestamp > timestamp:
|
||||||
|
return True
|
||||||
|
i.users.remove(user)
|
||||||
|
if i.users:
|
||||||
|
i.update()
|
||||||
|
else:
|
||||||
|
db.session.delete(i)
|
||||||
|
db.session.commit()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def action_addlist(self, user, timestamp, name, query=None):
|
||||||
|
from user.models import List
|
||||||
|
l = List.create(user.id, name)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def action_editlist(self, user, timestamp, name, new):
|
||||||
|
from user.models import List
|
||||||
|
l = List.get_or_create(user.id, name)
|
||||||
|
if 'name' in new:
|
||||||
|
l.name = new['name']
|
||||||
|
l.save()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def action_orderlists(self, user, timestamp, lists):
|
||||||
|
from user.models import List
|
||||||
|
position = 0
|
||||||
|
for name in lists:
|
||||||
|
l = List.get_or_create(user.id, name)
|
||||||
|
l.position = position
|
||||||
|
l.save()
|
||||||
|
position += 1
|
||||||
|
return True
|
||||||
|
|
||||||
|
def action_removelist(self, user, timestamp, name):
|
||||||
|
from user.models import List
|
||||||
|
l = List.get(user.id, name)
|
||||||
|
if l:
|
||||||
|
l.remove()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def action_addlistitem(self, user, timestamp, name, itemid):
|
||||||
|
from item.models import Item
|
||||||
|
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()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def action_removelistitem(self, user, timestamp, name, itemid):
|
||||||
|
from item.models import Item
|
||||||
|
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()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def action_editusername(self, user, timestamp, username):
|
||||||
|
user.info['username'] = username
|
||||||
|
user.save()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def action_editcontact(self, user, timestamp, contact):
|
||||||
|
user.info['contact'] = contact
|
||||||
|
user.save()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def action_adduser(self, user, timestamp, peerid, username):
|
||||||
|
from user.models import User
|
||||||
|
if not 'users' in user.info:
|
||||||
|
user.info['users'] = {}
|
||||||
|
user.info['users'][peerid] = username
|
||||||
|
user.save()
|
||||||
|
User.get_or_create(peerid)
|
||||||
|
#fixme, add username to user?
|
||||||
|
return True
|
||||||
|
|
||||||
|
def action_removeuser(self, user, timestamp, peerid):
|
||||||
|
if 'users' in user.info and peerid in user.info['users']:
|
||||||
|
del user.info['users'][peerid]
|
||||||
|
user.save()
|
||||||
|
#fixme, remove from User table if no other connection exists
|
||||||
|
return True
|
|
@ -0,0 +1,115 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vi:si:et:sw=4:sts=4:ts=4
|
||||||
|
|
||||||
|
from flask.ext.script import Command
|
||||||
|
|
||||||
|
|
||||||
|
class Setup(Command):
|
||||||
|
"""
|
||||||
|
setup new node
|
||||||
|
"""
|
||||||
|
def run(self):
|
||||||
|
import setup
|
||||||
|
setup.create_default_lists()
|
||||||
|
|
||||||
|
class UpdateStatic(Command):
|
||||||
|
"""
|
||||||
|
setup new node
|
||||||
|
"""
|
||||||
|
def run(self):
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
import settings
|
||||||
|
|
||||||
|
def r(*cmd):
|
||||||
|
print ' '.join(cmd)
|
||||||
|
return subprocess.call(cmd)
|
||||||
|
|
||||||
|
oxjs = os.path.join(settings.static_path, 'oxjs')
|
||||||
|
if not os.path.exists(oxjs):
|
||||||
|
r('git', 'clone', 'https://git.0x2620.org/oxjs.git', oxjs)
|
||||||
|
r('python', os.path.join(oxjs, 'tools', 'build', 'build.py'))
|
||||||
|
r('python', os.path.join(settings.static_path, 'py', 'build.py'))
|
||||||
|
|
||||||
|
class Release(Command):
|
||||||
|
"""
|
||||||
|
release new version
|
||||||
|
"""
|
||||||
|
def run(self):
|
||||||
|
print 'checking...'
|
||||||
|
import settings
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import json
|
||||||
|
import hashlib
|
||||||
|
import ed25519
|
||||||
|
from os.path import join, exists, dirname
|
||||||
|
|
||||||
|
root_dir = dirname(settings.base_dir)
|
||||||
|
os.chdir(root_dir)
|
||||||
|
|
||||||
|
def run(*cmd):
|
||||||
|
p = subprocess.Popen(cmd)
|
||||||
|
p.wait()
|
||||||
|
return p.returncode
|
||||||
|
|
||||||
|
def get(*cmd):
|
||||||
|
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
|
stdout, error = p.communicate()
|
||||||
|
return stdout
|
||||||
|
|
||||||
|
def version(module):
|
||||||
|
os.chdir(join(root_dir, module))
|
||||||
|
version = get('git', 'log', '-1', '--format=%cd', '--date=iso').split(' ')[0].replace('-', '')
|
||||||
|
version += '-' + get('git', 'rev-list', 'HEAD', '--count').strip()
|
||||||
|
version += '-' + get('git', 'describe', '--always').strip()
|
||||||
|
os.chdir(root_dir)
|
||||||
|
return version
|
||||||
|
|
||||||
|
with open(os.path.expanduser('~/Private/openmedialibrary_release.key')) as fd:
|
||||||
|
SIG_KEY=ed25519.SigningKey(fd.read())
|
||||||
|
SIG_ENCODING='base64'
|
||||||
|
|
||||||
|
def sign(release):
|
||||||
|
value = []
|
||||||
|
for module in sorted(release['modules']):
|
||||||
|
value += ['%s/%s' % (release['modules'][module]['version'], release['modules'][module]['sha1'])]
|
||||||
|
value = '\n'.join(value)
|
||||||
|
sig = SIG_KEY.sign(value, encoding=SIG_ENCODING)
|
||||||
|
release['signature'] = sig
|
||||||
|
|
||||||
|
def sha1sum(path):
|
||||||
|
h = hashlib.sha1()
|
||||||
|
with open(path) as fd:
|
||||||
|
for chunk in iter(lambda: fd.read(128*h.block_size), ''):
|
||||||
|
h.update(chunk)
|
||||||
|
return h.hexdigest()
|
||||||
|
|
||||||
|
MODULES = ['platform', 'openmedialibrary']
|
||||||
|
VERSIONS = {module:version(module) for module in MODULES}
|
||||||
|
|
||||||
|
EXCLUDE=[
|
||||||
|
'--exclude', '.git', '--exclude', '.bzr',
|
||||||
|
'--exclude', '.*.swp', '--exclude', '._*', '--exclude', '.DS_Store'
|
||||||
|
]
|
||||||
|
|
||||||
|
#run('./ctl', 'update_static')
|
||||||
|
for module in MODULES:
|
||||||
|
tar = join('updates', '%s-%s.tar.bz2' % (module, VERSIONS[module]))
|
||||||
|
if not exists(tar):
|
||||||
|
cmd = ['tar', 'cvjf', tar, '%s/' % module] + EXCLUDE
|
||||||
|
if module in ('openmedialibrary', ):
|
||||||
|
cmd += ['--exclude', '*.pyc']
|
||||||
|
if module == 'openmedialibrary':
|
||||||
|
cmd += ['--exclude', 'oxjs/examples', '--exclude', 'gunicorn.pid']
|
||||||
|
run(*cmd)
|
||||||
|
release = {}
|
||||||
|
release['modules'] = {module: {
|
||||||
|
'name': '%s-%s.tar.bz2' % (module, VERSIONS[module]),
|
||||||
|
'version': VERSIONS[module],
|
||||||
|
'sha1': sha1sum(join('updates', '%s-%s.tar.bz2' % (module, VERSIONS[module])))
|
||||||
|
} for module in MODULES}
|
||||||
|
sign(release)
|
||||||
|
with open('updates/release.json', 'w') as fd:
|
||||||
|
json.dump(release, fd, indent=2)
|
||||||
|
print 'signed latest release in updates/release.json'
|
|
@ -0,0 +1,44 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vi:si:et:sw=4:sts=4:ts=4
|
||||||
|
|
||||||
|
# DHT placeholder
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import ed25519
|
||||||
|
import json
|
||||||
|
import settings
|
||||||
|
|
||||||
|
base = settings.server['directory_service']
|
||||||
|
|
||||||
|
def get(vk):
|
||||||
|
id = vk.to_ascii(encoding='base64')
|
||||||
|
url ='%s/%s' % (base, id)
|
||||||
|
r = requests.get(url)
|
||||||
|
sig = r.headers.get('X-Ed25519-Signature')
|
||||||
|
data = r.content
|
||||||
|
if sig and data:
|
||||||
|
vk = ed25519.VerifyingKey(id, encoding='base64')
|
||||||
|
try:
|
||||||
|
vk.verify(sig, data, encoding='base64')
|
||||||
|
data = json.loads(data)
|
||||||
|
except ed25519.BadSignatureError:
|
||||||
|
print 'invalid signature'
|
||||||
|
data = None
|
||||||
|
return data
|
||||||
|
|
||||||
|
def put(sk, data):
|
||||||
|
id = sk.get_verifying_key().to_ascii(encoding='base64')
|
||||||
|
data = json.dumps(data)
|
||||||
|
sig = sk.sign(data, encoding='base64')
|
||||||
|
url ='%s/%s' % (base, id)
|
||||||
|
headers = {
|
||||||
|
'X-Ed25519-Signature': sig
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
r = requests.put(url, data, headers=headers)
|
||||||
|
except:
|
||||||
|
import traceback
|
||||||
|
print 'directory.put failed:', data
|
||||||
|
traceback.print_exc()
|
||||||
|
return False
|
||||||
|
return r.status_code == 200
|
|
@ -0,0 +1,46 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vi:si:et:sw=4:sts=4:ts=4
|
||||||
|
from __future__ import division
|
||||||
|
|
||||||
|
from threading import Thread
|
||||||
|
import time
|
||||||
|
|
||||||
|
import state
|
||||||
|
|
||||||
|
class Downloads(Thread):
|
||||||
|
|
||||||
|
def __init__(self, app):
|
||||||
|
self._app = app
|
||||||
|
self._running = True
|
||||||
|
Thread.__init__(self)
|
||||||
|
self.daemon = True
|
||||||
|
self.start()
|
||||||
|
|
||||||
|
def download_next(self):
|
||||||
|
import item.models
|
||||||
|
for i in item.models.Item.query.filter(
|
||||||
|
item.models.Item.transferadded!=None).filter(
|
||||||
|
item.models.Item.transferprogress<1):
|
||||||
|
print 'DOWNLOAD', i, i.users
|
||||||
|
for p in i.users:
|
||||||
|
if state.nodes.check_online(p.id):
|
||||||
|
r = state.nodes.download(p.id, i)
|
||||||
|
print 'download ok?', r
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
time.sleep(2)
|
||||||
|
with self._app.app_context():
|
||||||
|
while self._running:
|
||||||
|
if state.online:
|
||||||
|
self.download_next()
|
||||||
|
time.sleep(10)
|
||||||
|
else:
|
||||||
|
time.sleep(20)
|
||||||
|
|
||||||
|
def join(self):
|
||||||
|
self._running = False
|
||||||
|
self._q.put(None)
|
||||||
|
return Thread.join(self)
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
import ed25519
|
||||||
|
ENCODING='base64'
|
||||||
|
|
||||||
|
def valid(key, value, sig):
|
||||||
|
'''
|
||||||
|
validate that value was signed by key
|
||||||
|
'''
|
||||||
|
vk = ed25519.VerifyingKey(str(key), encoding=ENCODING)
|
||||||
|
try:
|
||||||
|
vk.verify(str(sig), str(value), encoding=ENCODING)
|
||||||
|
#except ed25519.BadSignatureError:
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
return True
|
|
@ -0,0 +1,19 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vi:si:et:sw=4:sts=4:ts=4
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import models
|
||||||
|
|
||||||
|
import ox
|
||||||
|
|
||||||
|
import scan
|
||||||
|
|
||||||
|
def add(path):
|
||||||
|
info = scan.get_metadata(path)
|
||||||
|
id = info.pop('id')
|
||||||
|
item = models.Item.get_or_create(id)
|
||||||
|
item.path = path
|
||||||
|
item.info = info
|
||||||
|
models.db.session.add(item)
|
||||||
|
models.db.session.commit()
|
||||||
|
|
|
@ -0,0 +1,210 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vi:si:et:sw=4:sts=4:ts=4
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from flask import json
|
||||||
|
from oxflask.api import actions
|
||||||
|
from oxflask.shortcuts import returns_json
|
||||||
|
|
||||||
|
from oml import utils
|
||||||
|
import query
|
||||||
|
|
||||||
|
import models
|
||||||
|
import settings
|
||||||
|
from changelog import Changelog
|
||||||
|
import re
|
||||||
|
import state
|
||||||
|
|
||||||
|
import utils
|
||||||
|
|
||||||
|
@returns_json
|
||||||
|
def find(request):
|
||||||
|
'''
|
||||||
|
find items
|
||||||
|
'''
|
||||||
|
response = {}
|
||||||
|
data = json.loads(request.form['data']) if 'data' in request.form else {}
|
||||||
|
q = query.parse(data)
|
||||||
|
if 'group' in q:
|
||||||
|
response['items'] = []
|
||||||
|
'''
|
||||||
|
items = 'items'
|
||||||
|
item_qs = q['qs']
|
||||||
|
order_by = query.order_by_group(q)
|
||||||
|
qs = models.Facet.objects.filter(key=q['group']).filter(item__id__in=item_qs)
|
||||||
|
qs = qs.values('value').annotate(items=Count('id')).order_by(*order_by)
|
||||||
|
|
||||||
|
if 'positions' in q:
|
||||||
|
response['positions'] = {}
|
||||||
|
ids = [j['value'] for j in qs]
|
||||||
|
response['positions'] = utils.get_positions(ids, q['positions'])
|
||||||
|
elif 'range' in data:
|
||||||
|
qs = qs[q['range'][0]:q['range'][1]]
|
||||||
|
response['items'] = [{'name': i['value'], 'items': i[items]} for i in qs]
|
||||||
|
else:
|
||||||
|
response['items'] = qs.count()
|
||||||
|
'''
|
||||||
|
_g = {}
|
||||||
|
key = utils.get_by_id(settings.config['itemKeys'], q['group'])
|
||||||
|
for item in q['qs']:
|
||||||
|
i = item.json()
|
||||||
|
if q['group'] in i:
|
||||||
|
values = i[q['group']]
|
||||||
|
if isinstance(values, basestring):
|
||||||
|
values = [values]
|
||||||
|
for value in values:
|
||||||
|
if key.get('filterMap') and value:
|
||||||
|
value = re.compile(key.get('filterMap')).findall(value)
|
||||||
|
if value:
|
||||||
|
value = value[0]
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
if value not in _g:
|
||||||
|
_g[value] = 0
|
||||||
|
_g[value] += 1
|
||||||
|
g = [{'name': k, 'items': _g[k]} for k in _g]
|
||||||
|
if 'sort' in data: # parse adds default sort to q!
|
||||||
|
g.sort(key=lambda k: k[q['sort'][0]['key']])
|
||||||
|
if q['sort'][0]['operator'] == '-':
|
||||||
|
g.reverse()
|
||||||
|
if 'positions' in data:
|
||||||
|
response['positions'] = {}
|
||||||
|
ids = [k['name'] for k in g]
|
||||||
|
response['positions'] = utils.get_positions(ids, data['positions'])
|
||||||
|
elif 'range' in data:
|
||||||
|
response['items'] = g[q['range'][0]:q['range'][1]]
|
||||||
|
else:
|
||||||
|
response['items'] = len(g)
|
||||||
|
elif 'position' in data:
|
||||||
|
ids = [i.id for i in q['qs']]
|
||||||
|
response['position'] = utils.get_positions(ids, [data['qs'][0].id])[0]
|
||||||
|
elif 'positions' in data:
|
||||||
|
ids = [i.id for i in q['qs']]
|
||||||
|
response['positions'] = utils.get_positions(ids, data['positions'])
|
||||||
|
elif 'keys' in data:
|
||||||
|
'''
|
||||||
|
qs = qs[q['range'][0]:q['range'][1]]
|
||||||
|
response['items'] = [p.json(data['keys']) for p in qs]
|
||||||
|
'''
|
||||||
|
response['items'] = []
|
||||||
|
for i in q['qs'][q['range'][0]:q['range'][1]]:
|
||||||
|
j = i.json()
|
||||||
|
response['items'].append({k:j[k] for k in j if not data['keys'] or k in data['keys']})
|
||||||
|
else:
|
||||||
|
items = [i.json() for i in q['qs']]
|
||||||
|
response['items'] = len(items)
|
||||||
|
response['size'] = sum([i.get('size',0) for i in items])
|
||||||
|
return response
|
||||||
|
actions.register(find)
|
||||||
|
|
||||||
|
@returns_json
|
||||||
|
def get(request):
|
||||||
|
response = {}
|
||||||
|
data = json.loads(request.form['data']) if 'data' in request.form else {}
|
||||||
|
item = models.Item.get(data['id'])
|
||||||
|
if item:
|
||||||
|
response = item.json(data['keys'] if 'keys' in data else None)
|
||||||
|
return response
|
||||||
|
actions.register(get)
|
||||||
|
|
||||||
|
@returns_json
|
||||||
|
def edit(request):
|
||||||
|
response = {}
|
||||||
|
data = json.loads(request.form['data']) if 'data' in request.form else {}
|
||||||
|
print 'edit', data
|
||||||
|
item = models.Item.get(data['id'])
|
||||||
|
keys = filter(lambda k: k in models.Item.id_keys, data.keys())
|
||||||
|
print item, keys
|
||||||
|
if item and keys and item.json()['mediastate'] == 'available':
|
||||||
|
key = keys[0]
|
||||||
|
print 'update mainid', key, data[key]
|
||||||
|
item.update_mainid(key, data[key])
|
||||||
|
response = item.json()
|
||||||
|
else:
|
||||||
|
print 'can only edit available items'
|
||||||
|
response = item.json()
|
||||||
|
return response
|
||||||
|
actions.register(edit, cache=False)
|
||||||
|
|
||||||
|
|
||||||
|
@returns_json
|
||||||
|
def identify(request):
|
||||||
|
'''
|
||||||
|
takes {
|
||||||
|
title: string,
|
||||||
|
author: [string],
|
||||||
|
publisher: string,
|
||||||
|
date: string
|
||||||
|
}
|
||||||
|
returns {
|
||||||
|
title: string,
|
||||||
|
autor: [string],
|
||||||
|
date: string,
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
response = {}
|
||||||
|
data = json.loads(request.form['data']) if 'data' in request.form else {}
|
||||||
|
response = {
|
||||||
|
'items': [
|
||||||
|
{
|
||||||
|
u'title': u'Cinema',
|
||||||
|
u'author': [u'Gilles Deleuze'],
|
||||||
|
u'date': u'1986-10',
|
||||||
|
u'publisher': u'University of Minnesota Press',
|
||||||
|
u'isbn10': u'0816613990',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
u'title': u'How to Change the World: Reflections on Marx and Marxism',
|
||||||
|
u'author': [u'Eric Hobsbawm'],
|
||||||
|
u'date': u'2011-09-06',
|
||||||
|
u'publisher': u'Yale University Press',
|
||||||
|
u'isbn13': u'9780300176162',
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
actions.register(identify)
|
||||||
|
|
||||||
|
@returns_json
|
||||||
|
def download(request):
|
||||||
|
response = {}
|
||||||
|
data = json.loads(request.form['data']) if 'data' in request.form else {}
|
||||||
|
item = models.Item.get(data['id'])
|
||||||
|
if item:
|
||||||
|
item.transferprogress = 0
|
||||||
|
item.transferadded = datetime.now()
|
||||||
|
p = models.User.get(settings.USER_ID)
|
||||||
|
if p not in item.users:
|
||||||
|
item.users.append(p)
|
||||||
|
item.update()
|
||||||
|
response = {'status': 'queued'}
|
||||||
|
return response
|
||||||
|
actions.register(download, cache=False)
|
||||||
|
|
||||||
|
@returns_json
|
||||||
|
def cancelDownload(request):
|
||||||
|
response = {}
|
||||||
|
data = json.loads(request.form['data']) if 'data' in request.form else {}
|
||||||
|
item = models.Item.get(data['id'])
|
||||||
|
if item:
|
||||||
|
item.transferprogress = None
|
||||||
|
item.transferadded = None
|
||||||
|
p = models.User.get(settings.USER_ID)
|
||||||
|
if p in item.users:
|
||||||
|
item.users.remove(p)
|
||||||
|
item.update()
|
||||||
|
response = {'status': 'cancelled'}
|
||||||
|
return response
|
||||||
|
actions.register(cancelDownload, cache=False)
|
||||||
|
|
||||||
|
@returns_json
|
||||||
|
def scan(request):
|
||||||
|
state.main.add_callback(state.websockets[0].put, json.dumps(['scan', {}]))
|
||||||
|
return {}
|
||||||
|
actions.register(scan, cache=False)
|
||||||
|
|
||||||
|
@returns_json
|
||||||
|
def _import(request):
|
||||||
|
state.main.add_callback(state.websockets[0].put, json.dumps(['import', {}]))
|
||||||
|
return {}
|
||||||
|
actions.register(_import, 'import', cache=False)
|
|
@ -0,0 +1,74 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vi:si:et:sw=4:sts=4:ts=4
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
import Image
|
||||||
|
from StringIO import StringIO
|
||||||
|
|
||||||
|
from settings import covers_db_path
|
||||||
|
|
||||||
|
class Covers(dict):
|
||||||
|
def __init__(self, db):
|
||||||
|
self._db = db
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
self.conn = sqlite3.connect(self._db, timeout=10)
|
||||||
|
self.create()
|
||||||
|
|
||||||
|
def create(self):
|
||||||
|
c = self.conn.cursor()
|
||||||
|
c.execute(u'CREATE TABLE IF NOT EXISTS cover (id varchar(64) unique, data blob)')
|
||||||
|
c.execute(u'CREATE TABLE IF NOT EXISTS setting (key varchar(256) unique, value text)')
|
||||||
|
if int(self.get_setting(c, 'version', 0)) < 1:
|
||||||
|
self.set_setting(c, 'version', 1)
|
||||||
|
|
||||||
|
def get_setting(self, c, key, default=None):
|
||||||
|
c.execute(u'SELECT value FROM setting WHERE key = ?', (key, ))
|
||||||
|
for row in c:
|
||||||
|
return row[0]
|
||||||
|
return default
|
||||||
|
|
||||||
|
def set_setting(self, c, key, value):
|
||||||
|
c.execute(u'INSERT OR REPLACE INTO setting values (?, ?)', (key, str(value)))
|
||||||
|
|
||||||
|
def black(self):
|
||||||
|
img = Image.new('RGB', (80, 128))
|
||||||
|
o = StringIO()
|
||||||
|
img.save(o, format='jpeg')
|
||||||
|
data = o.getvalue()
|
||||||
|
o.close()
|
||||||
|
return data
|
||||||
|
|
||||||
|
def __getitem__(self, id, default=None):
|
||||||
|
sql = u'SELECT data FROM cover WHERE id=?'
|
||||||
|
self.connect()
|
||||||
|
c = self.conn.cursor()
|
||||||
|
c.execute(sql, (id, ))
|
||||||
|
data = default
|
||||||
|
for row in c:
|
||||||
|
data = row[0]
|
||||||
|
break
|
||||||
|
c.close()
|
||||||
|
self.conn.close()
|
||||||
|
return data
|
||||||
|
|
||||||
|
def __setitem__(self, id, data):
|
||||||
|
sql = u'INSERT OR REPLACE INTO cover values (?, ?)'
|
||||||
|
self.connect()
|
||||||
|
c = self.conn.cursor()
|
||||||
|
data = sqlite3.Binary(data)
|
||||||
|
c.execute(sql, (id, data))
|
||||||
|
self.conn.commit()
|
||||||
|
c.close()
|
||||||
|
self.conn.close()
|
||||||
|
|
||||||
|
def __delitem__(self, id):
|
||||||
|
sql = u'DELETE FROM cover WHERE id = ?'
|
||||||
|
self.connect()
|
||||||
|
c = self.conn.cursor()
|
||||||
|
c.execute(sql, (id, ))
|
||||||
|
self.conn.commit()
|
||||||
|
c.close()
|
||||||
|
self.conn.close()
|
||||||
|
|
||||||
|
covers = Covers(covers_db_path)
|
|
@ -0,0 +1,13 @@
|
||||||
|
import models
|
||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
|
def import_all():
|
||||||
|
for i in models.items:
|
||||||
|
item = models.Item.get_or_create(i['id'])
|
||||||
|
item.path = i['path']
|
||||||
|
item.info = deepcopy(i)
|
||||||
|
del item.info['path']
|
||||||
|
del item.info['id']
|
||||||
|
item.meta = item.info.pop('meta', {})
|
||||||
|
models.db.session.add(item)
|
||||||
|
models.db.session.commit()
|
|
@ -0,0 +1,427 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vi:si:et:sw=4:sts=4:ts=4
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import hashlib
|
||||||
|
from datetime import datetime
|
||||||
|
from StringIO import StringIO
|
||||||
|
|
||||||
|
import Image
|
||||||
|
import ox
|
||||||
|
|
||||||
|
import settings
|
||||||
|
from settings import db, config
|
||||||
|
|
||||||
|
from user.models import User
|
||||||
|
|
||||||
|
from person import get_sort_name
|
||||||
|
|
||||||
|
import media
|
||||||
|
from meta import scraper
|
||||||
|
|
||||||
|
import utils
|
||||||
|
|
||||||
|
from oxflask.db import MutableDict
|
||||||
|
|
||||||
|
from covers import covers
|
||||||
|
from changelog import Changelog
|
||||||
|
from websocket import trigger_event
|
||||||
|
|
||||||
|
class Work(db.Model):
|
||||||
|
|
||||||
|
created = db.Column(db.DateTime())
|
||||||
|
modified = db.Column(db.DateTime())
|
||||||
|
|
||||||
|
id = db.Column(db.String(32), primary_key=True)
|
||||||
|
|
||||||
|
meta = db.Column(MutableDict.as_mutable(db.PickleType(pickler=json)))
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return self.id
|
||||||
|
|
||||||
|
def __init__(self, id):
|
||||||
|
self.id = id
|
||||||
|
self.created = datetime.now()
|
||||||
|
self.modified = datetime.now()
|
||||||
|
|
||||||
|
class Edition(db.Model):
|
||||||
|
|
||||||
|
created = db.Column(db.DateTime())
|
||||||
|
modified = db.Column(db.DateTime())
|
||||||
|
|
||||||
|
id = db.Column(db.String(32), primary_key=True)
|
||||||
|
|
||||||
|
meta = db.Column(MutableDict.as_mutable(db.PickleType(pickler=json)))
|
||||||
|
|
||||||
|
work_id = db.Column(db.String(32), db.ForeignKey('work.id'))
|
||||||
|
work = db.relationship('Work', backref=db.backref('editions', lazy='dynamic'))
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return self.id
|
||||||
|
|
||||||
|
def __init__(self, id):
|
||||||
|
self.id = id
|
||||||
|
self.created = datetime.now()
|
||||||
|
self.modified = datetime.now()
|
||||||
|
|
||||||
|
user_items = db.Table('useritem',
|
||||||
|
db.Column('user_id', db.String(43), db.ForeignKey('user.id')),
|
||||||
|
db.Column('item_id', db.String(32), db.ForeignKey('item.id'))
|
||||||
|
)
|
||||||
|
|
||||||
|
class Item(db.Model):
|
||||||
|
|
||||||
|
created = db.Column(db.DateTime())
|
||||||
|
modified = db.Column(db.DateTime())
|
||||||
|
|
||||||
|
id = db.Column(db.String(32), primary_key=True)
|
||||||
|
|
||||||
|
info = db.Column(MutableDict.as_mutable(db.PickleType(pickler=json)))
|
||||||
|
meta = db.Column(MutableDict.as_mutable(db.PickleType(pickler=json)))
|
||||||
|
|
||||||
|
added = db.Column(db.DateTime()) # added to local library
|
||||||
|
accessed = db.Column(db.DateTime())
|
||||||
|
timesaccessed = db.Column(db.Integer())
|
||||||
|
|
||||||
|
transferadded = db.Column(db.DateTime())
|
||||||
|
transferprogress = db.Column(db.Float())
|
||||||
|
|
||||||
|
users = db.relationship('User', secondary=user_items,
|
||||||
|
backref=db.backref('items', lazy='dynamic'))
|
||||||
|
|
||||||
|
edition_id = db.Column(db.String(32), db.ForeignKey('edition.id'))
|
||||||
|
edition = db.relationship('Edition', backref=db.backref('items', lazy='dynamic'))
|
||||||
|
|
||||||
|
work_id = db.Column(db.String(32), db.ForeignKey('work.id'))
|
||||||
|
work = db.relationship('Work', backref=db.backref('items', lazy='dynamic'))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def timestamp(self):
|
||||||
|
return self.modified.strftime('%s')
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return self.id
|
||||||
|
|
||||||
|
def __init__(self, id):
|
||||||
|
if isinstance(id, list):
|
||||||
|
id = base64.b32encode(hashlib.sha1(''.join(id)).digest())
|
||||||
|
self.id = id
|
||||||
|
self.created = datetime.now()
|
||||||
|
self.modified = datetime.now()
|
||||||
|
self.info = {}
|
||||||
|
self.meta = {}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get(cls, id):
|
||||||
|
if isinstance(id, list):
|
||||||
|
id = base64.b32encode(hashlib.sha1(''.join(id)).digest())
|
||||||
|
return cls.query.filter_by(id=id).first()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_or_create(cls, id, info=None):
|
||||||
|
if isinstance(id, list):
|
||||||
|
id = base64.b32encode(hashlib.sha1(''.join(id)).digest())
|
||||||
|
item = cls.query.filter_by(id=id).first()
|
||||||
|
if not item:
|
||||||
|
item = cls(id=id)
|
||||||
|
if info:
|
||||||
|
item.info = info
|
||||||
|
db.session.add(item)
|
||||||
|
db.session.commit()
|
||||||
|
return item
|
||||||
|
|
||||||
|
def json(self, keys=None):
|
||||||
|
j = {}
|
||||||
|
j['id'] = self.id
|
||||||
|
j['created'] = self.created
|
||||||
|
j['modified'] = self.modified
|
||||||
|
j['timesaccessed'] = self.timesaccessed
|
||||||
|
j['accessed'] = self.accessed
|
||||||
|
j['added'] = self.added
|
||||||
|
j['transferadded'] = self.transferadded
|
||||||
|
j['transferprogress'] = self.transferprogress
|
||||||
|
j['users'] = map(str, list(self.users))
|
||||||
|
|
||||||
|
if self.info:
|
||||||
|
j.update(self.info)
|
||||||
|
if self.meta:
|
||||||
|
j.update(self.meta)
|
||||||
|
|
||||||
|
for key in self.id_keys + ['mainid']:
|
||||||
|
if key not in self.meta and key in j:
|
||||||
|
del j[key]
|
||||||
|
'''
|
||||||
|
if self.work_id:
|
||||||
|
j['work'] = {
|
||||||
|
'olid': self.work_id
|
||||||
|
}
|
||||||
|
j['work'].update(self.work.meta)
|
||||||
|
'''
|
||||||
|
if keys:
|
||||||
|
for k in j.keys():
|
||||||
|
if k not in keys:
|
||||||
|
del j[k]
|
||||||
|
return j
|
||||||
|
|
||||||
|
def get_path(self):
|
||||||
|
f = self.files.first()
|
||||||
|
prefs = settings.preferences
|
||||||
|
prefix = os.path.join(os.path.expanduser(prefs['libraryPath']), 'Books/')
|
||||||
|
return os.path.join(prefix, f.path) if f else None
|
||||||
|
|
||||||
|
def update_sort(self):
|
||||||
|
for key in config['itemKeys']:
|
||||||
|
if key.get('sort'):
|
||||||
|
value = self.json().get(key['id'], None)
|
||||||
|
sort_type = key.get('sortType', key['type'])
|
||||||
|
if value:
|
||||||
|
if sort_type == 'integer':
|
||||||
|
value = int(value)
|
||||||
|
elif sort_type == 'float':
|
||||||
|
value = float(value)
|
||||||
|
elif sort_type == 'date':
|
||||||
|
pass
|
||||||
|
elif sort_type == 'name':
|
||||||
|
if not isinstance(value, list):
|
||||||
|
value = [value]
|
||||||
|
value = map(get_sort_name, value)
|
||||||
|
value = ox.sort_string(u'\n'.join(value))
|
||||||
|
elif sort_type == 'title':
|
||||||
|
value = utils.sort_title(value).lower()
|
||||||
|
else:
|
||||||
|
if isinstance(value, list):
|
||||||
|
value = u'\n'.join(value)
|
||||||
|
if value:
|
||||||
|
value = unicode(value)
|
||||||
|
value = ox.sort_string(value).lower()
|
||||||
|
setattr(self, 'sort_%s' % key['id'], value)
|
||||||
|
|
||||||
|
def update_find(self):
|
||||||
|
for key in config['itemKeys']:
|
||||||
|
if key.get('find') or key.get('filter'):
|
||||||
|
value = self.json().get(key['id'], None)
|
||||||
|
if key.get('filterMap') and value:
|
||||||
|
value = re.compile(key.get('filterMap')).findall(value)[0]
|
||||||
|
print key['id'], value
|
||||||
|
if value:
|
||||||
|
if isinstance(value, list):
|
||||||
|
Find.query.filter_by(item_id=self.id, key=key['id']).delete()
|
||||||
|
for v in value:
|
||||||
|
f = Find(item_id=self.id, key=key['id'])
|
||||||
|
f.value = v.lower()
|
||||||
|
db.session.add(f)
|
||||||
|
else:
|
||||||
|
f = Find.get_or_create(self.id, key['id'])
|
||||||
|
f.value = value.lower()
|
||||||
|
db.session.add(f)
|
||||||
|
else:
|
||||||
|
f = Find.get(self.id, key['id'])
|
||||||
|
if f:
|
||||||
|
db.session.delete(f)
|
||||||
|
|
||||||
|
def update_lists(self):
|
||||||
|
Find.query.filter_by(item_id=self.id, key='list').delete()
|
||||||
|
for p in self.users:
|
||||||
|
f = Find()
|
||||||
|
f.item_id = self.id
|
||||||
|
f.key = 'list'
|
||||||
|
if p.id == settings.USER_ID:
|
||||||
|
f.value = ':'
|
||||||
|
else:
|
||||||
|
f.value = '%s:' % p.id
|
||||||
|
db.session.add(f)
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
users = map(str, list(self.users))
|
||||||
|
self.meta['mediastate'] = 'available' # available, unavailable, transferring
|
||||||
|
if self.transferadded and self.transferprogress < 1:
|
||||||
|
self.meta['mediastate'] = 'transferring'
|
||||||
|
else:
|
||||||
|
self.meta['mediastate'] = 'available' if settings.USER_ID in users else 'unavailable'
|
||||||
|
self.update_sort()
|
||||||
|
self.update_find()
|
||||||
|
self.update_lists()
|
||||||
|
self.modified = datetime.now()
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
db.session.add(self)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
def update_mainid(self, key, id):
|
||||||
|
record = {}
|
||||||
|
if id:
|
||||||
|
self.meta[key] = id
|
||||||
|
self.meta['mainid'] = key
|
||||||
|
record[key] = id
|
||||||
|
else:
|
||||||
|
if key in self.meta:
|
||||||
|
del self.meta[key]
|
||||||
|
if 'mainid' in self.meta:
|
||||||
|
del self.meta['mainid']
|
||||||
|
record[key] = ''
|
||||||
|
for k in self.id_keys:
|
||||||
|
if k != key:
|
||||||
|
if k in self.meta:
|
||||||
|
del self.meta[k]
|
||||||
|
print 'mainid', 'mainid' in self.meta, self.meta.get('mainid')
|
||||||
|
print 'key', key, self.meta.get(key)
|
||||||
|
# get metadata from external resources
|
||||||
|
self.scrape()
|
||||||
|
self.update()
|
||||||
|
self.update_cover()
|
||||||
|
db.session.add(self)
|
||||||
|
db.session.commit()
|
||||||
|
user = User.get_or_create(settings.USER_ID)
|
||||||
|
if user in self.users:
|
||||||
|
Changelog.record(user, 'edititem', self.id, record)
|
||||||
|
|
||||||
|
def extract_cover(self):
|
||||||
|
path = self.get_path()
|
||||||
|
if not path:
|
||||||
|
return getattr(media, self.meta['extensions']).cover(path)
|
||||||
|
|
||||||
|
def update_cover(self):
|
||||||
|
cover = None
|
||||||
|
if 'cover' in self.meta:
|
||||||
|
cover = ox.cache.read_url(self.meta['cover'])
|
||||||
|
#covers[self.id] = requests.get(self.meta['cover']).content
|
||||||
|
if cover:
|
||||||
|
covers[self.id] = cover
|
||||||
|
path = self.get_path()
|
||||||
|
if not cover and path:
|
||||||
|
cover = self.extract_cover()
|
||||||
|
if cover:
|
||||||
|
covers[self.id] = cover
|
||||||
|
if cover:
|
||||||
|
img = Image.open(StringIO(cover))
|
||||||
|
self.meta['coverRatio'] = img.size[0]/img.size[1]
|
||||||
|
for p in (':128', ':256'):
|
||||||
|
del covers['%s%s' % (self.id, p)]
|
||||||
|
return cover
|
||||||
|
|
||||||
|
def scrape(self):
|
||||||
|
mainid = self.meta.get('mainid')
|
||||||
|
print 'scrape', mainid, self.meta.get(mainid)
|
||||||
|
if mainid == 'olid':
|
||||||
|
scraper.update_ol(self)
|
||||||
|
scraper.add_lookupbyisbn(self)
|
||||||
|
elif mainid in ('isbn10', 'isbn13'):
|
||||||
|
scraper.add_lookupbyisbn(self)
|
||||||
|
elif mainid == 'lccn':
|
||||||
|
import meta.lccn
|
||||||
|
info = meta.lccn.info(self.meta[mainid])
|
||||||
|
for key in info:
|
||||||
|
self.meta[key] = info[key]
|
||||||
|
else:
|
||||||
|
print 'FIX UPDATE', mainid
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def save_file(self, content):
|
||||||
|
p = User.get(settings.USER_ID)
|
||||||
|
f = File.get(self.id)
|
||||||
|
if not f:
|
||||||
|
path = 'Downloads/%s.%s' % (self.id, self.info['extension'])
|
||||||
|
f = File.get_or_create(self.id, self.info, path=path)
|
||||||
|
path = self.get_path()
|
||||||
|
if not os.path.exists(path):
|
||||||
|
ox.makedirs(os.path.dirname(path))
|
||||||
|
with open(path, 'wb') as fd:
|
||||||
|
fd.write(content)
|
||||||
|
if p not in self.users:
|
||||||
|
self.users.append(p)
|
||||||
|
self.transferprogress = 1
|
||||||
|
self.added = datetime.now()
|
||||||
|
Changelog.record(p, 'additem', self.id, self.info)
|
||||||
|
self.update()
|
||||||
|
trigger_event('transfer', {
|
||||||
|
'id': self.id, 'progress': 1
|
||||||
|
})
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print 'TRIED TO SAVE EXISTING FILE!!!'
|
||||||
|
self.transferprogress = 1
|
||||||
|
self.update()
|
||||||
|
return False
|
||||||
|
|
||||||
|
for key in config['itemKeys']:
|
||||||
|
if key.get('sort'):
|
||||||
|
sort_type = key.get('sortType', key['type'])
|
||||||
|
if sort_type == 'integer':
|
||||||
|
col = db.Column(db.BigInteger(), index=True)
|
||||||
|
elif sort_type == 'float':
|
||||||
|
col = db.Column(db.Float(), index=True)
|
||||||
|
elif sort_type == 'date':
|
||||||
|
col = db.Column(db.DateTime(), index=True)
|
||||||
|
else:
|
||||||
|
col = db.Column(db.String(1000), index=True)
|
||||||
|
setattr(Item, 'sort_%s' % key['id'], col)
|
||||||
|
|
||||||
|
Item.id_keys = ['isbn10', 'isbn13', 'lccn', 'olid', 'oclc']
|
||||||
|
Item.item_keys = config['itemKeys']
|
||||||
|
Item.filter_keys = []
|
||||||
|
|
||||||
|
class Find(db.Model):
|
||||||
|
id = db.Column(db.Integer(), primary_key=True)
|
||||||
|
item_id = db.Column(db.String(32), db.ForeignKey('item.id'))
|
||||||
|
item = db.relationship('Item', backref=db.backref('find', lazy='dynamic'))
|
||||||
|
key = db.Column(db.String(200), index=True)
|
||||||
|
value = db.Column(db.Text())
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return (u'%s=%s' % (self.key, self.value)).encode('utf-8')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get(cls, item, key):
|
||||||
|
return cls.query.filter_by(item_id=item, key=key).first()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_or_create(cls, item, key):
|
||||||
|
f = cls.get(item, key)
|
||||||
|
if not f:
|
||||||
|
f = cls(item_id=item, key=key)
|
||||||
|
db.session.add(f)
|
||||||
|
db.session.commit()
|
||||||
|
return f
|
||||||
|
|
||||||
|
class File(db.Model):
|
||||||
|
|
||||||
|
created = db.Column(db.DateTime())
|
||||||
|
modified = db.Column(db.DateTime())
|
||||||
|
|
||||||
|
sha1 = db.Column(db.String(32), primary_key=True)
|
||||||
|
path = db.Column(db.String(2048))
|
||||||
|
|
||||||
|
info = db.Column(MutableDict.as_mutable(db.PickleType(pickler=json)))
|
||||||
|
|
||||||
|
item_id = db.Column(db.String(32), db.ForeignKey('item.id'))
|
||||||
|
item = db.relationship('Item', backref=db.backref('files', lazy='dynamic'))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get(cls, sha1):
|
||||||
|
return cls.query.filter_by(sha1=sha1).first()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_or_create(cls, sha1, info=None, path=None):
|
||||||
|
f = cls.get(sha1)
|
||||||
|
if not f:
|
||||||
|
f = cls(sha1=sha1)
|
||||||
|
if info:
|
||||||
|
f.info = info
|
||||||
|
if path:
|
||||||
|
f.path = path
|
||||||
|
f.item_id = Item.get_or_create(id=sha1, info=info).id
|
||||||
|
db.session.add(f)
|
||||||
|
db.session.commit()
|
||||||
|
return f
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return self.sha1
|
||||||
|
|
||||||
|
def __init__(self, sha1):
|
||||||
|
self.sha1 = sha1
|
||||||
|
self.created = datetime.now()
|
||||||
|
self.modified = datetime.now()
|
|
@ -0,0 +1,42 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vi:si:et:sw=4:sts=4:ts=4
|
||||||
|
|
||||||
|
import unicodedata
|
||||||
|
|
||||||
|
import ox
|
||||||
|
|
||||||
|
from settings import db
|
||||||
|
|
||||||
|
def get_sort_name(name, sortname=None):
|
||||||
|
name = unicodedata.normalize('NFKD', name).strip()
|
||||||
|
if name:
|
||||||
|
person = Person.get(name)
|
||||||
|
if not person:
|
||||||
|
person = Person(name=name, sortname=sortname)
|
||||||
|
person.save()
|
||||||
|
sortname = unicodedata.normalize('NFKD', person.sortname)
|
||||||
|
else:
|
||||||
|
sortname = u''
|
||||||
|
return sortname
|
||||||
|
|
||||||
|
class Person(db.Model):
|
||||||
|
name = db.Column(db.String(1024), primary_key=True)
|
||||||
|
sortname = db.Column(db.String())
|
||||||
|
numberofnames = db.Column(db.Integer())
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get(cls, name):
|
||||||
|
return cls.query.filter_by(name=name).first()
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
if not self.sortname:
|
||||||
|
self.sortname = ox.get_sort_name(self.name)
|
||||||
|
self.sortname = unicodedata.normalize('NFKD', self.sortname)
|
||||||
|
self.sortsortname = ox.sort_string(self.sortname)
|
||||||
|
self.numberofnames = len(self.name.split(' '))
|
||||||
|
db.session.add(self)
|
||||||
|
db.session.commit()
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vi:si:et:sw=4:sts=4:ts=4
|
||||||
|
|
||||||
|
import settings
|
||||||
|
import models
|
||||||
|
import utils
|
||||||
|
import oxflask.query
|
||||||
|
|
||||||
|
from sqlalchemy.sql.expression import nullslast
|
||||||
|
|
||||||
|
def parse(data):
|
||||||
|
query = {}
|
||||||
|
query['range'] = [0, 100]
|
||||||
|
query['sort'] = [{'key':'title', 'operator':'+'}]
|
||||||
|
for key in ('keys', 'group', 'list', 'range', 'sort', 'query'):
|
||||||
|
if key in data:
|
||||||
|
query[key] = data[key]
|
||||||
|
print data
|
||||||
|
query['qs'] = oxflask.query.Parser(models.Item).find(data)
|
||||||
|
if 'query' in query and 'conditions' in query['query'] and query['query']['conditions']:
|
||||||
|
conditions = query['query']['conditions']
|
||||||
|
condition = conditions[0]
|
||||||
|
if condition['key'] == '*':
|
||||||
|
value = condition['value'].lower()
|
||||||
|
query['qs'] = models.Item.query.join(
|
||||||
|
models.Find, models.Find.item_id==models.Item.id).filter(
|
||||||
|
models.Find.value.contains(value))
|
||||||
|
if 'group' in query:
|
||||||
|
query['qs'] = order_by_group(query['qs'], query['sort'])
|
||||||
|
else:
|
||||||
|
query['qs'] = order(query['qs'], query['sort'])
|
||||||
|
return query
|
||||||
|
|
||||||
|
def order(qs, sort, prefix='sort_'):
|
||||||
|
order_by = []
|
||||||
|
if len(sort) == 1:
|
||||||
|
additional_sort = settings.config['user']['ui']['listSort']
|
||||||
|
key = utils.get_by_id(models.Item.item_keys, sort[0]['key'])
|
||||||
|
for s in key.get('additionalSort', additional_sort):
|
||||||
|
if s['key'] not in [e['key'] for e in sort]:
|
||||||
|
sort.append(s)
|
||||||
|
for e in sort:
|
||||||
|
operator = e['operator']
|
||||||
|
if operator != '-':
|
||||||
|
operator = ''
|
||||||
|
else:
|
||||||
|
operator = ' DESC'
|
||||||
|
key = {}.get(e['key'], e['key'])
|
||||||
|
if key not in ('fixme', ):
|
||||||
|
key = "%s%s" % (prefix, key)
|
||||||
|
order = '%s%s' % (key, operator)
|
||||||
|
order_by.append(order)
|
||||||
|
if order_by:
|
||||||
|
#nulllast not supported in sqlite, use IS NULL hack instead
|
||||||
|
#order_by = map(nullslast, order_by)
|
||||||
|
_order_by = []
|
||||||
|
for order in order_by:
|
||||||
|
nulls = "%s IS NULL" % order.split(' ')[0]
|
||||||
|
_order_by.append(nulls)
|
||||||
|
_order_by.append(order)
|
||||||
|
order_by = _order_by
|
||||||
|
qs = qs.order_by(*order_by)
|
||||||
|
return qs
|
||||||
|
|
||||||
|
def order_by_group(qs, sort):
|
||||||
|
return qs
|
||||||
|
if 'sort' in query:
|
||||||
|
if len(query['sort']) == 1 and query['sort'][0]['key'] == 'items':
|
||||||
|
order_by = query['sort'][0]['operator'] == '-' and '-items' or 'items'
|
||||||
|
if query['group'] == "year":
|
||||||
|
secondary = query['sort'][0]['operator'] == '-' and '-sortvalue' or 'sortvalue'
|
||||||
|
order_by = (order_by, secondary)
|
||||||
|
elif query['group'] != "keyword":
|
||||||
|
order_by = (order_by, 'sortvalue')
|
||||||
|
else:
|
||||||
|
order_by = (order_by, 'value')
|
||||||
|
else:
|
||||||
|
order_by = query['sort'][0]['operator'] == '-' and '-sortvalue' or 'sortvalue'
|
||||||
|
order_by = (order_by, 'items')
|
||||||
|
else:
|
||||||
|
order_by = ('-sortvalue', 'items')
|
||||||
|
return order_by
|
||||||
|
|
|
@ -0,0 +1,182 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vi:si:et:sw=4:sts=4:ts=4
|
||||||
|
from __future__ import division
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import ox
|
||||||
|
|
||||||
|
from app import app
|
||||||
|
import settings
|
||||||
|
from settings import db
|
||||||
|
from item.models import File
|
||||||
|
from user.models import User
|
||||||
|
|
||||||
|
from changelog import Changelog
|
||||||
|
|
||||||
|
import media
|
||||||
|
from websocket import trigger_event
|
||||||
|
|
||||||
|
def remove_missing():
|
||||||
|
dirty = False
|
||||||
|
with app.app_context():
|
||||||
|
user = User.get_or_create(settings.USER_ID)
|
||||||
|
prefs = settings.preferences
|
||||||
|
prefix = os.path.join(os.path.expanduser(prefs['libraryPath']), 'Books/')
|
||||||
|
for f in File.query:
|
||||||
|
if not os.path.exists(f.item.get_path()):
|
||||||
|
dirty = True
|
||||||
|
print 'file gone', f, f.item.get_path()
|
||||||
|
f.item.users.remove(user)
|
||||||
|
if not f.item.users:
|
||||||
|
print 'last user, remove'
|
||||||
|
db.session.delete(f.item)
|
||||||
|
else:
|
||||||
|
f.item.update_lists()
|
||||||
|
Changelog.record(user, 'removeitem', f.item.id)
|
||||||
|
db.session.delete(f)
|
||||||
|
if dirty:
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
def run_scan():
|
||||||
|
remove_missing()
|
||||||
|
with app.app_context():
|
||||||
|
prefs = settings.preferences
|
||||||
|
prefix = os.path.join(os.path.expanduser(prefs['libraryPath']), 'Books/')
|
||||||
|
if not prefix[-1] == '/':
|
||||||
|
prefix += '/'
|
||||||
|
user = User.get_or_create(settings.USER_ID)
|
||||||
|
assert isinstance(prefix, unicode)
|
||||||
|
extensions = ['pdf', 'epub', 'txt']
|
||||||
|
books = []
|
||||||
|
for root, folders, files in os.walk(prefix):
|
||||||
|
for f in files:
|
||||||
|
#if f.startswith('._') or f == '.DS_Store':
|
||||||
|
if f.startswith('.'):
|
||||||
|
continue
|
||||||
|
f = os.path.join(root, f)
|
||||||
|
ext = f.split('.')[-1]
|
||||||
|
if ext in extensions:
|
||||||
|
books.append(f)
|
||||||
|
|
||||||
|
trigger_event('scan', {
|
||||||
|
'path': prefix,
|
||||||
|
'files': len(books)
|
||||||
|
})
|
||||||
|
position = 0
|
||||||
|
added = 0
|
||||||
|
for f in ox.sorted_strings(books):
|
||||||
|
position += 1
|
||||||
|
id = media.get_id(f)
|
||||||
|
file = File.get(id)
|
||||||
|
path = f[len(prefix):]
|
||||||
|
if not file:
|
||||||
|
data = media.metadata(f)
|
||||||
|
ext = f.split('.')[-1]
|
||||||
|
data['extension'] = ext
|
||||||
|
data['size'] = os.stat(f).st_size
|
||||||
|
file = File.get_or_create(id, data, path)
|
||||||
|
item = file.item
|
||||||
|
if 'mainid' in file.info:
|
||||||
|
del file.info['mainid']
|
||||||
|
db.session.add(file)
|
||||||
|
if 'mainid' in item.info:
|
||||||
|
item.meta['mainid'] = item.info.pop('mainid')
|
||||||
|
item.meta[item.meta['mainid']] = item.info[item.meta['mainid']]
|
||||||
|
db.session.add(item)
|
||||||
|
item.users.append(user)
|
||||||
|
Changelog.record(user, 'additem', item.id, item.info)
|
||||||
|
if item.meta.get('mainid'):
|
||||||
|
Changelog.record(user, 'edititem', item.id, {
|
||||||
|
item.meta['mainid']: item.meta[item.meta['mainid']]
|
||||||
|
})
|
||||||
|
item.added = datetime.now()
|
||||||
|
item.scrape()
|
||||||
|
added += 1
|
||||||
|
trigger_event('scan', {
|
||||||
|
'position': position,
|
||||||
|
'length': len(books),
|
||||||
|
'path': path,
|
||||||
|
'progress': position/len(books),
|
||||||
|
'added': added,
|
||||||
|
})
|
||||||
|
trigger_event('scan', {
|
||||||
|
'progress': 1,
|
||||||
|
'added': added,
|
||||||
|
'done': True
|
||||||
|
})
|
||||||
|
|
||||||
|
def run_import():
|
||||||
|
with app.app_context():
|
||||||
|
prefs = settings.preferences
|
||||||
|
prefix = os.path.expanduser(prefs['importPath'])
|
||||||
|
prefix_books = os.path.join(os.path.expanduser(prefs['libraryPath']), 'Books/')
|
||||||
|
prefix_imported = os.path.join(prefix_books, 'Imported/')
|
||||||
|
if not prefix[-1] == '/':
|
||||||
|
prefix += '/'
|
||||||
|
user = User.get_or_create(settings.USER_ID)
|
||||||
|
assert isinstance(prefix, unicode)
|
||||||
|
extensions = ['pdf', 'epub', 'txt']
|
||||||
|
books = []
|
||||||
|
for root, folders, files in os.walk(prefix):
|
||||||
|
for f in files:
|
||||||
|
#if f.startswith('._') or f == '.DS_Store':
|
||||||
|
if f.startswith('.'):
|
||||||
|
continue
|
||||||
|
f = os.path.join(root, f)
|
||||||
|
ext = f.split('.')[-1]
|
||||||
|
if ext in extensions:
|
||||||
|
books.append(f)
|
||||||
|
|
||||||
|
trigger_event('import', {
|
||||||
|
'path': prefix,
|
||||||
|
'files': len(books)
|
||||||
|
})
|
||||||
|
position = 0
|
||||||
|
added = 0
|
||||||
|
for f in ox.sorted_strings(books):
|
||||||
|
position += 1
|
||||||
|
id = media.get_id(f)
|
||||||
|
file = File.get(id)
|
||||||
|
path = f[len(prefix):]
|
||||||
|
if not file:
|
||||||
|
f_import = f
|
||||||
|
f = f.replace(prefix, prefix_imported)
|
||||||
|
ox.makedirs(os.path.dirname(f))
|
||||||
|
shutil.move(f_import, f)
|
||||||
|
path = f[len(prefix_books):]
|
||||||
|
data = media.metadata(f)
|
||||||
|
ext = f.split('.')[-1]
|
||||||
|
data['extension'] = ext
|
||||||
|
data['size'] = os.stat(f).st_size
|
||||||
|
file = File.get_or_create(id, data, path)
|
||||||
|
item = file.item
|
||||||
|
if 'mainid' in file.info:
|
||||||
|
del file.info['mainid']
|
||||||
|
db.session.add(file)
|
||||||
|
if 'mainid' in item.info:
|
||||||
|
item.meta['mainid'] = item.info.pop('mainid')
|
||||||
|
item.meta[item.meta['mainid']] = item.info[item.meta['mainid']]
|
||||||
|
db.session.add(item)
|
||||||
|
item.users.append(user)
|
||||||
|
Changelog.record(user, 'additem', item.id, item.info)
|
||||||
|
if item.meta.get('mainid'):
|
||||||
|
Changelog.record(user, 'edititem', item.id, {
|
||||||
|
item.meta['mainid']: item.meta[item.meta['mainid']]
|
||||||
|
})
|
||||||
|
item.scrape()
|
||||||
|
added += 1
|
||||||
|
trigger_event('import', {
|
||||||
|
'position': position,
|
||||||
|
'length': len(books),
|
||||||
|
'path': path,
|
||||||
|
'progress': position/len(books),
|
||||||
|
'added': added,
|
||||||
|
})
|
||||||
|
trigger_event('import', {
|
||||||
|
'progress': 1,
|
||||||
|
'added': added,
|
||||||
|
'done': True
|
||||||
|
})
|
|
@ -0,0 +1,101 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vi:si:et:sw=4:sts=4:ts=4
|
||||||
|
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
import zipfile
|
||||||
|
import mimetypes
|
||||||
|
from StringIO import StringIO
|
||||||
|
import Image
|
||||||
|
|
||||||
|
from flask import Blueprint
|
||||||
|
from flask import json, request, make_response, abort, send_file
|
||||||
|
from covers import covers
|
||||||
|
|
||||||
|
import settings
|
||||||
|
|
||||||
|
from models import Item, db
|
||||||
|
|
||||||
|
from utils import resize_image
|
||||||
|
|
||||||
|
app = Blueprint('item', __name__, static_folder=settings.static_path)
|
||||||
|
|
||||||
|
@app.route('/<string:id>/epub/')
|
||||||
|
@app.route('/<string:id>/epub/<path:filename>')
|
||||||
|
def epub(id, filename=''):
|
||||||
|
item = Item.get(id)
|
||||||
|
if not item or item.info['extension'] != 'epub':
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
path = item.get_path()
|
||||||
|
z = zipfile.ZipFile(path)
|
||||||
|
if filename == '':
|
||||||
|
return '<br>\n'.join([f.filename for f in z.filelist])
|
||||||
|
if filename not in [f.filename for f in z.filelist]:
|
||||||
|
abort(404)
|
||||||
|
resp = make_response(z.read(filename))
|
||||||
|
resp.content_type = {
|
||||||
|
'xpgt': 'application/vnd.adobe-page-template+xml'
|
||||||
|
}.get(filename.split('.')[0], mimetypes.guess_type(filename)[0]) or 'text/plain'
|
||||||
|
return resp
|
||||||
|
|
||||||
|
@app.route('/<string:id>/get')
|
||||||
|
@app.route('/<string:id>/txt/')
|
||||||
|
@app.route('/<string:id>/pdf')
|
||||||
|
def get(id):
|
||||||
|
item = Item.get(id)
|
||||||
|
if not item:
|
||||||
|
abort(404)
|
||||||
|
path = item.get_path()
|
||||||
|
mimetype={
|
||||||
|
'epub': 'application/epub+zip',
|
||||||
|
'pdf': 'application/pdf',
|
||||||
|
}.get(path.split('.')[-1], None)
|
||||||
|
return send_file(path, mimetype=mimetype)
|
||||||
|
|
||||||
|
@app.route('/<string:id>/cover.jpg')
|
||||||
|
@app.route('/<string:id>/cover<int:size>.jpg')
|
||||||
|
def cover(id, size=None):
|
||||||
|
item = Item.get(id)
|
||||||
|
if not item:
|
||||||
|
abort(404)
|
||||||
|
data = None
|
||||||
|
if size:
|
||||||
|
data = covers['%s:%s' % (id, size)]
|
||||||
|
if data:
|
||||||
|
size = None
|
||||||
|
if not data:
|
||||||
|
data = covers[id]
|
||||||
|
if not data:
|
||||||
|
print 'check for cover', id
|
||||||
|
data = item.update_cover()
|
||||||
|
if not data:
|
||||||
|
data = covers.black()
|
||||||
|
if size:
|
||||||
|
data = covers['%s:%s' % (id, size)] = resize_image(data, size=size)
|
||||||
|
data = str(data)
|
||||||
|
if not 'coverRatio' in item.meta:
|
||||||
|
#img = Image.open(StringIO(str(covers[id])))
|
||||||
|
img = Image.open(StringIO(data))
|
||||||
|
item.meta['coverRatio'] = float(img.size[0])/img.size[1]
|
||||||
|
db.session.add(item)
|
||||||
|
db.session.commit()
|
||||||
|
resp = make_response(data)
|
||||||
|
resp.content_type = "image/jpeg"
|
||||||
|
return resp
|
||||||
|
|
||||||
|
@app.route('/<string:id>/reader/')
|
||||||
|
def reader(id, filename=''):
|
||||||
|
item = Item.get(id)
|
||||||
|
if item.info['extension'] == 'epub':
|
||||||
|
html = 'html/epub.html'
|
||||||
|
elif item.info['extension'] == 'pdf':
|
||||||
|
html = 'html/pdf.html'
|
||||||
|
elif item.info['extension'] == 'txt':
|
||||||
|
html = 'html/txt.html'
|
||||||
|
else:
|
||||||
|
abort(404)
|
||||||
|
item.sort_accessed = item.accessed = datetime.now()
|
||||||
|
item.sort_timesaccessed = item.timesaccessed = (item.timesaccessed or 0) + 1
|
||||||
|
item.save()
|
||||||
|
return app.send_static_file(html)
|
|
@ -0,0 +1,45 @@
|
||||||
|
import pdf
|
||||||
|
import epub
|
||||||
|
import txt
|
||||||
|
import os
|
||||||
|
import base64
|
||||||
|
import ox
|
||||||
|
|
||||||
|
def get_id(f):
|
||||||
|
return base64.b32encode(ox.sha1sum(f).decode('hex'))
|
||||||
|
|
||||||
|
def metadata(f):
|
||||||
|
ext = f.split('.')[-1]
|
||||||
|
data = {}
|
||||||
|
if ext == 'pdf':
|
||||||
|
info = pdf.info(f)
|
||||||
|
elif ext == 'epub':
|
||||||
|
info = epub.info(f)
|
||||||
|
elif ext == 'txt':
|
||||||
|
info = txt.info(f)
|
||||||
|
|
||||||
|
for key in ('title', 'author', 'date', 'publisher', 'isbn'):
|
||||||
|
if key in info:
|
||||||
|
value = info[key]
|
||||||
|
if isinstance(value, str):
|
||||||
|
try:
|
||||||
|
value = value.decode('utf-8')
|
||||||
|
except:
|
||||||
|
value = None
|
||||||
|
if value:
|
||||||
|
data[key] = info[key]
|
||||||
|
|
||||||
|
if 'isbn' in data:
|
||||||
|
value = data.pop('isbn')
|
||||||
|
if len(value) == 10:
|
||||||
|
data['isbn10'] = value
|
||||||
|
data['mainid'] = 'isbn10'
|
||||||
|
else:
|
||||||
|
data['isbn13'] = value
|
||||||
|
data['mainid'] = 'isbn13'
|
||||||
|
if not 'title' in data:
|
||||||
|
data['title'] = os.path.splitext(os.path.basename(f))[0]
|
||||||
|
if 'author' in data and isinstance(data['author'], basestring):
|
||||||
|
data['author'] = [data['author']]
|
||||||
|
return data
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vi:si:et:sw=4:sts=4:ts=4
|
||||||
|
from __future__ import division
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
import zipfile
|
||||||
|
from StringIO import StringIO
|
||||||
|
|
||||||
|
import Image
|
||||||
|
import stdnum.isbn
|
||||||
|
|
||||||
|
from utils import normalize_isbn, find_isbns
|
||||||
|
|
||||||
|
def cover(path):
|
||||||
|
img = Image.new('RGB', (80, 128))
|
||||||
|
o = StringIO()
|
||||||
|
img.save(o, format='jpeg')
|
||||||
|
data = o.getvalue()
|
||||||
|
o.close()
|
||||||
|
return data
|
||||||
|
|
||||||
|
def info(epub):
|
||||||
|
data = {}
|
||||||
|
z = zipfile.ZipFile(epub)
|
||||||
|
opf = [f.filename for f in z.filelist if f.filename.endswith('opf')]
|
||||||
|
if opf:
|
||||||
|
info = ET.fromstring(z.read(opf[0]))
|
||||||
|
metadata = info.findall('{http://www.idpf.org/2007/opf}metadata')[0]
|
||||||
|
for e in metadata.getchildren():
|
||||||
|
if e.text:
|
||||||
|
key = e.tag.split('}')[-1]
|
||||||
|
key = {
|
||||||
|
'creator': 'author',
|
||||||
|
}.get(key, key)
|
||||||
|
value = e.text
|
||||||
|
if key == 'identifier':
|
||||||
|
value = normalize_isbn(value)
|
||||||
|
if stdnum.isbn.is_valid(value):
|
||||||
|
data['isbn'] = value
|
||||||
|
else:
|
||||||
|
data[key] = e.text
|
||||||
|
text = extract_text(epub)
|
||||||
|
data['textsize'] = len(text)
|
||||||
|
if not 'isbn' in data:
|
||||||
|
isbn = extract_isbn(text)
|
||||||
|
if isbn:
|
||||||
|
data['isbn'] = isbn
|
||||||
|
return data
|
||||||
|
|
||||||
|
def extract_text(path):
|
||||||
|
data = ''
|
||||||
|
z = zipfile.ZipFile(path)
|
||||||
|
for f in z.filelist:
|
||||||
|
if f.filename.endswith('html'):
|
||||||
|
data += z.read(f.filename)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def extract_isbn(data):
|
||||||
|
isbns = find_isbns(data)
|
||||||
|
if isbns:
|
||||||
|
return isbns[0]
|
||||||
|
|
|
@ -0,0 +1,140 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vi:si:et:sw=4:sts=4:ts=4
|
||||||
|
from __future__ import division
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
from glob import glob
|
||||||
|
|
||||||
|
from pyPdf import PdfFileReader
|
||||||
|
import stdnum.isbn
|
||||||
|
|
||||||
|
import settings
|
||||||
|
from utils import normalize_isbn, find_isbns
|
||||||
|
|
||||||
|
def cover(pdf):
|
||||||
|
if sys.platform == 'darwin':
|
||||||
|
return ql_cover(pdf)
|
||||||
|
else:
|
||||||
|
return page(pdf, 1)
|
||||||
|
|
||||||
|
def ql_cover(pdf):
|
||||||
|
tmp = tempfile.mkdtemp()
|
||||||
|
cmd = [
|
||||||
|
'qlmanage',
|
||||||
|
'-t',
|
||||||
|
'-s',
|
||||||
|
'1024',
|
||||||
|
'-o',
|
||||||
|
tmp,
|
||||||
|
pdf
|
||||||
|
]
|
||||||
|
p = subprocess.Popen(cmd)
|
||||||
|
p.wait()
|
||||||
|
image = glob('%s/*' % tmp)[0]
|
||||||
|
with open(image, 'rb') as fd:
|
||||||
|
data = fd.read()
|
||||||
|
shutil.rmtree(tmp)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def page(pdf, page):
|
||||||
|
image = tempfile.mkstemp('.jpg')[1]
|
||||||
|
cmd = [
|
||||||
|
'gs', '-q',
|
||||||
|
'-dBATCH', '-dSAFER', '-dNOPAUSE', '-dNOPROMPT',
|
||||||
|
'-dMaxBitmap=500000000',
|
||||||
|
'-dAlignToPixels=0', '-dGridFitTT=2',
|
||||||
|
'-sDEVICE=jpeg', '-dTextAlphaBits=4', '-dGraphicsAlphaBits=4',
|
||||||
|
'-r72',
|
||||||
|
'-dUseCropBox',
|
||||||
|
'-dFirstPage=%d' % page,
|
||||||
|
'-dLastPage=%d' % page,
|
||||||
|
'-sOutputFile=%s' % image,
|
||||||
|
pdf
|
||||||
|
]
|
||||||
|
p = subprocess.Popen(cmd)
|
||||||
|
p.wait()
|
||||||
|
with open(image, 'rb') as fd:
|
||||||
|
data = fd.read()
|
||||||
|
os.unlink(image)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def info(pdf):
|
||||||
|
data = {}
|
||||||
|
with open(pdf, 'rb') as fd:
|
||||||
|
try:
|
||||||
|
pdfreader = PdfFileReader(fd)
|
||||||
|
info = pdfreader.getDocumentInfo()
|
||||||
|
if info:
|
||||||
|
for key in info:
|
||||||
|
if info[key]:
|
||||||
|
data[key[1:].lower()] = info[key]
|
||||||
|
xmp =pdfreader.getXmpMetadata()
|
||||||
|
if xmp:
|
||||||
|
for key in dir(xmp):
|
||||||
|
if key.startswith('dc_'):
|
||||||
|
value = getattr(xmp, key)
|
||||||
|
if isinstance(value, dict) and 'x-default' in value:
|
||||||
|
value = value['x-default']
|
||||||
|
elif isinstance(value, list):
|
||||||
|
value = [v.strip() for v in value if v.strip()]
|
||||||
|
_key = key[3:]
|
||||||
|
if value and _key not in data:
|
||||||
|
data[_key] = value
|
||||||
|
except:
|
||||||
|
print 'FAILED TO PARSE', pdf
|
||||||
|
import traceback
|
||||||
|
print traceback.print_exc()
|
||||||
|
|
||||||
|
if 'identifier' in data:
|
||||||
|
value = normalize_isbn(data['identifier'])
|
||||||
|
if stdnum.isbn.is_valid(value):
|
||||||
|
data['isbn'] = value
|
||||||
|
del data['identifier']
|
||||||
|
'''
|
||||||
|
cmd = ['pdfinfo', pdf]
|
||||||
|
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
|
stdout, stderr = p.communicate()
|
||||||
|
for line in stdout.strip().split('\n'):
|
||||||
|
parts = line.split(':')
|
||||||
|
key = parts[0].lower().strip()
|
||||||
|
if key:
|
||||||
|
data[key] = ':'.join(parts[1:]).strip()
|
||||||
|
for key in data.keys():
|
||||||
|
if not data[key]:
|
||||||
|
del data[key]
|
||||||
|
'''
|
||||||
|
text = extract_text(pdf)
|
||||||
|
data['textsize'] = len(text)
|
||||||
|
if settings.server['extract_text']:
|
||||||
|
if not 'isbn' in data:
|
||||||
|
isbn = extract_isbn(text)
|
||||||
|
if isbn:
|
||||||
|
data['isbn'] = isbn
|
||||||
|
return data
|
||||||
|
|
||||||
|
'''
|
||||||
|
#possbile alternative with gs
|
||||||
|
tmp = tempfile.mkstemp('.txt')[1]
|
||||||
|
cmd = ['gs', '-dBATCH', '-dNOPAUSE', '-sDEVICE=txtwrite', '-dFirstPage=3', '-dLastPage=5', '-sOutputFile=%s'%tmp, pdf]
|
||||||
|
|
||||||
|
'''
|
||||||
|
def extract_text(pdf):
|
||||||
|
if sys.platform == 'darwin':
|
||||||
|
cmd = ['/usr/bin/mdimport' '-d2', pdf]
|
||||||
|
else:
|
||||||
|
cmd = ['pdftotext', pdf, '-']
|
||||||
|
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
|
stdout, stderr = p.communicate()
|
||||||
|
if sys.platform == 'darwin':
|
||||||
|
stdout = stderr.split('kMDItemTextContent = "')[-1].split('\n')[0][:-2]
|
||||||
|
return stdout.strip()
|
||||||
|
|
||||||
|
def extract_isbn(text):
|
||||||
|
isbns = find_isbns(text)
|
||||||
|
if isbns:
|
||||||
|
return isbns[0]
|
|
@ -0,0 +1,41 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vi:si:et:sw=4:sts=4:ts=4
|
||||||
|
from __future__ import division
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from utils import find_isbns
|
||||||
|
from StringIO import StringIO
|
||||||
|
import Image
|
||||||
|
|
||||||
|
from pdf import ql_cover
|
||||||
|
|
||||||
|
def cover(path):
|
||||||
|
if sys.platform == 'darwin':
|
||||||
|
return ql_cover(path)
|
||||||
|
img = Image.new('RGB', (80, 128))
|
||||||
|
o = StringIO()
|
||||||
|
img.save(o, format='jpeg')
|
||||||
|
data = o.getvalue()
|
||||||
|
o.close()
|
||||||
|
return data
|
||||||
|
|
||||||
|
def info(path):
|
||||||
|
data = {}
|
||||||
|
data['title'] = os.path.splitext(os.path.basename(path))[0]
|
||||||
|
text = extract_text(path)
|
||||||
|
isbn = extract_isbn(text)
|
||||||
|
if isbn:
|
||||||
|
data['isbn'] = isbn
|
||||||
|
data['textsize'] = len(text)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def extract_text(path):
|
||||||
|
with open(path) as fd:
|
||||||
|
data = fd.read()
|
||||||
|
return data
|
||||||
|
|
||||||
|
def extract_isbn(text):
|
||||||
|
isbns = find_isbns(text)
|
||||||
|
if isbns:
|
||||||
|
return isbns[0]
|
|
@ -0,0 +1,52 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vi:si:et:sw=4:sts=4:ts=4
|
||||||
|
from __future__ import division
|
||||||
|
|
||||||
|
import ox
|
||||||
|
from ox.cache import read_url
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
from utils import normalize_isbn
|
||||||
|
from marc_countries import COUNTRIES
|
||||||
|
|
||||||
|
def info(id):
|
||||||
|
ns = '{http://www.loc.gov/mods/v3}'
|
||||||
|
url = 'http://lccn.loc.gov/%s/mods' % id
|
||||||
|
data = read_url(url)
|
||||||
|
mods = ET.fromstring(data)
|
||||||
|
|
||||||
|
info = {}
|
||||||
|
info['title'] = ''.join([e.text for e in mods.findall(ns + 'titleInfo')[0]])
|
||||||
|
origin = mods.findall(ns + 'originInfo')
|
||||||
|
if origin:
|
||||||
|
info['place'] = []
|
||||||
|
for place in origin[0].findall(ns + 'place'):
|
||||||
|
terms = place.findall(ns + 'placeTerm')
|
||||||
|
if terms and terms[0].attrib['type'] == 'text':
|
||||||
|
e = terms[0]
|
||||||
|
info['place'].append(e.text)
|
||||||
|
elif terms and terms[0].attrib['type'] == 'code':
|
||||||
|
e = terms[0]
|
||||||
|
info['country'] = COUNTRIES.get(e.text, e.text)
|
||||||
|
info['publisher'] = ''.join([e.text for e in origin[0].findall(ns + 'publisher')])
|
||||||
|
info['date'] = ''.join([e.text for e in origin[0].findall(ns + 'dateIssued')])
|
||||||
|
for i in mods.findall(ns + 'identifier'):
|
||||||
|
if i.attrib['type'] == 'oclc':
|
||||||
|
info['oclc'] = i.text.replace('ocn', '')
|
||||||
|
if i.attrib['type'] == 'lccn':
|
||||||
|
info['lccn'] = i.text
|
||||||
|
if i.attrib['type'] == 'isbn':
|
||||||
|
isbn = normalize_isbn(i.text)
|
||||||
|
info['isbn%s'%len(isbn)] = isbn
|
||||||
|
for i in mods.findall(ns + 'classification'):
|
||||||
|
if i.attrib['authority'] == 'ddc':
|
||||||
|
info['classification'] = i.text
|
||||||
|
info['author'] = []
|
||||||
|
for a in mods.findall(ns + 'name'):
|
||||||
|
if a.attrib['usage'] == 'primary':
|
||||||
|
info['author'].append(''.join([e.text for e in a.findall(ns + 'namePart')]))
|
||||||
|
info['author'] = [ox.normalize_name(a[:-1]) for a in info['author']]
|
||||||
|
for key in info.keys():
|
||||||
|
if not info[key]:
|
||||||
|
del info[key]
|
||||||
|
return info
|
|
@ -0,0 +1,409 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vi:si:et:sw=4:sts=4:ts=4
|
||||||
|
|
||||||
|
COUNTRIES = {
|
||||||
|
"gw": "Germany",
|
||||||
|
"gv": "Guinea",
|
||||||
|
"gu": "Guam",
|
||||||
|
"gt": "Guatemala",
|
||||||
|
"gs": "Georgia (Republic)",
|
||||||
|
"gr": "Greece",
|
||||||
|
"-ge": "Germany (East)",
|
||||||
|
"gp": "Guadeloupe",
|
||||||
|
"mnu": "Minnesota",
|
||||||
|
"gy": "Guyana",
|
||||||
|
"gd": "Grenada",
|
||||||
|
"gb": "Kiribati",
|
||||||
|
"go": "Gabon",
|
||||||
|
"gm": "Gambia",
|
||||||
|
"alu": "Alabama",
|
||||||
|
"gi": "Gibraltar",
|
||||||
|
"gh": "Ghana",
|
||||||
|
"tz": "Tanzania",
|
||||||
|
"tv": "Tuvalu",
|
||||||
|
"tu": "Turkey",
|
||||||
|
"tr": "Trinidad and Tobago",
|
||||||
|
"ts": "United Arab Emirates",
|
||||||
|
"to": "Tonga",
|
||||||
|
"tl": "Tokelau",
|
||||||
|
"tk": "Turkmenistan",
|
||||||
|
"th": "Thailand",
|
||||||
|
"ti": "Tunisia",
|
||||||
|
"tg": "Togo",
|
||||||
|
"tc": "Turks and Caicos Islands",
|
||||||
|
"ta": "Tajikistan",
|
||||||
|
"-gn": "Gilbert and Ellice Islands",
|
||||||
|
"-us": "United States",
|
||||||
|
"-ajr": "Azerbaijan S.S.R.",
|
||||||
|
"-iu": "Israel-Syria Demilitarized Zones",
|
||||||
|
"-iw": "Israel-Jordan Demilitarized Zones",
|
||||||
|
"za": "Zambia",
|
||||||
|
"nbu": "Nebraska",
|
||||||
|
"scu": "South Carolina",
|
||||||
|
"bg": "Bangladesh",
|
||||||
|
"cau": "California",
|
||||||
|
"abc": "Alberta",
|
||||||
|
"xoa": "Northern Territory",
|
||||||
|
"meu": "Maine",
|
||||||
|
"ctu": "Connecticut",
|
||||||
|
"my": "Malaysia",
|
||||||
|
"aku": "Alaska",
|
||||||
|
"gl": "Greenland",
|
||||||
|
"-cn": "Canada",
|
||||||
|
"wiu": "Wisconsin",
|
||||||
|
"-cz": "Canal Zone",
|
||||||
|
"txu": "Texas",
|
||||||
|
"-cs": "Czechoslovakia",
|
||||||
|
"-cp": "Canton and Enderbury Islands",
|
||||||
|
"msu": "Mississippi",
|
||||||
|
"-ln": "Central and Southern Line Islands",
|
||||||
|
"nkc": "New Brunswick",
|
||||||
|
"it": "Italy",
|
||||||
|
"tnu": "Tennessee",
|
||||||
|
"vp": "Various places",
|
||||||
|
"mg": "Madagascar",
|
||||||
|
"mf": "Mauritius",
|
||||||
|
"mc": "Monaco",
|
||||||
|
"-ur": "Soviet Union",
|
||||||
|
"mm": "Malta",
|
||||||
|
"ml": "Mali",
|
||||||
|
"mo": "Montenegro",
|
||||||
|
"flu": "Florida",
|
||||||
|
"deu": "Delaware",
|
||||||
|
"mk": "Oman",
|
||||||
|
"mj": "Montserrat",
|
||||||
|
"mu": "Mauritania",
|
||||||
|
"mw": "Malawi",
|
||||||
|
"mv": "Moldova",
|
||||||
|
"mq": "Martinique",
|
||||||
|
"mp": "Mongolia",
|
||||||
|
"mr": "Morocco",
|
||||||
|
"-ui": "United Kingdom Misc. Islands",
|
||||||
|
"mx": "Mexico",
|
||||||
|
"-uk": "United Kingdom",
|
||||||
|
"mz": "Mozambique",
|
||||||
|
"kyu": "Kentucky",
|
||||||
|
"hiu": "Hawaii",
|
||||||
|
"enk": "England",
|
||||||
|
"nyu": "New York (State)",
|
||||||
|
"fp": "French Polynesia",
|
||||||
|
"fr": "France",
|
||||||
|
"fs": "Terres australes et antarctiques françaises",
|
||||||
|
"mau": "Massachusetts",
|
||||||
|
"snc": "Saskatchewan",
|
||||||
|
"fa": "Faroe Islands",
|
||||||
|
"fg": "French Guiana",
|
||||||
|
"lau": "Louisiana",
|
||||||
|
"fj": "Fiji",
|
||||||
|
"fk": "Falkland Islands",
|
||||||
|
"fm": "Micronesia (Federated States)",
|
||||||
|
"sz": "Switzerland",
|
||||||
|
"sy": "Syria",
|
||||||
|
"sx": "Namibia",
|
||||||
|
"ss": "Western Sahara",
|
||||||
|
"sr": "Surinam",
|
||||||
|
"sq": "Swaziland",
|
||||||
|
"sp": "Spain",
|
||||||
|
"sw": "Sweden",
|
||||||
|
"su": "Saudi Arabia",
|
||||||
|
"st": "Saint-Martin",
|
||||||
|
"sj": "Sudan",
|
||||||
|
"si": "Singapore",
|
||||||
|
"sh": "Spanish North Africa",
|
||||||
|
"so": "Somalia",
|
||||||
|
"sn": "Sint Maarten",
|
||||||
|
"sm": "San Marino",
|
||||||
|
"sl": "Sierra Leone",
|
||||||
|
"sc": "Saint-Barthélemy",
|
||||||
|
"sa": "South Africa",
|
||||||
|
"sg": "Senegal",
|
||||||
|
"sf": "Sao Tome and Principe",
|
||||||
|
"se": "Seychelles",
|
||||||
|
"sd": "South Sudan",
|
||||||
|
"-unr": "Ukraine",
|
||||||
|
"-kgr": "Kirghiz S.S.R.",
|
||||||
|
"le": "Lebanon",
|
||||||
|
"lb": "Liberia",
|
||||||
|
"-hk": "Hong Kong",
|
||||||
|
"lo": "Lesotho",
|
||||||
|
"lh": "Liechtenstein",
|
||||||
|
"li": "Lithuania",
|
||||||
|
"lv": "Latvia",
|
||||||
|
"lu": "Luxembourg",
|
||||||
|
"vtu": "Vermont",
|
||||||
|
"ls": "Laos",
|
||||||
|
"xc": "Maldives",
|
||||||
|
"ly": "Libya",
|
||||||
|
"oku": "Oklahoma",
|
||||||
|
"ye": "Yemen",
|
||||||
|
"-tkr": "Turkmen S.S.R.",
|
||||||
|
"nfc": "Newfoundland and Labrador",
|
||||||
|
"ft": "Djibouti",
|
||||||
|
"em": "Timor-Leste",
|
||||||
|
"eg": "Equatorial Guinea",
|
||||||
|
"ea": "Eritrea",
|
||||||
|
"ec": "Ecuador",
|
||||||
|
"-gsr": "Georgian S.S.R.",
|
||||||
|
"et": "Ethiopia",
|
||||||
|
"es": "El Salvador",
|
||||||
|
"er": "Estonia",
|
||||||
|
"ru": "Russia (Federation)",
|
||||||
|
"rw": "Rwanda",
|
||||||
|
"re": "Réunion",
|
||||||
|
"rb": "Serbia",
|
||||||
|
"rm": "Romania",
|
||||||
|
"rh": "Zimbabwe",
|
||||||
|
"-err": "Estonia",
|
||||||
|
"oru": "Oregon",
|
||||||
|
"quc": "Québec (Province)",
|
||||||
|
"ntc": "Northwest Territories",
|
||||||
|
"wlk": "Wales",
|
||||||
|
"xj": "Saint Helena",
|
||||||
|
"xk": "Saint Lucia",
|
||||||
|
"xh": "Niue",
|
||||||
|
"xn": "Macedonia",
|
||||||
|
"xo": "Slovakia",
|
||||||
|
"xl": "Saint Pierre and Miquelon",
|
||||||
|
"xm": "Saint Vincent and the Grenadines",
|
||||||
|
"xb": "Cocos (Keeling) Islands",
|
||||||
|
"onc": "Ontario",
|
||||||
|
"xa": "Christmas Island (Indian Ocean)",
|
||||||
|
"xf": "Midway Islands",
|
||||||
|
"xd": "Saint Kitts-Nevis",
|
||||||
|
"xe": "Marshall Islands",
|
||||||
|
"nhu": "New Hampshire",
|
||||||
|
"xx": "No place, unknown, or undetermined",
|
||||||
|
"fi": "Finland",
|
||||||
|
"xr": "Czech Republic",
|
||||||
|
"xs": "South Georgia and the South Sandwich Islands",
|
||||||
|
"xp": "Spratly Island",
|
||||||
|
"xv": "Slovenia",
|
||||||
|
"-tt": "Trust Territory of the Pacific Islands",
|
||||||
|
"iau": "Iowa",
|
||||||
|
"ncu": "North Carolina",
|
||||||
|
"stk": "Scotland",
|
||||||
|
"xra": "South Australia",
|
||||||
|
"miu": "Michigan",
|
||||||
|
"kg": "Kyrgyzstan",
|
||||||
|
"ke": "Kenya",
|
||||||
|
"ko": "Korea (South)",
|
||||||
|
"kn": "Korea (North)",
|
||||||
|
"kv": "Kosovo",
|
||||||
|
"ku": "Kuwait",
|
||||||
|
"kz": "Kazakhstan",
|
||||||
|
"-pt": "Portuguese Timor",
|
||||||
|
"ksu": "Kansas",
|
||||||
|
"dm": "Benin",
|
||||||
|
"dk": "Denmark",
|
||||||
|
"-ys": "Yemen (People's Democratic Republic)",
|
||||||
|
"-yu": "Serbia and Montenegro",
|
||||||
|
"-bwr": "Byelorussian S.S.R.",
|
||||||
|
"dr": "Dominican Republic",
|
||||||
|
"dq": "Dominica",
|
||||||
|
"qa": "Qatar",
|
||||||
|
"aru": "Arkansas",
|
||||||
|
"nuc": "Nunavut",
|
||||||
|
"wf": "Wallis and Futuna",
|
||||||
|
"wk": "Wake Island",
|
||||||
|
"wj": "West Bank of the Jordan River",
|
||||||
|
"jm": "Jamaica",
|
||||||
|
"vra": "Victoria",
|
||||||
|
"jo": "Jordan",
|
||||||
|
"ws": "Samoa",
|
||||||
|
"ji": "Johnston Atoll",
|
||||||
|
"-na": "Netherlands Antilles",
|
||||||
|
"ja": "Japan",
|
||||||
|
"cou": "Colorado",
|
||||||
|
"-wb": "West Berlin",
|
||||||
|
"ilu": "Illinois",
|
||||||
|
"-nm": "Northern Mariana Islands",
|
||||||
|
"ck": "Colombia",
|
||||||
|
"cj": "Cayman Islands",
|
||||||
|
"ci": "Croatia",
|
||||||
|
"ch": "China (Republic : 1949- )",
|
||||||
|
"co": "Curaçao",
|
||||||
|
"cm": "Cameroon",
|
||||||
|
"cl": "Chile",
|
||||||
|
"-rur": "Russian S.F.S.R.",
|
||||||
|
"cb": "Cambodia",
|
||||||
|
"ca": "Caribbean Netherlands",
|
||||||
|
"cg": "Congo (Democratic Republic)",
|
||||||
|
"cf": "Congo (Brazzaville)",
|
||||||
|
"-lir": "Lithuania",
|
||||||
|
"cd": "Chad",
|
||||||
|
"cy": "Cyprus",
|
||||||
|
"cx": "Central African Republic",
|
||||||
|
"cr": "Costa Rica",
|
||||||
|
"cq": "Comoros",
|
||||||
|
"cw": "Cook Islands",
|
||||||
|
"cv": "Cape Verde",
|
||||||
|
"cu": "Cuba",
|
||||||
|
"pr": "Puerto Rico",
|
||||||
|
"pp": "Papua New Guinea",
|
||||||
|
"pw": "Palau",
|
||||||
|
"py": "Paraguay",
|
||||||
|
"pc": "Pitcairn Island",
|
||||||
|
"pf": "Paracel Islands",
|
||||||
|
"pg": "Guinea-Bissau",
|
||||||
|
"pe": "Peru",
|
||||||
|
"pk": "Pakistan",
|
||||||
|
"ph": "Philippines",
|
||||||
|
"pn": "Panama",
|
||||||
|
"po": "Portugal",
|
||||||
|
"pl": "Poland",
|
||||||
|
"pic": "Prince Edward Island",
|
||||||
|
"xxu": "United States",
|
||||||
|
"gau": "Georgia",
|
||||||
|
"xxc": "Canada",
|
||||||
|
"xxk": "United Kingdom",
|
||||||
|
"iy": "Iraq-Saudi Arabia Neutral Zone",
|
||||||
|
"vb": "British Virgin Islands",
|
||||||
|
"vc": "Vatican City",
|
||||||
|
"ve": "Venezuela",
|
||||||
|
"iq": "Iraq",
|
||||||
|
"vi": "Virgin Islands of the United States",
|
||||||
|
"is": "Israel",
|
||||||
|
"ir": "Iran",
|
||||||
|
"vm": "Vietnam",
|
||||||
|
"iv": "Côte d'Ivoire",
|
||||||
|
"ii": "India",
|
||||||
|
"-ac": "Ashmore and Cartier Islands",
|
||||||
|
"io": "Indonesia",
|
||||||
|
"-ai": "Anguilla",
|
||||||
|
"ic": "Iceland",
|
||||||
|
"ie": "Ireland",
|
||||||
|
"pau": "Pennsylvania",
|
||||||
|
"-jn": "Jan Mayen",
|
||||||
|
"nik": "Northern Ireland",
|
||||||
|
"wyu": "Wyoming",
|
||||||
|
"-air": "Armenian S.S.R.",
|
||||||
|
"-sv": "Swan Islands",
|
||||||
|
"-mvr": "Moldavian S.S.R.",
|
||||||
|
"-sk": "Sikkim",
|
||||||
|
"riu": "Rhode Island",
|
||||||
|
"-sb": "Svalbard",
|
||||||
|
"-xi": "Saint Kitts-Nevis-Anguilla",
|
||||||
|
"wea": "Western Australia",
|
||||||
|
"cc": "China",
|
||||||
|
"nvu": "Nevada",
|
||||||
|
"mou": "Missouri",
|
||||||
|
"ce": "Sri Lanka",
|
||||||
|
"qea": "Queensland",
|
||||||
|
"-mh": "Macao",
|
||||||
|
"nju": "New Jersey",
|
||||||
|
"ykc": "Yukon Territory",
|
||||||
|
"-vs": "Vietnam, South",
|
||||||
|
"tma": "Tasmania",
|
||||||
|
"-vn": "Vietnam, North",
|
||||||
|
"bd": "Burundi",
|
||||||
|
"be": "Belgium",
|
||||||
|
"bf": "Bahamas",
|
||||||
|
"nmu": "New Mexico",
|
||||||
|
"ba": "Bahrain",
|
||||||
|
"bb": "Barbados",
|
||||||
|
"bl": "Brazil",
|
||||||
|
"bm": "Bermuda Islands",
|
||||||
|
"bn": "Bosnia and Hercegovina",
|
||||||
|
"bo": "Bolivia",
|
||||||
|
"bh": "Belize",
|
||||||
|
"bi": "British Indian Ocean Territory",
|
||||||
|
"bt": "Bhutan",
|
||||||
|
"bu": "Bulgaria",
|
||||||
|
"bv": "Bouvet Island",
|
||||||
|
"bw": "Belarus",
|
||||||
|
"bp": "Solomon Islands",
|
||||||
|
"br": "Burma",
|
||||||
|
"bs": "Botswana",
|
||||||
|
"dcu": "District of Columbia",
|
||||||
|
"bx": "Brunei",
|
||||||
|
"aca": "Australian Capital Territory",
|
||||||
|
"idu": "Idaho",
|
||||||
|
"xna": "New South Wales",
|
||||||
|
"ot": "Mayotte",
|
||||||
|
"ndu": "North Dakota",
|
||||||
|
"nsc": "Nova Scotia",
|
||||||
|
"-kzr": "Kazakh S.S.R.",
|
||||||
|
"mbc": "Manitoba",
|
||||||
|
"-lvr": "Latvia",
|
||||||
|
"-uzr": "Uzbek S.S.R.",
|
||||||
|
"wau": "Washington (State)",
|
||||||
|
"vau": "Virginia",
|
||||||
|
"sdu": "South Dakota",
|
||||||
|
"gz": "Gaza Strip",
|
||||||
|
"ht": "Haiti",
|
||||||
|
"hu": "Hungary",
|
||||||
|
"ho": "Honduras",
|
||||||
|
"hm": "Heard and McDonald Islands",
|
||||||
|
"xga": "Coral Sea Islands Territory",
|
||||||
|
"uy": "Uruguay",
|
||||||
|
"uz": "Uzbekistan",
|
||||||
|
"uv": "Burkina Faso",
|
||||||
|
"up": "United States Misc. Pacific Islands",
|
||||||
|
"mtu": "Montana",
|
||||||
|
"un": "Ukraine",
|
||||||
|
"utu": "Utah",
|
||||||
|
"ug": "Uganda",
|
||||||
|
"ua": "Egypt",
|
||||||
|
"azu": "Arizona",
|
||||||
|
"uc": "United States Misc. Caribbean Islands",
|
||||||
|
"aa": "Albania",
|
||||||
|
"ae": "Algeria",
|
||||||
|
"ag": "Argentina",
|
||||||
|
"af": "Afghanistan",
|
||||||
|
"ai": "Armenia (Republic)",
|
||||||
|
"inu": "Indiana",
|
||||||
|
"uik": "United Kingdom Misc. Islands",
|
||||||
|
"aj": "Azerbaijan",
|
||||||
|
"am": "Anguilla",
|
||||||
|
"ao": "Angola",
|
||||||
|
"an": "Andorra",
|
||||||
|
"aq": "Antigua and Barbuda",
|
||||||
|
"as": "American Samoa",
|
||||||
|
"au": "Austria",
|
||||||
|
"at": "Australia",
|
||||||
|
"aw": "Aruba",
|
||||||
|
"ay": "Antarctica",
|
||||||
|
"ohu": "Ohio",
|
||||||
|
"nl": "New Caledonia",
|
||||||
|
"-ry": "Ryukyu Islands, Southern",
|
||||||
|
"nn": "Vanuatu",
|
||||||
|
"no": "Norway",
|
||||||
|
"ne": "Netherlands",
|
||||||
|
"ng": "Niger",
|
||||||
|
"nx": "Norfolk Island",
|
||||||
|
"nz": "New Zealand",
|
||||||
|
"np": "Nepal",
|
||||||
|
"nq": "Nicaragua",
|
||||||
|
"nr": "Nigeria",
|
||||||
|
"mdu": "Maryland",
|
||||||
|
"nu": "Nauru",
|
||||||
|
"nw": "Northern Mariana Islands",
|
||||||
|
"wvu": "West Virginia",
|
||||||
|
"-xxr": "Soviet Union",
|
||||||
|
"-tar": "Tajik S.S.R.",
|
||||||
|
"bcc": "British Columbia"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import ox
|
||||||
|
from ox.cache import read_url
|
||||||
|
|
||||||
|
url = "http://www.loc.gov/marc/countries/countries_code.html"
|
||||||
|
data = read_url(url)
|
||||||
|
countries = dict([
|
||||||
|
[ox.strip_tags(c) for c in r]
|
||||||
|
for r in re.compile('<tr>.*?class="code">(.*?)</td>.*?<td>(.*?)</td>', re.DOTALL).findall(data)
|
||||||
|
])
|
||||||
|
|
||||||
|
data = json.dumps(countries, indent=4, ensure_ascii=False).encode('utf-8')
|
||||||
|
with open(__file__) as f:
|
||||||
|
pydata = f.read()
|
||||||
|
pydata = re.sub(
|
||||||
|
re.compile('\nCOUNTRIES = {.*?}\n\n', re.DOTALL),
|
||||||
|
'\nCOUNTRIES = %s\n\n' % data, pydata)
|
||||||
|
|
||||||
|
with open(__file__, 'w') as f:
|
||||||
|
f.write(pydata)
|
|
@ -0,0 +1,67 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vi:si:et:sw=4:sts=4:ts=4
|
||||||
|
from __future__ import division
|
||||||
|
|
||||||
|
from ox.cache import read_url
|
||||||
|
import json
|
||||||
|
|
||||||
|
from utils import normalize_isbn
|
||||||
|
from marc_countries import COUNTRIES
|
||||||
|
|
||||||
|
def find(query):
|
||||||
|
url = 'https://openlibrary.org/search.json?q=%s' % query
|
||||||
|
data = json.loads(read_url(url))
|
||||||
|
return data
|
||||||
|
|
||||||
|
def authors(authors):
|
||||||
|
return resolve_names(authors)
|
||||||
|
|
||||||
|
def resolve_names(objects, key='name'):
|
||||||
|
r = []
|
||||||
|
for o in objects:
|
||||||
|
url = 'https://openlibrary.org%s.json' % o['key']
|
||||||
|
data = json.loads(read_url(url))
|
||||||
|
r.append(data[key])
|
||||||
|
return r
|
||||||
|
|
||||||
|
def languages(languages):
|
||||||
|
return resolve_names(languages)
|
||||||
|
|
||||||
|
def info(id):
|
||||||
|
data = {}
|
||||||
|
url = 'https://openlibrary.org/books/%s.json' % id
|
||||||
|
info = json.loads(read_url(url))
|
||||||
|
keys = {
|
||||||
|
'title': 'title',
|
||||||
|
'authors': 'author',
|
||||||
|
'publishers': 'publisher',
|
||||||
|
'languages': 'language',
|
||||||
|
'publish_places': 'place',
|
||||||
|
'publish_country': 'country',
|
||||||
|
'covers': 'cover',
|
||||||
|
'isbn_10': 'isbn10',
|
||||||
|
'isbn_13': 'isbn13',
|
||||||
|
'lccn': 'lccn',
|
||||||
|
'oclc_numbers': 'oclc',
|
||||||
|
'dewey_decimal_class': 'classification',
|
||||||
|
'number_of_pages': 'pages',
|
||||||
|
}
|
||||||
|
for key in keys:
|
||||||
|
if key in info:
|
||||||
|
value = info[key]
|
||||||
|
if key == 'authors':
|
||||||
|
value = authors(value)
|
||||||
|
elif key == 'publish_country':
|
||||||
|
value = COUNTRIES.get(value, value)
|
||||||
|
elif key == 'covers':
|
||||||
|
value = 'https://covers.openlibrary.org/b/id/%s.jpg' % value[0]
|
||||||
|
value = COUNTRIES.get(value, value)
|
||||||
|
elif key == 'languages':
|
||||||
|
value = languages(value)
|
||||||
|
elif isinstance(value, list) and key not in ('publish_places'):
|
||||||
|
value = value[0]
|
||||||
|
if key in ('isbn_10', 'isbn_13'):
|
||||||
|
value = normalize_isbn(value)
|
||||||
|
data[keys[key]] = value
|
||||||
|
return data
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
import json
|
||||||
|
from ox.cache import read_url
|
||||||
|
import ox.web.lookupbyisbn
|
||||||
|
|
||||||
|
from utils import normalize_isbn
|
||||||
|
|
||||||
|
import ol
|
||||||
|
|
||||||
|
def add_lookupbyisbn(item):
|
||||||
|
isbn = item.meta.get('isbn10', item.meta.get('isbn13'))
|
||||||
|
if isbn:
|
||||||
|
more = ox.web.lookupbyisbn.get_data(isbn)
|
||||||
|
if more:
|
||||||
|
for key in more:
|
||||||
|
if more[key]:
|
||||||
|
value = more[key]
|
||||||
|
if isinstance(value, basestring):
|
||||||
|
value = ox.strip_tags(ox.decode_html(value))
|
||||||
|
elif isinstance(value, list):
|
||||||
|
value = [ox.strip_tags(ox.decode_html(v)) for v in value]
|
||||||
|
item.meta[key] = value
|
||||||
|
|
||||||
|
if 'author' in item.meta and isinstance(item.meta['author'], basestring):
|
||||||
|
item.meta['author'] = [item.meta['author']]
|
||||||
|
if 'isbn' in item.meta:
|
||||||
|
del item.meta['isbn']
|
||||||
|
|
||||||
|
def update_ol(item):
|
||||||
|
info = ol.info(item.meta['olid'])
|
||||||
|
for key in info:
|
||||||
|
item.meta[key] = info[key]
|
||||||
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
import settings
|
||||||
|
from changelog import Changelog
|
||||||
|
from user.models import User
|
||||||
|
|
||||||
|
import state
|
||||||
|
from websocket import trigger_event
|
||||||
|
|
||||||
|
def api_pullChanges(app, remote_id, user_id=None, from_=None, to=None):
|
||||||
|
if user_id and not from_ and not to:
|
||||||
|
from_ = user_id
|
||||||
|
user_id = None
|
||||||
|
if user_id and from_ and not to:
|
||||||
|
if isinstance(user_id, int):
|
||||||
|
to = from_
|
||||||
|
from_ = user_id
|
||||||
|
user_id = None
|
||||||
|
from_ = from_ or 0
|
||||||
|
if user_id:
|
||||||
|
return []
|
||||||
|
if not user_id:
|
||||||
|
user_id = settings.USER_ID
|
||||||
|
qs = Changelog.query.filter_by(user_id=user_id)
|
||||||
|
if from_:
|
||||||
|
qs = qs.filter(Changelog.revision>=from_)
|
||||||
|
if to:
|
||||||
|
qs = qs.filter(Changelog.revision<to)
|
||||||
|
state.nodes.queue('add', remote_id)
|
||||||
|
return [c.json() for c in qs]
|
||||||
|
|
||||||
|
def api_pushChanges(app, user_id, changes):
|
||||||
|
user = User.get(user_id)
|
||||||
|
for change in changes:
|
||||||
|
if not Changelog.apply_change(user, change):
|
||||||
|
print 'FAILED TO APPLY CHANGE', change
|
||||||
|
state.nodes.queue(user_id, 'pullChanges')
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def api_requestPeering(app, user_id, username, message):
|
||||||
|
user = User.get_or_create(user_id)
|
||||||
|
if not user.info:
|
||||||
|
user.info = {}
|
||||||
|
if not user.peered:
|
||||||
|
if user.pending == 'sent':
|
||||||
|
user.info['message'] = message
|
||||||
|
user.update_peering(True, username)
|
||||||
|
else:
|
||||||
|
user.pending = 'received'
|
||||||
|
user.info['username'] = username
|
||||||
|
user.info['message'] = message
|
||||||
|
user.save()
|
||||||
|
trigger_event('peering', user.json())
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def api_acceptPeering(app, user_id, username, message):
|
||||||
|
user = User.get(user_id)
|
||||||
|
if user and user.pending == 'sent':
|
||||||
|
if not user.info:
|
||||||
|
user.info = {}
|
||||||
|
user.info['username'] = username
|
||||||
|
user.info['message'] = message
|
||||||
|
user.update_peering(True, username)
|
||||||
|
trigger_event('peering', user.json())
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def api_rejectPeering(app, user_id, message):
|
||||||
|
user = User.get(user_id)
|
||||||
|
if user:
|
||||||
|
if not user.info:
|
||||||
|
user.info = {}
|
||||||
|
user.info['message'] = message
|
||||||
|
user.update_peering(False)
|
||||||
|
trigger_event('peering', user.json())
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def api_removePeering(app, user_id, message):
|
||||||
|
user = User.get(user_id)
|
||||||
|
if user:
|
||||||
|
user.peered = False
|
||||||
|
user.info['message'] = message
|
||||||
|
user.save()
|
||||||
|
trigger_event('peering', {'id': user.id, 'peered': user.peered})
|
||||||
|
return True
|
||||||
|
return False
|
|
@ -0,0 +1,25 @@
|
||||||
|
import OpenSSL
|
||||||
|
|
||||||
|
key = OpenSSL.crypto.PKey()
|
||||||
|
key.generate_key(OpenSSL.crypto.TYPE_RSA, 2048)
|
||||||
|
|
||||||
|
ca = OpenSSL.crypto.X509()
|
||||||
|
ca.set_version(2)
|
||||||
|
ca.set_serial_number(1)
|
||||||
|
ca.get_subject().CN = "put_ed25519_key_here"
|
||||||
|
ca.gmtime_adj_notBefore(0)
|
||||||
|
ca.gmtime_adj_notAfter(24 * 60 * 60)
|
||||||
|
ca.set_issuer(ca.get_subject())
|
||||||
|
ca.set_pubkey(key)
|
||||||
|
ca.add_extensions([
|
||||||
|
OpenSSL.crypto.X509Extension("basicConstraints", True,
|
||||||
|
"CA:TRUE, pathlen:0"),
|
||||||
|
OpenSSL.crypto.X509Extension("keyUsage", True,
|
||||||
|
"keyCertSign, cRLSign"),
|
||||||
|
OpenSSL.crypto.X509Extension("subjectKeyIdentifier", False, "hash",
|
||||||
|
subject=ca),
|
||||||
|
OpenSSL.crypto.X509Extension("authorityKeyIdentifier", False, "keyid:always",issuer=ca)
|
||||||
|
])
|
||||||
|
ca.sign(key, "sha1")
|
||||||
|
open("MyCertificate.crt.bin", "wb").write(OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_ASN1, ca))
|
||||||
|
|
|
@ -0,0 +1,119 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vi:si:et:sw=4:sts=4:ts=4
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tornado
|
||||||
|
from tornado.web import StaticFileHandler, Application, FallbackHandler
|
||||||
|
from tornado.wsgi import WSGIContainer
|
||||||
|
from tornado.httpserver import HTTPServer
|
||||||
|
from tornado.ioloop import IOLoop, PeriodicCallback
|
||||||
|
|
||||||
|
import settings
|
||||||
|
|
||||||
|
import directory
|
||||||
|
import utils
|
||||||
|
import state
|
||||||
|
import user
|
||||||
|
|
||||||
|
import json
|
||||||
|
from ed25519_utils import valid
|
||||||
|
import api
|
||||||
|
|
||||||
|
class NodeHandler(tornado.web.RequestHandler):
|
||||||
|
|
||||||
|
def initialize(self, app):
|
||||||
|
self.app = app
|
||||||
|
|
||||||
|
|
||||||
|
def post(self):
|
||||||
|
request = self.request
|
||||||
|
if request.method == 'POST':
|
||||||
|
'''
|
||||||
|
API
|
||||||
|
pullChanges [userid] from [to]
|
||||||
|
pushChanges [index, change]
|
||||||
|
requestPeering username message
|
||||||
|
acceptPeering username message
|
||||||
|
rejectPeering message
|
||||||
|
removePeering message
|
||||||
|
|
||||||
|
ping responds public ip
|
||||||
|
'''
|
||||||
|
key = str(request.headers['X-Ed25519-Key'])
|
||||||
|
sig = str(request.headers['X-Ed25519-Signature'])
|
||||||
|
data = request.body
|
||||||
|
content = {}
|
||||||
|
if valid(key, data, sig):
|
||||||
|
action, args = json.loads(data)
|
||||||
|
print 'action', action, args
|
||||||
|
if action == 'ping':
|
||||||
|
content = {
|
||||||
|
'ip': request.remote_addr
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
with self.app.app_context():
|
||||||
|
if action in (
|
||||||
|
'requestPeering', 'acceptPeering', 'rejectPeering', 'removePeering'
|
||||||
|
) or user.models.User.get(key):
|
||||||
|
content = getattr(api, 'api_' + action)(self.app, key, *args)
|
||||||
|
else:
|
||||||
|
print 'PEER', key, 'IS UNKNOWN SEND 403'
|
||||||
|
self.set_status(403)
|
||||||
|
content = {
|
||||||
|
'status': 'not peered'
|
||||||
|
}
|
||||||
|
content = json.dumps(content)
|
||||||
|
sig = settings.sk.sign(content, encoding='base64')
|
||||||
|
self.set_header('X-Ed25519-Signature', sig)
|
||||||
|
self.write(content)
|
||||||
|
self.finish()
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
self.write('Open Media Library')
|
||||||
|
self.finish()
|
||||||
|
|
||||||
|
class ShareHandler(tornado.web.RequestHandler):
|
||||||
|
|
||||||
|
def initialize(self, app):
|
||||||
|
self.app = app
|
||||||
|
|
||||||
|
def get(self, id):
|
||||||
|
with self.app.app_context():
|
||||||
|
import item.models
|
||||||
|
i = item.models.Item.get(id)
|
||||||
|
if not i:
|
||||||
|
self.set_status(404)
|
||||||
|
self.finish()
|
||||||
|
path = i.get_path()
|
||||||
|
mimetype = {
|
||||||
|
'epub': 'application/epub+zip',
|
||||||
|
'pdf': 'application/pdf',
|
||||||
|
'txt': 'text/plain',
|
||||||
|
}.get(path.split('.')[-1], None)
|
||||||
|
self.set_header('Content-Type', mimetype)
|
||||||
|
print 'GET file', id
|
||||||
|
with open(path, 'rb') as f:
|
||||||
|
while 1:
|
||||||
|
data = f.read(16384)
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
self.write(data)
|
||||||
|
self.finish()
|
||||||
|
|
||||||
|
|
||||||
|
def start(app):
|
||||||
|
http_server = tornado.web.Application([
|
||||||
|
(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()
|
||||||
|
state.online = directory.put(settings.sk, {
|
||||||
|
'host': host,
|
||||||
|
'port': settings.server['node_port']
|
||||||
|
})
|
||||||
|
return http_server
|
|
@ -0,0 +1,19 @@
|
||||||
|
import socket
|
||||||
|
import requests
|
||||||
|
from urlparse import urlparse
|
||||||
|
|
||||||
|
def get_public_ipv6():
|
||||||
|
host = ('2a01:4f8:120:3201::3', 25519)
|
||||||
|
s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
|
||||||
|
s.connect(host)
|
||||||
|
ip = s.getsockname()[0]
|
||||||
|
s.close()
|
||||||
|
return ip
|
||||||
|
|
||||||
|
def get_public_ipv4():
|
||||||
|
host = ('10.0.3.1', 25519)
|
||||||
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
s.connect(host)
|
||||||
|
ip = s.getsockname()[0]
|
||||||
|
s.close()
|
||||||
|
return ip
|
|
@ -0,0 +1,263 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vi:si:et:sw=4:sts=4:ts=4
|
||||||
|
from __future__ import division
|
||||||
|
|
||||||
|
from Queue import Queue
|
||||||
|
from threading import Thread
|
||||||
|
import json
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
import os
|
||||||
|
|
||||||
|
import ox
|
||||||
|
import ed25519
|
||||||
|
import requests
|
||||||
|
|
||||||
|
import settings
|
||||||
|
import user.models
|
||||||
|
from changelog import Changelog
|
||||||
|
|
||||||
|
import directory
|
||||||
|
from websocket import trigger_event
|
||||||
|
|
||||||
|
ENCODING='base64'
|
||||||
|
|
||||||
|
class Node(object):
|
||||||
|
online = False
|
||||||
|
download_speed = 0
|
||||||
|
|
||||||
|
def __init__(self, app, user):
|
||||||
|
self._app = app
|
||||||
|
self.user_id = user.id
|
||||||
|
key = str(user.id)
|
||||||
|
self.vk = ed25519.VerifyingKey(key, encoding=ENCODING)
|
||||||
|
self.go_online()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url(self):
|
||||||
|
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):
|
||||||
|
r = directory.get(self.vk)
|
||||||
|
if r:
|
||||||
|
self.host = r['host']
|
||||||
|
if 'port' in r:
|
||||||
|
self.port = r['port']
|
||||||
|
else:
|
||||||
|
self.host = None
|
||||||
|
self.port = 9851
|
||||||
|
|
||||||
|
def request(self, action, *args):
|
||||||
|
if not self.host:
|
||||||
|
self.resolve_host()
|
||||||
|
if not self.host:
|
||||||
|
return None
|
||||||
|
content = json.dumps([action, args])
|
||||||
|
sig = settings.sk.sign(content, encoding=ENCODING)
|
||||||
|
headers = {
|
||||||
|
'User-Agent': settings.USER_AGENT,
|
||||||
|
'Accept': 'text/plain',
|
||||||
|
'Accept-Encoding': 'gzip',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Ed25519-Key': settings.USER_ID,
|
||||||
|
'X-Ed25519-Signature': sig,
|
||||||
|
}
|
||||||
|
r = requests.post(self.url, data=content, headers=headers)
|
||||||
|
if r.status_code == 403:
|
||||||
|
print 'REMOTE ENDED PEERING'
|
||||||
|
if self.user.peered:
|
||||||
|
self.user.update_peering(False)
|
||||||
|
data = r.content
|
||||||
|
sig = r.headers.get('X-Ed25519-Signature')
|
||||||
|
if sig and self._valid(data, sig):
|
||||||
|
response = json.loads(data)
|
||||||
|
else:
|
||||||
|
response = None
|
||||||
|
return response
|
||||||
|
|
||||||
|
def _valid(self, data, sig):
|
||||||
|
try:
|
||||||
|
self.vk.verify(sig, data, encoding=ENCODING)
|
||||||
|
#except ed25519.BadSignatureError:
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def user(self):
|
||||||
|
return user.models.User.get_or_create(self.user_id)
|
||||||
|
|
||||||
|
def go_online(self):
|
||||||
|
self.resolve_host()
|
||||||
|
if self.user.peered:
|
||||||
|
try:
|
||||||
|
self.online = False
|
||||||
|
print 'type to connect to', self.user_id
|
||||||
|
self.pullChanges()
|
||||||
|
print 'connected to', self.user_id
|
||||||
|
self.online = True
|
||||||
|
except:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
print 'failed to connect to', self.user_id
|
||||||
|
self.online = False
|
||||||
|
else:
|
||||||
|
self.online = False
|
||||||
|
trigger_event('status', {
|
||||||
|
'id': self.user_id,
|
||||||
|
'status': 'online' if self.online else 'offline'
|
||||||
|
})
|
||||||
|
|
||||||
|
def pullChanges(self):
|
||||||
|
with self._app.app_context():
|
||||||
|
last = Changelog.query.filter_by(user_id=self.user_id).order_by('-revision').first()
|
||||||
|
from_revision = last.revision + 1 if last else 0
|
||||||
|
changes = self.request('pullChanges', from_revision)
|
||||||
|
if not changes:
|
||||||
|
return False
|
||||||
|
for change in changes:
|
||||||
|
if not Changelog.apply_change(self.user, change):
|
||||||
|
print 'FAIL', change
|
||||||
|
break
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def pushChanges(self, changes):
|
||||||
|
print 'pushing changes to', self.user_id, changes
|
||||||
|
try:
|
||||||
|
r = self.request('pushChanges', changes)
|
||||||
|
except:
|
||||||
|
self.online = False
|
||||||
|
trigger_event('status', {
|
||||||
|
'id': self.user_id,
|
||||||
|
'status': 'offline'
|
||||||
|
})
|
||||||
|
r = False
|
||||||
|
print r
|
||||||
|
|
||||||
|
def requestPeering(self, message):
|
||||||
|
p = self.user
|
||||||
|
p.pending = 'sent'
|
||||||
|
p.save()
|
||||||
|
r = self.request('requestPeering', settings.preferences['username'], message)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def acceptPeering(self, message):
|
||||||
|
r = self.request('acceptPeering', settings.preferences['username'], message)
|
||||||
|
p = self.user
|
||||||
|
p.update_peering(True)
|
||||||
|
self.go_online()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def rejectPeering(self, message):
|
||||||
|
r = self.request('rejectPeering', message)
|
||||||
|
p = self.user
|
||||||
|
p.update_peering(False)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def removePeering(self, message):
|
||||||
|
r = self.request('removePeering', message)
|
||||||
|
p = self.user
|
||||||
|
p.update_peering(False)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def download(self, item):
|
||||||
|
url = '%s/get/%s' % (self.url, item.id)
|
||||||
|
headers = {
|
||||||
|
'User-Agent': settings.USER_AGENT,
|
||||||
|
}
|
||||||
|
t1 = datetime.now()
|
||||||
|
print 'GET', url
|
||||||
|
r = requests.get(url, headers=headers)
|
||||||
|
if r.status_code == 200:
|
||||||
|
t2 = datetime.now()
|
||||||
|
duration = (t2-t1).total_seconds()
|
||||||
|
if duration:
|
||||||
|
self.download_speed = len(r.content) / duration
|
||||||
|
print 'SPEED', ox.format_bits(self.download_speed)
|
||||||
|
return item.save_file(r.content)
|
||||||
|
else:
|
||||||
|
print 'FAILED', url
|
||||||
|
return False
|
||||||
|
|
||||||
|
def download_upgrade(self):
|
||||||
|
for module in settings.release['modules']:
|
||||||
|
path = os.path.join(settings.update_path, settings.release['modules'][module]['name'])
|
||||||
|
if not os.path.exists(path):
|
||||||
|
url = '%s/oml/%s' % (self.url, settings.release['modules'][module]['name'])
|
||||||
|
sha1 = settings.release['modules'][module]['sha1']
|
||||||
|
headers = {
|
||||||
|
'User-Agent': settings.USER_AGENT,
|
||||||
|
}
|
||||||
|
r = requests.get(url, headers=headers)
|
||||||
|
if r.status_code == 200:
|
||||||
|
with open(path, 'w') as fd:
|
||||||
|
fd.write(r.content)
|
||||||
|
if (ox.sha1sum(path) != sha1):
|
||||||
|
print 'invalid update!'
|
||||||
|
os.unlink(path)
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
class Nodes(Thread):
|
||||||
|
_nodes = {}
|
||||||
|
|
||||||
|
def __init__(self, app):
|
||||||
|
self._app = app
|
||||||
|
self._q = Queue()
|
||||||
|
self._running = True
|
||||||
|
Thread.__init__(self)
|
||||||
|
self.daemon = True
|
||||||
|
self.start()
|
||||||
|
|
||||||
|
def queue(self, *args):
|
||||||
|
self._q.put(list(args))
|
||||||
|
|
||||||
|
def check_online(self, id):
|
||||||
|
return id in self._nodes and self._nodes[id].online
|
||||||
|
|
||||||
|
def download(self, id, item):
|
||||||
|
return id in self._nodes and self._nodes[id].download(item)
|
||||||
|
|
||||||
|
def _call(self, target, action, *args):
|
||||||
|
print 'call', target, action, args
|
||||||
|
if target == 'all':
|
||||||
|
nodes = self._nodes.values()
|
||||||
|
elif target == 'online':
|
||||||
|
nodes = [n for n in self._nodes.values() if n.online]
|
||||||
|
else:
|
||||||
|
nodes = [self._nodes[target]]
|
||||||
|
for node in nodes:
|
||||||
|
getattr(node, action)(*args)
|
||||||
|
|
||||||
|
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))
|
||||||
|
else:
|
||||||
|
self._nodes[user_id].online = True
|
||||||
|
trigger_event('status', {
|
||||||
|
'id': user_id,
|
||||||
|
'status': 'online'
|
||||||
|
})
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
with self._app.app_context():
|
||||||
|
while self._running:
|
||||||
|
args = self._q.get()
|
||||||
|
if args:
|
||||||
|
if args[0] == 'add':
|
||||||
|
self._add_node(args[1])
|
||||||
|
else:
|
||||||
|
print 'next', args
|
||||||
|
self._call(*args)
|
||||||
|
|
||||||
|
def join(self):
|
||||||
|
self._running = False
|
||||||
|
self._q.put(None)
|
||||||
|
return Thread.join(self)
|
|
@ -0,0 +1,154 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vi:si:et:sw=4:sts=4:ts=4
|
||||||
|
from __future__ import division, with_statement
|
||||||
|
import inspect
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
|
||||||
|
from flask import request, Blueprint
|
||||||
|
from .shortcuts import render_to_json_response, json_response
|
||||||
|
|
||||||
|
app = Blueprint('oxflask', __name__)
|
||||||
|
|
||||||
|
@app.route('/api/', methods=['POST', 'OPTIONS'])
|
||||||
|
def api():
|
||||||
|
if request.method == "OPTIONS":
|
||||||
|
response = render_to_json_response({'status': {'code': 200, 'text': 'use POST'}})
|
||||||
|
response.headers['Access-Control-Allow-Origin'] = '*'
|
||||||
|
return response
|
||||||
|
if not 'action' in request.form:
|
||||||
|
methods = actions.keys()
|
||||||
|
api = []
|
||||||
|
for f in sorted(methods):
|
||||||
|
api.append({'name': f,
|
||||||
|
'doc': actions.doc(f).replace('\n', '<br>\n')})
|
||||||
|
return render_to_json_response(api)
|
||||||
|
action = request.form['action']
|
||||||
|
f = actions.get(action)
|
||||||
|
if f:
|
||||||
|
response = f(request)
|
||||||
|
else:
|
||||||
|
response = render_to_json_response(json_response(status=400,
|
||||||
|
text='Unknown action %s' % action))
|
||||||
|
response.headers['Access-Control-Allow-Origin'] = '*'
|
||||||
|
return response
|
||||||
|
|
||||||
|
def trim(docstring):
|
||||||
|
if not docstring:
|
||||||
|
return ''
|
||||||
|
# Convert tabs to spaces (following the normal Python rules)
|
||||||
|
# and split into a list of lines:
|
||||||
|
lines = docstring.expandtabs().splitlines()
|
||||||
|
# Determine minimum indentation (first line doesn't count):
|
||||||
|
indent = sys.maxint
|
||||||
|
for line in lines[1:]:
|
||||||
|
stripped = line.lstrip()
|
||||||
|
if stripped:
|
||||||
|
indent = min(indent, len(line) - len(stripped))
|
||||||
|
# Remove indentation (first line is special):
|
||||||
|
trimmed = [lines[0].strip()]
|
||||||
|
if indent < sys.maxint:
|
||||||
|
for line in lines[1:]:
|
||||||
|
trimmed.append(line[indent:].rstrip())
|
||||||
|
# Strip off trailing and leading blank lines:
|
||||||
|
while trimmed and not trimmed[-1]:
|
||||||
|
trimmed.pop()
|
||||||
|
while trimmed and not trimmed[0]:
|
||||||
|
trimmed.pop(0)
|
||||||
|
# Return a single string:
|
||||||
|
return '\n'.join(trimmed)
|
||||||
|
|
||||||
|
|
||||||
|
class ApiActions(dict):
|
||||||
|
properties = {}
|
||||||
|
versions = {}
|
||||||
|
def __init__(self):
|
||||||
|
|
||||||
|
def api(request):
|
||||||
|
'''
|
||||||
|
returns list of all known api actions
|
||||||
|
param data {
|
||||||
|
docs: bool
|
||||||
|
}
|
||||||
|
if docs is true, action properties contain docstrings
|
||||||
|
return {
|
||||||
|
status: {'code': int, 'text': string},
|
||||||
|
data: {
|
||||||
|
actions: {
|
||||||
|
'api': {
|
||||||
|
cache: true,
|
||||||
|
doc: 'recursion'
|
||||||
|
},
|
||||||
|
'hello': {
|
||||||
|
cache: true,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
data = json.loads(request.form.get('data', '{}'))
|
||||||
|
docs = data.get('docs', False)
|
||||||
|
code = data.get('code', False)
|
||||||
|
version = getattr(request, 'version', None)
|
||||||
|
if version:
|
||||||
|
_actions = self.versions.get(version, {}).keys()
|
||||||
|
_actions = list(set(_actions + self.keys()))
|
||||||
|
else:
|
||||||
|
_actions = self.keys()
|
||||||
|
_actions.sort()
|
||||||
|
actions = {}
|
||||||
|
for a in _actions:
|
||||||
|
actions[a] = self.properties[a]
|
||||||
|
if docs:
|
||||||
|
actions[a]['doc'] = self.doc(a, version)
|
||||||
|
if code:
|
||||||
|
actions[a]['code'] = self.code(a, version)
|
||||||
|
response = json_response({'actions': actions})
|
||||||
|
return render_to_json_response(response)
|
||||||
|
self.register(api)
|
||||||
|
|
||||||
|
def doc(self, name, version=None):
|
||||||
|
if version:
|
||||||
|
f = self.versions[version].get(name, self.get(name))
|
||||||
|
else:
|
||||||
|
f = self[name]
|
||||||
|
return trim(f.__doc__)
|
||||||
|
|
||||||
|
def code(self, name, version=None):
|
||||||
|
if version:
|
||||||
|
f = self.versions[version].get(name, self.get(name))
|
||||||
|
else:
|
||||||
|
f = self[name]
|
||||||
|
if name != 'api' and hasattr(f, 'func_closure') and f.func_closure:
|
||||||
|
fc = filter(lambda c: hasattr(c.cell_contents, '__call__'), f.func_closure)
|
||||||
|
f = fc[len(fc)-1].cell_contents
|
||||||
|
info = f.func_code.co_filename
|
||||||
|
info = u'%s:%s' % (info, f.func_code.co_firstlineno)
|
||||||
|
return info, trim(inspect.getsource(f))
|
||||||
|
|
||||||
|
def register(self, method, action=None, cache=True, version=None):
|
||||||
|
if not action:
|
||||||
|
action = method.func_name
|
||||||
|
if version:
|
||||||
|
if not version in self.versions:
|
||||||
|
self.versions[version] = {}
|
||||||
|
self.versions[version][action] = method
|
||||||
|
else:
|
||||||
|
self[action] = method
|
||||||
|
self.properties[action] = {'cache': cache}
|
||||||
|
|
||||||
|
def unregister(self, action):
|
||||||
|
if action in self:
|
||||||
|
del self[action]
|
||||||
|
|
||||||
|
actions = ApiActions()
|
||||||
|
|
||||||
|
def error(request):
|
||||||
|
'''
|
||||||
|
this action is used to test api error codes, it should return a 503 error
|
||||||
|
'''
|
||||||
|
success = error_is_success
|
||||||
|
return render_to_json_response({})
|
||||||
|
actions.register(error)
|
|
@ -0,0 +1,27 @@
|
||||||
|
from sqlalchemy.ext.mutable import Mutable
|
||||||
|
|
||||||
|
class MutableDict(Mutable, dict):
|
||||||
|
@classmethod
|
||||||
|
def coerce(cls, key, value):
|
||||||
|
"Convert plain dictionaries to MutableDict."
|
||||||
|
|
||||||
|
if not isinstance(value, MutableDict):
|
||||||
|
if isinstance(value, dict):
|
||||||
|
return MutableDict(value)
|
||||||
|
|
||||||
|
# this call will raise ValueError
|
||||||
|
return Mutable.coerce(key, value)
|
||||||
|
else:
|
||||||
|
return value
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
"Detect dictionary set events and emit change events."
|
||||||
|
|
||||||
|
dict.__setitem__(self, key, value)
|
||||||
|
self.changed()
|
||||||
|
|
||||||
|
def __delitem__(self, key):
|
||||||
|
"Detect dictionary del events and emit change events."
|
||||||
|
|
||||||
|
dict.__delitem__(self, key)
|
||||||
|
self.changed()
|
|
@ -0,0 +1,245 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vi:si:et:sw=4:sts=4:ts=4
|
||||||
|
|
||||||
|
from sqlalchemy.sql.expression import and_, not_, or_, ClauseElement
|
||||||
|
from datetime import datetime
|
||||||
|
import unicodedata
|
||||||
|
from sqlalchemy.sql import operators, extract
|
||||||
|
|
||||||
|
import utils
|
||||||
|
import settings
|
||||||
|
|
||||||
|
def get_operator(op, type='str'):
|
||||||
|
return {
|
||||||
|
'str': {
|
||||||
|
'==': operators.ilike_op,
|
||||||
|
'>': operators.gt,
|
||||||
|
'>=': operators.ge,
|
||||||
|
'<': operators.lt,
|
||||||
|
'<=': operators.le,
|
||||||
|
'^': operators.startswith_op,
|
||||||
|
'$': operators.endswith_op,
|
||||||
|
},
|
||||||
|
'int': {
|
||||||
|
'==': operators.eq,
|
||||||
|
'>': operators.gt,
|
||||||
|
'>=': operators.ge,
|
||||||
|
'<': operators.lt,
|
||||||
|
'<=': operators.le,
|
||||||
|
}
|
||||||
|
}[type].get(op, {
|
||||||
|
'str': operators.contains_op,
|
||||||
|
'int': operators.eq
|
||||||
|
}[type])
|
||||||
|
|
||||||
|
|
||||||
|
class Parser(object):
|
||||||
|
|
||||||
|
def __init__(self, model):
|
||||||
|
self._model = model
|
||||||
|
self._find = model.find.mapper.class_
|
||||||
|
self._user = model.users.mapper.class_
|
||||||
|
self._list = model.lists.mapper.class_
|
||||||
|
self.item_keys = model.item_keys
|
||||||
|
self.filter_keys = model.filter_keys
|
||||||
|
|
||||||
|
def parse_condition(self, condition):
|
||||||
|
'''
|
||||||
|
condition: {
|
||||||
|
value: "war"
|
||||||
|
}
|
||||||
|
or
|
||||||
|
condition: {
|
||||||
|
key: "year",
|
||||||
|
value: [1970, 1980],
|
||||||
|
operator: "="
|
||||||
|
}
|
||||||
|
...
|
||||||
|
'''
|
||||||
|
k = condition.get('key', '*')
|
||||||
|
if not k:
|
||||||
|
k = '*'
|
||||||
|
v = condition['value']
|
||||||
|
op = condition.get('operator')
|
||||||
|
if not op:
|
||||||
|
op = '='
|
||||||
|
if op.startswith('!'):
|
||||||
|
op = op[1:]
|
||||||
|
exclude = True
|
||||||
|
else:
|
||||||
|
exclude = False
|
||||||
|
|
||||||
|
key_type = (utils.get_by_id(self.item_keys, k) or {'type': 'string'}).get('type')
|
||||||
|
if isinstance(key_type, list):
|
||||||
|
key_type = key_type[0]
|
||||||
|
if k == 'list':
|
||||||
|
key_type = ''
|
||||||
|
|
||||||
|
|
||||||
|
if (not exclude and op == '=' or op in ('$', '^')) and v == '':
|
||||||
|
return None
|
||||||
|
elif k == 'resolution':
|
||||||
|
q = self.parse_condition({'key': 'width', 'value': v[0], 'operator': op}) \
|
||||||
|
& self.parse_condition({'key': 'height', 'value': v[1], 'operator': op})
|
||||||
|
if exclude:
|
||||||
|
q = ~q
|
||||||
|
return q
|
||||||
|
elif isinstance(v, list) and len(v) == 2 and op == '=':
|
||||||
|
q = self.parse_condition({'key': k, 'value': v[0], 'operator': '>='}) \
|
||||||
|
& self.parse_condition({'key': k, 'value': v[1], 'operator': '<'})
|
||||||
|
if exclude:
|
||||||
|
q = ~q
|
||||||
|
return q
|
||||||
|
elif key_type == 'boolean':
|
||||||
|
q = getattr(self._model, 'find_%s' % k) == v
|
||||||
|
if exclude:
|
||||||
|
q = ~q
|
||||||
|
return q
|
||||||
|
elif key_type in ("string", "text"):
|
||||||
|
if isinstance(v, unicode):
|
||||||
|
v = unicodedata.normalize('NFKD', v).lower()
|
||||||
|
q = get_operator(op)(self._find.value, v.lower())
|
||||||
|
if k != '*':
|
||||||
|
q &= (self._find.key == k)
|
||||||
|
if exclude:
|
||||||
|
q = ~q
|
||||||
|
return q
|
||||||
|
elif k == 'list':
|
||||||
|
'''
|
||||||
|
q = Q(id=0)
|
||||||
|
l = v.split(":")
|
||||||
|
if len(l) == 1:
|
||||||
|
vqs = Volume.objects.filter(name=v, user=user)
|
||||||
|
if vqs.count() == 1:
|
||||||
|
v = vqs[0]
|
||||||
|
q = Q(files__instances__volume__id=v.id)
|
||||||
|
elif len(l) >= 2:
|
||||||
|
l = (l[0], ":".join(l[1:]))
|
||||||
|
lqs = list(List.objects.filter(name=l[1], user__username=l[0]))
|
||||||
|
if len(lqs) == 1 and lqs[0].accessible(user):
|
||||||
|
l = lqs[0]
|
||||||
|
if l.query.get('static', False) == False:
|
||||||
|
data = l.query
|
||||||
|
q = self.parse_conditions(data.get('conditions', []),
|
||||||
|
data.get('operator', '&'),
|
||||||
|
user, l.user)
|
||||||
|
else:
|
||||||
|
q = Q(id__in=l.items.all())
|
||||||
|
if exclude:
|
||||||
|
q = ~q
|
||||||
|
else:
|
||||||
|
q = Q(id=0)
|
||||||
|
'''
|
||||||
|
l = v.split(":")
|
||||||
|
nickname = l[0]
|
||||||
|
name = ':'.join(l[1:])
|
||||||
|
if nickname:
|
||||||
|
p = self._user.query.filter_by(nickname=nickname).first()
|
||||||
|
v = '%s:%s' % (p.id, name)
|
||||||
|
else:
|
||||||
|
p = self._user.query.filter_by(id=settings.USER_ID).first()
|
||||||
|
v = ':%s' % name
|
||||||
|
#print 'get list:', p.id, name, l, v
|
||||||
|
if name:
|
||||||
|
l = self._list.query.filter_by(user_id=p.id, name=name).first()
|
||||||
|
else:
|
||||||
|
l = None
|
||||||
|
if l and l._query:
|
||||||
|
data = l._query
|
||||||
|
q = self.parse_conditions(data.get('conditions', []),
|
||||||
|
data.get('operator', '&'))
|
||||||
|
else:
|
||||||
|
q = (self._find.key == 'list') & (self._find.value == v)
|
||||||
|
return q
|
||||||
|
elif key_type == 'date':
|
||||||
|
def parse_date(d):
|
||||||
|
while len(d) < 3:
|
||||||
|
d.append(1)
|
||||||
|
return datetime(*[int(i) for i in d])
|
||||||
|
#using sort here since find only contains strings
|
||||||
|
v = parse_date(v.split('-'))
|
||||||
|
vk = getattr(self._model, 'sort_%s' % k)
|
||||||
|
q = get_operator(op, 'int')(vk, v)
|
||||||
|
if exclude:
|
||||||
|
q = ~q
|
||||||
|
return q
|
||||||
|
else: #integer, float, time
|
||||||
|
q = get_operator(op, 'int')(getattr(self._model, 'sort_%s'%k), v)
|
||||||
|
if exclude:
|
||||||
|
q = ~q
|
||||||
|
return q
|
||||||
|
|
||||||
|
def parse_conditions(self, conditions, operator):
|
||||||
|
'''
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
value: "war"
|
||||||
|
}
|
||||||
|
{
|
||||||
|
key: "year",
|
||||||
|
value: "1970-1980,
|
||||||
|
operator: "!="
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "country",
|
||||||
|
value: "f",
|
||||||
|
operator: "^"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
operator: "&"
|
||||||
|
'''
|
||||||
|
conn = []
|
||||||
|
for condition in conditions:
|
||||||
|
if 'conditions' in condition:
|
||||||
|
q = self.parse_conditions(condition['conditions'],
|
||||||
|
condition.get('operator', '&'))
|
||||||
|
else:
|
||||||
|
q = self.parse_condition(condition)
|
||||||
|
if isinstance(q, list):
|
||||||
|
conn += q
|
||||||
|
else:
|
||||||
|
conn.append(q)
|
||||||
|
conn = [q for q in conn if not isinstance(q, None.__class__)]
|
||||||
|
if conn:
|
||||||
|
if operator == '|':
|
||||||
|
q = conn[0]
|
||||||
|
for c in conn[1:]:
|
||||||
|
q = q | c
|
||||||
|
q = [q]
|
||||||
|
else:
|
||||||
|
q = conn
|
||||||
|
return q
|
||||||
|
return []
|
||||||
|
|
||||||
|
def find(self, data):
|
||||||
|
'''
|
||||||
|
query: {
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
value: "war"
|
||||||
|
}
|
||||||
|
{
|
||||||
|
key: "year",
|
||||||
|
value: "1970-1980,
|
||||||
|
operator: "!="
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "country",
|
||||||
|
value: "f",
|
||||||
|
operator: "^"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
operator: "&"
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
|
||||||
|
#join query with operator
|
||||||
|
qs = self._model.query
|
||||||
|
#only include items that have hard metadata
|
||||||
|
conditions = self.parse_conditions(data.get('query', {}).get('conditions', []),
|
||||||
|
data.get('query', {}).get('operator', '&'))
|
||||||
|
for c in conditions:
|
||||||
|
qs = qs.join(self._find).filter(c)
|
||||||
|
qs = qs.group_by(self._model.id)
|
||||||
|
return qs
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
from functools import wraps
|
||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
|
||||||
|
from flask import Response
|
||||||
|
|
||||||
|
def json_response(data=None, status=200, text='ok'):
|
||||||
|
if not data:
|
||||||
|
data = {}
|
||||||
|
return {'status': {'code': status, 'text': text}, 'data': data}
|
||||||
|
|
||||||
|
def _to_json(python_object):
|
||||||
|
if isinstance(python_object, datetime.datetime):
|
||||||
|
if python_object.year < 1900:
|
||||||
|
tt = python_object.timetuple()
|
||||||
|
return '%d-%02d-%02dT%02d:%02d%02dZ' % tuple(list(tt)[:6])
|
||||||
|
return python_object.strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||||
|
raise TypeError(u'%s %s is not JSON serializable' % (repr(python_object), type(python_object)))
|
||||||
|
|
||||||
|
def json_dumps(obj):
|
||||||
|
indent = 2
|
||||||
|
return json.dumps(obj, indent=indent, default=_to_json, ensure_ascii=False).encode('utf-8')
|
||||||
|
|
||||||
|
def render_to_json_response(obj, content_type="text/json", status=200):
|
||||||
|
resp = Response(json_dumps(obj), status=status, content_type=content_type)
|
||||||
|
return resp
|
||||||
|
|
||||||
|
def returns_json(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
r = f(*args, **kwargs)
|
||||||
|
return render_to_json_response(json_response(r))
|
||||||
|
return decorated_function
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
|
||||||
|
def get_by_key(objects, key, value):
|
||||||
|
obj = filter(lambda o: o.get(key) == value, objects)
|
||||||
|
return obj and obj[0] or None
|
||||||
|
|
||||||
|
def get_by_id(objects, id):
|
||||||
|
return get_by_key(objects, 'id', id)
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
|
||||||
|
class pdict(dict):
|
||||||
|
def __init__(self, path, defaults=None):
|
||||||
|
self._path = None
|
||||||
|
self._defaults = defaults
|
||||||
|
if os.path.exists(path):
|
||||||
|
with open(path) as fd:
|
||||||
|
_data = json.load(fd)
|
||||||
|
for key in _data:
|
||||||
|
self[key] = _data[key]
|
||||||
|
self._path = path
|
||||||
|
|
||||||
|
def _save(self):
|
||||||
|
if self._path:
|
||||||
|
with open(self._path, 'w') as fd:
|
||||||
|
json.dump(self, fd, indent=1)
|
||||||
|
|
||||||
|
def get(self, key, default=None):
|
||||||
|
if default == None and self._defaults:
|
||||||
|
default = self._defaults.get(key)
|
||||||
|
return dict.get(self, key, default)
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
if key not in self and self._defaults and key in self._defaults:
|
||||||
|
return self._defaults[key]
|
||||||
|
return dict.__getitem__(self, key)
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
dict.__setitem__(self, key, value)
|
||||||
|
self._save()
|
||||||
|
|
||||||
|
def __delitem__(self, key):
|
||||||
|
dict.__delitem__(self, key)
|
||||||
|
self._save()
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vi:si:et:sw=4:sts=4:ts=4
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from tornado.web import StaticFileHandler, Application, FallbackHandler
|
||||||
|
from tornado.wsgi import WSGIContainer
|
||||||
|
from tornado.httpserver import HTTPServer
|
||||||
|
from tornado.ioloop import IOLoop
|
||||||
|
|
||||||
|
from app import app
|
||||||
|
import settings
|
||||||
|
import websocket
|
||||||
|
|
||||||
|
import state
|
||||||
|
import node.server
|
||||||
|
|
||||||
|
def run():
|
||||||
|
root_dir = os.path.normpath(os.path.join(os.path.abspath(os.path.dirname(__file__)), '..'))
|
||||||
|
PID = sys.argv[2] if len(sys.argv) > 2 else None
|
||||||
|
|
||||||
|
state.main = IOLoop.instance()
|
||||||
|
|
||||||
|
static_path = os.path.join(root_dir, 'static')
|
||||||
|
|
||||||
|
options = {
|
||||||
|
'debug': not PID
|
||||||
|
}
|
||||||
|
|
||||||
|
tr = WSGIContainer(app)
|
||||||
|
|
||||||
|
handlers = [
|
||||||
|
(r'/(favicon.ico)', StaticFileHandler, {'path': static_path}),
|
||||||
|
(r'/static/(.*)', StaticFileHandler, {'path': static_path}),
|
||||||
|
(r'/ws', websocket.Handler),
|
||||||
|
(r".*", FallbackHandler, dict(fallback=tr)),
|
||||||
|
]
|
||||||
|
|
||||||
|
http_server = HTTPServer(Application(handlers, **options))
|
||||||
|
|
||||||
|
http_server.listen(settings.server['port'], settings.server['address'])
|
||||||
|
if PID:
|
||||||
|
with open(PID, 'w') as pid:
|
||||||
|
pid.write('%s' % os.getpid())
|
||||||
|
|
||||||
|
def start_node():
|
||||||
|
import user
|
||||||
|
import downloads
|
||||||
|
import nodes
|
||||||
|
state.node = node.server.start(app)
|
||||||
|
state.nodes = nodes.Nodes(app)
|
||||||
|
state.downloads = downloads.Downloads(app)
|
||||||
|
def add_users(app):
|
||||||
|
with app.app_context():
|
||||||
|
for p in user.models.User.query.filter_by(peered=True):
|
||||||
|
state.nodes.queue('add', p.id)
|
||||||
|
state.main.add_callback(add_users, app)
|
||||||
|
state.main.add_callback(start_node)
|
||||||
|
state.main.start()
|
|
@ -0,0 +1,71 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vi:si:et:sw=4:sts=4:ts=4
|
||||||
|
|
||||||
|
from flask.ext.sqlalchemy import SQLAlchemy
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import ed25519
|
||||||
|
|
||||||
|
from pdict import pdict
|
||||||
|
|
||||||
|
base_dir = os.path.normpath(os.path.join(os.path.abspath(os.path.dirname(__file__)), '..'))
|
||||||
|
static_path = os.path.join(base_dir, 'static')
|
||||||
|
updates_path = os.path.join(base_dir, 'updates')
|
||||||
|
|
||||||
|
oml_config_path = os.path.join(base_dir, 'config.json')
|
||||||
|
|
||||||
|
config_dir = os.path.normpath(os.path.join(base_dir, '..', 'config'))
|
||||||
|
if not os.path.exists(config_dir):
|
||||||
|
os.makedirs(config_dir)
|
||||||
|
|
||||||
|
db_path = os.path.join(config_dir, 'openmedialibrary.db')
|
||||||
|
covers_db_path = os.path.join(config_dir, 'covers.db')
|
||||||
|
key_path = os.path.join(config_dir, 'node.key')
|
||||||
|
|
||||||
|
db = SQLAlchemy()
|
||||||
|
|
||||||
|
if os.path.exists(oml_config_path):
|
||||||
|
with open(oml_config_path) as fd:
|
||||||
|
config = json.load(fd)
|
||||||
|
else:
|
||||||
|
config = {}
|
||||||
|
|
||||||
|
preferences = pdict(os.path.join(config_dir, 'preferences.json'), config['user']['preferences'])
|
||||||
|
ui = pdict(os.path.join(config_dir, 'ui.json'), config['user']['ui'])
|
||||||
|
|
||||||
|
server = pdict(os.path.join(config_dir, 'server.json'))
|
||||||
|
server_defaults = {
|
||||||
|
'port': 9842,
|
||||||
|
'address': '127.0.0.1',
|
||||||
|
'node_port': 9851,
|
||||||
|
'node_address': '::',
|
||||||
|
'extract_text': True,
|
||||||
|
'directory_service': 'http://[2a01:4f8:120:3201::3]:25519',
|
||||||
|
'lookup_service': 'http://data.openmedialibrary.com',
|
||||||
|
}
|
||||||
|
|
||||||
|
for key in server_defaults:
|
||||||
|
if key not in server:
|
||||||
|
server[key] = server_defaults[key]
|
||||||
|
|
||||||
|
release = pdict(os.path.join(config_dir, 'release.json'))
|
||||||
|
|
||||||
|
if os.path.exists(key_path):
|
||||||
|
with open(key_path) as fd:
|
||||||
|
sk = ed25519.SigningKey(fd.read())
|
||||||
|
vk = sk.get_verifying_key()
|
||||||
|
else:
|
||||||
|
sk, vk = ed25519.create_keypair()
|
||||||
|
with open(key_path, 'w') as fd:
|
||||||
|
os.chmod(key_path, 0600)
|
||||||
|
fd.write(sk.to_bytes())
|
||||||
|
os.chmod(key_path, 0400)
|
||||||
|
|
||||||
|
USER_ID = vk.to_ascii(encoding='base64')
|
||||||
|
|
||||||
|
if 'modules' in release and 'openmedialibrary' in release['modules']:
|
||||||
|
VERSION = release['modules']['openmedialibrary']['version']
|
||||||
|
else:
|
||||||
|
VERSION = 'git'
|
||||||
|
|
||||||
|
USER_AGENT = 'OpenMediaLibrary/%s' % VERSION
|
|
@ -0,0 +1,14 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vi:si:et:sw=4:sts=4:ts=4
|
||||||
|
|
||||||
|
import settings
|
||||||
|
from user.models import List, User
|
||||||
|
|
||||||
|
def create_default_lists(user_id=None):
|
||||||
|
user_id = user_id or settings.USER_ID
|
||||||
|
user = User.get_or_create(user_id)
|
||||||
|
for list in settings.config['lists']:
|
||||||
|
l = List.get(user_id, list['title'])
|
||||||
|
if not l:
|
||||||
|
l = List.create(user_id, list['title'], list.get('query'))
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
websockets = []
|
||||||
|
nodes = False
|
||||||
|
main = None
|
||||||
|
online = False
|
||||||
|
|
||||||
|
def user():
|
||||||
|
import settings
|
||||||
|
import user.models
|
||||||
|
return user.models.User.get_or_create(settings.USER_ID)
|
|
@ -0,0 +1,97 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vi:si:et:sw=4:sts=4:ts=4
|
||||||
|
|
||||||
|
|
||||||
|
from __future__ import division
|
||||||
|
#https://github.com/hiroakis/tornado-websocket-example/blob/master/app.py
|
||||||
|
#http://stackoverflow.com/questions/5892895/tornado-websocket-question
|
||||||
|
|
||||||
|
#possibly get https://github.com/methane/wsaccel
|
||||||
|
|
||||||
|
|
||||||
|
#possibly run the full django app throw tornado instead of gunicorn
|
||||||
|
#https://github.com/bdarnell/django-tornado-demo/blob/master/testsite/tornado_main.py
|
||||||
|
#http://stackoverflow.com/questions/7190431/tornado-with-django
|
||||||
|
#http://www.tornadoweb.org/en/stable/wsgi.html
|
||||||
|
|
||||||
|
|
||||||
|
from tornado.httpserver import HTTPServer
|
||||||
|
from tornado.ioloop import IOLoop
|
||||||
|
from tornado.web import Application
|
||||||
|
from tornado.websocket import WebSocketHandler
|
||||||
|
from Queue import Queue
|
||||||
|
import urllib2
|
||||||
|
import os
|
||||||
|
from contextlib import closing
|
||||||
|
import json
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
|
|
||||||
|
class Background:
|
||||||
|
|
||||||
|
def __init__(self, handler):
|
||||||
|
self.handler = handler
|
||||||
|
self.q = Queue()
|
||||||
|
|
||||||
|
def worker(self):
|
||||||
|
while True:
|
||||||
|
message = self.q.get()
|
||||||
|
action, data = json.loads(message)
|
||||||
|
if action == 'get':
|
||||||
|
if 'url' in data and data['url'].startswith('http'):
|
||||||
|
self.download(data['url'], '/tmp/test.data')
|
||||||
|
elif action == 'update':
|
||||||
|
self.post({'error': 'not implemented'})
|
||||||
|
else:
|
||||||
|
self.post({'error': 'unknown action'})
|
||||||
|
self.q.task_done()
|
||||||
|
|
||||||
|
def join(self):
|
||||||
|
self.q.join()
|
||||||
|
|
||||||
|
def put(self, data):
|
||||||
|
self.q.put(data)
|
||||||
|
|
||||||
|
def post(self, data):
|
||||||
|
if not isinstance(data, basestring):
|
||||||
|
data = json.dumps(data)
|
||||||
|
main.add_callback(lambda: self.handler.write_message(data))
|
||||||
|
|
||||||
|
def download(self, url, filename):
|
||||||
|
dirname = os.path.dirname(filename)
|
||||||
|
if dirname and not os.path.exists(dirname):
|
||||||
|
os.makedirs(dirname)
|
||||||
|
with open(filename, 'w') as f:
|
||||||
|
with closing(urllib2.urlopen(url)) as u:
|
||||||
|
size = int(u.headers.get('content-length', 0))
|
||||||
|
done = 0
|
||||||
|
chunk_size = max(min(1024*1024, int(size/100)), 4096)
|
||||||
|
print 'chunksize', chunk_size
|
||||||
|
for data in iter(lambda: u.read(chunk_size), ''):
|
||||||
|
f.write(data)
|
||||||
|
done += len(data)
|
||||||
|
if size:
|
||||||
|
percent = done/size
|
||||||
|
self.post({'url': url, 'size': size, 'done': done, 'percent': percent})
|
||||||
|
|
||||||
|
class Handler(WebSocketHandler):
|
||||||
|
def open(self):
|
||||||
|
print "New connection opened."
|
||||||
|
self.background = Background(self)
|
||||||
|
self.t = Thread(target=self.background.worker)
|
||||||
|
self.t.daemon = True
|
||||||
|
self.t.start()
|
||||||
|
|
||||||
|
#websocket calls
|
||||||
|
def on_message(self, message):
|
||||||
|
self.background.put(message)
|
||||||
|
|
||||||
|
def on_close(self):
|
||||||
|
print "Connection closed."
|
||||||
|
self.background.join()
|
||||||
|
|
||||||
|
print "Server started."
|
||||||
|
HTTPServer(Application([("/", Handler)])).listen(28161)
|
||||||
|
main = IOLoop.instance()
|
||||||
|
main.start()
|
|
@ -0,0 +1,216 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vi:si:et:sw=4:sts=4:ts=4
|
||||||
|
|
||||||
|
import os
|
||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
|
from flask import json
|
||||||
|
from oxflask.api import actions
|
||||||
|
from oxflask.shortcuts import returns_json
|
||||||
|
|
||||||
|
import models
|
||||||
|
from item.models import Item
|
||||||
|
|
||||||
|
from utils import get_position_by_id
|
||||||
|
|
||||||
|
import settings
|
||||||
|
import state
|
||||||
|
from changelog import Changelog
|
||||||
|
|
||||||
|
@returns_json
|
||||||
|
def init(request):
|
||||||
|
'''
|
||||||
|
this is an init request to test stuff
|
||||||
|
'''
|
||||||
|
#print 'init', request
|
||||||
|
response = {}
|
||||||
|
if os.path.exists(settings.oml_config_path):
|
||||||
|
with open(settings.oml_config_path) as fd:
|
||||||
|
config = json.load(fd)
|
||||||
|
else:
|
||||||
|
config = {}
|
||||||
|
response['config'] = config
|
||||||
|
response['user'] = deepcopy(config['user'])
|
||||||
|
if settings.preferences:
|
||||||
|
response['user']['preferences'] = settings.preferences
|
||||||
|
response['user']['id'] = settings.USER_ID
|
||||||
|
response['user']['online'] = state.online
|
||||||
|
if settings.ui:
|
||||||
|
response['user']['ui'] = settings.ui
|
||||||
|
return response
|
||||||
|
actions.register(init)
|
||||||
|
|
||||||
|
def update_dict(root, data):
|
||||||
|
for key in data:
|
||||||
|
keys = map(lambda p: p.replace('\0', '\\.'), key.replace('\\.', '\0').split('.'))
|
||||||
|
value = data[key]
|
||||||
|
p = root
|
||||||
|
while len(keys)>1:
|
||||||
|
key = keys.pop(0)
|
||||||
|
if isinstance(p, list):
|
||||||
|
p = p[get_position_by_id(p, key)]
|
||||||
|
else:
|
||||||
|
if key not in p:
|
||||||
|
p[key] = {}
|
||||||
|
p = p[key]
|
||||||
|
if value == None and keys[0] in p:
|
||||||
|
del p[keys[0]]
|
||||||
|
else:
|
||||||
|
p[keys[0]] = value
|
||||||
|
|
||||||
|
@returns_json
|
||||||
|
def setPreferences(request):
|
||||||
|
data = json.loads(request.form['data']) if 'data' in request.form else {}
|
||||||
|
update_dict(settings.preferences, data)
|
||||||
|
return settings.preferences
|
||||||
|
actions.register(setPreferences)
|
||||||
|
|
||||||
|
@returns_json
|
||||||
|
def setUI(request):
|
||||||
|
data = json.loads(request.form['data']) if 'data' in request.form else {}
|
||||||
|
update_dict(settings.ui, data)
|
||||||
|
return settings.ui
|
||||||
|
actions.register(setUI)
|
||||||
|
|
||||||
|
@returns_json
|
||||||
|
def getUsers(request):
|
||||||
|
users = []
|
||||||
|
for u in models.User.query.filter(models.User.id!=settings.USER_ID).all():
|
||||||
|
users.append(u.json())
|
||||||
|
return {
|
||||||
|
"users": users
|
||||||
|
}
|
||||||
|
actions.register(getUsers)
|
||||||
|
|
||||||
|
@returns_json
|
||||||
|
def getLists(request):
|
||||||
|
lists = {}
|
||||||
|
for u in models.User.query.filter((models.User.peered==True)|(models.User.id==settings.USER_ID)):
|
||||||
|
lists[u.id] = u.lists_json()
|
||||||
|
return {
|
||||||
|
'lists': lists
|
||||||
|
}
|
||||||
|
actions.register(getLists)
|
||||||
|
|
||||||
|
@returns_json
|
||||||
|
def addList(request):
|
||||||
|
data = json.loads(request.form['data']) if 'data' in request.form else {}
|
||||||
|
user_id = settings.USER_ID
|
||||||
|
l = models.List.get(user_id, data['name'])
|
||||||
|
if not l:
|
||||||
|
l = models.List.create(user_id, data['name'], data.get('query'))
|
||||||
|
if 'items' in data:
|
||||||
|
l.add_items(data['items'])
|
||||||
|
return l.json()
|
||||||
|
return {}
|
||||||
|
actions.register(addList, cache=False)
|
||||||
|
|
||||||
|
@returns_json
|
||||||
|
def removeList(request):
|
||||||
|
data = json.loads(request.form['data']) if 'data' in request.form else {}
|
||||||
|
l = models.List.get(data['id'])
|
||||||
|
if l:
|
||||||
|
l.remove()
|
||||||
|
return {}
|
||||||
|
actions.register(removeList, cache=False)
|
||||||
|
|
||||||
|
@returns_json
|
||||||
|
def editList(request):
|
||||||
|
data = json.loads(request.form['data']) if 'data' in request.form else {}
|
||||||
|
l = models.List.get_or_create(data['id'])
|
||||||
|
name = l.name
|
||||||
|
if 'name' in data:
|
||||||
|
l.name = data['name']
|
||||||
|
l.type = 'static'
|
||||||
|
if 'query' in data:
|
||||||
|
l._query = data['query']
|
||||||
|
l.type = 'smart'
|
||||||
|
if l.type == 'static' and name != l.name:
|
||||||
|
Changelog.record(state.user(), 'editlist', name, {'name': l.name})
|
||||||
|
l.save()
|
||||||
|
return {}
|
||||||
|
actions.register(editList, cache=False)
|
||||||
|
|
||||||
|
@returns_json
|
||||||
|
def addListItem(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()
|
||||||
|
return {}
|
||||||
|
actions.register(addListItem, cache=False)
|
||||||
|
|
||||||
|
@returns_json
|
||||||
|
def removeListItem(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()
|
||||||
|
return {}
|
||||||
|
actions.register(removeListItem, cache=False)
|
||||||
|
|
||||||
|
@returns_json
|
||||||
|
def sortLists(request):
|
||||||
|
data = json.loads(request.form['data']) if 'data' in request.form else {}
|
||||||
|
n = 0
|
||||||
|
print 'sortLists', data
|
||||||
|
for id in data['ids']:
|
||||||
|
l = models.List.get(id)
|
||||||
|
l.position = n
|
||||||
|
n += 1
|
||||||
|
models.db.session.add(l)
|
||||||
|
models.db.session.commit()
|
||||||
|
return {}
|
||||||
|
actions.register(sortLists, cache=False)
|
||||||
|
|
||||||
|
@returns_json
|
||||||
|
def editUser(request):
|
||||||
|
data = json.loads(request.form['data']) if 'data' in request.form else {}
|
||||||
|
if 'nickname' in data:
|
||||||
|
p = models.User.get_or_create(data['id'])
|
||||||
|
p.set_nickname(data['nickname'])
|
||||||
|
p.save()
|
||||||
|
return {}
|
||||||
|
actions.register(editUser, cache=False)
|
||||||
|
|
||||||
|
@returns_json
|
||||||
|
def requestPeering(request):
|
||||||
|
data = json.loads(request.form['data']) if 'data' in request.form else {}
|
||||||
|
p = models.User.get_or_create(data['id'])
|
||||||
|
state.nodes.queue('add', p.id)
|
||||||
|
state.nodes.queue(p.id, 'requestPeering', data.get('message', ''))
|
||||||
|
return {}
|
||||||
|
actions.register(requestPeering, cache=False)
|
||||||
|
|
||||||
|
@returns_json
|
||||||
|
def acceptPeering(request):
|
||||||
|
data = json.loads(request.form['data']) if 'data' in request.form else {}
|
||||||
|
p = models.User.get_or_create(data['id'])
|
||||||
|
state.nodes.queue('add', p.id)
|
||||||
|
state.nodes.queue(p.id, 'acceptPeering', data.get('message', ''))
|
||||||
|
return {}
|
||||||
|
actions.register(acceptPeering, cache=False)
|
||||||
|
|
||||||
|
@returns_json
|
||||||
|
def rejectPeering(request):
|
||||||
|
data = json.loads(request.form['data']) if 'data' in request.form else {}
|
||||||
|
p = models.User.get_or_create(data['id'])
|
||||||
|
state.nodes.queue('add', p.id)
|
||||||
|
state.nodes.queue(p.id, 'rejectPeering', data.get('message', ''))
|
||||||
|
return {}
|
||||||
|
actions.register(rejectPeering, cache=False)
|
||||||
|
|
||||||
|
@returns_json
|
||||||
|
def removePeering(request):
|
||||||
|
data = json.loads(request.form['data']) if 'data' in request.form else {}
|
||||||
|
p = models.User.get_or_create(data['id'])
|
||||||
|
state.nodes.queue('add', p.id)
|
||||||
|
state.nodes.queue(p.id, 'removePeering', data.get('message', ''))
|
||||||
|
return {}
|
||||||
|
actions.register(removePeering, cache=False)
|
|
@ -0,0 +1,213 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vi:si:et:sw=4:sts=4:ts=4
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
from oxflask.db import MutableDict
|
||||||
|
import oxflask.query
|
||||||
|
|
||||||
|
from changelog import Changelog
|
||||||
|
import settings
|
||||||
|
from settings import db
|
||||||
|
|
||||||
|
import state
|
||||||
|
|
||||||
|
class User(db.Model):
|
||||||
|
|
||||||
|
created = db.Column(db.DateTime())
|
||||||
|
modified = db.Column(db.DateTime())
|
||||||
|
id = db.Column(db.String(43), primary_key=True)
|
||||||
|
info = db.Column(MutableDict.as_mutable(db.PickleType(pickler=json)))
|
||||||
|
|
||||||
|
#nickname = db.Column(db.String(256), unique=True)
|
||||||
|
nickname = db.Column(db.String(256))
|
||||||
|
|
||||||
|
pending = db.Column(db.String(64)) # sent|received
|
||||||
|
peered = db.Column(db.Boolean())
|
||||||
|
online = db.Column(db.Boolean())
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return self.id
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get(cls, id):
|
||||||
|
return cls.query.filter_by(id=id).first()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_or_create(cls, id):
|
||||||
|
user = cls.get(id)
|
||||||
|
if not user:
|
||||||
|
user = cls(id=id, peered=False, online=False)
|
||||||
|
user.info = {}
|
||||||
|
user.save()
|
||||||
|
return user
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
db.session.add(self)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
def json(self):
|
||||||
|
j = {}
|
||||||
|
if self.info:
|
||||||
|
j.update(self.info)
|
||||||
|
j['id'] = self.id
|
||||||
|
if self.pending:
|
||||||
|
j['pending'] = self.pending
|
||||||
|
j['peered'] = self.peered
|
||||||
|
j['online'] = self.check_online()
|
||||||
|
j['nickname'] = self.nickname
|
||||||
|
return j
|
||||||
|
|
||||||
|
def check_online(self):
|
||||||
|
return state.nodes.check_online(self.id)
|
||||||
|
|
||||||
|
def lists_json(self):
|
||||||
|
return [l.json() for l in self.lists.order_by('position')]
|
||||||
|
|
||||||
|
def update_peering(self, peered, username=None):
|
||||||
|
if peered:
|
||||||
|
self.pending = ''
|
||||||
|
self.peered = True
|
||||||
|
if username:
|
||||||
|
self.info['username'] = username
|
||||||
|
|
||||||
|
self.set_nickname(self.info.get('username', 'anonymous'))
|
||||||
|
else:
|
||||||
|
self.peered = False
|
||||||
|
self.nickname = None
|
||||||
|
for i in self.items:
|
||||||
|
i.users.remove(self)
|
||||||
|
if not i.users:
|
||||||
|
print 'last user, remove'
|
||||||
|
db.session.delete(i)
|
||||||
|
else:
|
||||||
|
i.update_lists()
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def set_nickname(self, nickname):
|
||||||
|
username = nickname
|
||||||
|
n = 2
|
||||||
|
while self.query.filter_by(nickname=nickname).filter(User.id!=self.id).first():
|
||||||
|
nickname = '%s [%d]' % (username, n)
|
||||||
|
n += 1
|
||||||
|
self.nickname = nickname
|
||||||
|
|
||||||
|
list_items = db.Table('listitem',
|
||||||
|
db.Column('list_id', db.Integer(), db.ForeignKey('list.id')),
|
||||||
|
db.Column('item_id', db.String(32), db.ForeignKey('item.id'))
|
||||||
|
)
|
||||||
|
|
||||||
|
class List(db.Model):
|
||||||
|
id = db.Column(db.Integer(), primary_key=True)
|
||||||
|
name = db.Column(db.String())
|
||||||
|
position = db.Column(db.Integer())
|
||||||
|
|
||||||
|
type = db.Column(db.String(64))
|
||||||
|
_query = db.Column('query', MutableDict.as_mutable(db.PickleType(pickler=json)))
|
||||||
|
|
||||||
|
user_id = db.Column(db.String(43), db.ForeignKey('user.id'))
|
||||||
|
user = db.relationship('User', backref=db.backref('lists', lazy='dynamic'))
|
||||||
|
|
||||||
|
items = db.relationship('Item', secondary=list_items,
|
||||||
|
backref=db.backref('lists', lazy='dynamic'))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get(cls, user_id, name=None):
|
||||||
|
if not name:
|
||||||
|
user_id, name = cls.get_user_name(user_id)
|
||||||
|
return cls.query.filter_by(user_id=user_id, name=name).first()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_user_name(cls, user_id):
|
||||||
|
l = user_id.split(':')
|
||||||
|
nickname = l[0]
|
||||||
|
name = ':'.join(l[1:])
|
||||||
|
if nickname:
|
||||||
|
user = User.query.filter_by(nickname=nickname).first()
|
||||||
|
user_id = user.id
|
||||||
|
else:
|
||||||
|
user_id = settings.USER_ID
|
||||||
|
return user_id, name
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_or_create(cls, user_id, name=None):
|
||||||
|
if not name:
|
||||||
|
user_id, name = cls.get_user_name(user_id)
|
||||||
|
l = cls.get(user_id, name)
|
||||||
|
if not l:
|
||||||
|
l = cls(name=name, user_id=user_id)
|
||||||
|
db.session.add(l)
|
||||||
|
db.session.commit()
|
||||||
|
return l
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(cls, user_id, name, query=None):
|
||||||
|
l = cls(name=name, user_id=user_id)
|
||||||
|
l._query = query
|
||||||
|
l.type = 'smart' if l._query else 'static'
|
||||||
|
l.position = cls.query.filter_by(user_id=user_id).count()
|
||||||
|
if user_id == settings.USER_ID:
|
||||||
|
p = User.get(settings.USER_ID)
|
||||||
|
if not l._query:
|
||||||
|
Changelog.record(p, 'addlist', l.name)
|
||||||
|
db.session.add(l)
|
||||||
|
db.session.commit()
|
||||||
|
return l
|
||||||
|
|
||||||
|
def add_items(self, items):
|
||||||
|
from item.models import Item
|
||||||
|
for item_id in items:
|
||||||
|
i = Item.get(item_id)
|
||||||
|
self.items.add(i)
|
||||||
|
db.session.add(self)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
def remove_items(self, items):
|
||||||
|
from item.models import Item
|
||||||
|
for item_id in items:
|
||||||
|
i = Item.get(item_id)
|
||||||
|
self.items.remove(i)
|
||||||
|
db.session.add(self)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
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)
|
||||||
|
db.session.delete(self)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def public_id(self):
|
||||||
|
id = ''
|
||||||
|
if self.user_id != settings.USER_ID:
|
||||||
|
id += self.user_id
|
||||||
|
id = '%s:%s' % (id, self.name)
|
||||||
|
return id
|
||||||
|
|
||||||
|
def items_count(self):
|
||||||
|
from item.models import Item
|
||||||
|
if self._query:
|
||||||
|
data = self._query
|
||||||
|
return oxflask.query.Parser(Item).find({'query': data}).count()
|
||||||
|
else:
|
||||||
|
return len(self.items)
|
||||||
|
|
||||||
|
def json(self):
|
||||||
|
r = {
|
||||||
|
'id': self.public_id,
|
||||||
|
'name': self.name,
|
||||||
|
'index': self.position,
|
||||||
|
'items': self.items_count(),
|
||||||
|
'type': self.type
|
||||||
|
}
|
||||||
|
if self.type == 'smart':
|
||||||
|
r['query'] = self._query
|
||||||
|
return r
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
db.session.add(self)
|
||||||
|
db.session.commit()
|
|
@ -0,0 +1,95 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vi:si:et:sw=4:sts=4:ts=4
|
||||||
|
|
||||||
|
import Image
|
||||||
|
from StringIO import StringIO
|
||||||
|
import re
|
||||||
|
import stdnum.isbn
|
||||||
|
|
||||||
|
import ox
|
||||||
|
|
||||||
|
def valid_olid(id):
|
||||||
|
return id.startswith('OL') and id.endswith('M')
|
||||||
|
|
||||||
|
def get_positions(ids, pos):
|
||||||
|
'''
|
||||||
|
>>> get_positions([1,2,3,4], [2,4])
|
||||||
|
{2: 1, 4: 3}
|
||||||
|
'''
|
||||||
|
positions = {}
|
||||||
|
for i in pos:
|
||||||
|
try:
|
||||||
|
positions[i] = ids.index(i)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return positions
|
||||||
|
|
||||||
|
def get_by_key(objects, key, value):
|
||||||
|
obj = filter(lambda o: o.get(key) == value, objects)
|
||||||
|
return obj and obj[0] or None
|
||||||
|
|
||||||
|
def get_by_id(objects, id):
|
||||||
|
return get_by_key(objects, 'id', id)
|
||||||
|
|
||||||
|
def resize_image(data, width=None, size=None):
|
||||||
|
source = Image.open(StringIO(data)).convert('RGB')
|
||||||
|
source_width = source.size[0]
|
||||||
|
source_height = source.size[1]
|
||||||
|
if size:
|
||||||
|
if source_width > source_height:
|
||||||
|
width = size
|
||||||
|
height = int(width / (float(source_width) / source_height))
|
||||||
|
height = height - height % 2
|
||||||
|
else:
|
||||||
|
height = size
|
||||||
|
width = int(height * (float(source_width) / source_height))
|
||||||
|
width = width - width % 2
|
||||||
|
|
||||||
|
else:
|
||||||
|
height = int(width / (float(source_width) / source_height))
|
||||||
|
height = height - height % 2
|
||||||
|
|
||||||
|
width = max(width, 1)
|
||||||
|
height = max(height, 1)
|
||||||
|
|
||||||
|
if width < source_width:
|
||||||
|
resize_method = Image.ANTIALIAS
|
||||||
|
else:
|
||||||
|
resize_method = Image.BICUBIC
|
||||||
|
output = source.resize((width, height), resize_method)
|
||||||
|
o = StringIO()
|
||||||
|
output.save(o, format='jpeg')
|
||||||
|
data = o.getvalue()
|
||||||
|
o.close()
|
||||||
|
return data
|
||||||
|
|
||||||
|
def sort_title(title):
|
||||||
|
|
||||||
|
title = title.replace(u'Æ', 'Ae')
|
||||||
|
if isinstance(title, str):
|
||||||
|
title = unicode(title)
|
||||||
|
title = ox.sort_string(title)
|
||||||
|
|
||||||
|
#title
|
||||||
|
title = re.sub(u'[\'!¿¡,\.;\-"\:\*\[\]]', '', title)
|
||||||
|
return title.strip()
|
||||||
|
|
||||||
|
def normalize_isbn(value):
|
||||||
|
return ''.join([s for s in value if s.isdigit() or s == 'X'])
|
||||||
|
|
||||||
|
def find_isbns(text):
|
||||||
|
matches = re.compile('\d[\d\-X\ ]+').findall(text)
|
||||||
|
matches = [normalize_isbn(value) for value in matches]
|
||||||
|
return [isbn for isbn in matches if stdnum.isbn.is_valid(isbn)
|
||||||
|
and len(isbn) in (10, 13)
|
||||||
|
and isbn not in (
|
||||||
|
'0' * 10,
|
||||||
|
'0' * 13,
|
||||||
|
)]
|
||||||
|
|
||||||
|
def get_position_by_id(list, key):
|
||||||
|
for i in range(0, len(list)):
|
||||||
|
if list[i]['id'] == key:
|
||||||
|
return i
|
||||||
|
return -1
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vi:si:et:sw=4:sts=4:ts=4
|
||||||
|
from __future__ import division
|
||||||
|
|
||||||
|
from tornado.websocket import WebSocketHandler
|
||||||
|
from tornado.ioloop import IOLoop
|
||||||
|
from Queue import Queue
|
||||||
|
import urllib2
|
||||||
|
import os
|
||||||
|
from contextlib import closing
|
||||||
|
import json
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
|
from oxflask.shortcuts import json_dumps
|
||||||
|
|
||||||
|
import state
|
||||||
|
|
||||||
|
class Background:
|
||||||
|
|
||||||
|
def __init__(self, handler):
|
||||||
|
self.handler = handler
|
||||||
|
self.main = IOLoop.instance()
|
||||||
|
self.q = Queue()
|
||||||
|
self.connected = True
|
||||||
|
|
||||||
|
def worker(self):
|
||||||
|
while self.connected:
|
||||||
|
message = self.q.get()
|
||||||
|
action, data = json.loads(message)
|
||||||
|
print action
|
||||||
|
print data
|
||||||
|
import item.scan
|
||||||
|
if action == 'ping':
|
||||||
|
self.post(['pong', data])
|
||||||
|
elif action == 'import':
|
||||||
|
item.scan.run_import()
|
||||||
|
elif action == 'scan':
|
||||||
|
item.scan.run_scan()
|
||||||
|
elif action == 'update':
|
||||||
|
self.post(['error', {'error': 'not implemented'}])
|
||||||
|
else:
|
||||||
|
self.post(['error', {'error': 'unknown action'}])
|
||||||
|
self.q.task_done()
|
||||||
|
|
||||||
|
def join(self):
|
||||||
|
self.q.join()
|
||||||
|
|
||||||
|
def put(self, data):
|
||||||
|
self.q.put(data)
|
||||||
|
|
||||||
|
def post(self, data):
|
||||||
|
if not isinstance(data, basestring):
|
||||||
|
data = json_dumps(data)
|
||||||
|
self.main.add_callback(lambda: self.handler.write_message(data))
|
||||||
|
|
||||||
|
|
||||||
|
class Handler(WebSocketHandler):
|
||||||
|
|
||||||
|
def open(self):
|
||||||
|
print "New connection opened."
|
||||||
|
self.background = Background(self)
|
||||||
|
state.websockets.append(self.background)
|
||||||
|
self.t = Thread(target=self.background.worker)
|
||||||
|
self.t.daemon = True
|
||||||
|
self.t.start()
|
||||||
|
|
||||||
|
#websocket calls
|
||||||
|
def on_message(self, message):
|
||||||
|
self.background.put(message)
|
||||||
|
|
||||||
|
def on_close(self):
|
||||||
|
print "Connection closed."
|
||||||
|
state.websockets.remove(self.background)
|
||||||
|
self.background.connected = False
|
||||||
|
|
||||||
|
def trigger_event(event, data):
|
||||||
|
if len(state.websockets):
|
||||||
|
print 'trigger event', event, data, len(state.websockets)
|
||||||
|
for ws in state.websockets:
|
||||||
|
try:
|
||||||
|
ws.post([event, data])
|
||||||
|
except:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
print 'failed to send to ws', ws, event, data
|
||||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,318 @@
|
||||||
|
EPUBJS.Hooks.register("beforeChapterDisplay").endnotes = function(callback, renderer){
|
||||||
|
|
||||||
|
var notes = renderer.contents.querySelectorAll('a[href]'),
|
||||||
|
items = Array.prototype.slice.call(notes), //[].slice.call()
|
||||||
|
attr = "epub:type",
|
||||||
|
type = "noteref",
|
||||||
|
folder = EPUBJS.core.folder(location.pathname),
|
||||||
|
cssPath = (folder + EPUBJS.cssPath) || folder,
|
||||||
|
popups = {};
|
||||||
|
|
||||||
|
EPUBJS.core.addCss(cssPath + "popup.css", false, renderer.render.document.head);
|
||||||
|
|
||||||
|
|
||||||
|
items.forEach(function(item){
|
||||||
|
var epubType = item.getAttribute(attr),
|
||||||
|
href,
|
||||||
|
id,
|
||||||
|
el,
|
||||||
|
pop,
|
||||||
|
pos,
|
||||||
|
left,
|
||||||
|
top,
|
||||||
|
txt;
|
||||||
|
|
||||||
|
if(epubType != type) return;
|
||||||
|
|
||||||
|
href = item.getAttribute("href");
|
||||||
|
id = href.replace("#", '');
|
||||||
|
el = renderer.render.document.getElementById(id);
|
||||||
|
|
||||||
|
|
||||||
|
item.addEventListener("mouseover", showPop, false);
|
||||||
|
item.addEventListener("mouseout", hidePop, false);
|
||||||
|
|
||||||
|
function showPop(){
|
||||||
|
var poppos,
|
||||||
|
iheight = renderer.height,
|
||||||
|
iwidth = renderer.width,
|
||||||
|
tip,
|
||||||
|
pop,
|
||||||
|
maxHeight = 225,
|
||||||
|
itemRect;
|
||||||
|
|
||||||
|
if(!txt) {
|
||||||
|
pop = el.cloneNode(true);
|
||||||
|
txt = pop.querySelector("p");
|
||||||
|
}
|
||||||
|
|
||||||
|
// chapter.replaceLinks.bind(this) //TODO:Fred - update?
|
||||||
|
//-- create a popup with endnote inside of it
|
||||||
|
if(!popups[id]) {
|
||||||
|
popups[id] = document.createElement("div");
|
||||||
|
popups[id].setAttribute("class", "popup");
|
||||||
|
|
||||||
|
pop_content = document.createElement("div");
|
||||||
|
|
||||||
|
popups[id].appendChild(pop_content);
|
||||||
|
|
||||||
|
pop_content.appendChild(txt);
|
||||||
|
pop_content.setAttribute("class", "pop_content");
|
||||||
|
|
||||||
|
renderer.render.document.body.appendChild(popups[id]);
|
||||||
|
|
||||||
|
//-- TODO: will these leak memory? - Fred
|
||||||
|
popups[id].addEventListener("mouseover", onPop, false);
|
||||||
|
popups[id].addEventListener("mouseout", offPop, false);
|
||||||
|
|
||||||
|
//-- Add hide on page change
|
||||||
|
// chapter.book.listenUntil("book:pageChanged", "book:chapterDestroy", hidePop);
|
||||||
|
// chapter.book.listenUntil("book:pageChanged", "book:chapterDestroy", offPop);
|
||||||
|
renderer.on("renderer:pageChanged", hidePop, this);
|
||||||
|
renderer.on("renderer:pageChanged", offPop, this);
|
||||||
|
// chapter.book.on("renderer:chapterDestroy", hidePop, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
pop = popups[id];
|
||||||
|
|
||||||
|
|
||||||
|
//-- get location of item
|
||||||
|
itemRect = item.getBoundingClientRect();
|
||||||
|
left = itemRect.left;
|
||||||
|
top = itemRect.top;
|
||||||
|
|
||||||
|
//-- show the popup
|
||||||
|
pop.classList.add("show");
|
||||||
|
|
||||||
|
//-- locations of popup
|
||||||
|
popRect = pop.getBoundingClientRect();
|
||||||
|
|
||||||
|
//-- position the popup
|
||||||
|
pop.style.left = left - popRect.width / 2 + "px";
|
||||||
|
pop.style.top = top + "px";
|
||||||
|
|
||||||
|
|
||||||
|
//-- Adjust max height
|
||||||
|
if(maxHeight > iheight / 2.5) {
|
||||||
|
maxHeight = iheight / 2.5;
|
||||||
|
pop_content.style.maxHeight = maxHeight + "px";
|
||||||
|
}
|
||||||
|
|
||||||
|
//-- switch above / below
|
||||||
|
if(popRect.height + top >= iheight - 25) {
|
||||||
|
pop.style.top = top - popRect.height + "px";
|
||||||
|
pop.classList.add("above");
|
||||||
|
}else{
|
||||||
|
pop.classList.remove("above");
|
||||||
|
}
|
||||||
|
|
||||||
|
//-- switch left
|
||||||
|
if(left - popRect.width <= 0) {
|
||||||
|
pop.style.left = left + "px";
|
||||||
|
pop.classList.add("left");
|
||||||
|
}else{
|
||||||
|
pop.classList.remove("left");
|
||||||
|
}
|
||||||
|
|
||||||
|
//-- switch right
|
||||||
|
if(left + popRect.width / 2 >= iwidth) {
|
||||||
|
//-- TEMP MOVE: 300
|
||||||
|
pop.style.left = left - 300 + "px";
|
||||||
|
|
||||||
|
popRect = pop.getBoundingClientRect();
|
||||||
|
pop.style.left = left - popRect.width + "px";
|
||||||
|
//-- switch above / below again
|
||||||
|
if(popRect.height + top >= iheight - 25) {
|
||||||
|
pop.style.top = top - popRect.height + "px";
|
||||||
|
pop.classList.add("above");
|
||||||
|
}else{
|
||||||
|
pop.classList.remove("above");
|
||||||
|
}
|
||||||
|
|
||||||
|
pop.classList.add("right");
|
||||||
|
}else{
|
||||||
|
pop.classList.remove("right");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPop(){
|
||||||
|
popups[id].classList.add("on");
|
||||||
|
}
|
||||||
|
|
||||||
|
function offPop(){
|
||||||
|
popups[id].classList.remove("on");
|
||||||
|
}
|
||||||
|
|
||||||
|
function hidePop(){
|
||||||
|
setTimeout(function(){
|
||||||
|
popups[id].classList.remove("show");
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
if(callback) callback();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
EPUBJS.Hooks.register("beforeChapterDisplay").mathml = function(callback, renderer){
|
||||||
|
|
||||||
|
// check of currentChapter properties contains 'mathml'
|
||||||
|
if(renderer.currentChapter.manifestProperties.indexOf("mathml") !== -1 ){
|
||||||
|
|
||||||
|
// Assign callback to be inside iframe window
|
||||||
|
renderer.iframe.contentWindow.mathmlCallback = callback;
|
||||||
|
|
||||||
|
// add MathJax config script tag to the renderer body
|
||||||
|
var s = document.createElement("script");
|
||||||
|
s.type = 'text/x-mathjax-config';
|
||||||
|
s.innerHTML = '\
|
||||||
|
MathJax.Hub.Register.StartupHook("End",function () { \
|
||||||
|
window.mathmlCallback(); \
|
||||||
|
});\
|
||||||
|
MathJax.Hub.Config({jax: ["input/TeX","input/MathML","output/SVG"],extensions: ["tex2jax.js","mml2jax.js","MathEvents.js"],TeX: {extensions: ["noErrors.js","noUndefined.js","autoload-all.js"]},MathMenu: {showRenderer: false},menuSettings: {zoom: "Click"},messageStyle: "none"}); \
|
||||||
|
';
|
||||||
|
renderer.doc.body.appendChild(s);
|
||||||
|
// add MathJax.js to renderer head
|
||||||
|
EPUBJS.core.addScript("http://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML", null, renderer.doc.head);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
if(callback) callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EPUBJS.Hooks.register("beforeChapterDisplay").smartimages = function(callback, renderer){
|
||||||
|
var images = renderer.contents.querySelectorAll('img'),
|
||||||
|
items = Array.prototype.slice.call(images),
|
||||||
|
iheight = renderer.height,//chapter.bodyEl.clientHeight,//chapter.doc.body.getBoundingClientRect().height,
|
||||||
|
oheight;
|
||||||
|
|
||||||
|
if(renderer.layoutSettings.layout != "reflowable") {
|
||||||
|
callback();
|
||||||
|
return; //-- Only adjust images for reflowable text
|
||||||
|
}
|
||||||
|
|
||||||
|
items.forEach(function(item){
|
||||||
|
|
||||||
|
function size() {
|
||||||
|
var itemRect = item.getBoundingClientRect(),
|
||||||
|
rectHeight = itemRect.height,
|
||||||
|
top = itemRect.top,
|
||||||
|
oHeight = item.getAttribute('data-height'),
|
||||||
|
height = oHeight || rectHeight,
|
||||||
|
newHeight,
|
||||||
|
fontSize = Number(getComputedStyle(item, "").fontSize.match(/(\d*(\.\d*)?)px/)[1]),
|
||||||
|
fontAdjust = fontSize ? fontSize / 2 : 0;
|
||||||
|
|
||||||
|
iheight = renderer.contents.clientHeight;
|
||||||
|
if(top < 0) top = 0;
|
||||||
|
|
||||||
|
if(height + top >= iheight) {
|
||||||
|
|
||||||
|
if(top < iheight/2) {
|
||||||
|
// Remove top and half font-size from height to keep container from overflowing
|
||||||
|
newHeight = iheight - top - fontAdjust;
|
||||||
|
item.style.maxHeight = newHeight + "px";
|
||||||
|
item.style.width= "auto";
|
||||||
|
}else{
|
||||||
|
if(height > iheight) {
|
||||||
|
item.style.maxHeight = iheight + "px";
|
||||||
|
item.style.width= "auto";
|
||||||
|
itemRect = item.getBoundingClientRect();
|
||||||
|
height = itemRect.height;
|
||||||
|
}
|
||||||
|
item.style.display = "block";
|
||||||
|
item.style["WebkitColumnBreakBefore"] = "always";
|
||||||
|
item.style["breakBefore"] = "column";
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
item.setAttribute('data-height', newHeight);
|
||||||
|
|
||||||
|
}else{
|
||||||
|
item.style.removeProperty('max-height');
|
||||||
|
item.style.removeProperty('margin-top');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item.addEventListener('load', size, false);
|
||||||
|
|
||||||
|
renderer.on("renderer:resized", size);
|
||||||
|
|
||||||
|
renderer.on("renderer:chapterUnloaded", function(){
|
||||||
|
item.removeEventListener('load', size);
|
||||||
|
renderer.off("renderer:resized", size);
|
||||||
|
});
|
||||||
|
|
||||||
|
size();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
if(callback) callback();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
EPUBJS.Hooks.register("beforeChapterDisplay").transculsions = function(callback, renderer){
|
||||||
|
/*
|
||||||
|
<aside ref="http://www.youtube.com/embed/DUL6MBVKVLI?html5=1" transclusion="video" width="560" height="315">
|
||||||
|
<a href="http://www.youtube.com/embed/DUL6MBVKVLI"> Watch the National Geographic: The Last Roll of Kodachrome</a>
|
||||||
|
</aside>
|
||||||
|
*/
|
||||||
|
|
||||||
|
var trans = renderer.contents.querySelectorAll('[transclusion]'),
|
||||||
|
items = Array.prototype.slice.call(trans);
|
||||||
|
|
||||||
|
items.forEach(function(item){
|
||||||
|
var src = item.getAttribute("ref"),
|
||||||
|
iframe = document.createElement('iframe'),
|
||||||
|
orginal_width = item.getAttribute("width"),
|
||||||
|
orginal_height = item.getAttribute("height"),
|
||||||
|
parent = item.parentNode,
|
||||||
|
width = orginal_width,
|
||||||
|
height = orginal_height,
|
||||||
|
ratio;
|
||||||
|
|
||||||
|
|
||||||
|
function size() {
|
||||||
|
width = orginal_width;
|
||||||
|
height = orginal_height;
|
||||||
|
|
||||||
|
if(width > chapter.colWidth){
|
||||||
|
ratio = chapter.colWidth / width;
|
||||||
|
|
||||||
|
width = chapter.colWidth;
|
||||||
|
height = height * ratio;
|
||||||
|
}
|
||||||
|
|
||||||
|
iframe.width = width;
|
||||||
|
iframe.height = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
size();
|
||||||
|
|
||||||
|
//-- resize event
|
||||||
|
|
||||||
|
|
||||||
|
renderer.listenUntil("renderer:resized", "renderer:chapterUnloaded", size);
|
||||||
|
|
||||||
|
iframe.src = src;
|
||||||
|
|
||||||
|
//<iframe width="560" height="315" src="http://www.youtube.com/embed/DUL6MBVKVLI" frameborder="0" allowfullscreen="true"></iframe>
|
||||||
|
parent.replaceChild(iframe, item);
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if(callback) callback();
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//# sourceMappingURL=hooks.js.map
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"file": "hooks.js",
|
||||||
|
"sources": [
|
||||||
|
"hooks/default/endnotes.js",
|
||||||
|
"hooks/default/mathml.js",
|
||||||
|
"hooks/default/smartimages.js",
|
||||||
|
"hooks/default/transculsions.js"
|
||||||
|
],
|
||||||
|
"names": [],
|
||||||
|
"mappings": "AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AC9JA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACxBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACrEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA"
|
||||||
|
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,103 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html class="no-js">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||||
|
<title>epub.js</title>
|
||||||
|
<meta name="description" content="">
|
||||||
|
<meta name="viewport" content="width=device-width">
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
|
||||||
|
|
||||||
|
<!-- EPUBJS Renderer -->
|
||||||
|
<script src="/static/epub.js/epub.min.js"></script>
|
||||||
|
|
||||||
|
<style type="text/css">
|
||||||
|
|
||||||
|
body {
|
||||||
|
overflow: hidden;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#main {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#area {
|
||||||
|
position: absolute;
|
||||||
|
right: 44px;
|
||||||
|
left: 44px;
|
||||||
|
top: 32px;
|
||||||
|
bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#area iframe {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#prev {
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 36px;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#next {
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 36px;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow:hover {
|
||||||
|
background: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow:active {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
function bookUrl() {
|
||||||
|
return document.location.pathname.replace(/\/reader\//, '/epub/');
|
||||||
|
}
|
||||||
|
|
||||||
|
var Book = ePub({store: false});
|
||||||
|
Book.open(bookUrl());
|
||||||
|
Book.getMetadata().then(function(meta) {
|
||||||
|
document.title = meta.bookTitle + " – " + meta.creator;
|
||||||
|
document.addEventListener("keydown", function(event) {
|
||||||
|
if (event.keyCode == 39 || event.keyCode == 40) {
|
||||||
|
Book.nextPage();
|
||||||
|
} else if (event.keyCode == 37 || event.keyCode == 38) {
|
||||||
|
Book.prevPage();
|
||||||
|
}
|
||||||
|
}, false);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="main">
|
||||||
|
<div id="prev" onclick="Book.prevPage();" class="arrow"></div>
|
||||||
|
<div id="area"></div>
|
||||||
|
<div id="next" onclick="Book.nextPage();"class="arrow"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
Book.renderTo("area");
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,11 @@
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Open Media Library</title>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
|
<link href="/static/png/oml.png" rel="icon" type="image/png">
|
||||||
|
<script src="/static/js/oml.js?1" type="text/javascript"></script>
|
||||||
|
<meta name="google" value="notranslate"/>
|
||||||
|
</head>
|
||||||
|
<body></body>
|
||||||
|
</html>
|
|
@ -0,0 +1,355 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<!--
|
||||||
|
Copyright 2012 Mozilla Foundation
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
-->
|
||||||
|
<html dir="ltr" mozdisallowselectionprint moznomarginboxes>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||||
|
<meta name="google" content="notranslate">
|
||||||
|
<title></title>
|
||||||
|
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/static/pdf.js/viewer.css"/>
|
||||||
|
|
||||||
|
<script type="text/javascript" src="/static/oxjs/build/Ox.js"></script>
|
||||||
|
<script type="text/javascript" src="/static/oxjs/source/Ox.UI/js/Core/Message.js"></script>
|
||||||
|
<script type="text/javascript" src="/static/pdf.js/compatibility.js"></script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- This snippet is used in production, see Makefile -->
|
||||||
|
<link rel="resource" type="application/l10n" href="/static/pdf.js/locale/locale.properties"/>
|
||||||
|
<script type="text/javascript" src="/static/pdf.js/l10n.js"></script>
|
||||||
|
<script type="text/javascript" src="/static/pdf.js/pdf.js"></script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script type="text/javascript" src="/static/pdf.js/debugger.js"></script>
|
||||||
|
<script type="text/javascript" src="/static/pdf.js/embeds.js"></script>
|
||||||
|
|
||||||
|
<script type="text/javascript" src="/static/pdf.js/viewer.js"></script>
|
||||||
|
<link rel="stylesheet" type="text/css" href="/static/pdf.js/css/videopdf.css"/>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body tabindex="1">
|
||||||
|
<div id="outerContainer" class="loadingInProgress">
|
||||||
|
|
||||||
|
<div id="sidebarContainer">
|
||||||
|
<div id="toolbarSidebar">
|
||||||
|
<div class="splitToolbarButton toggled">
|
||||||
|
<button id="viewThumbnail" class="toolbarButton group toggled" title="Show Thumbnails" tabindex="2" data-l10n-id="thumbs">
|
||||||
|
<span data-l10n-id="thumbs_label">Thumbnails</span>
|
||||||
|
</button>
|
||||||
|
<button id="viewOutline" class="toolbarButton group" title="Show Document Outline" tabindex="3" data-l10n-id="outline">
|
||||||
|
<span data-l10n-id="outline_label">Document Outline</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="sidebarContent">
|
||||||
|
<div id="thumbnailView">
|
||||||
|
</div>
|
||||||
|
<div id="outlineView" class="hidden">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div> <!-- sidebarContainer -->
|
||||||
|
|
||||||
|
<div id="mainContainer">
|
||||||
|
<div class="findbar hidden doorHanger hiddenSmallView" id="findbar">
|
||||||
|
<label for="findInput" class="toolbarLabel" data-l10n-id="find_label">Find:</label>
|
||||||
|
<input id="findInput" class="toolbarField" tabindex="41">
|
||||||
|
<div class="splitToolbarButton">
|
||||||
|
<button class="toolbarButton findPrevious" title="" id="findPrevious" tabindex="42" data-l10n-id="find_previous">
|
||||||
|
<span data-l10n-id="find_previous_label">Previous</span>
|
||||||
|
</button>
|
||||||
|
<div class="splitToolbarButtonSeparator"></div>
|
||||||
|
<button class="toolbarButton findNext" title="" id="findNext" tabindex="43" data-l10n-id="find_next">
|
||||||
|
<span data-l10n-id="find_next_label">Next</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<input type="checkbox" id="findHighlightAll" class="toolbarField">
|
||||||
|
<label for="findHighlightAll" class="toolbarLabel" tabindex="44" data-l10n-id="find_highlight">Highlight all</label>
|
||||||
|
<input type="checkbox" id="findMatchCase" class="toolbarField">
|
||||||
|
<label for="findMatchCase" class="toolbarLabel" tabindex="45" data-l10n-id="find_match_case_label">Match case</label>
|
||||||
|
<span id="findMsg" class="toolbarLabel"></span>
|
||||||
|
</div> <!-- findbar -->
|
||||||
|
|
||||||
|
<div id="secondaryToolbar" class="secondaryToolbar hidden doorHangerRight">
|
||||||
|
<div id="secondaryToolbarButtonContainer">
|
||||||
|
<button id="secondaryPresentationMode" class="secondaryToolbarButton presentationMode visibleLargeView" title="Switch to Presentation Mode" tabindex="18" data-l10n-id="presentation_mode">
|
||||||
|
<span data-l10n-id="presentation_mode_label">Presentation Mode</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button id="secondaryOpenFile" class="secondaryToolbarButton openFile visibleLargeView" title="Open File" tabindex="19" data-l10n-id="open_file">
|
||||||
|
<span data-l10n-id="open_file_label">Open</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button id="secondaryPrint" class="secondaryToolbarButton print visibleMediumView" title="Print" tabindex="20" data-l10n-id="print">
|
||||||
|
<span data-l10n-id="print_label">Print</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button id="secondaryDownload" class="secondaryToolbarButton download visibleMediumView" title="Download" tabindex="21" data-l10n-id="download">
|
||||||
|
<span data-l10n-id="download_label">Download</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<a href="#" id="secondaryViewBookmark" class="secondaryToolbarButton bookmark visibleSmallView" title="Current view (copy or open in new window)" tabindex="22" data-l10n-id="bookmark">
|
||||||
|
<span data-l10n-id="bookmark_label">Current View</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="horizontalToolbarSeparator visibleLargeView"></div>
|
||||||
|
|
||||||
|
<button id="firstPage" class="secondaryToolbarButton firstPage" title="Go to First Page" tabindex="23" data-l10n-id="first_page">
|
||||||
|
<span data-l10n-id="first_page_label">Go to First Page</span>
|
||||||
|
</button>
|
||||||
|
<button id="lastPage" class="secondaryToolbarButton lastPage" title="Go to Last Page" tabindex="24" data-l10n-id="last_page">
|
||||||
|
<span data-l10n-id="last_page_label">Go to Last Page</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="horizontalToolbarSeparator"></div>
|
||||||
|
|
||||||
|
<button id="pageRotateCw" class="secondaryToolbarButton rotateCw" title="Rotate Clockwise" tabindex="25" data-l10n-id="page_rotate_cw">
|
||||||
|
<span data-l10n-id="page_rotate_cw_label">Rotate Clockwise</span>
|
||||||
|
</button>
|
||||||
|
<button id="pageRotateCcw" class="secondaryToolbarButton rotateCcw" title="Rotate Counterclockwise" tabindex="26" data-l10n-id="page_rotate_ccw">
|
||||||
|
<span data-l10n-id="page_rotate_ccw_label">Rotate Counterclockwise</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="horizontalToolbarSeparator"></div>
|
||||||
|
|
||||||
|
<button id="toggleHandTool" class="secondaryToolbarButton handTool" title="Enable hand tool" tabindex="27" data-l10n-id="hand_tool_enable">
|
||||||
|
<span data-l10n-id="hand_tool_enable_label">Enable hand tool</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div> <!-- secondaryToolbar -->
|
||||||
|
|
||||||
|
<div class="toolbar">
|
||||||
|
<div id="toolbarContainer">
|
||||||
|
<div id="toolbarViewer">
|
||||||
|
<div id="toolbarViewerLeft">
|
||||||
|
<button id="sidebarToggle" class="toolbarButton" title="Toggle Sidebar" tabindex="4" data-l10n-id="toggle_sidebar">
|
||||||
|
<span data-l10n-id="toggle_sidebar_label">Toggle Sidebar</span>
|
||||||
|
</button>
|
||||||
|
<div class="toolbarButtonSpacer"></div>
|
||||||
|
<button id="viewFind" class="toolbarButton group hiddenSmallView" title="Find in Document" tabindex="5" data-l10n-id="findbar">
|
||||||
|
<span data-l10n-id="findbar_label">Find</span>
|
||||||
|
</button>
|
||||||
|
<div class="splitToolbarButton">
|
||||||
|
<button class="toolbarButton pageUp" title="Previous Page" id="previous" tabindex="6" data-l10n-id="previous">
|
||||||
|
<span data-l10n-id="previous_label">Previous</span>
|
||||||
|
</button>
|
||||||
|
<div class="splitToolbarButtonSeparator"></div>
|
||||||
|
<button class="toolbarButton pageDown" title="Next Page" id="next" tabindex="7" data-l10n-id="next">
|
||||||
|
<span data-l10n-id="next_label">Next</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<label id="pageNumberLabel" class="toolbarLabel" for="pageNumber" data-l10n-id="page_label">Page: </label>
|
||||||
|
<input type="number" id="pageNumber" class="toolbarField pageNumber" value="1" size="4" min="1" tabindex="8">
|
||||||
|
<span id="numPages" class="toolbarLabel"></span>
|
||||||
|
</div>
|
||||||
|
<div id="toolbarViewerRight">
|
||||||
|
<button id="presentationMode" class="toolbarButton presentationMode hiddenLargeView" title="Switch to Presentation Mode" tabindex="12" data-l10n-id="presentation_mode">
|
||||||
|
<span data-l10n-id="presentation_mode_label">Presentation Mode</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button id="openFile" class="toolbarButton openFile hiddenLargeView" title="Open File" tabindex="13" data-l10n-id="open_file">
|
||||||
|
<span data-l10n-id="open_file_label">Open</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button id="print" class="toolbarButton print hiddenMediumView" title="Print" tabindex="14" data-l10n-id="print">
|
||||||
|
<span data-l10n-id="print_label">Print</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button id="download" class="toolbarButton download hiddenMediumView" title="Download" tabindex="15" data-l10n-id="download">
|
||||||
|
<span data-l10n-id="download_label">Download</span>
|
||||||
|
</button>
|
||||||
|
<!-- <div class="toolbarButtonSpacer"></div> -->
|
||||||
|
<a href="#" id="viewBookmark" class="toolbarButton bookmark hiddenSmallView" title="Current view (copy or open in new window)" tabindex="16" data-l10n-id="bookmark">
|
||||||
|
<span data-l10n-id="bookmark_label">Current View</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="verticalToolbarSeparator hiddenSmallView"></div>
|
||||||
|
|
||||||
|
<button id="secondaryToolbarToggle" class="toolbarButton" title="Tools" tabindex="17" data-l10n-id="tools">
|
||||||
|
<span data-l10n-id="tools_label">Tools</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="outerCenter">
|
||||||
|
<div class="innerCenter" id="toolbarViewerMiddle">
|
||||||
|
<div class="splitToolbarButton">
|
||||||
|
<button id="zoomOut" class="toolbarButton zoomOut" title="Zoom Out" tabindex="9" data-l10n-id="zoom_out">
|
||||||
|
<span data-l10n-id="zoom_out_label">Zoom Out</span>
|
||||||
|
</button>
|
||||||
|
<div class="splitToolbarButtonSeparator"></div>
|
||||||
|
<button id="zoomIn" class="toolbarButton zoomIn" title="Zoom In" tabindex="10" data-l10n-id="zoom_in">
|
||||||
|
<span data-l10n-id="zoom_in_label">Zoom In</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<span id="scaleSelectContainer" class="dropdownToolbarButton">
|
||||||
|
<select id="scaleSelect" title="Zoom" tabindex="11" data-l10n-id="zoom">
|
||||||
|
<option id="pageAutoOption" value="auto" selected="selected" data-l10n-id="page_scale_auto">Automatic Zoom</option>
|
||||||
|
<option id="pageActualOption" value="page-actual" data-l10n-id="page_scale_actual">Actual Size</option>
|
||||||
|
<option id="pageFitOption" value="page-fit" data-l10n-id="page_scale_fit">Fit Page</option>
|
||||||
|
<option id="pageWidthOption" value="page-width" data-l10n-id="page_scale_width">Full Width</option>
|
||||||
|
<option id="customScaleOption" value="custom"></option>
|
||||||
|
<option value="0.5">50%</option>
|
||||||
|
<option value="0.75">75%</option>
|
||||||
|
<option value="1">100%</option>
|
||||||
|
<option value="1.25">125%</option>
|
||||||
|
<option value="1.5">150%</option>
|
||||||
|
<option value="2">200%</option>
|
||||||
|
</select>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="loadingBar">
|
||||||
|
<div class="progress">
|
||||||
|
<div class="glimmer">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<menu type="context" id="viewerContextMenu">
|
||||||
|
<menuitem id="contextFirstPage" label="First Page"
|
||||||
|
data-l10n-id="first_page"></menuitem>
|
||||||
|
<menuitem id="contextLastPage" label="Last Page"
|
||||||
|
data-l10n-id="last_page"></menuitem>
|
||||||
|
<menuitem id="contextPageRotateCw" label="Rotate Clockwise"
|
||||||
|
data-l10n-id="page_rotate_cw"></menuitem>
|
||||||
|
<menuitem id="contextPageRotateCcw" label="Rotate Counter-Clockwise"
|
||||||
|
data-l10n-id="page_rotate_ccw"></menuitem>
|
||||||
|
</menu>
|
||||||
|
|
||||||
|
<div id="viewerContainer" tabindex="0">
|
||||||
|
<div id="viewer"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="errorWrapper" hidden='true'>
|
||||||
|
<div id="errorMessageLeft">
|
||||||
|
<span id="errorMessage"></span>
|
||||||
|
<button id="errorShowMore" data-l10n-id="error_more_info">
|
||||||
|
More Information
|
||||||
|
</button>
|
||||||
|
<button id="errorShowLess" data-l10n-id="error_less_info" hidden='true'>
|
||||||
|
Less Information
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="errorMessageRight">
|
||||||
|
<button id="errorClose" data-l10n-id="error_close">
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="clearBoth"></div>
|
||||||
|
<textarea id="errorMoreInfo" hidden='true' readonly="readonly"></textarea>
|
||||||
|
</div>
|
||||||
|
</div> <!-- mainContainer -->
|
||||||
|
|
||||||
|
<div id="overlayContainer" class="hidden">
|
||||||
|
<div id="promptContainer">
|
||||||
|
<div id="passwordContainer" class="prompt doorHanger">
|
||||||
|
<div class="row">
|
||||||
|
<p id="passwordText" data-l10n-id="password_label">Enter the password to open this PDF file:</p>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<input type="password" id="password" class="toolbarField" />
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<button id="passwordCancel" class="promptButton"><span data-l10n-id="password_cancel">Cancel</span></button>
|
||||||
|
<button id="passwordSubmit" class="promptButton"><span data-l10n-id="password_ok">OK</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div> <!-- outerContainer -->
|
||||||
|
<div id="printContainer"></div>
|
||||||
|
<div id="mozPrintCallback-shim" hidden>
|
||||||
|
<style scoped>
|
||||||
|
#mozPrintCallback-shim {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 9999999;
|
||||||
|
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
#mozPrintCallback-shim[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
@media print {
|
||||||
|
#mozPrintCallback-shim {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#mozPrintCallback-shim .mozPrintCallback-dialog-box {
|
||||||
|
display: inline-block;
|
||||||
|
margin: -50px auto 0;
|
||||||
|
position: relative;
|
||||||
|
top: 45%;
|
||||||
|
left: 0;
|
||||||
|
min-width: 220px;
|
||||||
|
max-width: 400px;
|
||||||
|
|
||||||
|
padding: 9px;
|
||||||
|
|
||||||
|
border: 1px solid hsla(0, 0%, 0%, .5);
|
||||||
|
border-radius: 2px;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
|
||||||
|
background-color: #474747;
|
||||||
|
|
||||||
|
color: hsl(0, 0%, 85%);
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
#mozPrintCallback-shim .progress-row {
|
||||||
|
clear: both;
|
||||||
|
padding: 1em 0;
|
||||||
|
}
|
||||||
|
#mozPrintCallback-shim progress {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
#mozPrintCallback-shim .relative-progress {
|
||||||
|
clear: both;
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
#mozPrintCallback-shim .progress-actions {
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="mozPrintCallback-dialog-box">
|
||||||
|
<!-- TODO: Localise the following strings -->
|
||||||
|
Preparing document for printing...
|
||||||
|
<div class="progress-row">
|
||||||
|
<progress value="0" max="100"></progress>
|
||||||
|
<span class="relative-progress">0%</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress-actions">
|
||||||
|
<input type="button" value="Cancel" class="mozPrintCallback-cancel">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<script src="/static/oxjs/build/Ox.js" type="text/javascript"></script>
|
||||||
|
<script src="/static/txt.js/txt.js" type="text/javascript"></script>
|
||||||
|
<script>
|
||||||
|
txtjs.open(document.location.pathname.replace(/\/reader\//, '/txt/'));
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body></body>
|
||||||
|
</html>
|
|
@ -0,0 +1,41 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
oml.Preferences = (function() {
|
||||||
|
|
||||||
|
var that = {};
|
||||||
|
|
||||||
|
that.set = function() {
|
||||||
|
|
||||||
|
var args = Ox.isObject(arguments[0])
|
||||||
|
? args
|
||||||
|
: Ox.makeObject([arguments[0], arguments[1]]),
|
||||||
|
set = {},
|
||||||
|
preferences = oml.user.preferences,
|
||||||
|
previousPreferences = Ox.clone(preferences, true);
|
||||||
|
|
||||||
|
Ox.forEach(args, function(value, key) {
|
||||||
|
if (!Ox.isEqual(preferences[key], value)) {
|
||||||
|
preferences[key] = value;
|
||||||
|
set[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Ox.len(set)) {
|
||||||
|
oml.api.setPreferences(set);
|
||||||
|
Ox.forEach(set, function(value, key) {
|
||||||
|
Ox.forEach(oml.$ui, function($element) {
|
||||||
|
if (Ox.UI.isElement($element)) {
|
||||||
|
$element.triggerEvent('oml_' + key.toLowerCase(), {
|
||||||
|
value: value,
|
||||||
|
previousValue: previousPreferences[key]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
return that;
|
||||||
|
|
||||||
|
}());
|
|
@ -0,0 +1,181 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
oml.UI = (function() {
|
||||||
|
|
||||||
|
var previousUI = {},
|
||||||
|
that = {};
|
||||||
|
|
||||||
|
that.encode = function(value) {
|
||||||
|
return value.replace(/\./g, '\\.');
|
||||||
|
};
|
||||||
|
|
||||||
|
that.reset = function() {
|
||||||
|
var ui = oml.user.ui;
|
||||||
|
oml.api.resetUI({}, function() {
|
||||||
|
ui = oml.config.user.ui;
|
||||||
|
ui._list = oml.getListState(ui.find);
|
||||||
|
ui._filterState = oml.getFilterState(ui.find);
|
||||||
|
ui._findState = oml.getFindState(ui.find);
|
||||||
|
Ox.Theme(ui.theme);
|
||||||
|
oml.$ui.appPanel.reload();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// sets oml.user.ui.key to value
|
||||||
|
// key foo.bar.baz sets oml.user.ui.foo.bar.baz
|
||||||
|
// value null removes a key
|
||||||
|
that.set = function(/* {key: value}[, flag] or key, value[, flag] */) {
|
||||||
|
|
||||||
|
var add = {},
|
||||||
|
args,
|
||||||
|
item,
|
||||||
|
list,
|
||||||
|
listSettings = oml.config.listSettings,
|
||||||
|
listView,
|
||||||
|
set = {},
|
||||||
|
trigger = {},
|
||||||
|
triggerEvents,
|
||||||
|
ui = oml.user.ui;
|
||||||
|
|
||||||
|
if (Ox.isObject(arguments[0])) {
|
||||||
|
args = arguments[0];
|
||||||
|
triggerEvents = Ox.isUndefined(arguments[1]) ? true : arguments[1];
|
||||||
|
} else {
|
||||||
|
args = Ox.makeObject([arguments[0], arguments[1]]);
|
||||||
|
triggerEvents = Ox.isUndefined(arguments[2]) ? true : arguments[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
Ox.print('UI SET', JSON.stringify(args));
|
||||||
|
|
||||||
|
previousUI = Ox.clone(ui, true);
|
||||||
|
previousUI._list = oml.getListState(previousUI.find);
|
||||||
|
|
||||||
|
if ('find' in args) {
|
||||||
|
// the challenge here is that find may change list,
|
||||||
|
// and list may then change listSort and listView,
|
||||||
|
// which we don't want to trigger, since find triggers
|
||||||
|
// (values we put in add will be changed, but won't trigger)
|
||||||
|
list = oml.getListState(args.find);
|
||||||
|
ui._list = list;
|
||||||
|
ui._filterState = oml.getFilterState(args.find);
|
||||||
|
ui._findState = oml.getFindState(args.find);
|
||||||
|
if (oml.$ui.appPanel && !oml.stayInItemView) {
|
||||||
|
// if we're not on page load, and if find isn't a context change
|
||||||
|
// caused by an edit, then switch from item view to list view
|
||||||
|
args.item = '';
|
||||||
|
}
|
||||||
|
if (list != previousUI._list) {
|
||||||
|
// if find has changed list
|
||||||
|
Ox.forEach(listSettings, function(listSetting, setting) {
|
||||||
|
// then for each setting that corresponds to a list setting
|
||||||
|
if (!ui.lists[list]) {
|
||||||
|
// either add the default setting
|
||||||
|
add[setting] = oml.config.user.ui[setting];
|
||||||
|
} else {
|
||||||
|
// or the existing list setting
|
||||||
|
add[setting] = ui.lists[list][listSetting]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
list = previousUI._list;
|
||||||
|
}
|
||||||
|
// it is important to check for find first, so that
|
||||||
|
// if find changes list, list is correct here
|
||||||
|
item = args.item || ui.item;
|
||||||
|
listView = add.listView || args.listView;
|
||||||
|
|
||||||
|
if (!ui.lists[list]) {
|
||||||
|
add['lists.' + that.encode(list)] = {};
|
||||||
|
}
|
||||||
|
Ox.forEach(listSettings, function(listSetting, setting) {
|
||||||
|
// for each setting that corresponds to a list setting
|
||||||
|
// set that list setting to
|
||||||
|
var key = 'lists.' + that.encode(list) + '.' + listSetting;
|
||||||
|
if (setting in args) {
|
||||||
|
// the setting passed to UI.set
|
||||||
|
add[key] = args[setting];
|
||||||
|
} else if (setting in add) {
|
||||||
|
// or the setting changed via find
|
||||||
|
add[key] = add[setting];
|
||||||
|
} else if (!ui.lists[list]) {
|
||||||
|
// or the default setting
|
||||||
|
add[key] = oml.config.user.ui[setting];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (args.item) {
|
||||||
|
// when switching to an item, update list selection
|
||||||
|
add['listSelection'] = [args.item];
|
||||||
|
add['lists.' + that.encode(list) + '.selection'] = [args.item];
|
||||||
|
if (
|
||||||
|
!args.itemView
|
||||||
|
&& ui.itemView == 'book'
|
||||||
|
&& !ui.mediaState[item]
|
||||||
|
&& !args['mediaState.' + item]
|
||||||
|
) {
|
||||||
|
// if the item view doesn't change, remains a media view,
|
||||||
|
// media state doesn't exist yet, and won't be set, add
|
||||||
|
// default media state
|
||||||
|
add['mediaState.' + item] = {position: 0, zoom: 1};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
args.itemView == 'book'
|
||||||
|
&& !ui.mediaState[item]
|
||||||
|
&& !args['mediaState.' + item]
|
||||||
|
) {
|
||||||
|
// when switching to a media view, media state doesn't exist
|
||||||
|
// yet, and won't be set, add default media state
|
||||||
|
add['mediaState.' + item] = {position: 0, zoom: 1};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// items in args trigger events, items in add do not
|
||||||
|
[args, add].forEach(function(object, isAdd) {
|
||||||
|
Ox.forEach(object, function(value, key) {
|
||||||
|
// make sure to not split at escaped dots ('\.')
|
||||||
|
var keys = key.replace(/\\\./g, '\n').split('.').map(function(key) {
|
||||||
|
return key.replace(/\n/g, '.');
|
||||||
|
}),
|
||||||
|
ui_ = ui;
|
||||||
|
while (keys.length > 1) {
|
||||||
|
ui_ = ui_[keys.shift()];
|
||||||
|
}
|
||||||
|
if (!Ox.isEqual(ui_[keys[0]], value)) {
|
||||||
|
if (value === null) {
|
||||||
|
delete ui_[keys[0]];
|
||||||
|
} else {
|
||||||
|
ui_[keys[0]] = value;
|
||||||
|
}
|
||||||
|
set[key] = value;
|
||||||
|
if (!isAdd) {
|
||||||
|
trigger[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (Ox.len(set)) {
|
||||||
|
oml.api.setUI(set);
|
||||||
|
}
|
||||||
|
if (triggerEvents) {
|
||||||
|
Ox.forEach(trigger, function(value, key) {
|
||||||
|
Ox.print('UI TRIGGER', 'oml_' + key.toLowerCase(), value);
|
||||||
|
Ox.forEach(oml.$ui, function($elements) {
|
||||||
|
Ox.makeArray($elements).forEach(function($element) {
|
||||||
|
$element.triggerEvent('oml_' + key.toLowerCase(), {
|
||||||
|
value: value,
|
||||||
|
previousValue: previousUI[key]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
oml.URL.update(Object.keys(!oml.$ui.appPanel ? args : trigger));
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
return that;
|
||||||
|
|
||||||
|
}());
|
|
@ -0,0 +1,349 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
oml.URL = (function() {
|
||||||
|
|
||||||
|
var self = {}, that = {};
|
||||||
|
|
||||||
|
function getHash(state, callback) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getItem(state, string, callback) {
|
||||||
|
oml.api.get({id: string, keys: ['id']}, function(result) {
|
||||||
|
if (result.status.code == 200) {
|
||||||
|
state.item = result.data.id;
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPart(state, string, callback) {
|
||||||
|
var parts = Ox.getObjectById(oml.config.pages, state.page).parts || [];
|
||||||
|
if (Ox.contains(parts, string)) {
|
||||||
|
state.part = string;
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSort(state, value, callback) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSpan(state, value, callback) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
// translates UI settings to URL state
|
||||||
|
function getState() {
|
||||||
|
|
||||||
|
var state = {},
|
||||||
|
ui = oml.user.ui;
|
||||||
|
|
||||||
|
if (ui.page) {
|
||||||
|
state.page = ui.page;
|
||||||
|
if (Ox.contains(Object.keys(oml.config.user.ui.part), state.page)) {
|
||||||
|
state.part = ui.part[state.page];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
state.type = ui.section;
|
||||||
|
state.item = ui.item;
|
||||||
|
|
||||||
|
if (ui.section == 'books') {
|
||||||
|
if (!ui.item) {
|
||||||
|
state.view = ui.listView;
|
||||||
|
state.sort = [ui.listSort[0]];
|
||||||
|
state.find = ui.find;
|
||||||
|
} else {
|
||||||
|
state.view = ui.itemView;
|
||||||
|
if (ui.itemView == 'book') {
|
||||||
|
state.span = ui.mediaState[state.item] || [0, 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function getURLOptions() {
|
||||||
|
|
||||||
|
var sortKeys = {},
|
||||||
|
ui = oml.user.ui,
|
||||||
|
views = {};
|
||||||
|
|
||||||
|
views['books'] = {
|
||||||
|
// ui.listView is the default view
|
||||||
|
list: [ui.listView].concat(
|
||||||
|
oml.config.listViews.filter(function(view) {
|
||||||
|
return view.id != ui.listView;
|
||||||
|
}).map(function(view) {
|
||||||
|
return view.id;
|
||||||
|
})
|
||||||
|
),
|
||||||
|
// ui.itemView is the default view,
|
||||||
|
item: [ui.itemView].concat(
|
||||||
|
oml.config.itemViews.filter(function(view) {
|
||||||
|
return view.id != ui.itemView;
|
||||||
|
}).map(function(view) {
|
||||||
|
return view.id;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
sortKeys['books'] = {list: {}, item: {}};
|
||||||
|
views['books'].list.forEach(function(view) {
|
||||||
|
sortKeys['books'].list[view] = [].concat(
|
||||||
|
// ui.listSort[0].key is the default sort key
|
||||||
|
Ox.getObjectById(oml.config.sortKeys, ui.listSort[0].key),
|
||||||
|
oml.config.sortKeys.filter(function(key) {
|
||||||
|
return key.id != ui.listSort[0].key;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
findKeys: [{id: 'list', type: 'string'}].concat(
|
||||||
|
oml.config.itemKeys
|
||||||
|
),
|
||||||
|
pages: oml.config.pages.map(function(page) {
|
||||||
|
return page.id;
|
||||||
|
}),
|
||||||
|
spanType: {
|
||||||
|
books: {
|
||||||
|
list: {},
|
||||||
|
item: {
|
||||||
|
book: 'FIXME, no idea'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sortKeys: sortKeys,
|
||||||
|
types: ['books'],
|
||||||
|
views: views
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// translates URL state to UI settings
|
||||||
|
function setState(state, callback) {
|
||||||
|
|
||||||
|
var set = {},
|
||||||
|
ui = oml.user.ui;
|
||||||
|
|
||||||
|
ui._list = oml.getListState(ui.find);
|
||||||
|
ui._filterState = oml.getFilterState(ui.find);
|
||||||
|
ui._findState = oml.getFindState(ui.find);
|
||||||
|
|
||||||
|
if (Ox.isEmpty(state)) {
|
||||||
|
|
||||||
|
callback && callback();
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
if (state.page) {
|
||||||
|
|
||||||
|
set.page = state.page;
|
||||||
|
if (
|
||||||
|
Ox.contains(Object.keys(oml.config.user.ui.part), state.page)
|
||||||
|
&& state.part
|
||||||
|
) {
|
||||||
|
set['part.' + state.page] = state.part;
|
||||||
|
}
|
||||||
|
oml.UI.set(set);
|
||||||
|
callback && callback();
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
set.page = '';
|
||||||
|
|
||||||
|
if (state.type) {
|
||||||
|
set.section = state.type;
|
||||||
|
set.item = state.item;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (set.section == 'books') {
|
||||||
|
|
||||||
|
if (state.view) {
|
||||||
|
set[!state.item ? 'listView' : 'itemView'] = state.view;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.sort) {
|
||||||
|
set[!state.item ? 'listSort' : 'itemSort'] = state.sort;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.span) {
|
||||||
|
if (state.view == 'book') {
|
||||||
|
set['mediaState.' + state.item] = {
|
||||||
|
position: state.span[0],
|
||||||
|
zoom: state.span[1]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!state.item) {
|
||||||
|
if (state.find) {
|
||||||
|
set.find = state.find;
|
||||||
|
} else if (!oml.$ui.appPanel) {
|
||||||
|
// when loading results without find, clear find, so that
|
||||||
|
// removing a query and reloading works as expected
|
||||||
|
set.find = oml.config.user.ui.find;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Ox.Request.cancel();
|
||||||
|
|
||||||
|
if (!oml.$ui.appPanel && state.item && ui.find) {
|
||||||
|
// on page load, if item is set and there was a query,
|
||||||
|
// we have to check if the item actually matches the query,
|
||||||
|
// and otherwise reset find
|
||||||
|
oml.api.find({
|
||||||
|
query: ui.find,
|
||||||
|
positions: [state.item],
|
||||||
|
sort: [{key: 'id', operator: ''}]
|
||||||
|
}, function(result) {
|
||||||
|
if (Ox.isUndefined(result.data.positions[state.item])) {
|
||||||
|
set.find = oml.config.user.ui.find
|
||||||
|
}
|
||||||
|
oml.UI.set(set);
|
||||||
|
callback && callback();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
oml.UI.set(set);
|
||||||
|
callback && callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
that.init = function() {
|
||||||
|
|
||||||
|
self.URL = Ox.URL(Ox.extend({
|
||||||
|
getHash: getHash,
|
||||||
|
getItem: getItem,
|
||||||
|
getPart: getPart,
|
||||||
|
getSort: getSort,
|
||||||
|
getSpan: getSpan,
|
||||||
|
}, getURLOptions()));
|
||||||
|
|
||||||
|
window.addEventListener('hashchange', function() {
|
||||||
|
Ox.Request.cancel();
|
||||||
|
that.parse();
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('popstate', function(e) {
|
||||||
|
Ox.Request.cancel();
|
||||||
|
self.isPopState = true;
|
||||||
|
$('.OxDialog:visible').each(function() {
|
||||||
|
Ox.UI.elements[$(this).data('oxid')].close();
|
||||||
|
});
|
||||||
|
if (e.state && !Ox.isEmpty(e.state)) {
|
||||||
|
document.title = Ox.decodeHTMLEntities(e.state.title);
|
||||||
|
setState(e.state);
|
||||||
|
} else {
|
||||||
|
that.parse();
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
return that;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
// on page load, this sets the state from the URL
|
||||||
|
// can also be used to parse a URL
|
||||||
|
that.parse = function(url, callback) {
|
||||||
|
if (arguments.length == 2) {
|
||||||
|
self.URL.parse(url, callback);
|
||||||
|
} else {
|
||||||
|
callback = arguments[0];
|
||||||
|
url = null;
|
||||||
|
if (document.location.pathname.slice(0, 4) == 'url=') {
|
||||||
|
document.location.href = Ox.decodeURI(document.location.pathname.slice(4));
|
||||||
|
} else {
|
||||||
|
self.URL.parse(function(state) {
|
||||||
|
// setState -> UI.set -> URL.update
|
||||||
|
setState(state, callback);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return that;
|
||||||
|
};
|
||||||
|
|
||||||
|
// sets the URL to the previous URL
|
||||||
|
that.pop = function() {
|
||||||
|
self.URL.pop() || that.update();
|
||||||
|
return that;
|
||||||
|
};
|
||||||
|
|
||||||
|
// pushes a new URL (as string or from state)
|
||||||
|
that.push = function(stateOrURL, expandURL) {
|
||||||
|
var state,
|
||||||
|
title = oml.getPageTitle(stateOrURL)
|
||||||
|
url;
|
||||||
|
oml.replaceURL = expandURL;
|
||||||
|
if (Ox.isObject(stateOrURL)) {
|
||||||
|
state = stateOrURL;
|
||||||
|
} else {
|
||||||
|
url = stateOrURL;
|
||||||
|
}
|
||||||
|
self.URL.push(state, title, url, setState);
|
||||||
|
return that;
|
||||||
|
};
|
||||||
|
|
||||||
|
// replaces the current URL (as string or from state)
|
||||||
|
that.replace = function(stateOrURL, title) {
|
||||||
|
var state,
|
||||||
|
title = oml.getPageTitle(stateOrURL)
|
||||||
|
url;
|
||||||
|
if (Ox.isObject(stateOrURL)) {
|
||||||
|
state = stateOrURL;
|
||||||
|
} else {
|
||||||
|
url = stateOrURL;
|
||||||
|
}
|
||||||
|
self.URL.push(state, title, url, setState);
|
||||||
|
return that;
|
||||||
|
};
|
||||||
|
|
||||||
|
// this gets called from oml.UI
|
||||||
|
that.update = function(keys) {
|
||||||
|
var action, state;
|
||||||
|
if (keys.some(function(key) {
|
||||||
|
return Ox.contains(['itemView', 'listSort', 'listView'], key);
|
||||||
|
})) {
|
||||||
|
self.URL.options(getURLOptions());
|
||||||
|
}
|
||||||
|
if (self.isPopState) {
|
||||||
|
self.isPopState = false;
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
!oml.$ui.appPanel
|
||||||
|
|| oml.replaceURL
|
||||||
|
|| keys.every(function(key) {
|
||||||
|
return Ox.contains([
|
||||||
|
'listColumnWidth', 'listColumns', 'listSelection'
|
||||||
|
], key) || /^mediaState/.test(key);
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
action = 'replace';
|
||||||
|
} else {
|
||||||
|
action = 'push';
|
||||||
|
}
|
||||||
|
state = getState();
|
||||||
|
self.URL[action](
|
||||||
|
state,
|
||||||
|
oml.getPageTitle(state)
|
||||||
|
);
|
||||||
|
oml.replaceURL = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return that;
|
||||||
|
|
||||||
|
}());
|
|
@ -0,0 +1,69 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
oml.ui.allItems = function(user) {
|
||||||
|
|
||||||
|
var ui = oml.user.ui,
|
||||||
|
|
||||||
|
that = Ox.TableList({
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
format: function() {
|
||||||
|
return $('<img>')
|
||||||
|
.attr({
|
||||||
|
src: Ox.UI.getImageURL(user ? 'symbolUser' : 'symbolData')
|
||||||
|
})
|
||||||
|
.css({
|
||||||
|
width: '10px',
|
||||||
|
height: '10px',
|
||||||
|
margin: '2px -2px 2px 0'
|
||||||
|
});
|
||||||
|
},
|
||||||
|
id: 'id',
|
||||||
|
title: 'ID',
|
||||||
|
visible: true,
|
||||||
|
width: 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'title',
|
||||||
|
title: 'Title',
|
||||||
|
visible: true,
|
||||||
|
width: ui.sidebarSize - 58,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
align: 'right',
|
||||||
|
format: function(value) {
|
||||||
|
return value > -1
|
||||||
|
? '<span class="OxLight">'
|
||||||
|
+ Ox.formatNumber(value)
|
||||||
|
+ '</span>'
|
||||||
|
: '';
|
||||||
|
},
|
||||||
|
id: 'items',
|
||||||
|
title: 'Items',
|
||||||
|
visible: true,
|
||||||
|
width: 42
|
||||||
|
}
|
||||||
|
],
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: '',
|
||||||
|
title: Ox._(user ? 'Library' : 'All Libraries'),
|
||||||
|
items: -1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
sort: [{key: 'id', operator: '+'}],
|
||||||
|
selected: [],
|
||||||
|
unique: 'id'
|
||||||
|
})
|
||||||
|
.css({
|
||||||
|
width: ui.sidebarSize + 'px',
|
||||||
|
height: '16px'
|
||||||
|
});
|
||||||
|
|
||||||
|
that.resizeElement = function() {
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
|
||||||
|
return that;
|
||||||
|
|
||||||
|
};
|
|
@ -0,0 +1,108 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
oml.ui.appDialog = function() {
|
||||||
|
|
||||||
|
var ui = oml.user.ui,
|
||||||
|
|
||||||
|
tabs = Ox.getObjectById(oml.config.pages, 'app').parts.map(function(tab) {
|
||||||
|
return {
|
||||||
|
id: tab.id,
|
||||||
|
title: tab.title.replace(/ Open Media Library$/, ''),
|
||||||
|
selected: tab.id == ui.part.app
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
|
||||||
|
$panel = Ox.TabPanel({
|
||||||
|
content: function(id) {
|
||||||
|
var $logo = Ox.Element(),
|
||||||
|
$text = Ox.Element()
|
||||||
|
.addClass('OxTextPage'),
|
||||||
|
title = Ox.getObjectById(
|
||||||
|
Ox.getObjectById(oml.config.pages, 'app').parts,
|
||||||
|
id
|
||||||
|
).title;
|
||||||
|
$('<img>')
|
||||||
|
.attr({
|
||||||
|
src: '/static/png/oml.png'
|
||||||
|
})
|
||||||
|
.css({
|
||||||
|
position: 'absolute',
|
||||||
|
left: '16px',
|
||||||
|
top: '16px',
|
||||||
|
width: '192px',
|
||||||
|
height: '192px'
|
||||||
|
})
|
||||||
|
.appendTo($logo);
|
||||||
|
$('<div>')
|
||||||
|
.css({
|
||||||
|
position: 'absolute',
|
||||||
|
left: '16px',
|
||||||
|
right: '24px',
|
||||||
|
top: '24px',
|
||||||
|
overflowY: 'auto'
|
||||||
|
})
|
||||||
|
.html(
|
||||||
|
'<h1><b>' + title + '</b></h1>'
|
||||||
|
+ '<p>The lazy brown fox jumped over the lazy black fox, but otherwise not really much happened here since you last checked.'
|
||||||
|
)
|
||||||
|
.appendTo($text);
|
||||||
|
return Ox.SplitPanel({
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
element: $logo,
|
||||||
|
size: 208
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: $text
|
||||||
|
}
|
||||||
|
],
|
||||||
|
orientation: 'horizontal'
|
||||||
|
});
|
||||||
|
},
|
||||||
|
tabs: tabs
|
||||||
|
})
|
||||||
|
.bindEvent({
|
||||||
|
change: function(data) {
|
||||||
|
oml.UI.set({'part.app': data.selected});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
that = Ox.Dialog({
|
||||||
|
buttons: [
|
||||||
|
Ox.Button({
|
||||||
|
id: 'close',
|
||||||
|
title: Ox._('Close')
|
||||||
|
}).bindEvent({
|
||||||
|
click: function() {
|
||||||
|
that.close();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
],
|
||||||
|
closeButton: true,
|
||||||
|
content: $panel,
|
||||||
|
fixedSize: true,
|
||||||
|
height: 384,
|
||||||
|
removeOnClose: true,
|
||||||
|
title: 'Open Media Library',
|
||||||
|
width: 768
|
||||||
|
})
|
||||||
|
.bindEvent({
|
||||||
|
close: function() {
|
||||||
|
if (ui.page == 'app') {
|
||||||
|
oml.UI.set({page: ''});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'oml_part.app': function() {
|
||||||
|
if (ui.page == 'app') {
|
||||||
|
that.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
that.update = function(section) {
|
||||||
|
$panel.selectTab(section);
|
||||||
|
};
|
||||||
|
|
||||||
|
return that;
|
||||||
|
|
||||||
|
};
|
|
@ -0,0 +1,51 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
oml.ui.appPanel = function() {
|
||||||
|
|
||||||
|
var ui = oml.user.ui,
|
||||||
|
|
||||||
|
that = Ox.SplitPanel({
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
element: oml.$ui.mainMenu = oml.ui.mainMenu(),
|
||||||
|
size: 20
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: oml.$ui.mainPanel = oml.ui.mainPanel()
|
||||||
|
}
|
||||||
|
],
|
||||||
|
orientation: 'vertical'
|
||||||
|
})
|
||||||
|
.bindEvent({
|
||||||
|
oml_page: function(data) {
|
||||||
|
setPage(data.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setPage(ui.page);
|
||||||
|
|
||||||
|
function setPage(page) {
|
||||||
|
// close dialogs
|
||||||
|
$('.OxDialog:visible').each(function() {
|
||||||
|
Ox.UI.elements[$(this).data('oxid')].close();
|
||||||
|
});
|
||||||
|
// open dialog
|
||||||
|
if (Ox.contains([
|
||||||
|
'welcome', 'app', 'preferences', 'users',
|
||||||
|
'notifications', 'transfers', 'help'
|
||||||
|
], page)) {
|
||||||
|
oml.$ui[page + 'Dialog'] = oml.ui[page + 'Dialog']().open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
that.reload = function() {
|
||||||
|
Ox.Request.cancel();
|
||||||
|
Ox.Request.clearCache();
|
||||||
|
oml.$ui.appPanel.remove();
|
||||||
|
oml.$ui.appPanel = oml.ui.appPanel().appendTo(Ox.$body);
|
||||||
|
return that;
|
||||||
|
};
|
||||||
|
|
||||||
|
return that;
|
||||||
|
|
||||||
|
};
|
|
@ -0,0 +1,25 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
oml.ui.backButton = function() {
|
||||||
|
|
||||||
|
var ui = oml.user.ui,
|
||||||
|
|
||||||
|
that = Ox.Button({
|
||||||
|
style: 'squared',
|
||||||
|
title: 'arrowLeft',
|
||||||
|
tooltip: Ox._('Back to Books'),
|
||||||
|
type: 'image'
|
||||||
|
})
|
||||||
|
.css({
|
||||||
|
float: 'left',
|
||||||
|
margin: '4px 2px 4px 4px'
|
||||||
|
})
|
||||||
|
.bindEvent({
|
||||||
|
click: function() {
|
||||||
|
oml.UI.set({item: ''});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return that;
|
||||||
|
|
||||||
|
};
|
|
@ -0,0 +1,106 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
oml.ui.browser = function() {
|
||||||
|
|
||||||
|
var ui = oml.user.ui,
|
||||||
|
|
||||||
|
that = Ox.IconList({
|
||||||
|
centered: true,
|
||||||
|
defaultRatio: oml.config.coverRatio,
|
||||||
|
draggable: true,
|
||||||
|
item: function(data, sort, size) {
|
||||||
|
var color = oml.getFileTypeColor(data).map(function(rgb) {
|
||||||
|
return rgb.concat(0.8)
|
||||||
|
}),
|
||||||
|
ratio = data.coverRatio || oml.config.coverRatio,
|
||||||
|
width = Math.round(ratio >= 1 ? size : size * ratio),
|
||||||
|
height = Math.round(ratio <= 1 ? size : size / ratio),
|
||||||
|
sortKey = sort[0].key,
|
||||||
|
info = Ox.getObjectById(oml.config.sortKeys, sortKey).format(
|
||||||
|
Ox.contains(['title', 'random'], sortKey)
|
||||||
|
? (data.author || '') : data[sortKey]
|
||||||
|
);
|
||||||
|
size = size || 64;
|
||||||
|
return {
|
||||||
|
extra: ui.showFileInfo ? $('<div>')
|
||||||
|
.css({
|
||||||
|
width: width + 'px',
|
||||||
|
height: Math.round(size / 12.8) + 'px',
|
||||||
|
borderWidth: Math.round(size / 64) + 'px 0',
|
||||||
|
borderStyle: 'solid',
|
||||||
|
borderColor: 'rgba(' + color[1].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(', ') + ')',
|
||||||
|
WebkitTransform: 'rotate(45deg)'
|
||||||
|
})
|
||||||
|
.html(
|
||||||
|
ui.fileInfo == 'extension'
|
||||||
|
? data.extension.toUpperCase()
|
||||||
|
: Ox.formatValue(data.size, 'B')
|
||||||
|
) : null,
|
||||||
|
height: height,
|
||||||
|
id: data.id,
|
||||||
|
info: info,
|
||||||
|
title: data.title,
|
||||||
|
url: '/' + data.id + '/cover128.jpg',
|
||||||
|
width: width
|
||||||
|
};
|
||||||
|
},
|
||||||
|
items: function(data, callback) {
|
||||||
|
oml.api.find(Ox.extend(data, {
|
||||||
|
query: ui.find
|
||||||
|
}), callback);
|
||||||
|
},
|
||||||
|
keys: [
|
||||||
|
'author', 'coverRatio', 'extension',
|
||||||
|
'id', 'size', 'textsize', 'title'
|
||||||
|
],
|
||||||
|
max: 1,
|
||||||
|
min: 1,
|
||||||
|
orientation: 'horizontal',
|
||||||
|
// FIXME: is this correct?:
|
||||||
|
selected: ui.item ? [ui.item]
|
||||||
|
: ui.listSelection.length ? [ui.listSelection[0]]
|
||||||
|
: [],
|
||||||
|
size: 64,
|
||||||
|
sort: ui.listSort,
|
||||||
|
unique: 'id'
|
||||||
|
})
|
||||||
|
.bindEvent({
|
||||||
|
open: function() {
|
||||||
|
oml.UI.set({itemView: 'book'});
|
||||||
|
},
|
||||||
|
select: function(data) {
|
||||||
|
oml.UI.set({
|
||||||
|
item: data.ids[0],
|
||||||
|
itemView: 'info',
|
||||||
|
listSelection: data.ids
|
||||||
|
});
|
||||||
|
},
|
||||||
|
oml_find: function() {
|
||||||
|
that.reloadList();
|
||||||
|
},
|
||||||
|
oml_item: function(data) {
|
||||||
|
if (data.value && !data.previousValue) {
|
||||||
|
that.gainFocus();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
oml_listselection: function(data) {
|
||||||
|
if (data.value.length) {
|
||||||
|
that.options({selected: [data.value[0]]});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
oml_listsort: function(data) {
|
||||||
|
that.options({sort: data.value});
|
||||||
|
},
|
||||||
|
oml_sidebarsize: function(data) {
|
||||||
|
that.size(); // FIXME: DOESN'T CENTER
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return that;
|
||||||
|
|
||||||
|
};
|
|
@ -0,0 +1,148 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
oml.ui.columnView = function() {
|
||||||
|
|
||||||
|
var ui = oml.user.ui,
|
||||||
|
|
||||||
|
that = Ox.CustomColumnList({
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
id: 'authors',
|
||||||
|
item: getItemFunction('authors'),
|
||||||
|
itemHeight: 32,
|
||||||
|
items: function(data, selected, callback) {
|
||||||
|
oml.api.find(Ox.extend({
|
||||||
|
group: 'author',
|
||||||
|
query: {conditions: [], operator: '&'}
|
||||||
|
}, data), callback);
|
||||||
|
},
|
||||||
|
keys: ['name', 'items'],
|
||||||
|
max: -1,
|
||||||
|
sort: [{key: 'name', operator: '+'}],
|
||||||
|
selected: [],
|
||||||
|
title: Ox._('Authors'),
|
||||||
|
unique: 'name'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'items',
|
||||||
|
item: getItemFunction('items'),
|
||||||
|
itemHeight: 32,
|
||||||
|
items: function(data, selected, callback) {
|
||||||
|
if (selected[0].length) {
|
||||||
|
oml.api.find(Ox.extend({
|
||||||
|
query: {
|
||||||
|
conditions: selected[0].map(function(name) {
|
||||||
|
return {
|
||||||
|
key: 'author',
|
||||||
|
operator: '==',
|
||||||
|
value: name
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
operator: '|'
|
||||||
|
}
|
||||||
|
}, data), callback);
|
||||||
|
} else {
|
||||||
|
callback({
|
||||||
|
data: {
|
||||||
|
items: data.keys ? [] : 0
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
keys: ['author', 'title', 'date'],
|
||||||
|
max: -1,
|
||||||
|
selected: [],
|
||||||
|
sort: [{key: 'title', operator: '+'}],
|
||||||
|
title: Ox._('Items'),
|
||||||
|
unique: 'id'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'files',
|
||||||
|
item: getItemFunction('files'),
|
||||||
|
itemHeight: 32,
|
||||||
|
items: function(data, selected, callback) {
|
||||||
|
oml.api.find(Ox.extend({
|
||||||
|
query: {
|
||||||
|
conditions: selected[0].map(function(name) {
|
||||||
|
return {
|
||||||
|
key: 'author',
|
||||||
|
operator: '==',
|
||||||
|
value: name
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
operator: '|'
|
||||||
|
}
|
||||||
|
}, data), callback);
|
||||||
|
},
|
||||||
|
keys: ['id', 'name'],
|
||||||
|
selected: [],
|
||||||
|
sort: [{key: 'name', operator: '+'}],
|
||||||
|
title: Ox._('Files'),
|
||||||
|
unique: 'id'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
width: window.innerWidth - (ui.showSidebar * ui.sidebarSize) - 1
|
||||||
|
});
|
||||||
|
|
||||||
|
function getItemFunction(id) {
|
||||||
|
return function(data, width) {
|
||||||
|
var $item = $('<div>')
|
||||||
|
.css({
|
||||||
|
height: '32px',
|
||||||
|
width: width + 'px'
|
||||||
|
})
|
||||||
|
if (!Ox.isEmpty(data)) {
|
||||||
|
$('<img>')
|
||||||
|
.attr({
|
||||||
|
src: '/static/png/oml.png'
|
||||||
|
})
|
||||||
|
.css({
|
||||||
|
position: 'relative',
|
||||||
|
display: 'inline-block',
|
||||||
|
left: '2px',
|
||||||
|
top: '2px',
|
||||||
|
width: '26px',
|
||||||
|
height: '26px',
|
||||||
|
border: '1px solid rgb(192, 192, 192)',
|
||||||
|
backgroundImage: '-webkit-linear-gradient(top, rgb(255, 255, 255), rgb(224, 224, 224))'
|
||||||
|
})
|
||||||
|
.appendTo($item);
|
||||||
|
$('<div>')
|
||||||
|
.css({
|
||||||
|
position: 'relative',
|
||||||
|
left: '34px',
|
||||||
|
top: '-28px',
|
||||||
|
width: width - 36 + 'px',
|
||||||
|
height: '16px',
|
||||||
|
fontSize: '13px',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
overflow: 'hidden',
|
||||||
|
cursor: 'default'
|
||||||
|
})
|
||||||
|
.html(id == 'authors' ? data.name : 'foo')
|
||||||
|
.appendTo($item);
|
||||||
|
$('<div>')
|
||||||
|
.addClass('OxLight')
|
||||||
|
.css({
|
||||||
|
position: 'relative',
|
||||||
|
left: '34px',
|
||||||
|
top: '-28px',
|
||||||
|
width: width - 36 + 'px',
|
||||||
|
height: '12px',
|
||||||
|
fontSize: '9px',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
overflow: 'hidden',
|
||||||
|
cursor: 'default'
|
||||||
|
})
|
||||||
|
.html(id == 'authors' ? Ox.formatCount(data.items, 'Item') : 'bar')
|
||||||
|
.appendTo($item);
|
||||||
|
}
|
||||||
|
return $item;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return that;
|
||||||
|
|
||||||
|
};
|
|
@ -0,0 +1,23 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
oml.ui.confirmDialog = function(options, callback) {
|
||||||
|
|
||||||
|
options = Ox.extend(options, {
|
||||||
|
buttons: options.buttons.map(function($button, index) {
|
||||||
|
return $button
|
||||||
|
.options({id: index ? 'yes' : 'no'})
|
||||||
|
.bindEvent({
|
||||||
|
click: function() {
|
||||||
|
that.close();
|
||||||
|
index && callback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
keys: {enter: 'yes', escape: 'no'}
|
||||||
|
});
|
||||||
|
|
||||||
|
var that = oml.ui.iconDialog(options);
|
||||||
|
|
||||||
|
return that.open();
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
oml.ui.connectionButton = function() {
|
||||||
|
|
||||||
|
var that = Ox.Element({
|
||||||
|
tooltip: Ox._('Disconnected')
|
||||||
|
})
|
||||||
|
.css({
|
||||||
|
marginRight: '3px'
|
||||||
|
})
|
||||||
|
.bindEvent({
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
oml.ui.statusIcon(oml.user.online ? 'connected' : 'disconnected')
|
||||||
|
.css({float: 'left'})
|
||||||
|
.appendTo(that);
|
||||||
|
*/
|
||||||
|
|
||||||
|
Ox.Element()
|
||||||
|
.addClass('OxLight')
|
||||||
|
.css({
|
||||||
|
float: 'left',
|
||||||
|
marginTop: '2px',
|
||||||
|
fontSize: '9px'
|
||||||
|
})
|
||||||
|
.html('↓0K/↑0K')
|
||||||
|
.appendTo(that);
|
||||||
|
|
||||||
|
return that;
|
||||||
|
|
||||||
|
};
|
|
@ -0,0 +1,76 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
oml.ui.errorDialog = function() {
|
||||||
|
|
||||||
|
var ui = oml.user.ui,
|
||||||
|
|
||||||
|
that = oml.ui.iconDialog({
|
||||||
|
buttons: getButtons(),
|
||||||
|
content: Ox.Element(),
|
||||||
|
keys: {enter: 'close', escape: 'close'}
|
||||||
|
})
|
||||||
|
.addClass('OxErrorDialog')
|
||||||
|
.bindEvent({
|
||||||
|
oml_enabledebugmenu: function() {
|
||||||
|
that.options({buttons: getButtons()});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
open = that.open;
|
||||||
|
|
||||||
|
function getButtons() {
|
||||||
|
return (ui.enableDebugMenu ? [
|
||||||
|
Ox.Button({
|
||||||
|
title: Ox._('View Error Logs...')
|
||||||
|
})
|
||||||
|
.bindEvent({
|
||||||
|
click: function() {
|
||||||
|
that.close();
|
||||||
|
oml.UI.set({page: 'errorlogs'});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
] : []).concat([
|
||||||
|
Ox.Button({
|
||||||
|
id: 'close',
|
||||||
|
title: Ox._('Close')
|
||||||
|
})
|
||||||
|
.bindEvent({
|
||||||
|
click: function() {
|
||||||
|
that.close();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
that.open = function() {
|
||||||
|
// on window unload, pending request will time out, so
|
||||||
|
// in order to keep the dialog from appearing, delay it
|
||||||
|
setTimeout(function() {
|
||||||
|
if ($('.OxErrorDialog').length == 0 && !oml.isUnloading) {
|
||||||
|
open();
|
||||||
|
}
|
||||||
|
}, 250);
|
||||||
|
return that;
|
||||||
|
};
|
||||||
|
|
||||||
|
that.update = function(data) {
|
||||||
|
// 0 (timeout) or 500 (error)
|
||||||
|
var error = data.status.code == 0 ? 'a timeout' : 'an error',
|
||||||
|
title = data.status.code == 0 ? 'Timeout' : 'Error';
|
||||||
|
that.options({
|
||||||
|
content: Ox.Element().html(
|
||||||
|
Ox._(
|
||||||
|
'Sorry, {0} occured while handling your request.'
|
||||||
|
+ ' In case this happens repeatedly, you may want to file a bug report.'
|
||||||
|
+ ' Otherwise, please try again later.', [error]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
title: title
|
||||||
|
});
|
||||||
|
return that;
|
||||||
|
}
|
||||||
|
|
||||||
|
return that;
|
||||||
|
|
||||||
|
};
|
|
@ -0,0 +1,192 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
oml.ui.filter = function(id) {
|
||||||
|
|
||||||
|
var ui = oml.user.ui,
|
||||||
|
filter = Ox.getObjectById(oml.config.filters, id),
|
||||||
|
filterIndex = Ox.getIndexById(ui.filters, id),
|
||||||
|
filterSize = oml.getFilterSizes()[filterIndex],
|
||||||
|
|
||||||
|
that = Ox.TableList({
|
||||||
|
_selected: !ui.showFilters
|
||||||
|
? ui._filterState[filterIndex].selected
|
||||||
|
: false,
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
id: 'name',
|
||||||
|
operator: '+',
|
||||||
|
title: Ox._(filter.title),
|
||||||
|
visible: true,
|
||||||
|
width: filterSize - 44 - Ox.UI.SCROLLBAR_SIZE
|
||||||
|
},
|
||||||
|
{
|
||||||
|
align: 'right',
|
||||||
|
format: function(value) {
|
||||||
|
return Ox.formatNumber(value);
|
||||||
|
},
|
||||||
|
id: 'items',
|
||||||
|
operator: '-',
|
||||||
|
title: '#',
|
||||||
|
visible: true,
|
||||||
|
width: 44
|
||||||
|
}
|
||||||
|
],
|
||||||
|
columnsVisible: true,
|
||||||
|
items: function(data, callback) {
|
||||||
|
if (ui.showFilters) {
|
||||||
|
delete data.keys;
|
||||||
|
return oml.api.find(Ox.extend(data, {
|
||||||
|
group: filter.id,
|
||||||
|
query: ui._filterState[filterIndex].find
|
||||||
|
}), callback);
|
||||||
|
} else {
|
||||||
|
callback({
|
||||||
|
data: {items: data.keys ? [] : 0}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scrollbarVisible: true,
|
||||||
|
selected: ui.showFilters
|
||||||
|
? ui._filterState[filterIndex].selected
|
||||||
|
: [],
|
||||||
|
sort: Ox.clone(ui.filters[filterIndex].sort, true),
|
||||||
|
unique: 'name'
|
||||||
|
})
|
||||||
|
.bindEvent({
|
||||||
|
init: function(data) {
|
||||||
|
that.setColumnTitle(
|
||||||
|
'name',
|
||||||
|
Ox._(filter.title)
|
||||||
|
+ '<div class="OxColumnStatus OxLight">'
|
||||||
|
+ Ox.formatNumber(data.items) + '</div>'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
select: function(data) {
|
||||||
|
Ox.print('UI FILTER STATE', ui._filterState)
|
||||||
|
// fixme: cant index be an empty array, instead of -1?
|
||||||
|
// FIXME: this is still incorrect when deselecting a filter item
|
||||||
|
// makes a selected item in another filter disappear
|
||||||
|
var conditions = data.ids.map(function(value) {
|
||||||
|
return {
|
||||||
|
key: id,
|
||||||
|
value: value,
|
||||||
|
operator: '=='
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
index = ui._filterState[filterIndex].index,
|
||||||
|
find = Ox.clone(ui.find, true);
|
||||||
|
if (Ox.isArray(index)) {
|
||||||
|
// this filter had multiple selections and the | query
|
||||||
|
// was on the top level, i.e. not bracketed
|
||||||
|
find = {
|
||||||
|
conditions: conditions,
|
||||||
|
operator: conditions.length > 1 ? '|' : '&'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (index == -1) {
|
||||||
|
// this filter had no selection, i.e. no query
|
||||||
|
index = find.conditions.length;
|
||||||
|
if (find.operator == '|') {
|
||||||
|
find = {
|
||||||
|
conditions: [find],
|
||||||
|
operator: '&'
|
||||||
|
};
|
||||||
|
index = 1;
|
||||||
|
} else {
|
||||||
|
find.operator = '&';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (conditions.length == 0) {
|
||||||
|
// nothing selected
|
||||||
|
find.conditions.splice(index, 1);
|
||||||
|
if (find.conditions.length == 1) {
|
||||||
|
if (find.conditions[0].conditions) {
|
||||||
|
// unwrap single remaining bracketed query
|
||||||
|
find = {
|
||||||
|
conditions: find.conditions[0].conditions,
|
||||||
|
operator: '|'
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
find.operator = '&';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (conditions.length == 1) {
|
||||||
|
// one item selected
|
||||||
|
find.conditions[index] = conditions[0];
|
||||||
|
} else {
|
||||||
|
// multiple items selected
|
||||||
|
if (ui.find.conditions.length == 1) {
|
||||||
|
find = {
|
||||||
|
conditions: conditions,
|
||||||
|
operator: '|'
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
find.conditions[index] = {
|
||||||
|
conditions: conditions,
|
||||||
|
operator: '|'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
oml.UI.set({find: find});
|
||||||
|
oml.updateFilterMenus();
|
||||||
|
},
|
||||||
|
sort: function(data) {
|
||||||
|
var filters = Ox.clone(ui.filters, true);
|
||||||
|
filters[filterIndex].sort = [Ox.clone(data)];
|
||||||
|
oml.UI.set({filters: filters});
|
||||||
|
},
|
||||||
|
oml_find: function() {
|
||||||
|
Ox.print('%%%%', 'RELOADING FILTER')
|
||||||
|
that.reloadList(true);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
$menu = Ox.MenuButton({
|
||||||
|
items: [
|
||||||
|
{id: 'clearFilter', title: Ox._('Clear Filter'), keyboard: 'shift control a'},
|
||||||
|
{id: 'clearFilters', title: Ox._('Clear All Filters'), keyboard: 'shift alt control a'},
|
||||||
|
{},
|
||||||
|
{group: 'filter', max: 1, min: 1, items: oml.config.filters.map(function(filter) {
|
||||||
|
return Ox.extend({checked: filter.id == id}, filter);
|
||||||
|
})}
|
||||||
|
],
|
||||||
|
type: 'image',
|
||||||
|
})
|
||||||
|
.css(Ox.UI.SCROLLBAR_SIZE == 16 ? {
|
||||||
|
right: 0,
|
||||||
|
width: '14px'
|
||||||
|
} : {
|
||||||
|
right: '-1px',
|
||||||
|
width: '8px',
|
||||||
|
})
|
||||||
|
.bindEvent({
|
||||||
|
change: function(data) {
|
||||||
|
|
||||||
|
},
|
||||||
|
click: function(data) {
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.appendTo(that.$bar.$element);
|
||||||
|
|
||||||
|
if (Ox.UI.SCROLLBAR_SIZE < 16) {
|
||||||
|
$($menu.find('input')[0]).css({
|
||||||
|
marginRight: '-3px',
|
||||||
|
marginTop: '1px',
|
||||||
|
width: '8px',
|
||||||
|
height: '8px'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
that.disableMenuItem = function(id) {
|
||||||
|
$menu.disableItem(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
that.enableMenuItem = function(id) {
|
||||||
|
$menu.enableItem(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
return that;
|
||||||
|
|
||||||
|
};
|
|
@ -0,0 +1,26 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
oml.ui.filtersInnerPanel = function() {
|
||||||
|
|
||||||
|
var filterSizes = oml.getFilterSizes(),
|
||||||
|
|
||||||
|
that = Ox.SplitPanel({
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
element: oml.$ui.filters[1],
|
||||||
|
size: filterSizes[1]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: oml.$ui.filters[2]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: oml.$ui.filters[3],
|
||||||
|
size: filterSizes[3]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
orientation: 'horizontal'
|
||||||
|
});
|
||||||
|
|
||||||
|
return that;
|
||||||
|
|
||||||
|
};
|
|
@ -0,0 +1,82 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
oml.ui.filtersOuterPanel = function() {
|
||||||
|
|
||||||
|
var ui = oml.user.ui,
|
||||||
|
|
||||||
|
$filters = oml.$ui.filters = ui.filters.map(function(filter) {
|
||||||
|
return oml.ui.filter(filter.id);
|
||||||
|
}),
|
||||||
|
|
||||||
|
filterSizes = oml.getFilterSizes(),
|
||||||
|
|
||||||
|
that = Ox.SplitPanel({
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
element: $filters[0],
|
||||||
|
size: filterSizes[0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: oml.$ui.filtersInnerPanel = oml.ui.filtersInnerPanel()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: $filters[4],
|
||||||
|
size: filterSizes[4]
|
||||||
|
},
|
||||||
|
],
|
||||||
|
orientation: 'horizontal'
|
||||||
|
})
|
||||||
|
.bindEvent({
|
||||||
|
resize: function() {
|
||||||
|
oml.$ui.filters.forEach(function($filter) {
|
||||||
|
$filter.size();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
resizeend: function(data) {
|
||||||
|
oml.UI.set({filtersSize: data.size});
|
||||||
|
},
|
||||||
|
toggle: function(data) {
|
||||||
|
if (data.collapsed) {
|
||||||
|
oml.$ui.list.gainFocus();
|
||||||
|
}
|
||||||
|
oml.UI.set({showFilters: !data.collapsed});
|
||||||
|
if (!data.collapsed) {
|
||||||
|
oml.$ui.filters.forEach(function($filter) {
|
||||||
|
var selected = $filter.options('_selected');
|
||||||
|
if (selected) {
|
||||||
|
$filter.bindEventOnce({
|
||||||
|
load: function() {
|
||||||
|
$filter.options({
|
||||||
|
_selected: false,
|
||||||
|
selected: selected
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).reloadList();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
oml.updateFilterMenus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
oml.updateFilterMenus();
|
||||||
|
|
||||||
|
that.update = function() {
|
||||||
|
var filterSizes = oml.getFilterSizes();
|
||||||
|
that.size(0, filterSizes[0])
|
||||||
|
.size(2, filterSizes[4]);
|
||||||
|
oml.$ui.filtersInnerPanel
|
||||||
|
.size(0, filterSizes[1])
|
||||||
|
.size(2, filterSizes[3]);
|
||||||
|
oml.$ui.filters.forEach(function($filter, index) {
|
||||||
|
$filter.resizeColumn(
|
||||||
|
'name',
|
||||||
|
filterSizes[index] - 44 - Ox.UI.SCROLLBAR_SIZE
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return that;
|
||||||
|
};
|
||||||
|
|
||||||
|
return that;
|
||||||
|
|
||||||
|
};
|
|
@ -0,0 +1,203 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
oml.ui.findElement = function() {
|
||||||
|
|
||||||
|
var ui = oml.user.ui,
|
||||||
|
findIndex = ui._findState.index,
|
||||||
|
findKey = ui._findState.key,
|
||||||
|
findValue = ui._findState.value,
|
||||||
|
hasPressedClear = false,
|
||||||
|
previousFindKey = findKey,
|
||||||
|
|
||||||
|
that = Ox.FormElementGroup({
|
||||||
|
|
||||||
|
elements: [
|
||||||
|
|
||||||
|
oml.$ui.findScopeSelect = renderFindScopeSelect(),
|
||||||
|
|
||||||
|
oml.$ui.findSelect = Ox.Select({
|
||||||
|
id: 'select',
|
||||||
|
items: [].concat(
|
||||||
|
oml.config.findKeys.map(function(key) {
|
||||||
|
return {
|
||||||
|
id: key.id,
|
||||||
|
title: Ox._('Find: {0}', [Ox._(key.title)])
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
[{}, {
|
||||||
|
id: 'advanced',
|
||||||
|
title: Ox._('Find: Advanced...')
|
||||||
|
}]
|
||||||
|
),
|
||||||
|
overlap: 'right',
|
||||||
|
style: 'squared',
|
||||||
|
value: findKey,
|
||||||
|
width: 160
|
||||||
|
})
|
||||||
|
.bindEvent({
|
||||||
|
change: function(data) {
|
||||||
|
if (data.value == 'advanced') {
|
||||||
|
that.updateElement();
|
||||||
|
//oml.$ui.mainMenu.checkItem('findMenu_find_' + previousFindKey);
|
||||||
|
oml.$ui.filterDialog = oml.ui.filterDialog().open();
|
||||||
|
} else {
|
||||||
|
//oml.$ui.mainMenu.checkItem('findMenu_find_' + data.value);
|
||||||
|
oml.$ui.findInput.options({
|
||||||
|
autocomplete: getAutocomplete(),
|
||||||
|
placeholder: ''
|
||||||
|
}).focusInput(true);
|
||||||
|
previousFindKey = data.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
oml.$ui.findInput = Ox.Input({
|
||||||
|
autocomplete: getAutocomplete(),
|
||||||
|
autocompleteSelect: true,
|
||||||
|
autocompleteSelectHighlight: true,
|
||||||
|
autocompleteSelectMaxWidth: 256,
|
||||||
|
autocompleteSelectSubmit: true,
|
||||||
|
clear: true,
|
||||||
|
clearTooltip: Ox._('Click to clear or doubleclick to reset query'),
|
||||||
|
id: 'input',
|
||||||
|
placeholder: findKey == 'advanced' ? Ox._('Edit Query...') : '',
|
||||||
|
style: 'squared',
|
||||||
|
value: findValue,
|
||||||
|
width: 240
|
||||||
|
})
|
||||||
|
.bindEvent({
|
||||||
|
clear: function() {
|
||||||
|
hasPressedClear = true;
|
||||||
|
},
|
||||||
|
focus: function(data) {
|
||||||
|
if (oml.$ui.findSelect.value() == 'advanced') {
|
||||||
|
if (hasPressedClear) {
|
||||||
|
oml.UI.set({find: oml.site.user.ui.find});
|
||||||
|
that.updateElement();
|
||||||
|
hasPressedClear = false;
|
||||||
|
}
|
||||||
|
oml.$ui.findInput.blurInput();
|
||||||
|
oml.$ui.filterDialog = oml.ui.filterDialog().open();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
submit: function(data) {
|
||||||
|
var scope = oml.$ui.findScopeSelect.value(),
|
||||||
|
key = oml.$ui.findSelect.value(),
|
||||||
|
conditions = [].concat(
|
||||||
|
scope == 'list' ? [{
|
||||||
|
key: 'list',
|
||||||
|
value: ui._list,
|
||||||
|
operator: '=='
|
||||||
|
}] : [],
|
||||||
|
scope == 'user' ? [{
|
||||||
|
key: 'list',
|
||||||
|
value: ui._list.split(':')[0],
|
||||||
|
operator: '=='
|
||||||
|
}] : [],
|
||||||
|
data.value ? [{
|
||||||
|
key: key,
|
||||||
|
value: data.value,
|
||||||
|
operator: '='
|
||||||
|
}] : []
|
||||||
|
);
|
||||||
|
oml.UI.set({
|
||||||
|
find: {
|
||||||
|
conditions: conditions,
|
||||||
|
operator: '&'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.css({
|
||||||
|
float: 'right',
|
||||||
|
margin: '4px 4px 4px 2px'
|
||||||
|
})
|
||||||
|
.bindEvent({
|
||||||
|
oml_find: function() {
|
||||||
|
that.replaceElement(
|
||||||
|
0, oml.$ui.findScopeSelect = renderFindScopeSelect()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function getAutocomplete() {
|
||||||
|
var key = !that
|
||||||
|
? ui._findState.key
|
||||||
|
: that.value()[ui._list ? 1 : 0],
|
||||||
|
findKey = Ox.getObjectById(oml.config.findKeys, key);
|
||||||
|
return findKey && findKey.autocomplete ? function(value, callback) {
|
||||||
|
oml.api.autocomplete({
|
||||||
|
key: key,
|
||||||
|
query: {
|
||||||
|
conditions: ui._list
|
||||||
|
&& oml.$ui.findScopeSelect.value() == 'list'
|
||||||
|
? [{
|
||||||
|
key: 'list',
|
||||||
|
operator: '==',
|
||||||
|
value: ui._list
|
||||||
|
}]
|
||||||
|
: [],
|
||||||
|
operator: '&'
|
||||||
|
},
|
||||||
|
range: [0, 20],
|
||||||
|
sort: findKey.autocompleteSort,
|
||||||
|
value: value
|
||||||
|
}, function(result) {
|
||||||
|
callback(result.data.items.map(function(item) {
|
||||||
|
return Ox.decodeHTMLEntities(item);
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
} : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderFindScopeSelect() {
|
||||||
|
var scope = !ui._list ? 'all'
|
||||||
|
: Ox.endsWith(ui._list, ':') ? 'user'
|
||||||
|
: 'list';
|
||||||
|
return Ox.Select({
|
||||||
|
items: [
|
||||||
|
{id: 'all', title: Ox._('Find: All Libraries')},
|
||||||
|
].concat(scope != 'all' ? [
|
||||||
|
{id: 'user', title: Ox._('Find: This Library')},
|
||||||
|
] : []).concat(scope == 'list' ? [
|
||||||
|
{id: 'list', title: Ox._('Find: This List')}
|
||||||
|
] : []),
|
||||||
|
overlap: 'right',
|
||||||
|
style: 'squared',
|
||||||
|
title: scope == 'all' ? 'data' : scope,
|
||||||
|
type: 'image',
|
||||||
|
tooltip: Ox._('Find: FIXME'),
|
||||||
|
value: scope
|
||||||
|
})
|
||||||
|
.bindEvent({
|
||||||
|
change: function(data) {
|
||||||
|
oml.$ui.findScopeSelect.options({
|
||||||
|
title: data.value == 'all' ? 'data' : data.value,
|
||||||
|
tooltip: data.title
|
||||||
|
});
|
||||||
|
oml.$ui.findInput.focusInput(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
that.updateElement = function() {
|
||||||
|
var findState = ui._findState;
|
||||||
|
oml.$ui.findSelect.value(findState.key);
|
||||||
|
oml.$ui.findInput.options(
|
||||||
|
findState.key == 'advanced' ? {
|
||||||
|
placeholder: Ox._('Edit Query...'),
|
||||||
|
value: ''
|
||||||
|
} : {
|
||||||
|
autocomplete: getAutocomplete(),
|
||||||
|
placeholder: '',
|
||||||
|
value: findState.value
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return that;
|
||||||
|
|
||||||
|
};
|
|
@ -0,0 +1,67 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
oml.ui.folderList = function(options) {
|
||||||
|
|
||||||
|
var ui = oml.user.ui,
|
||||||
|
|
||||||
|
that = Ox.TableList({
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
format: function(value) {
|
||||||
|
return $('<img>')
|
||||||
|
.attr({
|
||||||
|
src: Ox.UI.getImageURL(
|
||||||
|
value == 'libraries' ? 'symbolData'
|
||||||
|
: value == 'library' ? 'symbolUser'
|
||||||
|
: value == 'static' ? 'symbolClick'
|
||||||
|
: 'symbolFind'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.css({
|
||||||
|
width: '10px',
|
||||||
|
height: '10px',
|
||||||
|
margin: '2px -2px 2px 0'
|
||||||
|
});
|
||||||
|
},
|
||||||
|
id: 'type',
|
||||||
|
visible: true,
|
||||||
|
width: 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'name',
|
||||||
|
visible: true,
|
||||||
|
width: ui.sidebarSize - 58,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
align: 'right',
|
||||||
|
format: function(value) {
|
||||||
|
return value > -1
|
||||||
|
? '<span class="OxLight">'
|
||||||
|
+ Ox.formatNumber(value)
|
||||||
|
+ '</span>'
|
||||||
|
: '';
|
||||||
|
},
|
||||||
|
id: 'items',
|
||||||
|
visible: true,
|
||||||
|
width: 42
|
||||||
|
}
|
||||||
|
],
|
||||||
|
draggable: options.draggable,
|
||||||
|
items: options.items,
|
||||||
|
sort: [{key: 'index', operator: '+'}],
|
||||||
|
sortable: options.sortable,
|
||||||
|
selected: [],
|
||||||
|
unique: 'id'
|
||||||
|
})
|
||||||
|
.css({
|
||||||
|
width: ui.sidebarSize + 'px',
|
||||||
|
height: '16px'
|
||||||
|
});
|
||||||
|
|
||||||
|
that.resizeElement = function() {
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
|
||||||
|
return that;
|
||||||
|
|
||||||
|
};
|
|
@ -0,0 +1,20 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// FIXME: UNUSED
|
||||||
|
|
||||||
|
oml.ui.folderPlaceholder = function(text) {
|
||||||
|
|
||||||
|
var that = Ox.Element()
|
||||||
|
.addClass('OxLight')
|
||||||
|
.css({
|
||||||
|
height: '14px',
|
||||||
|
padding: '1px 4px',
|
||||||
|
});
|
||||||
|
|
||||||
|
that.updateText = function(text) {
|
||||||
|
return that.html(text);
|
||||||
|
};
|
||||||
|
|
||||||
|
return that.updateText(text);
|
||||||
|
|
||||||
|
};
|
|
@ -0,0 +1,286 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
oml.ui.folders = function() {
|
||||||
|
|
||||||
|
var ui = oml.user.ui,
|
||||||
|
|
||||||
|
userIndex = {},
|
||||||
|
|
||||||
|
$lists = [],
|
||||||
|
|
||||||
|
that = Ox.Element()
|
||||||
|
.css({
|
||||||
|
//overflowX: 'hidden',
|
||||||
|
//overflowY: 'auto',
|
||||||
|
})
|
||||||
|
.bindEvent({
|
||||||
|
oml_find: selectList
|
||||||
|
});
|
||||||
|
|
||||||
|
$lists.push(
|
||||||
|
oml.$ui.librariesList = oml.ui.folderList({
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: '',
|
||||||
|
name: Ox._('All Libraries'),
|
||||||
|
type: 'libraries',
|
||||||
|
items: -1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.bindEvent({
|
||||||
|
load: function() {
|
||||||
|
oml.api.find({query: getFind()}, function(result) {
|
||||||
|
oml.$ui.librariesList.value('', 'items', result.data.items);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
open: function() {
|
||||||
|
|
||||||
|
},
|
||||||
|
select: function() {
|
||||||
|
oml.UI.set({find: getFind('')});
|
||||||
|
oml.$ui.librariesList.options({selected: ['']});
|
||||||
|
},
|
||||||
|
selectnext: function() {
|
||||||
|
oml.UI.set(Ox.extend(
|
||||||
|
{find: getFind(':')},
|
||||||
|
'showFolder.' + oml.user.preferences.username,
|
||||||
|
true
|
||||||
|
));
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.css({height: '16px'})
|
||||||
|
.appendTo(that)
|
||||||
|
);
|
||||||
|
oml.$ui.librariesList.$body.css({height: '16px'}); // FIXME!
|
||||||
|
|
||||||
|
oml.$ui.folder = [];
|
||||||
|
oml.$ui.libraryList = [];
|
||||||
|
oml.$ui.folderList = [];
|
||||||
|
|
||||||
|
oml.api.getUsers(function(result) {
|
||||||
|
|
||||||
|
var peers = result.data.users.filter(function(user) {
|
||||||
|
return user.peered;
|
||||||
|
});
|
||||||
|
|
||||||
|
oml.api.getLists(function(result) {
|
||||||
|
|
||||||
|
Ox.print('GOT LISTS', result.data);
|
||||||
|
|
||||||
|
var users = [
|
||||||
|
{
|
||||||
|
id: oml.user.id,
|
||||||
|
nickname: oml.user.preferences.username,
|
||||||
|
online: oml.user.online
|
||||||
|
}
|
||||||
|
].concat(peers),
|
||||||
|
|
||||||
|
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'})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
],
|
||||||
|
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 + 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() {
|
||||||
|
// ...
|
||||||
|
},
|
||||||
|
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)});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.bindEvent(function(data, event) {
|
||||||
|
if (!index) {
|
||||||
|
Ox.print('LIST EVENT', event, data);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.css({height: lists[user.id].length * 16 + 'px'})
|
||||||
|
.appendTo($content)
|
||||||
|
);
|
||||||
|
|
||||||
|
oml.$ui.folderList[index].$body.css({top: '16px'});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
selectList();
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function getFind(list) {
|
||||||
|
return {
|
||||||
|
conditions: list ? [{
|
||||||
|
key: 'list',
|
||||||
|
operator: '==',
|
||||||
|
value: list
|
||||||
|
}] : [],
|
||||||
|
operator: '&'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectList() {
|
||||||
|
var split = ui._list.split(':'),
|
||||||
|
index = userIndex[split[0] || oml.user.preferences.username],
|
||||||
|
list = split[1],
|
||||||
|
$selectedList = !ui._list ? oml.$ui.librariesList
|
||||||
|
: !list ? oml.$ui[!list ? 'libraryList' : 'folderList'][index]
|
||||||
|
: oml.$ui.folderList[index];
|
||||||
|
$lists.forEach(function($list) {
|
||||||
|
if ($list == $selectedList) {
|
||||||
|
$list.options({selected: [ui._list]}).gainFocus();
|
||||||
|
} else {
|
||||||
|
$list.options({selected: []})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return that;
|
||||||
|
|
||||||
|
};
|
|
@ -0,0 +1,34 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
oml.ui.fullscreenButton = function() {
|
||||||
|
|
||||||
|
var ui = oml.user.ui,
|
||||||
|
|
||||||
|
that = Ox.Button({
|
||||||
|
style: 'squared',
|
||||||
|
title: 'grow',
|
||||||
|
tooltip: Ox._('Enter Fullscreen'),
|
||||||
|
type: 'image'
|
||||||
|
})
|
||||||
|
.css({
|
||||||
|
float: 'left',
|
||||||
|
margin: '4px 2px'
|
||||||
|
})
|
||||||
|
.bindEvent({
|
||||||
|
click: function() {
|
||||||
|
Ox.Fullscreen.enter(oml.$ui.viewer.find('iframe')[0]);
|
||||||
|
},
|
||||||
|
oml_itemview: function() {
|
||||||
|
that.update();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
that.update = function() {
|
||||||
|
return that.options({
|
||||||
|
disabled: ui.itemView != 'book'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return that.update();
|
||||||
|
|
||||||
|
};
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue