Compare commits

...

37 Commits

Author SHA1 Message Date
Andrew a2ead0a4a3 Fix read overflow 3 years ago
Andrew 68000ba7e0 Update Settings to use older version of Vue Toaszt 3 years ago
Andrew 7715dd32ae Fix to use Vue2 Version of Toast 3 years ago
Andrew 58a6bc5e2d Fixed Clear Issues 4 years ago
Andrew 314e5c5041 Added ability to clear scraped folders 4 years ago
Andrew a2c1f87230 Fix some scrape issues 4 years ago
Andrew 82d9107349 Updates for better scraping 4 years ago
Andrew a42a2c1d68 Change index routing to route to all volumes 4 years ago
Andrew 97765b7ff3 Quick CSS fix for menu button 4 years ago
Andrew a97724e461 More template changes 4 years ago
Andrew 5efce67e50 Update issues in library from new release update 4 years ago
Andrew 15889a4bbf Add sort and filter to new release 4 years ago
Andrew 3d57531fcc Fix scrape modal change 4 years ago
Andrew 03e2be9fa8 Moved submenu to offcanvas 4 years ago
Andrew 0349435dcf Added more filtering options 4 years ago
Andrew e82d583dce Added sort and filter to all volumes page 4 years ago
Andrew ada4bce0eb Applied new layout 4 years ago
Andrew 38ae681a9b Updated to Bootstrap 5beta3 4 years ago
Andrew 93c98d5c30 More fixes 4 years ago
Andrew d15a04ab58 More restart fixes 4 years ago
Andrew 2a452df527 Rewrite of plugins page 4 years ago
Andrew 88a4422e29 Create delete plugin path 4 years ago
Andrew 99212164a2 Fixed missing character 4 years ago
Andrew b012174e65 Plugin and Restart changes 4 years ago
Andrew 9ab2b08adf Added desktop PWA support 4 years ago
Andrew 55a6933311 Add image signal call to image download 4 years ago
Andrew 1979df8481 Add internal signals to core 4 years ago
Andrew e78cef07f3 Moved around restart functions 4 years ago
Andrew c78283e2b3 Minor fix 4 years ago
Andrew 67d7e8e4ff Rework server restart 4 years ago
Andrew 68d8de24fe Start work on installing plugins 4 years ago
Andrew 7438c3ffe6 Fix for scanning directories 4 years ago
Andrew e0ea7aa619 Fix Page Title on Single Volume Issues 4 years ago
Andrew 44c6e1c171 Another fix 4 years ago
Andrew 182b4594d5 Wrap directory update in a try 4 years ago
Andrew ecda2ad52a Fix for match found on scan 4 years ago
Andrew 4725cf58fc Scrape Add selected if match dound 4 years ago
  1. 348
      stashr/api.py
  2. 10
      stashr/database.py
  3. 9
      stashr/folders.py
  4. 63
      stashr/routes.py
  5. 21
      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. 4294
      stashr/static/js/bootstrap.bundle.js
  19. 3986
      stashr/static/js/bootstrap.esm.js
  20. 4010
      stashr/static/js/bootstrap.js
  21. 86
      stashr/static/js/service-worker.js
  22. 21
      stashr/static/js/stashr.js
  23. 12
      stashr/static/manifest/manifest.json
  24. 10
      stashr/static/offline.html
  25. 34
      stashr/templates/all_collections_page.html
  26. 22
      stashr/templates/all_publishers_page.html
  27. 211
      stashr/templates/all_volumes_page.html
  28. 189
      stashr/templates/base.html
  29. 104
      stashr/templates/first_run_page.html
  30. 83
      stashr/templates/login_page.html
  31. 96
      stashr/templates/new_releases_page.html
  32. 6
      stashr/templates/read_issue_page.html
  33. 68
      stashr/templates/reading_list_page.html
  34. 45
      stashr/templates/register_page.html
  35. 170
      stashr/templates/scrape_page.html
  36. 35
      stashr/templates/search_page.html
  37. 151
      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. 20
      stashr/templates/settings_page_new_user.html
  43. 215
      stashr/templates/settings_page_plugins.html
  44. 16
      stashr/templates/settings_page_single_user.html
  45. 35
      stashr/templates/settings_page_tasks.html
  46. 140
      stashr/templates/single_collection_page.html
  47. 70
      stashr/templates/single_publisher_page.html
  48. 230
      stashr/templates/single_volume_page.html
  49. 211
      stashr/utils.py

