Compare commits

..

No commits in common. 'nightly' and 'main' have entirely different histories.

  1. 350
      stashr/api.py
  2. 10
      stashr/database.py
  3. 9
      stashr/folders.py
  4. 67
      stashr/routes.py
  5. 23
      stashr/server.py
  6. 9
      stashr/stashr.py
  7. 130
      stashr/static/assets/title.svg
  8. 2
      stashr/static/css/bootstrap-grid.css
  9. 2
      stashr/static/css/bootstrap-grid.rtl.css
  10. 10
      stashr/static/css/bootstrap-reboot.css
  11. 10
      stashr/static/css/bootstrap-reboot.rtl.css
  12. 112
      stashr/static/css/bootstrap-utilities.css
  13. 96
      stashr/static/css/bootstrap-utilities.rtl.css
  14. 303
      stashr/static/css/bootstrap.css
  15. 287
      stashr/static/css/bootstrap.rtl.css
  16. 2
      stashr/static/css/stashr-read.css
  17. 111
      stashr/static/css/stashr.css
  18. 4362
      stashr/static/js/bootstrap.bundle.js
  19. 4038
      stashr/static/js/bootstrap.esm.js
  20. 4060
      stashr/static/js/bootstrap.js
  21. 86
      stashr/static/js/service-worker.js
  22. 23
      stashr/static/js/stashr.js
  23. 12
      stashr/static/manifest/manifest.json
  24. 10
      stashr/static/offline.html
  25. 38
      stashr/templates/all_collections_page.html
  26. 26
      stashr/templates/all_publishers_page.html
  27. 213
      stashr/templates/all_volumes_page.html
  28. 255
      stashr/templates/base.html
  29. 110
      stashr/templates/first_run_page.html
  30. 89
      stashr/templates/login_page.html
  31. 100
      stashr/templates/new_releases_page.html
  32. 6
      stashr/templates/read_issue_page.html
  33. 88
      stashr/templates/reading_list_page.html
  34. 45
      stashr/templates/register_page.html
  35. 186
      stashr/templates/scrape_page.html
  36. 49
      stashr/templates/search_page.html
  37. 225
      stashr/templates/settings_page.html
  38. 16
      stashr/templates/settings_page_all_users.html
  39. 13
      stashr/templates/settings_page_app.html
  40. 13
      stashr/templates/settings_page_directories.html
  41. 13
      stashr/templates/settings_page_mail.html
  42. 86
      stashr/templates/settings_page_new_user.html
  43. 225
      stashr/templates/settings_page_plugins.html
  44. 16
      stashr/templates/settings_page_single_user.html
  45. 81
      stashr/templates/settings_page_tasks.html
  46. 194
      stashr/templates/single_collection_page.html
  47. 74
      stashr/templates/single_publisher_page.html
  48. 352
      stashr/templates/single_volume_page.html
  49. 217
      stashr/utils.py

@ -2064,7 +2064,6 @@ def api_get_plugins():
list_item['plugin_url'] = item.plugin_url
list_item['plugin_license'] = item.plugin_license
list_item['plugin_state'] = item.plugin_state
list_item['plugin_package_name'] = item.plugin_package_name
# list_item[]
data.append(list_item)
@ -2122,45 +2121,6 @@ def api_get_directories():
return create_json_return('200', results=data, total_results=total_results)
@api.route('/scrape/directories/candidates/<scrape_id>', methods=['GET'])
def api_get_directories_candidates(scrape_id):
"""To Update Later
This is using docstrings for specifications.
---
tags:
- scrape
"""
user = current_user
if not user.is_authenticated:
api_key = request.args.get('api_key')
if api_key == "":
return jsonify(create_json_return('100'))
user = database.session \
.query(database.Users) \
.filter(database.Users.api_key == api_key) \
.first()
if user is None:
return jsonify(create_json_return('100'))
if user.role != 'admin':
return jsonify(create_json_return('401'))
scrape_item = database.session \
.query(database.ScrapeItems) \
.filter(database.ScrapeItems.scrape_id == scrape_id) \
.first()
if scrape_item is None:
return jsonify(create_json_return('404'))
data = scrape_item.scrape_json
return create_json_return('200', results=data)
@api.route('/scrape/directories/scan', methods=['POST'])
def api_post_directories_scan():
"""To Update Later
@ -2277,8 +2237,7 @@ def api_put_directories_edit(scrape_id):
allowed_keys = [
'scrape_add',
'scrape_candidate',
'scrape_ignore_directory'
'scrape_candidate'
]
check_directory = database.session \
@ -2290,318 +2249,17 @@ def api_put_directories_edit(scrape_id):
return jsonify(create_json_return('404'))
for key, value in request.json['data'].items():
print(f'{key} - {value}')
if key not in allowed_keys:
database.session.rollback()
database.session.rollbak()
return jsonify(create_json_return('400'))
setattr(check_directory, key, value)
try:
database.session.merge(check_directory)
database.session.commit()
except Exception as e:
print(e)
database.session.rollback()
return jsonify(create_json_return('200'))
@api.route('/scrape/directories/extend/<scrape_id>', methods=['POST'])
def api_post_directories_extend(scrape_id):
"""To Update Later
This is using docstrings for specifications.
---
tags:
- scrape
"""
user = current_user
if not user.is_authenticated:
if not request.json:
return jsonify(create_json_return('400'))
if "api_key" not in request.json:
return jsonify(create_json_return('400'))
if request.json['api_key'] == "":
return jsonify(create_json_return('100'))
user = database.session \
.query(database.Users) \
.filter(database.Users.api_key == request.json['api_key']) \
.first()
if user is None:
return jsonify(create_json_return('100'))
if user.role != 'admin':
return jsonify(create_json_return('401'))
allowed_keys = [
'search_terms'
]
print(f'JSON: {request.data}')
if not request.json:
return jsonify(create_json_return('400'))
if "data" not in request.json:
return jsonify(create_json_return('400'))
if "search_terms" not in request.json['data']:
return jsonify(create_json_return('400'))
for key, value in request.json['data'].items():
if key not in allowed_keys:
return jsonify(create_json_return('400'))
search_terms = request.json['data']['search_terms']
# DO THINGS HERE
utils.new_scrape_extend_matches(scrape_id, search_terms)
# tasks.extend_scrape(scrape_id)
return jsonify(create_json_return('200'))
@api.route('/scrape/directories', methods=['DELETE'])
def api_delete_scrape_directories():
"""To Update Later
This is using docstrings for specifications.
---
tags:
- scrape
"""
user = current_user
if not user.is_authenticated:
if not request.json:
return jsonify(create_json_return('400'))
if "api_key" not in request.json:
return jsonify(create_json_return('400'))
if request.json['api_key'] == "":
return jsonify(create_json_return('100'))
user = database.session \
.query(database.Users) \
.filter(database.Users.api_key == request.json['api_key']) \
.first()
if user is None:
return jsonify(create_json_return('100'))
if user.role != 'admin':
return jsonify(create_json_return('401'))
database.session \
.query(database.ScrapeItems) \
.delete()
database.session \
.query(database.Directories) \
.filter(database.Directories.directory_in_library == 0) \
.delete()
database.session.merge(check_directory)
database.session.commit()
# print(check_directory.scrape_id)
return jsonify(create_json_return('200'))
""" --- PLUGIN UPLOAD/INSTALL --- """
@api.route('/plugins/upload', methods=['POST'])
def api_post_upload_plugin():
"""To Update Later
This is using docstrings for specifications.
---
tags:
- plugins
"""
user = current_user
if not user.is_authenticated:
if not request.json:
return jsonify(create_json_return('400'))
if "api_key" not in request.json:
return jsonify(create_json_return('400'))
if request.json['api_key'] == "":
return jsonify(create_json_return('100'))
user = database.session \
.query(database.Users) \
.filter(database.Users.api_key == request.json['api_key']) \
.first()
if user is None:
return jsonify(create_json_return('100'))
if user.role != 'admin':
return jsonify(create_json_return('401'))
if 'file' not in request.files:
return jsonify(create_json_return('400'))
# DO THINGS HERE
file = request.files['file']
file_path =os.path.join(
folders.StashrFolders().temp_folder(),
file.filename
)
file.save(file_path)
utils.install_plugin(file_path)
return jsonify(create_json_return('200'))
@api.route('/plugins/enable/<plugin>', methods=['POST'])
def api_post_plugin_enable(plugin):
"""To Update Later
This is using docstrings for specifications.
---
tags:
- plugins
"""
user = current_user
if not user.is_authenticated:
if not request.json:
return jsonify(create_json_return('400'))
if "api_key" not in request.json:
return jsonify(create_json_return('400'))
if request.json['api_key'] == "":
return jsonify(create_json_return('100'))
user = database.session \
.query(database.Users) \
.filter(database.Users.api_key == request.json['api_key']) \
.first()
if user is None:
return jsonify(create_json_return('100'))
if user.role != 'admin':
return jsonify(create_json_return('401'))
utils.enable_plugin(plugin)
return jsonify(create_json_return('200'))
@api.route('/plugins/disable/<plugin>', methods=['POST'])
def api_post_plugin_disable(plugin):
"""To Update Later
This is using docstrings for specifications.
---
tags:
- plugins
"""
user = current_user
if not user.is_authenticated:
if not request.json:
return jsonify(create_json_return('400'))
if "api_key" not in request.json:
return jsonify(create_json_return('400'))
if request.json['api_key'] == "":
return jsonify(create_json_return('100'))
user = database.session \
.query(database.Users) \
.filter(database.Users.api_key == request.json['api_key']) \
.first()
if user is None:
return jsonify(create_json_return('100'))
if user.role != 'admin':
return jsonify(create_json_return('401'))
utils.disable_plugin(plugin)
return jsonify(create_json_return('200'))
@api.route('/plugins/remove/<plugin>', methods=['POST'])
def api_post_plugin_remove(plugin):
"""To Update Later
This is using docstrings for specifications.
---
tags:
- plugins
"""
user = current_user
if not user.is_authenticated:
if not request.json:
return jsonify(create_json_return('400'))
if "api_key" not in request.json:
return jsonify(create_json_return('400'))
if request.json['api_key'] == "":
return jsonify(create_json_return('100'))
user = database.session \
.query(database.Users) \
.filter(database.Users.api_key == request.json['api_key']) \
.first()
if user is None:
return jsonify(create_json_return('100'))
if user.role != 'admin':
return jsonify(create_json_return('401'))
utils.uninstall_plugin(plugin)
return jsonify(create_json_return('200'))
""" --- RESTART SERVER --- """
@api.route('/restart', methods=['POST'])
def restart_server():
"""To Update Later
This is using docstrings for specifications.
---
tags:
- server
"""
user = current_user
if not user.is_authenticated:
if not request.json:
return jsonify(create_json_return('400'))
if "api_key" not in request.json:
return jsonify(create_json_return('400'))
if request.json['api_key'] == "":
return jsonify(create_json_return('100'))
user = database.session \
.query(database.Users) \
.filter(database.Users.api_key == request.json['api_key']) \
.first()
if user is None:
return jsonify(create_json_return('100'))
if user.role != 'admin':
return jsonify(create_json_return('401'))
if hasattr(signal, 'SIGHUP'):
os.kill(os.getpid(), signal.SIGHUP)
else:
print('no sighup')
import ctypes
ucrtbase = ctypes.CDLL('ucrtbase')
c_raise = ucrtbase['raise']
c_raise(signal.SIGINT)
return create_json_return('200')
""" --- API WRAPPER --- """

@ -49,7 +49,6 @@ from flask_bcrypt import generate_password_hash
""" --- SQLALCHEMY IMPORTS --- """
from sqlalchemy import *
from sqlalchemy import exc
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import *
@ -442,8 +441,6 @@ class ScrapeItems(Base):
scrape_candidate = Column(Integer)
scrape_json = Column(String)
scrape_ignore_directory = Column(Boolean, server_default='0')
directory = relationship('Directories',
primaryjoin='ScrapeItems.scrape_directory_id == Directories.directory_id',
backref='scrape_items',
@ -656,13 +653,8 @@ def migrate_database():
# CHECK FOR NEW DATABASE TABLES
if not engine.dialect.has_table(engine.connect(), "directory_link"):
Directories.__table__.create(bind=engine)
# CHECK FOR NEW TABLE COLUMNS
try:
session.query(exists().where(ScrapeItems.scrape_ignore_directory)).scalar()
except exc.OperationalError:
with engine.connect() as conn:
conn.execute("ALTER TABLE scrape_items ADD column 'scrape_ignore_directory' BOOLEAN DEFAULT 0")
# Create Database
if not os.path.exists(folders.StashrPaths().db_path()):

@ -138,19 +138,10 @@ class StashrFolders:
stashrconfig['DIRECTORY']['plugins']
)
init_file = os.path.join(
folder,
'__init__.py'
)
if not os.path.isdir(folder):
logger.info('Creating Plugins Folder')
os.mkdir(folder)
if not os.path.isfile(init_file):
with open(init_file, 'w') as f:
f.write('')
return folder

@ -116,18 +116,11 @@ def index_page():
flash('First Run')
return redirect(url_for('first_run_page'))
if not current_user.is_authenticated:
flash('Please Log IN', 'Info')
return redirect(url_for('login_page'))
return redirect(url_for('all_volumes_page'))
"""
return render_template(
'index_page.html',
title='Home',
open_registration=stashrconfig['APP']['open_registration']
)
"""
# LOGIN
@ -163,6 +156,28 @@ def login_page():
)
# REGISTER
@app.route('/register', methods=['GET', 'POST'])
def register_page():
if not stashrconfig['APP']['open_registration']:
flash('Registration Closed', 'error')
return redirect(url_for('login_page'))
registration_form = forms.registration_form()
if registration_form.validate_on_submit() and registration_form.register_button.data:
utils.register_new_user(registration_form)
return redirect(url_for('login_page'))
return render_template(
'register_page.html',
registration_form=registration_form,
title='Register',
open_registration=stashrconfig['APP']['open_registration']
)
# FORGOT PASSWORD
@app.route('/forgot', methods=['GET', 'POST'])
def forgot_page():
@ -196,7 +211,7 @@ def logout_page():
logout_user()
# identity_changed.send(current_app._get_current_object(), identity=AnonymousIdentity())
flash('Logged Out', 'info')
return redirect(url_for('login_page'))
return redirect(url_for('index_page'))
""" --- SETTINGS --- """
@ -462,21 +477,21 @@ def settings_plugins_page():
# FIRST RUN
@app.route('/firstrun', methods=['GET', 'POST'])
def first_run_page():
if current_user.is_authenticated:
flash('App Configured', 'info')
return redirect(url_for('login_page'))
return redirect(url_for('index_page'))
if not stashrconfig['APP']['first_run']:
flash('App Configured', 'info')
return redirect(url_for('login_page'))
return redirect(url_for('index_page'))
first_run_form = forms.app_first_run_form()
if request.method == 'POST' and first_run_form.first_run_button.data and first_run_form.validate():
if request.method == 'POST' and first_run_form.first_run_button.data:
utils.complete_first_run(first_run_form)
flash('Setup Complete', 'success')
return redirect(url_for('login_page'))
return redirect(url_for('index_page'))
return render_template(
'first_run_page.html',
@ -690,30 +705,6 @@ def custom_image_static(foldername, filename):
filename
)
@app.route('/service-worker.js')
def custom_service_worker():
return app.send_static_file('js/service-worker.js')
""" --- v TEST AREA v --- """
@app.route('/test_login')
def test_login():
login_form = forms.login_form()
return render_template(
'test_login.html',
login_form = login_form
)
@app.route('/test_base')
def test_base():
return render_template(
'test_base.html'
)
""" --- ^ TEST AREA ^ --- """
"""-------------------------------------------------------------------------------------------
-- DEVELOPMENT

