mirror of
https://github.com/janeczku/calibre-web
synced 2024-11-18 03:25:37 +00:00
commit
4c030700fb
9
.editorconfig
Normal file
9
.editorconfig
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
indent_style = space
|
||||||
|
|
||||||
|
[*.{js,py}]
|
||||||
|
indent_size = 4
|
96
.eslintrc
Normal file
96
.eslintrc
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"jquery": true
|
||||||
|
},
|
||||||
|
"globals": {
|
||||||
|
"alert": true
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"arrow-parens": 2,
|
||||||
|
"block-scoped-var": 1,
|
||||||
|
"brace-style": 2,
|
||||||
|
"camelcase": 1,
|
||||||
|
"comma-spacing": 2,
|
||||||
|
"curly": [2, "multi-line", "consistent"],
|
||||||
|
"eqeqeq": 2,
|
||||||
|
"indent": [
|
||||||
|
2,
|
||||||
|
4,
|
||||||
|
{
|
||||||
|
"SwitchCase": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"keyword-spacing": 2,
|
||||||
|
"linebreak-style": 2,
|
||||||
|
"new-cap": 2,
|
||||||
|
"no-dupe-args": 2,
|
||||||
|
"no-dupe-class-members": 2,
|
||||||
|
"no-dupe-keys": 2,
|
||||||
|
"no-duplicate-case": 2,
|
||||||
|
"no-caller": 2,
|
||||||
|
"no-class-assign": 2,
|
||||||
|
"no-cond-assign": 2,
|
||||||
|
"no-const-assign": 2,
|
||||||
|
"no-console": 2,
|
||||||
|
"no-debugger": 2,
|
||||||
|
"no-delete-var": 2,
|
||||||
|
"no-empty": 2,
|
||||||
|
"no-eval": 2,
|
||||||
|
"no-extend-native": 2,
|
||||||
|
"no-extra-boolean-cast": 2,
|
||||||
|
"no-extra-semi": 2,
|
||||||
|
"no-fallthrough": [
|
||||||
|
2,
|
||||||
|
{
|
||||||
|
"commentPattern": "break[\\s\\w]*omitted"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"no-implied-eval": 2,
|
||||||
|
"no-invalid-regexp": 2,
|
||||||
|
"no-irregular-whitespace": 2,
|
||||||
|
"no-iterator": 2,
|
||||||
|
"no-loop-func": 2,
|
||||||
|
"no-mixed-operators": 2,
|
||||||
|
"no-mixed-spaces-and-tabs": 2,
|
||||||
|
"no-multi-str": 2,
|
||||||
|
"no-new": 2,
|
||||||
|
"no-obj-calls": 2,
|
||||||
|
"no-octal": 2,
|
||||||
|
"no-redeclare": 2,
|
||||||
|
"no-regex-spaces": 2,
|
||||||
|
"no-script-url": 2,
|
||||||
|
"no-sparse-arrays": 2,
|
||||||
|
"no-undef": 2,
|
||||||
|
"no-undefined": 2,
|
||||||
|
"no-unreachable": 2,
|
||||||
|
"no-unsafe-negation": 2,
|
||||||
|
"no-unused-vars": 2,
|
||||||
|
"no-use-before-define": [
|
||||||
|
2,
|
||||||
|
{
|
||||||
|
"classes": false,
|
||||||
|
"functions": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"quotes": [
|
||||||
|
2,
|
||||||
|
"double"
|
||||||
|
],
|
||||||
|
"require-yield": 2,
|
||||||
|
"semi": [
|
||||||
|
2,
|
||||||
|
"always"
|
||||||
|
],
|
||||||
|
"space-before-blocks": 2,
|
||||||
|
"space-infix-ops": 2,
|
||||||
|
"space-unary-ops": 2,
|
||||||
|
"use-isnan": 2,
|
||||||
|
"valid-typeof": 2,
|
||||||
|
"wrap-iife": [
|
||||||
|
2,
|
||||||
|
"any"
|
||||||
|
],
|
||||||
|
"yield-star-spacing": 2
|
||||||
|
}
|
||||||
|
}
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -27,5 +27,4 @@ tags
|
|||||||
settings.yaml
|
settings.yaml
|
||||||
gdrive_credentials
|
gdrive_credentials
|
||||||
|
|
||||||
#kindlegen
|
vendor
|
||||||
vendor/kindlegen
|
|
||||||
|
60
cps/cache_buster.py
Normal file
60
cps/cache_buster.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
# Inspired by https://github.com/ChrisTM/Flask-CacheBust
|
||||||
|
# Uses query strings so CSS font files are found without having to resort to absolute URLs
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
def init_cache_busting(app):
|
||||||
|
"""
|
||||||
|
Configure `app` to so that `url_for` adds a unique query string to URLs generated
|
||||||
|
for the `'static'` endpoint.
|
||||||
|
|
||||||
|
This allows setting long cache expiration values on static resources
|
||||||
|
because whenever the resource changes, so does its URL.
|
||||||
|
"""
|
||||||
|
|
||||||
|
static_folder = os.path.join(app.static_folder, '') # path to the static file folder, with trailing slash
|
||||||
|
|
||||||
|
hash_table = {} # map of file hashes
|
||||||
|
|
||||||
|
app.logger.debug('Computing cache-busting values...')
|
||||||
|
# compute file hashes
|
||||||
|
for dirpath, dirnames, filenames in os.walk(static_folder):
|
||||||
|
for filename in filenames:
|
||||||
|
# compute version component
|
||||||
|
rooted_filename = os.path.join(dirpath, filename)
|
||||||
|
with open(rooted_filename, 'r') as f:
|
||||||
|
file_hash = hashlib.md5(f.read()).hexdigest()[:7]
|
||||||
|
|
||||||
|
# save version to tables
|
||||||
|
file_path = rooted_filename.replace(static_folder, "")
|
||||||
|
file_path = file_path.replace("\\", "/") # Convert Windows path to web path
|
||||||
|
hash_table[file_path] = file_hash
|
||||||
|
app.logger.debug('Finished computing cache-busting values')
|
||||||
|
|
||||||
|
def bust_filename(filename):
|
||||||
|
return hash_table.get(filename, "")
|
||||||
|
|
||||||
|
def unbust_filename(filename):
|
||||||
|
return filename.split("?", 1)[0]
|
||||||
|
|
||||||
|
@app.url_defaults
|
||||||
|
def reverse_to_cache_busted_url(endpoint, values):
|
||||||
|
"""
|
||||||
|
Make `url_for` produce busted filenames when using the 'static' endpoint.
|
||||||
|
"""
|
||||||
|
if endpoint == "static":
|
||||||
|
file_hash = bust_filename(values["filename"])
|
||||||
|
if file_hash:
|
||||||
|
values["q"] = file_hash
|
||||||
|
|
||||||
|
def debusting_static_view(filename):
|
||||||
|
"""
|
||||||
|
Serve a request for a static file having a busted name.
|
||||||
|
"""
|
||||||
|
return original_static_view(filename=unbust_filename(filename))
|
||||||
|
|
||||||
|
# Replace the default static file view with our debusting view.
|
||||||
|
original_static_view = app.view_functions["static"]
|
||||||
|
app.view_functions["static"] = debusting_static_view
|
23
cps/epub.py
23
cps/epub.py
@ -43,13 +43,16 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
|
|||||||
|
|
||||||
epub_metadata = {}
|
epub_metadata = {}
|
||||||
|
|
||||||
for s in ['title', 'description', 'creator', 'language']:
|
for s in ['title', 'description', 'creator', 'language', 'subject']:
|
||||||
tmp = p.xpath('dc:%s/text()' % s, namespaces=ns)
|
tmp = p.xpath('dc:%s/text()' % s, namespaces=ns)
|
||||||
if len(tmp) > 0:
|
if len(tmp) > 0:
|
||||||
epub_metadata[s] = p.xpath('dc:%s/text()' % s, namespaces=ns)[0]
|
epub_metadata[s] = p.xpath('dc:%s/text()' % s, namespaces=ns)[0]
|
||||||
else:
|
else:
|
||||||
epub_metadata[s] = "Unknown"
|
epub_metadata[s] = "Unknown"
|
||||||
|
|
||||||
|
if epub_metadata['subject'] == "Unknown":
|
||||||
|
epub_metadata['subject'] = ''
|
||||||
|
|
||||||
if epub_metadata['description'] == "Unknown":
|
if epub_metadata['description'] == "Unknown":
|
||||||
description = tree.xpath("//*[local-name() = 'description']/text()")
|
description = tree.xpath("//*[local-name() = 'description']/text()")
|
||||||
if len(description) > 0:
|
if len(description) > 0:
|
||||||
@ -68,6 +71,18 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
|
|||||||
else:
|
else:
|
||||||
epub_metadata['language'] = ""
|
epub_metadata['language'] = ""
|
||||||
|
|
||||||
|
series = tree.xpath("/pkg:package/pkg:metadata/pkg:meta[@name='calibre:series']/@content", namespaces=ns)
|
||||||
|
if len(series) > 0:
|
||||||
|
epub_metadata['series'] = series[0]
|
||||||
|
else:
|
||||||
|
epub_metadata['series'] = ''
|
||||||
|
|
||||||
|
series_id = tree.xpath("/pkg:package/pkg:metadata/pkg:meta[@name='calibre:series_index']/@content", namespaces=ns)
|
||||||
|
if len(series_id) > 0:
|
||||||
|
epub_metadata['series_id'] = series_id[0]
|
||||||
|
else:
|
||||||
|
epub_metadata['series_id'] = '1'
|
||||||
|
|
||||||
coversection = tree.xpath("/pkg:package/pkg:manifest/pkg:item[@id='cover-image']/@href", namespaces=ns)
|
coversection = tree.xpath("/pkg:package/pkg:manifest/pkg:item[@id='cover-image']/@href", namespaces=ns)
|
||||||
coverfile = None
|
coverfile = None
|
||||||
if len(coversection) > 0:
|
if len(coversection) > 0:
|
||||||
@ -101,7 +116,7 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
|
|||||||
author=epub_metadata['creator'].encode('utf-8').decode('utf-8'),
|
author=epub_metadata['creator'].encode('utf-8').decode('utf-8'),
|
||||||
cover=coverfile,
|
cover=coverfile,
|
||||||
description=epub_metadata['description'],
|
description=epub_metadata['description'],
|
||||||
tags="",
|
tags=epub_metadata['subject'].encode('utf-8').decode('utf-8'),
|
||||||
series="",
|
series=epub_metadata['series'].encode('utf-8').decode('utf-8'),
|
||||||
series_id="",
|
series_id=epub_metadata['series_id'].encode('utf-8').decode('utf-8'),
|
||||||
languages=epub_metadata['language'])
|
languages=epub_metadata['language'])
|
||||||
|
@ -278,7 +278,7 @@ def get_valid_filename(value, replace_whitespace=True):
|
|||||||
else:
|
else:
|
||||||
value = unicode(re_slugify.sub('', value).strip())
|
value = unicode(re_slugify.sub('', value).strip())
|
||||||
if replace_whitespace:
|
if replace_whitespace:
|
||||||
#*+:\"/<>? werden durch _ ersetzt
|
#*+:\"/<>? are replaced by _
|
||||||
value = re.sub('[\*\+:\\\"/<>\?]+', u'_', value, flags=re.U)
|
value = re.sub('[\*\+:\\\"/<>\?]+', u'_', value, flags=re.U)
|
||||||
|
|
||||||
value = value[:128]
|
value = value[:128]
|
||||||
|
@ -55,10 +55,38 @@ span.glyphicon.glyphicon-tags {padding-right: 5px;color: #999;vertical-align: te
|
|||||||
.block-label {display: block;}
|
.block-label {display: block;}
|
||||||
.fake-input {position: absolute; pointer-events: none; top: 0;}
|
.fake-input {position: absolute; pointer-events: none; top: 0;}
|
||||||
|
|
||||||
|
input.pill { position: absolute; opacity: 0; }
|
||||||
|
input.pill + label {
|
||||||
|
border: 2px solid #45b29d;
|
||||||
|
border-radius: 15px;
|
||||||
|
color: #45b29d;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 3px 15px;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
input.pill:checked + label {
|
||||||
|
background-color: #45b29d;
|
||||||
|
border-color: #fff;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
input.pill:not(:checked) + label .glyphicon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.author-bio img {margin: 0 1em 1em 0;}
|
.author-bio img {margin: 0 1em 1em 0;}
|
||||||
.author-link img {display: inline-block;max-width: 100px;}
|
.author-link {display: inline-block; margin-top: 10px; width: 100px;}
|
||||||
|
.author-link img {display: block; height: 100%;}
|
||||||
|
|
||||||
#remove-from-shelves .btn,
|
#remove-from-shelves .btn,
|
||||||
#shelf-action-errors {
|
#shelf-action-errors {
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tags_click, .serie_click, .language_click {margin-right: 5px;}
|
||||||
|
|
||||||
|
#meta-info img { max-height: 150px; max-width: 100px; cursor: pointer; }
|
||||||
|
|
||||||
|
.padded-bottom { margin-bottom: 15px; }
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
$( document ).ready(function() {
|
/* global _ */
|
||||||
|
|
||||||
|
$(function() {
|
||||||
$("#have_read_form").ajaxForm();
|
$("#have_read_form").ajaxForm();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -6,34 +8,51 @@ $("#have_read_cb").on("change", function() {
|
|||||||
$(this).closest("form").submit();
|
$(this).closest("form").submit();
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#shelf-actions").on("click", "[data-shelf-action]", function (e) {
|
(function() {
|
||||||
|
var templates = {
|
||||||
|
add: _.template(
|
||||||
|
$("#template-shelf-add").html()
|
||||||
|
),
|
||||||
|
remove: _.template(
|
||||||
|
$("#template-shelf-remove").html()
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
$("#shelf-actions").on("click", "[data-shelf-action]", function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
$.get(this.href)
|
$.get(this.href)
|
||||||
.done(() => {
|
.done(function() {
|
||||||
const $this = $(this);
|
var $this = $(this);
|
||||||
switch ($this.data("shelf-action")) {
|
switch ($this.data("shelf-action")) {
|
||||||
case "add":
|
case "add":
|
||||||
$("#remove-from-shelves").append(`<a href="${$this.data("remove-href")}"
|
$("#remove-from-shelves").append(
|
||||||
data-add-href="${this.href}"
|
templates.remove({
|
||||||
class="btn btn-sm btn-default" data-shelf-action="remove"
|
add: this.href,
|
||||||
><span class="glyphicon glyphicon-remove"></span> ${this.textContent}</a>`);
|
remove: $this.data("remove-href"),
|
||||||
|
content: this.textContent
|
||||||
|
})
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case "remove":
|
case "remove":
|
||||||
$("#add-to-shelves").append(`<li><a href="${$this.data("add-href")}"
|
$("#add-to-shelves").append(
|
||||||
data-remove-href="${this.href}"
|
templates.add({
|
||||||
data-shelf-action="add"
|
add: $this.data("add-href"),
|
||||||
>${this.textContent}</a></li>`);
|
remove: this.href,
|
||||||
|
content: this.textContent
|
||||||
|
})
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
this.parentNode.removeChild(this);
|
this.parentNode.removeChild(this);
|
||||||
})
|
}.bind(this))
|
||||||
.fail((xhr) => {
|
.fail(function(xhr) {
|
||||||
const $msg = $("<span/>", { "class": "text-danger"}).text(xhr.responseText);
|
var $msg = $("<span/>", { "class": "text-danger"}).text(xhr.responseText);
|
||||||
$("#shelf-action-status").html($msg);
|
$("#shelf-action-status").html($msg);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(function() {
|
||||||
$msg.remove();
|
$msg.remove();
|
||||||
}, 10000);
|
}, 10000);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
})();
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
/* global Bloodhound, language, Modernizr, tinymce */
|
/* global Bloodhound, language, Modernizr, tinymce */
|
||||||
|
|
||||||
|
if ($("#description").length) {
|
||||||
tinymce.init({
|
tinymce.init({
|
||||||
selector: "#description",
|
selector: "#description",
|
||||||
branding: false,
|
branding: false,
|
||||||
@ -10,6 +11,7 @@ tinymce.init({
|
|||||||
language
|
language
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
if (!Modernizr.inputtypes.date) {
|
if (!Modernizr.inputtypes.date) {
|
||||||
$("#pubdate").datepicker({
|
$("#pubdate").datepicker({
|
||||||
format: "yyyy-mm-dd",
|
format: "yyyy-mm-dd",
|
||||||
@ -26,13 +28,13 @@ if (!Modernizr.inputtypes.date) {
|
|||||||
.removeClass("hidden");
|
.removeClass("hidden");
|
||||||
}).trigger("change");
|
}).trigger("change");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
/*
|
/*
|
||||||
Takes a prefix, query typeahead callback, Bloodhound typeahead adapter
|
Takes a prefix, query typeahead callback, Bloodhound typeahead adapter
|
||||||
and returns the completions it gets from the bloodhound engine prefixed.
|
and returns the completions it gets from the bloodhound engine prefixed.
|
||||||
*/
|
*/
|
||||||
function prefixedSource(prefix, query, cb, bhAdapter) {
|
function prefixedSource(prefix, query, cb, bhAdapter) {
|
||||||
bhAdapter(query, function(retArray){
|
bhAdapter(query, function(retArray) {
|
||||||
var matches = [];
|
var matches = [];
|
||||||
for (var i = 0; i < retArray.length; i++) {
|
for (var i = 0; i < retArray.length; i++) {
|
||||||
var obj = {name : prefix + retArray[i].name};
|
var obj = {name : prefix + retArray[i].name};
|
||||||
@ -41,7 +43,7 @@ function prefixedSource(prefix, query, cb, bhAdapter) {
|
|||||||
cb(matches);
|
cb(matches);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function getPath(){
|
function getPath() {
|
||||||
var jsFileLocation = $("script[src*=edit_books]").attr("src"); // the js file path
|
var jsFileLocation = $("script[src*=edit_books]").attr("src"); // the js file path
|
||||||
jsFileLocation = jsFileLocation.replace("/static/js/edit_books.js", ""); // the js folder path
|
jsFileLocation = jsFileLocation.replace("/static/js/edit_books.js", ""); // the js folder path
|
||||||
return jsFileLocation;
|
return jsFileLocation;
|
||||||
@ -49,7 +51,7 @@ function getPath(){
|
|||||||
|
|
||||||
var authors = new Bloodhound({
|
var authors = new Bloodhound({
|
||||||
name: "authors",
|
name: "authors",
|
||||||
datumTokenizer(datum) {
|
datumTokenizer: function datumTokenizer(datum) {
|
||||||
return [datum.name];
|
return [datum.name];
|
||||||
},
|
},
|
||||||
queryTokenizer: Bloodhound.tokenizers.whitespace,
|
queryTokenizer: Bloodhound.tokenizers.whitespace,
|
||||||
@ -60,15 +62,15 @@ var authors = new Bloodhound({
|
|||||||
|
|
||||||
var series = new Bloodhound({
|
var series = new Bloodhound({
|
||||||
name: "series",
|
name: "series",
|
||||||
datumTokenizer(datum) {
|
datumTokenizer: function datumTokenizer(datum) {
|
||||||
return [datum.name];
|
return [datum.name];
|
||||||
},
|
},
|
||||||
queryTokenizer(query) {
|
queryTokenizer: function queryTokenizer(query) {
|
||||||
return [query];
|
return [query];
|
||||||
},
|
},
|
||||||
remote: {
|
remote: {
|
||||||
url: getPath()+"/get_series_json?q=",
|
url: getPath()+"/get_series_json?q=",
|
||||||
replace(url, query) {
|
replace: function replace(url, query) {
|
||||||
return url+encodeURIComponent(query);
|
return url+encodeURIComponent(query);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -77,10 +79,10 @@ var series = new Bloodhound({
|
|||||||
|
|
||||||
var tags = new Bloodhound({
|
var tags = new Bloodhound({
|
||||||
name: "tags",
|
name: "tags",
|
||||||
datumTokenizer(datum) {
|
datumTokenizer: function datumTokenizer(datum) {
|
||||||
return [datum.name];
|
return [datum.name];
|
||||||
},
|
},
|
||||||
queryTokenizer(query) {
|
queryTokenizer: function queryTokenizer(query) {
|
||||||
var tokens = query.split(",");
|
var tokens = query.split(",");
|
||||||
tokens = [tokens[tokens.length-1].trim()];
|
tokens = [tokens[tokens.length-1].trim()];
|
||||||
return tokens;
|
return tokens;
|
||||||
@ -92,15 +94,15 @@ var tags = new Bloodhound({
|
|||||||
|
|
||||||
var languages = new Bloodhound({
|
var languages = new Bloodhound({
|
||||||
name: "languages",
|
name: "languages",
|
||||||
datumTokenizer(datum) {
|
datumTokenizer: function datumTokenizer(datum) {
|
||||||
return [datum.name];
|
return [datum.name];
|
||||||
},
|
},
|
||||||
queryTokenizer(query) {
|
queryTokenizer: function queryTokenizer(query) {
|
||||||
return [query];
|
return [query];
|
||||||
},
|
},
|
||||||
remote: {
|
remote: {
|
||||||
url: getPath()+"/get_languages_json?q=",
|
url: getPath()+"/get_languages_json?q=",
|
||||||
replace(url, query) {
|
replace: function replace(url, query) {
|
||||||
return url+encodeURIComponent(query);
|
return url+encodeURIComponent(query);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -115,9 +117,9 @@ function sourceSplit(query, cb, split, source) {
|
|||||||
tokens.splice(tokens.length-1, 1); // remove last element
|
tokens.splice(tokens.length-1, 1); // remove last element
|
||||||
var prefix = "";
|
var prefix = "";
|
||||||
var newSplit;
|
var newSplit;
|
||||||
if (split === "&"){
|
if (split === "&") {
|
||||||
newSplit = " " + split + " ";
|
newSplit = " " + split + " ";
|
||||||
}else{
|
} else {
|
||||||
newSplit = split + " ";
|
newSplit = split + " ";
|
||||||
}
|
}
|
||||||
for (var i = 0; i < tokens.length; i++) {
|
for (var i = 0; i < tokens.length; i++) {
|
||||||
@ -127,7 +129,7 @@ function sourceSplit(query, cb, split, source) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var promiseAuthors = authors.initialize();
|
var promiseAuthors = authors.initialize();
|
||||||
promiseAuthors.done(function(){
|
promiseAuthors.done(function() {
|
||||||
$("#bookAuthor").typeahead(
|
$("#bookAuthor").typeahead(
|
||||||
{
|
{
|
||||||
highlight: true, minLength: 1,
|
highlight: true, minLength: 1,
|
||||||
@ -135,14 +137,15 @@ var promiseAuthors = authors.initialize();
|
|||||||
}, {
|
}, {
|
||||||
name: "authors",
|
name: "authors",
|
||||||
displayKey: "name",
|
displayKey: "name",
|
||||||
source(query, cb){
|
source: function source(query, cb) {
|
||||||
return sourceSplit(query, cb, "&", authors); //sourceSplit //("&")
|
return sourceSplit(query, cb, "&", authors); //sourceSplit //("&")
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
var promiseSeries = series.initialize();
|
var promiseSeries = series.initialize();
|
||||||
promiseSeries.done(function(){
|
promiseSeries.done(function() {
|
||||||
$("#series").typeahead(
|
$("#series").typeahead(
|
||||||
{
|
{
|
||||||
highlight: true, minLength: 0,
|
highlight: true, minLength: 0,
|
||||||
@ -156,7 +159,7 @@ var promiseSeries = series.initialize();
|
|||||||
});
|
});
|
||||||
|
|
||||||
var promiseTags = tags.initialize();
|
var promiseTags = tags.initialize();
|
||||||
promiseTags.done(function(){
|
promiseTags.done(function() {
|
||||||
$("#tags").typeahead(
|
$("#tags").typeahead(
|
||||||
{
|
{
|
||||||
highlight: true, minLength: 0,
|
highlight: true, minLength: 0,
|
||||||
@ -164,14 +167,15 @@ var promiseTags = tags.initialize();
|
|||||||
}, {
|
}, {
|
||||||
name: "tags",
|
name: "tags",
|
||||||
displayKey: "name",
|
displayKey: "name",
|
||||||
source(query, cb){
|
source: function source(query, cb) {
|
||||||
return sourceSplit(query, cb, ",", tags);
|
return sourceSplit(query, cb, ",", tags);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
});
|
);
|
||||||
|
});
|
||||||
|
|
||||||
var promiseLanguages = languages.initialize();
|
var promiseLanguages = languages.initialize();
|
||||||
promiseLanguages.done(function(){
|
promiseLanguages.done(function() {
|
||||||
$("#languages").typeahead(
|
$("#languages").typeahead(
|
||||||
{
|
{
|
||||||
highlight: true, minLength: 0,
|
highlight: true, minLength: 0,
|
||||||
@ -179,13 +183,14 @@ var promiseLanguages = languages.initialize();
|
|||||||
}, {
|
}, {
|
||||||
name: "languages",
|
name: "languages",
|
||||||
displayKey: "name",
|
displayKey: "name",
|
||||||
source(query, cb){
|
source: function source(query, cb) {
|
||||||
return sourceSplit(query, cb, ",", languages); //(",")
|
return sourceSplit(query, cb, ",", languages); //(",")
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
});
|
);
|
||||||
|
});
|
||||||
|
|
||||||
$("#search").on("change input.typeahead:selected", function(){
|
$("#search").on("change input.typeahead:selected", function() {
|
||||||
var form = $("form").serialize();
|
var form = $("form").serialize();
|
||||||
$.getJSON( getPath()+"/get_matching_tags", form, function( data ) {
|
$.getJSON( getPath()+"/get_matching_tags", form, function( data ) {
|
||||||
$(".tags_click").each(function() {
|
$(".tags_click").each(function() {
|
||||||
@ -193,8 +198,7 @@ $("#search").on("change input.typeahead:selected", function(){
|
|||||||
if (!($(this).hasClass("active"))) {
|
if (!($(this).hasClass("active"))) {
|
||||||
$(this).addClass("disabled");
|
$(this).addClass("disabled");
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
$(this).removeClass("disabled");
|
$(this).removeClass("disabled");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -4,11 +4,11 @@
|
|||||||
* Google Books api document: https://developers.google.com/books/docs/v1/using
|
* Google Books api document: https://developers.google.com/books/docs/v1/using
|
||||||
* Douban Books api document: https://developers.douban.com/wiki/?title=book_v2 (Chinese Only)
|
* Douban Books api document: https://developers.douban.com/wiki/?title=book_v2 (Chinese Only)
|
||||||
*/
|
*/
|
||||||
/* global i18nMsg, tinymce */
|
/* global _, i18nMsg, tinymce */
|
||||||
var dbResults = [];
|
var dbResults = [];
|
||||||
var ggResults = [];
|
var ggResults = [];
|
||||||
|
|
||||||
$(document).ready(function () {
|
$(function () {
|
||||||
var msg = i18nMsg;
|
var msg = i18nMsg;
|
||||||
var douban = "https://api.douban.com";
|
var douban = "https://api.douban.com";
|
||||||
var dbSearch = "/v2/book/search";
|
var dbSearch = "/v2/book/search";
|
||||||
@ -22,113 +22,138 @@ $(document).ready(function () {
|
|||||||
var ggDone = false;
|
var ggDone = false;
|
||||||
|
|
||||||
var showFlag = 0;
|
var showFlag = 0;
|
||||||
String.prototype.replaceAll = function (s1, s2) {
|
|
||||||
return this.replace(new RegExp(s1, "gm"), s2);
|
var templates = {
|
||||||
|
bookResult: _.template(
|
||||||
|
$("#template-book-result").html()
|
||||||
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function populateForm (book) {
|
||||||
|
tinymce.get("description").setContent(book.description);
|
||||||
|
$("#bookAuthor").val(book.authors);
|
||||||
|
$("#book_title").val(book.title);
|
||||||
|
$("#tags").val(book.tags.join(","));
|
||||||
|
$("#rating").data("rating").setValue(Math.round(book.rating));
|
||||||
|
$(".cover img").attr("src", book.cover);
|
||||||
|
$("#cover_url").val(book.cover);
|
||||||
|
}
|
||||||
|
|
||||||
function showResult () {
|
function showResult () {
|
||||||
var book;
|
|
||||||
var i;
|
|
||||||
var bookHtml;
|
|
||||||
showFlag++;
|
showFlag++;
|
||||||
if (showFlag === 1) {
|
if (showFlag === 1) {
|
||||||
$("#metaModal #meta-info").html("<ul id=\"book-list\" class=\"media-list\"></ul>");
|
$("#meta-info").html("<ul id=\"book-list\" class=\"media-list\"></ul>");
|
||||||
}
|
}
|
||||||
if (ggDone && dbDone) {
|
if (ggDone && dbDone) {
|
||||||
if (!ggResults && !dbResults) {
|
if (!ggResults && !dbResults) {
|
||||||
$("#metaModal #meta-info").html("<p class=\"text-danger\">"+ msg.no_result +"</p>");
|
$("#meta-info").html("<p class=\"text-danger\">" + msg.no_result + "</p>");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (ggDone && ggResults.length > 0) {
|
if (ggDone && ggResults.length > 0) {
|
||||||
for (i = 0; i < ggResults.length; i++) {
|
ggResults.forEach(function(result) {
|
||||||
book = ggResults[i];
|
var book = {
|
||||||
var bookCover;
|
id: result.id,
|
||||||
if (book.volumeInfo.imageLinks) {
|
title: result.volumeInfo.title,
|
||||||
bookCover = book.volumeInfo.imageLinks.thumbnail;
|
authors: result.volumeInfo.authors || [],
|
||||||
} else {
|
description: result.volumeInfo.description || "",
|
||||||
bookCover = "/static/generic_cover.jpg";
|
publisher: result.volumeInfo.publisher || "",
|
||||||
}
|
publishedDate: result.volumeInfo.publishedDate || "",
|
||||||
bookHtml = "<li class=\"media\">" +
|
tags: result.volumeInfo.categories || [],
|
||||||
"<img class=\"pull-left img-responsive\" data-toggle=\"modal\" data-target=\"#metaModal\" src=\"" +
|
rating: result.volumeInfo.averageRating || 0,
|
||||||
bookCover + "\" alt=\"Cover\" style=\"width:100px;height:150px\" onclick='getMeta(\"google\"," +
|
cover: result.volumeInfo.imageLinks ?
|
||||||
i + ")'>" +
|
result.volumeInfo.imageLinks.thumbnail :
|
||||||
"<div class=\"media-body\">" +
|
"/static/generic_cover.jpg",
|
||||||
"<h4 class=\"media-heading\"><a href=\"https://books.google.com/books?id=" +
|
url: "https://books.google.com/books?id=" + result.id,
|
||||||
book.id + "\" target=\"_blank\">" + book.volumeInfo.title + "</a></h4>" +
|
source: {
|
||||||
"<p>"+ msg.author +":" + book.volumeInfo.authors + "</p>" +
|
id: "google",
|
||||||
"<p>"+ msg.publisher + ":" + book.volumeInfo.publisher + "</p>" +
|
description: "Google Books",
|
||||||
"<p>"+ msg.description + ":" + book.volumeInfo.description + "</p>" +
|
url: "https://books.google.com/"
|
||||||
"<p>"+ msg.source + ":<a href=\"https://books.google.com\" target=\"_blank\">Google Books</a></p>" +
|
|
||||||
"</div>" +
|
|
||||||
"</li>";
|
|
||||||
$("#metaModal #book-list").append(bookHtml);
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var $book = $(templates.bookResult(book));
|
||||||
|
$book.find("img").on("click", function () {
|
||||||
|
populateForm(book);
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#book-list").append($book);
|
||||||
|
});
|
||||||
ggDone = false;
|
ggDone = false;
|
||||||
}
|
}
|
||||||
if (dbDone && dbResults.length > 0) {
|
if (dbDone && dbResults.length > 0) {
|
||||||
for (i = 0; i < dbResults.length; i++) {
|
dbResults.forEach(function(result) {
|
||||||
book = dbResults[i];
|
var book = {
|
||||||
bookHtml = "<li class=\"media\">" +
|
id: result.id,
|
||||||
"<img class=\"pull-left img-responsive\" data-toggle=\"modal\" data-target=\"#metaModal\" src=\"" +
|
title: result.title,
|
||||||
book.image + "\" alt=\"Cover\" style=\"width:100px;height: 150px\" onclick='getMeta(\"douban\"," +
|
authors: result.author || [],
|
||||||
i + ")'>" +
|
description: result.summary,
|
||||||
"<div class=\"media-body\">" +
|
publisher: result.publisher || "",
|
||||||
"<h4 class=\"media-heading\"><a href=\"https://book.douban.com/subject/" +
|
publishedDate: result.pubdate || "",
|
||||||
book.id + "\" target=\"_blank\">" + book.title + "</a></h4>" +
|
tags: result.tags.map(function(tag) {
|
||||||
"<p>" + msg.author + ":" + book.author + "</p>" +
|
return tag.title;
|
||||||
"<p>" + msg.publisher + ":" + book.publisher + "</p>" +
|
}),
|
||||||
"<p>" + msg.description + ":" + book.summary + "</p>" +
|
rating: result.rating.average || 0,
|
||||||
"<p>" + msg.source + ":<a href=\"https://book.douban.com\" target=\"_blank\">Douban Books</a></p>" +
|
cover: result.image,
|
||||||
"</div>" +
|
url: "https://book.douban.com/subject/" + result.id,
|
||||||
"</li>";
|
source: {
|
||||||
$("#metaModal #book-list").append(bookHtml);
|
id: "douban",
|
||||||
|
description: "Douban Books",
|
||||||
|
url: "https://book.douban.com/"
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var $book = $(templates.bookResult(book));
|
||||||
|
$book.find("img").on("click", function () {
|
||||||
|
populateForm(book);
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#book-list").append($book);
|
||||||
|
});
|
||||||
dbDone = false;
|
dbDone = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ggSearchBook (title) {
|
function ggSearchBook (title) {
|
||||||
title = title.replaceAll(/\s+/, "+");
|
|
||||||
var url = google + ggSearch + "?q=" + title;
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url,
|
url: google + ggSearch + "?q=" + title.replace(/\s+/gm, "+"),
|
||||||
type: "GET",
|
type: "GET",
|
||||||
dataType: "jsonp",
|
dataType: "jsonp",
|
||||||
jsonp: "callback",
|
jsonp: "callback",
|
||||||
success (data) {
|
success: function success(data) {
|
||||||
ggResults = data.items;
|
ggResults = data.items;
|
||||||
},
|
},
|
||||||
complete () {
|
complete: function complete() {
|
||||||
ggDone = true;
|
ggDone = true;
|
||||||
showResult();
|
showResult();
|
||||||
|
$("#show-google").trigger("change");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function dbSearchBook (title) {
|
function dbSearchBook (title) {
|
||||||
var url = douban + dbSearch + "?q=" + title + "&fields=all&count=10";
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url,
|
url: douban + dbSearch + "?q=" + title + "&fields=all&count=10",
|
||||||
type: "GET",
|
type: "GET",
|
||||||
dataType: "jsonp",
|
dataType: "jsonp",
|
||||||
jsonp: "callback",
|
jsonp: "callback",
|
||||||
success (data) {
|
success: function success(data) {
|
||||||
dbResults = data.books;
|
dbResults = data.books;
|
||||||
},
|
},
|
||||||
error () {
|
error: function error() {
|
||||||
$("#metaModal #meta-info").html("<p class=\"text-danger\">"+ msg.search_error+"!</p>");
|
$("#meta-info").html("<p class=\"text-danger\">" + msg.search_error + "!</p>");
|
||||||
},
|
},
|
||||||
complete () {
|
complete: function complete() {
|
||||||
dbDone = true;
|
dbDone = true;
|
||||||
showResult();
|
showResult();
|
||||||
|
$("#show-douban").trigger("change");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function doSearch (keyword) {
|
function doSearch (keyword) {
|
||||||
showFlag = 0;
|
showFlag = 0;
|
||||||
$("#metaModal #meta-info").text(msg.loading);
|
$("#meta-info").text(msg.loading);
|
||||||
// var keyword = $("#keyword").val();
|
// var keyword = $("#keyword").val();
|
||||||
if (keyword) {
|
if (keyword) {
|
||||||
dbSearchBook(keyword);
|
dbSearchBook(keyword);
|
||||||
@ -136,7 +161,8 @@ $(document).ready(function () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$("#do-search").click(function () {
|
$("#meta-search").on("submit", function (e) {
|
||||||
|
e.preventDefault();
|
||||||
var keyword = $("#keyword").val();
|
var keyword = $("#keyword").val();
|
||||||
if (keyword) {
|
if (keyword) {
|
||||||
doSearch(keyword);
|
doSearch(keyword);
|
||||||
@ -152,35 +178,3 @@ $(document).ready(function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function getMeta (source, id) {
|
|
||||||
var meta;
|
|
||||||
var tags;
|
|
||||||
if (source === "google") {
|
|
||||||
meta = ggResults[id];
|
|
||||||
tinymce.get("description").setContent(meta.volumeInfo.description);
|
|
||||||
$("#bookAuthor").val(meta.volumeInfo.authors.join(" & "));
|
|
||||||
$("#book_title").val(meta.volumeInfo.title);
|
|
||||||
if (meta.volumeInfo.categories) {
|
|
||||||
tags = meta.volumeInfo.categories.join(",");
|
|
||||||
$("#tags").val(tags);
|
|
||||||
}
|
|
||||||
if (meta.volumeInfo.averageRating) {
|
|
||||||
$("#rating").val(Math.round(meta.volumeInfo.averageRating));
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (source === "douban") {
|
|
||||||
meta = dbResults[id];
|
|
||||||
tinymce.get("description").setContent(meta.summary);
|
|
||||||
$("#bookAuthor").val(meta.author.join(" & "));
|
|
||||||
$("#book_title").val(meta.title);
|
|
||||||
tags = "";
|
|
||||||
for (var i = 0; i < meta.tags.length; i++) {
|
|
||||||
tags = tags + meta.tags[i].title + ",";
|
|
||||||
}
|
|
||||||
$("#tags").val(tags);
|
|
||||||
$("#rating").val(Math.round(meta.rating.average / 2));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1 +1,2 @@
|
|||||||
!function(a){"use strict";function b(a){return"[data-value"+(a?"="+a:"")+"]"}function c(a,b,c){var d=c.activeIcon,e=c.inactiveIcon;a.removeClass(b?e:d).addClass(b?d:e)}function d(b,c){var d=a.extend({},i,b.data(),c);return d.inline=""===d.inline||d.inline,d.readonly=""===d.readonly||d.readonly,d.clearable===!1?d.clearableLabel="":d.clearableLabel=d.clearable,d.clearable=""===d.clearable||d.clearable,d}function e(b,c){if(c.inline)var d=a('<span class="rating-input"></span>');else var d=a('<div class="rating-input"></div>');d.addClass(b.attr("class")),d.removeClass("rating");for(var e=c.min;e<=c.max;e++)d.append('<i class="'+c.iconLib+'" data-value="'+e+'"></i>');return c.clearable&&!c.readonly&&d.append(" ").append('<a class="'+f+'"><i class="'+c.iconLib+" "+c.clearableIcon+'"/>'+c.clearableLabel+"</a>"),d}var f="rating-clear",g="."+f,h="hidden",i={min:1,max:5,"empty-value":0,iconLib:"glyphicon",activeIcon:"glyphicon-star",inactiveIcon:"glyphicon-star-empty",clearable:!1,clearableIcon:"glyphicon-remove",clearableRemain:!1,inline:!1,readonly:!1},j=function(a,b){var c=this.$input=a;this.options=d(c,b);var f=this.$el=e(c,this.options);c.addClass(h).before(f),c.attr("type","hidden"),this.highlight(c.val())};j.VERSION="0.4.0",j.DEFAULTS=i,j.prototype={clear:function(){this.setValue(this.options["empty-value"])},setValue:function(a){this.highlight(a),this.updateInput(a)},highlight:function(a,d){var e=this.options,f=this.$el;if(a>=this.options.min&&a<=this.options.max){var i=f.find(b(a));c(i.prevAll("i").andSelf(),!0,e),c(i.nextAll("i"),!1,e)}else c(f.find(b()),!1,e);d||(this.options.clearableRemain?f.find(g).removeClass(h):a&&a!=this.options["empty-value"]?f.find(g).removeClass(h):f.find(g).addClass(h))},updateInput:function(a){var b=this.$input;b.val()!=a&&b.val(a).change()}};var k=a.fn.rating=function(c){return this.filter("input[type=number]").each(function(){var d=a(this),e="object"==typeof c&&c||{},f=new j(d,e);f.options.readonly||f.$el.on("mouseenter",b(),function(){f.highlight(a(this).data("value"),!0)}).on("mouseleave",b(),function(){f.highlight(d.val(),!0)}).on("click",b(),function(){f.setValue(a(this).data("value"))}).on("click",g,function(){f.clear()})})};k.Constructor=j,a(function(){a("input.rating[type=number]").each(function(){a(this).rating()})})}(jQuery);
|
/** @link https://github.com/javiertoledo/bootstrap-rating-input */
|
||||||
|
!function(a){"use strict";function n(a){return"[data-value"+(a?"="+a:"")+"]"}function e(a,n,e){var i=e.activeIcon,t=e.inactiveIcon;a.removeClass(n?t:i).addClass(n?i:t)}function i(n,e){var i=a.extend({},s,n.data(),e);return i.inline=""===i.inline||i.inline,i.readonly=""===i.readonly||i.readonly,!1===i.clearable?i.clearableLabel="":i.clearableLabel=i.clearable,i.clearable=""===i.clearable||i.clearable,i}function t(n,e){if(e.inline)i=a('<span class="rating-input"></span>');else var i=a('<div class="rating-input"></div>');i.addClass(n.attr("class")),i.removeClass("rating");for(var t=e.min;t<=e.max;t++)i.append('<i class="'+e.iconLib+'" data-value="'+t+'"></i>');return e.clearable&&!e.readonly&&i.append(" ").append('<a class="'+l+'"><i class="'+e.iconLib+" "+e.clearableIcon+'"/>'+e.clearableLabel+"</a>"),i}var l="rating-clear",o="."+l,s={min:1,max:5,"empty-value":0,iconLib:"glyphicon",activeIcon:"glyphicon-star",inactiveIcon:"glyphicon-star-empty",clearable:!1,clearableIcon:"glyphicon-remove",clearableRemain:!1,inline:!1,readonly:!1},r=function(a,n){var e=this.$input=a;this.options=i(e,n);var l=this.$el=t(e,this.options);e.addClass("hidden").before(l),e.attr("type","hidden"),this.highlight(e.val())};r.VERSION="0.4.0",r.DEFAULTS=s,r.prototype={clear:function(){this.setValue(this.options["empty-value"])},setValue:function(a){this.highlight(a),this.updateInput(a)},highlight:function(a,i){var t=this.options,l=this.$el;if(a>=this.options.min&&a<=this.options.max){var s=l.find(n(a));e(s.prevAll("i").addBack(),!0,t),e(s.nextAll("i"),!1,t)}else e(l.find(n()),!1,t);i||(this.options.clearableRemain?l.find(o).removeClass("hidden"):a&&a!=this.options["empty-value"]?l.find(o).removeClass("hidden"):l.find(o).addClass("hidden"))},updateInput:function(a){var n=this.$input;n.val()!=a&&n.val(a).change()}},(a.fn.rating=function(e){return this.filter("input[type=number]").each(function(){var i=a(this),t=new r(i,"object"==typeof e&&e||{});t.options.readonly||(t.$el.on("mouseenter",n(),function(){t.highlight(a(this).data("value"),!0)}).on("mouseleave",n(),function(){t.highlight(i.val(),!0)}).on("click",n(),function(){t.setValue(a(this).data("value"))}).on("click",o,function(){t.clear()}),i.data("rating",t))})}).Constructor=r,a(function(){a("input.rating[type=number]").each(function(){a(this).rating()})})}(jQuery);
|
||||||
|
@ -1,7 +1,3 @@
|
|||||||
var displaytext;
|
|
||||||
var updateTimerID;
|
|
||||||
var updateText;
|
|
||||||
|
|
||||||
// Generic control/related handler to show/hide fields based on a checkbox' value
|
// Generic control/related handler to show/hide fields based on a checkbox' value
|
||||||
// e.g.
|
// e.g.
|
||||||
// <input type="checkbox" data-control="stuff-to-show">
|
// <input type="checkbox" data-control="stuff-to-show">
|
||||||
@ -11,16 +7,18 @@ $(document).on("change", "input[type=\"checkbox\"][data-control]", function () {
|
|||||||
var name = $this.data("control");
|
var name = $this.data("control");
|
||||||
var showOrHide = $this.prop("checked");
|
var showOrHide = $this.prop("checked");
|
||||||
|
|
||||||
$("[data-related=\""+name+"\"]").each(function () {
|
$("[data-related=\"" + name + "\"]").each(function () {
|
||||||
$(this).toggle(showOrHide);
|
$(this).toggle(showOrHide);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
$(function() {
|
$(function() {
|
||||||
|
var updateTimerID;
|
||||||
|
var updateText;
|
||||||
|
|
||||||
// Allow ajax prefilters to be added/removed dynamically
|
// Allow ajax prefilters to be added/removed dynamically
|
||||||
// eslint-disable-next-line new-cap
|
// eslint-disable-next-line new-cap
|
||||||
const preFilters = $.Callbacks();
|
var preFilters = $.Callbacks();
|
||||||
$.ajaxPrefilter(preFilters.fire);
|
$.ajaxPrefilter(preFilters.fire);
|
||||||
|
|
||||||
function restartTimer() {
|
function restartTimer() {
|
||||||
@ -31,28 +29,28 @@ $(function() {
|
|||||||
function updateTimer() {
|
function updateTimer() {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
url: window.location.pathname+"/../../get_updater_status",
|
url: window.location.pathname + "/../../get_updater_status",
|
||||||
success(data) {
|
success: function success(data) {
|
||||||
// console.log(data.status);
|
// console.log(data.status);
|
||||||
$("#UpdateprogressDialog #Updatecontent").html(updateText[data.status]);
|
$("#Updatecontent").html(updateText[data.status]);
|
||||||
if (data.status >6){
|
if (data.status > 6) {
|
||||||
clearInterval(updateTimerID);
|
clearInterval(updateTimerID);
|
||||||
$("#spinner2").hide();
|
$("#spinner2").hide();
|
||||||
$("#UpdateprogressDialog #updateFinished").removeClass("hidden");
|
$("#updateFinished").removeClass("hidden");
|
||||||
$("#check_for_update").removeClass("hidden");
|
$("#check_for_update").removeClass("hidden");
|
||||||
$("#perform_update").addClass("hidden");
|
$("#perform_update").addClass("hidden");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error() {
|
error: function error() {
|
||||||
// console.log('Done');
|
// console.log('Done');
|
||||||
clearInterval(updateTimerID);
|
clearInterval(updateTimerID);
|
||||||
$("#spinner2").hide();
|
$("#spinner2").hide();
|
||||||
$("#UpdateprogressDialog #Updatecontent").html(updateText[7]);
|
$("#Updatecontent").html(updateText[7]);
|
||||||
$("#UpdateprogressDialog #updateFinished").removeClass("hidden");
|
$("#updateFinished").removeClass("hidden");
|
||||||
$("#check_for_update").removeClass("hidden");
|
$("#check_for_update").removeClass("hidden");
|
||||||
$("#perform_update").addClass("hidden");
|
$("#perform_update").addClass("hidden");
|
||||||
},
|
},
|
||||||
timeout:2000
|
timeout: 2000
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,13 +68,13 @@ $(function() {
|
|||||||
// selector for the NEXT link (to page 2)
|
// selector for the NEXT link (to page 2)
|
||||||
itemSelector : ".load-more .book",
|
itemSelector : ".load-more .book",
|
||||||
animate : true,
|
animate : true,
|
||||||
extraScrollPx: 300,
|
extraScrollPx: 300
|
||||||
// selector for all items you'll retrieve
|
// selector for all items you'll retrieve
|
||||||
}, function(data){
|
}, function(data) {
|
||||||
$(".load-more .row").isotope( "appended", $(data), null );
|
$(".load-more .row").isotope( "appended", $(data), null );
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#sendbtn").click(function(){
|
$("#sendbtn").click(function() {
|
||||||
var $this = $(this);
|
var $this = $(this);
|
||||||
$this.text("Please wait...");
|
$this.text("Please wait...");
|
||||||
$this.addClass("disabled");
|
$this.addClass("disabled");
|
||||||
@ -84,36 +82,39 @@ $(function() {
|
|||||||
$("#restart").click(function() {
|
$("#restart").click(function() {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
url: window.location.pathname+"/../../shutdown",
|
url: window.location.pathname + "/../../shutdown",
|
||||||
data: {"parameter":0},
|
data: {"parameter":0},
|
||||||
success(data) {
|
success: function success() {
|
||||||
$("#spinner").show();
|
$("#spinner").show();
|
||||||
displaytext=data.text;
|
setTimeout(restartTimer, 3000);
|
||||||
setTimeout(restartTimer, 3000);}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
$("#shutdown").click(function() {
|
$("#shutdown").click(function() {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
url: window.location.pathname+"/../../shutdown",
|
url: window.location.pathname + "/../../shutdown",
|
||||||
data: {"parameter":1},
|
data: {"parameter":1},
|
||||||
success(data) {
|
success: function success(data) {
|
||||||
return alert(data.text);}
|
return alert(data.text);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
$("#check_for_update").click(function() {
|
$("#check_for_update").click(function() {
|
||||||
var buttonText = $("#check_for_update").html();
|
var $this = $(this);
|
||||||
$("#check_for_update").html("...");
|
var buttonText = $this.html();
|
||||||
|
$this.html("...");
|
||||||
$.ajax({
|
$.ajax({
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
url: window.location.pathname+"/../../get_update_status",
|
url: window.location.pathname + "/../../get_update_status",
|
||||||
success(data) {
|
success: function success(data) {
|
||||||
$("#check_for_update").html(buttonText);
|
$this.html(buttonText);
|
||||||
if (data.status === true) {
|
if (data.status === true) {
|
||||||
$("#check_for_update").addClass("hidden");
|
$("#check_for_update").addClass("hidden");
|
||||||
$("#perform_update").removeClass("hidden");
|
$("#perform_update").removeClass("hidden");
|
||||||
$("#update_info").removeClass("hidden");
|
$("#update_info")
|
||||||
$("#update_info").find("span").html(data.commit);
|
.removeClass("hidden")
|
||||||
|
.find("span").html(data.commit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -121,7 +122,7 @@ $(function() {
|
|||||||
$("#restart_database").click(function() {
|
$("#restart_database").click(function() {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
url: window.location.pathname+"/../../shutdown",
|
url: window.location.pathname + "/../../shutdown",
|
||||||
data: {"parameter":2}
|
data: {"parameter":2}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -131,12 +132,13 @@ $(function() {
|
|||||||
type: "POST",
|
type: "POST",
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
data: { start: "True"},
|
data: { start: "True"},
|
||||||
url: window.location.pathname+"/../../get_updater_status",
|
url: window.location.pathname + "/../../get_updater_status",
|
||||||
success(data) {
|
success: function success(data) {
|
||||||
updateText=data.text;
|
updateText = data.text;
|
||||||
$("#UpdateprogressDialog #Updatecontent").html(updateText[data.status]);
|
$("#Updatecontent").html(updateText[data.status]);
|
||||||
// console.log(data.status);
|
// console.log(data.status);
|
||||||
updateTimerID=setInterval(updateTimer, 2000);}
|
updateTimerID = setInterval(updateTimer, 2000);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -144,10 +146,10 @@ $(function() {
|
|||||||
|
|
||||||
$("#bookDetailsModal")
|
$("#bookDetailsModal")
|
||||||
.on("show.bs.modal", function(e) {
|
.on("show.bs.modal", function(e) {
|
||||||
const $modalBody = $(this).find(".modal-body");
|
var $modalBody = $(this).find(".modal-body");
|
||||||
|
|
||||||
// Prevent static assets from loading multiple times
|
// Prevent static assets from loading multiple times
|
||||||
const useCache = (options) => {
|
var useCache = function(options) {
|
||||||
options.async = true;
|
options.async = true;
|
||||||
options.cache = true;
|
options.cache = true;
|
||||||
};
|
};
|
||||||
@ -162,7 +164,7 @@ $(function() {
|
|||||||
$(this).find(".modal-body").html("...");
|
$(this).find(".modal-body").html("...");
|
||||||
});
|
});
|
||||||
|
|
||||||
$(window).resize(function(event) {
|
$(window).resize(function() {
|
||||||
$(".discover .row").isotope("reLayout");
|
$(".discover .row").isotope("reLayout");
|
||||||
});
|
});
|
||||||
});
|
});
|
@ -1,30 +1,30 @@
|
|||||||
/* global Sortable,sortTrue */
|
/* global Sortable,sortTrue */
|
||||||
|
|
||||||
var sortable = Sortable.create(sortTrue, {
|
Sortable.create(sortTrue, {
|
||||||
group: "sorting",
|
group: "sorting",
|
||||||
sort: true
|
sort: true
|
||||||
});
|
});
|
||||||
|
|
||||||
function sendData(path){
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
function sendData(path) {
|
||||||
var elements;
|
var elements;
|
||||||
var counter;
|
var counter;
|
||||||
var maxElements;
|
var maxElements;
|
||||||
var tmp=[];
|
var tmp = [];
|
||||||
|
|
||||||
elements=Sortable.utils.find(sortTrue,"div");
|
elements = Sortable.utils.find(sortTrue, "div");
|
||||||
maxElements=elements.length;
|
maxElements = elements.length;
|
||||||
|
|
||||||
var form = document.createElement("form");
|
var form = document.createElement("form");
|
||||||
form.setAttribute("method", "post");
|
form.setAttribute("method", "post");
|
||||||
form.setAttribute("action", path);
|
form.setAttribute("action", path);
|
||||||
|
|
||||||
|
for (counter = 0;counter < maxElements;counter++) {
|
||||||
for(counter=0;counter<maxElements;counter++){
|
tmp[counter] = elements[counter].getAttribute("id");
|
||||||
tmp[counter]=elements[counter].getAttribute("id");
|
|
||||||
var hiddenField = document.createElement("input");
|
var hiddenField = document.createElement("input");
|
||||||
hiddenField.setAttribute("type", "hidden");
|
hiddenField.setAttribute("type", "hidden");
|
||||||
hiddenField.setAttribute("name", elements[counter].getAttribute("id"));
|
hiddenField.setAttribute("name", elements[counter].getAttribute("id"));
|
||||||
hiddenField.setAttribute("value", counter+1);
|
hiddenField.setAttribute("value", String(counter + 1));
|
||||||
form.appendChild(hiddenField);
|
form.appendChild(hiddenField);
|
||||||
}
|
}
|
||||||
document.body.appendChild(form);
|
document.body.appendChild(form);
|
||||||
|
@ -11,16 +11,17 @@
|
|||||||
{%if author.about is not none %}
|
{%if author.about is not none %}
|
||||||
<p>{{author.about|safe}}</p>
|
<p>{{author.about|safe}}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</section>
|
|
||||||
|
|
||||||
<a href="{{author.link}}" class="author-link" target="_blank">
|
- {{_("via")}} <a href="{{author.link}}" class="author-link" target="_blank" rel="noopener">Goodreads</a>
|
||||||
<img src="{{ url_for('static', filename='img/goodreads.svg') }}" alt="Goodreads">
|
</section>
|
||||||
</a>
|
|
||||||
|
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="discover load-more">
|
<div class="discover load-more">
|
||||||
|
{% if author is not none %}
|
||||||
|
<h3>{{_("In Library")}}</h3>
|
||||||
|
{% endif %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
{% if entries[0] %}
|
{% if entries[0] %}
|
||||||
{% for entry in entries %}
|
{% for entry in entries %}
|
||||||
@ -62,4 +63,48 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if other_books is not none %}
|
||||||
|
<div class="discover">
|
||||||
|
<h3>{{_("More by")}} {{ author.name|safe }}</h3>
|
||||||
|
<div class="row">
|
||||||
|
{% for entry in other_books %}
|
||||||
|
<div class="col-sm-3 col-lg-2 col-xs-6 book">
|
||||||
|
<div class="cover">
|
||||||
|
<a href="https://www.goodreads.com/book/show/{{ entry.gid['#text'] }}" target="_blank" rel="noopener">
|
||||||
|
<img src="{{ entry.image_url }}" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="meta">
|
||||||
|
<p class="title">{{entry.title|shortentitle}}</p>
|
||||||
|
<p class="author">
|
||||||
|
{% for author in entry.authors %}
|
||||||
|
<a href="https://www.goodreads.com/author/show/{{ author.gid }}" target="_blank" rel="noopener">
|
||||||
|
{{author.name}}
|
||||||
|
</a>
|
||||||
|
{% if not loop.last %}
|
||||||
|
&
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</p>
|
||||||
|
<div class="rating">
|
||||||
|
{% for number in range((entry.average_rating)|float|round|int(2)) %}
|
||||||
|
<span class="glyphicon glyphicon-star good"></span>
|
||||||
|
{% if loop.last and loop.index < 5 %}
|
||||||
|
{% for numer in range(5 - loop.index) %}
|
||||||
|
<span class="glyphicon glyphicon-star"></span>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="{{author.link}}" class="author-link" target="_blank" rel="noopener">
|
||||||
|
<img src="{{ url_for('static', filename='img/goodreads.svg') }}" alt="Goodreads">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -139,7 +139,7 @@
|
|||||||
<div class="modal-header bg-danger text-center">
|
<div class="modal-header bg-danger text-center">
|
||||||
<span>{{_('Are really you sure?')}}</span>
|
<span>{{_('Are really you sure?')}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body text-center" id="meta-info">
|
<div class="modal-body text-center">
|
||||||
<span>{{_('Book will be deleted from Calibre database')}}</span>
|
<span>{{_('Book will be deleted from Calibre database')}}</span>
|
||||||
<span>{{_('and from hard disk')}}</span>
|
<span>{{_('and from hard disk')}}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -154,23 +154,36 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="modal fade" id="metaModal" tabindex="-1" role="dialog" aria-labelledby="metaModalLabel">
|
<div class="modal fade" id="metaModal" tabindex="-1" role="dialog" aria-labelledby="metaModalLabel">
|
||||||
<div class="modal-dialog" role="document">
|
<div class="modal-dialog modal-lg" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||||
<h4 class="modal-title" id="metaModalLabel">{{_('Get metadata')}}</h4>
|
<h4 class="modal-title" id="metaModalLabel">{{_('Get metadata')}}</h4>
|
||||||
<form class="form-inline">
|
<form class="padded-bottom" id="meta-search">
|
||||||
<div class="form-group">
|
<div class="input-group">
|
||||||
<label class="sr-only" for="keyword">{{_('Keyword')}}</label>
|
<label class="sr-only" for="keyword">{{_('Keyword')}}</label>
|
||||||
<input type="text" class="form-control" id="keyword" placeholder="{{_(" Search keyword ")}}">
|
<input type="text" class="form-control" id="keyword" name="keyword" placeholder="{{_(" Search keyword ")}}">
|
||||||
|
<span class="input-group-btn">
|
||||||
|
<button type="submit" class="btn btn-primary" id="do-search">{{_("Go!")}}</button>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn btn-default" id="do-search">{{_("Go!")}}</button>
|
|
||||||
<span>{{_('Click the cover to load metadata to the form')}}</span>
|
|
||||||
</form>
|
</form>
|
||||||
|
<div>{{_('Click the cover to load metadata to the form')}}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body" id="meta-info">
|
<div class="modal-body">
|
||||||
|
<div class="text-center padded-bottom">
|
||||||
|
<input type="checkbox" id="show-douban" class="pill" data-control="douban" checked>
|
||||||
|
<label for="show-douban">Douban <span class="glyphicon glyphicon-ok"></span></label>
|
||||||
|
|
||||||
|
<input type="checkbox" id="show-google" class="pill" data-control="google" checked>
|
||||||
|
<label for="show-google">Google <span class="glyphicon glyphicon-ok"></span></label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="meta-info">
|
||||||
{{_("Loading...")}}
|
{{_("Loading...")}}
|
||||||
</div>
|
</div>
|
||||||
|
<ul id="book-list" class="media-list"></ul>
|
||||||
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{_('Close')}}</button>
|
<button type="button" class="btn btn-default" data-dismiss="modal">{{_('Close')}}</button>
|
||||||
</div>
|
</div>
|
||||||
@ -180,6 +193,31 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
|
<script type="text/template" id="template-book-result">
|
||||||
|
<li class="media" data-related="<%= source.id %>">
|
||||||
|
<img class="pull-left img-responsive"
|
||||||
|
data-toggle="modal"
|
||||||
|
data-target="#metaModal"
|
||||||
|
src="<%= cover %>"
|
||||||
|
alt="Cover"
|
||||||
|
>
|
||||||
|
<div class="media-body">
|
||||||
|
<h4 class="media-heading">
|
||||||
|
<a href="<%= url %>" target="_blank" rel="noopener"><%= title %></a>
|
||||||
|
</h4>
|
||||||
|
<p>{{_('Author')}}:<%= authors.join(" & ") %></p>
|
||||||
|
<% if (publisher) { %>
|
||||||
|
<p>{{_('Publisher')}}:<%= publisher %></p>
|
||||||
|
<% } %>
|
||||||
|
<% if (description) { %>
|
||||||
|
<p>{{_('Description')}}: <%= description %></p>
|
||||||
|
<% } %>
|
||||||
|
<p>{{_('Source')}}:
|
||||||
|
<a href="<%= source.url %>" target="_blank" rel="noopener"><%= source.description %></a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</script>
|
||||||
<script>
|
<script>
|
||||||
var i18nMsg = {
|
var i18nMsg = {
|
||||||
'loading': {{_('Loading...')|safe|tojson}},
|
'loading': {{_('Loading...')|safe|tojson}},
|
||||||
|
@ -72,6 +72,13 @@
|
|||||||
<label for="config_title_regex">{{_('Regular expression for title sorting')}}</label>
|
<label for="config_title_regex">{{_('Regular expression for title sorting')}}</label>
|
||||||
<input type="text" class="form-control" name="config_title_regex" id="config_title_regex" value="{% if content.config_title_regex != None %}{{ content.config_title_regex }}{% endif %}" autocomplete="off">
|
<input type="text" class="form-control" name="config_title_regex" id="config_title_regex" value="{% if content.config_title_regex != None %}{{ content.config_title_regex }}{% endif %}" autocomplete="off">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="config_mature_content_tags">{{_('Tags for Mature Content')}}</label>
|
||||||
|
<input type="text" class="form-control" name="config_mature_content_tags" id="config_mature_content_tags"
|
||||||
|
value="{% if content.config_mature_content_tags != None%}{{ content.config_mature_content_tags }}{% endif %}"
|
||||||
|
autocomplete="off"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="config_log_level">{{_('Log Level')}}</label>
|
<label for="config_log_level">{{_('Log Level')}}</label>
|
||||||
<select name="config_log_level" id="config_log_level" class="form-control">
|
<select name="config_log_level" id="config_log_level" class="form-control">
|
||||||
|
@ -259,5 +259,17 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
|
<script type="text/template" id="template-shelf-add">
|
||||||
|
<li>
|
||||||
|
<a href="<%= add %>" data-remove-href="<%= remove %>" data-shelf-action="add">
|
||||||
|
<%= content %>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</script>
|
||||||
|
<script type="text/template" id="template-shelf-remove">
|
||||||
|
<a href="<%= remove %>" data-add-href="<%= add %>" class="btn btn-sm btn-default" data-shelf-action="remove">
|
||||||
|
<span class="glyphicon glyphicon-remove"></span> <%= content %>
|
||||||
|
</a>
|
||||||
|
</script>
|
||||||
<script src="{{ url_for('static', filename='js/details.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/details.js') }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -26,9 +26,7 @@
|
|||||||
href="{{request.script_root + request.path}}?offset={{ pagination.previous_offset }}"
|
href="{{request.script_root + request.path}}?offset={{ pagination.previous_offset }}"
|
||||||
type="application/atom+xml;profile=opds-catalog;type=feed;kind=navigation"/>
|
type="application/atom+xml;profile=opds-catalog;type=feed;kind=navigation"/>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<link rel="search"
|
<link title="{{_('Search')}}" type="application/atom+xml" href="{{url_for('feed_normal_search')}}?query={searchTerms}" rel="search"/>
|
||||||
href="{{url_for('feed_osd')}}"
|
|
||||||
type="application/opensearchdescription+xml"/>
|
|
||||||
<title>{{instance}}</title>
|
<title>{{instance}}</title>
|
||||||
<author>
|
<author>
|
||||||
<name>{{instance}}</name>
|
<name>{{instance}}</name>
|
||||||
|
@ -4,8 +4,7 @@
|
|||||||
<link rel="self" href="{{url_for('feed_index')}}" type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
|
<link rel="self" href="{{url_for('feed_index')}}" type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
|
||||||
<link rel="start" title="{{_('Start')}}" href="{{url_for('feed_index')}}"
|
<link rel="start" title="{{_('Start')}}" href="{{url_for('feed_index')}}"
|
||||||
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
|
type="application/atom+xml;profile=opds-catalog;kind=navigation"/>
|
||||||
<link rel="search" title="{{_('Search')}}" href="{{url_for('feed_osd')}}"
|
<link title="{{_('Search')}}" type="application/atom+xml" href="{{url_for('feed_normal_search')}}?query={searchTerms}" rel="search"/>
|
||||||
type="application/opensearchdescription+xml"/>
|
|
||||||
<title>{{instance}}</title>
|
<title>{{instance}}</title>
|
||||||
<author>
|
<author>
|
||||||
<name>{{instance}}</name>
|
<name>{{instance}}</name>
|
||||||
|
@ -41,7 +41,10 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="checkbox" name="show_mature_content" id="show_mature_content" {% if content.mature_content %}checked{% endif %}>
|
||||||
|
<label for="show_mature_content">{{_('Show mature content')}}</label>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input type="checkbox" name="show_random" id="show_random" {% if content.show_random_books() %}checked{% endif %}>
|
<input type="checkbox" name="show_random" id="show_random" {% if content.show_random_books() %}checked{% endif %}>
|
||||||
<label for="show_random">{{_('Show random books')}}</label>
|
<label for="show_random">{{_('Show random books')}}</label>
|
||||||
@ -78,7 +81,7 @@
|
|||||||
<input type="checkbox" name="show_detail_random" id="show_detail_random" {% if content.show_detail_random() %}checked{% endif %}>
|
<input type="checkbox" name="show_detail_random" id="show_detail_random" {% if content.show_detail_random() %}checked{% endif %}>
|
||||||
<label for="show_detail_random">{{_('Show random books in detail view')}}</label>
|
<label for="show_detail_random">{{_('Show random books in detail view')}}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
{% if g.user and g.user.role_admin() and not profile %}
|
{% if g.user and g.user.role_admin() and not profile %}
|
||||||
{% if not content.role_anonymous() %}
|
{% if not content.role_anonymous() %}
|
||||||
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
20
cps/ub.py
20
cps/ub.py
@ -157,6 +157,7 @@ class User(UserBase, Base):
|
|||||||
locale = Column(String(2), default="en")
|
locale = Column(String(2), default="en")
|
||||||
sidebar_view = Column(Integer, default=1)
|
sidebar_view = Column(Integer, default=1)
|
||||||
default_language = Column(String(3), default="all")
|
default_language = Column(String(3), default="all")
|
||||||
|
mature_content = Column(Boolean, default=True)
|
||||||
|
|
||||||
|
|
||||||
# Class for anonymous user is derived from User base and complets overrides methods and properties for the
|
# Class for anonymous user is derived from User base and complets overrides methods and properties for the
|
||||||
@ -166,13 +167,14 @@ class Anonymous(AnonymousUserMixin, UserBase):
|
|||||||
self.loadSettings()
|
self.loadSettings()
|
||||||
|
|
||||||
def loadSettings(self):
|
def loadSettings(self):
|
||||||
data = session.query(User).filter(User.role.op('&')(ROLE_ANONYMOUS) == ROLE_ANONYMOUS).first()
|
data = session.query(User).filter(User.role.op('&')(ROLE_ANONYMOUS) == ROLE_ANONYMOUS).first() # type: User
|
||||||
settings = session.query(Settings).first()
|
settings = session.query(Settings).first()
|
||||||
self.nickname = data.nickname
|
self.nickname = data.nickname
|
||||||
self.role = data.role
|
self.role = data.role
|
||||||
self.sidebar_view = data.sidebar_view
|
self.sidebar_view = data.sidebar_view
|
||||||
self.default_language = data.default_language
|
self.default_language = data.default_language
|
||||||
self.locale = data.locale
|
self.locale = data.locale
|
||||||
|
self.mature_content = data.mature_content
|
||||||
self.anon_browse = settings.config_anonbrowse
|
self.anon_browse = settings.config_anonbrowse
|
||||||
|
|
||||||
def role_admin(self):
|
def role_admin(self):
|
||||||
@ -266,6 +268,7 @@ class Settings(Base):
|
|||||||
config_use_goodreads = Column(Boolean)
|
config_use_goodreads = Column(Boolean)
|
||||||
config_goodreads_api_key = Column(String)
|
config_goodreads_api_key = Column(String)
|
||||||
config_goodreads_api_secret = Column(String)
|
config_goodreads_api_secret = Column(String)
|
||||||
|
config_mature_content_tags = Column(String) # type: str
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
pass
|
pass
|
||||||
@ -297,7 +300,7 @@ class Config:
|
|||||||
self.loadSettings()
|
self.loadSettings()
|
||||||
|
|
||||||
def loadSettings(self):
|
def loadSettings(self):
|
||||||
data = session.query(Settings).first()
|
data = session.query(Settings).first() # type: Settings
|
||||||
self.config_calibre_dir = data.config_calibre_dir
|
self.config_calibre_dir = data.config_calibre_dir
|
||||||
self.config_port = data.config_port
|
self.config_port = data.config_port
|
||||||
self.config_calibre_web_title = data.config_calibre_web_title
|
self.config_calibre_web_title = data.config_calibre_web_title
|
||||||
@ -326,6 +329,7 @@ class Config:
|
|||||||
self.config_use_goodreads = data.config_use_goodreads
|
self.config_use_goodreads = data.config_use_goodreads
|
||||||
self.config_goodreads_api_key = data.config_goodreads_api_key
|
self.config_goodreads_api_key = data.config_goodreads_api_key
|
||||||
self.config_goodreads_api_secret = data.config_goodreads_api_secret
|
self.config_goodreads_api_secret = data.config_goodreads_api_secret
|
||||||
|
self.config_mature_content_tags = data.config_mature_content_tags
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def get_main_dir(self):
|
def get_main_dir(self):
|
||||||
@ -371,6 +375,8 @@ class Config:
|
|||||||
return bool((self.config_default_role is not None) and
|
return bool((self.config_default_role is not None) and
|
||||||
(self.config_default_role & ROLE_DELETE_BOOKS == ROLE_DELETE_BOOKS))
|
(self.config_default_role & ROLE_DELETE_BOOKS == ROLE_DELETE_BOOKS))
|
||||||
|
|
||||||
|
def mature_content_tags(self):
|
||||||
|
return list(map(unicode.lstrip, self.config_mature_content_tags.split(",")))
|
||||||
|
|
||||||
def get_Log_Level(self):
|
def get_Log_Level(self):
|
||||||
ret_value=""
|
ret_value=""
|
||||||
@ -470,6 +476,11 @@ def migrate_Database():
|
|||||||
'side_lang': SIDEBAR_LANGUAGE, 'side_series': SIDEBAR_SERIES, 'side_category': SIDEBAR_CATEGORY,
|
'side_lang': SIDEBAR_LANGUAGE, 'side_series': SIDEBAR_SERIES, 'side_category': SIDEBAR_CATEGORY,
|
||||||
'side_hot': SIDEBAR_HOT, 'side_autor': SIDEBAR_AUTHOR, 'detail_random': DETAIL_RANDOM})
|
'side_hot': SIDEBAR_HOT, 'side_autor': SIDEBAR_AUTHOR, 'detail_random': DETAIL_RANDOM})
|
||||||
session.commit()
|
session.commit()
|
||||||
|
try:
|
||||||
|
session.query(exists().where(User.mature_content)).scalar()
|
||||||
|
except exc.OperationalError:
|
||||||
|
conn = engine.connect()
|
||||||
|
conn.execute("ALTER TABLE user ADD column `mature_content` INTEGER DEFAULT 1")
|
||||||
if session.query(User).filter(User.role.op('&')(ROLE_ANONYMOUS) == ROLE_ANONYMOUS).first() is None:
|
if session.query(User).filter(User.role.op('&')(ROLE_ANONYMOUS) == ROLE_ANONYMOUS).first() is None:
|
||||||
create_anonymous_user()
|
create_anonymous_user()
|
||||||
try:
|
try:
|
||||||
@ -484,6 +495,11 @@ def migrate_Database():
|
|||||||
conn.execute("ALTER TABLE Settings ADD column `config_use_goodreads` INTEGER DEFAULT 0")
|
conn.execute("ALTER TABLE Settings ADD column `config_use_goodreads` INTEGER DEFAULT 0")
|
||||||
conn.execute("ALTER TABLE Settings ADD column `config_goodreads_api_key` String DEFAULT ''")
|
conn.execute("ALTER TABLE Settings ADD column `config_goodreads_api_key` String DEFAULT ''")
|
||||||
conn.execute("ALTER TABLE Settings ADD column `config_goodreads_api_secret` String DEFAULT ''")
|
conn.execute("ALTER TABLE Settings ADD column `config_goodreads_api_secret` String DEFAULT ''")
|
||||||
|
try:
|
||||||
|
session.query(exists().where(Settings.config_mature_content_tags)).scalar()
|
||||||
|
except exc.OperationalError:
|
||||||
|
conn = engine.connect()
|
||||||
|
conn.execute("ALTER TABLE Settings ADD column `config_mature_content_tags` String DEFAULT ''")
|
||||||
|
|
||||||
def clean_database():
|
def clean_database():
|
||||||
# Remove expired remote login tokens
|
# Remove expired remote login tokens
|
||||||
|
230
cps/web.py
230
cps/web.py
@ -8,11 +8,16 @@ except ImportError:
|
|||||||
gdrive_support = False
|
gdrive_support = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from goodreads import client as gr_client
|
from goodreads.client import GoodreadsClient
|
||||||
goodreads_support = True
|
goodreads_support = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
goodreads_support = False
|
goodreads_support = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
from functools import reduce
|
||||||
|
except ImportError:
|
||||||
|
pass # We're not using Python 3
|
||||||
|
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import logging
|
import logging
|
||||||
from logging.handlers import RotatingFileHandler
|
from logging.handlers import RotatingFileHandler
|
||||||
@ -21,6 +26,7 @@ from flask import (Flask, render_template, request, Response, redirect,
|
|||||||
url_for, send_from_directory, make_response, g, flash,
|
url_for, send_from_directory, make_response, g, flash,
|
||||||
abort, Markup, stream_with_context)
|
abort, Markup, stream_with_context)
|
||||||
from flask import __version__ as flaskVersion
|
from flask import __version__ as flaskVersion
|
||||||
|
import cache_buster
|
||||||
import ub
|
import ub
|
||||||
from ub import config
|
from ub import config
|
||||||
import helper
|
import helper
|
||||||
@ -200,6 +206,7 @@ mimetypes.add_type('image/vnd.djvu', '.djvu')
|
|||||||
|
|
||||||
app = (Flask(__name__))
|
app = (Flask(__name__))
|
||||||
app.wsgi_app = ReverseProxied(app.wsgi_app)
|
app.wsgi_app = ReverseProxied(app.wsgi_app)
|
||||||
|
cache_buster.init_cache_busting(app)
|
||||||
|
|
||||||
gevent_server = None
|
gevent_server = None
|
||||||
|
|
||||||
@ -499,21 +506,30 @@ def edit_required(f):
|
|||||||
return inner
|
return inner
|
||||||
|
|
||||||
|
|
||||||
# Fill indexpage with all requested data from database
|
# Language and content filters
|
||||||
def fill_indexpage(page, database, db_filter, order):
|
def common_filters():
|
||||||
if current_user.filter_language() != "all":
|
if current_user.filter_language() != "all":
|
||||||
lang_filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language())
|
lang_filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language())
|
||||||
else:
|
else:
|
||||||
lang_filter = True
|
lang_filter = True
|
||||||
|
content_rating_filter = false() if current_user.mature_content else \
|
||||||
|
db.Books.tags.any(db.Tags.name.in_(config.mature_content_tags()))
|
||||||
|
return and_(lang_filter, ~content_rating_filter)
|
||||||
|
|
||||||
|
|
||||||
|
# Fill indexpage with all requested data from database
|
||||||
|
def fill_indexpage(page, database, db_filter, order):
|
||||||
if current_user.show_detail_random():
|
if current_user.show_detail_random():
|
||||||
random = db.session.query(db.Books).filter(lang_filter).order_by(func.random()).limit(config.config_random_books)
|
random = db.session.query(db.Books).filter(common_filters())\
|
||||||
|
.order_by(func.random()).limit(config.config_random_books)
|
||||||
else:
|
else:
|
||||||
random = false
|
random = false
|
||||||
off = int(int(config.config_books_per_page) * (page - 1))
|
off = int(int(config.config_books_per_page) * (page - 1))
|
||||||
pagination = Pagination(page, config.config_books_per_page,
|
pagination = Pagination(page, config.config_books_per_page,
|
||||||
len(db.session.query(database).filter(db_filter).filter(lang_filter).all()))
|
len(db.session.query(database)
|
||||||
entries = db.session.query(database).filter(db_filter).filter(lang_filter).order_by(order).offset(off).limit(
|
.filter(db_filter).filter(common_filters()).all()))
|
||||||
config.config_books_per_page)
|
entries = db.session.query(database).filter(common_filters())\
|
||||||
|
.order_by(order).offset(off).limit(config.config_books_per_page)
|
||||||
return entries, random, pagination
|
return entries, random, pagination
|
||||||
|
|
||||||
|
|
||||||
@ -634,16 +650,13 @@ def feed_normal_search():
|
|||||||
|
|
||||||
|
|
||||||
def feed_search(term):
|
def feed_search(term):
|
||||||
if current_user.filter_language() != "all":
|
|
||||||
lang_filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language())
|
|
||||||
else:
|
|
||||||
lang_filter = True
|
|
||||||
if term:
|
if term:
|
||||||
entries = db.session.query(db.Books).filter(db.or_(db.Books.tags.any(db.Tags.name.like("%" + term + "%")),
|
entries = db.session.query(db.Books).filter(db.or_(db.Books.tags.any(db.Tags.name.like("%" + term + "%")),
|
||||||
db.Books.series.any(db.Series.name.like("%" + term + "%")),
|
db.Books.series.any(db.Series.name.like("%" + term + "%")),
|
||||||
db.Books.authors.any(db.Authors.name.like("%" + term + "%")),
|
db.Books.authors.any(db.Authors.name.like("%" + term + "%")),
|
||||||
db.Books.publishers.any(db.Publishers.name.like("%" + term + "%")),
|
db.Books.publishers.any(db.Publishers.name.like("%" + term + "%")),
|
||||||
db.Books.title.like("%" + term + "%"))).filter(lang_filter).all()
|
db.Books.title.like("%" + term + "%")))\
|
||||||
|
.filter(common_filters()).all()
|
||||||
entriescount = len(entries) if len(entries) > 0 else 1
|
entriescount = len(entries) if len(entries) > 0 else 1
|
||||||
pagination = Pagination(1, entriescount, entriescount)
|
pagination = Pagination(1, entriescount, entriescount)
|
||||||
xml = render_title_template('feed.xml', searchterm=term, entries=entries, pagination=pagination)
|
xml = render_title_template('feed.xml', searchterm=term, entries=entries, pagination=pagination)
|
||||||
@ -671,11 +684,8 @@ def feed_new():
|
|||||||
@app.route("/opds/discover")
|
@app.route("/opds/discover")
|
||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
def feed_discover():
|
def feed_discover():
|
||||||
if current_user.filter_language() != "all":
|
entries = db.session.query(db.Books).filter(common_filters()).order_by(func.random())\
|
||||||
lang_filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language())
|
.limit(config.config_books_per_page)
|
||||||
else:
|
|
||||||
lang_filter = True
|
|
||||||
entries = db.session.query(db.Books).filter(lang_filter).order_by(func.random()).limit(config.config_books_per_page)
|
|
||||||
pagination = Pagination(1, config.config_books_per_page, int(config.config_books_per_page))
|
pagination = Pagination(1, config.config_books_per_page, int(config.config_books_per_page))
|
||||||
xml = render_title_template('feed.xml', entries=entries, pagination=pagination)
|
xml = render_title_template('feed.xml', entries=entries, pagination=pagination)
|
||||||
response = make_response(xml)
|
response = make_response(xml)
|
||||||
@ -703,10 +713,6 @@ def feed_hot():
|
|||||||
off = request.args.get("offset")
|
off = request.args.get("offset")
|
||||||
if not off:
|
if not off:
|
||||||
off = 0
|
off = 0
|
||||||
if current_user.filter_language() != "all":
|
|
||||||
lang_filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language())
|
|
||||||
else:
|
|
||||||
lang_filter = True
|
|
||||||
all_books = ub.session.query(ub.Downloads, ub.func.count(ub.Downloads.book_id)).order_by(
|
all_books = ub.session.query(ub.Downloads, ub.func.count(ub.Downloads.book_id)).order_by(
|
||||||
ub.func.count(ub.Downloads.book_id).desc()).group_by(ub.Downloads.book_id)
|
ub.func.count(ub.Downloads.book_id).desc()).group_by(ub.Downloads.book_id)
|
||||||
hot_books = all_books.offset(off).limit(config.config_books_per_page)
|
hot_books = all_books.offset(off).limit(config.config_books_per_page)
|
||||||
@ -715,7 +721,9 @@ def feed_hot():
|
|||||||
downloadBook = db.session.query(db.Books).filter(db.Books.id == book.Downloads.book_id).first()
|
downloadBook = db.session.query(db.Books).filter(db.Books.id == book.Downloads.book_id).first()
|
||||||
if downloadBook:
|
if downloadBook:
|
||||||
entries.append(
|
entries.append(
|
||||||
db.session.query(db.Books).filter(lang_filter).filter(db.Books.id == book.Downloads.book_id).first())
|
db.session.query(db.Books).filter(common_filters())
|
||||||
|
.filter(db.Books.id == book.Downloads.book_id).first()
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
ub.session.query(ub.Downloads).filter(book.Downloads.book_id == ub.Downloads.book_id).delete()
|
ub.session.query(ub.Downloads).filter(book.Downloads.book_id == ub.Downloads.book_id).delete()
|
||||||
ub.session.commit()
|
ub.session.commit()
|
||||||
@ -733,11 +741,7 @@ def feed_authorindex():
|
|||||||
off = request.args.get("offset")
|
off = request.args.get("offset")
|
||||||
if not off:
|
if not off:
|
||||||
off = 0
|
off = 0
|
||||||
if current_user.filter_language() != "all":
|
entries = db.session.query(db.Authors).join(db.books_authors_link).join(db.Books).filter(common_filters())\
|
||||||
lang_filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language())
|
|
||||||
else:
|
|
||||||
lang_filter = True
|
|
||||||
entries = db.session.query(db.Authors).join(db.books_authors_link).join(db.Books).filter(lang_filter)\
|
|
||||||
.group_by('books_authors_link.author').order_by(db.Authors.sort).limit(config.config_books_per_page).offset(off)
|
.group_by('books_authors_link.author').order_by(db.Authors.sort).limit(config.config_books_per_page).offset(off)
|
||||||
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
||||||
len(db.session.query(db.Authors).all()))
|
len(db.session.query(db.Authors).all()))
|
||||||
@ -767,12 +771,8 @@ def feed_categoryindex():
|
|||||||
off = request.args.get("offset")
|
off = request.args.get("offset")
|
||||||
if not off:
|
if not off:
|
||||||
off = 0
|
off = 0
|
||||||
if current_user.filter_language() != "all":
|
entries = db.session.query(db.Tags).join(db.books_tags_link).join(db.Books).filter(common_filters())\
|
||||||
lang_filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language())
|
.group_by('books_tags_link.tag').order_by(db.Tags.name).offset(off).limit(config.config_books_per_page)
|
||||||
else:
|
|
||||||
lang_filter = True
|
|
||||||
entries = db.session.query(db.Tags).join(db.books_tags_link).join(db.Books).filter(lang_filter).\
|
|
||||||
group_by('books_tags_link.tag').order_by(db.Tags.name).offset(off).limit(config.config_books_per_page)
|
|
||||||
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
||||||
len(db.session.query(db.Tags).all()))
|
len(db.session.query(db.Tags).all()))
|
||||||
xml = render_title_template('feed.xml', listelements=entries, folder='feed_category', pagination=pagination)
|
xml = render_title_template('feed.xml', listelements=entries, folder='feed_category', pagination=pagination)
|
||||||
@ -801,12 +801,8 @@ def feed_seriesindex():
|
|||||||
off = request.args.get("offset")
|
off = request.args.get("offset")
|
||||||
if not off:
|
if not off:
|
||||||
off = 0
|
off = 0
|
||||||
if current_user.filter_language() != "all":
|
entries = db.session.query(db.Series).join(db.books_series_link).join(db.Books).filter(common_filters())\
|
||||||
lang_filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language())
|
.group_by('books_series_link.series').order_by(db.Series.sort).offset(off).all()
|
||||||
else:
|
|
||||||
lang_filter = True
|
|
||||||
entries = db.session.query(db.Series).join(db.books_series_link).join(db.Books).filter(lang_filter).\
|
|
||||||
group_by('books_series_link.series').order_by(db.Series.sort).offset(off).all()
|
|
||||||
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
||||||
len(db.session.query(db.Series).all()))
|
len(db.session.query(db.Series).all()))
|
||||||
xml = render_title_template('feed.xml', listelements=entries, folder='feed_series', pagination=pagination)
|
xml = render_title_template('feed.xml', listelements=entries, folder='feed_series', pagination=pagination)
|
||||||
@ -1071,12 +1067,9 @@ def titles_descending(page):
|
|||||||
@app.route('/hot/page/<int:page>')
|
@app.route('/hot/page/<int:page>')
|
||||||
@login_required_if_no_ano
|
@login_required_if_no_ano
|
||||||
def hot_books(page):
|
def hot_books(page):
|
||||||
if current_user.filter_language() != "all":
|
|
||||||
lang_filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language())
|
|
||||||
else:
|
|
||||||
lang_filter = True
|
|
||||||
if current_user.show_detail_random():
|
if current_user.show_detail_random():
|
||||||
random = db.session.query(db.Books).filter(lang_filter).order_by(func.random()).limit(config.config_random_books)
|
random = db.session.query(db.Books).filter(common_filters())\
|
||||||
|
.order_by(func.random()).limit(config.config_random_books)
|
||||||
else:
|
else:
|
||||||
random = false
|
random = false
|
||||||
off = int(int(config.config_books_per_page) * (page - 1))
|
off = int(int(config.config_books_per_page) * (page - 1))
|
||||||
@ -1088,7 +1081,9 @@ def hot_books(page):
|
|||||||
downloadBook = db.session.query(db.Books).filter(db.Books.id == book.Downloads.book_id).first()
|
downloadBook = db.session.query(db.Books).filter(db.Books.id == book.Downloads.book_id).first()
|
||||||
if downloadBook:
|
if downloadBook:
|
||||||
entries.append(
|
entries.append(
|
||||||
db.session.query(db.Books).filter(lang_filter).filter(db.Books.id == book.Downloads.book_id).first())
|
db.session.query(db.Books).filter(common_filters())
|
||||||
|
.filter(db.Books.id == book.Downloads.book_id).first()
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
ub.session.query(ub.Downloads).filter(book.Downloads.book_id == ub.Downloads.book_id).delete()
|
ub.session.query(ub.Downloads).filter(book.Downloads.book_id == ub.Downloads.book_id).delete()
|
||||||
ub.session.commit()
|
ub.session.commit()
|
||||||
@ -1120,13 +1115,9 @@ def discover(page):
|
|||||||
@app.route("/author")
|
@app.route("/author")
|
||||||
@login_required_if_no_ano
|
@login_required_if_no_ano
|
||||||
def author_list():
|
def author_list():
|
||||||
if current_user.filter_language() != "all":
|
entries = db.session.query(db.Authors, func.count('books_authors_link.book').label('count'))\
|
||||||
lang_filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language())
|
.join(db.books_authors_link).join(db.Books).filter(common_filters())\
|
||||||
else:
|
.group_by('books_authors_link.author').order_by(db.Authors.sort).all()
|
||||||
lang_filter = True
|
|
||||||
entries = db.session.query(db.Authors, func.count('books_authors_link.book').label('count')).join(
|
|
||||||
db.books_authors_link).join(db.Books).filter(
|
|
||||||
lang_filter).group_by('books_authors_link.author').order_by(db.Authors.sort).all()
|
|
||||||
return render_title_template('list.html', entries=entries, folder='author', title=_(u"Author list"))
|
return render_title_template('list.html', entries=entries, folder='author', title=_(u"Author list"))
|
||||||
|
|
||||||
|
|
||||||
@ -1136,31 +1127,34 @@ def author_list():
|
|||||||
def author(book_id, page):
|
def author(book_id, page):
|
||||||
entries, random, pagination = fill_indexpage(page, db.Books, db.Books.authors.any(db.Authors.id == book_id),
|
entries, random, pagination = fill_indexpage(page, db.Books, db.Books.authors.any(db.Authors.id == book_id),
|
||||||
db.Books.timestamp.desc())
|
db.Books.timestamp.desc())
|
||||||
if entries:
|
if entries is None:
|
||||||
|
flash(_(u"Error opening eBook. File does not exist or file is not accessible:"), category="error")
|
||||||
|
return redirect(url_for("index"))
|
||||||
|
|
||||||
name = db.session.query(db.Authors).filter(db.Authors.id == book_id).first().name
|
name = db.session.query(db.Authors).filter(db.Authors.id == book_id).first().name
|
||||||
|
|
||||||
author_info = None
|
author_info = None
|
||||||
|
other_books = None
|
||||||
if goodreads_support and config.config_use_goodreads:
|
if goodreads_support and config.config_use_goodreads:
|
||||||
gc = gr_client.GoodreadsClient(config.config_goodreads_api_key, config.config_goodreads_api_secret)
|
gc = GoodreadsClient(config.config_goodreads_api_key, config.config_goodreads_api_secret)
|
||||||
author_info = gc.find_author(author_name=name)
|
author_info = gc.find_author(author_name=name)
|
||||||
|
|
||||||
|
# Get all identifiers (ISBN, Goodreads, etc) and filter author's books by that list so we show fewer duplicates
|
||||||
|
# Note: Not all images will be shown, even though they're available on Goodreads.com.
|
||||||
|
# See https://www.goodreads.com/topic/show/18213769-goodreads-book-images
|
||||||
|
identifiers = reduce(lambda acc, book: acc + map(lambda identifier: identifier.val, book.identifiers), entries.all(), [])
|
||||||
|
other_books = filter(lambda book: book.isbn not in identifiers and book.gid["#text"] not in identifiers, author_info.books)
|
||||||
|
|
||||||
return render_title_template('author.html', entries=entries, pagination=pagination,
|
return render_title_template('author.html', entries=entries, pagination=pagination,
|
||||||
title=name, author=author_info)
|
title=name, author=author_info, other_books=other_books)
|
||||||
else:
|
|
||||||
flash(_(u"Error opening eBook. File does not exist or file is not accessible:"), category="error")
|
|
||||||
return redirect(url_for("index"))
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/series")
|
@app.route("/series")
|
||||||
@login_required_if_no_ano
|
@login_required_if_no_ano
|
||||||
def series_list():
|
def series_list():
|
||||||
if current_user.filter_language() != "all":
|
entries = db.session.query(db.Series, func.count('books_series_link.book').label('count'))\
|
||||||
lang_filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language())
|
.join(db.books_series_link).join(db.Books).filter(common_filters())\
|
||||||
else:
|
.group_by('books_series_link.series').order_by(db.Series.sort).all()
|
||||||
lang_filter = True
|
|
||||||
entries = db.session.query(db.Series, func.count('books_series_link.book').label('count')).join(
|
|
||||||
db.books_series_link).join(db.Books).filter(
|
|
||||||
lang_filter).group_by('books_series_link.series').order_by(db.Series.sort).all()
|
|
||||||
return render_title_template('list.html', entries=entries, folder='series', title=_(u"Series list"))
|
return render_title_template('list.html', entries=entries, folder='series', title=_(u"Series list"))
|
||||||
|
|
||||||
|
|
||||||
@ -1227,13 +1221,9 @@ def language(name, page):
|
|||||||
@app.route("/category")
|
@app.route("/category")
|
||||||
@login_required_if_no_ano
|
@login_required_if_no_ano
|
||||||
def category_list():
|
def category_list():
|
||||||
if current_user.filter_language() != "all":
|
entries = db.session.query(db.Tags, func.count('books_tags_link.book').label('count'))\
|
||||||
lang_filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language())
|
.join(db.books_tags_link).join(db.Books).filter(common_filters())\
|
||||||
else:
|
.group_by('books_tags_link.tag').all()
|
||||||
lang_filter = True
|
|
||||||
entries = db.session.query(db.Tags, func.count('books_tags_link.book').label('count')).join(
|
|
||||||
db.books_tags_link).join(db.Books).filter(
|
|
||||||
lang_filter).group_by('books_tags_link.tag').all()
|
|
||||||
return render_title_template('list.html', entries=entries, folder='category', title=_(u"Category list"))
|
return render_title_template('list.html', entries=entries, folder='category', title=_(u"Category list"))
|
||||||
|
|
||||||
|
|
||||||
@ -1270,11 +1260,7 @@ def toggle_read(book_id):
|
|||||||
@app.route("/book/<int:book_id>")
|
@app.route("/book/<int:book_id>")
|
||||||
@login_required_if_no_ano
|
@login_required_if_no_ano
|
||||||
def show_book(book_id):
|
def show_book(book_id):
|
||||||
if current_user.filter_language() != "all":
|
entries = db.session.query(db.Books).filter(db.Books.id == book_id).filter(common_filters()).first()
|
||||||
lang_filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language())
|
|
||||||
else:
|
|
||||||
lang_filter = True
|
|
||||||
entries = db.session.query(db.Books).filter(db.Books.id == book_id).filter(lang_filter).first()
|
|
||||||
if entries:
|
if entries:
|
||||||
for index in range(0, len(entries.languages)):
|
for index in range(0, len(entries.languages)):
|
||||||
try:
|
try:
|
||||||
@ -1547,15 +1533,12 @@ def update():
|
|||||||
def search():
|
def search():
|
||||||
term = request.args.get("query").strip()
|
term = request.args.get("query").strip()
|
||||||
if term:
|
if term:
|
||||||
if current_user.filter_language() != "all":
|
|
||||||
lang_filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language())
|
|
||||||
else:
|
|
||||||
lang_filter = True
|
|
||||||
entries = db.session.query(db.Books).filter(db.or_(db.Books.tags.any(db.Tags.name.like("%" + term + "%")),
|
entries = db.session.query(db.Books).filter(db.or_(db.Books.tags.any(db.Tags.name.like("%" + term + "%")),
|
||||||
db.Books.series.any(db.Series.name.like("%" + term + "%")),
|
db.Books.series.any(db.Series.name.like("%" + term + "%")),
|
||||||
db.Books.authors.any(db.Authors.name.like("%" + term + "%")),
|
db.Books.authors.any(db.Authors.name.like("%" + term + "%")),
|
||||||
db.Books.publishers.any(db.Publishers.name.like("%" + term + "%")),
|
db.Books.publishers.any(db.Publishers.name.like("%" + term + "%")),
|
||||||
db.Books.title.like("%" + term + "%"))).filter(lang_filter).all()
|
db.Books.title.like("%" + term + "%")))\
|
||||||
|
.filter(common_filters()).all()
|
||||||
return render_title_template('search.html', searchterm=term, entries=entries)
|
return render_title_template('search.html', searchterm=term, entries=entries)
|
||||||
else:
|
else:
|
||||||
return render_title_template('search.html', searchterm="")
|
return render_title_template('search.html', searchterm="")
|
||||||
@ -2273,8 +2256,9 @@ def profile():
|
|||||||
content.sidebar_view += ub.SIDEBAR_READ_AND_UNREAD
|
content.sidebar_view += ub.SIDEBAR_READ_AND_UNREAD
|
||||||
if "show_detail_random" in to_save:
|
if "show_detail_random" in to_save:
|
||||||
content.sidebar_view += ub.DETAIL_RANDOM
|
content.sidebar_view += ub.DETAIL_RANDOM
|
||||||
if "default_language" in to_save:
|
|
||||||
content.default_language = to_save["default_language"]
|
content.mature_content = "show_mature_content" in to_save
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ub.session.commit()
|
ub.session.commit()
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
@ -2319,7 +2303,7 @@ def configuration_helper(origin):
|
|||||||
success = False
|
success = False
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
to_save = request.form.to_dict()
|
to_save = request.form.to_dict()
|
||||||
content = ub.session.query(ub.Settings).first()
|
content = ub.session.query(ub.Settings).first() # type: ub.Settings
|
||||||
if "config_calibre_dir" in to_save:
|
if "config_calibre_dir" in to_save:
|
||||||
if content.config_calibre_dir != to_save["config_calibre_dir"]:
|
if content.config_calibre_dir != to_save["config_calibre_dir"]:
|
||||||
content.config_calibre_dir = to_save["config_calibre_dir"]
|
content.config_calibre_dir = to_save["config_calibre_dir"]
|
||||||
@ -2393,6 +2377,9 @@ def configuration_helper(origin):
|
|||||||
if "config_goodreads_api_secret" in to_save:
|
if "config_goodreads_api_secret" in to_save:
|
||||||
content.config_goodreads_api_secret = to_save["config_goodreads_api_secret"]
|
content.config_goodreads_api_secret = to_save["config_goodreads_api_secret"]
|
||||||
|
|
||||||
|
# Mature Content configuration
|
||||||
|
if "config_mature_content_tags" in to_save:
|
||||||
|
content.config_mature_content_tags = to_save["config_mature_content_tags"].strip()
|
||||||
|
|
||||||
content.config_default_role = 0
|
content.config_default_role = 0
|
||||||
if "admin_role" in to_save:
|
if "admin_role" in to_save:
|
||||||
@ -2470,6 +2457,7 @@ def new_user():
|
|||||||
content.nickname = to_save["nickname"]
|
content.nickname = to_save["nickname"]
|
||||||
content.email = to_save["email"]
|
content.email = to_save["email"]
|
||||||
content.default_language = to_save["default_language"]
|
content.default_language = to_save["default_language"]
|
||||||
|
content.mature_content = "show_mature_content" in to_save
|
||||||
if "locale" in to_save:
|
if "locale" in to_save:
|
||||||
content.locale = to_save["locale"]
|
content.locale = to_save["locale"]
|
||||||
content.sidebar_view = 0
|
content.sidebar_view = 0
|
||||||
@ -2557,7 +2545,7 @@ def edit_mailsettings():
|
|||||||
@login_required
|
@login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def edit_user(user_id):
|
def edit_user(user_id):
|
||||||
content = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
|
content = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() # type: ub.User
|
||||||
downloads = list()
|
downloads = list()
|
||||||
languages = db.session.query(db.Languages).all()
|
languages = db.session.query(db.Languages).all()
|
||||||
for lang in languages:
|
for lang in languages:
|
||||||
@ -2665,6 +2653,8 @@ def edit_user(user_id):
|
|||||||
elif "show_detail_random" not in to_save and content.show_detail_random():
|
elif "show_detail_random" not in to_save and content.show_detail_random():
|
||||||
content.sidebar_view -= ub.DETAIL_RANDOM
|
content.sidebar_view -= ub.DETAIL_RANDOM
|
||||||
|
|
||||||
|
content.mature_content = "show_mature_content" in to_save
|
||||||
|
|
||||||
if "default_language" in to_save:
|
if "default_language" in to_save:
|
||||||
content.default_language = to_save["default_language"]
|
content.default_language = to_save["default_language"]
|
||||||
if "locale" in to_save and to_save["locale"]:
|
if "locale" in to_save and to_save["locale"]:
|
||||||
@ -2691,11 +2681,8 @@ def edit_book(book_id):
|
|||||||
# create the function for sorting...
|
# create the function for sorting...
|
||||||
db.session.connection().connection.connection.create_function("title_sort", 1, db.title_sort)
|
db.session.connection().connection.connection.create_function("title_sort", 1, db.title_sort)
|
||||||
cc = db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all()
|
cc = db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all()
|
||||||
if current_user.filter_language() != "all":
|
book = db.session.query(db.Books)\
|
||||||
lang_filter = db.Books.languages.any(db.Languages.lang_code == current_user.filter_language())
|
.filter(db.Books.id == book_id).filter(common_filters()).first()
|
||||||
else:
|
|
||||||
lang_filter = True
|
|
||||||
book = db.session.query(db.Books).filter(db.Books.id == book_id).filter(lang_filter).first()
|
|
||||||
author_names = []
|
author_names = []
|
||||||
|
|
||||||
# Book not found
|
# Book not found
|
||||||
@ -2738,18 +2725,7 @@ def edit_book(book_id):
|
|||||||
edited_books_id.add(book.id)
|
edited_books_id.add(book.id)
|
||||||
book.author_sort = helper.get_sorted_author(input_authors[0])
|
book.author_sort = helper.get_sorted_author(input_authors[0])
|
||||||
|
|
||||||
if to_save["cover_url"] and os.path.splitext(to_save["cover_url"])[1].lower() == ".jpg":
|
if to_save["cover_url"] and save_cover(to_save["cover_url"], book.path):
|
||||||
img = requests.get(to_save["cover_url"])
|
|
||||||
if config.config_use_google_drive:
|
|
||||||
tmpDir = tempfile.gettempdir()
|
|
||||||
f = open(os.path.join(tmpDir, "uploaded_cover.jpg"), "wb")
|
|
||||||
f.write(img.content)
|
|
||||||
f.close()
|
|
||||||
gdriveutils.uploadFileToEbooksFolder(Gdrive.Instance().drive, os.path.join(book.path, 'cover.jpg'), os.path.join(tmpDir, f.name))
|
|
||||||
else:
|
|
||||||
f = open(os.path.join(config.config_calibre_dir, book.path, "cover.jpg"), "wb")
|
|
||||||
f.write(img.content)
|
|
||||||
f.close()
|
|
||||||
book.has_cover = 1
|
book.has_cover = 1
|
||||||
|
|
||||||
if book.series_index != to_save["series_index"]:
|
if book.series_index != to_save["series_index"]:
|
||||||
@ -2901,6 +2877,25 @@ def edit_book(book_id):
|
|||||||
title=_(u"edit metadata"))
|
title=_(u"edit metadata"))
|
||||||
|
|
||||||
|
|
||||||
|
def save_cover(url, book_path):
|
||||||
|
img = requests.get(url)
|
||||||
|
if img.headers.get('content-type') != 'image/jpeg':
|
||||||
|
return false
|
||||||
|
|
||||||
|
if config.config_use_google_drive:
|
||||||
|
tmpDir = tempfile.gettempdir()
|
||||||
|
f = open(os.path.join(tmpDir, "uploaded_cover.jpg"), "wb")
|
||||||
|
f.write(img.content)
|
||||||
|
f.close()
|
||||||
|
gdriveutils.uploadFileToEbooksFolder(Gdrive.Instance().drive, os.path.join(book_path, 'cover.jpg'), os.path.join(tmpDir, f.name))
|
||||||
|
return true
|
||||||
|
|
||||||
|
f = open(os.path.join(config.config_calibre_dir, book_path, "cover.jpg"), "wb")
|
||||||
|
f.write(img.content)
|
||||||
|
f.close()
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
@app.route("/upload", methods=["GET", "POST"])
|
@app.route("/upload", methods=["GET", "POST"])
|
||||||
@login_required_if_no_ano
|
@login_required_if_no_ano
|
||||||
@upload_required
|
@upload_required
|
||||||
@ -2928,12 +2923,14 @@ def upload():
|
|||||||
|
|
||||||
title = meta.title
|
title = meta.title
|
||||||
author = meta.author
|
author = meta.author
|
||||||
|
tags = meta.tags
|
||||||
title_dir = helper.get_valid_filename(title, False)
|
series = meta.series
|
||||||
author_dir = helper.get_valid_filename(author, False)
|
series_index = meta.series_id
|
||||||
|
title_dir = helper.get_valid_filename(title)
|
||||||
|
author_dir = helper.get_valid_filename(author)
|
||||||
data_name = title_dir
|
data_name = title_dir
|
||||||
filepath = config.config_calibre_dir + os.sep + author_dir + os.sep + title_dir
|
filepath = config.config_calibre_dir + os.sep + author_dir + os.sep + title_dir
|
||||||
saved_filename = filepath + os.sep + data_name + meta.extension
|
saved_filename = filepath + os.sep + data_name + meta.extension.lower()
|
||||||
|
|
||||||
if not os.path.exists(filepath):
|
if not os.path.exists(filepath):
|
||||||
try:
|
try:
|
||||||
@ -2967,6 +2964,14 @@ def upload():
|
|||||||
db_author = db.Authors(author, helper.get_sorted_author(author), "")
|
db_author = db.Authors(author, helper.get_sorted_author(author), "")
|
||||||
db.session.add(db_author)
|
db.session.add(db_author)
|
||||||
|
|
||||||
|
db_series = None
|
||||||
|
is_series = db.session.query(db.Series).filter(db.Series.name == series).first()
|
||||||
|
if is_series:
|
||||||
|
db_series = is_series
|
||||||
|
elif series != '':
|
||||||
|
db_series = db.Series(series, "")
|
||||||
|
db.session.add(db_series)
|
||||||
|
|
||||||
# add language actually one value in list
|
# add language actually one value in list
|
||||||
input_language = meta.languages
|
input_language = meta.languages
|
||||||
db_language = None
|
db_language = None
|
||||||
@ -2980,9 +2985,11 @@ def upload():
|
|||||||
db.session.add(db_language)
|
db.session.add(db_language)
|
||||||
# combine path and normalize path from windows systems
|
# combine path and normalize path from windows systems
|
||||||
path = os.path.join(author_dir, title_dir).replace('\\', '/')
|
path = os.path.join(author_dir, title_dir).replace('\\', '/')
|
||||||
db_book = db.Books(title, "", db_author.sort, datetime.datetime.now(), datetime.datetime(101, 1, 1), 1,
|
db_book = db.Books(title, "", db_author.sort, datetime.datetime.now(), datetime.datetime(101, 1, 1),
|
||||||
datetime.datetime.now(), path, has_cover, db_author, [], db_language)
|
series_index, datetime.datetime.now(), path, has_cover, db_author, [], db_language)
|
||||||
db_book.authors.append(db_author)
|
db_book.authors.append(db_author)
|
||||||
|
if db_series:
|
||||||
|
db_book.series.append(db_series)
|
||||||
if db_language is not None:
|
if db_language is not None:
|
||||||
db_book.languages.append(db_language)
|
db_book.languages.append(db_language)
|
||||||
db_data = db.Data(db_book, meta.extension.upper()[1:], file_size, data_name)
|
db_data = db.Data(db_book, meta.extension.upper()[1:], file_size, data_name)
|
||||||
@ -2995,6 +3002,11 @@ def upload():
|
|||||||
if upload_comment != "":
|
if upload_comment != "":
|
||||||
db.session.add(db.Comments(upload_comment, db_book.id))
|
db.session.add(db.Comments(upload_comment, db_book.id))
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
input_tags = tags.split(',')
|
||||||
|
input_tags = map(lambda it: it.strip(), input_tags)
|
||||||
|
modify_database_object(input_tags, db_book.tags, db.Tags, db.session, 'tags')
|
||||||
|
|
||||||
if db_language is not None: # display Full name instead of iso639.part3
|
if db_language is not None: # display Full name instead of iso639.part3
|
||||||
db_book.languages[0].language_name = _(meta.languages)
|
db_book.languages[0].language_name = _(meta.languages)
|
||||||
author_names = []
|
author_names = []
|
||||||
|
410
messages.pot
410
messages.pot
File diff suppressed because it is too large
Load Diff
@ -30,7 +30,7 @@ Calibre Web is a web app providing a clean interface for browsing, reading and d
|
|||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
1. Install required dependencies by executing `pip install -r requirements.txt`
|
1. Install dependencies by running `pip install --target vendor -r requirements.txt`.
|
||||||
2. Execute the command: `python cps.py` (or `nohup python cps.py` - recommended if you want to exit the terminal window)
|
2. Execute the command: `python cps.py` (or `nohup python cps.py` - recommended if you want to exit the terminal window)
|
||||||
3. Point your browser to `http://localhost:8083` or `http://localhost:8083/opds` for the OPDS catalog
|
3. Point your browser to `http://localhost:8083` or `http://localhost:8083/opds` for the OPDS catalog
|
||||||
4. Set `Location of Calibre database` to the path of the folder where your Calibre library (metadata.db) lives, push "submit" button
|
4. Set `Location of Calibre database` to the path of the folder where your Calibre library (metadata.db) lives, push "submit" button
|
||||||
@ -70,7 +70,7 @@ Optionally, to enable on-the-fly conversion from EPUB to MOBI when using the sen
|
|||||||
|
|
||||||
## Using Google Drive integration
|
## Using Google Drive integration
|
||||||
|
|
||||||
Additional optional dependencys are necessary to get this work. Please install all optional requirements by executing `pip install -r optional-requirements.txt`
|
Additional optional dependencys are necessary to get this work. Please install all optional requirements by executing `pip install --target vendor -r optional-requirements.txt`
|
||||||
|
|
||||||
To use google drive integration, you have to use the google developer console to create a new app. https://console.developers.google.com
|
To use google drive integration, you have to use the google developer console to create a new app. https://console.developers.google.com
|
||||||
|
|
||||||
|
0
vendor/.gitempty
vendored
0
vendor/.gitempty
vendored
Loading…
Reference in New Issue
Block a user