@ -2064,6 +2064,7 @@ def api_get_plugins():
list_item['plugin_url'] = item.plugin_url list_item['plugin_url'] = item.plugin_url
list_item['plugin_license'] = item.plugin_license list_item['plugin_license'] = item.plugin_license
list_item['plugin_state'] = item.plugin_state list_item['plugin_state'] = item.plugin_state
list_item['plugin_package_name'] = item.plugin_package_name
# list_item[] # list_item[]
data.append(list_item) data.append(list_item)
@ -2121,6 +2122,45 @@ def api_get_directories():
return create_json_return('200', results=data, total_results=total_results) 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']) @api.route('/scrape/directories/scan', methods=['POST'])
def api_post_directories_scan(): def api_post_directories_scan():
"""To Update Later """To Update Later
@ -2237,7 +2277,8 @@ def api_put_directories_edit(scrape_id):
allowed_keys = [ allowed_keys = [
'scrape_add', 'scrape_add',
'scrape_candidate' 'scrape_candidate',
'scrape_ignore_directory'
] ]
check_directory = database.session \ check_directory = database.session \
@ -2249,18 +2290,319 @@ def api_put_directories_edit(scrape_id):
return jsonify(create_json_return('404')) return jsonify(create_json_return('404'))
for key, value in request.json['data'].items(): for key, value in request.json['data'].items():
print(f'{key} - {value}')
if key not in allowed_keys: if key not in allowed_keys:
database.session.rollbak() database.session.rollback()
return jsonify(create_json_return('400')) return jsonify(create_json_return('400'))
setattr(check_directory, key, value) setattr(check_directory, key, value)
try:
database.session.merge(check_directory) database.session.merge(check_directory)
database.session.commit() database.session.commit()
# print(check_directory.scrape_id) except Exception as e:
print(e)
database.session.rollback()
return jsonify(create_json_return('200')) 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.commit()
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 --- """ """ --- API WRAPPER --- """
"""--------------------- """---------------------

@ -49,6 +49,7 @@ from flask_bcrypt import generate_password_hash
""" --- SQLALCHEMY IMPORTS --- """ """ --- SQLALCHEMY IMPORTS --- """
from sqlalchemy import * from sqlalchemy import *
from sqlalchemy import exc
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import * from sqlalchemy.orm import *
@ -441,6 +442,8 @@ class ScrapeItems(Base):
scrape_candidate = Column(Integer) scrape_candidate = Column(Integer)
scrape_json = Column(String) scrape_json = Column(String)
scrape_ignore_directory = Column(Boolean, server_default='0')
directory = relationship('Directories', directory = relationship('Directories',
primaryjoin='ScrapeItems.scrape_directory_id == Directories.directory_id', primaryjoin='ScrapeItems.scrape_directory_id == Directories.directory_id',
backref='scrape_items', backref='scrape_items',
@ -653,8 +656,13 @@ def migrate_database():
# CHECK FOR NEW DATABASE TABLES # CHECK FOR NEW DATABASE TABLES
if not engine.dialect.has_table(engine.connect(), "directory_link"): if not engine.dialect.has_table(engine.connect(), "directory_link"):
Directories.__table__.create(bind=engine) Directories.__table__.create(bind=engine)
# CHECK FOR NEW TABLE COLUMNS
# 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 # Create Database
if not os.path.exists(folders.StashrPaths().db_path()): if not os.path.exists(folders.StashrPaths().db_path()):

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

@ -116,11 +116,18 @@ def index_page():
flash('First Run') flash('First Run')
return redirect(url_for('first_run_page')) 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( return render_template(
'index_page.html', 'index_page.html',
title='Home', title='Home',
open_registration=stashrconfig['APP']['open_registration'] open_registration=stashrconfig['APP']['open_registration']
) )
"""
# LOGIN # LOGIN
@ -156,28 +163,6 @@ 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 # FORGOT PASSWORD
@app.route('/forgot', methods=['GET', 'POST']) @app.route('/forgot', methods=['GET', 'POST'])
def forgot_page(): def forgot_page():
@ -211,7 +196,7 @@ def logout_page():
logout_user() logout_user()
# identity_changed.send(current_app._get_current_object(), identity=AnonymousIdentity()) # identity_changed.send(current_app._get_current_object(), identity=AnonymousIdentity())
flash('Logged Out', 'info') flash('Logged Out', 'info')
return redirect(url_for('index_page')) return redirect(url_for('login_page'))
""" --- SETTINGS --- """ """ --- SETTINGS --- """
@ -480,18 +465,18 @@ def first_run_page():
if current_user.is_authenticated: if current_user.is_authenticated:
flash('App Configured', 'info') flash('App Configured', 'info')
return redirect(url_for('index_page')) return redirect(url_for('login_page'))
if not stashrconfig['APP']['first_run']: if not stashrconfig['APP']['first_run']:
flash('App Configured', 'info') flash('App Configured', 'info')
return redirect(url_for('index_page')) return redirect(url_for('login_page'))
first_run_form = forms.app_first_run_form() first_run_form = forms.app_first_run_form()
if request.method == 'POST' and first_run_form.first_run_button.data: if request.method == 'POST' and first_run_form.first_run_button.data and first_run_form.validate():
utils.complete_first_run(first_run_form) utils.complete_first_run(first_run_form)
flash('Setup Complete', 'success') flash('Setup Complete', 'success')
return redirect(url_for('index_page')) return redirect(url_for('login_page'))
return render_template( return render_template(
'first_run_page.html', 'first_run_page.html',
@ -705,6 +690,30 @@ def custom_image_static(foldername, filename):
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 -- DEVELOPMENT

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

@ -43,7 +43,6 @@ import os
""" --- STASHR DEPENDENCY IMPORTS --- """ """ --- STASHR DEPENDENCY IMPORTS --- """
""" --- STASHR CORE IMPORTS --- """ """ --- STASHR CORE IMPORTS --- """
from stashr import paths, log, comicvine, config, api from stashr import paths, log, comicvine, config, api
from stashr import tasks
""" --- FLASK IMPORT --- """ """ --- FLASK IMPORT --- """
from flask import Flask from flask import Flask
@ -73,8 +72,16 @@ logger = log.stashr_logger(__name__)
""" --- CREATE SIGNALS --- """ """ --- CREATE SIGNALS --- """
namespace = Namespace() namespace = Namespace()
# STASHR NOTIFICATIONS
stashr_notification = namespace.signal('stashr_notification') stashr_notification = namespace.signal('stashr_notification')
# STASHR NEW RELEASE UPDATED
stashr_new_releases_update = namespace.signal('stashr_new_releases_update') 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 --- """ """ --- CREATE READING LIST --- """
# reading_image_list = [] # reading_image_list = []

@ -0,0 +1,130 @@
<?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>

After

Width:  |  Height:  |  Size: 13 KiB

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

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

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

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

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

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

@ -1,6 +1,6 @@
@charset "UTF-8"; @charset "UTF-8";
/*! /*!
* Bootstrap v5.0.0-beta2 (https://getbootstrap.com/) * Bootstrap v5.0.0-beta3 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors * Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc. * Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
@ -56,10 +56,6 @@ body {
-webkit-tap-highlight-color: rgba(0, 0, 0, 0); -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
} }
[tabindex="-1"]:focus:not(:focus-visible) {
outline: 0 !important;
}
hr { hr {
margin: 1rem 0; margin: 1rem 0;
color: inherit; color: inherit;
@ -130,7 +126,6 @@ p {
abbr[title], abbr[title],
abbr[data-bs-original-title] { abbr[data-bs-original-title] {
text-decoration: underline;
-webkit-text-decoration: underline dotted; -webkit-text-decoration: underline dotted;
text-decoration: underline dotted; text-decoration: underline dotted;
cursor: help; cursor: help;
@ -336,6 +331,9 @@ select {
select { select {
word-wrap: normal; word-wrap: normal;
} }
select:disabled {
opacity: 1;
}
[list]::-webkit-calendar-picker-indicator { [list]::-webkit-calendar-picker-indicator {
display: none; display: none;
@ -2184,10 +2182,6 @@ progress {
.form-control::-webkit-date-and-time-value { .form-control::-webkit-date-and-time-value {
height: 1.5em; height: 1.5em;
} }
.form-control::-webkit-input-placeholder {
color: #6c757d;
opacity: 1;
}
.form-control::-moz-placeholder { .form-control::-moz-placeholder {
color: #6c757d; color: #6c757d;
opacity: 1; opacity: 1;
@ -2359,7 +2353,6 @@ textarea.form-control-lg {
background-image: none; background-image: none;
} }
.form-select:disabled { .form-select:disabled {
color: #6c757d;
background-color: #e9ecef; background-color: #e9ecef;
} }
.form-select:-moz-focusring { .form-select:-moz-focusring {
@ -2600,9 +2593,6 @@ textarea.form-control-lg {
transition: none; transition: none;
} }
} }
.form-floating > .form-control::-webkit-input-placeholder {
color: transparent;
}
.form-floating > .form-control::-moz-placeholder { .form-floating > .form-control::-moz-placeholder {
color: transparent; color: transparent;
} }
@ -2796,6 +2786,12 @@ textarea.form-control-lg {
margin-left: 0.5em; 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 { .invalid-feedback {
display: none; display: none;
width: 100%; width: 100%;
@ -2872,6 +2868,12 @@ textarea.form-control-lg {
margin-left: 0.5em; 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 { .btn {
display: inline-block; display: inline-block;
font-weight: 400; font-weight: 400;
@ -3546,11 +3548,9 @@ textarea.form-control-lg {
left: auto /* rtl:ignore */; left: auto /* rtl:ignore */;
} }
} }
.dropup .dropdown-menu { .dropup .dropdown-menu[data-bs-popper] {
top: auto; top: auto;
bottom: 100%; bottom: 100%;
}
.dropup .dropdown-menu[data-bs-popper] {
margin-top: 0; margin-top: 0;
margin-bottom: 0.125rem; margin-bottom: 0.125rem;
} }
@ -3887,6 +3887,11 @@ textarea.form-control-lg {
text-align: center; text-align: center;
} }
.nav-fill .nav-item .nav-link,
.nav-justified .nav-item .nav-link {
width: 100%;
}
.tab-content > .tab-pane { .tab-content > .tab-pane {
display: none; display: none;
} }
@ -4268,7 +4273,7 @@ textarea.form-control-lg {
text-decoration: none; text-decoration: none;
} }
.card-link + .card-link { .card-link + .card-link {
margin-left: 1rem /* rtl:ignore */; margin-left: 1rem;
} }
.card-header { .card-header {
@ -4381,8 +4386,8 @@ textarea.form-control-lg {
font-size: 1rem; font-size: 1rem;
color: #212529; color: #212529;
text-align: left; text-align: left;
background-color: transparent; background-color: #fff;
border: 1px solid rgba(0, 0, 0, 0.125); border: 0;
border-radius: 0; border-radius: 0;
overflow-anchor: none; 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; 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;
@ -4392,12 +4397,10 @@ textarea.form-control-lg {
transition: none; transition: none;
} }
} }
.accordion-button.collapsed {
border-bottom-width: 0;
}
.accordion-button:not(.collapsed) { .accordion-button:not(.collapsed) {
color: #0c63e4; color: #0c63e4;
background-color: #e7f1ff; background-color: #e7f1ff;
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.125);
} }
.accordion-button:not(.collapsed)::after { .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"); 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");
@ -4433,47 +4436,53 @@ textarea.form-control-lg {
margin-bottom: 0; margin-bottom: 0;
} }
.accordion-item:first-of-type .accordion-button { .accordion-item {
margin-bottom: -1px;
background-color: #fff;
border: 1px solid rgba(0, 0, 0, 0.125);
}
.accordion-item:first-of-type {
border-top-left-radius: 0.25rem; border-top-left-radius: 0.25rem;
border-top-right-radius: 0.25rem; border-top-right-radius: 0.25rem;
} }
.accordion-item:last-of-type .accordion-button.collapsed { .accordion-item:first-of-type .accordion-button {
border-bottom-width: 1px; border-top-left-radius: calc(0.25rem - 1px);
border-top-right-radius: calc(0.25rem - 1px);
}
.accordion-item:last-of-type {
margin-bottom: 0;
border-bottom-right-radius: 0.25rem; border-bottom-right-radius: 0.25rem;
border-bottom-left-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 { .accordion-item:last-of-type .accordion-collapse {
border-bottom-width: 1px;
border-bottom-right-radius: 0.25rem; border-bottom-right-radius: 0.25rem;
border-bottom-left-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 { .accordion-body {
padding: 1rem 1.25rem; padding: 1rem 1.25rem;
} }
.accordion-flush .accordion-button { .accordion-flush .accordion-collapse {
border-width: 0;
}
.accordion-flush .accordion-item {
border-right: 0; border-right: 0;
border-left: 0; border-left: 0;
border-radius: 0; border-radius: 0;
} }
.accordion-flush .accordion-collapse { .accordion-flush .accordion-item:first-child {
border-width: 0; border-top: 0;
} }
.accordion-flush .accordion-item:first-of-type .accordion-button { .accordion-flush .accordion-item:last-child {
border-top-width: 0; border-bottom: 0;
border-top-left-radius: 0;
border-top-right-radius: 0;
} }
.accordion-flush .accordion-item:last-of-type .accordion-button.collapsed { .accordion-flush .accordion-item .accordion-button {
border-bottom-width: 0; border-radius: 0;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
} }
.breadcrumb { .breadcrumb {
@ -4767,6 +4776,15 @@ textarea.form-control-lg {
border-radius: 0.25rem; 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 { .list-group-item-action {
width: 100%; width: 100%;
color: #495057; color: #495057;
@ -4787,6 +4805,7 @@ textarea.form-control-lg {
position: relative; position: relative;
display: block; display: block;
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
color: #212529;
text-decoration: none; text-decoration: none;
background-color: #fff; background-color: #fff;
border: 1px solid rgba(0, 0, 0, 0.125); border: 1px solid rgba(0, 0, 0, 0.125);
@ -5178,7 +5197,7 @@ textarea.form-control-lg {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
z-index: 1050; z-index: 1060;
display: none; display: none;
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -5242,7 +5261,7 @@ textarea.form-control-lg {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
z-index: 1040; z-index: 1050;
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
background-color: #000; background-color: #000;
@ -5465,7 +5484,7 @@ textarea.form-control-lg {
} }
.tooltip { .tooltip {
position: absolute; position: absolute;
z-index: 1070; z-index: 1080;
display: block; display: block;
margin: 0; margin: 0;
font-family: var(--bs-font-sans-serif); font-family: var(--bs-font-sans-serif);
@ -5567,7 +5586,7 @@ textarea.form-control-lg {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0 /* rtl:ignore */; left: 0 /* rtl:ignore */;
z-index: 1060; z-index: 1070;
display: block; display: block;
max-width: 276px; max-width: 276px;
font-family: var(--bs-font-sans-serif); font-family: var(--bs-font-sans-serif);
@ -5972,6 +5991,86 @@ textarea.form-control-lg {
animation-duration: 1.5s; 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 { .clearfix::after {
display: block; display: block;
clear: both; clear: both;
@ -6424,10 +6523,6 @@ textarea.form-control-lg {
border-color: #fff !important; border-color: #fff !important;
} }
.border-0 {
border-width: 0 !important;
}
.border-1 { .border-1 {
border-width: 1px !important; border-width: 1px !important;
} }
@ -7098,6 +7193,10 @@ textarea.form-control-lg {
padding-left: 3rem !important; padding-left: 3rem !important;
} }
.font-monospace {
font-family: var(--bs-font-monospace) !important;
}
.fs-1 { .fs-1 {
font-size: calc(1.375rem + 1.5vw) !important; font-size: calc(1.375rem + 1.5vw) !important;
} }
@ -7150,16 +7249,20 @@ textarea.form-control-lg {
font-weight: bolder !important; font-weight: bolder !important;
} }
.text-lowercase { .lh-1 {
text-transform: lowercase !important; line-height: 1 !important;
} }
.text-uppercase { .lh-sm {
text-transform: uppercase !important; line-height: 1.25 !important;
} }
.text-capitalize { .lh-base {
text-transform: capitalize !important; line-height: 1.5 !important;
}
.lh-lg {
line-height: 2 !important;
} }
.text-start { .text-start {
@ -7174,6 +7277,45 @@ textarea.form-control-lg {
text-align: center !important; 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;
}
/* rtl:begin:remove */
.text-break {
word-wrap: break-word !important;
word-break: break-word !important;
}
/* rtl:end:remove */
.text-primary { .text-primary {
color: #0d6efd !important; color: #0d6efd !important;
} }
@ -7230,22 +7372,6 @@ textarea.form-control-lg {
color: inherit !important; 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 { .bg-primary {
background-color: #0d6efd !important; background-color: #0d6efd !important;
} }
@ -7294,37 +7420,6 @@ textarea.form-control-lg {
background-image: var(--bs-gradient) !important; 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 { .user-select-all {
-webkit-user-select: all !important; -webkit-user-select: all !important;
-moz-user-select: all !important; -moz-user-select: all !important;

@ -1,6 +1,6 @@
@charset "UTF-8"; @charset "UTF-8";
/*! /*!
* Bootstrap v5.0.0-beta2 (https://getbootstrap.com/) * Bootstrap v5.0.0-beta3 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors * Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc. * Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
@ -56,10 +56,6 @@ body {
-webkit-tap-highlight-color: rgba(0, 0, 0, 0); -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
} }
[tabindex="-1"]:focus:not(:focus-visible) {
outline: 0 !important;
}
hr { hr {
margin: 1rem 0; margin: 1rem 0;
color: inherit; color: inherit;
@ -130,7 +126,6 @@ p {
abbr[title], abbr[title],
abbr[data-bs-original-title] { abbr[data-bs-original-title] {
text-decoration: underline;
-webkit-text-decoration: underline dotted; -webkit-text-decoration: underline dotted;
text-decoration: underline dotted; text-decoration: underline dotted;
cursor: help; cursor: help;
@ -336,6 +331,9 @@ select {
select { select {
word-wrap: normal; word-wrap: normal;
} }
select:disabled {
opacity: 1;
}
[list]::-webkit-calendar-picker-indicator { [list]::-webkit-calendar-picker-indicator {
display: none; display: none;
@ -2182,10 +2180,6 @@ progress {
.form-control::-webkit-date-and-time-value { .form-control::-webkit-date-and-time-value {
height: 1.5em; height: 1.5em;
} }
.form-control::-webkit-input-placeholder {
color: #6c757d;
opacity: 1;
}
.form-control::-moz-placeholder { .form-control::-moz-placeholder {
color: #6c757d; color: #6c757d;
opacity: 1; opacity: 1;
@ -2357,7 +2351,6 @@ textarea.form-control-lg {
background-image: none; background-image: none;
} }
.form-select:disabled { .form-select:disabled {
color: #6c757d;
background-color: #e9ecef; background-color: #e9ecef;
} }
.form-select:-moz-focusring { .form-select:-moz-focusring {
@ -2598,9 +2591,6 @@ textarea.form-control-lg {
transition: none; transition: none;
} }
} }
.form-floating > .form-control::-webkit-input-placeholder {
color: transparent;
}
.form-floating > .form-control::-moz-placeholder { .form-floating > .form-control::-moz-placeholder {
color: transparent; color: transparent;
} }
@ -2794,6 +2784,12 @@ textarea.form-control-lg {
margin-right: 0.5em; 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 { .invalid-feedback {
display: none; display: none;
width: 100%; width: 100%;
@ -2870,6 +2866,12 @@ textarea.form-control-lg {
margin-right: 0.5em; 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 { .btn {
display: inline-block; display: inline-block;
font-weight: 400; font-weight: 400;
@ -3544,11 +3546,9 @@ textarea.form-control-lg {
left: auto ; left: auto ;
} }
} }
.dropup .dropdown-menu { .dropup .dropdown-menu[data-bs-popper] {
top: auto; top: auto;
bottom: 100%; bottom: 100%;
}
.dropup .dropdown-menu[data-bs-popper] {
margin-top: 0; margin-top: 0;
margin-bottom: 0.125rem; margin-bottom: 0.125rem;
} }
@ -3885,6 +3885,11 @@ textarea.form-control-lg {
text-align: center; text-align: center;
} }
.nav-fill .nav-item .nav-link,
.nav-justified .nav-item .nav-link {
width: 100%;
}
.tab-content > .tab-pane { .tab-content > .tab-pane {
display: none; display: none;
} }
@ -4266,7 +4271,7 @@ textarea.form-control-lg {
text-decoration: none; text-decoration: none;
} }
.card-link + .card-link { .card-link + .card-link {
margin-left: 1rem ; margin-right: 1rem;
} }
.card-header { .card-header {
@ -4379,8 +4384,8 @@ textarea.form-control-lg {
font-size: 1rem; font-size: 1rem;
color: #212529; color: #212529;
text-align: right; text-align: right;
background-color: transparent; background-color: #fff;
border: 1px solid rgba(0, 0, 0, 0.125); border: 0;
border-radius: 0; border-radius: 0;
overflow-anchor: none; 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; 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;
@ -4390,12 +4395,10 @@ textarea.form-control-lg {
transition: none; transition: none;
} }
} }
.accordion-button.collapsed {
border-bottom-width: 0;
}
.accordion-button:not(.collapsed) { .accordion-button:not(.collapsed) {
color: #0c63e4; color: #0c63e4;
background-color: #e7f1ff; background-color: #e7f1ff;
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.125);
} }
.accordion-button:not(.collapsed)::after { .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"); 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");
@ -4431,47 +4434,53 @@ textarea.form-control-lg {
margin-bottom: 0; margin-bottom: 0;
} }
.accordion-item:first-of-type .accordion-button { .accordion-item {
margin-bottom: -1px;
background-color: #fff;
border: 1px solid rgba(0, 0, 0, 0.125);
}
.accordion-item:first-of-type {
border-top-right-radius: 0.25rem; border-top-right-radius: 0.25rem;
border-top-left-radius: 0.25rem; border-top-left-radius: 0.25rem;
} }
.accordion-item:last-of-type .accordion-button.collapsed { .accordion-item:first-of-type .accordion-button {
border-bottom-width: 1px; border-top-right-radius: calc(0.25rem - 1px);
border-top-left-radius: calc(0.25rem - 1px);
}
.accordion-item:last-of-type {
margin-bottom: 0;
border-bottom-left-radius: 0.25rem; border-bottom-left-radius: 0.25rem;
border-bottom-right-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 { .accordion-item:last-of-type .accordion-collapse {
border-bottom-width: 1px;
border-bottom-left-radius: 0.25rem; border-bottom-left-radius: 0.25rem;
border-bottom-right-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 { .accordion-body {
padding: 1rem 1.25rem; padding: 1rem 1.25rem;
} }
.accordion-flush .accordion-button { .accordion-flush .accordion-collapse {
border-width: 0;
}
.accordion-flush .accordion-item {
border-left: 0; border-left: 0;
border-right: 0; border-right: 0;
border-radius: 0; border-radius: 0;
} }
.accordion-flush .accordion-collapse { .accordion-flush .accordion-item:first-child {
border-width: 0; border-top: 0;
} }
.accordion-flush .accordion-item:first-of-type .accordion-button { .accordion-flush .accordion-item:last-child {
border-top-width: 0; border-bottom: 0;
border-top-right-radius: 0;
border-top-left-radius: 0;
} }
.accordion-flush .accordion-item:last-of-type .accordion-button.collapsed { .accordion-flush .accordion-item .accordion-button {
border-bottom-width: 0; border-radius: 0;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
} }
.breadcrumb { .breadcrumb {
@ -4765,6 +4774,15 @@ textarea.form-control-lg {
border-radius: 0.25rem; 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 { .list-group-item-action {
width: 100%; width: 100%;
color: #495057; color: #495057;
@ -4785,6 +4803,7 @@ textarea.form-control-lg {
position: relative; position: relative;
display: block; display: block;
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
color: #212529;
text-decoration: none; text-decoration: none;
background-color: #fff; background-color: #fff;
border: 1px solid rgba(0, 0, 0, 0.125); border: 1px solid rgba(0, 0, 0, 0.125);
@ -5176,7 +5195,7 @@ textarea.form-control-lg {
position: fixed; position: fixed;
top: 0; top: 0;
right: 0; right: 0;
z-index: 1050; z-index: 1060;
display: none; display: none;
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -5240,7 +5259,7 @@ textarea.form-control-lg {
position: fixed; position: fixed;
top: 0; top: 0;
right: 0; right: 0;
z-index: 1040; z-index: 1050;
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
background-color: #000; background-color: #000;
@ -5463,7 +5482,7 @@ textarea.form-control-lg {
} }
.tooltip { .tooltip {
position: absolute; position: absolute;
z-index: 1070; z-index: 1080;
display: block; display: block;
margin: 0; margin: 0;
font-family: var(--bs-font-sans-serif); font-family: var(--bs-font-sans-serif);
@ -5565,7 +5584,7 @@ textarea.form-control-lg {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0 ; left: 0 ;
z-index: 1060; z-index: 1070;
display: block; display: block;
max-width: 276px; max-width: 276px;
font-family: var(--bs-font-sans-serif); font-family: var(--bs-font-sans-serif);
@ -5957,6 +5976,86 @@ textarea.form-control-lg {
animation-duration: 1.5s; 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 { .clearfix::after {
display: block; display: block;
clear: both; clear: both;
@ -6409,10 +6508,6 @@ textarea.form-control-lg {
border-color: #fff !important; border-color: #fff !important;
} }
.border-0 {
border-width: 0 !important;
}
.border-1 { .border-1 {
border-width: 1px !important; border-width: 1px !important;
} }
@ -7083,6 +7178,10 @@ textarea.form-control-lg {
padding-right: 3rem !important; padding-right: 3rem !important;
} }
.font-monospace {
font-family: var(--bs-font-monospace) !important;
}
.fs-1 { .fs-1 {
font-size: calc(1.375rem + 1.5vw) !important; font-size: calc(1.375rem + 1.5vw) !important;
} }
@ -7135,16 +7234,20 @@ textarea.form-control-lg {
font-weight: bolder !important; font-weight: bolder !important;
} }
.text-lowercase { .lh-1 {
text-transform: lowercase !important; line-height: 1 !important;
} }
.text-uppercase { .lh-sm {
text-transform: uppercase !important; line-height: 1.25 !important;
} }
.text-capitalize { .lh-base {
text-transform: capitalize !important; line-height: 1.5 !important;
}
.lh-lg {
line-height: 2 !important;
} }
.text-start { .text-start {
@ -7159,6 +7262,37 @@ textarea.form-control-lg {
text-align: center !important; 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 { .text-primary {
color: #0d6efd !important; color: #0d6efd !important;
} }
@ -7215,22 +7349,6 @@ textarea.form-control-lg {
color: inherit !important; 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 { .bg-primary {
background-color: #0d6efd !important; background-color: #0d6efd !important;
} }
@ -7279,29 +7397,6 @@ textarea.form-control-lg {
background-image: var(--bs-gradient) !important; 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 { .user-select-all {
-webkit-user-select: all !important; -webkit-user-select: all !important;
-moz-user-select: all !important; -moz-user-select: all !important;

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

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

@ -0,0 +1,86 @@
/*
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,7 +2,26 @@ function stashrToast(message, type) {
Vue.$toast.open({ Vue.$toast.open({
message: message, message: message,
type: type, type: type,
position: 'bottom-left', position: 'bottom-right',
dismissable: true 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,6 +16,16 @@
"start_url": "/", "start_url": "/",
"background_color": "#3367D6", "background_color": "#3367D6",
"display": "fullscreen", "display": "fullscreen",
"orientation": "portrait",
"scope": "/", "scope": "/",
"theme_color": "#3367D6" "theme_color": "#3367D6",
"shortcuts": [
{
"name": "All Volumes",
"short_name": "Volumes",
"description": "All Volumes",
"url": "/volumes",
"icons": [{ "src": "/static/assets/stashr-192.png", "sizes": "192x192"}]
}
]
} }

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

@ -30,8 +30,13 @@ Vue.component('collections', {
props: ['collections'], props: ['collections'],
template: ` template: `
<div> <div>
<div class="mb-3 px-5"> <div class="d-grid w-100 bg-mine m-0 p-0 sticky-top shadow">
<input type="text" v-model="search" class="form-control" placeholder="Search Collections..." /> <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> </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 py-2 justify-content-center">
<collection <collection
@ -40,6 +45,15 @@ Vue.component('collections', {
v-bind:key="collection.collection_id" v-bind:key="collection.collection_id"
></collection> ></collection>
</ul> </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> </div>
`, `,
data() { return { search: '', } }, data() { return { search: '', } },
@ -56,18 +70,18 @@ Vue.component('collections', {
Vue.component('collection', { Vue.component('collection', {
props: ['collection'], props: ['collection'],
template: ` template: `
<li class='stashr-cover_size m-2' <li class='stashr-item_container m-2'
@mouseover="hover = true" @mouseover="hover = true"
@mouseleave="hover = false" @mouseleave="hover = false"
> >
<div class='stashr-poster_wrapper rounded'> <div class="stashr-poster_container border border-dark rounded-3">
<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"> <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'" /> <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 ]]
</a> </a>
</div> </div>
</div> </div>

@ -47,9 +47,11 @@ Vue.component('publishers', {
props: ['publishers'], props: ['publishers'],
template: ` template: `
<div> <div>
<div class="mb-3 px-5"> <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..." /> <input type="text" v-model="search" class="form-control" placeholder="Search Publishers..." />
</div> </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 py-2 justify-content-center">
<publisher <publisher
v-for="publisher in filteredList" v-for="publisher in filteredList"
@ -77,18 +79,18 @@ Vue.component('publishers', {
Vue.component('publisher',{ Vue.component('publisher',{
props: ['publisher'], props: ['publisher'],
template: ` template: `
<li class='stashr-cover_size m-2' <li class='stashr-item_container m-2'
@mouseover="hover = true" @mouseover="hover = true"
@mouseleave="hover = false" @mouseleave="hover = false"
> >
<div class='stashr-poster_wrapper rounded'> <div class="stashr-poster_container border border-dark rounded-3">
<div class="stashr-poster_container border rounded"> <a :href="'{{ url_for('single_publisher_page', publisher_id='PUBLISHERID') }}'.replace('PUBLISHERID', publisher.publisher_id)">
<div class="stashr-overlay_bottom w-100 center" v-if="hover"> <img class="stashr-background w-100" loading="eager" src="/static/assets/cover.svg" />
<a href="#">[[ publisher.publisher_name ]]</a> <img class="stashr-poster w-100" loading="lazy" v-bind:src=publisher.publisher_image />
</div> </a>
<img class="stashr-poster_background w-100" loading="eager" src="/static/assets/cover.svg" /> <div class="stashr-overlay_bottom w-100 text-center shadow" v-if="hover">
<a class="stashr-poster_link align-middle" :href="'{{ url_for('single_publisher_page', publisher_id='PUBLISHERID') }}'.replace('PUBLISHERID', publisher.publisher_id)"> <a class="stashr-link" :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 /> [[ publisher.publisher_name ]]
</a> </a>
</div> </div>
</div> </div>

@ -6,6 +6,7 @@
{% block header %} {% block header %}
{{ emit_tep('all_volumes_page_header') }} {{ emit_tep('all_volumes_page_header') }}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
@ -28,7 +29,7 @@
</div> </div>
<div id="app"> <div id="app">
<volumes v-bind:volumes='volumesList'></volumes> <volumes v-bind:volumes='volumesList' v-bind:publishers="publishers" v-bind:ratings="ratings"></volumes>
</div> </div>
{% endblock %} {% endblock %}
@ -48,43 +49,76 @@
{% block script %} {% 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', { Vue.component('volume-item', {
props: ['volume'], props: ['volume'],
template: ` template: `
<li class='stashr-cover_size m-2' <li class='stashr-item_container m-2'
@mouseover="hover = true" @mouseover="hover = true"
@mouseleave="hover = false" @mouseleave="hover = false"
> >
<div class='stashr-poster_wrapper rounded'> <div class="stashr-poster_container border border-dark rounded-3">
<div class="stashr-badge_tl badge rounded-pill bg-info border">[[ volume.age_rating[0].rating_short ]]</div> <div :class="volumeTag"></div>
<div class="stashr-badge_tr badge rounded-pill bg-primary border">[[ volume.volume_have ]]/[[ volume.volume_total ]]</div> <div class="stashr-poster_info bg-info text-white px-1">
<div class="stashr-badge_br badge rounded-pill border" :class="statusClass">[[ statusWord ]]</div> [[ volume.age_rating[0].rating_short ]]
<div class="stashr-poster_container border rounded"> </div>
<div class="stashr-overlay_bottom w-100 center" v-if="hover"> <div class="stashr-poster_wrapper">
<a :href="'/volumes/'+volume.volume_slug">[[ volume.volume_name ]]</a> <a :href="'/volumes/'+volume.volume_slug">
</div> <img class="stashr-background w-100" loading="eager" src="/static/assets/cover.svg" />
<img class="stashr-poster_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 class="stashr-poster_link" :href="'/volumes/'+volume.volume_slug"> </a>
<img class="w-100" loading="lazy" v-bind:src="'/images/volumes/'+volume.volume_id+'.jpg'" @error="$event.target.src=volume.volume_image_med"/> </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> </a>
</div> </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>
</div>
</div> </div>
</li> </li>
`, `,
computed: { computed: {
statusClass() { volumeTag() {
let classname = 'bg-danger'; let cornertag = '';
if(this.volume.volume_status) { if(!this.volume.volume_status) {
classname = 'bg-success'; cornertag = "stashr-poster_tag bg-danger shadow";
}; }
return classname; return cornertag;
}, },
statusWord() { progressWidth() {
let status = 'ENDED'; return (this.volume.volume_have / this.volume.volume_total)*100;
},
progressColor() {
let classname = 'bg-success';
if(this.volume.volume_status) { if(this.volume.volume_status) {
status = 'ONGOING'; classname = 'bg-info';
}; }
return status; if(this.progressWidth < 100) {
classname = 'bg-danger';
}
return classname
},
progressStyle() {
return "width: " + this.progressWidth + "%;"
} }
}, },
data() { return { hover: false } }, data() { return { hover: false } },
@ -92,11 +126,16 @@ Vue.component('volume-item', {
}) })
Vue.component('volumes', { Vue.component('volumes', {
props: ['volumes'], props: ['volumes', 'ratings', 'publishers'],
template: ` template: `
<div> <div>
<div class="mb-3 px-5"> <div class="d-grid w-100 bg-mine m-0 p-0 sticky-top shadow">
<input type="text" v-model="search" class="form-control" placeholder="Search Volumes..." /> <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> </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 py-2 justify-content-center">
<volume-item <volume-item
@ -105,17 +144,91 @@ Vue.component('volumes', {
v-bind:key="volume.volume_id" v-bind:key="volume.volume_id"
></volume-item> ></volume-item>
</ul> </ul>
<!-- <div class="offcanvas offcanvas-end" tabindex="-1" id="offcanvasRight" aria-labelledby="offcanvasRightLabel">
<i class="text-primary fas fa-spinner fa-spin fa-3x" v-if="loading"></i> <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>
</div> </div>
`, `,
data() { return { loading: true, search: '', } }, data() { return { loading: true, search: '', filter: '', sorted: 'volume_sort_title', rating: '', publisher: ''} },
computed: { computed: {
filteredList() { filteredList() {
return this.volumes.filter(volume => { if (this.sorted.toLowerCase() == 'progress') {
return volume.volume_name.toLowerCase().includes(this.search.toLowerCase()) 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])
}
}, },
}, },
delimiters: ["[[","]]"], delimiters: ["[[","]]"],
@ -125,11 +238,39 @@ var app = new Vue({
el: '#app', el: '#app',
data: { data: {
volumesList: [], volumesList: [],
ratings: [],
publishers: [],
}, },
created() { created() {
this.getVolumes() this.getVolumes();
this.getPublishers();
this.getRatings();
}, },
methods:{ 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() { getVolumes() {
axios.get('{{ url_for('api.api_get_all_volumes') }}', { axios.get('{{ url_for('api.api_get_all_volumes') }}', {
params: { params: {

@ -1,14 +1,13 @@
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, shrink-to-fit=no">
<title>Stashr - {{ title }}</title> <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/bootstrap.css') }}">
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/stashr.css') }}"> <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/stashr.css') }}?v=0.1.0">
<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') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/all.css') }}">
@ -26,162 +25,131 @@
<meta name="apple-mobile-web-app-status-bar-style" content="default"> <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/stashr.js') }}"></script>
<!-- VueJS -->
<script src="{{ url_for('static', filename='js/vue.dev.js') }}"></script> <script src="{{ url_for('static', filename='js/vue.dev.js') }}"></script>
<!-- VueJS Toasts -->
<script src="https://cdn.jsdelivr.net/npm/vue-toast-notification"></script>
<!-- START HEADER SCRIPT INCLUDES --> <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 %} {% block header_script_files %}{% endblock %}
{{ emit_tep('base_page_header_script_files') }} {{ emit_tep('base_page_header_script_files') }}
<!-- END HEADER SCRIPT INCLUDES -->
</head> </head>
<body> <body>
<!-- START NAVBAR --> <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;">
<nav class="navbar navbar-expand-lg navbar-dark bg-primary sticky-top"> <div class="d-flex flex-row">
<div class="container-fluid">
<a class="navbar-brand" href="{{ url_for('index_page') }}"> <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 class="border rounded-circle" src="{{ url_for('static', filename='assets/stashr.svg') }}" width="40" height="40" />
<span class="stashr-project_title">Stashr</span> <img src="{{ url_for('static', filename='assets/title.svg') }}" height="25" />
</a> </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"> </div>
<span class="navbar-toggler-icon"></span> <hr/>
</button> <ul class="nav nav-pills flex-column mb-auto text-start">
<li>
{% if not current_user.is_authenticated %} <a data-bs-toggle="collapse" data-bs-target="#collapseLibrary" class="nav-link text-white collapsed">
{% if not request.endpoint == 'first_run_page' %} <i class="fas fa-chevron-right"></i>
<div class="collapse navbar-collapse" id="navbarStashr"> Library
<ul class="navbar-nav ms-auto"> </a>
<li class="nav-item"> <div class="collapse{% if (request.path == url_for('all_volumes_page')) or
<a class="nav-link" href="{{ url_for('login_page') }}"> (request.path == url_for('all_publishers_page')) or
LOGIN (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> </a>
</li> </li>
{% if open_registration %} {% if current_user.role == 'admin' %}
<li class="nav-item"> <li>
<a class="nav-link" href="{{ url_for('register_page') }}"> <a href="{{ url_for('scrape_folders_page') }}" class="nav-link text-white{% if request.path == url_for('scrape_folders_page') %} active{% endif %}">
REGISTER Scrape
</a> </a>
</li> </li>
{% endif %} {% endif %}
</ul> </ul>
</div> </div>
{% 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 class="dropdown-item" href="{{ url_for('scrape_folders_page') }}">SCRAPE</a></li>
{% endif %}
</ul>
</li> </li>
{% if current_user.role == 'admin' or current_user.role == 'librarian' %} {% if current_user.role == 'admin' or current_user.role == 'librarian' %}
<li class="nav-item"> <li>
<a class="nav-link{% if request.path == url_for('new_releases_page') %} active{% endif %}" href="{{ url_for('new_releases_page') }}"> <a href="{{ url_for('new_releases_page') }}" class="nav-link text-white{% if request.path == url_for('new_releases_page') %} active{% endif %}">
NEW RELEASES New Releases
</a> </a>
</li> </li>
{% endif %} {% endif %}
{% if current_user.role == 'admin' or current_user.role == 'librarian' or current_user.role == 'reader' %} {% if current_user.role == 'admin' or current_user.role == 'librarian' or current_user.role == 'reader' %}
<li class="nav-item"> <li>
<a class="nav-link{% if request.path == url_for('reading_list_page') %} active{% endif %}" href="{{ url_for('reading_list_page') }}"> <a href="{{ url_for('reading_list_page') }}" class="nav-link text-white{% if request.path == url_for('reading_list_page') %} active{% endif %}">
READING LIST Reading List
</a> </a>
</li> </li>
<li class="nav-item"> <li>
<a class="nav-link{% if request.path == url_for('all_collections_page') %} active{% endif %}" href="{{ url_for('all_collections_page') }}"> <a href="{{ url_for('all_collections_page') }}" class="nav-link text-white{% if request.path == url_for('all_collections_page') %} active{% endif %}">
COLLECTIONS Collections
</a> </a>
</li> </li>
{% endif %} {% endif %}
{{ emit_tep("base_page_main_menu") }} {{ emit_tep("base_page_main_menu") }}
</ul> </ul>
<ul class="navbar-nav ms-auto"> <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' %} {% if current_user.role == 'admin' %}
<li class="nav-item"> <li>
<a class="nav-link" href="{{ url_for('settings_page') }}"> <a href="{{ url_for('settings_page') }}" class="nav-link text-white">
<i class="fa fa-cogs"></i> <i class="fas fa-cogs"></i>
Settings Settings
</a> </a>
</li> </li>
{% endif %} {% endif %}
<li class="nav-item dropdown"> <li>
<a class="nav-link dropdown-toggle" href="#" id="libraryDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false"> <a href="{{ url_for('logout_page') }}" class="nav-link text-white">
<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> <i class="fas fa-sign-out-alt"></i>
Logout Logout
</a></li> </a>
</ul>
</li> </li>
</ul> </ul>
</div> </div>
{% endif %} <div class="d-flex flex-column w-100 m-0 px-0 min-vh-100 overflow-auto">
</div>
</nav>
<!-- END NAVBAR -->
<!-- START HEADER --> <!-- v CONTENT v -->
<div class="py-2" id="stashr_header"> {% block content %}{% endblock %}
{% block header %}{% endblock %} <!-- ^ CONTENT ^ -->
</div>
<!-- END HEADER -->
<!-- START CONTENT --> <!-- v BUTTON CONTAINER v -->
<div class="py-2" id="stashr_content"> <div>
{% block content %}{% endblock %} <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>
</div>
<!-- ^ BUTTON CONTAINER ^ -->
</div>
</div> </div>
<!-- END CONTENT -->
<!-- START MODALS --> <!-- v MODALS v -->
{% block modals %}{% endblock %} {% block modals %}{% endblock %}
{{ emit_tep('base_page_modals') }} {{ emit_tep('base_page_modals') }}
<!-- END MODALS --> <!-- ^ MODALS ^ -->
<!-- START BUTTON CONTAINER --> <!-- v FOOTER SCRIPT INCLUDES v -->
<div class="stashr-button_container p-2">
{% block button_container %}{% endblock %}
</div>
<!-- END BUTTON CONTAINER -->
<!-- START FOOTER SCRIPT INCLUDES -->
<script src="{{ url_for('static', filename='js/bootstrap.bundle.js') }}"></script> <script src="{{ url_for('static', filename='js/bootstrap.bundle.js') }}"></script>
{% block footer_script_files %}{% endblock %} {% block footer_script_files %}{% endblock %}
{{ emit_tep('base_page_footer_script_files') }} {{ emit_tep('base_page_footer_script_files') }}
<!-- END FOOTER SCRIPT INCLUDES --> <!-- ^ FOOTER SCRIPT INCLUDES ^ -->
<!-- START FOOTER SCRIPT --> <!-- v FOOTER SCRIPT v -->
<script type="text/javascript"> <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); Vue.use(VueToast);
// Flashes // Flashes
@ -193,11 +161,18 @@ Vue.use(VueToast);
{% endif %} {% endif %}
{% endwith %} {% endwith %}
/*
window.addEventListener("load", () => {
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("service-worker.js");
}
});
*/
{% block script %}{% endblock %} {% block script %}{% endblock %}
{{ emit_tep('base_page_script') }}
</script> </script>
<!-- END FOOTER SCRIPT --> <!-- ^ FOOTER SCRIPT ^ -->
</body> </body>
</html> </html>

@ -1,52 +1,92 @@
{% extends "base.html" %} <html>
<head>
{% block content %} <title>Stashr - {{ title }}</title>
<div class="d-flex justify-content-center flex-wrap w-80">
<div class='position-relative my-2'> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, shrink-to-fit=no">
<img class="border rounded-circle my-3" src="{{ url_for('static', filename='assets/stashr.svg') }}" width="200" height="200" />
</div> <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/bootstrap.css') }}">
<div class="bg-light m-2 px-2 rounded stashr-series_info text-center text-lg-start py-3 px-5"> <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/stashr.css') }}">
<h5 class="text-center">First Run</h5>
<hr /> <link href="https://cdn.jsdelivr.net/npm/vue-toast-notification/dist/theme-sugar.css" rel="stylesheet">
<form method="POST">
<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">
{{ first_run_form.csrf_token }} {{ first_run_form.csrf_token }}
<img class="stashr-logo" src="{{ url_for('static', filename='assets/stashr.svg') }}" w="170" h="170">
<div class="mb-3"> {% if first_run_form.errors %}
{{ first_run_form.username.label }} <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) }} {{ first_run_form.username(class_='form-control', placeholder=first_run_form.username.label.text) }}
{{ first_run_form.username.label }}
</div> </div>
<div class="mb-3"> <div class="form-floating">
{{ first_run_form.email.label }}
{{ first_run_form.email(class_='form-control', placeholder=first_run_form.email.label.text) }} {{ first_run_form.email(class_='form-control', placeholder=first_run_form.email.label.text) }}
{{ first_run_form.email.label }}
</div> </div>
<div class="mb-3"> <div class="form-floating">
{{ first_run_form.password.label }}
{{ first_run_form.password(type='password', class_='form-control', placeholder=first_run_form.password.label.text) }} {{ first_run_form.password(type='password', class_='form-control', placeholder=first_run_form.password.label.text) }}
{{ first_run_form.password.label }}
</div> </div>
<div class="mb-3"> <div class="form-floating">
{{ first_run_form.confirm_password.label }}
{{ first_run_form.confirm_password(type='password', class_='form-control', placeholder=first_run_form.confirm_password.label.text) }} {{ first_run_form.confirm_password(type='password', class_='form-control', placeholder=first_run_form.confirm_password.label.text) }}
{{ first_run_form.confirm_password.label }}
</div> </div>
<hr /> <div class="form-floating">
<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) }} {{ first_run_form.comicvine_api_key(class_='form-control', placeholder=first_run_form.comicvine_api_key.label.text) }}
{{ first_run_form.comicvine_api_key.label }}
</div> </div>
<hr /> <div class="form-floating">
<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') }} {{ first_run_form.logging_level(class_='form-control') }}
{{ first_run_form.logging_level.label }}
</div> </div>
<hr /> <hr />
<div class="mb-3 text-end">
{{ first_run_form.first_run_button(class_='btn btn-success') }} {{ first_run_form.first_run_button(class_='btn btn-success') }}
</div>
</form> </form>
</div> </div>
</div> </main>
{% endblock %}
<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>

@ -1,15 +1,37 @@
{% extends "base.html" %} <html>
<head>
{% block content %} <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">
<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 %} {% if login_form.errors %}
<div class="notification bg-danger rounded m-3 text-white" role="alert" id='error_container'> <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 %} {% for field_name, field_errors in login_form.errors|dictsort if field_errors %}
@ -20,24 +42,41 @@
</ul> </ul>
{% endfor %} {% endfor %}
</div> </div>
{% else %}
{% endif %} {% endif %}
{{ login_form.csrf_token }} <div class="form-floating">
<div class="mb-3"> {{ login_form.username(class_='input form-control', type='username', placeholder='Username') }}
{{ login_form.username(class_='input form-control', placeholder='Username') }} {{ login_form.username.label }}
</div> </div>
<div class="mb-3"> <div class="form-floating">
{{ login_form.password(class_='input form-control', type='password', placeholder='Password') }} {{ login_form.password(class_='input form-control', type='password', placeholder='Password') }}
{{ login_form.password.label }}
</div> </div>
<div class="mb-3">
<div class="checkbox mb-3">
<label class="text-white">
{{ login_form.remember_me() }} Remember Me {{ login_form.remember_me() }} Remember Me
</label>
</div> </div>
<div class="mb-3">
{{ login_form.login_button(class_='btn btn-outline-success') }} {{ login_form.login_button(class_='w-100 btn btn-lg btn-success shadow') }}
<a href="{{ url_for('forgot_page') }}" class="btn btn-outline-danger">Forgot Password</a> <a class="text-white" href="#">Forgot your password?</a>
</div>
</form> </form>
</div> </div>
</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>
{% endblock %} </body>
</html>

@ -73,33 +73,34 @@ Vue.component('modals', {
Vue.component('release-item', { Vue.component('release-item', {
props: ['release'], props: ['release'],
template: ` template: `
<li class='stashr-cover_size m-2' <li class='stashr-item_container m-2'
@mouseover="hover = true" @mouseover="hover = true"
@mouseleave="hover = false" @mouseleave="hover = false"
> >
<div class='stashr-poster_wrapper rounded'> <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">
<div class="stashr-badge_tl badge rounded-pill bg-info border">#[[ release.new_release_issue_number ]]</div> #[[ release.new_release_issue_number ]]
<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> </div>
<img class="stashr-poster_background w-100" loading="eager" src="/static/assets/cover.svg" /> <div class="stashr-badge_bottom_right bg-dark">
<a class="stashr-poster_link" data-bs-toggle="modal" data-bs-target="#modalSubscription" v-on:click="this.changeModal"> <span class="fa-stack p-0">
<img class="w-100" loading="lazy" :src="release.new_release_image_url" /> <i class="fas fa-university fa-stack-1x" :class="className"></i>
</a> <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> </div>
</div> </div>
</li> </li>
`, `,
computed: { computed: {
className() { className() {
let classname = 'bg-danger'; let classname = 'text-danger';
try { try {
if (this.release.status) { if (this.release.status) {
classname = 'bg-success'; classname = 'text-success';
} }
} finally { } finally {
return classname; return classname;
@ -152,8 +153,13 @@ Vue.component('releases', {
props: ['releases'], props: ['releases'],
template: ` template: `
<div> <div>
<div class="mb-3 px-5"> <div class="d-grid w-100 bg-mine m-0 p-0 sticky-top shadow">
<input type="text" v-model="search" class="form-control" placeholder="Search New Releases..." /> <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> </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 py-2 justify-content-center">
<release-item <release-item
@ -162,14 +168,62 @@ Vue.component('releases', {
v-bind:key="release.new_release_id" v-bind:key="release.new_release_id"
></release-item> ></release-item>
</ul> </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> </div>
`, `,
data() { return { search: '', } }, data() { return { search: '', sorted: 'new_release_comic_name', library: ''} },
computed: { computed: {
filteredList() { filteredList() {
return this.releases.filter(release => { if (this.library == null) {
return release.new_release_comic_name.toLowerCase().includes(this.search.toLowerCase()) 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])
}
}, },
}, },
delimiters: ["[[","]]"] delimiters: ["[[","]]"]

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

@ -53,7 +53,7 @@ Vue.component('modals', {
</h5> </h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body center"> <div class="modal-body text-center">
<a :href="'/read/'+issue.issue_id" id="readRead" class="btn btn-success"> <a :href="'/read/'+issue.issue_id" id="readRead" class="btn btn-success">
<i class="fas fa-book-open"></i> <i class="fas fa-book-open"></i>
Read Read
@ -99,14 +99,46 @@ Vue.component('modals', {
Vue.component('issues', { Vue.component('issues', {
props: ['issues'], props: ['issues'],
template: ` 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'> <ul class="d-flex flex-wrap w-100 m-0 p-0 justify-content-center" v-sortable="$options.sortOptions" @sorted='handleSorted'>
<issue <issue
v-for="issue in issues" v-for="issue in filteredList"
v-bind:issue="issue" v-bind:issue="issue"
v-bind:key="issue.issue_id" v-bind:key="issue.issue_id"
></issue> ></issue>
</ul> </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>
`, `,
data() { return { loading: true, search: '', } },
computed: {
filteredList() {
return this.issues.filter(issue => {
return issue.volume.volume_name.toLowerCase().includes(this.search.toLowerCase())
})
},
},
methods: { methods: {
handleSorted(event) { handleSorted(event) {
app.handleSorted(event) app.handleSorted(event)
@ -124,28 +156,28 @@ Vue.component('issues', {
Vue.component('issue', { Vue.component('issue', {
props: ['issue'], props: ['issue'],
template: ` template: `
<li class='stashr-cover_size m-2 js-sortable-block' <li class='stashr-item_container m-2 js-sortable-block'
@mouseover="hover = true" @mouseover="hover = true"
@mouseleave="hover = false" @mouseleave="hover = false"
> >
<div class='stashr-poster_wrapper rounded'> <div class="stashr-poster_container border border-dark rounded-3 js-drag-handle">
<div class="stashr-badge_tl badge rounded-pill bg-dark border js-drag-handle"> <div class="stashr-badge_top_left bg-dark">
<i class="fas fa-arrows-alt"></i> <span class="fa-stack p-0">
<i class="fas fa-arrows-alt fa-stack-1x text-white"></i>
</span>
</div> </div>
<div class="stashr-badge_br badge rounded-pill bg-dark border"> <div class="stashr-badge_bottom_right bg-dark">
<i class="fas fa-eye" v-bind:class="statusRead" v-on:click="toggleRead"></i> <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>
<!-- <div data-bs-toggle="modal" data-bs-target="#modalRead" v-on:click="this.changeModal">
<div class="stashr-badge_tr badge badge-pill badge-info border">[[ issue.reading_list_position ]]</div> <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 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> </div>
<img class="stashr-poster_background w-100" loading="eager" src="/static/assets/cover.svg" /> <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">
<a class="stashr-poster_link" data-bs-toggle="modal" data-bs-target="#modalRead" v-on:click="this.changeModal"> [[ issue.volume.volume_name ]] #[[ issue.issue_number ]]
<img class="w-100" loading="lazy" v-bind:src="'/images/issues/'+issue.issue_id+'.jpg'"/>
</a>
</div> </div>
</div> </div>
</li> </li>

@ -1,45 +0,0 @@
{% 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 %} {% block content %}
<div id="app"> <div id="app">
<modal ref="modal" v-bind:individual="individual"></modal> <modal ref="modal" v-bind:individual="individual" v-on:do-search="searchNewResults"></modal>
<directories v-bind:directories='directories'></directories> <directories v-bind:directories='directories'></directories>
</div> </div>
@ -59,15 +59,23 @@ Vue.component('modal', {
<div class="modal-body"> <div class="modal-body">
<div class="row"> <div class="row">
<div class="col-3"> <div class="col-3">
<div class="stashr-poster_container border rounded"> <div class="stashr-item_container border rounded">
<img class="stashr-poster_background w-100" loading="eager" src="/static/assets/folder.svg" />
<a class="stashr-poster_link"> <a class="stashr-poster_link">
<img class="w-100" :src="[[ match['image']['small_url'] ]]" loading="lazy"/> <img class="w-100" :src="[[ match['image']['small_url'] ]]" loading="lazy"/>
</a> </a>
</div> </div>
</div> </div>
<div class="col-9"> <div class="col-9">
<select id="selectVolume" class="form-select" aria-label="Default select example" @change='doSomething($event)' v-if='!this.individual.scrape_add'> <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 <candidate
v-for="item in json" v-for="item in json"
v-bind:candidate="item" v-bind:candidate="item"
@ -75,22 +83,21 @@ Vue.component('modal', {
v-bind:key="item['id']" v-bind:key="item['id']"
></candidate> ></candidate>
</select> </select>
</div>
<div class='py-2'> <div class='py-2'>
<h5>[[ match['name'] ]] ([[ match['start_year'] ]])</h5> <h5>[[ match['name'] ]] ([[ match['start_year'] ]])</h5>
<p v-html="match['description']"></p> <p v-html="match['description']"></p>
</div> </div>
</div> </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>
<div class="modal-footer"> <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> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div> </div>
</div> </div>
@ -118,11 +125,13 @@ Vue.component('modal', {
this.individual.scrape_candidate = something.target.value this.individual.scrape_candidate = something.target.value
}, },
setTest() { setTest() {
console.log('in here')
this.test = JSON.parse(this.individual.scrape_json).filter(item => { this.test = JSON.parse(this.individual.scrape_json).filter(item => {
return item['id'] == this.individual.scrape_candidate return item['id'] == this.individual.scrape_candidate
})[0] })[0]
document.getElementById('selectVolume').value = this.individual.scrape_candidate document.getElementById('selectVolume').value = this.individual.scrape_candidate
},
toggleDirectoryIgnore() {
app.toggleDirectoryIgnore();
} }
}, },
delimiters: ["[[","]]"] delimiters: ["[[","]]"]
@ -131,32 +140,22 @@ Vue.component('modal', {
Vue.component('directory', { Vue.component('directory', {
props: ['directory'], props: ['directory'],
template: ` template: `
<li class='stashr-cover_size m-2' <li class='stashr-item_container m-2'
@mouseover="hover = true" @mouseover="hover = true"
@mouseleave="hover = false" @mouseleave="hover = false"
> >
<div class='stashr-poster_wrapper rounded' v-on:click="changeModal()"> <div class="stashr-poster_container border border-dark rounded-3" v-on:click="this.changeModal">
<div class="stashr-badge_tl badge badge-pill badge-info border"></div> <div class="stashr-badge_top_right bg-dark" @click='changeChecked'>
<div class="stashr-badge_br badge badge-pill border"></div> <span class="fa-stack p-0">
<div class="stashr-poster_container border rounded"> <i class="fa-stack-1x" :class='checkedStatus'></i>
<div class="stashr-check_box p-1"> </span>
<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 class="stashr-overlay_bottom w-100 center">
<span>[[ directory.scrape_directory ]]</span>
</div> </div>
<div data-bs-toggle="modal" data-bs-target="#modalScrape"> <div data-bs-toggle="modal" data-bs-target="#modalScrape">
<img class="stashr-poster_background w-100" loading="eager" src="/static/assets/folder.svg" /> <img class="stashr-background w-100" loading="eager" src="/static/assets/folder.svg" />
<a class="stashr-poster_link"> <img class="stashr-poster w-100" loading="lazy" :src="imageURL" />
<img class="w-100" :src="imageURL" loading="lazy"/>
</a>
</div> </div>
<div class="stashr-overlay_bottom w-100 text-center shadow">
[[ directory.scrape_directory ]]
</div> </div>
</div> </div>
</li> </li>
@ -168,9 +167,9 @@ Vue.component('directory', {
})[0]['image']['small_url'] })[0]['image']['small_url']
}, },
checkedStatus() { checkedStatus() {
string = `text-info fa-2x fas fa-circle` string = `text-info fas fa-circle`
if(this.directory.scrape_add) { if(this.directory.scrape_add) {
string = `text-success fa-2x fas fa-check-circle` string = `text-success fas fa-check-circle`
} }
return string return string
}, },
@ -204,24 +203,63 @@ Vue.component('directories', {
props: ['directories'], props: ['directories'],
template: ` template: `
<div> <div>
<div class="mb-3 px-5"> <div class="d-grid w-100 bg-mine m-0 p-0 sticky-top shadow">
<input type="text" v-model="search" class="form-control" placeholder="Search Folders..." /> <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> </div>
<ul class="d-flex flex-wrap w-100 m-0 p-0 justify-content-center"> <ul class="d-flex flex-wrap w-100 m-0 p-0 justify-content-center">
<directory <directory
v-for="directory in filteredList" v-for="directory in filteredList"
v-bind:directory="directory" v-bind:directory="directory"
v-bind:key="directory.scrape_id" v-bind:key="directory.scrape_id"
ref="filteredList"
></directory> ></directory>
</ul> </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> </div>
`, `,
data() { return { search: '', } }, data() { return { search: '', ignore: 'false', checked: false} },
computed: { computed: {
filteredList() { filteredList() {
return this.directories.filter(directory => { if (!this.checked) {
return directory.scrape_directory.toLowerCase().includes(this.search.toLowerCase()) 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()) })
}
}, },
}, },
methods: {}, methods: {},
@ -281,6 +319,56 @@ 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: ["[","]]"] delimiters: ["[","]]"]
}) })

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

@ -1,61 +1,176 @@
{% extends "base.html" %} <html>
<head>
{% block content %} <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">
<div class="row w-100 m-0"> <link rel="stylesheet" href="{{ url_for('static', filename='css/all.css') }}">
<div class="col-12 col-md-10 offset-md-1 bg-light rounded p-2">
<div class="row"> <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' %} {% if current_user.role == 'admin' %}
<div class="col-12 col-md-3 col-lg-2 p-3"> <li>
<ul class="nav flex-column nav-pills"> <a class="nav-link text-white{% if request.path == url_for('settings_app_page') %} active{% endif %}" href="{{ url_for('settings_app_page') }}">
<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> <i class="fa fa-cogs"></i>
Application Application
</a> </a>
</li> </li>
<li class="nav-item"> <li>
<a class="nav-link{% if request.path == url_for('settings_directories_page') %} active{% endif %}" href="{{ url_for('settings_directories_page') }}"> <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> <i class="fas fa-folder"></i>
Directories Directories
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link{% if request.path == url_for('settings_mail_page') %} active{% endif %}" href="{{ url_for('settings_mail_page') }}"> <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> <i class="fa fa-envelope"></i>
Mail Mail
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link{% if request.path == url_for('settings_tasks_page') %} active{% endif %}" href="{{ url_for('settings_tasks_page') }}"> <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> <i class="fa fa-tasks"></i>
Tasks Tasks
</a> </a>
</li> </li>
<li class="nav-item"> <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') }}"> <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> <i class="fa fa-users"></i>
Users Users
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link{% if request.path == url_for('settings_plugins_page') %} active{% endif %}" href="{{ url_for('settings_plugins_page') }}"> <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> <i class="fa fa-plug"></i>
Plugins Plugins
</a> </a>
</li> </li>
{{ emit_tep('settings_menu') }} {{ emit_tep('settings_menu') }}
{% endif %}
</ul> </ul>
</div> <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 %} {% endif %}
<div class="col-12{% if current_user.role=='admin' %} col-md-9 col-lg-10{% endif %} my-2"> <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 %} {% 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> </div>
</div> </div>
</div> </div>
<!-- ^ BUTTON CONTAINER ^ -->
</div>
</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 ^ -->
{% endblock %} </body>
</html>

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

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

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

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

@ -2,16 +2,18 @@
{% block settings_pane %} {% block settings_pane %}
<div id="app"> <div class="d-grid w-100 bg-mine m-0 p-0 sticky-top shadow">
<div class="row r-10 m-2"> <div class="row w-100 m-0 p-3">
<div class="col col-12"> <div class="col-sm-12 col-md-6 text-center text-md-start text-white ">
<div class="row">
<div class="col-sm-12 col-md-6 text-center text-md-start">
<h2>New User</h2> <h2>New User</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>
</div> </div>
<hr /> </div>
<div class="row"> </div>
<div class="d-grid w-100 m-0 p-0">
<div class="row w-100 m-0 p-3">
<form method="POST"> <form method="POST">
{{ new_user_form.csrf_token }} {{ new_user_form.csrf_token }}
@ -46,12 +48,8 @@
</div> </div>
</form> </form>
</div> </div>
</div>
</div>
</div> </div>
{% endblock %} {% endblock %}
{% block script %} {% block script %}

@ -8,29 +8,85 @@
<div id="app"> <div id="app">
<plugins ref="plugins" v-bind:plugins="plugins"></plugins> <plugins ref="plugins" v-bind:plugins="plugins"></plugins>
<modals v-bind:plugin="plugin"></modals>
</div> </div>
{% endblock %} {% endblock %}
{% block script %} {% block script %}
Vue.component('plugin',{ Vue.component('modals', {
props:['plugin'], props: ['plugin'],
template: ` template: `
<tr> <div>
<th scope="row">[[ plugin.plugin_name ]]</th> <div class="modal" id="modalUpload" tabindex="-1" role="dialog" aria-labelledby="notesModal" aria-hidden="true">
<td>[[ plugin.plugin_description ]]</td> <div class="modal-dialog modal-dialog-centered" role="document">
<td>[[ plugin.plugin_version ]]</td> <div class="modal-content">
<td>[[ plugin.plugin_author ]]</td> <div class="modal-header">
<td>[[ plugin.plugin_url ]]</td> <h5 class="modal-title" id="notesModalTitle">
<td>[[ plugin.plugin_license ]]</td> Upload Plugin
<td>[[ plugin.plugin_state ]]</td> </h5>
<td><button type="button" class="btn" v-bind:class="pluginClass">[[ pluginAction ]]</button> </td> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</tr> </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>
`, `,
computed: { computed: {
pluginClass() { pluginClass() {
let classname = 'btn-danger'; let classname = 'btn-warning';
if(this.plugin.plugin_state == 'disabled') { if(this.plugin.plugin_state == 'disabled') {
classname = 'btn-success'; classname = 'btn-success';
}; };
@ -44,49 +100,118 @@ Vue.component('plugin',{
return text; 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: ["[[","]]"] delimiters: ["[[","]]"]
}) })
Vue.component('plugins',{ Vue.component('plugins',{
props: ['plugins'], props: ['plugins'],
template: ` template: `
<div class="row r-10 m-2"> <div>
<div class="col col-12"> <div class="d-grid w-100 bg-mine m-0 p-0 sticky-top shadow">
<div class="row"> <div class="row w-100 m-0 p-3">
<div class="col-sm-12 col-md-6 text-center text-md-start"> <div class="col-sm-12 col-md-6 text-center text-md-start text-white ">
<h2>Plugins</h2> <h2>Plugins</h2>
</div> </div>
<div class="col-sm-12 col-md-6 text-center text-md-end"> <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">Install Plugin</button> <button type="button" class="btn btn-warning" @click="restartServer">Restart Server</button>
</div> <button type="button" class="btn btn-info" data-bs-toggle="modal" data-bs-target="#modalUpload">Install Plugin</button>
</div> </div>
<hr /> </div>
<div class="row"> </div>
<table class="table"> <div class="d-grid w-100 m-0 p-0">
<thead> <div class="row w-100 m-0 p-3">
<tr> <ul class="d-flex flex-wrap w-100 m-0 p-0 justify-content-center">
<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 <plugin
v-for="plugin in plugins" v-for="plugin in plugins"
v-bind:plugin="plugin" v-bind:plugin="plugin"
v-bind:key="plugin.plugin_name" v-bind:key="plugin.plugin_name"
></plugin> ></plugin>
</tbody> </ul>
</table>
</div> </div>
</div> </div>
</div> </div>
`, `,
methods: {
restartServer() {
app.restartServer()
}
},
delimiters: ["[[","]]"] delimiters: ["[[","]]"]
}) })
@ -94,6 +219,7 @@ var app = new Vue({
el: '#app', el: '#app',
data: { data: {
plugins: [], plugins: [],
plugin: []
}, },
created() { created() {
this.getPlugins() this.getPlugins()
@ -103,8 +229,21 @@ var app = new Vue({
axios.get('{{ url_for('api.api_get_plugins') }}') axios.get('{{ url_for('api.api_get_plugins') }}')
.then(res => { .then(res => {
this.plugins = res.data.results 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: ["[[","]]"] delimiters: ["[[","]]"]
}) })

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

@ -1,15 +1,22 @@
{% extends "settings_page.html" %} {% extends "settings_page.html" %}
{% block header_script_files %}
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
{% endblock %}
{% block settings_pane %} {% block settings_pane %}
<div class="row r-10 m-2"> <div class="d-grid w-100 bg-mine m-0 p-0 sticky-top shadow">
<div class="col col-12"> <div class="row w-100 m-0 p-3">
<div class="row"> <div class="col-sm-12 col-md-6 text-center text-md-start text-white ">
<div class="col-sm-12 col-md-6 text-center text-md-start">
<h2>Tasks</h2> <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>
</div>
</div> </div>
<hr /> </div>
<div class="row"> <div class="d-grid w-100 m-0 p-0">
<div class="row w-100 m-0 p-3">
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
@ -28,12 +35,24 @@
<th scope="row">Clean Database</th><td><i class="fas fa-play"></i></td> <th scope="row">Clean Database</th><td><i class="fas fa-play"></i></td>
</tr> </tr>
<tr> <tr>
<th scope="row">Restart Server</th><td><i class="fas fa-play"></i></td> <th scope="row">Restart Server</th><td><a onclick="restartServer()"><i class="fas fa-play"></i></a></td>
</tr> </tr>
{{ emit_tep('settings_page_tasks_task') }} {{ emit_tep('settings_page_tasks_task') }}
</tbody> </tbody>
</table> </table>
</div> </div>
</div>
</div> </div>
{% endblock %} {% 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> </h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body center"> <div class="modal-body text-center">
<a :href="'/read/'+issue.issue_id" id="actionRead" class="btn btn-success my-1"> <a :href="'/read/'+issue.issue_id" id="actionRead" class="btn btn-success my-1">
<i class="fas fa-book-open"></i> <i class="fas fa-book-open"></i>
Read Read
@ -201,56 +201,71 @@ Vue.component('collection-modals', {
Vue.component('collection-jumbo', { Vue.component('collection-jumbo', {
props: ['collection'], props: ['collection'],
template: ` template: `
<div class="row w-100 m-0"> <div>
<div class='col-12 col-md-10 offset-md-1 p-2'> <div class="d-grid w-100 bg-mine m-0 p-0 sticky-top shadow">
<div class='row'> <div class="row d-flex flex-nowrap w-100 m-0 p-3 bg-mine">
<div class='col-12 col-md-2 p-2'> <div class="flex-shrink-1">
<div class='row'> <div class="row w-100 m-0 p-0">
<div class='col-10 offest-1'> <div class="col-12 col-md-10 text-start">
<!-- START POSTER --> <span role="button" data-bs-toggle="modal" data-bs-target="#modalInfo">
<div class="new-stashr_poster-wrapper border rounded"> <span class="fs-1 text-white stashr-series_title">[[ collection.collection_name ]]</span>
<div class="stashr-poster_container rounded"> <i class="fas fa-info-circle text-secondary"></i>
<img class="stashr-poster_background w-100" src="/static/assets/cover.svg" id="poster-bg"> </span>
<a class="rounded stashr-poster_link"> <h5>
<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'"> <span class="badge bg-dark">
</a> <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> </div>
</div> </div>
<!-- END POSTER --> <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> </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>
<div class="offcanvas-body">
<div class="col-12 col-md-10 p-2"> <button type="button" class="btn btn-info w-100 m-1" data-bs-toggle="modal" data-bs-target="#modalEditCollection">
<div class="row w-100 bg-light rounded m-0 p-4"> <i class="fas fa-edit mx-1"></i>
<div class="col-12 col-md-10"> Edit Collection
<div class="row"> </button>
<div class="col text-center text-md-start"> <button type="button" class="btn btn-info w-100 m-1" data-bs-toggle="modal" data-bs-target="#modalDeleteCollection">
<h1 class="stashr-series_title">[[ collection.collection_name ]]</h1> <i class="fas fa-trash-alt mx-1"></i>
</div> Delete Collection
</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> </button>
{{ emit_tep('single_collection_page_button_row', collection=collection) }}
<!-- END BUTTON -->
</div> </div>
</div> </div>
</div> </div>
<div class="col-12 col-md-2 text-center text-md-right"> <!--
<!-- START ACTIONS --> <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 %} {% if collection.collection_user_id == current_user.id %}
<div class="dropdown"> <div class="dropdown">
<button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuButton" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuButton" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
@ -265,23 +280,19 @@ Vue.component('collection-jumbo', {
<i class="fas fa-trash-alt"></i> <i class="fas fa-trash-alt"></i>
Delete Collection Delete Collection
</a> </a>
{{ emit_tep('single_collection_page_action_dropdown', collection=collection) }}
</div> </div>
</div> </div>
{% endif %} {% endif %}
<!-- END ACTIONS -->
</div> </div>
</div> </div>
</div> </div>
-->
</div> `,
</div>
</div>`,
computed: { computed: {
statusClass() { statusClass() {
let classname = 'bg-danger'; let classname = 'text-danger';
if (this.collection.collection_public) { if (this.collection.collection_public) {
classname = 'bg-success'; classname = 'text-success';
}; };
return classname; return classname;
}, },
@ -325,27 +336,30 @@ Vue.component('issues', {
Vue.component('issue', { Vue.component('issue', {
props: ['issue'], props: ['issue'],
template: ` template: `
<li class='stashr-cover_size m-2 js-sortable-block' <li class='stashr-item_container m-2 js-sortable-block'
@mouseover="hover = true" @mouseover="hover = true"
@mouseleave="hover = false" @mouseleave="hover = false"
> >
<div class='stashr-poster_wrapper rounded'> <div class="stashr-poster_container border border-dark rounded-3">
{% if collection.collection_user_id == current_user.id %} {% if collection.collection_user_id == current_user.id %}
<div class="stashr-badge_tl badge rounded-pill bg-dark border js-drag-handle"> <div class="stashr-badge_top_left bg-dark js-drag-handle">
<i class="fas fa-arrows-alt"></i> <span class="fa-stack p-0">
<i class="fas fa-arrows-alt fa-stack-1x text-white"></i>
</span>
</div> </div>
{% endif %} {% endif %}
<div class="stashr-badge_br badge rounded-pill bg-dark border"> <div class="stashr-badge_bottom_right bg-dark">
<i class="fas fa-eye" :class="statusClass"></i> <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>
<div class="stashr-poster_container border rounded"> <div data-bs-toggle="modal" data-bs-target="#modalAction" @click="changeModal">
<div class="stashr-overlay_bottom w-100 center" v-if="hover"> <img class="stashr-background w-100" loading="eager" src="/static/assets/cover.svg" />
<a href="#">[[ issue.volume.volume_name ]] #[[ issue.issue_number ]]</a> <img class="stashr-poster w-100" loading="lazy" v-bind:src="'/images/issues/'+ issue.issue_id +'.jpg'" />
</div> </div>
<img class="stashr-poster_background w-100" loading="eager" src="/static/assets/cover.svg" /> <div class="stashr-overlay_bottom w-100 text-center shadow" v-if="hover" data-bs-toggle="modal" data-bs-target="#modalAction" @click="changeModal">
<a class="stashr-poster_link" data-bs-toggle="modal" data-bs-target="#modalAction" @click="changeModal"> [[ issue.volume.volume_name ]] #[[ issue.issue_number ]]
<img class="w-100" loading="lazy" v-bind:src="'/images/issues/'+ issue.issue_id +'.jpg'"/>
</a>
</div> </div>
</div> </div>
</li> </li>

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

@ -143,7 +143,7 @@ Vue.component('modals-issue', {
</h5> </h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body center"> <div class="modal-body text-center">
<span v-if="issue.issue_file_status"> <span v-if="issue.issue_file_status">
<a id="actionHaveRead" class="btn btn-success my-1" :href="'/read/'+issue.issue_id"> <a id="actionHaveRead" class="btn btn-success my-1" :href="'/read/'+issue.issue_id">
<i class="fas fa-book-open"></i> <i class="fas fa-book-open"></i>
@ -418,66 +418,123 @@ Vue.component('modals-volume', {
Vue.component('volume-jumbo', { Vue.component('volume-jumbo', {
props: ['volume'], props: ['volume'],
template: ` template: `
<div class="row w-100 m-0"> <div>
<div class='col-12 col-md-10 offset-md-1 p-2'> <div class="d-grid w-100 bg-mine m-0 p-0 sticky-top shadow">
<div class='row'> <div class="row d-flex flex-nowrap w-100 m-0 p-3 bg-mine">
<div class='col-12 col-md-2 p-2'> <div class="flex-shrink-1">
<div class='row'> <div class="row w-100 m-0 p-0">
<div class='col-10 offset-1'> <div class="col-12 col-md-10 text-start">
<!-- START POSTER --> <span role="button" data-bs-toggle="modal" data-bs-target="#modalInfo">
<div class="new-stashr_poster-wrapper border rounded"> <span class="fs-1 text-white stashr-series_title">[[ volume.volume_name ]]</span>
<div class="stashr-poster_container rounded"> <i class="fas fa-info-circle text-secondary"></i>
<img class="stashr-poster_background w-100" src="/static/assets/cover.svg" id="poster-bg"> </span>
<a class="rounded stashr-poster_link"> <h5>
<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"> <span class="badge bg-dark">
</a> <i class="fas fa-circle" v-bind:class="volumeStatus"></i>
</div> [[ 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>
<!-- END POSTER -->
</div> </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="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="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>
<div class="row"> <div class="offcanvas-body">
<div class="col text-center text-md-start"> {% if (current_user.role.lower() == 'admin') or
<!-- START BUTTON --> (current_user.role.lower() == 'librarian') %}
<button class="btn btn-primary m-1" type="button" data-bs-toggle="modal" data-bs-target="#modalInfo"> <button type="button" class="btn btn-info w-100 m-1" data-bs-toggle="modal" data-bs-target="#modalRatingEdit">
INFORMATION <i class="far fa-sticky-note mx-1"></i>
Edit Rating
</button> </button>
{{ emit_tep('single_volume_page_button_row', volume_id=volume_id) }} <button type="button" class="btn btn-info w-100 m-1" v-on:click="refreshVolume">
<!-- END BUTTON --> <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>
</div> </div>
<div class="col-12 col-md-2 text-center text-md-right"> <!--
<!-- START NEW ACTIONS --> <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"> <div class="dropdown">
<a class="btn btn-secondary dropdown-toggle" href="#" role="button" id="actionMenu" data-bs-toggle="dropdown" aria-expanded="false"> <a class="btn btn-dark dropdown-toggle" href="#" role="button" id="actionMenu" data-bs-toggle="dropdown" aria-expanded="false">
Actions Actions
</a> </a>
@ -510,14 +567,10 @@ Vue.component('volume-jumbo', {
{{ emit_tep("single_volume_page_action_dropdown", volume_id=volume_id) }} {{ emit_tep("single_volume_page_action_dropdown", volume_id=volume_id) }}
</ul> </ul>
</div> </div>
<!-- END NEW ACTIONS -->
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
-->
`, `,
computed: { computed: {
publisherName() { publisherName() {
@ -545,9 +598,9 @@ Vue.component('volume-jumbo', {
}; };
}, },
volumeStatus() { volumeStatus() {
let classname = 'bg-danger'; let classname = 'text-danger';
if (this.volume.volume_status) { if (this.volume.volume_status) {
classname = 'bg-success' classname = 'text-success'
}; };
return classname; return classname;
}, },
@ -587,33 +640,42 @@ Vue.component('volume-jumbo', {
.catch(err => console.log(err)) .catch(err => console.log(err))
}, },
}, },
created() {
document.title = 'Stashr - ' + this.volume.volume_name;
},
delimiters: ["[[","]]"] delimiters: ["[[","]]"]
}) })
Vue.component('issue-item', { Vue.component('issue-item', {
props: ['issue'], props: ['issue'],
template: ` template: `
<li class='stashr-cover_size m-2' <li class='stashr-item_container m-2'
@mouseover="hover = true" @mouseover="hover = true"
@mouseleave="hover = false" @mouseleave="hover = false"
> >
<div class='stashr-poster_wrapper rounded'> <div class="stashr-poster_container border border-dark rounded-3">
<div class="stashr-badge_tl badge rounded-pill bg-info border">[[ issue.issue_number ]]</div> <div class="stashr-poster_info bg-info text-white px-1">
<div class="stashr-badge_bl badge rounded-pill bg-dark border"> #[[ issue.issue_number ]]
<i class="fas fa-check" v-bind:class="statusFile"></i> </div>
<i class="fas fa-book" v-bind:class="statusOwned" v-on:click="toggleOwned"></i> <div class="stashr-badge_bottom_left bg-dark">
</div> <span class="fa-stack p-0">
<div class="stashr-badge_br badge rounded-pill bg-dark border"> <i class="fas fa-stack-1x" v-bind:class="statusFile"></i>
<i class="fas fa-eye" v-bind:class="statusRead" v-on:click="toggleRead"></i> </span>
</div> <!--
<div class="stashr-poster_container border rounded"> <span class="fa-stack p-0" v-on:click="toggleOwned">
<div class="stashr-overlay_top w-100 center" v-if="hover" > <i class="fas fa-book fa-stack-1x" v-bind:class="statusOwned"></i>
<!-- START BUTTONS --> </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"> <span v-if="issue.issue_file_status">
<!-- HAVE -->
{% if (current_user.role.lower() == 'admin') or {% if (current_user.role.lower() == 'admin') or
(current_user.role.lower() == 'librarian') or (current_user.role.lower() == 'librarian') or
(current_user.role.lower() == 'patron') %} (current_user.role.lower() == 'patron') %}
@ -621,17 +683,9 @@ Vue.component('issue-item', {
{% endif %} {% endif %}
<a :href="'/read/'+issue.issue_id" data-toggle="tooltip" title="Read"><i class="text-primary px-1 fas fa-book"></i></a> <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> <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) }} {{ emit_tep("single_volume_page_top_overlay_have", volume_id=volume_id) }}
</span> </span>
<span v-else> <span v-else>
<!-- MISSING -->
{% if (current_user.role.lower() == 'admin') or {% if (current_user.role.lower() == 'admin') or
(current_user.role.lower() == 'librarian') %} (current_user.role.lower() == 'librarian') %}
<span data-toggle="tooltip" title="Upload to Server"> <span data-toggle="tooltip" title="Upload to Server">
@ -642,12 +696,6 @@ Vue.component('issue-item', {
{% endif %} {% endif %}
{{ emit_tep("single_volume_page_top_overlay_missing", volume_id=volume_id) }} {{ emit_tep("single_volume_page_top_overlay_missing", volume_id=volume_id) }}
</span> </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> </div>
</li> </li>
@ -655,9 +703,9 @@ Vue.component('issue-item', {
data() { return { hover: false, have: false, } }, data() { return { hover: false, have: false, } },
computed: { computed: {
statusFile() { statusFile() {
let classname = 'text-danger'; let classname = 'text-danger fa-times';
if(this.issue.issue_file_status) { if(this.issue.issue_file_status) {
classname = 'text-success'; classname = 'text-success fa-check';
}; };
return classname; return classname;
}, },
@ -789,7 +837,7 @@ Vue.component('issue-item', {
Vue.component('issues', { Vue.component('issues', {
props: ['issues'], props: ['issues'],
template: ` template: `
<div> <div class="py-3">
<ul class="d-flex flex-wrap w-100 m-0 p-0 justify-content-center"> <ul class="d-flex flex-wrap w-100 m-0 p-0 justify-content-center">
<issue-item <issue-item
v-for="issue in issues" v-for="issue in issues"

@ -35,20 +35,22 @@ SOFTWARE.
""" --- HUEY IMPORT --- """ """ --- HUEY IMPORT --- """
""" --- PYTHON IMPORTS --- """ """ --- PYTHON IMPORTS --- """
import datetime, time, pathlib, os, shutil, requests, json import datetime, time, pathlib, os, shutil, requests, json, sys, subprocess
from slugify import slugify from slugify import slugify
""" --- STASHR DEPENDENCY IMPORTS --- """ """ --- STASHR DEPENDENCY IMPORTS --- """
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
""" --- STASHR CORE IMPORTS --- """ """ --- STASHR CORE IMPORTS --- """
from stashr import log, database, parsefilename, paths, folders, naming from stashr import log, database, parsefilename, paths, folders, naming, stashr
# from stashr.stashr import stashr_image_downloaded
from stashr.comicvine import cv from stashr.comicvine import cv
from stashr.config import stashrconfig from stashr.config import stashrconfig
from flask import flash from flask import flash
from flask_login import current_user from flask_login import current_user
from flask_bcrypt import generate_password_hash from flask_bcrypt import generate_password_hash
from flask_pluginkit import PluginInstaller
from sqlalchemy.orm import contains_eager from sqlalchemy.orm import contains_eager
@ -160,6 +162,8 @@ def update_release_list():
database.session.bulk_save_objects(releases) database.session.bulk_save_objects(releases)
database.session.commit() database.session.commit()
update_volumes_from_release_list()
""" --- COMIC STATUS TASKS --- """ """ --- COMIC STATUS TASKS --- """
@ -913,6 +917,8 @@ def download_image(id, image_type, url):
with open(filepath, 'wb') as f: with open(filepath, 'wb') as f:
f.write(r.content) 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(): def update_volumes_from_release_list():
@ -925,91 +931,6 @@ def update_volumes_from_release_list():
refresh_single_volume(update.NewReleases.new_release_volume_id) 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 --------------------- """ """ --------------------- SCRAPE FOLDERS (RE)(RE)WRITE --------------------- """
@ -1051,6 +972,15 @@ def new_create_scrape_entries():
def new_create_empty_scrape_entry(item): 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( new_scrape_item = database.ScrapeItems(
scrape_directory = item.directory_path, scrape_directory = item.directory_path,
scrape_directory_id = item.directory_id scrape_directory_id = item.directory_id
@ -1069,6 +999,11 @@ def new_get_scrape_candidates(item):
.query(database.ScrapeItems) \ .query(database.ScrapeItems) \
.filter(database.ScrapeItems.scrape_directory == item.directory_path) \ .filter(database.ScrapeItems.scrape_directory == item.directory_path) \
.first() .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']) candidates = cv.search(item.directory_path, limit=10, resources=['volume'])
scrape_candidate = candidates.results[0]['id'] scrape_candidate = candidates.results[0]['id']
match_found = False match_found = False
@ -1080,14 +1015,21 @@ def new_get_scrape_candidates(item):
if not match_found: if not match_found:
if str(candidate['start_year']) in item.directory_path: if str(candidate['start_year']) in item.directory_path:
scrape_candidate = candidate['id'] scrape_candidate = candidate['id']
match_found = True match_found = False
scrape_item.scrape_json = json.dumps(candidates.results) scrape_item.scrape_json = json.dumps(candidates.results)
scrape_item.scrape_candidate = scrape_candidate 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.merge(scrape_item)
database.session.commit() 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(): def new_add_scraped_matches():
matched_directories = database.session \ matched_directories = database.session \
@ -1100,13 +1042,106 @@ def new_add_scraped_matches():
def new_add_scrape_match(item): 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_volume_id = item.scrape_candidate
item.directory.directory_in_library = 1 item.directory.directory_in_library = 1
database.session.merge(item) database.session.merge(item)
database.session.delete(item) database.session.delete(item)
try:
database.session.commit() database.session.commit()
except:
logger.warning(f'Volume {item.scrape_candidate} in database')
database.session.rollback()
return
add_volume_to_library(item.scrape_candidate) add_volume_to_library(item.scrape_candidate)
scrape_volume_issues(item.scrape_candidate) scrape_volume_issues(item.scrape_candidate)
scan_volume_files(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