@ -34,7 +34,7 @@ SOFTWARE.
-------------------------------------------------------------------------------------------"""
""" --- PYTHON IMPORTS --- """
import os, signal, sys, subprocess
import os, signal
from socket import error as SocketError
""" --- STASHR DEPENDENCY IMPORTS --- """
@ -86,23 +86,20 @@ class Server:
app.logger.debug('Unknown Error while starting gevent')
def start_server(self):
app.logger.info('STARTING SERVER')
app.logger.debug('STARTING SERVER')
self.define_wsgi()
self.wsgiserver.serve_forever()
if self.restart:
app.logger.info('RESTARTING SERVER')
try:
subprocess.call([sys.executable, 'start.py'], close_fds=True)
except subprocess.CalledProcessError as e:
print(e)
def stop_server(self):
app.logger.info('STOPPING SERVER')
app.logger.debug('STOPPING SERVER')
self.wsgiserver.stop()
def restart_server(self, ignored_signum=None, ignored_frame=None):
self.restart = True
if self.wsgiserver is not None :
self.wsgiserver.close()
def restart_server(self, ignored_signum, ignored_frame):
app.logger.debug('Restarting Server')
print('restatr')
self.wsgiserver.close()
# self.define_wsgi()
self.wsgiserver.start()
server = Server()

@ -43,6 +43,7 @@ import os
""" --- STASHR DEPENDENCY IMPORTS --- """
""" --- STASHR CORE IMPORTS --- """
from stashr import paths, log, comicvine, config, api
from stashr import tasks
""" --- FLASK IMPORT --- """
from flask import Flask
@ -72,16 +73,8 @@ logger = log.stashr_logger(__name__)
""" --- CREATE SIGNALS --- """
namespace = Namespace()
# STASHR NOTIFICATIONS
stashr_notification = namespace.signal('stashr_notification')
# STASHR NEW RELEASE UPDATED
stashr_new_releases_update = namespace.signal('stashr_new_releases_update')
# STASHR TASK COMPLETED
stashr_task_complete = namespace.signal('stashr_task_complete')
# STASHR VOLUME?ISSUE ADD
stashr_volume_added = namespace.signal('stashr_volume_added')
stashr_issue_added = namespace.signal('stashr_image_added')
stashr_image_downloaded = namespace.signal('stashr_image_downloaded')
""" --- CREATE READING LIST --- """
# reading_image_list = []

@ -1,130 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="110px" height="40px" viewBox="0 0 110 40" enable-background="new 0 0 110 40" xml:space="preserve">
<g>
<path fill="#FFFFFF" d="M9.957,39.944c-1.123,0-2.055-0.126-2.797-0.376c-0.743-0.251-1.329-0.531-1.761-0.842
c-0.432-0.311-0.733-0.608-0.907-0.894c-0.171-0.285-0.259-0.469-0.259-0.557c0-0.31,0.07-0.643,0.208-0.996
c0.138-0.355,0.311-0.683,0.518-0.984c0.207-0.303,0.432-0.557,0.673-0.765c0.242-0.207,0.457-0.311,0.647-0.311
c0.241,0,0.439,0.156,0.596,0.467c0.034,0.035,0.121,0.121,0.258,0.259c0.139,0.139,0.333,0.281,0.583,0.428
c0.25,0.146,0.561,0.28,0.933,0.4c0.371,0.121,0.807,0.183,1.308,0.183c1.087,0,1.976-0.333,2.667-0.999
c0.69-0.662,1.036-1.565,1.036-2.704c0-0.795-0.187-1.503-0.557-2.124c-0.371-0.622-0.859-1.2-1.463-1.734
c-0.604-0.536-1.294-1.059-2.071-1.567c-0.776-0.509-1.571-1.04-2.382-1.593c-0.812-0.551-1.605-1.147-2.382-1.786
c-0.778-0.639-1.467-1.373-2.072-2.202c-0.604-0.828-1.092-1.764-1.462-2.809c-0.372-1.044-0.557-2.24-0.557-3.586
c0-1.157,0.199-2.322,0.595-3.496c0.397-1.173,0.945-2.291,1.645-3.354c0.7-1.062,1.523-2.054,2.473-2.978
c0.95-0.924,1.977-1.727,3.082-2.408c1.105-0.682,2.257-1.218,3.457-1.605c1.2-0.389,2.404-0.583,3.612-0.583
c1.037,0,1.969,0.164,2.797,0.492c0.829,0.329,1.529,0.79,2.099,1.386c0.57,0.595,1.009,1.312,1.32,2.148
c0.311,0.838,0.466,1.775,0.466,2.81c0,0.984-0.172,1.951-0.518,2.9c-0.345,0.95-0.803,1.848-1.373,2.693
c-0.57,0.846-1.213,1.627-1.929,2.344c-0.716,0.716-1.45,1.338-2.201,1.864c-0.751,0.527-1.48,0.937-2.188,1.23
c-0.707,0.294-1.33,0.44-1.864,0.44c-0.466,0-0.886-0.108-1.256-0.324c-0.372-0.215-0.683-0.483-0.933-0.802
c-0.25-0.319-0.444-0.656-0.583-1.009c-0.138-0.355-0.207-0.67-0.207-0.946c0-0.225,0.03-0.371,0.09-0.44
c0.061-0.068,0.146-0.1,0.258-0.091c0.113,0.009,0.255,0.035,0.428,0.078c0.172,0.043,0.371,0.064,0.595,0.064
c0.725,0,1.528-0.25,2.408-0.75s1.709-1.14,2.486-1.917c0.777-0.777,1.429-1.631,1.956-2.563c0.526-0.933,0.79-1.839,0.79-2.72
c0-0.896-0.221-1.605-0.66-2.123c-0.44-0.518-1.188-0.777-2.241-0.777c-0.656,0-1.368,0.125-2.136,0.375
c-0.769,0.251-1.541,0.601-2.318,1.049C10.086,6.289,9.334,6.833,8.61,7.471C7.884,8.11,7.246,8.813,6.694,9.582
c-0.553,0.769-0.998,1.597-1.334,2.486c-0.336,0.889-0.505,1.817-0.505,2.784c0,1.019,0.186,1.929,0.557,2.732
c0.371,0.803,0.863,1.533,1.477,2.188c0.613,0.656,1.308,1.261,2.084,1.813c0.776,0.553,1.57,1.099,2.382,1.644
c0.812,0.544,1.606,1.105,2.383,1.683c0.777,0.579,1.472,1.219,2.085,1.917c0.613,0.698,1.104,1.477,1.476,2.331
c0.372,0.854,0.557,1.834,0.557,2.938c0,1.07-0.203,2.085-0.608,3.043c-0.405,0.958-0.962,1.791-1.671,2.498
c-0.707,0.707-1.545,1.27-2.511,1.684C12.097,39.737,11.061,39.944,9.957,39.944z"/>
<path fill="#FFFFFF" d="M18.787,13.091c0.397,0.035,0.837,0.061,1.321,0.078c0.414,0.034,0.915,0.056,1.501,0.065
c0.587,0.009,1.243,0.021,1.968,0.038V4.286c0-0.379-0.03-0.682-0.09-0.906c-0.061-0.224-0.122-0.401-0.181-0.531
c-0.061-0.129-0.121-0.233-0.182-0.311c-0.061-0.078-0.09-0.159-0.09-0.246c0-0.104,0.052-0.19,0.155-0.259
c0.104-0.069,0.242-0.13,0.415-0.181c0.172-0.052,0.358-0.091,0.557-0.116c0.198-0.026,0.393-0.04,0.583-0.04
c0.881,0,1.574,0.229,2.084,0.688c0.509,0.457,0.764,1.006,0.764,1.644v9.296c1.623,0,3.336-0.017,5.141-0.052
c1.804-0.034,3.612-0.094,5.425-0.181c0.241-0.259,0.436-0.354,0.582-0.285c0.147,0.07,0.241,0.233,0.285,0.493
c0.042,0.258,0.047,0.578,0.014,0.958c-0.036,0.38-0.104,0.747-0.208,1.101c-0.104,0.354-0.247,0.66-0.427,0.919
c-0.182,0.258-0.384,0.388-0.608,0.388c-2.124,0.052-4.001,0.082-5.632,0.091c-1.631,0.008-3.155,0.013-4.571,0.013v16.807
c0,0.862,0.164,1.484,0.492,1.864c0.328,0.38,0.768,0.569,1.32,0.569c0.535,0,0.998-0.189,1.385-0.569
c0.389-0.38,0.726-0.855,1.011-1.424c0.285-0.57,0.518-1.187,0.699-1.853c0.181-0.664,0.332-1.29,0.453-1.876
c0.104-0.554,0.275-0.955,0.518-1.206c0.241-0.248,0.491-0.374,0.75-0.374c0.208,0,0.423,0.063,0.648,0.193
c0.224,0.129,0.427,0.297,0.608,0.505c0.181,0.207,0.328,0.44,0.44,0.699c0.112,0.26,0.168,0.519,0.168,0.777
c0,0.035,0,0.077,0,0.13c0,0.051-0.018,0.129-0.052,0.231c-0.224,0.882-0.53,1.806-0.919,2.772
c-0.388,0.967-0.89,1.855-1.501,2.667c-0.614,0.813-1.342,1.484-2.189,2.02c-0.846,0.534-1.838,0.802-2.978,0.802
c-0.708,0-1.36-0.106-1.955-0.323c-0.596-0.216-1.11-0.561-1.542-1.035c-0.432-0.474-0.768-1.083-1.01-1.826
c-0.242-0.742-0.362-1.64-0.362-2.692V16.768c-0.587,0-1.109-0.004-1.566-0.013c-0.458-0.009-0.842-0.021-1.152-0.039
c-0.364-0.017-0.683-0.035-0.958-0.052c-0.415,0-0.781-0.186-1.101-0.557c-0.32-0.372-0.544-0.781-0.674-1.23
c-0.129-0.448-0.146-0.859-0.051-1.23S18.407,13.091,18.787,13.091z"/>
<path fill="#FFFFFF" d="M48.178,36.008c0.534,0,0.996-0.189,1.385-0.569c0.388-0.38,0.725-0.855,1.01-1.424
c0.285-0.57,0.518-1.187,0.7-1.853c0.18-0.664,0.332-1.29,0.453-1.876c0.121-0.554,0.298-0.955,0.531-1.206
c0.233-0.248,0.487-0.374,0.764-0.374c0.208,0,0.418,0.063,0.634,0.193c0.216,0.129,0.415,0.297,0.597,0.505
c0.181,0.207,0.327,0.44,0.439,0.699c0.113,0.26,0.168,0.519,0.168,0.777c0,0.035,0,0.077,0,0.13c0,0.051-0.017,0.129-0.052,0.231
c-0.225,0.882-0.53,1.806-0.919,2.772c-0.389,0.967-0.884,1.855-1.488,2.667c-0.605,0.813-1.334,1.484-2.188,2.02
c-0.855,0.534-1.853,0.802-2.992,0.802c-0.932,0-1.743-0.198-2.434-0.595c-0.691-0.398-1.235-0.924-1.632-1.579
c-0.536,0.604-1.179,1.117-1.93,1.54c-0.75,0.423-1.626,0.634-2.627,0.634c-1.037,0-1.946-0.206-2.732-0.621
c-0.786-0.414-1.446-0.979-1.98-1.696c-0.536-0.716-0.938-1.562-1.205-2.537c-0.268-0.975-0.402-2.023-0.402-3.146
c0-2.212,0.354-4.195,1.062-5.956c0.708-1.761,1.649-3.26,2.823-4.493c1.174-1.234,2.516-2.18,4.026-2.836
c1.511-0.655,3.077-0.984,4.7-0.984c0.726,0,1.313,0.199,1.761,0.597c0.449,0.396,0.674,0.94,0.674,1.63
c0,0.449-0.047,0.764-0.143,0.946c-0.095,0.181-0.272,0.271-0.531,0.271c-0.241,0-0.491-0.017-0.75-0.052
c-0.26-0.034-0.528-0.052-0.804-0.052c-1.001,0-2.02,0.221-3.056,0.661c-1.036,0.44-1.972,1.114-2.81,2.021
c-0.837,0.906-1.524,2.054-2.058,3.442c-0.536,1.392-0.803,3.044-0.803,4.96c0,1.399,0.267,2.473,0.803,3.224
c0.534,0.752,1.268,1.127,2.2,1.127c0.726,0,1.329-0.271,1.813-0.814c0.483-0.545,0.872-1.187,1.165-1.931V25.08
c0-0.26-0.013-0.474-0.039-0.646s-0.052-0.32-0.077-0.44c-0.027-0.121-0.048-0.229-0.065-0.324
c-0.018-0.095-0.025-0.186-0.025-0.272c0-0.275,0.133-0.497,0.401-0.66c0.267-0.164,0.591-0.246,0.971-0.246
c0.88,0,1.58,0.229,2.098,0.687c0.517,0.458,0.776,1.007,0.776,1.644v8.754c0,0.294,0.035,0.582,0.104,0.867
c0.068,0.285,0.172,0.544,0.31,0.776c0.138,0.233,0.324,0.424,0.557,0.569C47.596,35.936,47.867,36.008,48.178,36.008z"/>
<path fill="#FFFFFF" d="M54.962,30.751c-0.138,0.553-0.397,0.894-0.776,1.022c-0.38,0.13-0.756,0.117-1.126-0.038
c-0.372-0.155-0.683-0.423-0.933-0.803s-0.323-0.794-0.22-1.244c0.259-1.172,0.634-2.472,1.126-3.896
c0.492-1.423,0.979-2.843,1.462-4.26c-0.258-0.396-0.465-0.798-0.621-1.204c-0.155-0.405-0.233-0.833-0.233-1.282
c0-0.448,0.168-0.919,0.505-1.411s0.742-0.945,1.217-1.359c0.474-0.415,0.966-0.755,1.476-1.023
c0.51-0.267,0.945-0.401,1.308-0.401c0.638,0,1.117,0.216,1.438,0.647c0.319,0.432,0.479,1.01,0.479,1.735
c0,0.398-0.064,0.791-0.194,1.178c-0.131,0.389-0.272,0.751-0.427,1.088c-0.157,0.336-0.299,0.626-0.428,0.867
c-0.13,0.242-0.203,0.397-0.221,0.466c0.621,1.105,1.27,2.081,1.942,2.927c0.673,0.847,1.29,1.683,1.852,2.513
c0.561,0.827,1.023,1.708,1.386,2.641c0.362,0.932,0.543,2.027,0.543,3.288c0,0.312-0.021,0.631-0.063,0.959
c-0.045,0.328-0.109,0.639-0.195,0.932c0.621-0.12,1.171-0.311,1.645-0.57c0.474-0.259,0.89-0.556,1.244-0.894
c0.352-0.336,0.646-0.702,0.878-1.101c0.235-0.396,0.428-0.811,0.585-1.241c0.188-0.503,0.379-0.864,0.569-1.09
c0.188-0.224,0.405-0.336,0.647-0.336c0.224,0,0.453,0.074,0.685,0.22c0.234,0.148,0.44,0.334,0.623,0.558
c0.182,0.225,0.327,0.457,0.439,0.699s0.168,0.458,0.168,0.647c0,0.258-0.068,0.569-0.207,0.932s-0.323,0.734-0.557,1.113
c-0.232,0.381-0.495,0.747-0.789,1.101c-0.295,0.354-0.597,0.661-0.908,0.919c-0.828,0.675-1.803,1.239-2.924,1.697
c-1.123,0.458-2.513,0.748-4.171,0.867c-0.639,0.553-1.394,1.006-2.266,1.359c-0.872,0.354-1.852,0.53-2.939,0.53
c-0.553,0-1.118-0.068-1.696-0.206s-1.129-0.315-1.657-0.531c-0.527-0.216-1.005-0.453-1.436-0.712
c-0.432-0.259-0.778-0.519-1.037-0.777c-0.656-0.621-1.078-1.246-1.268-1.877c-0.19-0.63-0.237-1.194-0.143-1.696
c0.095-0.501,0.281-0.91,0.556-1.23c0.277-0.318,0.562-0.479,0.854-0.479c0.172,0,0.445,0.074,0.816,0.22
c0.371,0.146,0.798,0.337,1.281,0.57c0.484,0.232,1.01,0.479,1.581,0.737c0.57,0.26,1.139,0.505,1.708,0.739
c0.57,0.232,1.128,0.423,1.671,0.569c0.544,0.146,1.04,0.219,1.49,0.219c0.292-0.344,0.561-0.802,0.802-1.371
c0.242-0.57,0.362-1.191,0.362-1.865c0-0.828-0.111-1.597-0.336-2.305c-0.227-0.706-0.519-1.363-0.88-1.968
c-0.364-0.603-0.774-1.17-1.231-1.695c-0.457-0.527-0.919-1.024-1.385-1.49c-0.414,1.019-0.777,2.025-1.088,3.017
C55.636,28.365,55.307,29.492,54.962,30.751z"/>
<path fill="#FFFFFF" d="M68.867,5.348c0-0.38-0.028-0.682-0.089-0.907c-0.062-0.224-0.122-0.401-0.182-0.531
c-0.063-0.129-0.121-0.233-0.182-0.311s-0.091-0.159-0.091-0.246c0-0.104,0.052-0.189,0.154-0.259
c0.105-0.068,0.244-0.129,0.416-0.181c0.17-0.052,0.357-0.091,0.558-0.117c0.197-0.026,0.391-0.039,0.581-0.039
c0.881,0,1.576,0.229,2.085,0.687c0.509,0.458,0.764,1.006,0.764,1.645V22.49c0.364-0.742,0.755-1.432,1.179-2.071
c0.424-0.638,0.879-1.195,1.372-1.67c0.492-0.476,1.02-0.847,1.579-1.114c0.563-0.268,1.17-0.402,1.826-0.402
c0.88,0,1.605,0.165,2.175,0.492c0.57,0.329,1.019,0.751,1.348,1.269c0.326,0.518,0.557,1.097,0.686,1.735
c0.131,0.64,0.194,1.261,0.194,1.863v10.981c0,0.121,0.009,0.315,0.027,0.582c0.015,0.269,0.066,0.541,0.153,0.815
c0.085,0.278,0.221,0.519,0.402,0.725c0.182,0.208,0.442,0.312,0.79,0.312c0.533,0,0.996-0.189,1.384-0.569
c0.389-0.38,0.724-0.855,1.009-1.424c0.286-0.57,0.521-1.187,0.701-1.853c0.181-0.664,0.332-1.29,0.453-1.876
c0.104-0.536,0.264-0.924,0.479-1.167c0.216-0.24,0.446-0.375,0.697-0.4c0.25-0.025,0.506,0.038,0.765,0.194
c0.26,0.155,0.484,0.357,0.675,0.607c0.189,0.251,0.332,0.531,0.428,0.843c0.095,0.311,0.105,0.604,0.038,0.879
c-0.241,0.882-0.557,1.806-0.945,2.772c-0.39,0.967-0.884,1.855-1.489,2.667c-0.604,0.813-1.33,1.484-2.177,2.02
c-0.845,0.534-1.838,0.802-2.976,0.802c-0.708,0-1.334-0.133-1.878-0.4s-1.01-0.627-1.398-1.074
c-0.39-0.448-0.682-0.976-0.882-1.58c-0.197-0.604-0.296-1.243-0.296-1.917V23.76c0-0.415-0.011-0.804-0.027-1.167
c-0.019-0.361-0.063-0.681-0.143-0.957c-0.077-0.275-0.188-0.496-0.336-0.66c-0.146-0.164-0.35-0.246-0.607-0.246
c-0.381,0-0.847,0.259-1.398,0.777c-0.554,0.519-1.097,1.191-1.633,2.02c-0.535,0.828-1.004,1.765-1.412,2.81
c-0.404,1.044-0.658,2.095-0.763,3.146v7.744c0,0.258-0.004,0.521-0.013,0.789c-0.008,0.269-0.061,0.51-0.154,0.726
c-0.096,0.216-0.243,0.397-0.442,0.542c-0.197,0.148-0.487,0.221-0.865,0.221c-0.261,0-0.541-0.028-0.843-0.091
c-0.302-0.06-0.578-0.171-0.828-0.336c-0.251-0.163-0.459-0.397-0.622-0.698c-0.164-0.303-0.247-0.704-0.247-1.205V5.348z"/>
<path fill="#FFFFFF" d="M99.943,39.503c-0.622,0-1.256-0.095-1.906-0.285c-0.645-0.188-1.233-0.517-1.759-0.981
c-0.525-0.468-0.958-1.085-1.295-1.853c-0.337-0.769-0.505-1.74-0.505-2.914c0-1.708,0.229-3.517,0.687-5.425
c0.458-1.907,1.117-4.009,1.98-6.306h-4.039H92.51c-0.017,0.518-0.047,1.175-0.09,1.968c-0.044,0.796-0.117,1.641-0.221,2.538
c-0.102,0.898-0.238,1.806-0.399,2.719c-0.166,0.916-0.367,1.762-0.609,2.539c-0.175,0.551-0.445,0.871-0.816,0.958
c-0.372,0.086-0.738,0.02-1.1-0.195c-0.362-0.215-0.658-0.53-0.881-0.944c-0.226-0.415-0.276-0.846-0.156-1.295
c0.139-0.534,0.26-1.186,0.363-1.955c0.104-0.769,0.186-1.58,0.247-2.435c0.06-0.854,0.107-1.709,0.142-2.563
c0.035-0.854,0.059-1.619,0.077-2.292c-0.655-0.362-1.2-0.824-1.63-1.386c-0.433-0.56-0.647-1.229-0.647-2.006
c0-0.362,0.07-0.695,0.217-0.997c0.147-0.302,0.342-0.562,0.584-0.777c0.241-0.215,0.527-0.38,0.854-0.492
c0.329-0.112,0.664-0.168,1.012-0.168c0.312,0,0.633,0.043,0.97,0.129c0.337,0.087,0.657,0.242,0.957,0.466
c0.305,0.225,0.574,0.518,0.816,0.88s0.44,0.812,0.596,1.347c0.433,0.052,0.8,0.09,1.101,0.116
c0.305,0.026,0.557,0.048,0.764,0.065c0.208,0.018,0.381,0.026,0.519,0.026c0.139,0,0.277,0,0.415,0h2.769
c0.156,0,0.386,0.095,0.688,0.285c0.303,0.19,0.601,0.436,0.895,0.738c0.293,0.302,0.538,0.626,0.736,0.971
c0.199,0.345,0.271,0.673,0.221,0.983c0,0.104-0.062,0.355-0.182,0.752c-0.121,0.397-0.269,0.897-0.44,1.501
c-0.174,0.605-0.361,1.3-0.568,2.085c-0.21,0.786-0.397,1.611-0.57,2.474c-0.174,0.864-0.319,1.756-0.44,2.681
c-0.121,0.923-0.182,1.824-0.182,2.706c0,0.361,0.043,0.717,0.13,1.061c0.086,0.346,0.215,0.652,0.389,0.92
c0.173,0.269,0.393,0.479,0.66,0.634c0.269,0.156,0.582,0.233,0.944,0.233c0.604,0,1.121-0.189,1.553-0.569
c0.433-0.38,0.797-0.855,1.09-1.424c0.293-0.57,0.531-1.187,0.713-1.853c0.181-0.664,0.332-1.29,0.45-1.876
c0.104-0.554,0.279-0.955,0.521-1.206c0.24-0.248,0.491-0.374,0.75-0.374c0.207,0,0.424,0.063,0.646,0.193
c0.227,0.129,0.43,0.297,0.608,0.505c0.182,0.207,0.329,0.44,0.441,0.699c0.114,0.26,0.168,0.519,0.168,0.777
c0,0.087-0.009,0.154-0.026,0.207c-0.018,0.052-0.026,0.104-0.026,0.154c-0.241,0.882-0.557,1.806-0.945,2.772
c-0.387,0.967-0.884,1.855-1.488,2.667c-0.604,0.813-1.328,1.484-2.173,2.02C102.075,39.235,101.081,39.503,99.943,39.503z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 13 KiB

@ -1,5 +1,5 @@
/*!
* Bootstrap Grid v5.0.0-beta3 (https://getbootstrap.com/)
* Bootstrap Grid v5.0.0-beta2 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)

@ -1,5 +1,5 @@
/*!
* Bootstrap Grid v5.0.0-beta3 (https://getbootstrap.com/)
* Bootstrap Grid v5.0.0-beta2 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)

@ -1,5 +1,5 @@
/*!
* Bootstrap Reboot v5.0.0-beta3 (https://getbootstrap.com/)
* Bootstrap Reboot v5.0.0-beta2 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
@ -29,6 +29,10 @@ body {
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
[tabindex="-1"]:focus:not(:focus-visible) {
outline: 0 !important;
}
hr {
margin: 1rem 0;
color: inherit;
@ -99,6 +103,7 @@ p {
abbr[title],
abbr[data-bs-original-title] {
text-decoration: underline;
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
@ -304,9 +309,6 @@ select {
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]::-webkit-calendar-picker-indicator {
display: none;

@ -1,5 +1,5 @@
/*!
* Bootstrap Reboot v5.0.0-beta3 (https://getbootstrap.com/)
* Bootstrap Reboot v5.0.0-beta2 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
@ -29,6 +29,10 @@ body {
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
[tabindex="-1"]:focus:not(:focus-visible) {
outline: 0 !important;
}
hr {
margin: 1rem 0;
color: inherit;
@ -99,6 +103,7 @@ p {
abbr[title],
abbr[data-bs-original-title] {
text-decoration: underline;
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
@ -304,9 +309,6 @@ select {
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]::-webkit-calendar-picker-indicator {
display: none;

@ -1,5 +1,5 @@
/*!
* Bootstrap Utilities v5.0.0-beta3 (https://getbootstrap.com/)
* Bootstrap Utilities v5.0.0-beta2 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
@ -456,6 +456,10 @@
border-color: #fff !important;
}
.border-0 {
border-width: 0 !important;
}
.border-1 {
border-width: 1px !important;
}
@ -1126,10 +1130,6 @@
padding-left: 3rem !important;
}
.font-monospace {
font-family: var(--bs-font-monospace) !important;
}
.fs-1 {
font-size: calc(1.375rem + 1.5vw) !important;
}
@ -1182,46 +1182,6 @@
font-weight: bolder !important;
}
.lh-1 {
line-height: 1 !important;
}
.lh-sm {
line-height: 1.25 !important;
}
.lh-base {
line-height: 1.5 !important;
}
.lh-lg {
line-height: 2 !important;
}
.text-start {
text-align: left !important;
}
.text-end {
text-align: right !important;
}
.text-center {
text-align: center !important;
}
.text-decoration-none {
text-decoration: none !important;
}
.text-decoration-underline {
text-decoration: underline !important;
}
.text-decoration-line-through {
text-decoration: line-through !important;
}
.text-lowercase {
text-transform: lowercase !important;
}
@ -1234,21 +1194,18 @@
text-transform: capitalize !important;
}
.text-wrap {
white-space: normal !important;
.text-start {
text-align: left !important;
}
.text-nowrap {
white-space: nowrap !important;
.text-end {
text-align: right !important;
}
/* rtl:begin:remove */
.text-break {
word-wrap: break-word !important;
word-break: break-word !important;
.text-center {
text-align: center !important;
}
/* rtl:end:remove */
.text-primary {
color: #0d6efd !important;
}
@ -1305,6 +1262,22 @@
color: inherit !important;
}
.lh-1 {
line-height: 1 !important;
}
.lh-sm {
line-height: 1.25 !important;
}
.lh-base {
line-height: 1.5 !important;
}
.lh-lg {
line-height: 2 !important;
}
.bg-primary {
background-color: #0d6efd !important;
}
@ -1353,6 +1326,37 @@
background-image: var(--bs-gradient) !important;
}
.text-wrap {
white-space: normal !important;
}
.text-nowrap {
white-space: nowrap !important;
}
.text-decoration-none {
text-decoration: none !important;
}
.text-decoration-underline {
text-decoration: underline !important;
}
.text-decoration-line-through {
text-decoration: line-through !important;
}
/* rtl:begin:remove */
.text-break {
word-wrap: break-word !important;
word-break: break-word !important;
}
/* rtl:end:remove */
.font-monospace {
font-family: var(--bs-font-monospace) !important;
}
.user-select-all {
-webkit-user-select: all !important;
-moz-user-select: all !important;

@ -1,5 +1,5 @@
/*!
* Bootstrap Utilities v5.0.0-beta3 (https://getbootstrap.com/)
* Bootstrap Utilities v5.0.0-beta2 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
@ -456,6 +456,10 @@
border-color: #fff !important;
}
.border-0 {
border-width: 0 !important;
}
.border-1 {
border-width: 1px !important;
}
@ -1126,10 +1130,6 @@
padding-right: 3rem !important;
}
.font-monospace {
font-family: var(--bs-font-monospace) !important;
}
.fs-1 {
font-size: calc(1.375rem + 1.5vw) !important;
}
@ -1182,20 +1182,16 @@
font-weight: bolder !important;
}
.lh-1 {
line-height: 1 !important;
}
.lh-sm {
line-height: 1.25 !important;
.text-lowercase {
text-transform: lowercase !important;
}
.lh-base {
line-height: 1.5 !important;
.text-uppercase {
text-transform: uppercase !important;
}
.lh-lg {
line-height: 2 !important;
.text-capitalize {
text-transform: capitalize !important;
}
.text-start {
@ -1210,37 +1206,6 @@
text-align: center !important;
}
.text-decoration-none {
text-decoration: none !important;
}
.text-decoration-underline {
text-decoration: underline !important;
}
.text-decoration-line-through {
text-decoration: line-through !important;
}
.text-lowercase {
text-transform: lowercase !important;
}
.text-uppercase {
text-transform: uppercase !important;
}
.text-capitalize {
text-transform: capitalize !important;
}
.text-wrap {
white-space: normal !important;
}
.text-nowrap {
white-space: nowrap !important;
}
.text-primary {
color: #0d6efd !important;
}
@ -1297,6 +1262,22 @@
color: inherit !important;
}
.lh-1 {
line-height: 1 !important;
}
.lh-sm {
line-height: 1.25 !important;
}
.lh-base {
line-height: 1.5 !important;
}
.lh-lg {
line-height: 2 !important;
}
.bg-primary {
background-color: #0d6efd !important;
}
@ -1345,6 +1326,29 @@
background-image: var(--bs-gradient) !important;
}
.text-wrap {
white-space: normal !important;
}
.text-nowrap {
white-space: nowrap !important;
}
.text-decoration-none {
text-decoration: none !important;
}
.text-decoration-underline {
text-decoration: underline !important;
}
.text-decoration-line-through {
text-decoration: line-through !important;
}
.font-monospace {
font-family: var(--bs-font-monospace) !important;
}
.user-select-all {
-webkit-user-select: all !important;
-moz-user-select: all !important;

@ -1,6 +1,6 @@
@charset "UTF-8";
/*!
* Bootstrap v5.0.0-beta3 (https://getbootstrap.com/)
* Bootstrap v5.0.0-beta2 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
@ -56,6 +56,10 @@ body {
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
[tabindex="-1"]:focus:not(:focus-visible) {
outline: 0 !important;
}
hr {
margin: 1rem 0;
color: inherit;
@ -126,6 +130,7 @@ p {
abbr[title],
abbr[data-bs-original-title] {
text-decoration: underline;
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
@ -331,9 +336,6 @@ select {
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]::-webkit-calendar-picker-indicator {
display: none;
@ -2182,6 +2184,10 @@ progress {
.form-control::-webkit-date-and-time-value {
height: 1.5em;
}
.form-control::-webkit-input-placeholder {
color: #6c757d;
opacity: 1;
}
.form-control::-moz-placeholder {
color: #6c757d;
opacity: 1;
@ -2353,6 +2359,7 @@ textarea.form-control-lg {
background-image: none;
}
.form-select:disabled {
color: #6c757d;
background-color: #e9ecef;
}
.form-select:-moz-focusring {
@ -2593,6 +2600,9 @@ textarea.form-control-lg {
transition: none;
}
}
.form-floating > .form-control::-webkit-input-placeholder {
color: transparent;
}
.form-floating > .form-control::-moz-placeholder {
color: transparent;
}
@ -2786,12 +2796,6 @@ textarea.form-control-lg {
margin-left: 0.5em;
}
.was-validated .input-group .form-control:valid, .input-group .form-control.is-valid,
.was-validated .input-group .form-select:valid,
.input-group .form-select.is-valid {
z-index: 3;
}
.invalid-feedback {
display: none;
width: 100%;
@ -2868,12 +2872,6 @@ textarea.form-control-lg {
margin-left: 0.5em;
}
.was-validated .input-group .form-control:invalid, .input-group .form-control.is-invalid,
.was-validated .input-group .form-select:invalid,
.input-group .form-select.is-invalid {
z-index: 3;
}
.btn {
display: inline-block;
font-weight: 400;
@ -3548,9 +3546,11 @@ textarea.form-control-lg {
left: auto /* rtl:ignore */;
}
}
.dropup .dropdown-menu[data-bs-popper] {
.dropup .dropdown-menu {
top: auto;
bottom: 100%;
}
.dropup .dropdown-menu[data-bs-popper] {
margin-top: 0;
margin-bottom: 0.125rem;
}
@ -3887,11 +3887,6 @@ textarea.form-control-lg {
text-align: center;
}
.nav-fill .nav-item .nav-link,
.nav-justified .nav-item .nav-link {
width: 100%;
}
.tab-content > .tab-pane {
display: none;
}
@ -4273,7 +4268,7 @@ textarea.form-control-lg {
text-decoration: none;
}
.card-link + .card-link {
margin-left: 1rem;
margin-left: 1rem /* rtl:ignore */;
}
.card-header {
@ -4386,8 +4381,8 @@ textarea.form-control-lg {
font-size: 1rem;
color: #212529;
text-align: left;
background-color: #fff;
border: 0;
background-color: transparent;
border: 1px solid rgba(0, 0, 0, 0.125);
border-radius: 0;
overflow-anchor: none;
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, border-radius 0.15s ease;
@ -4397,10 +4392,12 @@ textarea.form-control-lg {
transition: none;
}
}
.accordion-button.collapsed {
border-bottom-width: 0;
}
.accordion-button:not(.collapsed) {
color: #0c63e4;
background-color: #e7f1ff;
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.125);
}
.accordion-button:not(.collapsed)::after {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%230c63e4'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");
@ -4436,53 +4433,47 @@ textarea.form-control-lg {
margin-bottom: 0;
}
.accordion-item {
margin-bottom: -1px;
background-color: #fff;
border: 1px solid rgba(0, 0, 0, 0.125);
}
.accordion-item:first-of-type {
.accordion-item:first-of-type .accordion-button {
border-top-left-radius: 0.25rem;
border-top-right-radius: 0.25rem;
}
.accordion-item:first-of-type .accordion-button {
border-top-left-radius: calc(0.25rem - 1px);
border-top-right-radius: calc(0.25rem - 1px);
}
.accordion-item:last-of-type {
margin-bottom: 0;
.accordion-item:last-of-type .accordion-button.collapsed {
border-bottom-width: 1px;
border-bottom-right-radius: 0.25rem;
border-bottom-left-radius: 0.25rem;
}
.accordion-item:last-of-type .accordion-button.collapsed {
border-bottom-right-radius: calc(0.25rem - 1px);
border-bottom-left-radius: calc(0.25rem - 1px);
}
.accordion-item:last-of-type .accordion-collapse {
border-bottom-width: 1px;
border-bottom-right-radius: 0.25rem;
border-bottom-left-radius: 0.25rem;
}
.accordion-collapse {
border: solid rgba(0, 0, 0, 0.125);
border-width: 0 1px;
}
.accordion-body {
padding: 1rem 1.25rem;
}
.accordion-flush .accordion-collapse {
border-width: 0;
}
.accordion-flush .accordion-item {
.accordion-flush .accordion-button {
border-right: 0;
border-left: 0;
border-radius: 0;
}
.accordion-flush .accordion-item:first-child {
border-top: 0;
.accordion-flush .accordion-collapse {
border-width: 0;
}
.accordion-flush .accordion-item:last-child {
border-bottom: 0;
.accordion-flush .accordion-item:first-of-type .accordion-button {
border-top-width: 0;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.accordion-flush .accordion-item .accordion-button {
border-radius: 0;
.accordion-flush .accordion-item:last-of-type .accordion-button.collapsed {
border-bottom-width: 0;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.breadcrumb {
@ -4776,15 +4767,6 @@ textarea.form-control-lg {
border-radius: 0.25rem;
}
.list-group-numbered {
list-style-type: none;
counter-reset: section;
}
.list-group-numbered > li::before {
content: counters(section, ".") ". ";
counter-increment: section;
}
.list-group-item-action {
width: 100%;
color: #495057;
@ -4805,7 +4787,6 @@ textarea.form-control-lg {
position: relative;
display: block;
padding: 0.5rem 1rem;
color: #212529;
text-decoration: none;
background-color: #fff;
border: 1px solid rgba(0, 0, 0, 0.125);
@ -5197,7 +5178,7 @@ textarea.form-control-lg {
position: fixed;
top: 0;
left: 0;
z-index: 1060;
z-index: 1050;
display: none;
width: 100%;
height: 100%;
@ -5261,7 +5242,7 @@ textarea.form-control-lg {
position: fixed;
top: 0;
left: 0;
z-index: 1050;
z-index: 1040;
width: 100vw;
height: 100vh;
background-color: #000;
@ -5484,7 +5465,7 @@ textarea.form-control-lg {
}
.tooltip {
position: absolute;
z-index: 1080;
z-index: 1070;
display: block;
margin: 0;
font-family: var(--bs-font-sans-serif);
@ -5586,7 +5567,7 @@ textarea.form-control-lg {
position: absolute;
top: 0;
left: 0 /* rtl:ignore */;
z-index: 1070;
z-index: 1060;
display: block;
max-width: 276px;
font-family: var(--bs-font-sans-serif);
@ -5991,86 +5972,6 @@ textarea.form-control-lg {
animation-duration: 1.5s;
}
}
.offcanvas {
position: fixed;
bottom: 0;
z-index: 1040;
display: flex;
flex-direction: column;
max-width: 100%;
visibility: hidden;
background-color: #fff;
background-clip: padding-box;
outline: 0;
transition: transform 0.3s ease-in-out;
}
@media (prefers-reduced-motion: reduce) {
.offcanvas {
transition: none;
}
}
.offcanvas-header {
display: flex;
justify-content: space-between;
padding: 1rem 1rem;
}
.offcanvas-header .btn-close {
padding: 0.5rem 0.5rem;
margin: -0.5rem -0.5rem -0.5rem auto;
}
.offcanvas-title {
margin-bottom: 0;
line-height: 1.5;
}
.offcanvas-body {
flex-grow: 1;
padding: 1rem 1rem;
overflow-y: auto;
}
.offcanvas-start {
top: 0;
left: 0;
width: 400px;
border-right: 1px solid rgba(0, 0, 0, 0.2);
transform: translateX(-100%);
}
.offcanvas-end {
top: 0;
right: 0;
width: 400px;
border-left: 1px solid rgba(0, 0, 0, 0.2);
transform: translateX(100%);
}
.offcanvas-bottom {
right: 0;
left: 0;
height: 30vh;
max-height: 100%;
border-top: 1px solid rgba(0, 0, 0, 0.2);
transform: translateY(100%);
}
.offcanvas.show {
transform: none;
}
.offcanvas-backdrop::before {
position: fixed;
top: 0;
left: 0;
z-index: 1039;
width: 100vw;
height: 100vh;
content: "";
background-color: rgba(0, 0, 0, 0.5);
}
.clearfix::after {
display: block;
clear: both;
@ -6523,6 +6424,10 @@ textarea.form-control-lg {
border-color: #fff !important;
}
.border-0 {
border-width: 0 !important;
}
.border-1 {
border-width: 1px !important;
}
@ -7193,10 +7098,6 @@ textarea.form-control-lg {
padding-left: 3rem !important;
}
.font-monospace {
font-family: var(--bs-font-monospace) !important;
}
.fs-1 {
font-size: calc(1.375rem + 1.5vw) !important;
}
@ -7249,46 +7150,6 @@ textarea.form-control-lg {
font-weight: bolder !important;
}
.lh-1 {
line-height: 1 !important;
}
.lh-sm {
line-height: 1.25 !important;
}
.lh-base {
line-height: 1.5 !important;
}
.lh-lg {
line-height: 2 !important;
}
.text-start {
text-align: left !important;
}
.text-end {
text-align: right !important;
}
.text-center {
text-align: center !important;
}
.text-decoration-none {
text-decoration: none !important;
}
.text-decoration-underline {
text-decoration: underline !important;
}
.text-decoration-line-through {
text-decoration: line-through !important;
}
.text-lowercase {
text-transform: lowercase !important;
}
@ -7301,21 +7162,18 @@ textarea.form-control-lg {
text-transform: capitalize !important;
}
.text-wrap {
white-space: normal !important;
.text-start {
text-align: left !important;
}
.text-nowrap {
white-space: nowrap !important;
.text-end {
text-align: right !important;
}
/* rtl:begin:remove */
.text-break {
word-wrap: break-word !important;
word-break: break-word !important;
.text-center {
text-align: center !important;
}
/* rtl:end:remove */
.text-primary {
color: #0d6efd !important;
}
@ -7372,6 +7230,22 @@ textarea.form-control-lg {
color: inherit !important;
}
.lh-1 {
line-height: 1 !important;
}
.lh-sm {
line-height: 1.25 !important;
}
.lh-base {
line-height: 1.5 !important;
}
.lh-lg {
line-height: 2 !important;
}
.bg-primary {
background-color: #0d6efd !important;
}
@ -7420,6 +7294,37 @@ textarea.form-control-lg {
background-image: var(--bs-gradient) !important;
}
.text-wrap {
white-space: normal !important;
}
.text-nowrap {
white-space: nowrap !important;
}
.text-decoration-none {
text-decoration: none !important;
}
.text-decoration-underline {
text-decoration: underline !important;
}
.text-decoration-line-through {
text-decoration: line-through !important;
}
/* rtl:begin:remove */
.text-break {
word-wrap: break-word !important;
word-break: break-word !important;
}
/* rtl:end:remove */
.font-monospace {
font-family: var(--bs-font-monospace) !important;
}
.user-select-all {
-webkit-user-select: all !important;
-moz-user-select: all !important;

@ -1,6 +1,6 @@
@charset "UTF-8";
/*!
* Bootstrap v5.0.0-beta3 (https://getbootstrap.com/)
* Bootstrap v5.0.0-beta2 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
@ -56,6 +56,10 @@ body {
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
[tabindex="-1"]:focus:not(:focus-visible) {
outline: 0 !important;
}
hr {
margin: 1rem 0;
color: inherit;
@ -126,6 +130,7 @@ p {
abbr[title],
abbr[data-bs-original-title] {
text-decoration: underline;
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
@ -331,9 +336,6 @@ select {
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]::-webkit-calendar-picker-indicator {
display: none;
@ -2180,6 +2182,10 @@ progress {
.form-control::-webkit-date-and-time-value {
height: 1.5em;
}
.form-control::-webkit-input-placeholder {
color: #6c757d;
opacity: 1;
}
.form-control::-moz-placeholder {
color: #6c757d;
opacity: 1;
@ -2351,6 +2357,7 @@ textarea.form-control-lg {
background-image: none;
}
.form-select:disabled {
color: #6c757d;
background-color: #e9ecef;
}
.form-select:-moz-focusring {
@ -2591,6 +2598,9 @@ textarea.form-control-lg {
transition: none;
}
}
.form-floating > .form-control::-webkit-input-placeholder {
color: transparent;
}
.form-floating > .form-control::-moz-placeholder {
color: transparent;
}
@ -2784,12 +2794,6 @@ textarea.form-control-lg {
margin-right: 0.5em;
}
.was-validated .input-group .form-control:valid, .input-group .form-control.is-valid,
.was-validated .input-group .form-select:valid,
.input-group .form-select.is-valid {
z-index: 3;
}
.invalid-feedback {
display: none;
width: 100%;
@ -2866,12 +2870,6 @@ textarea.form-control-lg {
margin-right: 0.5em;
}
.was-validated .input-group .form-control:invalid, .input-group .form-control.is-invalid,
.was-validated .input-group .form-select:invalid,
.input-group .form-select.is-invalid {
z-index: 3;
}
.btn {
display: inline-block;
font-weight: 400;
@ -3546,9 +3544,11 @@ textarea.form-control-lg {
left: auto ;
}
}
.dropup .dropdown-menu[data-bs-popper] {
.dropup .dropdown-menu {
top: auto;
bottom: 100%;
}
.dropup .dropdown-menu[data-bs-popper] {
margin-top: 0;
margin-bottom: 0.125rem;
}
@ -3885,11 +3885,6 @@ textarea.form-control-lg {
text-align: center;
}
.nav-fill .nav-item .nav-link,
.nav-justified .nav-item .nav-link {
width: 100%;
}
.tab-content > .tab-pane {
display: none;
}
@ -4271,7 +4266,7 @@ textarea.form-control-lg {
text-decoration: none;
}
.card-link + .card-link {
margin-right: 1rem;
margin-left: 1rem ;
}
.card-header {
@ -4384,8 +4379,8 @@ textarea.form-control-lg {
font-size: 1rem;
color: #212529;
text-align: right;
background-color: #fff;
border: 0;
background-color: transparent;
border: 1px solid rgba(0, 0, 0, 0.125);
border-radius: 0;
overflow-anchor: none;
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, border-radius 0.15s ease;
@ -4395,10 +4390,12 @@ textarea.form-control-lg {
transition: none;
}
}
.accordion-button.collapsed {
border-bottom-width: 0;
}
.accordion-button:not(.collapsed) {
color: #0c63e4;
background-color: #e7f1ff;
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.125);
}
.accordion-button:not(.collapsed)::after {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%230c63e4'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");
@ -4434,53 +4431,47 @@ textarea.form-control-lg {
margin-bottom: 0;
}
.accordion-item {
margin-bottom: -1px;
background-color: #fff;
border: 1px solid rgba(0, 0, 0, 0.125);
}
.accordion-item:first-of-type {
.accordion-item:first-of-type .accordion-button {
border-top-right-radius: 0.25rem;
border-top-left-radius: 0.25rem;
}
.accordion-item:first-of-type .accordion-button {
border-top-right-radius: calc(0.25rem - 1px);
border-top-left-radius: calc(0.25rem - 1px);
}
.accordion-item:last-of-type {
margin-bottom: 0;
.accordion-item:last-of-type .accordion-button.collapsed {
border-bottom-width: 1px;
border-bottom-left-radius: 0.25rem;
border-bottom-right-radius: 0.25rem;
}
.accordion-item:last-of-type .accordion-button.collapsed {
border-bottom-left-radius: calc(0.25rem - 1px);
border-bottom-right-radius: calc(0.25rem - 1px);
}
.accordion-item:last-of-type .accordion-collapse {
border-bottom-width: 1px;
border-bottom-left-radius: 0.25rem;
border-bottom-right-radius: 0.25rem;
}
.accordion-collapse {
border: solid rgba(0, 0, 0, 0.125);
border-width: 0 1px;
}
.accordion-body {
padding: 1rem 1.25rem;
}
.accordion-flush .accordion-collapse {
border-width: 0;
}
.accordion-flush .accordion-item {
.accordion-flush .accordion-button {
border-left: 0;
border-right: 0;
border-radius: 0;
}
.accordion-flush .accordion-item:first-child {
border-top: 0;
.accordion-flush .accordion-collapse {
border-width: 0;
}
.accordion-flush .accordion-item:last-child {
border-bottom: 0;
.accordion-flush .accordion-item:first-of-type .accordion-button {
border-top-width: 0;
border-top-right-radius: 0;
border-top-left-radius: 0;
}
.accordion-flush .accordion-item .accordion-button {
border-radius: 0;
.accordion-flush .accordion-item:last-of-type .accordion-button.collapsed {
border-bottom-width: 0;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.breadcrumb {
@ -4774,15 +4765,6 @@ textarea.form-control-lg {
border-radius: 0.25rem;
}
.list-group-numbered {
list-style-type: none;
counter-reset: section;
}
.list-group-numbered > li::before {
content: counters(section, ".") ". ";
counter-increment: section;
}
.list-group-item-action {
width: 100%;
color: #495057;
@ -4803,7 +4785,6 @@ textarea.form-control-lg {
position: relative;
display: block;
padding: 0.5rem 1rem;
color: #212529;
text-decoration: none;
background-color: #fff;
border: 1px solid rgba(0, 0, 0, 0.125);
@ -5195,7 +5176,7 @@ textarea.form-control-lg {
position: fixed;
top: 0;
right: 0;
z-index: 1060;
z-index: 1050;
display: none;
width: 100%;
height: 100%;
@ -5259,7 +5240,7 @@ textarea.form-control-lg {
position: fixed;
top: 0;
right: 0;
z-index: 1050;
z-index: 1040;
width: 100vw;
height: 100vh;
background-color: #000;
@ -5482,7 +5463,7 @@ textarea.form-control-lg {
}
.tooltip {
position: absolute;
z-index: 1080;
z-index: 1070;
display: block;
margin: 0;
font-family: var(--bs-font-sans-serif);
@ -5584,7 +5565,7 @@ textarea.form-control-lg {
position: absolute;
top: 0;
left: 0 ;
z-index: 1070;
z-index: 1060;
display: block;
max-width: 276px;
font-family: var(--bs-font-sans-serif);
@ -5976,86 +5957,6 @@ textarea.form-control-lg {
animation-duration: 1.5s;
}
}
.offcanvas {
position: fixed;
bottom: 0;
z-index: 1040;
display: flex;
flex-direction: column;
max-width: 100%;
visibility: hidden;
background-color: #fff;
background-clip: padding-box;
outline: 0;
transition: transform 0.3s ease-in-out;
}
@media (prefers-reduced-motion: reduce) {
.offcanvas {
transition: none;
}
}
.offcanvas-header {
display: flex;
justify-content: space-between;
padding: 1rem 1rem;
}
.offcanvas-header .btn-close {
padding: 0.5rem 0.5rem;
margin: -0.5rem auto -0.5rem -0.5rem;
}
.offcanvas-title {
margin-bottom: 0;
line-height: 1.5;
}
.offcanvas-body {
flex-grow: 1;
padding: 1rem 1rem;
overflow-y: auto;
}
.offcanvas-start {
top: 0;
right: 0;
width: 400px;
border-left: 1px solid rgba(0, 0, 0, 0.2);
transform: translateX(100%);
}
.offcanvas-end {
top: 0;
left: 0;
width: 400px;
border-right: 1px solid rgba(0, 0, 0, 0.2);
transform: translateX(-100%);
}
.offcanvas-bottom {
left: 0;
right: 0;
height: 30vh;
max-height: 100%;
border-top: 1px solid rgba(0, 0, 0, 0.2);
transform: translateY(100%);
}
.offcanvas.show {
transform: none;
}
.offcanvas-backdrop::before {
position: fixed;
top: 0;
right: 0;
z-index: 1039;
width: 100vw;
height: 100vh;
content: "";
background-color: rgba(0, 0, 0, 0.5);
}
.clearfix::after {
display: block;
clear: both;
@ -6508,6 +6409,10 @@ textarea.form-control-lg {
border-color: #fff !important;
}
.border-0 {
border-width: 0 !important;
}
.border-1 {
border-width: 1px !important;
}
@ -7178,10 +7083,6 @@ textarea.form-control-lg {
padding-right: 3rem !important;
}
.font-monospace {
font-family: var(--bs-font-monospace) !important;
}
.fs-1 {
font-size: calc(1.375rem + 1.5vw) !important;
}
@ -7234,20 +7135,16 @@ textarea.form-control-lg {
font-weight: bolder !important;
}
.lh-1 {
line-height: 1 !important;
}
.lh-sm {
line-height: 1.25 !important;
.text-lowercase {
text-transform: lowercase !important;
}
.lh-base {
line-height: 1.5 !important;
.text-uppercase {
text-transform: uppercase !important;
}
.lh-lg {
line-height: 2 !important;
.text-capitalize {
text-transform: capitalize !important;
}
.text-start {
@ -7262,37 +7159,6 @@ textarea.form-control-lg {
text-align: center !important;
}
.text-decoration-none {
text-decoration: none !important;
}
.text-decoration-underline {
text-decoration: underline !important;
}
.text-decoration-line-through {
text-decoration: line-through !important;
}
.text-lowercase {
text-transform: lowercase !important;
}
.text-uppercase {
text-transform: uppercase !important;
}
.text-capitalize {
text-transform: capitalize !important;
}
.text-wrap {
white-space: normal !important;
}
.text-nowrap {
white-space: nowrap !important;
}
.text-primary {
color: #0d6efd !important;
}
@ -7349,6 +7215,22 @@ textarea.form-control-lg {
color: inherit !important;
}
.lh-1 {
line-height: 1 !important;
}
.lh-sm {
line-height: 1.25 !important;
}
.lh-base {
line-height: 1.5 !important;
}
.lh-lg {
line-height: 2 !important;
}
.bg-primary {
background-color: #0d6efd !important;
}
@ -7397,6 +7279,29 @@ textarea.form-control-lg {
background-image: var(--bs-gradient) !important;
}
.text-wrap {
white-space: normal !important;
}
.text-nowrap {
white-space: nowrap !important;
}
.text-decoration-none {
text-decoration: none !important;
}
.text-decoration-underline {
text-decoration: underline !important;
}
.text-decoration-line-through {
text-decoration: line-through !important;
}
.font-monospace {
font-family: var(--bs-font-monospace) !important;
}
.user-select-all {
-webkit-user-select: all !important;
-moz-user-select: all !important;

@ -1,3 +1,3 @@
body { margin:0;background-color:black; overflow-x:hidden;}
body { margin:0;background-color:black; }
.stashr-button_container {position:fixed;bottom:0;right:0;opacity:.5;z-index:999;}

@ -1,51 +1,72 @@
body { display: flex; flex-wrap: nowrap; height: 100vh; height: -webkit-fill-available; overflow-x: auto; overflow-y: hidden; background-color:#2c2d35 !important; }
/* START NEW STASHR CLASSES */
body { background:#444444; }
ul { list-style-type: none; }
.login { align-items: center !important; }
.center { text-align:center; }
@media screen and (max-width: 768px) {
.stashr-menu { margin-left:-280px; }
.stashr-item_container {max-width:100px;min-width:20px;min-height:150px;}
@media screen and (max-width: 800px) {
.stashr-cover_size {max-width:100px;min-width:20px;min-height:150px;}
}
@media screen and (min-width: 768px) {
.stashr-menu { margin-left:0px; }
.stashr-item_container {max-width:160px;min-width:100px;min-height:150px;}
@media screen and (min-width: 800px) {
.stashr-cover_size {max-width:160px;min-width:100px;min-height:150px;}
}
.bg-mine { background: #3e3e44; }
.stashr-menu { transition: 0.5s; }
.stashr-submenu { transition: 0.5s; z-index:-5; margin-top:-100% !important; }
.stashr-footer { position:fixed; bottom:0; z-index:6; }
.stashr-item_container { overflow: hidden; }
.stashr-poster_container { position:relative; overflow: hidden; }
.stashr-poster_info { position:absolute; top:0; left:0; z-index:3; border-bottom-right-radius: 25%; }
.stashr-badge_top_left { position:absolute; top:0; left:0; z-index:3; border-bottom-right-radius: 25%; }
.stashr-badge_bottom_left { position:absolute; bottom:0; left:0; z-index:3; border-top-right-radius: 25%; }
.stashr-badge_top_right { position:absolute; top:0; right:0; z-index:3; border-bottom-left-radius: 25%; }
.stashr-badge_bottom_right { position:absolute; bottom:0; right:0; z-index:3; border-top-left-radius: 25%; }
.stashr-poster_tag { position: absolute; height:0; right:0; width:25%; padding-bottom:25%; transform: rotate(45deg); z-index:3; margin-top:-12.5%; margin-right:-12.5%; }
.stashr-poster { position:absolute; top:0; left:0; }
.stashr-overlay_top { position:absolute; top:0; background-color:rgba(220,220,220,.9); z-index:4; }
.stashr-overlay_bottom { position:absolute; bottom:0; background-color:rgba(220,220,220,.9); z-index:4; }
.stashr-progress_wrapper { position:absolute; bottom:0; z-index:3; }
.stashr-poster_wrapper { position:relative; overflow:hidden; }
.stashr-link { text-decoration: none; }
#app {margin:0;padding:0;}
body .stashrRead { margin:0;background-color:black; }
.stashr-project_title { font-family: 'Grand Hotel', cursive;font-size:xx-large;line-height:40px;vertical-align: middle; }
.stashr-series_title { font-family: 'Oswald', sans-serif;font-weight:bold; }
.stashr-series_info {min-width:30%;max-width:60%;}
.stashr-poster_wrapper {position:relative;background-color:pink;z-index:5;}
.stashr-badge_tl {position:absolute;top:-5;left:-5;z-index:4;}
.stashr-badge_tr {position:absolute;top:-5;right:-5;z-index:4;}
.stashr-badge_bl {position:absolute;bottom:-5;left:-5;z-index:4;}
.stashr-badge_br {position:absolute;bottom:-5;right:-5;z-index:4;}
.stashr-poster_container {position:relative;overflow:hidden;}
.stashr-overlay_top {position:absolute;top:0;background-color:rgba(220,220,220,.9);z-index:5;}
.stashr-overlay_bottom {position:absolute;bottom:0;background-color:rgba(220,220,220,.9);z-index:5;}
.stashr-poster_background {z-index:0;}
.stashr-poster_link {position:absolute;top:0;left:0;width:100%;height:100%;}
.stashr-poster_image {margin: 0 auto;display: block;vertical-align: middle;}
.stashr-test_display {position:relative;display:inline-block;vertical-align:middle;}
.stashr-test-image {margin:0 auto;display:block;vertical-align:middle;width:100%;}
.swiper-container { width:100%;height:100%;padding:0px;margin:0px; }
.swiper-wrapper { padding:0px;margin:0px; }
.swiper-slide { width:100%;height:100%;background: black; }
.swiper-slide img { height:100%; }
.stashr-button_container {position:fixed;bottom:10;right:10;opacity:.7;z-index:999;}
.stashr-menu_button {position:absolute;bottom:0;border-top-right-radius:25%;border-bottom-right-radius:25%;z-index:999;}
.stashr-reader { background:black; }
.new-stashr-button_container { opacity:.7; }
.btn-circle.btn-sm { width: 30px; height: 30px; padding: 6px 0px; border-radius: 15px; font-size: 8px; text-align: center; margin:2px; }
.btn-circle.btn-md { width: 50px; height: 50px; padding: 7px 10px; border-radius: 25px; font-size: 10px; text-align: center; margin:2px; }
.btn-circle.btn-xl { width: 70px; height: 70px; padding: 10px 16px; border-radius: 35px; font-size: 12px; text-align: center; margin:2px; }
.stashr-signin { width: 100%; max-width: 330px; padding: 15px; margin: auto; }
.stashr-signin .checkbox { font-weight: 400; }
.stashr-signin .form-floating:focus-within { z-index: 2; }
.stashr-signin input[type="username"] { margin-bottom: -1px; border-bottom-right-radius: 0; border-bottom-left-radius: 0; }
.stashr-signin input[type="password"] { margin-bottom: 10px; border-top-left-radius: 0; border-top-right-radius: 0; }
.stashr-logo { position:relative; top:-85px; width:170px; height:170px; border-radius: 50%; border: 10px solid #2c2d35; margin-bottom:-70px; }
.stashr-firstrun { width: 100%; max-width: 330px; padding: 15px; margin: auto; }
.stashr-firstrun .form-floating:focus-within { z-index: 2; }
.stashr-firstrun input { margin-bottom: 5px; }
.stashr-button_container_reader {position:fixed;bottom:0;right:0;opacity:.7;z-index:999;}
.stashr-button { display:table-cell;vertical-align:middle;width:60px;height:60px; }
.stashr-check_box {position:absolute;top:0;right:0;z-index:5;}
.btn-circle.btn-sm {
width: 30px;
height: 30px;
padding: 6px 0px;
border-radius: 15px;
font-size: 8px;
text-align: center;
}
.btn-circle.btn-md {
width: 50px;
height: 50px;
padding: 7px 10px;
border-radius: 25px;
font-size: 10px;
text-align: center;
}
.btn-circle.btn-xl {
width: 70px;
height: 70px;
padding: 10px 16px;
border-radius: 35px;
font-size: 12px;
text-align: center;
}

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

@ -1,86 +0,0 @@
/*
Copyright 2015, 2019, 2020, 2021 Google LLC. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Incrementing OFFLINE_VERSION will kick off the install event and force
// previously cached resources to be updated from the network.
const OFFLINE_VERSION = 2;
const CACHE_NAME = "offline";
// Customize this with a different URL if needed.
const OFFLINE_URL = "/static/offline.html";
self.addEventListener("install", (event) => {
event.waitUntil(
(async () => {
const cache = await caches.open(CACHE_NAME);
// Setting {cache: 'reload'} in the new request will ensure that the
// response isn't fulfilled from the HTTP cache; i.e., it will be from
// the network.
await cache.add(new Request(OFFLINE_URL, { cache: "reload" }));
})()
);
// Force the waiting service worker to become the active service worker.
self.skipWaiting();
});
self.addEventListener("activate", (event) => {
event.waitUntil(
(async () => {
// Enable navigation preload if it's supported.
// See https://developers.google.com/web/updates/2017/02/navigation-preload
if ("navigationPreload" in self.registration) {
await self.registration.navigationPreload.enable();
}
})()
);
// Tell the active service worker to take control of the page immediately.
self.clients.claim();
});
self.addEventListener("fetch", (event) => {
// We only want to call event.respondWith() if this is a navigation request
// for an HTML page.
if (event.request.mode === "navigate") {
event.respondWith(
(async () => {
try {
// First, try to use the navigation preload response if it's supported.
const preloadResponse = await event.preloadResponse;
if (preloadResponse) {
return preloadResponse;
}
// Always try the network first.
const networkResponse = await fetch(event.request);
return networkResponse;
} catch (error) {
// catch is only triggered if an exception is thrown, which is likely
// due to a network error.
// If fetch() returns a valid HTTP response with a response code in
// the 4xx or 5xx range, the catch() will NOT be called.
console.log("Fetch failed; returning offline page instead.", error);
const cache = await caches.open(CACHE_NAME);
const cachedResponse = await cache.match(OFFLINE_URL);
return cachedResponse;
}
})()
);
}
// If our if() condition is false, then this fetch handler won't intercept the
// request. If there are any other fetch handlers registered, they will get a
// chance to call event.respondWith(). If no fetch handlers call
// event.respondWith(), the request will be handled by the browser as if there
// were no service worker involvement.
});

@ -2,26 +2,7 @@ function stashrToast(message, type) {
Vue.$toast.open({
message: message,
type: type,
position: 'bottom-right',
position: 'bottom-left',
dismissable: true
});
}
function toggleMenu() {
console.log('Toggle MENU')
var margin = getComputedStyle(document.querySelector('#sideMenu')).marginLeft;
if(margin == '0px') {
document.getElementById('sideMenu').style.marginLeft = "-280px";
} else {
document.getElementById('sideMenu').style.marginLeft = "0px"
}
}
function toggleSubMenu() {
var margin = getComputedStyle(document.querySelector('.stashr-submenu')).marginTop;
if ( margin == '0px' ) {
document.querySelector('.stashr-submenu').style.setProperty('margin-top', '-100%', 'important')
} else {
document.querySelector('.stashr-submenu').style.setProperty('margin-top', '0px', 'important')
}
}
}

@ -16,16 +16,6 @@
"start_url": "/",
"background_color": "#3367D6",
"display": "fullscreen",
"orientation": "portrait",
"scope": "/",
"theme_color": "#3367D6",
"shortcuts": [
{
"name": "All Volumes",
"short_name": "Volumes",
"description": "All Volumes",
"url": "/volumes",
"icons": [{ "src": "/static/assets/stashr-192.png", "sizes": "192x192"}]
}
]
"theme_color": "#3367D6"
}

@ -1,10 +0,0 @@
<html>
<head>
<title>Stashr - OFFLINE</title>
</head>
<body>
Stashr is currently Offline
</body>
</html>

@ -30,13 +30,8 @@ Vue.component('collections', {
props: ['collections'],
template: `
<div>
<div class="d-grid w-100 bg-mine m-0 p-0 sticky-top shadow">
<div class="row d-flex flex-nowrap w-100 m-0 p-3 bg-mine">
<input type="text" v-model="search" class="form-control flex-shrink-1" placeholder="Search Collections..." />
<a class="fa-stack p-0 text-white">
<i class="fas fa-bars fa-stack-1x" data-bs-toggle="offcanvas" href="#offcanvasRight" role="button"></i>
</a>
</div>
<div class="mb-3 px-5">
<input type="text" v-model="search" class="form-control" placeholder="Search Collections..." />
</div>
<ul class="d-flex flex-wrap w-100 m-0 p-0 py-2 justify-content-center">
<collection
@ -45,15 +40,6 @@ Vue.component('collections', {
v-bind:key="collection.collection_id"
></collection>
</ul>
<div class="offcanvas offcanvas-end" tabindex="-1" id="offcanvasRight" aria-labelledby="offcanvasRightLabel">
<div class="offcanvas-header">
<h5 id="offcanvasRightLabel">Collections</h5>
<button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
</div>
</div>
</div>
`,
data() { return { search: '', } },
@ -70,18 +56,18 @@ Vue.component('collections', {
Vue.component('collection', {
props: ['collection'],
template: `
<li class='stashr-item_container m-2'
<li class='stashr-cover_size m-2'
@mouseover="hover = true"
@mouseleave="hover = false"
>
<div class="stashr-poster_container border border-dark rounded-3">
<a class="stashr-poster_link" v-bind:href="'/collections/'+collection.collection_slug">
<img class="stashr-background w-100" loading="eager" src="/static/assets/cover.svg" />
<img class="stashr-poster w-100" loading="eager" v-bind:src="'/images/issues/'+collection.collection_cover_image+'.jpg'" />
</a>
<div class="stashr-overlay_bottom w-100 text-center shadow" v-if="hover">
<a class="stashr-link" v-bind:href="'/collections/'+collection.collection_slug">
[[ collection.collection_name ]]
>
<div class='stashr-poster_wrapper rounded'>
<div class="stashr-poster_container border rounded">
<div class="stashr-overlay_bottom w-100 center" v-if="hover">
<a href="#">[[ collection.collection_name ]]</a>
</div>
<img class="stashr-poster_background w-100" loading="eager" src="/static/assets/cover.svg" />
<a class="stashr-poster_link" v-bind:href="'/collections/'+collection.collection_slug">
<img class="w-100" loading="lazy" v-bind:src="'/images/issues/'+collection.collection_cover_image+'.jpg'" onerror="this.src='/static/assets/cover.svg'" />
</a>
</div>
</div>

@ -47,10 +47,8 @@ Vue.component('publishers', {
props: ['publishers'],
template: `
<div>
<div class="d-grid w-100 bg-mine m-0 p-0 sticky-md-top shadow">
<div class="row w-100 m-0 p-3">
<input type="text" v-model="search" class="form-control" placeholder="Search Publishers..." />
</div>
<div class="mb-3 px-5">
<input type="text" v-model="search" class="form-control" placeholder="Search Publishers..." />
</div>
<ul class="d-flex flex-wrap w-100 m-0 p-0 py-2 justify-content-center">
<publisher
@ -79,18 +77,18 @@ Vue.component('publishers', {
Vue.component('publisher',{
props: ['publisher'],
template: `
<li class='stashr-item_container m-2'
<li class='stashr-cover_size m-2'
@mouseover="hover = true"
@mouseleave="hover = false"
>
<div class="stashr-poster_container border border-dark rounded-3">
<a :href="'{{ url_for('single_publisher_page', publisher_id='PUBLISHERID') }}'.replace('PUBLISHERID', publisher.publisher_id)">
<img class="stashr-background w-100" loading="eager" src="/static/assets/cover.svg" />
<img class="stashr-poster w-100" loading="lazy" v-bind:src=publisher.publisher_image />
</a>
<div class="stashr-overlay_bottom w-100 text-center shadow" v-if="hover">
<a class="stashr-link" :href="'{{ url_for('single_publisher_page', publisher_id='PUBLISHERID') }}'.replace('PUBLISHERID', publisher.publisher_id)">
[[ publisher.publisher_name ]]
>
<div class='stashr-poster_wrapper rounded'>
<div class="stashr-poster_container border rounded">
<div class="stashr-overlay_bottom w-100 center" v-if="hover">
<a href="#">[[ publisher.publisher_name ]]</a>
</div>
<img class="stashr-poster_background w-100" loading="eager" src="/static/assets/cover.svg" />
<a class="stashr-poster_link align-middle" :href="'{{ url_for('single_publisher_page', publisher_id='PUBLISHERID') }}'.replace('PUBLISHERID', publisher.publisher_id)">
<img class="w-100" loading="lazy" v-bind:src=publisher.publisher_image />
</a>
</div>
</div>

@ -6,7 +6,6 @@
{% block header %}
{{ emit_tep('all_volumes_page_header') }}
{% endblock %}
{% block content %}
@ -29,7 +28,7 @@
</div>
<div id="app">
<volumes v-bind:volumes='volumesList' v-bind:publishers="publishers" v-bind:ratings="ratings"></volumes>
<volumes v-bind:volumes='volumesList'></volumes>
</div>
{% endblock %}
@ -49,76 +48,43 @@
{% block script %}
Vue.component('publisher-item', {
props: ['publisher'],
template: `
<option :value="publisher.publisher_id">[[ publisher.publisher_name ]]</option>
`,
delimiters: ["[[","]]"],
})
Vue.component('rating-item', {
props: ['rating'],
template: `
<option :value="rating.rating_value">[[ rating.rating_long ]]</option>
`,
delimiters: ["[[","]]"],
})
Vue.component('volume-item', {
props: ['volume'],
template: `
<li class='stashr-item_container m-2'
<li class='stashr-cover_size m-2'
@mouseover="hover = true"
@mouseleave="hover = false"
>
<div class="stashr-poster_container border border-dark rounded-3">
<div :class="volumeTag"></div>
<div class="stashr-poster_info bg-info text-white px-1">
[[ volume.age_rating[0].rating_short ]]
</div>
<div class="stashr-poster_wrapper">
<a :href="'/volumes/'+volume.volume_slug">
<img class="stashr-background w-100" loading="eager" src="/static/assets/cover.svg" />
<img class="stashr-poster w-100" loading="lazy" v-bind:src="'/images/volumes/'+volume.volume_id+'.jpg'" />
</a>
</div>
<div class="stashr-overlay_bottom w-100 text-center shadow" v-if="hover">
<a class="stashr-link" :href="'/volumes/'+volume.volume_slug">
[[ volume.volume_name ]]
</a>
</div>
<div class="stashr-progress_wrapper w-100">
<div class="progress bg-dark" style="height: 5px;">
<div class="progress-bar" :class="progressColor" role="progressbar" :style="progressStyle" aria-valuenow="25" aria-valuemin="0" aria-valuemax="100"></div>
>
<div class='stashr-poster_wrapper rounded'>
<div class="stashr-badge_tl badge rounded-pill bg-info border">[[ volume.age_rating[0].rating_short ]]</div>
<div class="stashr-badge_tr badge rounded-pill bg-primary border">[[ volume.volume_have ]]/[[ volume.volume_total ]]</div>
<div class="stashr-badge_br badge rounded-pill border" :class="statusClass">[[ statusWord ]]</div>
<div class="stashr-poster_container border rounded">
<div class="stashr-overlay_bottom w-100 center" v-if="hover">
<a :href="'/volumes/'+volume.volume_slug">[[ volume.volume_name ]]</a>
</div>
<img class="stashr-poster_background w-100" loading="eager" src="/static/assets/cover.svg" />
<a class="stashr-poster_link" :href="'/volumes/'+volume.volume_slug">
<img class="w-100" loading="lazy" v-bind:src="'/images/volumes/'+volume.volume_id+'.jpg'" @error="$event.target.src=volume.volume_image_med"/>
</a>
</div>
</div>
</li>
`,
computed: {
volumeTag() {
let cornertag = '';
if(!this.volume.volume_status) {
cornertag = "stashr-poster_tag bg-danger shadow";
}
return cornertag;
},
progressWidth() {
return (this.volume.volume_have / this.volume.volume_total)*100;
},
progressColor() {
let classname = 'bg-success';
statusClass() {
let classname = 'bg-danger';
if(this.volume.volume_status) {
classname = 'bg-info';
}
if(this.progressWidth < 100) {
classname = 'bg-danger';
}
return classname
classname = 'bg-success';
};
return classname;
},
progressStyle() {
return "width: " + this.progressWidth + "%;"
statusWord() {
let status = 'ENDED';
if(this.volume.volume_status) {
status = 'ONGOING';
};
return status;
}
},
data() { return { hover: false } },
@ -126,16 +92,11 @@ Vue.component('volume-item', {
})
Vue.component('volumes', {
props: ['volumes', 'ratings', 'publishers'],
props: ['volumes'],
template: `
<div>
<div class="d-grid w-100 bg-mine m-0 p-0 sticky-top shadow">
<div class="row d-flex flex-nowrap w-100 m-0 p-3 bg-mine">
<input type="text" v-model="search" class="form-control flex-shrink-1" placeholder="Search Volumes..." />
<a class="fa-stack p-0 text-white">
<i class="fas fa-bars fa-stack-1x" data-bs-toggle="offcanvas" href="#offcanvasRight" role="button"></i>
</a>
</div>
<div class="mb-3 px-5">
<input type="text" v-model="search" class="form-control" placeholder="Search Volumes..." />
</div>
<ul class="d-flex flex-wrap w-100 m-0 p-0 py-2 justify-content-center">
<volume-item
@ -144,91 +105,17 @@ Vue.component('volumes', {
v-bind:key="volume.volume_id"
></volume-item>
</ul>
<div class="offcanvas offcanvas-end" tabindex="-1" id="offcanvasRight" aria-labelledby="offcanvasRightLabel">
<div class="offcanvas-header">
<h5 id="offcanvasRightLabel">All Volumes</h5>
<button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
<div class="m-3">
<div class="my-2">
<i class="fas fa-sort"></i>
<strong>Sort</strong>
</div>
<select class="form-select" aria-label="Default select example" v-model="sorted">
<option value="volume_sort_title">Name</option>
<option value="volume_year">Year</option>
<option value="volume_total">Length</option>
<option value="progress">Progress</option>
</select>
</div>
<div class="m-3">
<div class="my-2">
<i class="fas fa-filter"></i>
<strong>Volume Status</strong>
</div>
<select class="form-select" aria-label="Default select example" v-model="filter">
<option :value="''">All</option>
<option :value=false>Ended</option>
<option :value=true>Ongoing</option>
</select>
</div>
<div class="m-3">
<div class="my-2">
<i class="fas fa-filter"></i>
<strong>Publisher</strong>
</div>
<select class="form-select" aria-label="Default select example" v-model="publisher">
<option :value="''">All</option>
<publisher-item
v-for="publisher in publishers"
v-bind:publisher="publisher"
v-bind:key="publisher.publisher_id"
></publisher-item>
</select>
</div>
<div class="m-3">
<div class="my-2">
<i class="fas fa-filter"></i>
<strong>Age Rating</strong>
</div>
<select class="form-select" aria-label="Default select example" v-model="rating">
<option :value="''">All</option>
<rating-item
v-for="rating in ratings"
v-bind:rating="rating"
v-bind:key="rating.rating_id"
></rating-item>
</select>
</div>
<hr />
<div class="text-center w-100 m-0 p-3">
<button type="button" class="btn btn-info w-100" data-bs-toggle="tooltip" data-bs-placement="top" title="Add Volume from Comicvine" onclick="location.href='{{ url_for('search_page') }}';">
<i class="fas fa-plus"></i>
Add Volume From Comicvine
</button>
</div>
</div>
</div>
<!--
<i class="text-primary fas fa-spinner fa-spin fa-3x" v-if="loading"></i>
-->
</div>
`,
data() { return { loading: true, search: '', filter: '', sorted: 'volume_sort_title', rating: '', publisher: ''} },
data() { return { loading: true, search: '', } },
computed: {
filteredList() {
if (this.sorted.toLowerCase() == 'progress') {
return this.volumes
.filter(volume => { return volume.volume_name.toLowerCase().includes(this.search.toLowerCase()) })
.filter(volume => { return volume.volume_status.toString().includes(this.filter) })
.filter(volume => { return volume.publisher.publisher_id.toString().includes(this.publisher) })
.sort((a, b) => (a.volume_have / a.volume_total) - (b.volume_have / b.volume_total))
} else {
return this.volumes
.filter(volume => { return volume.volume_name.toLowerCase().includes(this.search.toLowerCase()) })
.filter(volume => { return volume.volume_status.toString().includes(this.filter) })
.filter(volume => { return volume.publisher.publisher_id.toString().includes(this.publisher) })
.filter(volume => { return volume.volume_age_rating.toString().includes(this.rating) })
.sort((a, b) => a[this.sorted] - b[this.sorted])
}
return this.volumes.filter(volume => {
return volume.volume_name.toLowerCase().includes(this.search.toLowerCase())
})
},
},
delimiters: ["[[","]]"],
@ -238,39 +125,11 @@ var app = new Vue({
el: '#app',
data: {
volumesList: [],
ratings: [],
publishers: [],
},
created() {
this.getVolumes();
this.getPublishers();
this.getRatings();
this.getVolumes()
},
methods:{
getRatings() {
axios.get('{{ url_for('api.api_get_all_ratings') }}')
.then(res => {
this.ratings = res.data.results;
})
.catch(err => console.log(err))
},
getPublishers() {
axios.get('{{ url_for('api.api_get_all_publishers') }}', {
params: {
offset: this.publishers.length
}
})
.then(res => {
if(res.data.number_of_page_results > 0) {
res.data.results.forEach(result => {
this.publishers.push(result)
})
if(this.publishers.length < res.data.number_of_total_results) {
this.getPublishers()
}
}
})
},
getVolumes() {
axios.get('{{ url_for('api.api_get_all_volumes') }}', {
params: {

@ -1,13 +1,14 @@
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, shrink-to-fit=no">
<title>Stashr - {{ title }}</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/bootstrap.css') }}">
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/stashr.css') }}?v=0.1.0">
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/stashr.css') }}">
<link href="https://cdn.jsdelivr.net/npm/vue-toast-notification/dist/theme-sugar.css" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='css/all.css') }}">
@ -25,154 +26,178 @@
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<script src="{{ url_for('static', filename='js/stashr.js') }}"></script>
<script src="{{ url_for('static', filename='js/vue.dev.js') }}"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-toast-notification@0.6"></script>
<link href="https://cdn.jsdelivr.net/npm/vue-toast-notification/@0.6/dist/theme-sugar.css" rel="stylesheet">
<!--
<!-- VueJS -->
<script src="{{ url_for('static', filename='js/vue.dev.js') }}"></script>
<!-- VueJS Toasts -->
<script src="https://cdn.jsdelivr.net/npm/vue-toast-notification"></script>
<link href="https://cdn.jsdelivr.net/npm/vue-toast-notification/dist/theme-sugar.css" rel="stylesheet">
-->
<!-- START HEADER SCRIPT INCLUDES -->
{% block header_script_files %}{% endblock %}
{{ emit_tep('base_page_header_script_files') }}
<!-- END HEADER SCRIPT INCLUDES -->
</head>
<body>
<div class="container-fluid d-flex min-vh-100 m-0 p-0">
<div class="stashr-menu d-flex flex-column p-3 text-white bg-dark text-center min-vh-100 sticky-top overflow-auto" id="sideMenu" style="width:280px;">
<div class="d-flex flex-row">
<a class="navbar-brand" href="{{ url_for('index_page') }}">
<img class="border rounded-circle" src="{{ url_for('static', filename='assets/stashr.svg') }}" width="40" height="40" />
<img src="{{ url_for('static', filename='assets/title.svg') }}" height="25" />
</a>
<!-- START NAVBAR -->
<nav class="navbar navbar-expand-lg navbar-dark bg-primary sticky-top">
<div class="container-fluid">
<a class="navbar-brand" href="{{ url_for('index_page') }}">
<img class="border rounded-circle" src="{{ url_for('static', filename='assets/stashr.svg') }}" width="40" height="40" />
<span class="stashr-project_title">Stashr</span>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarStashr" aria-cointrols="navbarStashr" aria-expanded="false" aria-label="Toggle Navigation">
<span class="navbar-toggler-icon"></span>
</button>
{% if not current_user.is_authenticated %}
{% if not request.endpoint == 'first_run_page' %}
<div class="collapse navbar-collapse" id="navbarStashr">
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link" href="{{ url_for('login_page') }}">
LOGIN
</a>
</li>
{% if open_registration %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('register_page') }}">
REGISTER
</a>
</li>
{% endif %}
</ul>
</div>
<hr/>
<ul class="nav nav-pills flex-column mb-auto text-start">
<li>
<a data-bs-toggle="collapse" data-bs-target="#collapseLibrary" class="nav-link text-white collapsed">
<i class="fas fa-chevron-right"></i>
Library
</a>
<div class="collapse{% if (request.path == url_for('all_volumes_page')) or
(request.path == url_for('all_publishers_page')) or
(request.path == url_for('scrape_folders_page')) %} show{% endif %}" id="collapseLibrary">
<ul class="ps-3 nav nav-pills flex-column">
<li>
<a href="{{ url_for('all_volumes_page') }}" class="nav-link text-white{% if request.path == url_for('all_volumes_page') %} active{% endif %}">
Volumes
</a>
</li>
{% endif %}
{% endif %}
{% if current_user.is_authenticated %}
<div class="collapse navbar-collapse" id="navbarStashr">
<ul class="navbar-nav me-auto">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="libraryDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
LIBRARY
</a>
<ul class="dropdown-menu" aria-labelledby="libraryDropdown">
<li><a class="dropdown-item" href="{{ url_for('all_volumes_page') }}">VOLUMES</a></li>
<li><a class="dropdown-item" href="{{ url_for('all_publishers_page') }}">PUBLISHERS</a></li>
{% if current_user.role == 'admin' %}
<li>
<a href="{{ url_for('scrape_folders_page') }}" class="nav-link text-white{% if request.path == url_for('scrape_folders_page') %} active{% endif %}">
Scrape
</a>
</li>
<li><a class="dropdown-item" href="{{ url_for('scrape_folders_page') }}">SCRAPE</a></li>
{% endif %}
</ul>
</div>
</li>
{% if current_user.role == 'admin' or current_user.role == 'librarian' %}
<li>
<a href="{{ url_for('new_releases_page') }}" class="nav-link text-white{% if request.path == url_for('new_releases_page') %} active{% endif %}">
New Releases
</a>
</li>
{% endif %}
{% if current_user.role == 'admin' or current_user.role == 'librarian' or current_user.role == 'reader' %}
<li>
<a href="{{ url_for('reading_list_page') }}" class="nav-link text-white{% if request.path == url_for('reading_list_page') %} active{% endif %}">
Reading List
</a>
</li>
<li>
<a href="{{ url_for('all_collections_page') }}" class="nav-link text-white{% if request.path == url_for('all_collections_page') %} active{% endif %}">
Collections
</a>
</li>
{% endif %}
{{ emit_tep("base_page_main_menu") }}
</ul>
<hr />
<ul class="nav nav-pills d-flex flex-column text-start">
<li>
<a href="{{ url_for('settings_single_user_page', user_id=current_user.id) }}" class="nav-link text-white">
<i class="fa fa-user"></i>
{{ current_user.username }}
</a>
</li>
{% if current_user.role == 'admin' %}
<li>
<a href="{{ url_for('settings_page') }}" class="nav-link text-white">
<i class="fas fa-cogs"></i>
Settings
</a>
</li>
{% endif %}
<li>
<a href="{{ url_for('logout_page') }}" class="nav-link text-white">
<i class="fas fa-sign-out-alt"></i>
Logout
</a>
</li>
</ul>
</div>
<div class="d-flex flex-column w-100 m-0 px-0 min-vh-100 overflow-auto">
<!-- v CONTENT v -->
{% block content %}{% endblock %}
<!-- ^ CONTENT ^ -->
<!-- v BUTTON CONTAINER v -->
<div>
<div class="d-grid m-0 p-0 stashr-menu_button bg-dark">
<div class="row d-flex flex-nowrap m-0 p-0 py-2">
<i class="text-white fas fa-ellipsis-v fa-2x" onclick="toggleMenu()"></i>
</div>
</div>
</li>
{% if current_user.role == 'admin' or current_user.role == 'librarian' %}
<li class="nav-item">
<a class="nav-link{% if request.path == url_for('new_releases_page') %} active{% endif %}" href="{{ url_for('new_releases_page') }}">
NEW RELEASES
</a>
</li>
{% endif %}
{% if current_user.role == 'admin' or current_user.role == 'librarian' or current_user.role == 'reader' %}
<li class="nav-item">
<a class="nav-link{% if request.path == url_for('reading_list_page') %} active{% endif %}" href="{{ url_for('reading_list_page') }}">
READING LIST
</a>
</li>
<li class="nav-item">
<a class="nav-link{% if request.path == url_for('all_collections_page') %} active{% endif %}" href="{{ url_for('all_collections_page') }}">
COLLECTIONS
</a>
</li>
{% endif %}
{{ emit_tep("base_page_main_menu") }}
</ul>
<ul class="navbar-nav ms-auto">
{% if current_user.role == 'admin' %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('settings_page') }}">
<i class="fa fa-cogs"></i>
Settings
</a>
</li>
{% endif %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="libraryDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa fa-user"></i>
{{ current_user.username }}
</a>
<ul class="dropdown-menu" aria-labelledby="libraryDropdown">
<li><a class="dropdown-item" href="{{ url_for('settings_single_user_page', user_id=current_user.id) }}">
<i class="fa fa-user"></i>
{{ current_user.username }}
</a></li>
<li><a class="dropdown-item" href="{{ url_for('logout_page') }}">
<i class="fas fa-sign-out-alt"></i>
Logout
</a></li>
</ul>
</li>
</ul>
</div>
<!-- ^ BUTTON CONTAINER ^ -->
{% endif %}
</div>
</nav>
<!-- END NAVBAR -->
<!-- START HEADER -->
<div class="py-2" id="stashr_header">
{% block header %}{% endblock %}
</div>
<!-- END HEADER -->
<!-- v MODALS v -->
<!-- START CONTENT -->
<div class="py-2" id="stashr_content">
{% block content %}{% endblock %}
</div>
<!-- END CONTENT -->
<!-- START MODALS -->
{% block modals %}{% endblock %}
{{ emit_tep('base_page_modals') }}
<!-- ^ MODALS ^ -->
<!-- END MODALS -->
<!-- START BUTTON CONTAINER -->
<div class="stashr-button_container p-2">
{% block button_container %}{% endblock %}
</div>
<!-- END BUTTON CONTAINER -->
<!-- v FOOTER SCRIPT INCLUDES v -->
<!-- START FOOTER SCRIPT INCLUDES -->
<script src="{{ url_for('static', filename='js/bootstrap.bundle.js') }}"></script>
{% block footer_script_files %}{% endblock %}
{{ emit_tep('base_page_footer_script_files') }}
<!-- ^ FOOTER SCRIPT INCLUDES ^ -->
<!-- END FOOTER SCRIPT INCLUDES -->
<!-- v FOOTER SCRIPT v -->
<!-- START FOOTER SCRIPT -->
<script type="text/javascript">
/* ----- CODE FOR TOOLTIPS WHEN WORKING
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl)
})
*/
Vue.use(VueToast);
// Flashes
{% with flashes = get_flashed_messages(with_categories=true) %}
{% if flashes %}
{% for category, message in flashes %}
{% if flashes %}
{% for category, message in flashes %}
stashrToast('{{ message }}', '{{ category }}');
{% endfor %}
{% endif %}
{% endfor %}
{% endif %}
{% endwith %}
/*
window.addEventListener("load", () => {
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("service-worker.js");
}
});
*/
{% block script %}{% endblock %}
{{ emit_tep('base_page_script') }}
</script>
<!-- ^ FOOTER SCRIPT ^ -->
<!-- END FOOTER SCRIPT -->
</body>
</html>

@ -1,92 +1,52 @@
<html>
<head>
<title>Stashr - {{ title }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, shrink-to-fit=no">
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/bootstrap.css') }}">
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/stashr.css') }}">
<link href="https://cdn.jsdelivr.net/npm/vue-toast-notification/dist/theme-sugar.css" rel="stylesheet">
<link rel="manifest" href="{{ url_for('static', filename='manifest/manifest.json') }}">
<link rel="apple-touch-icon" href="touch-icon-iphone.png">
<link rel="apple-touch-icon" sizes="152x152" href="{{ url_for('static', filename='assets/stashr-152.png') }}">
<link rel="apple-touch-icon" sizes="180x180" href="{{ url_for('static', filename='assets/stashr-180.png') }}">
<link rel="apple-touch-icon" sizes="167x167" href="{{ url_for('static', filename='assets/stashr-167.png') }}">
<meta name="apple-mobile-web-app-title" content="Stashr">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<script src="{{ url_for('static', filename='js/stashr.js') }}"></script>
<script src="{{ url_for('static', filename='js/vue.dev.js') }}"></script>
</head>
<body class="text-center login">
<main class="stashr-firstrun">
<div class="card bg-primary px-3 shadow">
<form action="{{ url_for('first_run_page') }}" method="post" id="first_run_form">
{% extends "base.html" %}
{% block content %}
<div class="d-flex justify-content-center flex-wrap w-80">
<div class='position-relative my-2'>
<img class="border rounded-circle my-3" src="{{ url_for('static', filename='assets/stashr.svg') }}" width="200" height="200" />
</div>
<div class="bg-light m-2 px-2 rounded stashr-series_info text-center text-lg-start py-3 px-5">
<h5 class="text-center">First Run</h5>
<hr />
<form method="POST">
{{ first_run_form.csrf_token }}
<img class="stashr-logo" src="{{ url_for('static', filename='assets/stashr.svg') }}" w="170" h="170">
{% if first_run_form.errors %}
<div class="notification bg-danger rounded m-3 text-white" role="alert" id='error_container'>
{% for field_name, field_errors in first_run_form.errors|dictsort if field_errors %}
<ul>
{% for error in field_errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endfor %}
</div>
{% endif %}
<div class="form-floating">
{{ first_run_form.username(class_='form-control', placeholder=first_run_form.username.label.text) }}
<div class="mb-3">
{{ first_run_form.username.label }}
{{ first_run_form.username(class_='form-control', placeholder=first_run_form.username.label.text) }}
</div>
<div class="form-floating">
{{ first_run_form.email(class_='form-control', placeholder=first_run_form.email.label.text) }}
<div class="mb-3">
{{ first_run_form.email.label }}
{{ first_run_form.email(class_='form-control', placeholder=first_run_form.email.label.text) }}
</div>
<div class="form-floating">
{{ first_run_form.password(type='password', class_='form-control', placeholder=first_run_form.password.label.text) }}
<div class="mb-3">
{{ first_run_form.password.label }}
{{ first_run_form.password(type='password', class_='form-control', placeholder=first_run_form.password.label.text) }}
</div>
<div class="form-floating">
{{ first_run_form.confirm_password(type='password', class_='form-control', placeholder=first_run_form.confirm_password.label.text) }}
<div class="mb-3">
{{ first_run_form.confirm_password.label }}
{{ first_run_form.confirm_password(type='password', class_='form-control', placeholder=first_run_form.confirm_password.label.text) }}
</div>
<div class="form-floating">
{{ first_run_form.comicvine_api_key(class_='form-control', placeholder=first_run_form.comicvine_api_key.label.text) }}
<hr />
<div class="mb-3">
{{ first_run_form.comicvine_api_key.label }}
{{ first_run_form.comicvine_api_key(class_='form-control', placeholder=first_run_form.comicvine_api_key.label.text) }}
</div>
<div class="form-floating">
{{ first_run_form.logging_level(class_='form-control') }}
<hr />
<div class="mb-3">
{{ first_run_form.open_registration }}
{{ first_run_form.open_registration.label }}
</div>
<div class="mb-3">
{{ first_run_form.logging_level.label }}
{{ first_run_form.logging_level(class_='form-control') }}
</div>
<hr />
{{ first_run_form.first_run_button(class_='btn btn-success') }}
<div class="mb-3 text-end">
{{ first_run_form.first_run_button(class_='btn btn-success') }}
</div>
</form>
</div>
</main>
<script type="text/javascript">
Vue.use(VueToast);
{% with flashes = get_flashed_messages(with_categories=true) %}
{% if flashes %}
{% for category, message in flashes %}
stashrToast('{{ message }}', '{{ category }}');
{% endfor %}
{% endif %}
{% endwith %}
</script>
</body>
</html>
</div>
{% endblock %}

@ -1,37 +1,15 @@
<html>
<head>
<title>Stashr - {{ title }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, shrink-to-fit=no">
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/bootstrap.css') }}">
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/stashr.css') }}">
<link href="https://cdn.jsdelivr.net/npm/vue-toast-notification/dist/theme-sugar.css" rel="stylesheet">
<link rel="manifest" href="{{ url_for('static', filename='manifest/manifest.json') }}">
<link rel="apple-touch-icon" href="touch-icon-iphone.png">
<link rel="apple-touch-icon" sizes="152x152" href="{{ url_for('static', filename='assets/stashr-152.png') }}">
<link rel="apple-touch-icon" sizes="180x180" href="{{ url_for('static', filename='assets/stashr-180.png') }}">
<link rel="apple-touch-icon" sizes="167x167" href="{{ url_for('static', filename='assets/stashr-167.png') }}">
<meta name="apple-mobile-web-app-title" content="Stashr">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<script src="{{ url_for('static', filename='js/stashr.js') }}"></script>
<script src="{{ url_for('static', filename='js/vue.dev.js') }}"></script>
</head>
<body class="text-center login">
<main class="stashr-signin">
<div class="card bg-primary px-3 shadow">
<form action="{{ url_for('login_page') }}" method="post" id="login_form">
{{ login_form.csrf_token }}
<img class="stashr-logo" src="{{ url_for('static', filename='assets/stashr.svg') }}" w="170" h="170">
{% extends "base.html" %}
{% block content %}
<div class="d-flex justify-content-center flex-wrap w-80">
<div class='position-relative my-2'>
<img class="border rounded-circle my-3" src="{{ url_for('static', filename='assets/stashr.svg') }}" width="200" height="200" />
</div>
<div class="bg-light m-2 px-2 rounded stashr-series_info text-center text-lg-start py-3 px-5">
<h5 class="text-center">Login</h5>
<hr />
<form action="{{ url_for('login_page') }}" method="post" id="registration_form">
{% if login_form.errors %}
<div class="notification bg-danger rounded m-3 text-white" role="alert" id='error_container'>
{% for field_name, field_errors in login_form.errors|dictsort if field_errors %}
@ -42,41 +20,24 @@
</ul>
{% endfor %}
</div>
{% else %}
{% endif %}
<div class="form-floating">
{{ login_form.username(class_='input form-control', type='username', placeholder='Username') }}
{{ login_form.username.label }}
{{ login_form.csrf_token }}
<div class="mb-3">
{{ login_form.username(class_='input form-control', placeholder='Username') }}
</div>
<div class="form-floating">
<div class="mb-3">
{{ login_form.password(class_='input form-control', type='password', placeholder='Password') }}
{{ login_form.password.label }}
</div>
<div class="checkbox mb-3">
<label class="text-white">
{{ login_form.remember_me() }} Remember Me
</label>
<div class="mb-3">
{{ login_form.remember_me() }} Remember Me
</div>
<div class="mb-3">
{{ login_form.login_button(class_='btn btn-outline-success') }}
<a href="{{ url_for('forgot_page') }}" class="btn btn-outline-danger">Forgot Password</a>
</div>
{{ login_form.login_button(class_='w-100 btn btn-lg btn-success shadow') }}
<a class="text-white" href="#">Forgot your password?</a>
</form>
</div>
</main>
<script type="text/javascript">
Vue.use(VueToast);
{% with flashes = get_flashed_messages(with_categories=true) %}
{% if flashes %}
{% for category, message in flashes %}
stashrToast('{{ message }}', '{{ category }}');
{% endfor %}
{% endif %}
{% endwith %}
</script>
</div>
</body>
</html>
{% endblock %}

@ -73,34 +73,33 @@ Vue.component('modals', {
Vue.component('release-item', {
props: ['release'],
template: `
<li class='stashr-item_container m-2'
<li class='stashr-cover_size m-2'
@mouseover="hover = true"
@mouseleave="hover = false"
>
<div class="stashr-poster_container border border-dark rounded-3" data-bs-toggle="modal" data-bs-target="#modalSubscription" v-on:click="this.changeModal">
<div class="stashr-badge_top_left bg-info text-white px-1">
#[[ release.new_release_issue_number ]]
</div>
<div class="stashr-badge_bottom_right bg-dark">
<span class="fa-stack p-0">
<i class="fas fa-university fa-stack-1x" :class="className"></i>
<i class="fas fa-slash fa-stack-1x text-danger" v-if="!this.release.status"></i>
</span>
</div>
<img class="stashr-background w-100" loading="eager" src="/static/assets/cover.svg" />
<img class="stashr-poster w-100" loading="lazy" :src="release.new_release_image_url" />
<div class="stashr-overlay_bottom w-100 text-center shadow" v-if="hover">
[[ release.new_release_comic_name ]]
>
<div class='stashr-poster_wrapper rounded'>
<div class="stashr-badge_tl badge rounded-pill bg-info border">#[[ release.new_release_issue_number ]]</div>
<div class="stashr-badge_br badge rounded-pill border px-2" :class="className" >[[ subText ]]</div>
<div class="stashr-poster_container border rounded">
<div class="stashr-overlay_bottom w-100 center" v-if="hover">
<a data-bs-toggle="modal" data-bs-target="#modalSubscription" v-on:click="this.changeModal">[[ release.new_release_comic_name ]]</a>
</div>
<img class="stashr-poster_background w-100" loading="eager" src="/static/assets/cover.svg" />
<a class="stashr-poster_link" data-bs-toggle="modal" data-bs-target="#modalSubscription" v-on:click="this.changeModal">
<img class="w-100" loading="lazy" :src="release.new_release_image_url" />
</a>
</div>
</div>
</li>
`,
computed: {
className() {
let classname = 'text-danger';
let classname = 'bg-danger';
try {
if (this.release.status) {
classname = 'text-success';
classname = 'bg-success';
}
} finally {
return classname;
@ -153,13 +152,8 @@ Vue.component('releases', {
props: ['releases'],
template: `
<div>
<div class="d-grid w-100 bg-mine m-0 p-0 sticky-top shadow">
<div class="row d-flex flex-nowrap w-100 m-0 p-3 bg-mine">
<input type="text" v-model="search" class="form-control flex-shrink-1" placeholder="Search New Releases..." />
<a class="fa-stack p-0 text-white">
<i class="fas fa-bars fa-stack-1x" data-bs-toggle="offcanvas" href="#offcanvasRight" role="button"></i>
</a>
</div>
<div class="mb-3 px-5">
<input type="text" v-model="search" class="form-control" placeholder="Search New Releases..." />
</div>
<ul class="d-flex flex-wrap w-100 m-0 p-0 py-2 justify-content-center">
<release-item
@ -168,62 +162,14 @@ Vue.component('releases', {
v-bind:key="release.new_release_id"
></release-item>
</ul>
<div class="offcanvas offcanvas-end" tabindex="-1" id="offcanvasRight" aria-labelledby="offcanvasRightLabel">
<div class="offcanvas-header">
<h5 id="offcanvasRightLabel">New Releases</h5>
<button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
<div class="m-3">
<div class="my-2">
<i class="fas fa-sort"></i>
<strong>Sort</strong>
</div>
<select class="form-select" aria-label="Default select example" v-model="sorted">
<option value="new_release_comic_name">Name</option>
<option value="new_release_issue_number">Issue Number</option>
</select>
</div>
<div class="m-3">
<div class="my-2">
<i class="fas fa-sort"></i>
<strong>Library Status</strong>
</div>
<select class="form-select" aria-label="Default select example" v-model="library">
<option :value="''">All</option>
<option value="library">In Library</option>
<option :value="Null">Not In Library</option>
</select>
</div>
<hr />
<div class="text-center w-100 m-0 p-3">
<button type="button" class="btn btn-info w-100" data-bs-toggle="tooltip" data-bs-placement="top" title="Update New Releases" onclick="app.updateNewReleases()">
<i class="fas fa-sync-alt"></i>
Update New Releases
</button>
</div>
</div>
</div>
</div>
`,
data() { return { search: '', sorted: 'new_release_comic_name', library: ''} },
data() { return { search: '', } },
computed: {
filteredList() {
if (this.library == null) {
return this.releases
.filter(release => { return release.new_release_comic_name.toLowerCase().includes(this.search.toLowerCase()) })
.filter(release => { return release.status == null })
.sort((a, b) => a[this.sorted] - b[this.sorted])
} else if (this.library == 'library') {
return this.releases
.filter(release => { return release.new_release_comic_name.toLowerCase().includes(this.search.toLowerCase()) })
.filter(release => { return release.status != null })
.sort((a, b) => a[this.sorted] - b[this.sorted])
} else {
return this.releases
.filter(release => { return release.new_release_comic_name.toLowerCase().includes(this.search.toLowerCase()) })
.sort((a, b) => a[this.sorted] - b[this.sorted])
}
return this.releases.filter(release => {
return release.new_release_comic_name.toLowerCase().includes(this.search.toLowerCase())
})
},
},
delimiters: ["[[","]]"]

@ -3,7 +3,7 @@
<title>Stashr - Reading - {{ comic_name }}</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/bootstrap.css') }}">
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/stashr-read.css') }}">
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/stashr.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/all.css') }}">
@ -18,14 +18,14 @@
</head>
<body class="stashr-reader">
<body class="stashrRead">
<div id="app">
<swiper v-bind:slides='slides' v-swiper='$options.swiperOptions'></swiper>
<modal></modal>
</div>
<div class="stashr-button_container p-2">
<div class="stashr-button_container_reader p-2">
<button type="button" class="btn btn-link" data-bs-toggle="modal" data-bs-target="#modalRead">
<i class="text-white fas fa-bars fa-2x"></i>
</button>

@ -53,7 +53,7 @@ Vue.component('modals', {
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body text-center">
<div class="modal-body center">
<a :href="'/read/'+issue.issue_id" id="readRead" class="btn btn-success">
<i class="fas fa-book-open"></i>
Read
@ -99,46 +99,14 @@ Vue.component('modals', {
Vue.component('issues', {
props: ['issues'],
template: `
<div>
<div class="d-grid w-100 bg-mine m-0 p-0 sticky-top shadow">
<div class="row d-flex flex-nowrap w-100 m-0 p-3 bg-mine">
<input type="text" v-model="search" class="form-control flex-shrink-1" placeholder="Search Reading List..." />
<a class="fa-stack p-0 text-white">
<i class="fas fa-bars fa-stack-1x" data-bs-toggle="offcanvas" href="#offcanvasRight" role="button"></i>
</a>
</div>
</div>
<ul class="d-flex flex-wrap w-100 m-0 p-0 justify-content-center" v-sortable="$options.sortOptions" @sorted='handleSorted'>
<issue
v-for="issue in filteredList"
v-bind:issue="issue"
v-bind:key="issue.issue_id"
></issue>
</ul>
<div class="offcanvas offcanvas-end" tabindex="-1" id="offcanvasRight" aria-labelledby="offcanvasRightLabel">
<div class="offcanvas-header">
<h5 id="offcanvasRightLabel">Reading List</h5>
<button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
<div class="text-center w-100 m-0 p-3">
<button type="button" class="btn btn-danger w-100" data-bs-toggle="tooltip" data-bs-placement="top" title="Clear Reading List" onclick="app.clearList()">
<i class="fas fa-minus"></i>
Clear Reading List
</button>
</div>
</div>
</div>
</div>
<ul class="d-flex flex-wrap w-100 m-0 p-0 justify-content-center" v-sortable="$options.sortOptions" @sorted='handleSorted'>
<issue
v-for="issue in issues"
v-bind:issue="issue"
v-bind:key="issue.issue_id"
></issue>
</ul>
`,
data() { return { loading: true, search: '', } },
computed: {
filteredList() {
return this.issues.filter(issue => {
return issue.volume.volume_name.toLowerCase().includes(this.search.toLowerCase())
})
},
},
methods: {
handleSorted(event) {
app.handleSorted(event)
@ -156,30 +124,30 @@ Vue.component('issues', {
Vue.component('issue', {
props: ['issue'],
template: `
<li class='stashr-item_container m-2 js-sortable-block'
<li class='stashr-cover_size m-2 js-sortable-block'
@mouseover="hover = true"
@mouseleave="hover = false"
>
<div class="stashr-poster_container border border-dark rounded-3 js-drag-handle">
<div class="stashr-badge_top_left bg-dark">
<span class="fa-stack p-0">
<i class="fas fa-arrows-alt fa-stack-1x text-white"></i>
</span>
</div>
<div class="stashr-badge_bottom_right bg-dark">
<span class="fa-stack p-0" v-on:click="toggleRead">
<i class="fas fa-eye fa-stack-1x" :class="statusRead"></i>
<i class="fas fa-slash fa-stack-1x text-danger" v-if="!this.issue.read_status[0].read_status"></i>
</span>
</div>
<div data-bs-toggle="modal" data-bs-target="#modalRead" v-on:click="this.changeModal">
<img class="stashr-background w-100" loading="eager" src="/static/assets/cover.svg" />
<img class="stashr-poster w-100" loading="lazy" v-bind:src="'/images/issues/'+issue.issue_id+'.jpg'" />
</div>
<div class="stashr-overlay_bottom w-100 text-center shadow" v-if="hover" data-bs-toggle="modal" data-bs-target="#modalRead" v-on:click="this.changeModal">
[[ issue.volume.volume_name ]] #[[ issue.issue_number ]]
>
<div class='stashr-poster_wrapper rounded'>
<div class="stashr-badge_tl badge rounded-pill bg-dark border js-drag-handle">
<i class="fas fa-arrows-alt"></i>
</div>
<div class="stashr-badge_br badge rounded-pill bg-dark border">
<i class="fas fa-eye" v-bind:class="statusRead" v-on:click="toggleRead"></i>
</div>
<!--
<div class="stashr-badge_tr badge badge-pill badge-info border">[[ issue.reading_list_position ]]</div>
-->
<div class="stashr-poster_container border rounded">
<div class="stashr-overlay_bottom w-100 center" v-if="hover">
<a href="#">[[ issue.volume.volume_name ]] #[[ issue.issue_number ]]</a>
</div>
<img class="stashr-poster_background w-100" loading="eager" src="/static/assets/cover.svg" />
<a class="stashr-poster_link" data-bs-toggle="modal" data-bs-target="#modalRead" v-on:click="this.changeModal">
<img class="w-100" loading="lazy" v-bind:src="'/images/issues/'+issue.issue_id+'.jpg'"/>
</a>
</div>
</div>
</li>
`,
computed: {

@ -0,0 +1,45 @@
{% extends "base.html" %}
{% block content %}
<div class="d-flex justify-content-center flex-wrap w-80">
<div class='position-relative my-2'>
<img class="border rounded-circle my-3" src="{{ url_for('static', filename='assets/stashr.svg') }}" width="200" height="200" />
</div>
<div class="bg-light m-2 px-2 rounded stashr-series_info text-center text-lg-start py-3 px-5">
<h5 class="text-center">Register</h5>
<hr />
<form action="{{ url_for('register_page') }}" method="post" id="registration_form">
{% if registration_form.errors %}
<div class="notification bg-danger rounded m-3 text-white" role="alert" id='error_container'>
{% for field_name, field_errors in registration_form.errors|dictsort if field_errors %}
<ul>
{% for error in field_errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endfor %}
</div>
{% else %}
{% endif %}
{{ registration_form.csrf_token }}
<div class="mb-3">
{{ registration_form.username(class_='input form-control', placeholder='Username') }}
</div>
<div class="mb-3">
{{ registration_form.email(class_='input form-control', placeholder='E-Mail') }}
</div>
<div class="mb-3">
{{ registration_form.reg_password(class_='input form-control', placeholder='Password', type='password') }}
</div>
<div class="mb-3">
{{ registration_form.confirm_reg_password(class_='input form-control', placeholder='Confirm Password', type='password') }}
</div>
<div class="mb-3">
{{ registration_form.register_button(class_='btn btn-outline-success') }}
</div>
</form>
</div>
</div>
{% endblock %}

@ -11,7 +11,7 @@
{% block content %}
<div id="app">
<modal ref="modal" v-bind:individual="individual" v-on:do-search="searchNewResults"></modal>
<modal ref="modal" v-bind:individual="individual"></modal>
<directories v-bind:directories='directories'></directories>
</div>
@ -59,45 +59,38 @@ Vue.component('modal', {
<div class="modal-body">
<div class="row">
<div class="col-3">
<div class="stashr-item_container border rounded">
<div class="stashr-poster_container border rounded">
<img class="stashr-poster_background w-100" loading="eager" src="/static/assets/folder.svg" />
<a class="stashr-poster_link">
<img class="w-100" :src="[[ match['image']['small_url'] ]]" loading="lazy"/>
</a>
</div>
</div>
<div class="col-9">
<div v-if='!this.individual.scrape_add'>
<div class="my-3 p-0 input-group">
<input type="text" v-model="search" class="form-control" placeholder="Search Volumes..." />
<div class="input-group-append">
<button class="btn btn-success" type="button" @click="$emit('do-search', search)">
Search
</button>
</div>
</div>
<select id="selectVolume" class="form-select" aria-label="Default select example" @change='doSomething($event)'>
<candidate
v-for="item in json"
v-bind:candidate="item"
v-bind:individual="this.individual"
v-bind:key="item['id']"
></candidate>
</select>
</div>
<select id="selectVolume" class="form-select" aria-label="Default select example" @change='doSomething($event)' v-if='!this.individual.scrape_add'>
<candidate
v-for="item in json"
v-bind:candidate="item"
v-bind:individual="this.individual"
v-bind:key="item['id']"
></candidate>
</select>
<div class='py-2'>
<h5>[[ match['name'] ]] ([[ match['start_year'] ]])</h5>
<p v-html="match['description']"></p>
</div>
</div>
</div>
<!--
<select class="form-select" aria-label="Default select example">
<option selected>Open this select menu</option>
<option value="1">One</option>
<option value="2">Two</option>
<option value="3">Three</option>
</select>
-->
</div>
<div class="modal-footer">
<span v-if="this.individual.scrape_ignore_directory">
<button type="button" class="btn btn-success" data-bs-dismiss="modal" @click='toggleDirectoryIgnore'>Show Directory</button>
</span>
<span v-else>
<button type="button" class="btn btn-danger" data-bs-dismiss="modal" @click='toggleDirectoryIgnore'>Hide Directory</button>
</span>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
@ -125,13 +118,11 @@ Vue.component('modal', {
this.individual.scrape_candidate = something.target.value
},
setTest() {
console.log('in here')
this.test = JSON.parse(this.individual.scrape_json).filter(item => {
return item['id'] == this.individual.scrape_candidate
})[0]
document.getElementById('selectVolume').value = this.individual.scrape_candidate
},
toggleDirectoryIgnore() {
app.toggleDirectoryIgnore();
}
},
delimiters: ["[[","]]"]
@ -140,24 +131,34 @@ Vue.component('modal', {
Vue.component('directory', {
props: ['directory'],
template: `
<li class='stashr-item_container m-2'
<li class='stashr-cover_size m-2'
@mouseover="hover = true"
@mouseleave="hover = false"
>
<div class="stashr-poster_container border border-dark rounded-3" v-on:click="this.changeModal">
<div class="stashr-badge_top_right bg-dark" @click='changeChecked'>
<span class="fa-stack p-0">
<i class="fa-stack-1x" :class='checkedStatus'></i>
</span>
<div class='stashr-poster_wrapper rounded' v-on:click="changeModal()">
<div class="stashr-badge_tl badge badge-pill badge-info border"></div>
<div class="stashr-badge_br badge badge-pill border"></div>
<div class="stashr-poster_container border rounded">
<div class="stashr-check_box p-1">
<a @click='changeChecked'>
<i :class='checkedStatus'></i>
<!--
<i class="text-info fa-2x fas fa-circle"></i>
<i class="text-success fa-2x fas fa-check-circle"></i>
-->
</a>
</div>
<div data-bs-toggle="modal" data-bs-target="#modalScrape">
<img class="stashr-background w-100" loading="eager" src="/static/assets/folder.svg" />
<img class="stashr-poster w-100" loading="lazy" :src="imageURL" />
<div class="stashr-overlay_bottom w-100 center">
<span>[[ directory.scrape_directory ]]</span>
</div>
<div class="stashr-overlay_bottom w-100 text-center shadow">
[[ directory.scrape_directory ]]
<div data-bs-toggle="modal" data-bs-target="#modalScrape">
<img class="stashr-poster_background w-100" loading="eager" src="/static/assets/folder.svg" />
<a class="stashr-poster_link">
<img class="w-100" :src="imageURL" loading="lazy"/>
</a>
</div>
</div>
</div>
</li>
`,
computed: {
@ -167,9 +168,9 @@ Vue.component('directory', {
})[0]['image']['small_url']
},
checkedStatus() {
string = `text-info fas fa-circle`
string = `text-info fa-2x fas fa-circle`
if(this.directory.scrape_add) {
string = `text-success fas fa-check-circle`
string = `text-success fa-2x fas fa-check-circle`
}
return string
},
@ -203,63 +204,24 @@ Vue.component('directories', {
props: ['directories'],
template: `
<div>
<div class="d-grid w-100 bg-mine m-0 p-0 sticky-top shadow">
<div class="row d-flex flex-nowrap w-100 m-0 p-3 bg-mine">
<input type="text" v-model="search" class="form-control flex-shrink-1" placeholder="Search Folders..." />
<a class="fa-stack p-0 text-white">
<i class="fas fa-bars fa-stack-1x" data-bs-toggle="offcanvas" href="#offcanvasRight" role="button"></i>
</a>
</div>
<div class="mb-3 px-5">
<input type="text" v-model="search" class="form-control" placeholder="Search Folders..." />
</div>
<ul class="d-flex flex-wrap w-100 m-0 p-0 justify-content-center">
<directory
v-for="directory in filteredList"
v-bind:directory="directory"
v-bind:key="directory.scrape_id"
ref="filteredList"
></directory>
</ul>
<div class="offcanvas offcanvas-end" tabindex="-1" id="offcanvasRight" aria-labelledby="offcanvasRightLabel">
<div class="offcanvas-header">
<h5 id="offcanvasRightLabel">Scrape</h5>
<button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
<div class="form-check">
<input class="form-check-input" type="checkbox" value="" id="showIgnoredDirectories" v-model="checked">
<label class="form-check-label" for="flexCheckDefault">
Show Hidden Directories
</label>
</div>
<div class="text-center w-100 m-0 p-3">
<button type="button" class="btn btn-info m-1 w-100" data-bs-toggle="tooltip" data-bs-placement="top" title="Add Selected Directories" onclick="app.addDirectories()">
<i class="fas fa-plus"></i>
Add Selected Directories
</button>
<button type="button" class="btn btn-info m-1 w-100" data-bs-toggle="tooltip" data-bs-placement="top" title="Rescan New Directories" onclick="app.rescanDirectories()">
<i class="fas fa-sync-alt"></i>
Refresh Directories
</button>
<button type="button" class="btn btn-info m-1 w-100" data-bs-toggle="tooltip" data-bs-placement="top" title="Clear Directories" onclick="app.clearDirectories()">
<i class="fas fa-trash-alt"></i>
Clear Directories
</button>
</div>
</div>
</div>
</div>
`,
data() { return { search: '', ignore: 'false', checked: false} },
data() { return { search: '', } },
computed: {
filteredList() {
if (!this.checked) {
return this.directories
.filter(directory => { return directory.scrape_directory.toLowerCase().includes(this.search.toLowerCase()) })
.filter(directory => { return directory.scrape_ignore_directory.toString().includes(this.checked) })
} else {
return this.directories
.filter(directory => { return directory.scrape_directory.toLowerCase().includes(this.search.toLowerCase()) })
}
return this.directories.filter(directory => {
return directory.scrape_directory.toLowerCase().includes(this.search.toLowerCase())
})
},
},
methods: {},
@ -319,56 +281,6 @@ var app = new Vue({
}
})
},
toggleDirectoryIgnore() {
console.log('toggle')
axios.put('{{ url_for('api.api_put_directories_edit', scrape_id='SCRAPEID') }}'.replace('SCRAPEID', this.individual.scrape_id), {
data: {
scrape_ignore_directory: !this.individual.scrape_ignore_directory
}
})
.then(res=> {
console.log(res)
if(res.data.status_code == 200) {
this.individual.scrape_ignore_directory = !this.individual.scrape_ignore_directory
if(this.individual.scrape_ignore_directory) {
stashrToast('Directory Hidden', 'success')
} else {
stashrToast('Directory Unhidden', 'success')
}
}
})
.catch(err => console.log(err))
},
searchNewResults(search) {
axios.post('{{ url_for('api.api_post_directories_extend', scrape_id='SCRAPEID') }}'.replace('SCRAPEID', this.individual.scrape_id), {
data: {
search_terms: search
}
})
.then(res => {
if(res.data.status_code == 200) {
axios.get('{{ url_for('api.api_get_directories_candidates', scrape_id='SCRAPEID') }}'.replace('SCRAPEID', this.individual.scrape_id))
.then(res => {
if (res.data.status_code == 200) {
item = this.directories
.filter(directory => { return directory.scrape_id == this.individual.scrape_id})
item[0].scrape_json = res.data.results;
item[0].scrape_candidate = JSON.parse(res.data.results)[0]['id'];
this.individual = item[0]
}
})
}
})
},
clearDirectories() {
axios.delete('{{ url_for('api.api_delete_scrape_directories') }}')
.then(res => {
if(res.data.status_code == 200) {
this.directories = [];
stashrToast('Scrape Directories Cleared', 'success')
}
})
}
},
delimiters: ["[","]]"]
})

@ -66,17 +66,19 @@ Vue.component('modals', {
Vue.component('volume-item', {
props: ['volume'],
template: `
<li class='stashr-item_container m-2'
<li class='stashr-cover_size m-2'
@mouseover="hover = true"
@mouseleave="hover = false"
>
<div class="stashr-poster_container border border-dark rounded-3" data-bs-toggle="modal" data-bs-target="#modalVolume" :data-volume_id=volume.id v-on:click="this.changeModal">
<div class="stashr-poster_wrapper">
<img class="stashr-background w-100" loading="eager" src="/static/assets/cover.svg" />
<img class="stashr-poster w-100" loading="lazy" :src="volume.image.medium_url" />
</div>
<div class="stashr-overlay_bottom w-100 text-center shadow" v-if="hover">
[[ volume.name ]]
>
<div class='stashr-poster_wrapper rounded'>
<div class="stashr-poster_container border rounded">
<div class="stashr-overlay_bottom w-100 center" v-if="hover">
<a href="#">[[ volume.name ]]</a>
</div>
<img class="stashr-poster_background w-100" loading="eager" src="/static/assets/cover.svg" />
<a class="stashr-poster_link" data-bs-toggle="modal" data-bs-target="#modalVolume" :data-volume_id=volume.id v-on:click="this.changeModal">
<img class="w-100" loading="lazy" :src="volume.image.medium_url"/>
</a>
</div>
</div>
</li>
@ -107,17 +109,28 @@ Vue.component('volumes', {
props: ['volumes'],
template: `
<div>
<div class="d-grid w-100 bg-mine m-0 p-0 sticky-top shadow">
<div class="m-0 p-3 input-group">
<input type="text" v-model="search" class="form-control" placeholder="Search Volumes..." />
<div class="input-group-append">
<button class="btn btn-success" type="button" @click="$emit('do-search', search)">
Search
</button>
</div>
<!--
<div class="form-group">
<input type="text" v-model="search" class="form-control" placeholder="Search Volumes..." />
</div>
<button @click="$emit('do-search', search)">Search</button>
<ul class="d-flex flex-wrap w-100 m-0 p-0 justify-content-center">
<volume-item
v-for="volume in filteredList"
v-bind:volume="volume"
v-bind:key="volume.id"
></volume-item>
</ul>
-->
<div class="input-group mb-3 px-5">
<input type="text" class="form-control" placeholder="Search Volumes..." aria-label="Recipient's username" aria-describedby="basic-addon2" v-model="search">
<div class="input-group-append">
<button class="btn btn-success" type="button" @click="$emit('do-search', search)">
Search
</button>
</div>
</div>
<ul class="d-flex flex-wrap w-100 m-0 p-0 py-2 justify-content-center">
<ul class="d-flex flex-wrap w-100 m-0 p-0 justify-content-center">
<volume-item
v-for="volume in filteredList"
v-bind:volume="volume"

@ -1,176 +1,61 @@
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, shrink-to-fit=no">
<title>Stashr - {{ title }}</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/bootstrap.css') }}">
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/new_base.css') }}">
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/stashr.css') }}?v=0.1.0">
<link rel="stylesheet" href="{{ url_for('static', filename='css/all.css') }}">
<link href="https://fonts.googleapis.com/css?family=Grand+Hotel" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Oswald" rel="stylesheet">
<link rel="manifest" href="{{ url_for('static', filename='manifest/manifest.json') }}">
<link rel="apple-touch-icon" href="touch-icon-iphone.png">
<link rel="apple-touch-icon" sizes="152x152" href="{{ url_for('static', filename='assets/stashr-152.png') }}">
<link rel="apple-touch-icon" sizes="180x180" href="{{ url_for('static', filename='assets/stashr-180.png') }}">
<link rel="apple-touch-icon" sizes="167x167" href="{{ url_for('static', filename='assets/stashr-167.png') }}">
<meta name="apple-mobile-web-app-title" content="Stashr">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<script src="{{ url_for('static', filename='js/stashr.js') }}"></script>
<script src="{{ url_for('static', filename='js/vue.dev.js') }}"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-toast-notification@0.6"></script>
<link href="https://cdn.jsdelivr.net/npm/vue-toast-notification/@0.6/dist/theme-sugar.css" rel="stylesheet">
<!--
<script src="https://cdn.jsdelivr.net/npm/vue-toast-notification"></script>
<link href="https://cdn.jsdelivr.net/npm/vue-toast-notification/dist/theme-sugar.css" rel="stylesheet">
-->
{% block header_script_files %}{% endblock %}
{{ emit_tep('base_page_header_script_files') }}
</head>
<body>
<div class="container-fluid d-flex min-vh-100 m-0 p-0">
<div class="stashr-menu d-flex flex-column p-3 text-white bg-dark text-center min-vh-100 sticky-top overflow-auto" id="sideMenu" style="width:280px;">
<div class="d-flex flex-row">
<a class="navbar-brand" href="{{ url_for('index_page') }}">
<img class="border rounded-circle" src="{{ url_for('static', filename='assets/stashr.svg') }}" width="40" height="40" />
<img src="{{ url_for('static', filename='assets/title.svg') }}" height="25" />
</a>
</div>
<hr/>
<ul class="nav nav-pills flex-column mb-auto text-start">
{% if current_user.role == 'admin' %}
<li>
<a class="nav-link text-white{% if request.path == url_for('settings_app_page') %} active{% endif %}" href="{{ url_for('settings_app_page') }}">
<i class="fa fa-cogs"></i>
Application
</a>
</li>
<li>
<a class="nav-link text-white{% if request.path == url_for('settings_directories_page') %} active{% endif %}" href="{{ url_for('settings_directories_page') }}">
<i class="fas fa-folder"></i>
Directories
</a>
</li>
<li class="nav-item">
<a class="nav-link text-white{% if request.path == url_for('settings_mail_page') %} active{% endif %}" href="{{ url_for('settings_mail_page') }}">
<i class="fa fa-envelope"></i>
Mail
</a>
</li>
<li class="nav-item">
<a class="nav-link text-white{% if request.path == url_for('settings_tasks_page') %} active{% endif %}" href="{{ url_for('settings_tasks_page') }}">
<i class="fa fa-tasks"></i>
Tasks
</a>
</li>
<li class="nav-item">
<a class="nav-link text-white{% if request.path == url_for('settings_all_users_page') %} active{% endif %}" href="{{ url_for('settings_all_users_page') }}">
<i class="fa fa-users"></i>
Users
</a>
</li>
<li class="nav-item">
<a class="nav-link text-white{% if request.path == url_for('settings_plugins_page') %} active{% endif %}" href="{{ url_for('settings_plugins_page') }}">
<i class="fa fa-plug"></i>
Plugins
</a>
</li>
{{ emit_tep('settings_menu') }}
{% endif %}
</ul>
<hr />
<ul class="nav nav-pills d-flex flex-column text-start">
<li>
<a href="{{ url_for('settings_single_user_page', user_id=current_user.id) }}" class="nav-link text-white">
<i class="fa fa-user"></i>
{{ current_user.username }}
</a>
</li>
{% if current_user.role == 'admin' %}
<li>
<a href="{{ url_for('settings_page') }}" class="nav-link text-white">
<i class="fas fa-cogs"></i>
Settings
</a>
</li>
{% endif %}
<li>
<a href="{{ url_for('logout_page') }}" class="nav-link text-white">
<i class="fas fa-sign-out-alt"></i>
Logout
</a>
</li>
</ul>
</div>
<div class="d-flex flex-column w-100 m-0 px-0 min-vh-100 overflow-auto bg-light">
<!-- v HEADER v -->
<!-- ^ HEADER ^ -->
<!-- v CONTENT v -->
{% block settings_pane %}{% endblock %}
<!-- ^ CONTENT ^ -->
<!-- v BUTTON CONTAINER v -->
<div>
<div class="d-grid m-0 p-0 stashr-menu_button bg-dark">
<div class="row d-flex flex-nowrap m-0 p-0 py-2">
<i class="text-white fas fa-ellipsis-v fa-2x" onclick="toggleMenu()"></i>
</div>
{% extends "base.html" %}
{% block content %}
<div class="row w-100 m-0">
<div class="col-12 col-md-10 offset-md-1 bg-light rounded p-2">
<div class="row">
{% if current_user.role == 'admin' %}
<div class="col-12 col-md-3 col-lg-2 p-3">
<ul class="nav flex-column nav-pills">
<li class="nav-item">
<a class="nav-link{% if request.path == url_for('settings_app_page') %} active{% endif %}" href="{{ url_for('settings_app_page') }}">
<i class="fa fa-cogs"></i>
Application
</a>
</li>
<li class="nav-item">
<a class="nav-link{% if request.path == url_for('settings_directories_page') %} active{% endif %}" href="{{ url_for('settings_directories_page') }}">
<i class="fas fa-folder"></i>
Directories
</a>
</li>
<li class="nav-item">
<a class="nav-link{% if request.path == url_for('settings_mail_page') %} active{% endif %}" href="{{ url_for('settings_mail_page') }}">
<i class="fa fa-envelope"></i>
Mail
</a>
</li>
<li class="nav-item">
<a class="nav-link{% if request.path == url_for('settings_tasks_page') %} active{% endif %}" href="{{ url_for('settings_tasks_page') }}">
<i class="fa fa-tasks"></i>
Tasks
</a>
</li>
<li class="nav-item">
<a class="nav-link{% if request.path == url_for('settings_all_users_page') %} active{% endif %}" href="{{ url_for('settings_all_users_page') }}">
<i class="fa fa-users"></i>
Users
</a>
</li>
<li class="nav-item">
<a class="nav-link{% if request.path == url_for('settings_plugins_page') %} active{% endif %}" href="{{ url_for('settings_plugins_page') }}">
<i class="fa fa-plug"></i>
Plugins
</a>
</li>
{{ emit_tep('settings_menu') }}
</ul>
</div>
{% endif %}
<div class="col-12{% if current_user.role=='admin' %} col-md-9 col-lg-10{% endif %} my-2">
{% block settings_pane %}{% endblock %}
</div>
</div>
<!-- ^ BUTTON CONTAINER ^ -->
</div>
</div>
<!-- v MODALS v -->
{% block modals %}{% endblock %}
{{ emit_tep('base_page_modals') }}
<!-- ^ MODALS ^ -->
<!-- v FOOTER SCRIPT INCLUDES v -->
<script src="{{ url_for('static', filename='js/bootstrap.bundle.js') }}"></script>
{% block footer_script_files %}{% endblock %}
{{ emit_tep('base_page_footer_script_files') }}
<!-- ^ FOOTER SCRIPT INCLUDES ^ -->
<!-- v FOOTER SCRIPT v -->
<script type="text/javascript">
Vue.use(VueToast);
// Flashes
{% with flashes = get_flashed_messages(with_categories=true) %}
{% if flashes %}
{% for category, message in flashes %}
stashrToast('{{ message }}', '{{ category }}');
{% endfor %}
{% endif %}
{% endwith %}
window.addEventListener("load", () => {
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("service-worker.js");
}
});
{% block script %}{% endblock %}
</script>
<!-- ^ FOOTER SCRIPT ^ -->
</body>
</html>
{% endblock %}

@ -18,6 +18,9 @@ Vue.component('user',{
props:['user'],
template: `
<tr>
<!--
:href="'{{ url_for('single_publisher_page', publisher_id='PUBLISHERID') }}'.replace('PUBLISHERID', publisher.publisher_id)"
-->
<td><a :href="'{{ url_for('settings_single_user_page', user_id='USERID') }}'.replace('USERID', user.id)">[[ user.id ]]</a></td>
<td><a :href="'{{ url_for('settings_single_user_page', user_id='USERID') }}'.replace('USERID', user.id)">[[ user.username ]]</a></td>
<td>[[ user.role ]]</td>
@ -30,19 +33,18 @@ Vue.component('user',{
Vue.component('users', {
props:['users'],
template: `
<div>
<div class="d-grid w-100 bg-mine m-0 p-0 sticky-top shadow">
<div class="row w-100 m-0 p-3">
<div class="col-sm-12 col-md-6 text-center text-md-start text-white ">
<div class="row r-10 m-2">
<div class="col col-12">
<div class="row">
<div class="col-sm-12 col-md-6 text-center text-md-start">
<h2>Users</h2>
</div>
<div class="col-sm-12 col-md-6 text-center text-md-end">
<a type="button" class="btn btn-info" href="{{ url_for('settings_new_user_page') }}">New User</a>
</div>
</div>
</div>
<div class="d-grid w-100 m-0 p-0">
<div class="row w-100 m-0 p-3">
<hr />
<div class="row">
<table class="table">
<thead>
<tr>

@ -18,19 +18,18 @@
Vue.component('settings', {
props: ['settings'],
template: `
<div>
<div class="d-grid w-100 bg-mine m-0 p-0 sticky-top shadow">
<div class="row w-100 m-0 p-3">
<div class="col-sm-12 col-md-6 text-center text-md-start text-white ">
<div class="row r-10 m-2">
<div class="col col-12">
<div class="row">
<div class="col-sm-12 col-md-6 text-center text-md-start">
<h2>Application</h2>
</div>
<div class="col-sm-12 col-md-6 text-center text-md-end">
<button type="button" class="btn btn-info" data-bs-toggle="modal" data-bs-target="#settingsModal">Modify Settings</button>
</div>
</div>
</div>
<div class="d-grid w-100 m-0 p-0">
<div class="row w-100 m-0 p-3">
<hr />
<div class="row">
<table class="table table-striped">
<tbody>
<tr>

@ -18,19 +18,18 @@
Vue.component('settings', {
props: ['settings'],
template: `
<div>
<div class="d-grid w-100 bg-mine m-0 p-0 sticky-top shadow">
<div class="row w-100 m-0 p-3">
<div class="col-sm-12 col-md-6 text-center text-md-start text-white ">
<div class="row r-10 m-2">
<div class="col col-12">
<div class="row">
<div class="col-sm-12 col-md-6 text-center text-md-start">
<h2>Directories</h2>
</div>
<div class="col-sm-12 col-md-6 text-center text-md-end">
<button type="button" class="btn btn-info" data-bs-toggle="modal" data-bs-target="#settingsModal">Modify Settings</button>
</div>
</div>
</div>
<div class="d-grid w-100 m-0 p-0">
<div class="row w-100 m-0 p-3">
<hr />
<div class="row">
<table class="table table-striped">
<tbody>
<tr>

@ -18,19 +18,18 @@
Vue.component('settings', {
props: ['settings'],
template: `
<div>
<div class="d-grid w-100 bg-mine m-0 p-0 sticky-top shadow">
<div class="row w-100 m-0 p-3">
<div class="col-sm-12 col-md-6 text-center text-md-start text-white ">
<div class="row r-10 m-2">
<div class="col col-12">
<div class="row">
<div class="col-sm-12 col-md-6 text-center text-md-start">
<h2>Mail Settings</h2>
</div>
<div class="col-sm-12 col-md-6 text-center text-md-end">
<button type="button" class="btn btn-info" data-bs-toggle="modal" data-bs-target="#settingsModal">Modify Settings</button>
</div>
</div>
</div>
<div class="d-grid w-100 m-0 p-0">
<div class="row w-100 m-0 p-3">
<hr />
<div class="row">
<table class="table table-striped">
<tbody>
<tr>

@ -2,54 +2,56 @@
{% block settings_pane %}
<div class="d-grid w-100 bg-mine m-0 p-0 sticky-top shadow">
<div class="row w-100 m-0 p-3">
<div class="col-sm-12 col-md-6 text-center text-md-start text-white ">
<h2>New User</h2>
</div>
<div class="col-sm-12 col-md-6 text-center text-md-end">
<button type="button" class="btn btn-info" data-bs-toggle="modal" data-bs-target="#settingsModal">Modify Settings</button>
</div>
</div>
</div>
<div class="d-grid w-100 m-0 p-0">
<div class="row w-100 m-0 p-3">
<form method="POST">
<div id="app">
<div class="row r-10 m-2">
<div class="col col-12">
<div class="row">
<div class="col-sm-12 col-md-6 text-center text-md-start">
<h2>New User</h2>
</div>
</div>
<hr />
<div class="row">
<form method="POST">
{{ new_user_form.csrf_token }}
{{ new_user_form.csrf_token }}
<div class="mb-3">
{{ new_user_form.username.label }}
{{ new_user_form.username(class_='form-control', placeholder=new_user_form.username.label.text) }}
</div>
<div class="mb-3">
{{ new_user_form.email.label }}
{{ new_user_form.email(class_='form-control', placeholder=new_user_form.email.label.text) }}
</div>
<div class="mb-3">
{{ new_user_form.password.label }}
{{ new_user_form.password(type='password', class_='form-control', placeholder=new_user_form.password.label.text) }}
<div class="mb-3">
{{ new_user_form.username.label }}
{{ new_user_form.username(class_='form-control', placeholder=new_user_form.username.label.text) }}
</div>
<div class="mb-3">
{{ new_user_form.email.label }}
{{ new_user_form.email(class_='form-control', placeholder=new_user_form.email.label.text) }}
</div>
<div class="mb-3">
{{ new_user_form.password.label }}
{{ new_user_form.password(type='password', class_='form-control', placeholder=new_user_form.password.label.text) }}
</div>
<div class="mb-3">
{{ new_user_form.confirm_password.label }}
{{ new_user_form.confirm_password(type='password', class_='form-control', placeholder=new_user_form.confirm_password.label.text) }}
</div>
<div class="mb-3">
{{ new_user_form.role.label }}
{{ new_user_form.role(class_='form-control') }}
</div>
<div class="mb-3">
{{ new_user_form.age_rating.label }}
{{ new_user_form.age_rating(class_='form-control') }}
</div>
<div class="modal-footer">
{{ new_user_form.new_user_button(class_='btn btn-success') }}
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
</div>
</form>
</div>
<div class="mb-3">
{{ new_user_form.confirm_password.label }}
{{ new_user_form.confirm_password(type='password', class_='form-control', placeholder=new_user_form.confirm_password.label.text) }}
</div>
<div class="mb-3">
{{ new_user_form.role.label }}
{{ new_user_form.role(class_='form-control') }}
</div>
<div class="mb-3">
{{ new_user_form.age_rating.label }}
{{ new_user_form.age_rating(class_='form-control') }}
</div>
<div class="modal-footer">
{{ new_user_form.new_user_button(class_='btn btn-success') }}
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% block script %}

@ -8,85 +8,29 @@
<div id="app">
<plugins ref="plugins" v-bind:plugins="plugins"></plugins>
<modals v-bind:plugin="plugin"></modals>
</div>
{% endblock %}
{% block script %}
Vue.component('modals', {
props: ['plugin'],
Vue.component('plugin',{
props:['plugin'],
template: `
<div>
<div class="modal" id="modalUpload" tabindex="-1" role="dialog" aria-labelledby="notesModal" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="notesModalTitle">
Upload Plugin
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form>
<div class="input-group mb-3">
<input id="pluginupload" type="file" accept=".zip" name="plugin" @change='uploadFile' required="required" class="form-control">
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<div class="modal" id="modalPlugin" tabindex="-1" role="dialog" aria-labelledby="notesModal" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="notesModalTitle">
[[ plugin.plugin_name ]]
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<strong>Version:</strong> [[ plugin.plugin_version ]]<br />
<strong>Author:</strong> [[ plugin.plugin_author ]]<br />
[[ plugin.plugin_description ]]
</div>
<div class="modal-footer">
<button type="button" class="btn" v-bind:class="pluginClass" data-bs-dismiss="modal" @click="togglePlugin">[[ pluginAction ]]</button>
<button type="button" class="btn btn-danger" data-bs-dismiss="modal" data-bs-toggle="modal" data-bs-target="#deletePlugin">Delete</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<div class="modal" id="deletePlugin" tabindex="-1" role="dialog" aria-labelledby="notesModal" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="notesModalTitle">
Delete Plugin
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
Are you sure you want to delete [[ plugin.plugin_name ]]
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" data-bs-dismiss="modal" @click="deletePlugin">Delete</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
</div>
<tr>
<th scope="row">[[ plugin.plugin_name ]]</th>
<td>[[ plugin.plugin_description ]]</td>
<td>[[ plugin.plugin_version ]]</td>
<td>[[ plugin.plugin_author ]]</td>
<td>[[ plugin.plugin_url ]]</td>
<td>[[ plugin.plugin_license ]]</td>
<td>[[ plugin.plugin_state ]]</td>
<td><button type="button" class="btn" v-bind:class="pluginClass">[[ pluginAction ]]</button> </td>
</tr>
`,
computed: {
pluginClass() {
let classname = 'btn-warning';
let classname = 'btn-danger';
if(this.plugin.plugin_state == 'disabled') {
classname = 'btn-success';
};
@ -100,118 +44,49 @@ Vue.component('modals', {
return text;
}
},
methods: {
uploadFile() {
let formData = new FormData();
var pluginfile = document.querySelector('#pluginupload');
formData.append("file", pluginfile.files[0]);
bootstrap.Modal.getInstance(document.getElementById('modalUpload')).hide();
stashrToast('Uploading Plugin', 'info');
axios.post('{{ url_for('api.api_post_upload_plugin') }}', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
.then(res => {
if(res.data.status_code == 200) {
stashrToast('Plugin Uploaded', 'success')
} else {
stashrToast(res.data.message, 'error')
}
})
},
togglePlugin() {
if(this.plugin.plugin_state == 'disabled') {
console.log('Enabling')
axios.post('{{ url_for('api.api_post_plugin_enable', plugin='PLUGINID') }}'.replace('PLUGINID', this.plugin.plugin_name))
.then(res=>{
if(res.data.status_code == 200) {
this.plugin.plugin_state='enabled'
stashrToast('Plugin Enabled', 'success')
}
})
} else {
console.log('Disabling')
axios.post('{{ url_for('api.api_post_plugin_disable', plugin='PLUGINID') }}'.replace('PLUGINID', this.plugin.plugin_name))
.then(res=>{
if(res.data.status_code == 200) {
this.plugin.plugin_state='disabled'
stashrToast('Plugin Disabled', 'success')
}
})
}
},
deletePlugin() {
axios.post('{{ url_for('api.api_post_plugin_remove', plugin='PLUGINID') }}'.replace('PLUGINID', this.plugin.plugin_package_name))
.then(res=>{
if(res.data.status_code == 200) {
stashrToast('Plugin Deleted', 'success')
}
})
},
},
delimiters: ["[[","]]"]
})
Vue.component('plugin',{
props:['plugin'],
template: `
<li @click="changePlugin" role="button" class='col-12 col-md-3 m-1 p-1 border rounded text-center text-light' v-bind:class="pluginStatusBG" data-bs-toggle="modal" data-bs-target="#modalPlugin">
<strong>[[ plugin.plugin_name ]]</strong><br />
[[ plugin.plugin_version ]]<br />
[[ plugin.plugin_author ]]
</li>
`,
computed: {
pluginStatusBG() {
let classname = 'bg-danger';
if(this.plugin.plugin_state == 'enabled') {
classname = 'bg-success'
}
return classname;
},
},
methods: {
changePlugin() {
app.changePlugin(this.plugin)
}
},
delimiters: ["[[","]]"]
})
Vue.component('plugins',{
props: ['plugins'],
template: `
<div>
<div class="d-grid w-100 bg-mine m-0 p-0 sticky-top shadow">
<div class="row w-100 m-0 p-3">
<div class="col-sm-12 col-md-6 text-center text-md-start text-white ">
<div class="row r-10 m-2">
<div class="col col-12">
<div class="row">
<div class="col-sm-12 col-md-6 text-center text-md-start">
<h2>Plugins</h2>
</div>
<div class="col-sm-12 col-md-6 text-center text-md-end">
<button type="button" class="btn btn-warning" @click="restartServer">Restart Server</button>
<button type="button" class="btn btn-info" data-bs-toggle="modal" data-bs-target="#modalUpload">Install Plugin</button>
<button type="button" class="btn btn-info" data-bs-toggle="modal" data-bs-target="#settingsModal">Install Plugin</button>
</div>
</div>
</div>
<div class="d-grid w-100 m-0 p-0">
<div class="row w-100 m-0 p-3">
<ul class="d-flex flex-wrap w-100 m-0 p-0 justify-content-center">
<plugin
v-for="plugin in plugins"
v-bind:plugin="plugin"
v-bind:key="plugin.plugin_name"
></plugin>
</ul>
<hr />
<div class="row">
<table class="table">
<thead>
<tr>
<th scop="col">Name</th>
<th scop="col">Description</th>
<th scop="col">Version</th>
<th scop="col">Author</th>
<th scop="col">URL</th>
<th scop="col">License</th>
<th scop="col">State</th>
<th scop="col">Action</th>
</tr>
</thead>
<tbody class="tablebody">
<plugin
v-for="plugin in plugins"
v-bind:plugin="plugin"
v-bind:key="plugin.plugin_name"
></plugin>
</tbody>
</table>
</div>
</div>
</div>
</div>
`,
methods: {
restartServer() {
app.restartServer()
}
},
delimiters: ["[[","]]"]
})
@ -219,7 +94,6 @@ var app = new Vue({
el: '#app',
data: {
plugins: [],
plugin: []
},
created() {
this.getPlugins()
@ -229,21 +103,8 @@ var app = new Vue({
axios.get('{{ url_for('api.api_get_plugins') }}')
.then(res => {
this.plugins = res.data.results
this.plugin = this.plugins[0]
})
},
changePlugin(plugin) {
this.plugin = plugin
},
restartServer() {
axios.post('{{ url_for('api.restart_server') }}')
.then(res=>{
if(res.data.status_code == 200) {
stashrToast('Restarting Server', 'success')
}
})
.catch()
},
}
},
delimiters: ["[[","]]"]
})

@ -18,11 +18,11 @@
Vue.component('user', {
props: ['user'],
template: `
<div>
<div class="d-grid w-100 bg-mine m-0 p-0 sticky-top shadow">
<div class="row w-100 m-0 p-3">
<div class="col-sm-12 col-md-6 text-center text-md-start text-white ">
<h2>[[ user.username ]]</h2>
<div class="row r-10 m-2">
<div class="col col-12">
<div class="row">
<div class="col-sm-12 col-md-6 text-center text-md-start">
<h2>User - [[ user.username ]]</h2>
</div>
<div class="col-sm-12 col-md-6 text-center text-md-end">
{% if current_user.id|int == user_id|int %}
@ -36,12 +36,12 @@ Vue.component('user', {
<button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#deleteUserModal">Delete User</button>
{% endif %}
{% endif %}
<br />
<button type="button" class="btn btn-info my-1" data-bs-toggle="modal" data-bs-target="#editUserModal">Edit Account</button>
</div>
</div>
</div>
<div class="d-grid w-100 m-0 p-0">
<div class="row w-100 m-0 p-3">
<hr />
<div class="row">
<table class="table table-striped">
<tbody>
<tr>

@ -1,58 +1,39 @@
{% extends "settings_page.html" %}
{% block header_script_files %}
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
{% endblock %}
{% block settings_pane %}
<div class="d-grid w-100 bg-mine m-0 p-0 sticky-top shadow">
<div class="row w-100 m-0 p-3">
<div class="col-sm-12 col-md-6 text-center text-md-start text-white ">
<h2>Tasks</h2>
<div class="row r-10 m-2">
<div class="col col-12">
<div class="row">
<div class="col-sm-12 col-md-6 text-center text-md-start">
<h2>Tasks</h2>
</div>
</div>
<div class="col-sm-12 col-md-6 text-center text-md-end">
<button type="button" class="btn btn-info" data-bs-toggle="modal" data-bs-target="#settingsModal">Modify Settings</button>
<hr />
<div class="row">
<table class="table table-striped">
<thead>
<tr>
<th scope="col">TASK</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Update New Releases</th><td><i class="fas fa-play"></i></td>
</tr>
<tr>
<th scope="row">Update New Release Subscriptions</th><td><i class="fas fa-play"></i></td>
</tr>
<tr>
<th scope="row">Clean Database</th><td><i class="fas fa-play"></i></td>
</tr>
<tr>
<th scope="row">Restart Server</th><td><i class="fas fa-play"></i></td>
</tr>
{{ emit_tep('settings_page_tasks_task') }}
</tbody>
</table>
</div>
</div>
</div>
<div class="d-grid w-100 m-0 p-0">
<div class="row w-100 m-0 p-3">
<table class="table table-striped">
<thead>
<tr>
<th scope="col">TASK</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Update New Releases</th><td><i class="fas fa-play"></i></td>
</tr>
<tr>
<th scope="row">Update New Release Subscriptions</th><td><i class="fas fa-play"></i></td>
</tr>
<tr>
<th scope="row">Clean Database</th><td><i class="fas fa-play"></i></td>
</tr>
<tr>
<th scope="row">Restart Server</th><td><a onclick="restartServer()"><i class="fas fa-play"></i></a></td>
</tr>
{{ emit_tep('settings_page_tasks_task') }}
</tbody>
</table>
</div>
</div>
{% endblock %}
{% block script %}
function restartServer() {
axios.post('{{ url_for('api.restart_server') }}')
.then(res=>{
console.log(res)
stashrToast('Restarting Server', 'info')
})
.catch()
}
{% endblock %}

@ -51,7 +51,7 @@ Vue.component('issue-modals', {
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body text-center">
<div class="modal-body center">
<a :href="'/read/'+issue.issue_id" id="actionRead" class="btn btn-success my-1">
<i class="fas fa-book-open"></i>
Read
@ -201,98 +201,87 @@ Vue.component('collection-modals', {
Vue.component('collection-jumbo', {
props: ['collection'],
template: `
<div>
<div class="d-grid w-100 bg-mine m-0 p-0 sticky-top shadow">
<div class="row d-flex flex-nowrap w-100 m-0 p-3 bg-mine">
<div class="flex-shrink-1">
<div class="row w-100 m-0 p-0">
<div class="col-12 col-md-10 text-start">
<span role="button" data-bs-toggle="modal" data-bs-target="#modalInfo">
<span class="fs-1 text-white stashr-series_title">[[ collection.collection_name ]]</span>
<i class="fas fa-info-circle text-secondary"></i>
</span>
<h5>
<span class="badge bg-dark">
<i class="fas fa-circle" v-bind:class="statusClass"></i>
[[ statusText ]]
</span>
<span class="badge bg-dark">
<i class="fas fa-user"></i>
[[ collection.user.username ]]
</span>
</h5>
<div class="row w-100 m-0">
<div class='col-12 col-md-10 offset-md-1 p-2'>
<div class='row'>
<div class='col-12 col-md-2 p-2'>
<div class='row'>
<div class='col-10 offest-1'>
<!-- START POSTER -->
<div class="new-stashr_poster-wrapper border rounded">
<div class="stashr-poster_container rounded">
<img class="stashr-poster_background w-100" src="/static/assets/cover.svg" id="poster-bg">
<a class="rounded stashr-poster_link">
<img class="rounded stashr-poster_image w-100" id="lazy-img" v-bind:src="'/images/issues/'+collection.collection_cover_image+'.jpg'" onerror="this.src='/static/assets/cover.svg'">
</a>
</div>
</div>
<!-- END POSTER -->
</div>
</div>
</div>
<a class="fa-stack p-0 text-white">
<i class="fas fa-bars fa-stack-1x" data-bs-toggle="offcanvas" href="#offcanvasRight" role="button"></i>
</a>
</div>
</div>
<div class="offcanvas offcanvas-end" tabindex="-1" id="offcanvasRight" aria-labelledby="offcanvasRightLabel">
<div class="offcanvas-header">
<h5 id="offcanvasRightLabel">[[ collection.collection_name ]]</h5>
<button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
<button type="button" class="btn btn-info w-100 m-1" data-bs-toggle="modal" data-bs-target="#modalEditCollection">
<i class="fas fa-edit mx-1"></i>
Edit Collection
</button>
<button type="button" class="btn btn-info w-100 m-1" data-bs-toggle="modal" data-bs-target="#modalDeleteCollection">
<i class="fas fa-trash-alt mx-1"></i>
Delete Collection
</button>
</div>
</div>
</div>
<!--
<div class="d-grid w-100 bg-mine m-0 p-0 sticky-md-top shadow">
<div class="row w-100 m-0 p-3">
<div class="col-12 col-md-10 text-center text-md-start">
<span class="fs-1 text-white stashr-series_title">[[ collection.collection_name ]]</span>
<span role="button" data-bs-toggle="modal" data-bs-target="#modalInfo">
<i class="fas fa-info-circle text-secondary"></i>
</span>
<h5>
<span class="badge bg-dark">
<i class="fas fa-circle" v-bind:class="statusClass"></i>
[[ statusText ]]
</span>
<span class="badge bg-dark">
<i class="fas fa-user"></i>
[[ collection.user.username ]]
</span>
</h5>
</div>
<div class="col-12 col-md-2 text-center text-md-end">
{% if collection.collection_user_id == current_user.id %}
<div class="dropdown">
<button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuButton" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Actions
</button>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
<a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#modalEditCollection">
<i class="fas fa-edit"></i>
Edit Collection
</a>
<a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#modalDeleteCollection">
<i class="fas fa-trash-alt"></i>
Delete Collection
</a>
<div class="col-12 col-md-10 p-2">
<div class="row w-100 bg-light rounded m-0 p-4">
<div class="col-12 col-md-10">
<div class="row">
<div class="col text-center text-md-start">
<h1 class="stashr-series_title">[[ collection.collection_name ]]</h1>
</div>
</div>
<div class="row text-center text-md-start">
<div class="col-12">
<!-- START BADGES -->
<span class="badge mx-1" :class="statusClass">[[ statusText ]]</span>
<span class="badge bg-info mx-1">[[ collection.user.username ]]</span>
{{ emit_tep('single_collection_page_badge_row', collection=collection) }}
<!-- END BADGES -->
</div>
</div>
<div class="row">
<div class="col text-center text-md-start">
<!-- START BUTTON -->
<button class="btn btn-primary m-1" type="button" data-bs-toggle="modal" data-bs-target="#modalInfo" :data-collection_name="collection.collection_name" :data-collection_description="collection.collection_description">
DESCRIPTION
</button>
{{ emit_tep('single_collection_page_button_row', collection=collection) }}
<!-- END BUTTON -->
</div>
</div>
</div>
<div class="col-12 col-md-2 text-center text-md-right">
<!-- START ACTIONS -->
{% if collection.collection_user_id == current_user.id %}
<div class="dropdown">
<button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuButton" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Actions
</button>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
<a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#modalEditCollection">
<i class="fas fa-edit"></i>
Edit Collection
</a>
<a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#modalDeleteCollection">
<i class="fas fa-trash-alt"></i>
Delete Collection
</a>
{{ emit_tep('single_collection_page_action_dropdown', collection=collection) }}
</div>
</div>
{% endif %}
<!-- END ACTIONS -->
</div>
</div>
</div>
{% endif %}
</div>
</div>
</div>
-->
`,
</div>`,
computed: {
statusClass() {
let classname = 'text-danger';
let classname = 'bg-danger';
if (this.collection.collection_public) {
classname = 'text-success';
classname = 'bg-success';
};
return classname;
},
@ -336,32 +325,29 @@ Vue.component('issues', {
Vue.component('issue', {
props: ['issue'],
template: `
<li class='stashr-item_container m-2 js-sortable-block'
<li class='stashr-cover_size m-2 js-sortable-block'
@mouseover="hover = true"
@mouseleave="hover = false"
>
<div class="stashr-poster_container border border-dark rounded-3">
{% if collection.collection_user_id == current_user.id %}
<div class="stashr-badge_top_left bg-dark js-drag-handle">
<span class="fa-stack p-0">
<i class="fas fa-arrows-alt fa-stack-1x text-white"></i>
</span>
</div>
{% endif %}
<div class="stashr-badge_bottom_right bg-dark">
<span class="fa-stack p-0">
<i class="fas fa-eye fa-stack-1x" :class="statusClass"></i>
<i class="fas fa-slash fa-stack-1x text-danger" v-if="!this.issue.read_status[0].read_status"></i>
</span>
</div>
<div data-bs-toggle="modal" data-bs-target="#modalAction" @click="changeModal">
<img class="stashr-background w-100" loading="eager" src="/static/assets/cover.svg" />
<img class="stashr-poster w-100" loading="lazy" v-bind:src="'/images/issues/'+ issue.issue_id +'.jpg'" />
</div>
<div class="stashr-overlay_bottom w-100 text-center shadow" v-if="hover" data-bs-toggle="modal" data-bs-target="#modalAction" @click="changeModal">
[[ issue.volume.volume_name ]] #[[ issue.issue_number ]]
<div class='stashr-poster_wrapper rounded'>
{% if collection.collection_user_id == current_user.id %}
<div class="stashr-badge_tl badge rounded-pill bg-dark border js-drag-handle">
<i class="fas fa-arrows-alt"></i>
</div>
{% endif %}
<div class="stashr-badge_br badge rounded-pill bg-dark border">
<i class="fas fa-eye" :class="statusClass"></i>
</div>
<div class="stashr-poster_container border rounded">
<div class="stashr-overlay_bottom w-100 center" v-if="hover">
<a href="#">[[ issue.volume.volume_name ]] #[[ issue.issue_number ]]</a>
</div>
<img class="stashr-poster_background w-100" loading="eager" src="/static/assets/cover.svg" />
<a class="stashr-poster_link" data-bs-toggle="modal" data-bs-target="#modalAction" @click="changeModal">
<img class="w-100" loading="lazy" v-bind:src="'/images/issues/'+ issue.issue_id +'.jpg'"/>
</a>
</div>
</div>
</li>
`,
computed: {

@ -11,6 +11,7 @@
{% block content %}
<div id="app">
<publisher-jumbo v-bind:publisher="publisher"></publisher-jumbo>
<volumes v-bind:volumes='volumes'></volumes>
</div>
@ -30,10 +31,8 @@ Vue.component('volumes', {
props: ['volumes'],
template: `
<div>
<div class="d-grid w-100 bg-mine m-0 p-0 sticky-md-top shadow">
<div class="row w-100 m-0 p-3">
<input type="text" v-model="search" class="form-control" placeholder="Search Publisher..." />
</div>
<div class="mb-3 px-5">
<input type="text" v-model="search" class="form-control" placeholder="Search Publisher..." />
</div>
<ul class="d-flex flex-wrap w-100 m-0 p-0 py-2 justify-content-center">
<volume-item
@ -42,6 +41,9 @@ Vue.component('volumes', {
v-bind:key="volume.volume_id"
></volume-item>
</ul>
<!--
<i class="text-primary fas fa-spinner fa-spin fa-3x" v-if="loading"></i>
-->
</div>
`,
data() { return { loading: true, search: '', } },
@ -58,54 +60,40 @@ Vue.component('volumes', {
Vue.component('volume-item', {
props: ['volume'],
template: `
<li class='stashr-item_container m-2'
<li class='stashr-cover_size m-2'
@mouseover="hover = true"
@mouseleave="hover = false"
>
<div class="stashr-poster_container border border-dark rounded-3">
<div :class="volumeTag"></div>
<div class="stashr-poster_info bg-info text-white px-1">
[[ volume.age_rating[0].rating_short ]]
</div>
<div class="stashr-poster_wrapper">
<a :href="'/volumes/'+volume.volume_slug">
<img class="stashr-background w-100" loading="eager" src="/static/assets/cover.svg" />
<img class="stashr-poster w-100" loading="lazy" v-bind:src="'/images/volumes/'+volume.volume_id+'.jpg'" />
</a>
</div>
<div class="stashr-overlay_bottom w-100 text-center shadow" v-if="hover">
<a class="stashr-link" :href="'/volumes/'+volume.volume_slug">
[[ volume.volume_name ]]
</a>
</div>
<div class="stashr-progress_wrapper w-100">
<div class="progress bg-dark" style="height: 5px;">
<div class="progress-bar" :class="progressColor" role="progressbar" :style="progressStyle" aria-valuenow="25" aria-valuemin="0" aria-valuemax="100"></div>
>
<div class='stashr-poster_wrapper rounded'>
<div class="stashr-badge_tl badge rounded-pill bg-info border">[[ volume.age_rating[0].rating_short ]]</div>
<div class="stashr-badge_tr badge rounded-pill bg-primary border">[[ volume.volume_have ]]/[[ volume.volume_total ]]</div>
<div class="stashr-badge_br badge rounded-pill border" :class="statusClass">[[ statusWord ]]</div>
<div class="stashr-poster_container border rounded">
<div class="stashr-overlay_bottom w-100 center" v-if="hover">
<a href="#">[[ volume.volume_name ]]</a>
</div>
<img class="stashr-poster_background w-100" loading="eager" src="/static/assets/cover.svg" />
<a class="stashr-poster_link" :href="'/volumes/'+volume.volume_slug">
<img class="w-100" loading="lazy" v-bind:src="'/images/volumes/'+volume.volume_id+'.jpg'" @error="$event.target.src=volume.volume_image_med"/>
</a>
</div>
</div>
</li>
`,
computed: {
volumeTag() {
let cornertag = '';
if(!this.volume.volume_status) {
cornertag = "stashr-poster_tag bg-danger shadow";
}
return cornertag;
},
progressWidth() {
return (this.volume.volume_have / this.volume.volume_total)*100;
},
progressColor() {
let classname = 'bg-success';
if(this.progressWidth < 100) {
classname = 'bg-warning';
}
return classname
statusClass() {
let classname = 'bg-danger';
if(this.volume.volume_status) {
classname = 'bg-success';
};
return classname;
},
progressStyle() {
return "width: " + this.progressWidth + "%;"
statusWord() {
let status = 'ENDED';
if(this.volume.volume_status) {
status = 'ONGOING';
};
return status;
}
},
data() { return { hover: false } },

@ -143,7 +143,7 @@ Vue.component('modals-issue', {
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body text-center">
<div class="modal-body center">
<span v-if="issue.issue_file_status">
<a id="actionHaveRead" class="btn btn-success my-1" :href="'/read/'+issue.issue_id">
<i class="fas fa-book-open"></i>
@ -418,159 +418,106 @@ Vue.component('modals-volume', {
Vue.component('volume-jumbo', {
props: ['volume'],
template: `
<div>
<div class="d-grid w-100 bg-mine m-0 p-0 sticky-top shadow">
<div class="row d-flex flex-nowrap w-100 m-0 p-3 bg-mine">
<div class="flex-shrink-1">
<div class="row w-100 m-0 p-0">
<div class="col-12 col-md-10 text-start">
<span role="button" data-bs-toggle="modal" data-bs-target="#modalInfo">
<span class="fs-1 text-white stashr-series_title">[[ volume.volume_name ]]</span>
<i class="fas fa-info-circle text-secondary"></i>
</span>
<h5>
<span class="badge bg-dark">
<i class="fas fa-circle" v-bind:class="volumeStatus"></i>
[[ statusWord ]]
</span>
<span class="badge bg-dark">[[ volume.age_rating[0].rating_long ]]</span>
<span class="badge bg-dark">
<i class="far fa-calendar"></i>
[[ volume.volume_year ]]
</span>
<br/>
<span class="badge bg-dark">
<i class="fas fa-book"></i>
[[ publisherName ]]
</span>
<span class="badge bg-dark">
<i class="fas fa-external-link-alt"></i>
ComicVine
</span>
<br/>
<span class="badge bg-dark">Digital: [[ volume.volume_have ]]/[[ volume.volume_total ]]</span>
<span class="badge bg-dark">Physical: [[ ownedNumber ]]/[[ volume.volume_total ]]</span>
<span class="badge bg-dark">Read: [[ readNumber ]]/[[ volume.volume_total ]]</span>
<br/>
{{ emit_tep('single_volume_page_badge_row', volume_id=volume_id) }}
</h5>
<div class="row w-100 m-0">
<div class='col-12 col-md-10 offset-md-1 p-2'>
<div class='row'>
<div class='col-12 col-md-2 p-2'>
<div class='row'>
<div class='col-10 offset-1'>
<!-- START POSTER -->
<div class="new-stashr_poster-wrapper border rounded">
<div class="stashr-poster_container rounded">
<img class="stashr-poster_background w-100" src="/static/assets/cover.svg" id="poster-bg">
<a class="rounded stashr-poster_link">
<img class="rounded stashr-poster_image w-100" id="lazy-img" v-bind:src="'/images/volumes/'+volume.volume_id+'.jpg'" @error="$event.target.src=volume.volume_image_med">
</a>
</div>
</div>
<!-- END POSTER -->
</div>
</div>
</div>
<a class="fa-stack p-0 text-white">
<i class="fas fa-bars fa-stack-1x" data-bs-toggle="offcanvas" href="#offcanvasRight" role="button"></i>
</a>
</div>
</div>
<div class="offcanvas offcanvas-end" tabindex="-1" id="offcanvasRight" aria-labelledby="offcanvasRightLabel">
<div class="offcanvas-header">
<h5 id="offcanvasRightLabel">[[ volume.volume_name ]]</h5>
<button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
{% if (current_user.role.lower() == 'admin') or
(current_user.role.lower() == 'librarian') %}
<button type="button" class="btn btn-info w-100 m-1" data-bs-toggle="modal" data-bs-target="#modalRatingEdit">
<i class="far fa-sticky-note mx-1"></i>
Edit Rating
</button>
<button type="button" class="btn btn-info w-100 m-1" v-on:click="refreshVolume">
<i class="fas fa-sync-alt mx-1"></i>
Refresh
</button>
<button type="button" class="btn btn-info w-100 m-1" v-on:click="toggleStatus">
<span v-if="volume.volume_status">
<i class="fas fa-stop mx-1"></i>
Mark Ended
</span>
<span v-else>
<i class="fas fa-play mx-1"></i>
Mark Ongoing
</span>
</button>
<button type="button" class="btn btn-info w-100 m-1" data-bs-toggle="modal" data-bs-target="#modalRemove">
<i class="fas fa-trash-alt mx-1"></i>
Remove From Library
</button>
{% endif %}
{{ emit_tep("single_volume_page_action_dropdown", volume_id=volume_id) }}
</div>
</div>
</div>
<!--
<div class="d-grid w-100 bg-mine m-0 p-0 sticky-md-top shadow">
<div class="row w-100 m-0 p-3">
<div class="col-12 col-md-10 text-center text-md-start">
<span class="fs-1 text-white stashr-series_title">[[ volume.volume_name ]]</span>
<span role="button" data-bs-toggle="modal" data-bs-target="#modalInfo">
<i class="fas fa-info-circle text-secondary"></i>
</span>
<h5>
<span class="badge bg-dark">
<i class="fas fa-circle" v-bind:class="volumeStatus"></i>
[[ statusWord ]]
</span>
<span class="badge bg-dark">[[ volume.age_rating[0].rating_long ]]</span>
<span class="badge bg-dark">
<i class="far fa-calendar"></i>
[[ volume.volume_year ]]
</span>
<br/>
<span class="badge bg-dark">
<i class="fas fa-book"></i>
[[ publisherName ]]
</span>
<span class="badge bg-dark">
<i class="fas fa-external-link-alt"></i>
ComicVine
</span>
<br/>
<span class="badge bg-dark">Digital: [[ volume.volume_have ]]/[[ volume.volume_total ]]</span>
<span class="badge bg-dark">Physical: [[ ownedNumber ]]/[[ volume.volume_total ]]</span>
<span class="badge bg-dark">Read: [[ readNumber ]]/[[ volume.volume_total ]]</span>
<br/>
{{ emit_tep('single_volume_page_badge_row', volume_id=volume_id) }}
</h5>
</div>
<div class="col-12 col-md-2 text-center text-md-end">
<div class="dropdown">
<a class="btn btn-dark dropdown-toggle" href="#" role="button" id="actionMenu" data-bs-toggle="dropdown" aria-expanded="false">
Actions
</a>
<ul class="dropdown-menu" aria-labelledby="actionMenu">
{% if (current_user.role.lower() == 'admin') or
(current_user.role.lower() == 'librarian') %}
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#modalRatingEdit">
<i class="far fa-sticky-note mx-1"></i>
Edit Rating
</a></li>
<li><a class="dropdown-item" v-on:click="refreshVolume">
<i class="fas fa-sync-alt mx-1"></i>
Refresh
</a></li>
<li><a class="dropdown-item" v-on:click="toggleStatus">
<span v-if="volume.volume_status">
<i class="fas fa-stop mx-1"></i>
Mark Ended
</span>
<span v-else>
<i class="fas fa-play mx-1"></i>
Mark Ongoing
</span>
</a></li>
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#modalRemove">
<i class="fas fa-trash-alt mx-1"></i>
Remove From Library
</a></li>
{% endif %}
{{ emit_tep("single_volume_page_action_dropdown", volume_id=volume_id) }}
</ul>
<div class="col-12 col-md-10 p-2">
<div class="row w-100 bg-light rounded m-0 p-4">
<div class="col-12 col-md-10">
<div class="row">
<div class="col text-center text-md-start">
<h1 class="stashr-series_title">[[ volume.volume_name ]]</h1>
</div>
</div>
<div class="row text-center text-md-start">
<div class="col-12">
<!-- START BADGES -->
<span class="badge mx-1" v-bind:class="volumeStatus">[[ statusWord ]]</span>
<span class="badge bg-info mx-1">[[ volume.age_rating[0].rating_long ]]</span>
<span class="badge bg-info mx-1">[[ publisherName ]]</span>
<span class="badge bg-info mx-1">[[ volume.volume_year ]]</span>
<br>
<span class="badge bg-primary mx-1">Digital: [[ volume.volume_have ]]/[[ volume.volume_total ]]</span>
<span class="badge bg-primary mx-1">Physical: [[ ownedNumber ]]/[[ volume.volume_total ]]</span>
<span class="badge bg-primary mx-1">Read: [[ readNumber ]]/[[ volume.volume_total ]]</span>
<a class="badge bg-primary mx-1" :href="volume.volume_url" target="new">ComicVine</a>
<br>
{{ emit_tep('single_volume_page_badge_row', volume_id=volume_id) }}
<!-- END BADGES -->
</div>
</div>
<div class="row">
<div class="col text-center text-md-start">
<!-- START BUTTON -->
<button class="btn btn-primary m-1" type="button" data-bs-toggle="modal" data-bs-target="#modalInfo">
INFORMATION
</button>
{{ emit_tep('single_volume_page_button_row', volume_id=volume_id) }}
<!-- END BUTTON -->
</div>
</div>
</div>
<div class="col-12 col-md-2 text-center text-md-right">
<!-- START NEW ACTIONS -->
<div class="dropdown">
<a class="btn btn-secondary dropdown-toggle" href="#" role="button" id="actionMenu" data-bs-toggle="dropdown" aria-expanded="false">
Actions
</a>
<ul class="dropdown-menu" aria-labelledby="actionMenu">
{% if (current_user.role.lower() == 'admin') or
(current_user.role.lower() == 'librarian') %}
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#modalRatingEdit">
<i class="far fa-sticky-note mx-1"></i>
Edit Rating
</a></li>
<li><a class="dropdown-item" v-on:click="refreshVolume">
<i class="fas fa-sync-alt mx-1"></i>
Refresh
</a></li>
<li><a class="dropdown-item" v-on:click="toggleStatus">
<span v-if="volume.volume_status">
<i class="fas fa-stop mx-1"></i>
Mark Ended
</span>
<span v-else>
<i class="fas fa-play mx-1"></i>
Mark Ongoing
</span>
</a></li>
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#modalRemove">
<i class="fas fa-trash-alt mx-1"></i>
Remove From Library
</a></li>
{% endif %}
{{ emit_tep("single_volume_page_action_dropdown", volume_id=volume_id) }}
</ul>
</div>
<!-- END NEW ACTIONS -->
</div>
</div>
</div>
</div>
</div>
</div>
-->
`,
computed: {
publisherName() {
@ -598,9 +545,9 @@ Vue.component('volume-jumbo', {
};
},
volumeStatus() {
let classname = 'text-danger';
let classname = 'bg-danger';
if (this.volume.volume_status) {
classname = 'text-success'
classname = 'bg-success'
};
return classname;
},
@ -640,72 +587,77 @@ Vue.component('volume-jumbo', {
.catch(err => console.log(err))
},
},
created() {
document.title = 'Stashr - ' + this.volume.volume_name;
},
delimiters: ["[[","]]"]
})
Vue.component('issue-item', {
props: ['issue'],
template: `
<li class='stashr-item_container m-2'
<li class='stashr-cover_size m-2'
@mouseover="hover = true"
@mouseleave="hover = false"
>
<div class="stashr-poster_container border border-dark rounded-3">
<div class="stashr-poster_info bg-info text-white px-1">
#[[ issue.issue_number ]]
</div>
<div class="stashr-badge_bottom_left bg-dark">
<span class="fa-stack p-0">
<i class="fas fa-stack-1x" v-bind:class="statusFile"></i>
</span>
<!--
<span class="fa-stack p-0" v-on:click="toggleOwned">
<i class="fas fa-book fa-stack-1x" v-bind:class="statusOwned"></i>
</span>
-->
</div>
<div class="stashr-badge_bottom_right bg-dark">
<span class="fa-stack p-0" v-on:click="toggleRead">
<i class="fas fa-eye fa-stack-1x" v-bind:class="statusRead"></i>
<i class="fas fa-slash fa-stack-1x text-danger" v-if="!issue.read_status[0].read_status"></i>
</span>
</div>
<a class="stashr-poster_link" data-bs-toggle="modal" data-bs-target="#modalAction" :data-volume_name=issue.volume.volume_name :data-issue_number=issue.issue_number :data-issue_id=issue.issue_id :data-issue_status=issue.issue_file_status v-on:click="this.changeModal">
<img class="stashr-background w-100" loading="eager" src="/static/assets/cover.svg" />
<img class="stashr-poster w-100" loading="lazy" :src="'/images/issues/' + this.issue.issue_id + '.jpg'" @error="$event.target.src=issue.issue_cover_url"/>
</a>
<div class="stashr-overlay_top w-100 text-center p-1" v-if="hover">
<span v-if="issue.issue_file_status">
{% if (current_user.role.lower() == 'admin') or
(current_user.role.lower() == 'librarian') or
(current_user.role.lower() == 'patron') %}
<a :href="'/api/downloads/'+issue.issue_id" data-toggle='tooltip' data-placement="top" title="Download"><i class="text-primary px-1 fas fa-download"></i></a>
{% endif %}
<a :href="'/read/'+issue.issue_id" data-toggle="tooltip" title="Read"><i class="text-primary px-1 fas fa-book"></i></a>
<a v-on:click="addToReadingList" data-toggle="tooltip" title="Add to Reading List"><i class="text-primary px-1 fas fa-plus"></i></a>
{{ emit_tep("single_volume_page_top_overlay_have", volume_id=volume_id) }}
</span>
<span v-else>
{% if (current_user.role.lower() == 'admin') or
(current_user.role.lower() == 'librarian') %}
<span data-toggle="tooltip" title="Upload to Server">
<a data-bs-toggle="modal" data-bs-target="#modalUpload" v-bind:data-issue_id=issue.issue_id v-bind:data-number=issue.issue_number >
<i class="text-primary px-1 fas fa-cloud-upload-alt"></i>
</a>
</span>
{% endif %}
{{ emit_tep("single_volume_page_top_overlay_missing", volume_id=volume_id) }}
</span>
<div class='stashr-poster_wrapper rounded'>
<div class="stashr-badge_tl badge rounded-pill bg-info border">[[ issue.issue_number ]]</div>
<div class="stashr-badge_bl badge rounded-pill bg-dark border">
<i class="fas fa-check" v-bind:class="statusFile"></i>
<i class="fas fa-book" v-bind:class="statusOwned" v-on:click="toggleOwned"></i>
</div>
<div class="stashr-badge_br badge rounded-pill bg-dark border">
<i class="fas fa-eye" v-bind:class="statusRead" v-on:click="toggleRead"></i>
</div>
<div class="stashr-poster_container border rounded">
<div class="stashr-overlay_top w-100 center" v-if="hover" >
<!-- START BUTTONS -->
<span v-if="issue.issue_file_status">
<!-- HAVE -->
{% if (current_user.role.lower() == 'admin') or
(current_user.role.lower() == 'librarian') or
(current_user.role.lower() == 'patron') %}
<a :href="'/api/downloads/'+issue.issue_id" data-toggle='tooltip' data-placement="top" title="Download"><i class="text-primary px-1 fas fa-download"></i></a>
{% endif %}
<a :href="'/read/'+issue.issue_id" data-toggle="tooltip" title="Read"><i class="text-primary px-1 fas fa-book"></i></a>
<a v-on:click="addToReadingList" data-toggle="tooltip" title="Add to Reading List"><i class="text-primary px-1 fas fa-plus"></i></a>
<!--
<span data-toggle="tooltip" title="Delete">
<a data-bs-toggle="modal" data-bs-target="#modalDelete" :data-id="issue.issue_id" v-bind:data-number="issue.issue_number" >
<i class="text-primary px-1 fas fa-trash-alt"></i>
</a>
</span>
-->
{{ emit_tep("single_volume_page_top_overlay_have", volume_id=volume_id) }}
</span>
<span v-else>
<!-- MISSING -->
{% if (current_user.role.lower() == 'admin') or
(current_user.role.lower() == 'librarian') %}
<span data-toggle="tooltip" title="Upload to Server">
<a data-bs-toggle="modal" data-bs-target="#modalUpload" v-bind:data-issue_id=issue.issue_id v-bind:data-number=issue.issue_number >
<i class="text-primary px-1 fas fa-cloud-upload-alt"></i>
</a>
</span>
{% endif %}
{{ emit_tep("single_volume_page_top_overlay_missing", volume_id=volume_id) }}
</span>
<!-- END BUTTONS -->
</div>
<img class="stashr-poster_background w-100" loading="eager" src="/static/assets/cover.svg" />
<a class="stashr-poster_link" data-bs-toggle="modal" data-bs-target="#modalAction" :data-volume_name=issue.volume.volume_name :data-issue_number=issue.issue_number :data-issue_id=issue.issue_id :data-issue_status=issue.issue_file_status v-on:click="this.changeModal">
<img class="w-100" loading="lazy" :src="'/images/issues/' + this.issue.issue_id + '.jpg'" @error="$event.target.src=issue.issue_cover_url" />
</a>
</div>
</div>
</div>
</li>
`,
data() { return { hover: false, have: false, } },
computed: {
statusFile() {
let classname = 'text-danger fa-times';
let classname = 'text-danger';
if(this.issue.issue_file_status) {
classname = 'text-success fa-check';
classname = 'text-success';
};
return classname;
},
@ -837,7 +789,7 @@ Vue.component('issue-item', {
Vue.component('issues', {
props: ['issues'],
template: `
<div class="py-3">
<div>
<ul class="d-flex flex-wrap w-100 m-0 p-0 justify-content-center">
<issue-item
v-for="issue in issues"

@ -35,22 +35,20 @@ SOFTWARE.
""" --- HUEY IMPORT --- """
""" --- PYTHON IMPORTS --- """
import datetime, time, pathlib, os, shutil, requests, json, sys, subprocess
import datetime, time, pathlib, os, shutil, requests, json
from slugify import slugify
""" --- STASHR DEPENDENCY IMPORTS --- """
from werkzeug.utils import secure_filename
""" --- STASHR CORE IMPORTS --- """
from stashr import log, database, parsefilename, paths, folders, naming, stashr
# from stashr.stashr import stashr_image_downloaded
from stashr import log, database, parsefilename, paths, folders, naming
from stashr.comicvine import cv
from stashr.config import stashrconfig
from flask import flash
from flask_login import current_user
from flask_bcrypt import generate_password_hash
from flask_pluginkit import PluginInstaller
from sqlalchemy.orm import contains_eager
@ -162,8 +160,6 @@ def update_release_list():
database.session.bulk_save_objects(releases)
database.session.commit()
update_volumes_from_release_list()
""" --- COMIC STATUS TASKS --- """
@ -917,8 +913,6 @@ def download_image(id, image_type, url):
with open(filepath, 'wb') as f:
f.write(r.content)
# CALL STASHR SIGNAL FOR IMAGE DL HERE
stashr.stashr_image_downloaded.send(filepath=filepath, folderpath=folderpath, image_id=id, image_type=image_type)
def update_volumes_from_release_list():
@ -931,6 +925,91 @@ def update_volumes_from_release_list():
refresh_single_volume(update.NewReleases.new_release_volume_id)
""" --------------------- SCRAPE FOLDERS (RE)WRITE --------------------- """
def create_scrape_entries():
missing_links = database.session \
.query(database.Directories) \
.filter(database.Directories.directory_in_library == False) \
.all()
# for item in missing_links:
# create_empty_scrape_entry(item)
# get_scrape_candidates(item)
for item in missing_links:
candidates = cv.search(item.directory_path, limit=10, resources=['volume'])
match_found = False
scrape_candidate = candidates.results[0]['id']
for candidate in candidates.results:
if str(candidate['id']) in item.directory_path:
scrape_candidate = candidate['id']
match_found = True
break
if not match_found:
if str(candidate['start_year']) in item.directory_path:
scrape_candidate = candidate['id']
match_found = True
new_scrape_item = database.ScrapeItems(
scrape_directory = item.directory_path,
scrape_directory_id = item.directory_id,
scrape_json = json.dumps(candidates.results),
scrape_candidate = scrape_candidate,
scrape_match = match_found
)
try:
database.session.merge(new_scrape_item)
database.session.commit()
except:
database.session.rollback()
logger.warning('Scrape Item in database')
def get_scrape_candidates(directory):
scrape_item = database.session \
.query(database.ScrapeItems) \
.filter(database.ScrapeItems.scrape_directory == directory) \
.first()
candidates = cv.search(directory, limit=10, resources=['volume'])
setattr(scrape_item, 'scrape_json', candidates)
database.session.merge(scrape_item)
database.session.commit()
def add_scrape_match(match_item):
match_item.directory.directory_volume_id = match_item.scrape_candidate
match_item.directory.directory_in_library = 1
database.session.merge(match_item)
database.session.delete(match_item)
database.session.commit()
add_volume_to_library(match_item.scrape_candidate)
scrape_volume_issues(match_item.scrape_candidate)
scan_volume_files(match_item.scrape_candidate)
def create_empty_scrape_entry(item):
new_scrape_item = database.ScrapeItems(
scrape_directory = item.directory_path,
scrape_directory_id = item.directory_id
)
try:
database.session.merge(new_scrape_item)
database.session.commit()
except:
database.session.rollback()
logger.warning('Scrape Item in database')
""" --------------------- SCRAPE FOLDERS (RE)(RE)WRITE --------------------- """
@ -972,15 +1051,6 @@ def new_create_scrape_entries():
def new_create_empty_scrape_entry(item):
check_scrape_item = database.session \
.query(database.ScrapeItems) \
.filter(database.ScrapeItems.scrape_directory_id == item.directory_id) \
.first() \
if check_scrape_item is not None:
logger.warning('Scrape Item in database')
return
new_scrape_item = database.ScrapeItems(
scrape_directory = item.directory_path,
scrape_directory_id = item.directory_id
@ -999,11 +1069,6 @@ def new_get_scrape_candidates(item):
.query(database.ScrapeItems) \
.filter(database.ScrapeItems.scrape_directory == item.directory_path) \
.first()
if scrape_item.scrape_candidate is not None:
logger.warning('Item Already Scraped')
return
candidates = cv.search(item.directory_path, limit=10, resources=['volume'])
scrape_candidate = candidates.results[0]['id']
match_found = False
@ -1015,21 +1080,14 @@ def new_get_scrape_candidates(item):
if not match_found:
if str(candidate['start_year']) in item.directory_path:
scrape_candidate = candidate['id']
match_found = False
match_found = True
scrape_item.scrape_json = json.dumps(candidates.results)
scrape_item.scrape_candidate = scrape_candidate
scrape_item.scrape_match = match_found
scrape_item.scrape_add = match_found
database.session.merge(scrape_item)
database.session.commit()
if not match_found:
print('extend the results')
# new_scrape_extend_matches_by_item(scrape_item)
new_scrape_extend_matches(scrape_item.scrape_id)
pass
def new_add_scraped_matches():
matched_directories = database.session \
@ -1042,106 +1100,13 @@ def new_add_scraped_matches():
def new_add_scrape_match(item):
check_directory = database.session \
.query(database.Directories) \
.filter(database.Directories.directory_volume_id == item.scrape_candidate) \
.first()
if check_directory is not None:
logger.warning(f'Volume {item.scrape_candidate} in database')
return
item.directory.directory_volume_id = item.scrape_candidate
item.directory.directory_in_library = 1
database.session.merge(item)
database.session.delete(item)
try:
database.session.commit()
except:
logger.warning(f'Volume {item.scrape_candidate} in database')
database.session.rollback()
return
database.session.commit()
add_volume_to_library(item.scrape_candidate)
scrape_volume_issues(item.scrape_candidate)
scan_volume_files(item.scrape_candidate)
def new_scrape_extend_matches_by_item(item):
candidates = cv.search(item.scrape_directory, limit=100, resources=['volume'])
# print(candidates.results)
print(len(candidates.results))
item.scrape_json = json.dumps(candidates.results)
database.session.merge(item)
database.session.commit()
def new_scrape_extend_matches(scrape_id, search_terms=None):
scrape_item = database.session \
.query(database.ScrapeItems) \
.filter(database.ScrapeItems.scrape_id == scrape_id) \
.first()
if search_terms is None:
candidates = cv.search(scrape_item.scrape_directory, limit=100, resources=['volume'])
else:
candidates = cv.search(search_terms, limit=100, resources=['volume'])
print(len(candidates.results))
if len(candidates.results) > 0:
scrape_item.scrape_json = json.dumps(candidates.results)
scrape_item.scrape_candidate = candidates.results[0]['id']
database.session.merge(scrape_item)
database.session.commit()
""" --------------------- (UN)INSTALL PLUGINS --------------------- """
def install_plugin(filepath):
plugin_file = os.path.join(
folders.StashrFolders().temp_folder(),
filepath
)
if not os.path.isfile(plugin_file):
logger.warning(f'Plugin {filepath} not found')
return
logger.info(f'Installing Plugin: {filepath}')
install = PluginInstaller(plugin_abspath=folders.StashrFolders().plugins_folder()).addPlugin(method='local', filepath=plugin_file, remove=True)
if install['code'] == 0:
logger.info(f'Plugin {filepath} installed')
else:
print(install)
logger.warning(f'Error occurred installing {filepath}')
def uninstall_plugin(package_name):
uninstall = PluginInstaller(plugin_abspath=stashrconfig['DIRECTORY']['plugins']).removePlugin('notify_pushbullet_2')
if uninstall['code'] == 0:
logger.info(f'Plugin {package_name} uninstalled')
else:
logger.warning(f'Error uninstalling {package_name}')
def enable_plugin(plugin):
stashr.pm.enable_plugin(plugin)
def disable_plugin(plugin):
stashr.pm.disable_plugin(plugin)
def install_package(package):
subprocess.check_call([sys.executable, '-m', 'pip', 'install', package])
Loading…
Cancel
Save