mirror of
				https://github.com/janeczku/calibre-web
				synced 2025-10-31 07:13:02 +00:00 
			
		
		
		
	fix linter errors
Fix the sorting Save the sorting state Remove unnecessary filter Add support for grid view
This commit is contained in:
		
							
								
								
									
										17
									
								
								cps/static/css/caliBlur_override.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								cps/static/css/caliBlur_override.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | |||||||
|  | body.serieslist.grid-view div.container-fluid>div>div.col-sm-10:before{ | ||||||
|  |     display: none; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .cover .badge{ | ||||||
|  |     position: absolute; | ||||||
|  |     top: 0; | ||||||
|  |     right: 0; | ||||||
|  |     background-color: #cc7b19; | ||||||
|  |     border-radius: 0; | ||||||
|  |     padding: 0 8px; | ||||||
|  |     box-shadow: 0 0 4px rgba(0,0,0,.6); | ||||||
|  |     line-height: 24px; | ||||||
|  | } | ||||||
|  | .cover{ | ||||||
|  |     box-shadow: 0 0 4px rgba(0,0,0,.6); | ||||||
|  | } | ||||||
| @@ -64,6 +64,12 @@ span.glyphicon.glyphicon-tags {padding-right: 5px;color: #999;vertical-align: te | |||||||
| .navbar-default .navbar-toggle .icon-bar {background-color: #000;} | .navbar-default .navbar-toggle .icon-bar {background-color: #000;} | ||||||
| .navbar-default .navbar-toggle {border-color: #000;} | .navbar-default .navbar-toggle {border-color: #000;} | ||||||
| .cover { margin-bottom: 10px;} | .cover { margin-bottom: 10px;} | ||||||
|  | .cover .badge{ | ||||||
|  |    position: absolute; | ||||||
|  |    top: 10px; | ||||||
|  |    right: 10px; | ||||||
|  |    background-color: #45b29d; | ||||||
|  | } | ||||||
| .cover-height { max-height: 100px;} | .cover-height { max-height: 100px;} | ||||||
| .col-sm-2 a .cover-small { | .col-sm-2 a .cover-small { | ||||||
|     margin:5px; |     margin:5px; | ||||||
|   | |||||||
							
								
								
									
										62
									
								
								cps/static/js/filter_grid.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								cps/static/js/filter_grid.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | |||||||
|  | /* This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) | ||||||
|  |  *    Copyright (C) 2018 OzzieIsaacs | ||||||
|  |  * | ||||||
|  |  *  This program is free software: you can redistribute it and/or modify | ||||||
|  |  *  it under the terms of the GNU General Public License as published by | ||||||
|  |  *  the Free Software Foundation, either version 3 of the License, or | ||||||
|  |  *  (at your option) any later version. | ||||||
|  |  * | ||||||
|  |  *  This program is distributed in the hope that it will be useful, | ||||||
|  |  *  but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  |  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  |  *  GNU General Public License for more details. | ||||||
|  |  * | ||||||
|  |  *  You should have received a copy of the GNU General Public License | ||||||
|  |  *  along with this program. If not, see <http://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | var $list = $("#list").isotope({ | ||||||
|  |     itemSelector: ".book", | ||||||
|  |     layoutMode: "fitRows", | ||||||
|  |     getSortData: { | ||||||
|  |         title: ".title", | ||||||
|  |     } | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | $("#desc").click(function() { | ||||||
|  |     $list.isotope({ | ||||||
|  |         sortBy: "name", | ||||||
|  |         sortAscending: false | ||||||
|  |     }); | ||||||
|  |     return; | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | $("#asc").click(function() { | ||||||
|  |     $list.isotope({ | ||||||
|  |         sortBy: "name", | ||||||
|  |         sortAscending: true | ||||||
|  |     }); | ||||||
|  |     return; | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | $("#all").click(function() { | ||||||
|  |     // go through all elements and make them visible | ||||||
|  |     $(".sortable").each(function() { | ||||||
|  |         $(this).show(); | ||||||
|  |     }); | ||||||
|  |     // We need to trigger the resize event to have all the grid item to re-align. | ||||||
|  |     window.dispatchEvent(new Event('resize')); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | $(".char").click(function() { | ||||||
|  |     var character = this.innerText; | ||||||
|  |     $(".sortable").each(function() { | ||||||
|  |         if (this.attributes["data-id"].value.charAt(0).toUpperCase() !== character) { | ||||||
|  |             $(this).hide(); | ||||||
|  |         } else { | ||||||
|  |             $(this).show(); | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  |     // We need to trigger the resize event to have all the grid item to re-align. | ||||||
|  |     window.dispatchEvent(new Event("resize")); | ||||||
|  | }); | ||||||
| @@ -325,4 +325,17 @@ $(function() { | |||||||
|         $(".discover .row").isotope("layout"); |         $(".discover .row").isotope("layout"); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|  |     $(".update-view").click(function(e) { | ||||||
|  |         var target = $(this).data("target"); | ||||||
|  |         var view = $(this).data("view"); | ||||||
|  |  | ||||||
|  |         e.preventDefault(); | ||||||
|  |         e.stopPropagation(); | ||||||
|  |         var data = {}; | ||||||
|  |         data[target] = view; | ||||||
|  |         console.debug("Updating view data: ", data); | ||||||
|  |         $.post( "/ajax/view", data).done(function( ) { | ||||||
|  |             location.reload(); | ||||||
|  |         }); | ||||||
|  |     }); | ||||||
| }); | }); | ||||||
|   | |||||||
							
								
								
									
										54
									
								
								cps/templates/grid.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								cps/templates/grid.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | |||||||
|  | {% extends "layout.html" %} | ||||||
|  | {% block body %} | ||||||
|  | <div class="discover load-more grid-view"> | ||||||
|  |   <h2 class="{{title}}">{{_(title)}}</h2> | ||||||
|  |  | ||||||
|  |     <div class="filterheader hidden-xs hidden-sm"> | ||||||
|  |       <button id="asc" class="btn btn-success"><span class="glyphicon glyphicon-sort-by-alphabet"></span></button> | ||||||
|  |       <button id="desc" class="btn btn-success"><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></button> | ||||||
|  |       {% if charlist|length %} | ||||||
|  |       <button id="all" class="btn btn-success">{{_('All')}}</button> | ||||||
|  |       {% endif %} | ||||||
|  |       <div class="btn-group character" role="group"> | ||||||
|  |         {% for char in charlist%} | ||||||
|  |         <button class="btn btn-success char">{{char.char}}</button> | ||||||
|  |         {% endfor %} | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |         <!-- View selection --> | ||||||
|  |         <div class="btn-group"> | ||||||
|  |           <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> | ||||||
|  |             View <span class="caret"></span> | ||||||
|  |           </button> | ||||||
|  |           <ul class="dropdown-menu"> | ||||||
|  |             <li><a class="update-view" href="#" data-target="series_view" data-view="grid">{{_('Grid')}}</a></li> | ||||||
|  |             <li><a class="update-view" href="#"  data-target="series_view" data-view="list">{{_('List')}}</a></li> | ||||||
|  |           </ul> | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     {% if entries[0] %} | ||||||
|  |         <div id="list" class="row"> | ||||||
|  |           {% for entry in entries %} | ||||||
|  |               <div class="col-sm-3 col-lg-2 col-xs-6 book sortable" {% if entry[0].sort %}data-name="{{entry[0].series[0].name}}"{% endif %} data-id="{% if entry[0].series[0].name %}{{entry[0].series[0].name}}{% endif %}"> | ||||||
|  |                   <div class="cover"> | ||||||
|  |                       <a href="{{url_for('web.books_list', data=data, sort='new', book_id=entry[0].series[0].id )}}"> | ||||||
|  |                           <img src="{{ url_for('web.get_cover', book_id=entry[0].id) }}" alt="{{ entry[0].name }}"/> | ||||||
|  |                           <span class="badge">{{entry.count}}</span> | ||||||
|  |                       </a> | ||||||
|  |                   </div> | ||||||
|  |                   <div class="meta"> | ||||||
|  |                       <a href="{{url_for('web.books_list', data=data, sort='new', book_id=entry[0].series[0].id )}}"> | ||||||
|  |                           <p class="title">{{entry[0].series[0].name|shortentitle}}</p> | ||||||
|  |                       </a> | ||||||
|  |                   </div> | ||||||
|  |               </div> | ||||||
|  |         {% endfor %} | ||||||
|  |         </div> | ||||||
|  |     {% endif %} | ||||||
|  |  | ||||||
|  |  | ||||||
|  | {% endblock %} | ||||||
|  | {% block js %} | ||||||
|  | <script src="{{ url_for('static', filename='js/filter_grid.js') }}"></script> | ||||||
|  | {% endblock %} | ||||||
| @@ -17,6 +17,7 @@ | |||||||
|     <link href="{{ url_for('static', filename='css/upload.css') }}" rel="stylesheet" media="screen"> |     <link href="{{ url_for('static', filename='css/upload.css') }}" rel="stylesheet" media="screen"> | ||||||
|     {% if g.current_theme == 1 %} |     {% if g.current_theme == 1 %} | ||||||
|        <link href="{{ url_for('static', filename='css/caliBlur.min.css') }}" rel="stylesheet" media="screen"> |        <link href="{{ url_for('static', filename='css/caliBlur.min.css') }}" rel="stylesheet" media="screen"> | ||||||
|  |        <link href="{{ url_for('static', filename='css/caliBlur_override.css') }}" rel="stylesheet" media="screen"> | ||||||
|     {% endif %} |     {% endif %} | ||||||
|     <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries --> |     <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries --> | ||||||
|     <!-- WARNING: Respond.js doesn't work if you view the page via file:// --> |     <!-- WARNING: Respond.js doesn't work if you view the page via file:// --> | ||||||
| @@ -25,7 +26,7 @@ | |||||||
|       <script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script> |       <script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script> | ||||||
|     <![endif]--> |     <![endif]--> | ||||||
|   </head> |   </head> | ||||||
|   <body class="{{ page }}" data-text="{{_('Home')}}" data-textback="{{_('Back')}}"> |   <body class="{{ page }} {{ bodyClass }}" data-text="{{_('Home')}}" data-textback="{{_('Back')}}"> | ||||||
|     <!-- Static navbar --> |     <!-- Static navbar --> | ||||||
|     <div class="navbar navbar-default navbar-static-top" role="navigation"> |     <div class="navbar navbar-default navbar-static-top" role="navigation"> | ||||||
|       <div class="container-fluid"> |       <div class="container-fluid"> | ||||||
|   | |||||||
| @@ -18,6 +18,17 @@ | |||||||
|         <button class="btn btn-primary char">{{char.char}}</button> |         <button class="btn btn-primary char">{{char.char}}</button> | ||||||
|         {% endfor %} |         {% endfor %} | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|  |         <!-- View selection --> | ||||||
|  |         <div class="btn-group"> | ||||||
|  |           <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> | ||||||
|  |             View <span class="caret"></span> | ||||||
|  |           </button> | ||||||
|  |           <ul class="dropdown-menu"> | ||||||
|  |             <li><a class="update-view" href="#" data-target="series_view" data-view="grid">{{_('Grid')}}</a></li> | ||||||
|  |             <li><a class="update-view" href="#"  data-target="series_view" data-view="list">{{_('List')}}</a></li> | ||||||
|  |           </ul> | ||||||
|  |         </div> | ||||||
|     </div> |     </div> | ||||||
|   <div class="container"> |   <div class="container"> | ||||||
|     <div id="list" class="col-xs-12 col-sm-6"> |     <div id="list" class="col-xs-12 col-sm-6"> | ||||||
|   | |||||||
							
								
								
									
										15
									
								
								cps/ub.py
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								cps/ub.py
									
									
									
									
									
								
							| @@ -204,6 +204,7 @@ class User(UserBase, Base): | |||||||
|     denied_column_value = Column(String, default="") |     denied_column_value = Column(String, default="") | ||||||
|     allowed_column_value = Column(String, default="") |     allowed_column_value = Column(String, default="") | ||||||
|     remote_auth_token = relationship('RemoteAuthToken', backref='user', lazy='dynamic') |     remote_auth_token = relationship('RemoteAuthToken', backref='user', lazy='dynamic') | ||||||
|  |     series_view = Column(String(10), default="list") | ||||||
|  |  | ||||||
|  |  | ||||||
| if oauth_support: | if oauth_support: | ||||||
| @@ -243,6 +244,7 @@ class Anonymous(AnonymousUserMixin, UserBase): | |||||||
|         self.allowed_tags = data.allowed_tags |         self.allowed_tags = data.allowed_tags | ||||||
|         self.denied_column_value = data.denied_column_value |         self.denied_column_value = data.denied_column_value | ||||||
|         self.allowed_column_value = data.allowed_column_value |         self.allowed_column_value = data.allowed_column_value | ||||||
|  |         self.series_view = data.series_view | ||||||
|  |  | ||||||
|     def role_admin(self): |     def role_admin(self): | ||||||
|         return False |         return False | ||||||
| @@ -349,7 +351,7 @@ class RemoteAuthToken(Base): | |||||||
|  |  | ||||||
|  |  | ||||||
| # Migrate database to current version, has to be updated after every database change. Currently migration from | # Migrate database to current version, has to be updated after every database change. Currently migration from | ||||||
| # everywhere to curent should work. Migration is done by checking if relevant coloums are existing, and than adding | # everywhere to current should work. Migration is done by checking if relevant columns are existing, and than adding | ||||||
| # rows with SQL commands | # rows with SQL commands | ||||||
| def migrate_Database(session): | def migrate_Database(session): | ||||||
|     engine = session.bind |     engine = session.bind | ||||||
| @@ -428,6 +430,12 @@ def migrate_Database(session): | |||||||
|         conn.execute("ALTER TABLE user ADD column `allowed_tags` String DEFAULT ''") |         conn.execute("ALTER TABLE user ADD column `allowed_tags` String DEFAULT ''") | ||||||
|         conn.execute("ALTER TABLE user ADD column `denied_column_value` DEFAULT ''") |         conn.execute("ALTER TABLE user ADD column `denied_column_value` DEFAULT ''") | ||||||
|         conn.execute("ALTER TABLE user ADD column `allowed_column_value` DEFAULT ''") |         conn.execute("ALTER TABLE user ADD column `allowed_column_value` DEFAULT ''") | ||||||
|  |     try: | ||||||
|  |         session.query(exists().where(User.series_view)).scalar() | ||||||
|  |     except exc.OperationalError: | ||||||
|  |         conn = engine.connect() | ||||||
|  |         conn.execute("ALTER TABLE user ADD column `series_view` VARCHAR(10) DEFAULT 'list'") | ||||||
|  |  | ||||||
|     if session.query(User).filter(User.role.op('&')(constants.ROLE_ANONYMOUS) == constants.ROLE_ANONYMOUS).first() is None: |     if session.query(User).filter(User.role.op('&')(constants.ROLE_ANONYMOUS) == constants.ROLE_ANONYMOUS).first() is None: | ||||||
|         create_anonymous_user(session) |         create_anonymous_user(session) | ||||||
|     try: |     try: | ||||||
| @@ -446,10 +454,11 @@ def migrate_Database(session): | |||||||
|                             "locale VARCHAR(2)," |                             "locale VARCHAR(2)," | ||||||
|                             "sidebar_view INTEGER," |                             "sidebar_view INTEGER," | ||||||
|                             "default_language VARCHAR(3)," |                             "default_language VARCHAR(3)," | ||||||
|  |                             "series_view VARCHAR(10)," | ||||||
|                             "UNIQUE (nickname)," |                             "UNIQUE (nickname)," | ||||||
|                             "UNIQUE (email))") |                             "UNIQUE (email))") | ||||||
|         conn.execute("INSERT INTO user_id(id, nickname, email, role, password, kindle_mail,locale," |         conn.execute("INSERT INTO user_id(id, nickname, email, role, password, kindle_mail,locale," | ||||||
|                         "sidebar_view, default_language) " |                         "sidebar_view, default_language, series_view) " | ||||||
|                      "SELECT id, nickname, email, role, password, kindle_mail, locale," |                      "SELECT id, nickname, email, role, password, kindle_mail, locale," | ||||||
|                         "sidebar_view, default_language FROM user") |                         "sidebar_view, default_language FROM user") | ||||||
|         # delete old user table and rename new user_id table to user: |         # delete old user table and rename new user_id table to user: | ||||||
| @@ -486,7 +495,7 @@ def delete_download(book_id): | |||||||
|     session.query(Downloads).filter(book_id == Downloads.book_id).delete() |     session.query(Downloads).filter(book_id == Downloads.book_id).delete() | ||||||
|     session.commit() |     session.commit() | ||||||
|  |  | ||||||
| # Generate user Guest (translated text), as anoymous user, no rights | # Generate user Guest (translated text), as anonymous user, no rights | ||||||
| def create_anonymous_user(session): | def create_anonymous_user(session): | ||||||
|     user = User() |     user = User() | ||||||
|     user.nickname = "Guest" |     user.nickname = "Guest" | ||||||
|   | |||||||
							
								
								
									
										48
									
								
								cps/web.py
									
									
									
									
									
								
							
							
						
						
									
										48
									
								
								cps/web.py
									
									
									
									
									
								
							| @@ -37,7 +37,7 @@ from flask import Blueprint | |||||||
| from flask import render_template, request, redirect, send_from_directory, make_response, g, flash, abort, url_for | from flask import render_template, request, redirect, send_from_directory, make_response, g, flash, abort, url_for | ||||||
| from flask_babel import gettext as _ | from flask_babel import gettext as _ | ||||||
| from flask_login import login_user, logout_user, login_required, current_user | from flask_login import login_user, logout_user, login_required, current_user | ||||||
| from sqlalchemy.exc import IntegrityError | from sqlalchemy.exc import IntegrityError, InvalidRequestError | ||||||
| from sqlalchemy.sql.expression import text, func, true, false, not_, and_, exists, or_ | from sqlalchemy.sql.expression import text, func, true, false, not_, and_, exists, or_ | ||||||
| from werkzeug.exceptions import default_exceptions | from werkzeug.exceptions import default_exceptions | ||||||
| from werkzeug.datastructures import Headers | from werkzeug.datastructures import Headers | ||||||
| @@ -416,6 +416,25 @@ def toggle_read(book_id): | |||||||
|     return "" |     return "" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @web.route("/ajax/view", methods=["POST"]) | ||||||
|  | @login_required | ||||||
|  | def update_view(): | ||||||
|  |     to_save = request.form.to_dict() | ||||||
|  |     allowed_view = ['grid', 'list'] | ||||||
|  |     if "series_view" in to_save and to_save["series_view"] in allowed_view: | ||||||
|  |         current_user.series_view = to_save["series_view"] | ||||||
|  |     else: | ||||||
|  |         log.error("Invalid request received: %r %r", request, to_save) | ||||||
|  |         return "Invalid request", 400 | ||||||
|  |  | ||||||
|  |     try: | ||||||
|  |         ub.session.commit() | ||||||
|  |     except InvalidRequestError: | ||||||
|  |         log.error("Invalid request received: %r ", request, ) | ||||||
|  |         return "Invalid request", 400 | ||||||
|  |     return "", 200 | ||||||
|  |  | ||||||
|  |  | ||||||
| ''' | ''' | ||||||
| @web.route("/ajax/getcomic/<int:book_id>/<book_format>/<int:page>") | @web.route("/ajax/getcomic/<int:book_id>/<book_format>/<int:page>") | ||||||
| @login_required | @login_required | ||||||
| @@ -775,14 +794,25 @@ def publisher_list(): | |||||||
| @login_required_if_no_ano | @login_required_if_no_ano | ||||||
| def series_list(): | def series_list(): | ||||||
|     if current_user.check_visibility(constants.SIDEBAR_SERIES): |     if current_user.check_visibility(constants.SIDEBAR_SERIES): | ||||||
|         entries = db.session.query(db.Series, func.count('books_series_link.book').label('count')) \ |         if current_user.series_view == 'list': | ||||||
|             .join(db.books_series_link).join(db.Books).filter(common_filters()) \ |             entries = db.session.query(db.Series, func.count('books_series_link.book').label('count')) \ | ||||||
|             .group_by(text('books_series_link.series')).order_by(db.Series.sort).all() |                 .join(db.books_series_link).join(db.Books).filter(common_filters()) \ | ||||||
|         charlist = db.session.query(func.upper(func.substr(db.Series.sort, 1, 1)).label('char')) \ |                 .group_by(text('books_series_link.series')).order_by(db.Series.sort).all() | ||||||
|             .join(db.books_series_link).join(db.Books).filter(common_filters()) \ |             charlist = db.session.query(func.upper(func.substr(db.Series.sort, 1, 1)).label('char')) \ | ||||||
|             .group_by(func.upper(func.substr(db.Series.sort, 1, 1))).all() |                 .join(db.books_series_link).join(db.Books).filter(common_filters()) \ | ||||||
|         return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=charlist, |                 .group_by(func.upper(func.substr(db.Series.sort, 1, 1))).all() | ||||||
|                                      title=_(u"Series"), page="serieslist", data="series") |             return render_title_template('list.html', entries=entries, folder='web.books_list', charlist=charlist, | ||||||
|  |                                          title=_(u"Series"), page="serieslist", data="series") | ||||||
|  |         else: | ||||||
|  |             entries = db.session.query(db.Books, func.count('books_series_link').label('count')) \ | ||||||
|  |                 .join(db.books_series_link).join(db.Series).filter(common_filters()) \ | ||||||
|  |                 .group_by(text('books_series_link.series')).order_by(db.Series.sort).all() | ||||||
|  |             charlist = db.session.query(func.upper(func.substr(db.Series.sort, 1, 1)).label('char')) \ | ||||||
|  |                 .join(db.books_series_link).join(db.Books).filter(common_filters()) \ | ||||||
|  |                 .group_by(func.upper(func.substr(db.Series.sort, 1, 1))).all() | ||||||
|  |  | ||||||
|  |             return render_title_template('grid.html', entries=entries, folder='web.books_list', charlist=charlist, | ||||||
|  |                                          title=_(u"Series list"), page="serieslist", data="series", bodyClass="grid-view") | ||||||
|     else: |     else: | ||||||
|         abort(404) |         abort(404) | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 pthiben
					pthiben