Move Code into repo
This commit is contained in:
parent
1eef1854bc
commit
53ffe927f3
5
.dockerignore
Normal file
5
.dockerignore
Normal file
@ -0,0 +1,5 @@
|
||||
credentials.py
|
||||
.idea/
|
||||
__pycache__/
|
||||
venv/
|
||||
.git/
|
37
.editorconfig
Normal file
37
.editorconfig
Normal file
@ -0,0 +1,37 @@
|
||||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# Unix-style newlines with a newline ending every file
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
|
||||
# Matches multiple files with brace expansion notation
|
||||
# Set default charset
|
||||
[*.{js,py}]
|
||||
charset = utf-8
|
||||
|
||||
# 4 space indentation
|
||||
[*.py]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[{*.jinja2,*.html.j2}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
# Tab indentation (no size specified)
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
|
||||
# Indentation override for all JS under lib directory
|
||||
[lib/**.js]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
# Matches the exact files either package.json or .travis.yml
|
||||
[{package.json,.travis.yml}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
4
.gitattributes
vendored
Normal file
4
.gitattributes
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
app/static/bootstrap-4.2.1/* linguist-vendored
|
||||
app/static/bootswatch-master/* linguist-vendored
|
||||
app/static/datatables/* linguist-vendored
|
||||
app/static/font-awesome/* linguist-vendored
|
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
credentials.py
|
||||
resources.py
|
||||
.idea/
|
||||
__pycache__/
|
||||
venv/
|
||||
static/policy/
|
||||
app/static/site.css
|
||||
app/static/.webassets-cache/**/*
|
||||
app/static/brickdb/*
|
||||
locale.json
|
||||
app/static/ldddb/*
|
||||
.vscode/settings.json
|
||||
locale.xml
|
||||
app/luclient/*
|
||||
app/cache/*
|
21
Dockerfile
Normal file
21
Dockerfile
Normal file
@ -0,0 +1,21 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
FROM python:3.8-slim-buster
|
||||
|
||||
RUN apt update
|
||||
RUN apt -y install zip
|
||||
RUN apt -y install imagemagick
|
||||
|
||||
COPY requirements.txt requirements.txt
|
||||
|
||||
RUN pip install -r requirements.txt
|
||||
RUN pip install gunicorn
|
||||
|
||||
COPY wsgi.py wsgi.py
|
||||
COPY entrypoint.sh entrypoint.sh
|
||||
COPY ./app /app
|
||||
COPY ./migrations /migrations
|
||||
|
||||
EXPOSE 8000
|
||||
RUN chmod +x entrypoint.sh
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
50
Jenkinsfile
vendored
Normal file
50
Jenkinsfile
vendored
Normal file
@ -0,0 +1,50 @@
|
||||
properties([
|
||||
parameters([
|
||||
gitParameter(
|
||||
branch: '',
|
||||
branchFilter: 'origin/(.*)',
|
||||
defaultValue: 'origin/refactor',
|
||||
description: '',
|
||||
name: 'BRANCH',
|
||||
quickFilterEnabled: false,
|
||||
selectedValue: 'NONE',
|
||||
sortMode: 'NONE',
|
||||
tagFilter: '*',
|
||||
useRepository: 'git@github.com:aronwk-aaron/AccountManager.git',
|
||||
type: 'PT_BRANCH'
|
||||
)
|
||||
])
|
||||
])
|
||||
|
||||
node('worker'){
|
||||
stage('Clone Code'){
|
||||
checkout([
|
||||
$class: 'GitSCM',
|
||||
branches: [[name: params.BRANCH]],
|
||||
extensions: [],
|
||||
userRemoteConfigs: [
|
||||
[
|
||||
credentialsId: 'aronwk',
|
||||
url: 'git@github.com:aronwk-aaron/AccountManager.git'
|
||||
]
|
||||
]
|
||||
])
|
||||
}
|
||||
def tag = ''
|
||||
stage("Build Container"){
|
||||
|
||||
if (params.BRANCH.contains('master')){
|
||||
tag = 'latest'
|
||||
} else {
|
||||
tag = params.BRANCH.replace('\\', '-')
|
||||
}
|
||||
sh "docker build -t aronwk/dlu-account_manager:${tag} ."
|
||||
}
|
||||
stage("Push Container"){
|
||||
withCredentials([usernamePassword(credentialsId: 'docker-hub-token', passwordVariable: 'password', usernameVariable: 'username')]) {
|
||||
sh "docker login -u ${username} -p ${password}"
|
||||
sh "docker push aronwk/dlu-account_manager:${tag}"
|
||||
sh 'docker logout'
|
||||
}
|
||||
}
|
||||
}
|
69
README.md
Normal file
69
README.md
Normal file
@ -0,0 +1,69 @@
|
||||
# Nexus Dashboard
|
||||
|
||||
**This is a WIP: For Advanced Users**
|
||||
|
||||
<p align="center">
|
||||
<img src="app/static/logo/logo.png" alt="Sublime's custom image"/>
|
||||
</p>
|
||||
|
||||
# Deployment
|
||||
|
||||
## Docker
|
||||
|
||||
```bash
|
||||
|
||||
docker run -d \
|
||||
-e APP_SECRET_KEY='<secret_key>' \
|
||||
-e APP_DATABASE_URI='mysql+pymysql://<username>:<password>@<host>:<port>/<database>' \
|
||||
# you can include other optional Environment Variables from below like this
|
||||
-e REQUIRE_PLAY_KEY=True
|
||||
-p 8000:8000/tcp
|
||||
-v /path/to/unpacked/client:/app/luclient:rw \
|
||||
-v /path/to/cachedir:/app/cache:rw \ # optional for persistent cache for conversions
|
||||
aronwk/dlu-account_manager:refactor
|
||||
|
||||
```
|
||||
|
||||
* /app/luclient must be mapped to the location of an unpacked client
|
||||
* you only need `res/` and `locale/` from the client, but dropping the whole cleint in there won't hurt
|
||||
* Use `fdb_to_sqlite.py` in lcdr's utilities on `res/cdclient.fdb` in the unpacked client to convert the client database to `cdclient.sqlite`
|
||||
* Put teh resulting `cdclient.sqlite` in the res folder: `res/cdclient.sqlite`
|
||||
* unzip `res/brickdb.zip` in-place
|
||||
* **Docker will do this for you**
|
||||
* you should have new folders and files in the following places:
|
||||
* `res/Assemblies/../..` with a bunch of sub folders
|
||||
* `res/Primitives/../..` with a bunch of sub folders
|
||||
* `res/info.xml`
|
||||
* `res/Materials.xml`
|
||||
|
||||
### Environmental Variables
|
||||
* Required:
|
||||
* APP_SECRET_KEY (Must be provided)
|
||||
* APP_DATABASE_URI (Must be provided)
|
||||
* Optional
|
||||
* USER_ENABLE_REGISTER (Default: True)
|
||||
* USER_ENABLE_EMAIL (Default: True, Needs Mail to be configured)
|
||||
* USER_ENABLE_CONFIRM_EMAIL (Default: True)
|
||||
* USER_ENABLE_INVITE_USER (Default: False)
|
||||
* USER_REQUIRE_INVITATION (Default: False)
|
||||
* REQUIRE_PLAY_KEY (Default: True)
|
||||
* MAIL_SERVER (Default: smtp.gmail.com)
|
||||
* MAIL_PORT (Default: 587)
|
||||
* MAIL_USE_SSL (Default: False)
|
||||
* MAIL_USE_TLS (Default: True)
|
||||
* MAIL_USERNAME (Default: None)
|
||||
* MAIL_PASSWORD (Default: None)
|
||||
* USER_EMAIL_SENDER_NAME (Default: None)
|
||||
* USER_EMAIL_SENDER_EMAIL (Default: None)
|
||||
|
||||
## Manual
|
||||
|
||||
Don't, use Docker /s
|
||||
|
||||
TODO: Make manual deployment easier to configure
|
||||
|
||||
# Development
|
||||
|
||||
Please use [Editor Config](https://editorconfig.org/)
|
||||
|
||||
* `flask run` to run local dev server
|
203
app/__init__.py
Normal file
203
app/__init__.py
Normal file
@ -0,0 +1,203 @@
|
||||
import os
|
||||
from flask import Flask, url_for, g, redirect
|
||||
from functools import wraps
|
||||
from flask_assets import Environment
|
||||
from webassets import Bundle
|
||||
import time
|
||||
from app.models import db, migrate, PlayKey
|
||||
from app.schemas import ma
|
||||
from app.forms import CustomUserManager
|
||||
from flask_user import user_registered, current_user
|
||||
from flask_wtf.csrf import CSRFProtect
|
||||
from flask_apscheduler import APScheduler
|
||||
from app.luclient import query_cdclient, register_luclient_jinja_helpers
|
||||
|
||||
from app.commands import init_db, init_accounts
|
||||
from app.models import Account, AccountInvitation
|
||||
|
||||
# Instantiate Flask extensions
|
||||
csrf_protect = CSRFProtect()
|
||||
scheduler = APScheduler()
|
||||
# db and migrate is instantiated in models.py
|
||||
|
||||
|
||||
def create_app():
|
||||
|
||||
app = Flask(__name__, instance_relative_config=True)
|
||||
|
||||
# decrement uses on a play key after a successful registration
|
||||
# and increment the times it has been used
|
||||
@user_registered.connect_via(app)
|
||||
def after_register_hook(sender, user, **extra):
|
||||
if app.config["REQUIRE_PLAY_KEY"]:
|
||||
play_key_used = PlayKey.query.filter(PlayKey.id == user.play_key_id).first()
|
||||
play_key_used.key_uses = play_key_used.key_uses - 1
|
||||
play_key_used.times_used = play_key_used.times_used + 1
|
||||
db.session.add(play_key_used)
|
||||
db.session.commit()
|
||||
|
||||
# A bunch of jinja filters to make things easiers
|
||||
@app.template_filter('ctime')
|
||||
def timectime(s):
|
||||
if s:
|
||||
return time.ctime(s) # or datetime.datetime.fromtimestamp(s)
|
||||
else:
|
||||
return "Never"
|
||||
|
||||
@app.template_filter('check_perm_map')
|
||||
def check_perm_map(perm_map, bit):
|
||||
if perm_map:
|
||||
return perm_map & (1 << bit)
|
||||
else:
|
||||
return 0 & (1 << bit)
|
||||
|
||||
@app.teardown_appcontext
|
||||
def close_connection(exception):
|
||||
cdclient = getattr(g, '_cdclient', None)
|
||||
if cdclient is not None:
|
||||
cdclient.close()
|
||||
|
||||
# add the commands to flask cli
|
||||
app.cli.add_command(init_db)
|
||||
app.cli.add_command(init_accounts)
|
||||
|
||||
register_settings(app)
|
||||
register_extensions(app)
|
||||
register_blueprints(app)
|
||||
register_luclient_jinja_helpers(app)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def register_extensions(app):
|
||||
"""Register extensions for Flask app
|
||||
|
||||
Args:
|
||||
app (Flask): Flask app to register for
|
||||
"""
|
||||
db.init_app(app)
|
||||
migrate.init_app(app, db)
|
||||
ma.init_app(app)
|
||||
|
||||
scheduler.init_app(app)
|
||||
scheduler.start()
|
||||
|
||||
csrf_protect.init_app(app)
|
||||
|
||||
user_manager = CustomUserManager(
|
||||
app, db, Account, UserInvitationClass=AccountInvitation
|
||||
)
|
||||
|
||||
assets = Environment(app)
|
||||
assets.url = app.static_url_path
|
||||
scss = Bundle('scss/site.scss', filters='libsass', output='site.css')
|
||||
assets.register('scss_all', scss)
|
||||
|
||||
|
||||
def register_blueprints(app):
|
||||
"""Register blueprints for Flask app
|
||||
|
||||
Args:
|
||||
app (Flask): Flask app to register for
|
||||
"""
|
||||
|
||||
from .main import main_blueprint
|
||||
app.register_blueprint(main_blueprint)
|
||||
from .play_keys import play_keys_blueprint
|
||||
app.register_blueprint(play_keys_blueprint, url_prefix='/play_keys')
|
||||
from .accounts import accounts_blueprint
|
||||
app.register_blueprint(accounts_blueprint, url_prefix='/accounts')
|
||||
from .characters import character_blueprint
|
||||
app.register_blueprint(character_blueprint, url_prefix='/characters')
|
||||
from .properties import property_blueprint
|
||||
app.register_blueprint(property_blueprint, url_prefix='/properties')
|
||||
from .moderation import moderation_blueprint
|
||||
app.register_blueprint(moderation_blueprint, url_prefix='/moderation')
|
||||
from .log import log_blueprint
|
||||
app.register_blueprint(log_blueprint, url_prefix='/log')
|
||||
from .bug_reports import bug_report_blueprint
|
||||
app.register_blueprint(bug_report_blueprint, url_prefix='/bug_reports')
|
||||
from .mail import mail_blueprint
|
||||
app.register_blueprint(mail_blueprint, url_prefix='/mail')
|
||||
from .luclient import luclient_blueprint
|
||||
app.register_blueprint(luclient_blueprint, url_prefix='/luclient')
|
||||
from .reports import reports_blueprint
|
||||
app.register_blueprint(reports_blueprint, url_prefix='/reports')
|
||||
|
||||
|
||||
|
||||
def register_settings(app):
|
||||
"""Register setting from setting and env
|
||||
|
||||
Args:
|
||||
app (Flask): Flask app to register for
|
||||
"""
|
||||
|
||||
# Load common settings
|
||||
app.config.from_object('app.settings')
|
||||
|
||||
# Load environment specific settings
|
||||
app.config['TESTING'] = False
|
||||
app.config['DEBUG'] = False
|
||||
|
||||
# always pull these two from the env
|
||||
app.config['SECRET_KEY'] = os.getenv('APP_SECRET_KEY')
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('APP_DATABASE_URI')
|
||||
|
||||
# try to get overides, otherwise just use what we have already
|
||||
app.config['USER_ENABLE_REGISTER'] = os.getenv(
|
||||
'USER_ENABLE_REGISTER',
|
||||
app.config['USER_ENABLE_REGISTER']
|
||||
)
|
||||
app.config['USER_ENABLE_EMAIL'] = os.getenv(
|
||||
'USER_ENABLE_EMAIL',
|
||||
app.config['USER_ENABLE_EMAIL']
|
||||
)
|
||||
app.config['USER_ENABLE_CONFIRM_EMAIL'] = os.getenv(
|
||||
'USER_ENABLE_CONFIRM_EMAIL',
|
||||
app.config['USER_ENABLE_CONFIRM_EMAIL']
|
||||
)
|
||||
app.config['REQUIRE_PLAY_KEY'] = os.getenv(
|
||||
'REQUIRE_PLAY_KEY',
|
||||
app.config['REQUIRE_PLAY_KEY']
|
||||
)
|
||||
app.config['USER_ENABLE_INVITE_USER'] = os.getenv(
|
||||
'USER_ENABLE_INVITE_USER',
|
||||
app.config['USER_ENABLE_INVITE_USER']
|
||||
)
|
||||
app.config['USER_REQUIRE_INVITATION'] = os.getenv(
|
||||
'USER_REQUIRE_INVITATION',
|
||||
app.config['USER_REQUIRE_INVITATION']
|
||||
)
|
||||
app.config['SQLALCHEMY_ENGINE_OPTIONS'] = {
|
||||
"pool_pre_ping": True,
|
||||
"pool_size": 10,
|
||||
"max_overflow": 2,
|
||||
"pool_recycle": 300,
|
||||
"pool_pre_ping": True,
|
||||
"pool_use_lifo": True
|
||||
}
|
||||
app.config['MAIL_SERVER'] = os.getenv('MAIL_SERVER', 'smtp.gmail.com')
|
||||
app.config['MAIL_PORT'] = os.getenv('MAIL_USE_SSL', 587)
|
||||
app.config['MAIL_USE_SSL'] = os.getenv('MAIL_USE_SSL', False)
|
||||
app.config['MAIL_USE_TLS'] = os.getenv('MAIL_USE_TLS', True)
|
||||
app.config['MAIL_USERNAME'] = os.getenv('MAIL_USERNAME', None)
|
||||
app.config['MAIL_PASSWORD'] = os.getenv('MAIL_PASSWORD', None)
|
||||
app.config['USER_EMAIL_SENDER_NAME'] = os.getenv('USER_EMAIL_SENDER_NAME', None)
|
||||
app.config['USER_EMAIL_SENDER_EMAIL'] = os.getenv('USER_EMAIL_SENDER_EMAIL', None)
|
||||
|
||||
|
||||
def gm_level(gm_level):
|
||||
"""Decorator for handling permissions based on the user's GM Level
|
||||
|
||||
Args:
|
||||
gm_level (int): 0-9
|
||||
"""
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
if current_user.gm_level < gm_level:
|
||||
return redirect(url_for('main.index'))
|
||||
return func(*args, **kwargs)
|
||||
return wrapper
|
||||
return decorator
|
170
app/accounts.py
Normal file
170
app/accounts.py
Normal file
@ -0,0 +1,170 @@
|
||||
from flask import render_template, Blueprint, redirect, url_for, request, abort, current_app, flash
|
||||
from flask_user import login_required, current_user
|
||||
import json
|
||||
from datatables import ColumnDT, DataTables
|
||||
import datetime
|
||||
import time
|
||||
from app.models import Account, AccountInvitation, db
|
||||
from app.schemas import AccountSchema
|
||||
from app import gm_level
|
||||
from app.forms import EditGMLevelForm
|
||||
|
||||
accounts_blueprint = Blueprint('accounts', __name__)
|
||||
|
||||
account_schema = AccountSchema()
|
||||
|
||||
@accounts_blueprint.route('/', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
def index():
|
||||
return render_template('accounts/index.html.j2')
|
||||
|
||||
|
||||
@accounts_blueprint.route('/view/<id>', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
def view(id):
|
||||
account_data = Account.query.filter(Account.id == id).first()
|
||||
if account_data:
|
||||
return render_template('accounts/view.html.j2', account_data=account_data)
|
||||
else:
|
||||
return redirect(url_for('main.index'))
|
||||
|
||||
|
||||
@accounts_blueprint.route('/edit_gm_level/<id>', methods=('GET', 'POST'))
|
||||
@login_required
|
||||
@gm_level(8)
|
||||
def edit_gm_level(id):
|
||||
if current_user.id == int(id):
|
||||
flash("You cannot your own GM Level", "danger")
|
||||
return redirect(request.referrer if request.referrer else url_for("main.index"))
|
||||
account_data = Account.query.filter(Account.id==id).first()
|
||||
if account_data.gm_level >= 8 and current_user.gm_level == 8:
|
||||
flash("You cannot edit this user's GM Level", "warning")
|
||||
return redirect(request.referrer if request.referrer else url_for("main.index"))
|
||||
|
||||
form = EditGMLevelForm()
|
||||
|
||||
if form.validate_on_submit():
|
||||
account_data.gm_level = form.gm_level.data
|
||||
account_data.save()
|
||||
return redirect(url_for('accounts.view', id=account_data.id))
|
||||
|
||||
form.gm_level.data = account_data.gm_level
|
||||
|
||||
return render_template('accounts/edit_gm_level.html.j2', form=form, username=account_data.username)
|
||||
|
||||
|
||||
@accounts_blueprint.route('/lock/<id>', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
def lock(id):
|
||||
account = Account.query.filter(Account.id == id).first()
|
||||
account.locked = not account.locked
|
||||
account.save()
|
||||
if account.locked:
|
||||
flash("Locked Account", "danger")
|
||||
else:
|
||||
flash("Unlocked account", "success")
|
||||
return redirect(request.referrer if request.referrer else url_for("main.index"))
|
||||
|
||||
|
||||
@accounts_blueprint.route('/ban/<id>', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
def ban(id):
|
||||
account = Account.query.filter(Account.id == id).first()
|
||||
account.banned = not account.banned
|
||||
account.save()
|
||||
if account.banned:
|
||||
flash("Banned Account", "danger")
|
||||
else:
|
||||
flash("Unbanned account", "success")
|
||||
return redirect(request.referrer if request.referrer else url_for("main.index"))
|
||||
|
||||
|
||||
@accounts_blueprint.route('/muted/<id>/<days>', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
def mute(id, days=0):
|
||||
account = Account.query.filter(Account.id == id).first()
|
||||
if days == "0":
|
||||
account.mute_expire = 0
|
||||
flash("Unmuted Account", "success")
|
||||
else:
|
||||
muted_intil = datetime.datetime.now() + datetime.timedelta(days=int(days))
|
||||
account.mute_expire = muted_intil.timestamp()
|
||||
flash(f"Muted account for {days} days", "danger")
|
||||
account.save()
|
||||
|
||||
return redirect(request.referrer if request.referrer else url_for("main.index"))
|
||||
|
||||
|
||||
@accounts_blueprint.route('/get', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
def get():
|
||||
columns = [
|
||||
ColumnDT(Account.id), # 0
|
||||
ColumnDT(Account.username), # 1
|
||||
ColumnDT(Account.email), # 2
|
||||
ColumnDT(Account.gm_level), # 3
|
||||
ColumnDT(Account.locked), # 4
|
||||
ColumnDT(Account.banned), # 5
|
||||
ColumnDT(Account.mute_expire), # 6
|
||||
ColumnDT(Account.created_at), # 7
|
||||
ColumnDT(Account.email_confirmed_at) # 8
|
||||
]
|
||||
|
||||
query = db.session.query().select_from(Account)
|
||||
|
||||
params = request.args.to_dict()
|
||||
|
||||
rowTable = DataTables(params, query, columns)
|
||||
|
||||
data = rowTable.output_result()
|
||||
for account in data["data"]:
|
||||
account["0"] = f"""
|
||||
<a role="button" class="btn btn-primary btn btn-block"
|
||||
href='{url_for('accounts.view', id=account["0"])}'>
|
||||
View
|
||||
</a>
|
||||
"""
|
||||
# <a role="button" class="btn btn-danger btn btn-block"
|
||||
# href='{url_for('acounts.delete', id=account["0"])}'>
|
||||
# Delete
|
||||
# </a>
|
||||
|
||||
if account["4"]:
|
||||
account["4"] = '''<h2 class="far fa-times-circle text-danger"></h2>'''
|
||||
else:
|
||||
account["4"] = '''<h2 class="far fa-check-square text-success"></h2>'''
|
||||
|
||||
if account["5"]:
|
||||
account["5"] = '''<h2 class="far fa-times-circle text-danger"></h2>'''
|
||||
else:
|
||||
account["5"] = '''<h2 class="far fa-check-square text-success"></h2>'''
|
||||
|
||||
if account["6"]:
|
||||
account["6"] = f'''<h2 class="far fa-times-circle text-danger"></h2>'''
|
||||
else:
|
||||
account["6"] = '''<h2 class="far fa-check-square text-success"></h2>'''
|
||||
|
||||
if current_app.config["USER_ENABLE_EMAIL"]:
|
||||
if account["8"]:
|
||||
account["8"] = f'''<h2 class="far fa-check-square text-success"></h2>'''
|
||||
else:
|
||||
account["8"] = '''<h2 class="far fa-times-circle text-danger"></h2>'''
|
||||
else:
|
||||
# shift columns to fill in gap of 2
|
||||
account["2"] = account["3"]
|
||||
account["3"] = account["4"]
|
||||
account["4"] = account["5"]
|
||||
account["5"] = account["6"]
|
||||
account["6"] = account["7"]
|
||||
# remove last two columns
|
||||
del account["7"]
|
||||
del account["8"]
|
||||
|
||||
return data
|
||||
|
115
app/bug_reports.py
Normal file
115
app/bug_reports.py
Normal file
@ -0,0 +1,115 @@
|
||||
from flask import render_template, Blueprint, redirect, url_for, request, abort, flash
|
||||
from flask_user import login_required, current_user
|
||||
from app.models import db, BugReport, CharacterInfo
|
||||
from datatables import ColumnDT, DataTables
|
||||
from app.forms import ResolveBugReportForm
|
||||
from app import gm_level
|
||||
from app.luclient import translate_from_locale
|
||||
|
||||
bug_report_blueprint = Blueprint('bug_reports', __name__)
|
||||
|
||||
@bug_report_blueprint.route('/<status>', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
def index(status):
|
||||
return render_template('bug_reports/index.html.j2', status=status)
|
||||
|
||||
|
||||
@bug_report_blueprint.route('/view/<id>', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
def view(id):
|
||||
report = BugReport.query.filter(BugReport.id == id).first()
|
||||
if report.resoleved_by:
|
||||
rb = report.resoleved_by.username
|
||||
else:
|
||||
rb=""
|
||||
return render_template('bug_reports/view.html.j2', report=report, resolved_by=rb)
|
||||
|
||||
|
||||
@bug_report_blueprint.route('/resolve/<id>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
def resolve(id):
|
||||
report = BugReport.query.filter(BugReport.id == id).first()
|
||||
if report.resolved_time:
|
||||
flash("Bug report already resolved!", "danger")
|
||||
return redirect(request.referrer if request.referrer else url_for("main.index"))
|
||||
|
||||
form = ResolveBugReportForm()
|
||||
if form.validate_on_submit():
|
||||
report.resolution = form.resolution.data
|
||||
report.resoleved_by_id = current_user.id
|
||||
report.resolved_time = db.func.now()
|
||||
report.save()
|
||||
return redirect(url_for("bug_reports.index", status="unresolved"))
|
||||
|
||||
return render_template('bug_reports/resolve.html.j2', form=form, report=report)
|
||||
|
||||
|
||||
@bug_report_blueprint.route('/get/<status>', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
def get(status):
|
||||
columns = [
|
||||
ColumnDT(BugReport.id), # 0
|
||||
ColumnDT(BugReport.body), # 1
|
||||
ColumnDT(BugReport.client_version), # 2
|
||||
ColumnDT(BugReport.other_player_id), # 3
|
||||
ColumnDT(BugReport.selection), # 4
|
||||
ColumnDT(BugReport.submitted), # 5
|
||||
ColumnDT(BugReport.resolved_time), # 6
|
||||
]
|
||||
|
||||
query = None
|
||||
if status=="all":
|
||||
query = db.session.query().select_from(BugReport)
|
||||
elif status=="resolved":
|
||||
query = db.session.query().select_from(BugReport).filter(BugReport.resolved_time != None)
|
||||
elif status=="unresolved":
|
||||
query = db.session.query().select_from(BugReport).filter(BugReport.resolved_time == None)
|
||||
else:
|
||||
raise Exception("Not a valid filter")
|
||||
|
||||
params = request.args.to_dict()
|
||||
|
||||
rowTable = DataTables(params, query, columns)
|
||||
|
||||
data = rowTable.output_result()
|
||||
for report in data["data"]:
|
||||
id = report["0"]
|
||||
report["0"] = f"""
|
||||
<a role="button" class="btn btn-primary btn btn-block"
|
||||
href='{url_for('bug_reports.view', id=id)}'>
|
||||
View
|
||||
</a>
|
||||
"""
|
||||
|
||||
if not report["6"]:
|
||||
report["0"] += f"""
|
||||
<a role="button" class="btn btn-danger btn btn-block"
|
||||
href='{url_for('bug_reports.resolve', id=id)}'>
|
||||
Resolve
|
||||
</a>
|
||||
"""
|
||||
|
||||
if report["3"] == "0":
|
||||
report["3"] = "None"
|
||||
else:
|
||||
character = CharacterInfo.query.filter(CharacterInfo.id == int(report["3"]) & 0xFFFFFFFF).first()
|
||||
if character:
|
||||
report["3"] = f"""
|
||||
<a role="button" class="btn btn-primary btn btn-block"
|
||||
href='{url_for('characters.view', id=(int(report["3"]) & 0xFFFFFFFF))}'>
|
||||
{character.name}
|
||||
</a>
|
||||
"""
|
||||
else:
|
||||
report["3"] = "Player Deleted"
|
||||
|
||||
report["4"] = translate_from_locale(report["4"][2:-1])
|
||||
|
||||
if not report["6"]:
|
||||
report["6"] = '''<h1 class="far fa-times-circle text-danger"></h1>'''
|
||||
|
||||
return data
|
195
app/characters.py
Normal file
195
app/characters.py
Normal file
@ -0,0 +1,195 @@
|
||||
from flask import render_template, Blueprint, redirect, url_for, request, abort, flash
|
||||
from flask_user import login_required, current_user
|
||||
import json
|
||||
from datatables import ColumnDT, DataTables
|
||||
import datetime, time
|
||||
from app.models import CharacterInfo, CharacterXML, Account, db
|
||||
from app.schemas import CharacterInfoSchema
|
||||
from app import gm_level
|
||||
import xmltodict
|
||||
|
||||
character_blueprint = Blueprint('characters', __name__)
|
||||
|
||||
character_schema = CharacterInfoSchema()
|
||||
|
||||
@character_blueprint.route('/', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
def index():
|
||||
return render_template('character/index.html.j2')
|
||||
|
||||
|
||||
@character_blueprint.route('/approve_name/<id>/<action>', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
def approve_name(id, action):
|
||||
character = CharacterInfo.query.filter(CharacterInfo.id == id).first()
|
||||
|
||||
if action == "approve":
|
||||
if character.pending_name:
|
||||
character.name = character.pending_name
|
||||
character.pending_name = ""
|
||||
character.needs_rename = False
|
||||
flash(
|
||||
f"Approved name {character.name}",
|
||||
"success"
|
||||
)
|
||||
elif action == "rename":
|
||||
character.needs_rename = True
|
||||
flash(
|
||||
f"Marked character {character.name} (Pending Name: {character.pending_name if character.pending_name else 'None'}) as needing Rename",
|
||||
"danger"
|
||||
)
|
||||
|
||||
character.save()
|
||||
return redirect(request.referrer if request.referrer else url_for("main.index"))
|
||||
|
||||
|
||||
@character_blueprint.route('/view/<id>', methods=['GET'])
|
||||
@login_required
|
||||
def view(id):
|
||||
|
||||
character_data = CharacterInfo.query.filter(CharacterInfo.id == id).first()
|
||||
|
||||
if character_data == {}:
|
||||
abort(404)
|
||||
return
|
||||
|
||||
if current_user.gm_level < 3:
|
||||
if character_data.account_id and character_data.account_id != current_user.id:
|
||||
abort(403)
|
||||
return
|
||||
character_json = xmltodict.parse(
|
||||
CharacterXML.query.filter(
|
||||
CharacterXML.id==id
|
||||
).first().xml_data,
|
||||
attr_prefix="attr_"
|
||||
)
|
||||
|
||||
# print json for reference
|
||||
# with open("errorchar.json", "a") as file:
|
||||
# file.write(
|
||||
# json.dumps(character_json, indent=4)
|
||||
# )
|
||||
|
||||
# stupid fix for jinja parsing
|
||||
character_json["obj"]["inv"]["holdings"] = character_json["obj"]["inv"].pop("items")
|
||||
# sort by items slot index
|
||||
for inv in character_json["obj"]["inv"]["holdings"]["in"]:
|
||||
if "i" in inv.keys() and type(inv["i"]) == list:
|
||||
inv["i"] = sorted(inv["i"], key = lambda i: int(i['attr_s']))
|
||||
|
||||
|
||||
return render_template(
|
||||
'character/view.html.j2',
|
||||
character_data=character_data,
|
||||
character_json=character_json
|
||||
)
|
||||
|
||||
|
||||
@character_blueprint.route('/restrict/<bit>/<id>', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
def restrict(id, bit):
|
||||
|
||||
# restrict to bit 4-6
|
||||
if 6 < int(bit) < 3:
|
||||
abort(403)
|
||||
return
|
||||
|
||||
character_data = CharacterInfo.query.filter(CharacterInfo.id == id).first()
|
||||
|
||||
if character_data == {}:
|
||||
abort(404)
|
||||
return
|
||||
|
||||
character_data.permission_map ^= (1 << int(bit))
|
||||
character_data.save()
|
||||
|
||||
return redirect(request.referrer if request.referrer else url_for("main.index"))
|
||||
|
||||
|
||||
@character_blueprint.route('/get/<status>', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
def get(status):
|
||||
columns = [
|
||||
ColumnDT(CharacterInfo.id), # 0
|
||||
ColumnDT(Account.username), # 1
|
||||
ColumnDT(CharacterInfo.name), # 2
|
||||
ColumnDT(CharacterInfo.pending_name), # 3
|
||||
ColumnDT(CharacterInfo.needs_rename), # 4
|
||||
ColumnDT(CharacterInfo.last_login), # 5
|
||||
ColumnDT(CharacterInfo.permission_map), # 6
|
||||
]
|
||||
|
||||
query = None
|
||||
if status=="all":
|
||||
query = db.session.query().select_from(CharacterInfo).join(Account)
|
||||
elif status=="approved":
|
||||
query = db.session.query().select_from(CharacterInfo).join(Account).filter((CharacterInfo.pending_name == "") & (CharacterInfo.needs_rename == False))
|
||||
elif status=="unapproved":
|
||||
query = db.session.query().select_from(CharacterInfo).join(Account).filter((CharacterInfo.pending_name != "") | (CharacterInfo.needs_rename == True))
|
||||
else:
|
||||
raise Exception("Not a valid filter")
|
||||
|
||||
params = request.args.to_dict()
|
||||
|
||||
rowTable = DataTables(params, query, columns)
|
||||
|
||||
data = rowTable.output_result()
|
||||
for character in data["data"]:
|
||||
id = character["0"]
|
||||
character["0"] = f"""
|
||||
<div class="d-none">{id}</div>
|
||||
<a role="button" class="btn btn-primary btn btn-block"
|
||||
href='{url_for('characters.view', id=id)}'>
|
||||
View
|
||||
</a>
|
||||
"""
|
||||
|
||||
if not character["4"]:
|
||||
character["0"] += f"""
|
||||
<a role="button" class="btn btn-danger btn btn-block"
|
||||
href='{url_for('characters.approve_name', id=id, action="rename")}'>
|
||||
Needs Rename
|
||||
</a>
|
||||
"""
|
||||
|
||||
if character["3"] or character["4"]:
|
||||
character["0"] += f"""
|
||||
<a role="button" class="btn btn-success btn btn-block"
|
||||
href='{url_for('characters.approve_name', id=id, action="approve")}'>
|
||||
Approve Name
|
||||
</a>
|
||||
"""
|
||||
|
||||
character["1"] = f"""
|
||||
<a role="button" class="btn btn-primary btn btn-block"
|
||||
href='{url_for('accounts.view', id=Account.query.filter(Account.username==character["1"]).first().id)}'>
|
||||
View {character["1"]}
|
||||
</a>
|
||||
"""
|
||||
|
||||
if character["4"]:
|
||||
character["4"] = '''<h1 class="far fa-check-square text-danger"></h1>'''
|
||||
else:
|
||||
character["4"] = '''<h1 class="far fa-times-circle text-success"></h1>'''
|
||||
|
||||
character["5"] = time.ctime(character["5"])
|
||||
|
||||
perm_map = character["6"]
|
||||
character["6"] = ""
|
||||
|
||||
if perm_map & (1 << 4):
|
||||
character["6"] += "Restricted Trade</br>"
|
||||
|
||||
if perm_map & (1 << 5):
|
||||
character["6"] += "Restricted Mail</br>"
|
||||
|
||||
if perm_map & (1 << 6):
|
||||
character["6"] += "Restricted Chat</br>"
|
||||
|
||||
|
||||
return data
|
||||
|
70
app/commands.py
Normal file
70
app/commands.py
Normal file
@ -0,0 +1,70 @@
|
||||
import click
|
||||
import json
|
||||
from flask.cli import with_appcontext
|
||||
import random, string, datetime
|
||||
from flask_user import current_app
|
||||
from app import db
|
||||
from app.models import Account, PlayKey
|
||||
|
||||
@click.command("init_db")
|
||||
@click.argument('drop_tables', nargs=1)
|
||||
@with_appcontext
|
||||
def init_db(drop_tables=False):
|
||||
""" Initialize the database."""
|
||||
|
||||
print('Initializing Database.')
|
||||
if drop_tables:
|
||||
print('Dropping all tables.')
|
||||
db.drop_all()
|
||||
print('Creating all tables.')
|
||||
db.create_all()
|
||||
print('Database has been initialized.')
|
||||
return
|
||||
|
||||
|
||||
@click.command("init_accounts")
|
||||
@with_appcontext
|
||||
def init_accounts():
|
||||
""" Initialize the accounts."""
|
||||
|
||||
# Add accounts
|
||||
print('Creating Admin account.')
|
||||
admin_account = find_or_create_account(
|
||||
'admin',
|
||||
'example@example.com',
|
||||
'Nope',
|
||||
)
|
||||
|
||||
|
||||
return
|
||||
|
||||
|
||||
def find_or_create_account(name, email, password, gm_level=9):
|
||||
""" Find existing account or create new account """
|
||||
account = Account.query.filter(Account.email == email).first()
|
||||
if not account:
|
||||
key = ""
|
||||
for j in range(4):
|
||||
key += ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(4)) + '-'
|
||||
# Remove last dash
|
||||
key = key[:-1]
|
||||
|
||||
play_key = PlayKey(
|
||||
key_string=key
|
||||
)
|
||||
db.session.add(play_key)
|
||||
db.session.commit()
|
||||
|
||||
play_key = PlayKey.query.filter(PlayKey.key_string == key).first()
|
||||
account = Account(email=email,
|
||||
username=name,
|
||||
password=current_app.user_manager.password_manager.hash_password(password),
|
||||
play_key_id=play_key.id,
|
||||
email_confirmed_at=datetime.datetime.utcnow(),
|
||||
gm_level=gm_level
|
||||
)
|
||||
play_key.key_uses = 0
|
||||
db.session.add(account)
|
||||
db.session.add(play_key)
|
||||
db.session.commit()
|
||||
return # account
|
173
app/forms.py
Normal file
173
app/forms.py
Normal file
@ -0,0 +1,173 @@
|
||||
from flask_wtf import FlaskForm
|
||||
from flask import current_app
|
||||
|
||||
from flask_user.forms import (
|
||||
unique_email_validator,
|
||||
password_validator,
|
||||
unique_username_validator
|
||||
)
|
||||
from flask_user import UserManager
|
||||
from wtforms.widgets import TextArea, NumberInput
|
||||
from wtforms import (
|
||||
StringField,
|
||||
HiddenField,
|
||||
PasswordField,
|
||||
BooleanField,
|
||||
SubmitField,
|
||||
validators,
|
||||
IntegerField,
|
||||
StringField,
|
||||
SelectField
|
||||
)
|
||||
|
||||
from wtforms.validators import DataRequired, Optional
|
||||
from app.models import PlayKey
|
||||
|
||||
def validate_play_key(form, field):
|
||||
"""Validates a field for a valid phone number
|
||||
Args:
|
||||
form: REQUIRED, the field's parent form
|
||||
field: REQUIRED, the field with data
|
||||
Returns:
|
||||
None, raises ValidationError if failed
|
||||
"""
|
||||
# jank to get the fireign key that we need back into the field
|
||||
if current_app.config["REQUIRE_PLAY_KEY"]:
|
||||
field.data = PlayKey.key_is_valid(key_string=field.data)
|
||||
return
|
||||
|
||||
|
||||
class CustomUserManager(UserManager):
|
||||
def customize(self, app):
|
||||
self.RegisterFormClass = CustomRegisterForm
|
||||
|
||||
|
||||
class CustomRegisterForm(FlaskForm):
|
||||
"""Registration form"""
|
||||
next = HiddenField()
|
||||
reg_next = HiddenField()
|
||||
|
||||
# Login Info
|
||||
email = StringField(
|
||||
'E-Mail',
|
||||
validators=[
|
||||
Optional(),
|
||||
validators.Email('Invalid email address'),
|
||||
unique_email_validator,
|
||||
]
|
||||
)
|
||||
|
||||
username = StringField(
|
||||
'Username',
|
||||
validators=[
|
||||
DataRequired(),
|
||||
unique_username_validator,
|
||||
]
|
||||
)
|
||||
|
||||
play_key_id = StringField(
|
||||
'Play Key',
|
||||
validators=[
|
||||
Optional(),
|
||||
validate_play_key,
|
||||
]
|
||||
)
|
||||
|
||||
password = PasswordField('Password', validators=[
|
||||
DataRequired(),
|
||||
password_validator
|
||||
])
|
||||
retype_password = PasswordField('Retype Password', validators=[
|
||||
validators.EqualTo('password', message='Passwords did not match')
|
||||
])
|
||||
|
||||
invite_token = HiddenField('Token')
|
||||
|
||||
submit = SubmitField('Register')
|
||||
|
||||
class CreatePlayKeyForm(FlaskForm):
|
||||
|
||||
count = IntegerField(
|
||||
'How many Play Keys to create',
|
||||
validators=[DataRequired()]
|
||||
)
|
||||
uses = IntegerField(
|
||||
'How many uses each new play key will have',
|
||||
validators=[DataRequired()]
|
||||
)
|
||||
submit = SubmitField('Create!')
|
||||
|
||||
class EditPlayKeyForm(FlaskForm):
|
||||
|
||||
active = BooleanField(
|
||||
'Active'
|
||||
)
|
||||
|
||||
uses = IntegerField(
|
||||
'Play Key Uses'
|
||||
)
|
||||
|
||||
notes = StringField(
|
||||
'Notes',
|
||||
widget=TextArea()
|
||||
)
|
||||
|
||||
submit = SubmitField('Submit')
|
||||
|
||||
|
||||
class EditGMLevelForm(FlaskForm):
|
||||
|
||||
gm_level = IntegerField(
|
||||
'GM Level',
|
||||
widget=NumberInput(min = 0, max = 9)
|
||||
)
|
||||
|
||||
submit = SubmitField('Submit')
|
||||
|
||||
|
||||
class ResolveBugReportForm(FlaskForm):
|
||||
|
||||
resolution = StringField(
|
||||
'Resolution',
|
||||
widget=TextArea(),
|
||||
validators=[DataRequired()]
|
||||
)
|
||||
|
||||
submit = SubmitField('Submit')
|
||||
|
||||
|
||||
class SendMailForm(FlaskForm):
|
||||
|
||||
recipient = SelectField(
|
||||
'Recipient: ',
|
||||
coerce=str,
|
||||
choices=[
|
||||
("",""),
|
||||
("0","All Characters"),
|
||||
],
|
||||
validators=[validators.DataRequired()]
|
||||
)
|
||||
|
||||
subject = StringField(
|
||||
'Subject',
|
||||
validators=[validators.DataRequired()]
|
||||
)
|
||||
|
||||
body = StringField(
|
||||
'Body',
|
||||
widget=TextArea(),
|
||||
validators=[validators.DataRequired()]
|
||||
)
|
||||
|
||||
attachment = SelectField(
|
||||
"Attachment",
|
||||
coerce=str,
|
||||
choices=[(0,"No Attachment")]
|
||||
)
|
||||
|
||||
attachment_count = IntegerField(
|
||||
'Attachment Count',
|
||||
default=0
|
||||
)
|
||||
|
||||
submit = SubmitField('Submit')
|
98
app/log.py
Normal file
98
app/log.py
Normal file
@ -0,0 +1,98 @@
|
||||
from flask import render_template, Blueprint, request, url_for
|
||||
from flask_user import login_required, current_user
|
||||
from app.models import CommandLog, ActivityLog, db, Account, CharacterInfo
|
||||
from datatables import ColumnDT, DataTables
|
||||
import time
|
||||
from app import gm_level
|
||||
|
||||
log_blueprint = Blueprint('log', __name__)
|
||||
|
||||
@log_blueprint.route('/activities', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(8)
|
||||
def activity():
|
||||
return render_template('logs/activity.html.j2')
|
||||
|
||||
|
||||
@log_blueprint.route('/commands', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(8)
|
||||
def command():
|
||||
return render_template('logs/command.html.j2')
|
||||
|
||||
|
||||
@log_blueprint.route('/get_activities', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(8)
|
||||
def get_activities():
|
||||
columns = [
|
||||
ColumnDT(ActivityLog.id), # 0
|
||||
ColumnDT(ActivityLog.character_id), # 1
|
||||
ColumnDT(ActivityLog.activity), # 2
|
||||
ColumnDT(ActivityLog.time), # 3
|
||||
ColumnDT(ActivityLog.map_id), # 4
|
||||
]
|
||||
|
||||
query = db.session.query().select_from(ActivityLog)
|
||||
|
||||
params = request.args.to_dict()
|
||||
|
||||
rowTable = DataTables(params, query, columns)
|
||||
|
||||
data = rowTable.output_result()
|
||||
for activity in data["data"]:
|
||||
char_id = activity["1"]
|
||||
activity["1"] = f"""
|
||||
<a role="button" class="btn btn-primary btn btn-block"
|
||||
href='{url_for('characters.view', id=char_id)}'>
|
||||
View Character: {CharacterInfo.query.filter(CharacterInfo.id==char_id).first().name}
|
||||
</a>
|
||||
<a role="button" class="btn btn-primary btn btn-block"
|
||||
href='{url_for('accounts.view', id=CharacterInfo.query.filter(CharacterInfo.id==char_id).first().account_id)}'>
|
||||
View Account: {Account.query.filter(Account.id==CharacterInfo.query.filter(CharacterInfo.id==char_id).first().account_id).first().username}
|
||||
</a>
|
||||
"""
|
||||
|
||||
if activity["2"] == 0:
|
||||
activity["2"] = "Entered World"
|
||||
elif activity["2"] == 1:
|
||||
activity["2"] = "Left World"
|
||||
|
||||
activity["3"] = time.ctime(activity["3"])
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@log_blueprint.route('/get_commands', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(8)
|
||||
def get_commands():
|
||||
columns = [
|
||||
ColumnDT(CommandLog.id), # 0
|
||||
ColumnDT(CommandLog.character_id), # 1
|
||||
ColumnDT(CommandLog.command), # 2
|
||||
]
|
||||
|
||||
query = db.session.query().select_from(CommandLog)
|
||||
|
||||
params = request.args.to_dict()
|
||||
|
||||
rowTable = DataTables(params, query, columns)
|
||||
|
||||
data = rowTable.output_result()
|
||||
for command in data["data"]:
|
||||
char_id = command["1"]
|
||||
command["1"] = f"""
|
||||
<a role="button" class="btn btn-primary btn btn-block"
|
||||
href='{url_for('characters.view', id=char_id)}'>
|
||||
View Character: {CharacterInfo.query.filter(CharacterInfo.id==command['1']).first().name}
|
||||
</a>
|
||||
"""
|
||||
command["1"] += f"""
|
||||
<a role="button" class="btn btn-primary btn btn-block"
|
||||
href='{url_for('accounts.view', id=CharacterInfo.query.filter(CharacterInfo.id==char_id).first().account_id)}'>
|
||||
View Account: {Account.query.filter(Account.id==CharacterInfo.query.filter(CharacterInfo.id==char_id).first().account_id).first().username}
|
||||
</a>
|
||||
"""
|
||||
|
||||
return data
|
346
app/luclient.py
Normal file
346
app/luclient.py
Normal file
@ -0,0 +1,346 @@
|
||||
from flask import (
|
||||
Blueprint,
|
||||
send_file,
|
||||
g,
|
||||
redirect,
|
||||
url_for
|
||||
)
|
||||
from flask_user import login_required
|
||||
from app.models import CharacterInfo
|
||||
import glob
|
||||
import os
|
||||
from wand import image
|
||||
from wand.exceptions import BlobError as BE
|
||||
|
||||
import sqlite3
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
luclient_blueprint = Blueprint('luclient', __name__)
|
||||
locale = {}
|
||||
|
||||
@luclient_blueprint.route('/get_dds_as_png/<filename>')
|
||||
@login_required
|
||||
def get_dds_as_png(filename):
|
||||
if filename.split('.')[-1] != 'dds':
|
||||
return (404, "NO")
|
||||
|
||||
cache = f'cache/{filename.split(".")[0]}.png'
|
||||
|
||||
if not os.path.exists("app/" + cache):
|
||||
root = 'app/luclient/res/'
|
||||
|
||||
path = glob.glob(
|
||||
root + f'**/{filename}',
|
||||
recursive=True
|
||||
)[0]
|
||||
|
||||
with image.Image(filename=path) as img:
|
||||
img.compression = "no"
|
||||
img.save(filename='app/cache/'+filename.split('.')[0] + '.png')
|
||||
|
||||
return send_file(cache)
|
||||
|
||||
|
||||
@luclient_blueprint.route('/get_dds/<filename>')
|
||||
@login_required
|
||||
def get_dds(filename):
|
||||
if filename.split('.')[-1] != 'dds':
|
||||
return 404
|
||||
|
||||
root = 'app/luclient/res/'
|
||||
|
||||
dds = glob.glob(
|
||||
root + f'**/{filename}',
|
||||
recursive=True
|
||||
)[0]
|
||||
|
||||
return send_file(dds)
|
||||
|
||||
|
||||
@luclient_blueprint.route('/get_icon_lot/<id>')
|
||||
@login_required
|
||||
def get_icon_lot(id):
|
||||
|
||||
render_component_id = query_cdclient(
|
||||
'select component_id from ComponentsRegistry where component_type = 2 and id = ?',
|
||||
[id],
|
||||
one=True
|
||||
)[0]
|
||||
|
||||
# find the asset from rendercomponent given the component id
|
||||
filename = query_cdclient('select icon_asset from RenderComponent where id = ?',
|
||||
[render_component_id],
|
||||
one=True
|
||||
)[0]
|
||||
|
||||
filename = filename.replace("..\\", "").replace("\\", "/")
|
||||
|
||||
cache = f'cache/{filename.split("/")[-1].split(".")[0]}.png'
|
||||
|
||||
if not os.path.exists("app/" + cache):
|
||||
root = 'app/luclient/res/'
|
||||
try:
|
||||
|
||||
with image.Image(filename=f'{root}{filename}'.lower()) as img:
|
||||
img.compression = "no"
|
||||
img.save(filename=f'app/cache/{filename.split("/")[-1].split(".")[0]}.png')
|
||||
except BE:
|
||||
return redirect(url_for('luclient.unknown'))
|
||||
|
||||
return send_file(cache)
|
||||
|
||||
|
||||
@luclient_blueprint.route('/get_icon_iconid/<id>')
|
||||
@login_required
|
||||
def get_icon_iconid(id):
|
||||
|
||||
filename = query_cdclient(
|
||||
'select IconPath from Icons where IconID = ?',
|
||||
[id],
|
||||
one=True
|
||||
)[0]
|
||||
|
||||
filename = filename.replace("..\\", "").replace("\\", "/")
|
||||
|
||||
cache = f'cache/{filename.split("/")[-1].split(".")[0]}.png'
|
||||
|
||||
if not os.path.exists("app/" + cache):
|
||||
root = 'app/luclient/res/'
|
||||
try:
|
||||
|
||||
with image.Image(filename=f'{root}{filename}'.lower()) as img:
|
||||
img.compression = "no"
|
||||
img.save(filename=f'app/cache/{filename.split("/")[-1].split(".")[0]}.png')
|
||||
except BE:
|
||||
return redirect(url_for('luclient.unknown'))
|
||||
|
||||
return send_file(cache)
|
||||
|
||||
@luclient_blueprint.route('/unknown')
|
||||
@login_required
|
||||
def unknown():
|
||||
filename = "textures/ui/inventory/unknown.dds"
|
||||
|
||||
cache = f'cache/{filename.split("/")[-1].split(".")[0]}.png'
|
||||
|
||||
if not os.path.exists("app/" + cache):
|
||||
root = 'app/luclient/res/'
|
||||
|
||||
with image.Image(filename=f'{root}{filename}'.lower()) as img:
|
||||
img.compression = "no"
|
||||
img.save(filename=f'app/cache/{filename.split("/")[-1].split(".")[0]}.png')
|
||||
|
||||
|
||||
return send_file(cache)
|
||||
|
||||
|
||||
def get_cdclient():
|
||||
"""Connect to CDClient from file system Relative Path
|
||||
|
||||
Args:
|
||||
None
|
||||
"""
|
||||
cdclient = getattr(g, '_cdclient', None)
|
||||
if cdclient is None:
|
||||
cdclient = g._database = sqlite3.connect('app/luclient/res/cdclient.sqlite')
|
||||
return cdclient
|
||||
|
||||
|
||||
def query_cdclient(query, args=(), one=False):
|
||||
"""Run sql queries on CDClient
|
||||
|
||||
Args:
|
||||
query (string) : SQL query
|
||||
args (list) : List of args to place in query
|
||||
one (bool) : Return only on result or all results
|
||||
"""
|
||||
cur = get_cdclient().execute(query, args)
|
||||
rv = cur.fetchall()
|
||||
cur.close()
|
||||
return (rv[0] if rv else None) if one else rv
|
||||
|
||||
|
||||
def translate_from_locale(trans_string):
|
||||
"""Finds the string translation from locale.xml
|
||||
|
||||
Args:
|
||||
trans_string (string) : ID to find translation
|
||||
"""
|
||||
if not trans_string:
|
||||
return "INVALID STRING"
|
||||
|
||||
global locale
|
||||
|
||||
locale_data = ""
|
||||
|
||||
if not locale:
|
||||
locale_path = "app/luclient/locale/locale.xml"
|
||||
|
||||
with open(locale_path, 'r') as file:
|
||||
locale_data = file.read()
|
||||
locale_xml = ET.XML(locale_data)
|
||||
for item in locale_xml.findall('.//phrase'):
|
||||
translation = ""
|
||||
for translation_item in item.findall('.//translation'):
|
||||
if translation_item.attrib["locale"] == "en_US":
|
||||
translation = translation_item.text
|
||||
|
||||
locale[item.attrib['id']] = translation
|
||||
|
||||
if trans_string in locale:
|
||||
return locale[trans_string]
|
||||
else:
|
||||
return trans_string
|
||||
|
||||
def register_luclient_jinja_helpers(app):
|
||||
|
||||
@app.template_filter('get_zone_name')
|
||||
def get_zone_name(zone_id):
|
||||
return translate_from_locale(f'ZoneTable_{zone_id}_DisplayDescription')
|
||||
|
||||
@app.template_filter('get_skill_desc')
|
||||
def get_skill_desc(skill_id):
|
||||
return translate_from_locale(f'SkillBehavior_{skill_id}_descriptionUI').replace(
|
||||
"%(DamageCombo)", "Damage Combo: "
|
||||
).replace(
|
||||
"%(AltCombo)", "<br/>Skeleton Combo: "
|
||||
).replace(
|
||||
"%(Description)", "<br/>"
|
||||
).replace(
|
||||
"%(ChargeUp)", "<br/>Charge-up: "
|
||||
)
|
||||
|
||||
@app.template_filter('parse_lzid')
|
||||
def parse_lzid(lzid):
|
||||
return[
|
||||
(int(lzid) & ((1 << 16) - 1)),
|
||||
((int(lzid) >> 16) & ((1 << 16) - 1)),
|
||||
((int(lzid) >> 32) & ((1 << 30) - 1))
|
||||
]
|
||||
|
||||
@app.template_filter('parse_other_player_id')
|
||||
def parse_other_player_id(other_player_id):
|
||||
char_id = (int(other_player_id) & 0xFFFFFFFF)
|
||||
character = CharacterInfo.query.filter(CharacterInfo.id == char_id).first()
|
||||
if character:
|
||||
return[character.id, character.name]
|
||||
else:
|
||||
return None
|
||||
|
||||
@app.template_filter('get_lot_name')
|
||||
def get_lot_name(lot_id):
|
||||
name = translate_from_locale(f'Objects_{lot_id}_name')
|
||||
if name == translate_from_locale(f'Objects_{lot_id}_name'):
|
||||
intermed = query_cdclient(
|
||||
'select * from Objects where id = ?',
|
||||
[lot_id],
|
||||
one=True
|
||||
)
|
||||
name = intermed[7] if (intermed[7] != "None" and intermed[7] !="" and intermed[7] != None) else intermed[1]
|
||||
return name
|
||||
|
||||
@app.template_filter('get_lot_rarity')
|
||||
def get_lot_rarity(lot_id):
|
||||
|
||||
render_component_id = query_cdclient(
|
||||
'select component_id from ComponentsRegistry where component_type = 2 and id = ?',
|
||||
[lot_id],
|
||||
one=True
|
||||
)[0]
|
||||
|
||||
rarity = query_cdclient('select rarity from ItemComponent where id = ?',
|
||||
[render_component_id],
|
||||
one=True
|
||||
)
|
||||
if rarity:
|
||||
rarity = rarity[0]
|
||||
return rarity
|
||||
|
||||
@app.template_filter('get_lot_desc')
|
||||
def get_lot_desc(lot_id):
|
||||
desc = translate_from_locale(f'Objects_{lot_id}_description')
|
||||
if desc == f'Objects_{lot_id}_description':
|
||||
desc = query_cdclient(
|
||||
'select description from Objects where id = ?',
|
||||
[lot_id],
|
||||
one=True
|
||||
)[0]
|
||||
if desc in ("", None):
|
||||
desc = None
|
||||
return desc
|
||||
|
||||
@app.template_filter('get_item_set')
|
||||
def check_if_in_set(lot_id):
|
||||
item_set = query_cdclient(
|
||||
'select * from ItemSets where itemIDs like ? or itemIDs like ? or itemIDs like ?',
|
||||
[f'{lot_id}%', f'%, {lot_id}%', f'%,{lot_id}%'],
|
||||
one=True
|
||||
)
|
||||
if item_set in ("", None):
|
||||
return None
|
||||
else:
|
||||
return item_set
|
||||
|
||||
@app.template_filter('get_lot_stats')
|
||||
def get_lot_stats(lot_id):
|
||||
stats = query_cdclient(
|
||||
'SELECT imBonusUI, lifeBonusUI, armorBonusUI, skillID, skillIcon FROM SkillBehavior WHERE skillID IN (\
|
||||
SELECT skillID FROM ObjectSkills WHERE objectTemplate=?\
|
||||
)',
|
||||
[lot_id]
|
||||
)
|
||||
|
||||
return consolidate_stats(stats)
|
||||
|
||||
|
||||
@app.template_filter('get_set_stats')
|
||||
def get_set_stats(lot_id):
|
||||
stats = query_cdclient(
|
||||
'SELECT imBonusUI, lifeBonusUI, armorBonusUI, skillID, skillIcon FROM SkillBehavior WHERE skillID IN (\
|
||||
SELECT skillID FROM ItemSetSkills WHERE SkillSetID=?\
|
||||
)',
|
||||
[lot_id]
|
||||
)
|
||||
|
||||
return consolidate_stats(stats)
|
||||
|
||||
@app.template_filter('query_cdclient')
|
||||
def jinja_query_cdclient(query, items):
|
||||
print(query, items)
|
||||
return query_cdclient(
|
||||
query,
|
||||
items,
|
||||
one=True
|
||||
)[0]
|
||||
|
||||
@app.template_filter('lu_translate')
|
||||
def lu_translate(to_translate):
|
||||
return translate_from_locale(to_translate)
|
||||
|
||||
|
||||
def consolidate_stats(stats):
|
||||
|
||||
if len(stats) > 1:
|
||||
consolidated_stats = {"im": 0,"life": 0,"armor": 0, "skill": []}
|
||||
for stat in stats:
|
||||
if stat[0]:
|
||||
consolidated_stats["im"] += stat[0]
|
||||
if stat[1]:
|
||||
consolidated_stats["life"] += stat[1]
|
||||
if stat[2]:
|
||||
consolidated_stats["armor"] += stat[2]
|
||||
if stat[3]:
|
||||
consolidated_stats["skill"].append([stat[3],stat[4]])
|
||||
|
||||
|
||||
stats = consolidated_stats
|
||||
elif len(stats) == 1:
|
||||
stats = {
|
||||
"im": stats[0][0] if stats[0][0] else 0,
|
||||
"life": stats[0][1] if stats[0][1] else 0,
|
||||
"armor": stats[0][2] if stats[0][2] else 0,
|
||||
"skill": [[stats[0][3], stats[0][4]]] if stats[0][3] else None,
|
||||
}
|
||||
else:
|
||||
stats = None
|
||||
return stats
|
85
app/mail.py
Normal file
85
app/mail.py
Normal file
@ -0,0 +1,85 @@
|
||||
from flask import render_template, Blueprint, redirect, url_for, request, abort, flash, request
|
||||
from flask_user import login_required, current_user
|
||||
from app.models import db, Mail, CharacterInfo
|
||||
from datatables import ColumnDT, DataTables
|
||||
from app.forms import SendMailForm
|
||||
from app import gm_level
|
||||
from app.luclient import translate_from_locale, query_cdclient
|
||||
import time
|
||||
|
||||
mail_blueprint = Blueprint('mail', __name__)
|
||||
|
||||
|
||||
@mail_blueprint.route('/view/<id>', methods=['GET'])
|
||||
@login_required
|
||||
def view(id):
|
||||
mail = Mail.query.filter(Mail.id == id).first()
|
||||
|
||||
return render_template('mail/view.html.j2', mail=mail)
|
||||
|
||||
|
||||
@mail_blueprint.route('/send', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
def send():
|
||||
form = SendMailForm()
|
||||
|
||||
if request.method == "POST":
|
||||
# if form.validate_on_submit():
|
||||
if form.attachment.data != "0" and form.attachment_count.data == 0:
|
||||
form.attachment_count.data = 1
|
||||
if form.recipient.data == "0":
|
||||
for character in CharacterInfo.query.all():
|
||||
Mail(
|
||||
sender_id = 0,
|
||||
sender_name = f"[GM] {current_user.username}",
|
||||
receiver_id = character.id,
|
||||
receiver_name = character.name,
|
||||
time_sent = time.time(),
|
||||
subject = form.subject.data,
|
||||
body = form.body.data,
|
||||
attachment_id = 0,
|
||||
attachment_lot = form.attachment.data,
|
||||
attachment_count = form.attachment_count.data
|
||||
).save()
|
||||
else:
|
||||
Mail(
|
||||
sender_id = 0,
|
||||
sender_name = f"[GM] {current_user.username}",
|
||||
receiver_id = form.recipient.data,
|
||||
receiver_name = CharacterInfo.query.filter(CharacterInfo.id == form.recipient.data).first().name,
|
||||
time_sent = time.time(),
|
||||
subject = form.subject.data,
|
||||
body = form.body.data,
|
||||
attachment_id = 0,
|
||||
attachment_lot = form.attachment.data,
|
||||
attachment_count = form.attachment_count.data
|
||||
).save()
|
||||
|
||||
flash("Sent Mail", "success")
|
||||
return redirect(url_for('mail.send'))
|
||||
|
||||
|
||||
recipients = CharacterInfo.query.all()
|
||||
for character in recipients:
|
||||
form.recipient.choices.append((character.id, character.name))
|
||||
|
||||
items = query_cdclient(
|
||||
'Select id, name, displayName from Objects where type = ?',
|
||||
["Loot"]
|
||||
)
|
||||
|
||||
for item in items:
|
||||
name = translate_from_locale(f'Objects_{item[0]}_name')
|
||||
if name == f'Objects_{item[0]}_name':
|
||||
name = (item[2] if (item[2] != "None" and item[2] !="" and item[2] != None) else item[1])
|
||||
form.attachment.choices.append(
|
||||
(
|
||||
item[0],
|
||||
f'({item[0]}) {name}'
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
return render_template('mail/send.html.j2', form=form)
|
||||
|
42
app/main.py
Normal file
42
app/main.py
Normal file
@ -0,0 +1,42 @@
|
||||
from flask import render_template, Blueprint, redirect, request, send_from_directory, make_response, send_file
|
||||
from flask_user import login_required, current_user
|
||||
import json, glob, os
|
||||
from wand import image
|
||||
|
||||
from app.models import Account, AccountInvitation, CharacterInfo
|
||||
from app.schemas import AccountSchema, CharacterInfoSchema
|
||||
from app.luclient import query_cdclient
|
||||
|
||||
main_blueprint = Blueprint('main', __name__)
|
||||
|
||||
account_schema = AccountSchema()
|
||||
char_info_schema = CharacterInfoSchema()
|
||||
|
||||
@main_blueprint.route('/', methods=['GET'])
|
||||
def index():
|
||||
"""Home/Index Page"""
|
||||
if current_user.is_authenticated:
|
||||
|
||||
account_data = Account.query.filter(Account.id == current_user.id).first()
|
||||
|
||||
return render_template(
|
||||
'main/index.html.j2',
|
||||
account_data=account_data
|
||||
)
|
||||
else:
|
||||
return render_template('main/index.html.j2')
|
||||
|
||||
|
||||
@main_blueprint.route('/about')
|
||||
def about():
|
||||
"""About Page"""
|
||||
return render_template('main/about.html.j2')
|
||||
|
||||
|
||||
@main_blueprint.route('/favicon.ico')
|
||||
def favicon():
|
||||
return send_from_directory(
|
||||
'static/logo/',
|
||||
'favicon.ico',
|
||||
mimetype='image/vnd.microsoft.icon'
|
||||
)
|
1010
app/models.py
Normal file
1010
app/models.py
Normal file
File diff suppressed because it is too large
Load Diff
108
app/moderation.py
Normal file
108
app/moderation.py
Normal file
@ -0,0 +1,108 @@
|
||||
from flask import render_template, Blueprint, redirect, url_for, request, abort, flash
|
||||
from flask_user import login_required
|
||||
from app.models import PetNames, db
|
||||
from datatables import ColumnDT, DataTables
|
||||
from app.forms import CreatePlayKeyForm, EditPlayKeyForm
|
||||
from app import gm_level
|
||||
|
||||
moderation_blueprint = Blueprint('moderation', __name__)
|
||||
|
||||
|
||||
@moderation_blueprint.route('/<status>', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
def index(status):
|
||||
return render_template('moderation/index.html.j2', status=status)
|
||||
|
||||
|
||||
@moderation_blueprint.route('/approve_pet/<id>', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
def approve_pet(id):
|
||||
|
||||
pet_data = PetNames.query.filter(PetNames.id == id).first()
|
||||
|
||||
pet_data.approved = 2
|
||||
flash(f"Approved pet name {pet_data.pet_name}", "success")
|
||||
pet_data.save()
|
||||
return redirect(request.referrer if request.referrer else url_for("main.index"))
|
||||
|
||||
|
||||
@moderation_blueprint.route('/reject_pet/<id>', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
def reject_pet(id):
|
||||
|
||||
pet_data = PetNames.query.filter(PetNames.id == id).first()
|
||||
|
||||
pet_data.approved = 0
|
||||
flash(f"Rejected pet name {pet_data.pet_name}", "danger")
|
||||
pet_data.save()
|
||||
return redirect(request.referrer if request.referrer else url_for("main.index"))
|
||||
|
||||
|
||||
@moderation_blueprint.route('/get_pets/<status>', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
def get_pets(status="all"):
|
||||
columns = [
|
||||
ColumnDT(PetNames.id),
|
||||
ColumnDT(PetNames.pet_name),
|
||||
ColumnDT(PetNames.approved),
|
||||
]
|
||||
|
||||
query = None
|
||||
if status=="all":
|
||||
query = db.session.query().select_from(PetNames)
|
||||
elif status=="approved":
|
||||
query = db.session.query().select_from(PetNames).filter(PetNames.approved==2)
|
||||
elif status=="unapproved":
|
||||
query = db.session.query().select_from(PetNames).filter(PetNames.approved==1)
|
||||
else:
|
||||
raise Exception("Not a valid filter")
|
||||
|
||||
|
||||
params = request.args.to_dict()
|
||||
|
||||
rowTable = DataTables(params, query, columns)
|
||||
|
||||
data = rowTable.output_result()
|
||||
for pet_data in data["data"]:
|
||||
id = pet_data["0"]
|
||||
status = pet_data["2"]
|
||||
if status == 1:
|
||||
pet_data["0"] = f"""
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<a role="button" class="btn btn-success btn btn-block"
|
||||
href='{url_for('moderation.approve_pet', id=id)}'>
|
||||
Approve
|
||||
</a>
|
||||
</div>
|
||||
<div class="col">
|
||||
<a role="button" class="btn btn-danger btn btn-block"
|
||||
href='{url_for('moderation.reject_pet', id=id)}'>
|
||||
Reject
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
pet_data["2"] = "Awaiting Moderation"
|
||||
elif status == 2:
|
||||
pet_data["0"] = f"""
|
||||
<a role="button" class="btn btn-danger btn btn-block"
|
||||
href='{url_for('moderation.reject_pet', id=id)}'>
|
||||
Reject
|
||||
</a>
|
||||
"""
|
||||
pet_data["2"] = "<span class='text-success'>Approved</span>"
|
||||
elif status == 0:
|
||||
pet_data["0"] = f"""
|
||||
<a role="button" class="btn btn-success btn btn-block"
|
||||
href='{url_for('moderation.approve_pet', id=id)}'>
|
||||
Approve
|
||||
</a>
|
||||
"""
|
||||
pet_data["2"] = "<span class='text-danger'>Rejected</span>"
|
||||
|
||||
return data
|
150
app/play_keys.py
Normal file
150
app/play_keys.py
Normal file
@ -0,0 +1,150 @@
|
||||
from flask import render_template, Blueprint, redirect, url_for, request, abort, flash
|
||||
from flask_user import login_required, current_user
|
||||
from app.models import Account, AccountInvitation, PlayKey, db
|
||||
from datatables import ColumnDT, DataTables
|
||||
from app.forms import CreatePlayKeyForm, EditPlayKeyForm
|
||||
from app import gm_level
|
||||
|
||||
play_keys_blueprint = Blueprint('play_keys', __name__)
|
||||
|
||||
# Key creation page
|
||||
|
||||
@play_keys_blueprint.route('/', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(9)
|
||||
def index():
|
||||
return render_template('play_keys/index.html.j2')
|
||||
|
||||
|
||||
@play_keys_blueprint.route('/create/<count>/<uses>', methods=['GET'], defaults={'count': 1, 'uses': 1})
|
||||
@login_required
|
||||
@gm_level(9)
|
||||
def create(count=1, uses=1):
|
||||
PlayKey.create(count=count, uses=uses)
|
||||
flash(f"Created {count} Play Key(s) with {uses} uses!", "success")
|
||||
return redirect(url_for('play_keys.index'))
|
||||
|
||||
|
||||
@play_keys_blueprint.route('/create/bulk', methods=('GET', 'POST'))
|
||||
@login_required
|
||||
@gm_level(9)
|
||||
def bulk_create():
|
||||
form = CreatePlayKeyForm()
|
||||
if form.validate_on_submit():
|
||||
PlayKey.create(count=form.count.data, uses=form.uses.data)
|
||||
return redirect(url_for('play_keys.index'))
|
||||
|
||||
return render_template('play_keys/bulk.html.j2', form=form)
|
||||
|
||||
|
||||
@play_keys_blueprint.route('/delete/<id>', methods=('GET', 'POST'))
|
||||
@login_required
|
||||
@gm_level(9)
|
||||
def delete(id):
|
||||
key = PlayKey.query.filter(PlayKey.id == id).first()
|
||||
associated_accounts = Account.query.filter(Account.play_key_id==id).all()
|
||||
flash(f"Deleted Play Key {key.key_string}", "danger")
|
||||
key.delete()
|
||||
return redirect(url_for('play_keys.index'))
|
||||
|
||||
|
||||
@play_keys_blueprint.route('/edit/<id>', methods=('GET', 'POST'))
|
||||
@login_required
|
||||
@gm_level(9)
|
||||
def edit(id):
|
||||
key = PlayKey.query.filter(PlayKey.id==id).first()
|
||||
form = EditPlayKeyForm()
|
||||
|
||||
if form.validate_on_submit():
|
||||
key.key_uses = form.uses.data
|
||||
key.active = form.active.data
|
||||
key.notes = form.notes.data
|
||||
key.save()
|
||||
return redirect(url_for('play_keys.index'))
|
||||
|
||||
form.uses.data = key.key_uses
|
||||
form.active.data = key.active
|
||||
form.notes.data = key.notes
|
||||
|
||||
return render_template('play_keys/edit.html.j2', form=form, key=key)
|
||||
|
||||
|
||||
@play_keys_blueprint.route('/view/<id>', methods=('GET', 'POST'))
|
||||
@login_required
|
||||
@gm_level(9)
|
||||
def view(id):
|
||||
key = PlayKey.query.filter(PlayKey.id == id).first()
|
||||
accounts = Account.query.filter(Account.play_key_id==id).all()
|
||||
return render_template('play_keys/view.html.j2', key=key, accounts=accounts)
|
||||
|
||||
|
||||
@play_keys_blueprint.route('/get', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(9)
|
||||
def get():
|
||||
columns = [
|
||||
ColumnDT(PlayKey.id),
|
||||
ColumnDT(PlayKey.key_string),
|
||||
ColumnDT(PlayKey.key_uses),
|
||||
ColumnDT(PlayKey.times_used),
|
||||
ColumnDT(PlayKey.created_at),
|
||||
ColumnDT(PlayKey.active),
|
||||
]
|
||||
|
||||
query = db.session.query().select_from(PlayKey)
|
||||
|
||||
params = request.args.to_dict()
|
||||
|
||||
rowTable = DataTables(params, query, columns)
|
||||
|
||||
data = rowTable.output_result()
|
||||
for play_key in data["data"]:
|
||||
# Hackily shove buttons into response
|
||||
play_key["0"] = f"""
|
||||
<a role="button" class="btn btn-primary btn btn-block"
|
||||
href='{url_for('play_keys.view', id=play_key["0"])}'>
|
||||
View
|
||||
</a>
|
||||
|
||||
<a role="button" class="btn btn-secondary btn btn-block"
|
||||
href='{url_for('play_keys.edit', id=play_key["0"])}'>
|
||||
Edit
|
||||
</a>
|
||||
|
||||
<a type="button" class="btn btn-danger btn-block" data-toggle="modal" data-target="#delete-{play_key["1"]}-modal">
|
||||
Delete
|
||||
</a>
|
||||
|
||||
<div class="modal fade bd-example-modal-lg" id="delete-{play_key["1"]}-modal" tabindex="-1" role="dialog" aria-labelledby="delete-{play_key["1"]}-modalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content bg-dark border-primary">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="delemodalLabel">Delete Play Key {play_key["1"]} ?</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body text-danger">
|
||||
Are you sure you want to delete the Play Key {play_key["1"]} ? </br></br>
|
||||
This will not delete accounts that have used this key, but they may be unable to play.
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" data-dismiss="modal">Close</button>
|
||||
<a href='{url_for('play_keys.delete', id=play_key["0"])}'
|
||||
class="btn btn-danger"
|
||||
role="button"
|
||||
aria-disabled="true">
|
||||
Delete
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
if play_key["5"]:
|
||||
play_key["5"] = '''<h1 class="far fa-check-square text-success"></h1>'''
|
||||
else:
|
||||
play_key["5"] = '''<h1 class="far fa-times-circle text-danger"></h1>'''
|
||||
|
||||
return data
|
387
app/properties.py
Normal file
387
app/properties.py
Normal file
@ -0,0 +1,387 @@
|
||||
from flask import (
|
||||
render_template,
|
||||
Blueprint,
|
||||
redirect,
|
||||
url_for,
|
||||
request,
|
||||
abort,
|
||||
jsonify,
|
||||
send_from_directory,
|
||||
make_response,
|
||||
flash
|
||||
)
|
||||
from flask_user import login_required, current_user
|
||||
import json
|
||||
from datatables import ColumnDT, DataTables
|
||||
import time
|
||||
from app.models import Property, db, UGC, CharacterInfo, PropertyContent, Account
|
||||
from app.schemas import PropertySchema
|
||||
from app import gm_level
|
||||
from app.luclient import query_cdclient
|
||||
|
||||
import zlib
|
||||
import xmltodict
|
||||
import os
|
||||
import app.pylddlib as ldd
|
||||
|
||||
property_blueprint = Blueprint('properties', __name__)
|
||||
|
||||
property_schema = PropertySchema()
|
||||
|
||||
@property_blueprint.route('/', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
def index():
|
||||
return render_template('properties/index.html.j2')
|
||||
|
||||
|
||||
@property_blueprint.route('/approve/<id>', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
def approve(id):
|
||||
|
||||
property_data = Property.query.filter(Property.id == id).first()
|
||||
|
||||
property_data.mod_approved = not property_data.mod_approved
|
||||
|
||||
# If we approved it, clear the rejection reason
|
||||
if property_data.mod_approved:
|
||||
property_data.rejection_reason = ""
|
||||
|
||||
if property_data.mod_approved:
|
||||
flash(
|
||||
f"""Approved Property
|
||||
{property_data.name if property_data.name else query_cdclient(
|
||||
'select DisplayDescription from ZoneTable where zoneID = ?',
|
||||
[property_data.zone_id],
|
||||
one=True
|
||||
)[0]}
|
||||
from {CharacterInfo.query.filter(CharacterInfo.id==property_data.owner_id).first().name}""",
|
||||
"success"
|
||||
)
|
||||
else:
|
||||
flash(
|
||||
f"""Unapproved Property
|
||||
{property_data.name if property_data.name else query_cdclient(
|
||||
'select DisplayDescription from ZoneTable where zoneID = ?',
|
||||
[property_data.zone_id],
|
||||
one=True
|
||||
)[0]}
|
||||
from {CharacterInfo.query.filter(CharacterInfo.id==property_data.owner_id).first().name}""",
|
||||
"danger"
|
||||
)
|
||||
|
||||
property_data.save()
|
||||
|
||||
go_to = ""
|
||||
|
||||
if request.referrer:
|
||||
if "view_models" in request.referrer:
|
||||
go_to = url_for('properties.view', id=id)
|
||||
else:
|
||||
go_to = request.referrer
|
||||
else:
|
||||
go_to = url_for('main.index')
|
||||
|
||||
|
||||
|
||||
return redirect(go_to)
|
||||
|
||||
|
||||
@property_blueprint.route('/view/<id>', methods=['GET'])
|
||||
@login_required
|
||||
def view(id):
|
||||
|
||||
property_data = Property.query.filter(Property.id == id).first()
|
||||
|
||||
if current_user.gm_level < 3:
|
||||
if property_data.owner_id and property_data.owner.account_id != current_user.id:
|
||||
abort(403)
|
||||
return
|
||||
|
||||
if property_data == {}:
|
||||
abort(404)
|
||||
return
|
||||
|
||||
return render_template('properties/view.html.j2', property_data=property_data)
|
||||
|
||||
|
||||
@property_blueprint.route('/get/<status>', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
def get(status="all"):
|
||||
columns = [
|
||||
ColumnDT(Property.id), # 0
|
||||
ColumnDT(CharacterInfo.name), # 1
|
||||
ColumnDT(Property.template_id), # 2
|
||||
ColumnDT(Property.clone_id), # 3
|
||||
ColumnDT(Property.name), # 4
|
||||
ColumnDT(Property.description), # 5
|
||||
ColumnDT(Property.privacy_option), # 6
|
||||
ColumnDT(Property.mod_approved), # 7
|
||||
ColumnDT(Property.last_updated), # 8
|
||||
ColumnDT(Property.time_claimed), # 9
|
||||
ColumnDT(Property.rejection_reason), # 10
|
||||
ColumnDT(Property.reputation), # 11
|
||||
ColumnDT(Property.zone_id), # 12
|
||||
ColumnDT(Account.username) # 13
|
||||
]
|
||||
|
||||
query = None
|
||||
if status=="all":
|
||||
query = db.session.query().select_from(Property).join(CharacterInfo, CharacterInfo.id==Property.owner_id).join(Account)
|
||||
elif status=="approved":
|
||||
query = db.session.query().select_from(Property).join(CharacterInfo, CharacterInfo.id==Property.owner_id).join(Account).filter(Property.mod_approved==True)
|
||||
elif status=="unapproved":
|
||||
query = db.session.query().select_from(Property).join(CharacterInfo, CharacterInfo.id==Property.owner_id).join(Account).filter(Property.mod_approved==False)
|
||||
else:
|
||||
raise Exception("Not a valid filter")
|
||||
|
||||
|
||||
params = request.args.to_dict()
|
||||
|
||||
rowTable = DataTables(params, query, columns)
|
||||
|
||||
data = rowTable.output_result()
|
||||
for property_data in data["data"]:
|
||||
id = property_data["0"]
|
||||
|
||||
property_data["0"] = f"""
|
||||
<a role="button" class="btn btn-primary btn btn-block"
|
||||
href='{url_for('properties.view', id=id)}'>
|
||||
View
|
||||
</a>
|
||||
"""
|
||||
|
||||
if not property_data["7"]:
|
||||
property_data["0"] += f"""
|
||||
<a role="button" class="btn btn-success btn btn-block"
|
||||
href='{url_for('properties.approve', id=id)}'>
|
||||
Approve
|
||||
</a>
|
||||
"""
|
||||
else:
|
||||
property_data["0"] += f"""
|
||||
<a role="button" class="btn btn-danger btn btn-block"
|
||||
href='{url_for('properties.approve', id=id)}'>
|
||||
Unapprove
|
||||
</a>
|
||||
"""
|
||||
|
||||
property_data["1"] = f"""
|
||||
<a role="button" class="btn btn-primary btn btn-block"
|
||||
href='{url_for('characters.view', id=CharacterInfo.query.filter(CharacterInfo.name==property_data['1']).first().id)}'>
|
||||
{property_data["1"]}
|
||||
</a>
|
||||
"""
|
||||
|
||||
if property_data["4"] == "":
|
||||
property_data["4"] = query_cdclient(
|
||||
'select DisplayDescription from ZoneTable where zoneID = ?',
|
||||
[property_data["12"]],
|
||||
one=True
|
||||
)
|
||||
|
||||
if property_data["6"] == 0:
|
||||
property_data["6"] = "Private"
|
||||
elif property_data["6"] == 1:
|
||||
property_data["6"] = "Best Friends"
|
||||
else:
|
||||
property_data["6"] = "Public"
|
||||
|
||||
property_data["8"] = time.ctime(property_data["8"])
|
||||
property_data["9"] = time.ctime(property_data["9"])
|
||||
|
||||
if not property_data["7"]:
|
||||
property_data["7"] = '''<h2 class="far fa-times-circle text-danger"></h2>'''
|
||||
else:
|
||||
property_data["7"] = '''<h2 class="far fa-check-square text-success"></h2>'''
|
||||
|
||||
property_data["12"] = query_cdclient(
|
||||
'select DisplayDescription from ZoneTable where zoneID = ?',
|
||||
[property_data["12"]],
|
||||
one=True
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@property_blueprint.route('/view_model/<id>', methods=['GET'])
|
||||
@login_required
|
||||
def view_model(id):
|
||||
property_content_data = PropertyContent.query.filter(PropertyContent.id==id).all()
|
||||
|
||||
# TODO: Restrict somehow
|
||||
formatted_data = [
|
||||
{
|
||||
"obj": url_for('properties.get_model', id=property_content_data[0].id, file_format='obj'),
|
||||
"mtl": url_for('properties.get_model', id=property_content_data[0].id, file_format='mtl'),
|
||||
"lot": property_content_data[0].lot,
|
||||
"id": property_content_data[0].id,
|
||||
"pos": [{
|
||||
"x": property_content_data[0].x,
|
||||
"y": property_content_data[0].y,
|
||||
"z": property_content_data[0].z,
|
||||
"rx": property_content_data[0].rx,
|
||||
"ry": property_content_data[0].ry,
|
||||
"rz": property_content_data[0].rz,
|
||||
"rw": property_content_data[0].rw
|
||||
}]
|
||||
}
|
||||
]
|
||||
|
||||
return render_template(
|
||||
'ldd/ldd.html.j2',
|
||||
content=formatted_data
|
||||
)
|
||||
|
||||
property_center = {
|
||||
1150: "(-17, 432, -60)",
|
||||
1151: "(0, 455, -110)",
|
||||
1250: "(-16, 432,-60)",
|
||||
1251: "(0, 455, 100)",
|
||||
1350: "(-10, 432, -57)",
|
||||
1450: "(-10, 432, -77)"
|
||||
}
|
||||
|
||||
|
||||
@property_blueprint.route('/view_models/<id>', methods=['GET'])
|
||||
@login_required
|
||||
def view_models(id):
|
||||
property_content_data = PropertyContent.query.filter(
|
||||
PropertyContent.property_id==id
|
||||
).order_by(PropertyContent.lot).all()
|
||||
|
||||
consolidated_list = []
|
||||
|
||||
for item in range(len(property_content_data)):
|
||||
if any((d["lot"] != 14 and d["lot"] == property_content_data[item].lot) for d in consolidated_list):
|
||||
# exiting lot, add rotations
|
||||
lot_index = next((index for (index, d) in enumerate(consolidated_list) if d["lot"] == property_content_data[item].lot), None)
|
||||
consolidated_list[lot_index]["pos"].append(
|
||||
{
|
||||
"x": property_content_data[item].x,
|
||||
"y": property_content_data[item].y,
|
||||
"z": property_content_data[item].z,
|
||||
"rx": property_content_data[item].rx,
|
||||
"ry": property_content_data[item].ry,
|
||||
"rz": property_content_data[item].rz,
|
||||
"rw": property_content_data[item].rw
|
||||
}
|
||||
)
|
||||
else:
|
||||
# add new lot
|
||||
consolidated_list.append(
|
||||
{
|
||||
"obj": url_for('properties.get_model', id=property_content_data[item].id, file_format='obj'),
|
||||
"mtl": url_for('properties.get_model', id=property_content_data[item].id, file_format='mtl'),
|
||||
"lot": property_content_data[item].lot,
|
||||
"id": property_content_data[item].id,
|
||||
"pos": [{
|
||||
"x": property_content_data[item].x,
|
||||
"y": property_content_data[item].y,
|
||||
"z": property_content_data[item].z,
|
||||
"rx": property_content_data[item].rx,
|
||||
"ry": property_content_data[item].ry,
|
||||
"rz": property_content_data[item].rz,
|
||||
"rw": property_content_data[item].rw
|
||||
}]
|
||||
}
|
||||
)
|
||||
property_data = Property.query.filter(Property.id==id).first()
|
||||
return render_template(
|
||||
'ldd/ldd.html.j2',
|
||||
property_data=property_data,
|
||||
content=consolidated_list,
|
||||
center=property_center[property_data.zone_id]
|
||||
)
|
||||
|
||||
@property_blueprint.route('/get_model/<id>/<file_format>', methods=['GET'])
|
||||
@login_required
|
||||
def get_model(id, file_format):
|
||||
content = PropertyContent.query.filter(PropertyContent.id==id).first()
|
||||
|
||||
if content.lot == 14: # ugc model
|
||||
response = ugc(content)[0]
|
||||
else: # prebuild model
|
||||
response = prebuilt(content, file_format)[0]
|
||||
|
||||
response.headers.set('Content-Type', 'text/xml')
|
||||
return response
|
||||
|
||||
|
||||
|
||||
@property_blueprint.route('/download_model/<id>', methods=['GET'])
|
||||
@login_required
|
||||
def download_model(id):
|
||||
content = PropertyContent.query.filter(PropertyContent.id==id).first()
|
||||
|
||||
if content.lot == 14: # ugc model
|
||||
response, filename = ugc(content)
|
||||
else: # prebuild model
|
||||
response, filename = prebuilt(content, "lxfml")
|
||||
|
||||
response.headers.set('Content-Type', 'attachment/xml')
|
||||
response.headers.set(
|
||||
'Content-Disposition',
|
||||
'attachment',
|
||||
filename=filename
|
||||
)
|
||||
return response
|
||||
|
||||
|
||||
def ugc(content):
|
||||
ugc_data = UGC.query.filter(UGC.id==content.ugc_id).first()
|
||||
uncompressed_lxfml = zlib.decompress(ugc_data.lxfml)
|
||||
response = make_response(uncompressed_lxfml)
|
||||
return response, ugc_data.filename
|
||||
|
||||
|
||||
def prebuilt(content, file_format):
|
||||
# translate LOT to component id
|
||||
# we need to get a type of 2 because reasons
|
||||
render_component_id = query_cdclient(
|
||||
'select component_id from ComponentsRegistry where component_type = 2 and id = ?',
|
||||
[content.lot],
|
||||
one=True
|
||||
)[0]
|
||||
# find the asset from rendercomponent given the component id
|
||||
filename = query_cdclient('select render_asset from RenderComponent where id = ?',
|
||||
[render_component_id],
|
||||
one=True
|
||||
)
|
||||
|
||||
if filename:
|
||||
filename = filename[0].split("\\\\")[-1].lower().split(".")[0]
|
||||
else:
|
||||
return f"No filename for LOT {content.lot}"
|
||||
|
||||
if file_format == "lxfml":
|
||||
lxfml = f'app/luclient/res/BrickModels/{filename.split(".")[0]}.lxfml'
|
||||
with open(lxfml, 'r') as file:
|
||||
lxfml_data = file.read()
|
||||
# print(lxfml_data)
|
||||
response = make_response(lxfml_data)
|
||||
|
||||
elif file_format in ["obj", "mtl"]:
|
||||
|
||||
cache = f"app/cache/{filename}.{file_format}"
|
||||
|
||||
if os.path.exists(cache):
|
||||
with open(cache, 'r') as file:
|
||||
cache_data = file.read()
|
||||
response = make_response(cache_data)
|
||||
|
||||
else:
|
||||
lxfml = f'app/luclient/res/BrickModels/{filename.split(".")[0]}.lxfml'
|
||||
ldd.main(lxfml, cache.split('.')[0]) # convert to OBJ
|
||||
|
||||
if os.path.exists(cache):
|
||||
with open(cache, 'r') as file:
|
||||
cache_data = file.read()
|
||||
response = make_response(cache_data)
|
||||
|
||||
else:
|
||||
raise(Exception("INVALID FILE FORMAT"))
|
||||
|
||||
return response, f"{filename}.{file_format}"
|
902
app/pylddlib.py
Normal file
902
app/pylddlib.py
Normal file
@ -0,0 +1,902 @@
|
||||
#!/usr/bin/env python
|
||||
# pylddlib version 0.4.9.7
|
||||
# based on pyldd2obj version 0.4.8 - Copyright (c) 2019 by jonnysp
|
||||
#
|
||||
# Updates:
|
||||
# 0.4.9.8 Make work with LEGO Universe brickdb
|
||||
# 0.4.9.7 corrected bug of incorrectly parsing the primitive xml file, specifically with comments. Add support LDDLIFTREE envirnment variable to set location of db.lif.
|
||||
# 0.4.9.6 preliminary Linux support
|
||||
# 0.4.9.5 corrected bug of incorrectly Bounding / GeometryBounding parsing the primitive xml file.
|
||||
# 0.4.9.4 improved lif.db checking for crucial files (because of the infamous botched 4.3.12 LDD Windows update).
|
||||
# 0.4.9.3 improved Windows and Python 3 compatibility
|
||||
# 0.4.9.2 changed handling of material = 0 for a part. Now a 0 will choose the 1st material (the base material of a part) and not the previous material of the subpart before. This will fix "Chicken Helmet Part 11262". It may break other parts and this change needs further regression.
|
||||
# 0.4.9.1 improved custom2DField handling, fixed decorations bug, improved material assignments handling
|
||||
# 0.4.9 updates to support reading extracted db.lif from db folder
|
||||
#
|
||||
# License: MIT License
|
||||
#
|
||||
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
import math
|
||||
import struct
|
||||
import zipfile
|
||||
from xml.dom import minidom
|
||||
import time
|
||||
|
||||
if sys.version_info < (3, 0):
|
||||
reload(sys)
|
||||
sys.setdefaultencoding('utf-8')
|
||||
|
||||
PRIMITIVEPATH = '/Primitives/'
|
||||
GEOMETRIEPATH = PRIMITIVEPATH + 'LOD0/'
|
||||
DECORATIONPATH = '/Decorations/'
|
||||
MATERIALNAMESPATH = '/MaterialNames/'
|
||||
|
||||
LOGOONSTUDSCONNTYPE = {"0:4", "0:4:1", "0:4:2", "0:4:33", "2:4:1", "2:4:34"}
|
||||
|
||||
class Matrix3D:
|
||||
def __init__(self, n11=1,n12=0,n13=0,n14=0,n21=0,n22=1,n23=0,n24=0,n31=0,n32=0,n33=1,n34=0,n41=0,n42=0,n43=0,n44=1):
|
||||
self.n11 = n11
|
||||
self.n12 = n12
|
||||
self.n13 = n13
|
||||
self.n14 = n14
|
||||
self.n21 = n21
|
||||
self.n22 = n22
|
||||
self.n23 = n23
|
||||
self.n24 = n24
|
||||
self.n31 = n31
|
||||
self.n32 = n32
|
||||
self.n33 = n33
|
||||
self.n34 = n34
|
||||
self.n41 = n41
|
||||
self.n42 = n42
|
||||
self.n43 = n43
|
||||
self.n44 = n44
|
||||
|
||||
def __str__(self):
|
||||
return '[{0},{1},{2},{3},{4},{5},{6},{7},{8},{9},{10},{11},{12},{13},{14},{15}]'.format(self.n11, self.n12, self.n13,self.n14,self.n21, self.n22, self.n23,self.n24,self.n31, self.n32, self.n33,self.n34,self.n41, self.n42, self.n43,self.n44)
|
||||
|
||||
def rotate(self,angle=0,axis=0):
|
||||
c = math.cos(angle)
|
||||
s = math.sin(angle)
|
||||
t = 1 - c
|
||||
|
||||
tx = t * axis.x
|
||||
ty = t * axis.y
|
||||
tz = t * axis.z
|
||||
|
||||
sx = s * axis.x
|
||||
sy = s * axis.y
|
||||
sz = s * axis.z
|
||||
|
||||
self.n11 = c + axis.x * tx
|
||||
self.n12 = axis.y * tx + sz
|
||||
self.n13 = axis.z * tx - sy
|
||||
self.n14 = 0
|
||||
|
||||
self.n21 = axis.x * ty - sz
|
||||
self.n22 = c + axis.y * ty
|
||||
self.n23 = axis.z * ty + sx
|
||||
self.n24 = 0
|
||||
|
||||
self.n31 = axis.x * tz + sy
|
||||
self.n32 = axis.y * tz - sx
|
||||
self.n33 = c + axis.z * tz
|
||||
self.n34 = 0
|
||||
|
||||
self.n41 = 0
|
||||
self.n42 = 0
|
||||
self.n43 = 0
|
||||
self.n44 = 1
|
||||
|
||||
def __mul__(self, other):
|
||||
return Matrix3D(
|
||||
self.n11 * other.n11 + self.n21 * other.n12 + self.n31 * other.n13 + self.n41 * other.n14,
|
||||
self.n12 * other.n11 + self.n22 * other.n12 + self.n32 * other.n13 + self.n42 * other.n14,
|
||||
self.n13 * other.n11 + self.n23 * other.n12 + self.n33 * other.n13 + self.n43 * other.n14,
|
||||
self.n14 * other.n11 + self.n24 * other.n12 + self.n34 * other.n13 + self.n44 * other.n14,
|
||||
self.n11 * other.n21 + self.n21 * other.n22 + self.n31 * other.n23 + self.n41 * other.n24,
|
||||
self.n12 * other.n21 + self.n22 * other.n22 + self.n32 * other.n23 + self.n42 * other.n24,
|
||||
self.n13 * other.n21 + self.n23 * other.n22 + self.n33 * other.n23 + self.n43 * other.n24,
|
||||
self.n14 * other.n21 + self.n24 * other.n22 + self.n34 * other.n23 + self.n44 * other.n24,
|
||||
self.n11 * other.n31 + self.n21 * other.n32 + self.n31 * other.n33 + self.n41 * other.n34,
|
||||
self.n12 * other.n31 + self.n22 * other.n32 + self.n32 * other.n33 + self.n42 * other.n34,
|
||||
self.n13 * other.n31 + self.n23 * other.n32 + self.n33 * other.n33 + self.n43 * other.n34,
|
||||
self.n14 * other.n31 + self.n24 * other.n32 + self.n34 * other.n33 + self.n44 * other.n34,
|
||||
self.n11 * other.n41 + self.n21 * other.n42 + self.n31 * other.n43 + self.n41 * other.n44,
|
||||
self.n12 * other.n41 + self.n22 * other.n42 + self.n32 * other.n43 + self.n42 * other.n44,
|
||||
self.n13 * other.n41 + self.n23 * other.n42 + self.n33 * other.n43 + self.n43 * other.n44,
|
||||
self.n14 * other.n41 + self.n24 * other.n42 + self.n34 * other.n43 + self.n44 * other.n44
|
||||
)
|
||||
|
||||
class Point3D:
|
||||
def __init__(self, x=0,y=0,z=0):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.z = z
|
||||
|
||||
def __str__(self):
|
||||
return '[{0},{1},{2}]'.format(self.x, self.y,self.z)
|
||||
|
||||
def string(self,prefix = "v"):
|
||||
return '{0} {1:f} {2:f} {3:f}\n'.format(prefix ,self.x , self.y, self.z)
|
||||
|
||||
def transformW(self,matrix):
|
||||
x = matrix.n11 * self.x + matrix.n21 * self.y + matrix.n31 * self.z
|
||||
y = matrix.n12 * self.x + matrix.n22 * self.y + matrix.n32 * self.z
|
||||
z = matrix.n13 * self.x + matrix.n23 * self.y + matrix.n33 * self.z
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.z = z
|
||||
|
||||
def transform(self,matrix):
|
||||
x = matrix.n11 * self.x + matrix.n21 * self.y + matrix.n31 * self.z + matrix.n41
|
||||
y = matrix.n12 * self.x + matrix.n22 * self.y + matrix.n32 * self.z + matrix.n42
|
||||
z = matrix.n13 * self.x + matrix.n23 * self.y + matrix.n33 * self.z + matrix.n43
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.z = z
|
||||
|
||||
def copy(self):
|
||||
return Point3D(x=self.x,y=self.y,z=self.z)
|
||||
|
||||
class Point2D:
|
||||
def __init__(self, x=0,y=0):
|
||||
self.x = x
|
||||
self.y = y
|
||||
def __str__(self):
|
||||
return '[{0},{1}]'.format(self.x, self.y * -1)
|
||||
def string(self,prefix="t"):
|
||||
return '{0} {1:f} {2:f}\n'.format(prefix , self.x, self.y * -1 )
|
||||
def copy(self):
|
||||
return Point2D(x=self.x,y=self.y)
|
||||
|
||||
class Face:
|
||||
def __init__(self,a=0,b=0,c=0):
|
||||
self.a = a
|
||||
self.b = b
|
||||
self.c = c
|
||||
def string(self,prefix="f", indexOffset=0 ,textureoffset=0):
|
||||
if textureoffset == 0:
|
||||
return prefix + ' {0}//{0} {1}//{1} {2}//{2}\n'.format(self.a + indexOffset, self.b + indexOffset, self.c + indexOffset)
|
||||
else:
|
||||
return prefix + ' {0}/{3}/{0} {1}/{4}/{1} {2}/{5}/{2}\n'.format(self.a + indexOffset, self.b + indexOffset, self.c + indexOffset,self.a + textureoffset, self.b + textureoffset, self.c + textureoffset)
|
||||
def __str__(self):
|
||||
return '[{0},{1},{2}]'.format(self.a, self.b, self.c)
|
||||
|
||||
class Group:
|
||||
def __init__(self, node):
|
||||
self.partRefs = node.getAttribute('partRefs').split(',')
|
||||
|
||||
class Bone:
|
||||
def __init__(self, node):
|
||||
self.refID = node.getAttribute('refID')
|
||||
(a, b, c, d, e, f, g, h, i, x, y, z) = map(float, node.getAttribute('transformation').split(','))
|
||||
self.matrix = Matrix3D(n11=a,n12=b,n13=c,n14=0,n21=d,n22=e,n23=f,n24=0,n31=g,n32=h,n33=i,n34=0,n41=x,n42=y,n43=z,n44=1)
|
||||
|
||||
class Part:
|
||||
def __init__(self, node):
|
||||
self.isGrouped = False
|
||||
self.GroupIDX = 0
|
||||
self.Bones = []
|
||||
self.refID = node.getAttribute('refID')
|
||||
self.designID = node.getAttribute('designID')
|
||||
self.materials = list(map(str, node.getAttribute('materials').split(',')))
|
||||
|
||||
lastm = '0'
|
||||
for i, m in enumerate(self.materials):
|
||||
if (m == '0'):
|
||||
# self.materials[i] = lastm
|
||||
self.materials[i] = self.materials[0] #in case of 0 choose the 'base' material
|
||||
else:
|
||||
lastm = m
|
||||
if node.hasAttribute('decoration'):
|
||||
self.decoration = list(map(str,node.getAttribute('decoration').split(',')))
|
||||
for childnode in node.childNodes:
|
||||
if childnode.nodeName == 'Bone':
|
||||
self.Bones.append(Bone(node=childnode))
|
||||
|
||||
class Brick:
|
||||
def __init__(self, node):
|
||||
self.refID = node.getAttribute('refID')
|
||||
self.designID = node.getAttribute('designID')
|
||||
self.Parts = []
|
||||
for childnode in node.childNodes:
|
||||
if childnode.nodeName == 'Part':
|
||||
self.Parts.append(Part(node=childnode))
|
||||
|
||||
class SceneCamera:
|
||||
def __init__(self, node):
|
||||
self.refID = node.getAttribute('refID')
|
||||
(a, b, c, d, e, f, g, h, i, x, y, z) = map(float, node.getAttribute('transformation').split(','))
|
||||
self.matrix = Matrix3D(n11=a,n12=b,n13=c,n14=0,n21=d,n22=e,n23=f,n24=0,n31=g,n32=h,n33=i,n34=0,n41=x,n42=y,n43=z,n44=1)
|
||||
self.fieldOfView = float(node.getAttribute('fieldOfView'))
|
||||
self.distance = float(node.getAttribute('distance'))
|
||||
|
||||
class Scene:
|
||||
def __init__(self, file):
|
||||
self.Bricks = []
|
||||
self.Scenecamera = []
|
||||
self.Groups = []
|
||||
|
||||
if file.endswith('.lxfml'):
|
||||
with open(file, "rb") as file:
|
||||
data = file.read()
|
||||
elif file.endswith('.lxf'):
|
||||
zf = zipfile.ZipFile(file, 'r')
|
||||
data = zf.read('IMAGE100.LXFML')
|
||||
else:
|
||||
return
|
||||
|
||||
xml = minidom.parseString(data)
|
||||
self.Name = xml.firstChild.getAttribute('name')
|
||||
|
||||
for node in xml.firstChild.childNodes:
|
||||
if node.nodeName == 'Meta':
|
||||
for childnode in node.childNodes:
|
||||
if childnode.nodeName == 'BrickSet':
|
||||
self.Version = str(childnode.getAttribute('version'))
|
||||
elif node.nodeName == 'Cameras':
|
||||
for childnode in node.childNodes:
|
||||
if childnode.nodeName == 'Camera':
|
||||
self.Scenecamera.append(SceneCamera(node=childnode))
|
||||
elif node.nodeName == 'Bricks':
|
||||
for childnode in node.childNodes:
|
||||
if childnode.nodeName == 'Brick':
|
||||
self.Bricks.append(Brick(node=childnode))
|
||||
elif node.nodeName == 'GroupSystems':
|
||||
for childnode in node.childNodes:
|
||||
if childnode.nodeName == 'GroupSystem':
|
||||
for childnode in childnode.childNodes:
|
||||
if childnode.nodeName == 'Group':
|
||||
self.Groups.append(Group(node=childnode))
|
||||
|
||||
for i in range(len(self.Groups)):
|
||||
for brick in self.Bricks:
|
||||
for part in brick.Parts:
|
||||
if part.refID in self.Groups[i].partRefs:
|
||||
part.isGrouped = True
|
||||
part.GroupIDX = i
|
||||
|
||||
# print('Scene "'+ self.Name + '" Brickversion: ' + str(self.Version))
|
||||
|
||||
class GeometryReader:
|
||||
def __init__(self, data):
|
||||
self.offset = 0
|
||||
self.data = data
|
||||
self.positions = []
|
||||
self.normals = []
|
||||
self.textures = []
|
||||
self.faces = []
|
||||
self.bonemap = {}
|
||||
self.texCount = 0
|
||||
self.outpositions = []
|
||||
self.outnormals = []
|
||||
|
||||
if self.readInt() == 1111961649:
|
||||
self.valueCount = self.readInt()
|
||||
self.indexCount = self.readInt()
|
||||
self.faceCount = int(self.indexCount / 3)
|
||||
options = self.readInt()
|
||||
|
||||
for i in range(0, self.valueCount):
|
||||
self.positions.append(Point3D(x=self.readFloat(),y= self.readFloat(),z=self.readFloat()))
|
||||
|
||||
for i in range(0, self.valueCount):
|
||||
self.normals.append(Point3D(x=self.readFloat(),y= self.readFloat(),z=self.readFloat()))
|
||||
|
||||
if (options & 3) == 3:
|
||||
self.texCount = self.valueCount
|
||||
for i in range(0, self.valueCount):
|
||||
self.textures.append(Point2D(x=self.readFloat(), y=self.readFloat()))
|
||||
|
||||
for i in range(0, self.faceCount):
|
||||
self.faces.append(Face(a=self.readInt(),b=self.readInt(),c=self.readInt()))
|
||||
|
||||
if (options & 48) == 48:
|
||||
num = self.readInt()
|
||||
self.offset += (num * 4) + (self.indexCount * 4)
|
||||
num = self.readInt()
|
||||
self.offset += (3 * num * 4) + (self.indexCount * 4)
|
||||
|
||||
bonelength = self.readInt()
|
||||
self.bonemap = [0] * self.valueCount
|
||||
|
||||
if (bonelength > self.valueCount) or (bonelength > self.faceCount):
|
||||
datastart = self.offset
|
||||
self.offset += bonelength
|
||||
for i in range(0, self.valueCount):
|
||||
boneoffset = self.readInt() + 4
|
||||
self.bonemap[i] = self.read_Int(datastart + boneoffset)
|
||||
|
||||
def read_Int(self,_offset):
|
||||
if sys.version_info < (3, 0):
|
||||
return int(struct.unpack_from('i', self.data, _offset)[0])
|
||||
else:
|
||||
return int.from_bytes(self.data[_offset:_offset + 4], byteorder='little')
|
||||
|
||||
def readInt(self):
|
||||
if sys.version_info < (3, 0):
|
||||
ret = int(struct.unpack_from('i', self.data, self.offset)[0])
|
||||
else:
|
||||
ret = int.from_bytes(self.data[self.offset:self.offset + 4], byteorder='little')
|
||||
self.offset += 4
|
||||
return ret
|
||||
|
||||
def readFloat(self):
|
||||
ret = float(struct.unpack_from('f', self.data, self.offset)[0])
|
||||
self.offset += 4
|
||||
return ret
|
||||
|
||||
class Geometry:
|
||||
def __init__(self, designID, database):
|
||||
self.designID = designID
|
||||
self.Parts = {}
|
||||
self.maxGeoBounding = -1
|
||||
self.studsFields2D = []
|
||||
|
||||
GeometryLocation = '{0}{1}{2}'.format(GEOMETRIEPATH, designID,'.g')
|
||||
GeometryCount = 0
|
||||
while str(GeometryLocation) in database.filelist:
|
||||
self.Parts[GeometryCount] = GeometryReader(data=database.filelist[GeometryLocation].read())
|
||||
GeometryCount += 1
|
||||
GeometryLocation = '{0}{1}{2}{3}'.format(GEOMETRIEPATH, designID,'.g',GeometryCount)
|
||||
|
||||
primitive = Primitive(data = database.filelist[PRIMITIVEPATH + designID + '.xml'].read())
|
||||
self.Partname = primitive.Designname
|
||||
self.studsFields2D = primitive.Fields2D
|
||||
try:
|
||||
geoBoundingList = [abs(float(primitive.Bounding['minX']) - float(primitive.Bounding['maxX'])), abs(float(primitive.Bounding['minY']) - float(primitive.Bounding['maxY'])), abs(float(primitive.Bounding['minZ']) - float(primitive.Bounding['maxZ']))]
|
||||
geoBoundingList.sort()
|
||||
self.maxGeoBounding = geoBoundingList[-1]
|
||||
except KeyError as e:
|
||||
# print('\nBounding errror in part {0}: {1}\n'.format(designID, e))
|
||||
pass
|
||||
|
||||
# preflex
|
||||
for part in self.Parts:
|
||||
# transform
|
||||
for i, b in enumerate(primitive.Bones):
|
||||
# positions
|
||||
for j, p in enumerate(self.Parts[part].positions):
|
||||
if (self.Parts[part].bonemap[j] == i):
|
||||
self.Parts[part].positions[j].transform(b.matrix)
|
||||
# normals
|
||||
for k, n in enumerate(self.Parts[part].normals):
|
||||
if (self.Parts[part].bonemap[k] == i):
|
||||
self.Parts[part].normals[k].transformW(b.matrix)
|
||||
|
||||
def valuecount(self):
|
||||
count = 0
|
||||
for part in self.Parts:
|
||||
count += self.Parts[part].valueCount
|
||||
return count
|
||||
|
||||
def facecount(self):
|
||||
count = 0
|
||||
for part in self.Parts:
|
||||
count += self.Parts[part].faceCount
|
||||
return count
|
||||
|
||||
def texcount(self):
|
||||
count = 0
|
||||
for part in self.Parts:
|
||||
count += self.Parts[part].texCount
|
||||
return count
|
||||
|
||||
class Bone2:
|
||||
def __init__(self,boneId=0, angle=0, ax=0, ay=0, az=0, tx=0, ty=0, tz=0):
|
||||
self.boneId = boneId
|
||||
rotationMatrix = Matrix3D()
|
||||
rotationMatrix.rotate(angle = -angle * math.pi / 180.0,axis = Point3D(x=ax,y=ay,z=az))
|
||||
p = Point3D(x=tx,y=ty,z=tz)
|
||||
p.transformW(rotationMatrix)
|
||||
rotationMatrix.n41 -= p.x
|
||||
rotationMatrix.n42 -= p.y
|
||||
rotationMatrix.n43 -= p.z
|
||||
self.matrix = rotationMatrix
|
||||
|
||||
class Field2D:
|
||||
def __init__(self, type=0, width=0, height=0, angle=0, ax=0, ay=0, az=0, tx=0, ty=0, tz=0, field2DRawData='none'):
|
||||
self.type = type
|
||||
self.field2DRawData = field2DRawData
|
||||
rotationMatrix = Matrix3D()
|
||||
rotationMatrix.rotate(angle = -angle * math.pi / 180.0, axis = Point3D(x=ax,y=ay,z=az))
|
||||
p = Point3D(x=tx,y=ty,z=tz)
|
||||
p.transformW(rotationMatrix)
|
||||
rotationMatrix.n41 -= p.x
|
||||
rotationMatrix.n42 -= p.y
|
||||
rotationMatrix.n43 -= p.z
|
||||
|
||||
self.matrix = rotationMatrix
|
||||
self.custom2DField = []
|
||||
|
||||
#The height and width are always double the number of studs. The contained text is a 2D array that is always height + 1 and width + 1.
|
||||
rows_count = height + 1
|
||||
cols_count = width + 1
|
||||
# creation looks reverse
|
||||
# create an array of "cols_count" cols, for each of the "rows_count" rows
|
||||
# all elements are initialized to 0
|
||||
self.custom2DField = [[0 for j in range(cols_count)] for i in range(rows_count)]
|
||||
custom2DFieldString = field2DRawData.replace('\r', '').replace('\n', '').replace(' ', '')
|
||||
custom2DFieldArr = custom2DFieldString.strip().split(',')
|
||||
|
||||
k = 0
|
||||
for i in range(rows_count):
|
||||
for j in range(cols_count):
|
||||
self.custom2DField[i][j] = custom2DFieldArr[k]
|
||||
k += 1
|
||||
|
||||
def __str__(self):
|
||||
return '[type="{0}" transform="{1}" custom2DField="{2}"]'.format(self.type, self.matrix, self.custom2DField)
|
||||
|
||||
class CollisionBox:
|
||||
def __init__(self, sX=0, sY=0, sZ=0, angle=0, ax=0, ay=0, az=0, tx=0, ty=0, tz=0):
|
||||
rotationMatrix = Matrix3D()
|
||||
rotationMatrix.rotate(angle = -angle * math.pi / 180.0, axis = Point3D(x=ax,y=ay,z=az))
|
||||
p = Point3D(x=tx,y=ty,z=tz)
|
||||
p.transformW(rotationMatrix)
|
||||
rotationMatrix.n41 -= p.x
|
||||
rotationMatrix.n42 -= p.y
|
||||
rotationMatrix.n43 -= p.z
|
||||
|
||||
self.matrix = rotationMatrix
|
||||
self.corner = Point3D(x=sX,y=sY,z=sZ)
|
||||
self.positions = []
|
||||
|
||||
self.positions.append(Point3D(x=0, y=0, z=0))
|
||||
self.positions.append(Point3D(x=sX, y=0, z=0))
|
||||
self.positions.append(Point3D(x=0, y=sY, z=0))
|
||||
self.positions.append(Point3D(x=sX, y=sY, z=0))
|
||||
self.positions.append(Point3D(x=0, y=0, z=sZ))
|
||||
self.positions.append(Point3D(x=0, y=sY, z=sZ))
|
||||
self.positions.append(Point3D(x=sX ,y=0, z=sZ))
|
||||
self.positions.append(Point3D(x=sX ,y=sY, z=sZ))
|
||||
|
||||
def __str__(self):
|
||||
return '[0,0,0] [{0},0,0] [0,{1},0] [{0},{1},0] [0,0,{2}] [0,{1},{2}] [{0},0,{2}] [{0},{1},{2}]'.format(self.corner.x, self.corner.y, self.corner.z)
|
||||
|
||||
class Primitive:
|
||||
def __init__(self, data):
|
||||
self.Designname = ''
|
||||
self.Bones = []
|
||||
self.Fields2D = []
|
||||
self.CollisionBoxes = []
|
||||
self.PhysicsAttributes = {}
|
||||
self.Bounding = {}
|
||||
self.GeometryBounding = {}
|
||||
xml = minidom.parseString(data)
|
||||
root = xml.documentElement
|
||||
for node in root.childNodes:
|
||||
if node.__class__.__name__.lower() == 'comment':
|
||||
self.comment = node[0].nodeValue
|
||||
if node.nodeName == 'Flex':
|
||||
for node in node.childNodes:
|
||||
if node.nodeName == 'Bone':
|
||||
self.Bones.append(Bone2(boneId=int(node.getAttribute('boneId')), angle=float(node.getAttribute('angle')), ax=float(node.getAttribute('ax')), ay=float(node.getAttribute('ay')), az=float(node.getAttribute('az')), tx=float(node.getAttribute('tx')), ty=float(node.getAttribute('ty')), tz=float(node.getAttribute('tz'))))
|
||||
elif node.nodeName == 'Annotations':
|
||||
for childnode in node.childNodes:
|
||||
if childnode.nodeName == 'Annotation' and childnode.hasAttribute('designname'):
|
||||
self.Designname = childnode.getAttribute('designname')
|
||||
elif node.nodeName == 'Collision':
|
||||
for childnode in node.childNodes:
|
||||
if childnode.nodeName == 'Box':
|
||||
self.CollisionBoxes.append(CollisionBox(sX=float(childnode.getAttribute('sX')), sY=float(childnode.getAttribute('sY')), sZ=float(childnode.getAttribute('sZ')), angle=float(childnode.getAttribute('angle')), ax=float(childnode.getAttribute('ax')), ay=float(childnode.getAttribute('ay')), az=float(childnode.getAttribute('az')), tx=float(childnode.getAttribute('tx')), ty=float(childnode.getAttribute('ty')), tz=float(childnode.getAttribute('tz'))))
|
||||
elif node.nodeName == 'PhysicsAttributes':
|
||||
self.PhysicsAttributes = {"inertiaTensor": node.getAttribute('inertiaTensor'),"centerOfMass": node.getAttribute('centerOfMass'),"mass": node.getAttribute('mass'),"frictionType": node.getAttribute('frictionType')}
|
||||
elif node.nodeName == 'Bounding':
|
||||
for childnode in node.childNodes:
|
||||
if childnode.nodeName == 'AABB':
|
||||
self.Bounding = {"minX": childnode.getAttribute('minX'), "minY": childnode.getAttribute('minY'), "minZ": childnode.getAttribute('minZ'), "maxX": childnode.getAttribute('maxX'), "maxY": childnode.getAttribute('maxY'), "maxZ": childnode.getAttribute('maxZ')}
|
||||
elif node.nodeName == 'GeometryBounding':
|
||||
for childnode in node.childNodes:
|
||||
if childnode.nodeName == 'AABB':
|
||||
self.GeometryBounding = {"minX": childnode.getAttribute('minX'), "minY": childnode.getAttribute('minY'), "minZ": childnode.getAttribute('minZ'), "maxX": childnode.getAttribute('maxX'), "maxY": childnode.getAttribute('maxY'), "maxZ": childnode.getAttribute('maxZ')}
|
||||
elif node.nodeName == 'Connectivity':
|
||||
for childnode in node.childNodes:
|
||||
if childnode.nodeName == 'Custom2DField':
|
||||
self.Fields2D.append(Field2D(type=int(childnode.getAttribute('type')), width=int(childnode.getAttribute('width')), height=int(childnode.getAttribute('height')), angle=float(childnode.getAttribute('angle')), ax=float(childnode.getAttribute('ax')), ay=float(childnode.getAttribute('ay')), az=float(childnode.getAttribute('az')), tx=float(childnode.getAttribute('tx')), ty=float(childnode.getAttribute('ty')), tz=float(childnode.getAttribute('tz')), field2DRawData=str(childnode.firstChild.data)))
|
||||
elif node.nodeName == 'Decoration':
|
||||
self.Decoration = {"faces": node.getAttribute('faces'), "subMaterialRedirectLookupTable": node.getAttribute('subMaterialRedirectLookupTable')}
|
||||
|
||||
class Materials:
|
||||
def __init__(self, data):
|
||||
self.Materials = {}
|
||||
xml = minidom.parseString(data)
|
||||
for node in xml.firstChild.childNodes:
|
||||
if node.nodeName == 'Material':
|
||||
self.Materials[node.getAttribute('MatID')] = Material(
|
||||
node.getAttribute('MatID'),
|
||||
r=int(node.getAttribute('Red')),
|
||||
g=int(node.getAttribute('Green')),
|
||||
b=int(node.getAttribute('Blue')),
|
||||
a=int(node.getAttribute('Alpha')),
|
||||
mtype=str(node.getAttribute('MaterialType'))
|
||||
)
|
||||
|
||||
def getMaterialbyId(self, mid):
|
||||
return self.Materials[mid]
|
||||
|
||||
class Material:
|
||||
def __init__(self,id, r, g, b, a, mtype):
|
||||
self.id = id
|
||||
self.name = id
|
||||
self.mattype = mtype
|
||||
self.r = float(r)
|
||||
self.g = float(g)
|
||||
self.b = float(b)
|
||||
self.a = float(a)
|
||||
def string(self):
|
||||
out = 'Kd {0} {1} {2}\nKa 1.600000 1.600000 1.600000\nKs 0.400000 0.400000 0.400000\nNs 3.482202\nTf 1 1 1\n'.format( self.r / 255, self.g / 255,self.b / 255)
|
||||
if self.a < 255:
|
||||
out += 'Ni 1.575\n' + 'd {0}'.format(0.05) + '\n' + 'Tr {0}\n'.format(0.05)
|
||||
return out
|
||||
|
||||
class DBinfo:
|
||||
def __init__(self, data):
|
||||
xml = minidom.parseString(data)
|
||||
self.Version = xml.getElementsByTagName('Bricks')[0].attributes['version'].value
|
||||
# print('DB Version: ' + str(self.Version))
|
||||
|
||||
class DBFolderFile:
|
||||
def __init__(self, name, handle):
|
||||
self.handle = handle
|
||||
self.name = name
|
||||
|
||||
def read(self):
|
||||
reader = open(self.handle, "rb")
|
||||
try:
|
||||
filecontent = reader.read()
|
||||
reader.close()
|
||||
return filecontent
|
||||
finally:
|
||||
reader.close()
|
||||
|
||||
class LIFFile:
|
||||
def __init__(self, name, offset, size, handle):
|
||||
self.handle = handle
|
||||
self.name = name
|
||||
self.offset = offset
|
||||
self.size = size
|
||||
|
||||
def read(self):
|
||||
self.handle.seek(self.offset, 0)
|
||||
return self.handle.read(self.size)
|
||||
|
||||
class DBFolderReader:
|
||||
def __init__(self, folder):
|
||||
self.filelist = {}
|
||||
self.initok = False
|
||||
self.location = folder
|
||||
self.dbinfo = None
|
||||
|
||||
try:
|
||||
os.path.isdir(self.location)
|
||||
except Exception as e:
|
||||
self.initok = False
|
||||
# print("db folder read FAIL")
|
||||
return
|
||||
else:
|
||||
self.parse()
|
||||
if self.fileexist(os.path.join(self.location,'Materials.xml')) and self.fileexist(os.path.join(self.location, 'info.xml')):
|
||||
self.dbinfo = DBinfo(data=self.filelist[os.path.join(self.location,'info.xml')].read())
|
||||
# print("DB folder OK.")
|
||||
self.initok = True
|
||||
else:
|
||||
# print("DB folder ERROR")
|
||||
# print(os.path.join(self.location,'Materials.xml'))
|
||||
# print(self.fileexist(os.path.join(self.location,'Materials.xml')))
|
||||
# print(os.path.join(self.location,'info.xml'))
|
||||
# print(self.fileexist(os.path.join(self.location, 'info.xml')))
|
||||
# print(MATERIALNAMESPATH)
|
||||
pass
|
||||
|
||||
|
||||
def fileexist(self, filename):
|
||||
return filename in self.filelist
|
||||
|
||||
def parse(self):
|
||||
for path, subdirs, files in os.walk(self.location):
|
||||
for name in files:
|
||||
entryName = os.path.join(path, name)
|
||||
self.filelist[entryName] = DBFolderFile(name=entryName, handle=entryName)
|
||||
|
||||
class LIFReader:
|
||||
def __init__(self, file):
|
||||
self.packedFilesOffset = 84
|
||||
self.filelist = {}
|
||||
self.initok = False
|
||||
self.location = file
|
||||
self.dbinfo = None
|
||||
|
||||
try:
|
||||
self.filehandle = open(self.location, "rb")
|
||||
self.filehandle.seek(0, 0)
|
||||
except Exception as e:
|
||||
self.initok = False
|
||||
# print("Database FAIL")
|
||||
return
|
||||
else:
|
||||
if self.filehandle.read(4).decode() == "LIFF":
|
||||
self.parse(prefix='', offset=self.readInt(offset=72) + 64)
|
||||
if self.fileexist('/Materials.xml') and self.fileexist('/info.xml'):
|
||||
self.dbinfo = DBinfo(data=self.filelist['/info.xml'].read())
|
||||
# print("Database OK.")
|
||||
self.initok = True
|
||||
else:
|
||||
# print("Database ERROR")
|
||||
pass
|
||||
else:
|
||||
# print("Database FAIL")
|
||||
self.initok = False
|
||||
|
||||
def fileexist(self,filename):
|
||||
return filename in self.filelist
|
||||
|
||||
def parse(self, prefix='', offset=0):
|
||||
if prefix == '':
|
||||
offset += 36
|
||||
else:
|
||||
offset += 4
|
||||
|
||||
count = self.readInt(offset=offset)
|
||||
|
||||
for i in range(0, count):
|
||||
offset += 4
|
||||
entryType = self.readShort(offset=offset)
|
||||
offset += 6
|
||||
|
||||
entryName = '{0}{1}'.format(prefix,'/');
|
||||
self.filehandle.seek(offset + 1, 0)
|
||||
if sys.version_info < (3, 0):
|
||||
t = ord(self.filehandle.read(1))
|
||||
else:
|
||||
t = int.from_bytes(self.filehandle.read(1), byteorder='big')
|
||||
|
||||
while not t == 0:
|
||||
entryName ='{0}{1}'.format(entryName,chr(t))
|
||||
self.filehandle.seek(1, 1)
|
||||
if sys.version_info < (3, 0):
|
||||
t = ord(self.filehandle.read(1))
|
||||
else:
|
||||
t = int.from_bytes(self.filehandle.read(1), byteorder='big')
|
||||
|
||||
offset += 2
|
||||
|
||||
offset += 6
|
||||
self.packedFilesOffset += 20
|
||||
|
||||
if entryType == 1:
|
||||
offset = self.parse(prefix=entryName, offset=offset)
|
||||
elif entryType == 2:
|
||||
fileSize = self.readInt(offset=offset) - 20
|
||||
self.filelist[entryName] = LIFFile(name=entryName, offset=self.packedFilesOffset, size=fileSize, handle=self.filehandle)
|
||||
offset += 24
|
||||
self.packedFilesOffset += fileSize
|
||||
|
||||
return offset
|
||||
|
||||
def readInt(self, offset=0):
|
||||
self.filehandle.seek(offset, 0)
|
||||
if sys.version_info < (3, 0):
|
||||
return int(struct.unpack('>i', self.filehandle.read(4))[0])
|
||||
else:
|
||||
return int.from_bytes(self.filehandle.read(4), byteorder='big')
|
||||
|
||||
def readShort(self, offset=0):
|
||||
self.filehandle.seek(offset, 0)
|
||||
if sys.version_info < (3, 0):
|
||||
return int(struct.unpack('>h', self.filehandle.read(2))[0])
|
||||
else:
|
||||
return int.from_bytes(self.filehandle.read(2), byteorder='big')
|
||||
|
||||
class Converter:
|
||||
def LoadDBFolder(self, dbfolderlocation):
|
||||
self.database = DBFolderReader(folder=dbfolderlocation)
|
||||
if self.database.initok and self.database.fileexist(os.path.join(dbfolderlocation,'Materials.xml')):
|
||||
self.allMaterials = Materials(data=self.database.filelist[os.path.join(dbfolderlocation,'Materials.xml')].read());
|
||||
|
||||
def LoadDatabase(self,databaselocation):
|
||||
self.database = LIFReader(file=databaselocation)
|
||||
|
||||
if self.database.initok and self.database.fileexist('/Materials.xml'):
|
||||
self.allMaterials = Materials(data=self.database.filelist['/Materials.xml'].read());
|
||||
|
||||
def LoadScene(self,filename):
|
||||
if self.database.initok:
|
||||
self.scene = Scene(file=filename)
|
||||
|
||||
def Export(self,filename):
|
||||
invert = Matrix3D()
|
||||
#invert.n33 = -1 #uncomment to invert the Z-Axis
|
||||
|
||||
indexOffset = 1
|
||||
textOffset = 1
|
||||
usedmaterials = []
|
||||
geometriecache = {}
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
out = open(filename + ".obj.tmp", "w+")
|
||||
out.write("mtllib " + filename + ".mtl" + '\n\n')
|
||||
outtext = open(filename + ".mtl.tmp", "w+")
|
||||
|
||||
total = len(self.scene.Bricks)
|
||||
current = 0
|
||||
|
||||
for bri in self.scene.Bricks:
|
||||
current += 1
|
||||
|
||||
for pa in bri.Parts:
|
||||
|
||||
if pa.designID not in geometriecache:
|
||||
geo = Geometry(designID=pa.designID, database=self.database)
|
||||
progress(current ,total , "(" + geo.designID + ") " + geo.Partname, ' ')
|
||||
geometriecache[pa.designID] = geo
|
||||
else:
|
||||
geo = geometriecache[pa.designID]
|
||||
|
||||
progress(current ,total , "(" + geo.designID + ") " + geo.Partname ,'-')
|
||||
|
||||
out.write("o\n")
|
||||
|
||||
for part in geo.Parts:
|
||||
geo.Parts[part].outpositions = [elem.copy() for elem in geo.Parts[part].positions]
|
||||
geo.Parts[part].outnormals = [elem.copy() for elem in geo.Parts[part].normals]
|
||||
|
||||
for i, b in enumerate(pa.Bones):
|
||||
# positions
|
||||
for j, p in enumerate(geo.Parts[part].outpositions):
|
||||
if (geo.Parts[part].bonemap[j] == i):
|
||||
p.transform( invert * b.matrix)
|
||||
# normals
|
||||
for k, n in enumerate(geo.Parts[part].outnormals):
|
||||
if (geo.Parts[part].bonemap[k] == i):
|
||||
n.transformW( invert * b.matrix)
|
||||
|
||||
for point in geo.Parts[part].outpositions:
|
||||
out.write(point.string("v"))
|
||||
|
||||
for normal in geo.Parts[part].outnormals:
|
||||
out.write(normal.string("vn"))
|
||||
|
||||
for text in geo.Parts[part].textures:
|
||||
out.write(text.string("vt"))
|
||||
|
||||
decoCount = 0
|
||||
out.write("g " + "(" + geo.designID + ") " + geo.Partname + '\n')
|
||||
last_color = 0
|
||||
for part in geo.Parts:
|
||||
|
||||
|
||||
#try catch here for possible problems in materials assignment of various g, g1, g2, .. files in lxf file
|
||||
try:
|
||||
materialCurrentPart = pa.materials[part]
|
||||
last_color = pa.materials[part]
|
||||
except IndexError:
|
||||
# print('WARNING: {0}.g{1} has NO material assignment in lxf. Replaced with color {2}. Fix {0}.xml faces values.'.format(pa.designID, part, last_color))
|
||||
materialCurrentPart = last_color
|
||||
|
||||
lddmat = self.allMaterials.getMaterialbyId(materialCurrentPart)
|
||||
matname = lddmat.name
|
||||
|
||||
deco = '0'
|
||||
if hasattr(pa, 'decoration') and len(geo.Parts[part].textures) > 0:
|
||||
#if decoCount <= len(pa.decoration):
|
||||
if decoCount < len(pa.decoration):
|
||||
deco = pa.decoration[decoCount]
|
||||
decoCount += 1
|
||||
|
||||
extfile = ''
|
||||
if not deco == '0':
|
||||
extfile = deco + '.png'
|
||||
matname += "_" + deco
|
||||
decofilename = DECORATIONPATH + deco + '.png'
|
||||
if not os.path.isfile(extfile) and self.database.fileexist(decofilename):
|
||||
with open(extfile, "wb") as f:
|
||||
f.write(self.database.filelist[decofilename].read())
|
||||
f.close()
|
||||
|
||||
if not matname in usedmaterials:
|
||||
usedmaterials.append(matname)
|
||||
outtext.write("newmtl " + matname + '\n')
|
||||
outtext.write(lddmat.string())
|
||||
if not deco == '0':
|
||||
outtext.write("map_Kd " + deco + ".png" + '\n')
|
||||
|
||||
out.write("usemtl " + matname + '\n')
|
||||
for face in geo.Parts[part].faces:
|
||||
if len(geo.Parts[part].textures) > 0:
|
||||
out.write(face.string("f",indexOffset,textOffset))
|
||||
else:
|
||||
out.write(face.string("f",indexOffset))
|
||||
|
||||
indexOffset += len(geo.Parts[part].outpositions)
|
||||
textOffset += len(geo.Parts[part].textures)
|
||||
# -----------------------------------------------------------------
|
||||
out.write('\n')
|
||||
os.rename(filename + ".obj.tmp", filename + ".obj")
|
||||
os.rename(filename + ".mtl.tmp", filename + ".mtl")
|
||||
|
||||
sys.stdout.write('%s\r' % (' '))
|
||||
# print("--- %s seconds ---" % (time.time() - start_time))
|
||||
|
||||
def setDBFolderVars(dbfolderlocation):
|
||||
global PRIMITIVEPATH
|
||||
global GEOMETRIEPATH
|
||||
global DECORATIONPATH
|
||||
global MATERIALNAMESPATH
|
||||
PRIMITIVEPATH = os.path.join(dbfolderlocation, 'Primitives', '')
|
||||
GEOMETRIEPATH = os.path.join(dbfolderlocation, 'brickprimitives', 'lod0', '')
|
||||
DECORATIONPATH = os.path.join(dbfolderlocation, 'Decorations', '')
|
||||
MATERIALNAMESPATH = os.path.join(dbfolderlocation, 'MaterialNames', '')
|
||||
# print(MATERIALNAMESPATH)
|
||||
|
||||
def FindDatabase():
|
||||
lddliftree = os.getenv('LDDLIFTREE')
|
||||
if lddliftree is not None:
|
||||
if os.path.isdir(str(lddliftree)): #LDDLIFTREE points to folder
|
||||
return str(lddliftree)
|
||||
elif os.path.isfile(str(lddliftree)): #LDDLIFTREE points to file (should be db.lif)
|
||||
return str(lddliftree)
|
||||
|
||||
else: #Env variable LDDLIFTREE not set. Check for default locations per different platform.
|
||||
if platform.system() == 'Darwin':
|
||||
if os.path.isdir(str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'Library','Application Support','LEGO Company','LEGO Digital Designer','db'))):
|
||||
return str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'Library','Application Support','LEGO Company','LEGO Digital Designer','db'))
|
||||
elif os.path.isfile(str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'Library','Application Support','LEGO Company','LEGO Digital Designer','db.lif'))):
|
||||
return str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'Library','Application Support','LEGO Company','LEGO Digital Designer','db.lif'))
|
||||
else:
|
||||
# print("no LDD database found please install LEGO-Digital-Designer")
|
||||
os._exit()
|
||||
elif platform.system() == 'Windows':
|
||||
if os.path.isdir(str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'AppData','Roaming','LEGO Company','LEGO Digital Designer','db'))):
|
||||
return str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'AppData','Roaming','LEGO Company','LEGO Digital Designer','db'))
|
||||
elif os.path.isfile(str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'AppData','Roaming','LEGO Company','LEGO Digital Designer','db.lif'))):
|
||||
return str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'AppData','Roaming','LEGO Company','LEGO Digital Designer','db.lif'))
|
||||
else:
|
||||
# print("no LDD database found please install LEGO-Digital-Designer")
|
||||
os._exit()
|
||||
elif platform.system() == 'Linux':
|
||||
if os.path.isdir(str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'.wine','drive_c','users',os.getenv('USER'),'Application Data','LEGO Company','LEGO Digital Designer','db'))):
|
||||
return str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'.wine','drive_c','users',os.getenv('USER'),'Application Data','LEGO Company','LEGO Digital Designer','db'))
|
||||
elif os.path.isfile(str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'.wine','drive_c','users',os.getenv('USER'),'Application Data','LEGO Company','LEGO Digital Designer','db.lif'))):
|
||||
return str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'.wine','drive_c','users',os.getenv('USER'),'Application Data','LEGO Company','LEGO Digital Designer','db.lif'))
|
||||
else:
|
||||
# print("no LDD database found please install LEGO-Digital-Designer")
|
||||
os._exit()
|
||||
else:
|
||||
# print('Your OS {0} is not supported yet.'.format(platform.system()))
|
||||
os._exit()
|
||||
|
||||
def progress(count, total, status='', suffix = ''):
|
||||
bar_len = 40
|
||||
filled_len = int(round(bar_len * count / float(total)))
|
||||
percents = round(100.0 * count / float(total), 1)
|
||||
bar = '#' * filled_len + '-' * (bar_len - filled_len)
|
||||
sys.stdout.write('Progress: [%s] %s%s %s %s\r' % (bar, percents, '%', suffix, ' '))
|
||||
sys.stdout.write('Progress: [%s] %s%s %s %s\r' % (bar, percents, '%', suffix, status))
|
||||
sys.stdout.flush()
|
||||
|
||||
def main(lxf_filename, obj_filename):
|
||||
# print("- - - pylddlib - - -")
|
||||
# print(" _ ")
|
||||
# print(" [_]")
|
||||
# print(" /| |\\")
|
||||
# print(" ()'---' C")
|
||||
# print(" | | |")
|
||||
# print(" [=|=]")
|
||||
# print("")
|
||||
# print("- - - - - - - - - - - -")
|
||||
|
||||
converter = Converter()
|
||||
# print("Found DB folder. Will use this instead of db.lif!")
|
||||
setDBFolderVars(dbfolderlocation = "app/luclient/res/")
|
||||
converter.LoadDBFolder(dbfolderlocation = "app/luclient/res/")
|
||||
converter.LoadScene(filename=lxf_filename)
|
||||
converter.Export(filename=obj_filename)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
58
app/reports.py
Normal file
58
app/reports.py
Normal file
@ -0,0 +1,58 @@
|
||||
from flask import render_template, Blueprint, redirect, url_for, request, abort, flash, request
|
||||
from flask_user import login_required, current_user
|
||||
from app.models import db, CharacterInfo, Account, CharacterXML, ItemReports
|
||||
from app import gm_level, scheduler
|
||||
import datetime, xmltodict
|
||||
|
||||
reports_blueprint = Blueprint('reports', __name__)
|
||||
|
||||
@reports_blueprint.route('/', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
def index():
|
||||
items = ItemReports.query.distinct(ItemReports.date).group_by(ItemReports.date).all()
|
||||
|
||||
return render_template('reports/index.html.j2', items=items)
|
||||
|
||||
@reports_blueprint.route('/items/by_date/<date>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
def items_by_date(date):
|
||||
items = ItemReports.query.filter(ItemReports.date==date).order_by(ItemReports.count.desc()).all()
|
||||
return render_template('reports/items/by_date.html.j2', items=items, date=date)
|
||||
|
||||
|
||||
@scheduler.task("cron", id="gen_item_report", hour=3)
|
||||
def gen_item_report():
|
||||
char_xmls = CharacterXML.query.join(
|
||||
CharacterInfo,
|
||||
CharacterInfo.id==CharacterXML.id
|
||||
).join(
|
||||
Account,
|
||||
CharacterInfo.account_id==Account.id
|
||||
).filter(Account.gm_level < 3).all()
|
||||
date = datetime.date.today().strftime('%Y-%m-%d')
|
||||
for char_xml in char_xmls:
|
||||
character_json = xmltodict.parse(
|
||||
char_xml.xml_data,
|
||||
attr_prefix="attr_"
|
||||
)
|
||||
for inv in character_json["obj"]["inv"]["items"]["in"]:
|
||||
if "i" in inv.keys() and type(inv["i"]) == list and (int(inv["attr_t"])==0 or int(inv["attr_t"])==0):
|
||||
for item in inv["i"]:
|
||||
entry = ItemReports.query.filter(
|
||||
ItemReports.item == int(item["attr_l"]) and \
|
||||
ItemReports.date == date
|
||||
).first()
|
||||
if entry:
|
||||
entry.count = entry.count + int(item["attr_c"])
|
||||
entry.save()
|
||||
else:
|
||||
new_entry = ItemReports(
|
||||
item=int(item["attr_l"]),
|
||||
count=int(item["attr_c"]),
|
||||
date=date
|
||||
)
|
||||
new_entry.save()
|
||||
|
||||
return "Done"
|
117
app/schemas.py
Normal file
117
app/schemas.py
Normal file
@ -0,0 +1,117 @@
|
||||
from flask_marshmallow import Marshmallow
|
||||
from app.models import *
|
||||
ma = Marshmallow()
|
||||
|
||||
class PlayKeySchema(ma.SQLAlchemyAutoSchema):
|
||||
class Meta:
|
||||
model = PlayKey
|
||||
include_relationships = False
|
||||
load_instance = True
|
||||
include_fk = True
|
||||
|
||||
|
||||
class PetNamesSchema(ma.SQLAlchemyAutoSchema):
|
||||
class Meta:
|
||||
model = PetNames
|
||||
include_relationships = False
|
||||
load_instance = True
|
||||
include_fk = False
|
||||
|
||||
|
||||
class MailSchema(ma.SQLAlchemyAutoSchema):
|
||||
class Meta:
|
||||
model = Mail
|
||||
include_relationships = False
|
||||
load_instance = True
|
||||
include_fk = False
|
||||
|
||||
|
||||
class UGCSchema(ma.SQLAlchemyAutoSchema):
|
||||
class Meta:
|
||||
model = UGC
|
||||
include_relationships = False
|
||||
load_instance = True
|
||||
include_fk = False
|
||||
|
||||
|
||||
class PropertyContentSchema(ma.SQLAlchemyAutoSchema):
|
||||
class Meta:
|
||||
model = PropertyContent
|
||||
include_relationships = True
|
||||
load_instance = True
|
||||
include_fk = True
|
||||
|
||||
ugc = ma.Nested(UGCSchema)
|
||||
|
||||
|
||||
|
||||
class PropertySchema(ma.SQLAlchemyAutoSchema):
|
||||
class Meta:
|
||||
model = Property
|
||||
include_relationships = False
|
||||
load_instance = True
|
||||
include_fk = False
|
||||
|
||||
properties_contents = ma.Nested(PropertyContentSchema, many=True)
|
||||
|
||||
|
||||
class CharacterXMLSchema(ma.SQLAlchemyAutoSchema):
|
||||
class Meta:
|
||||
model = CharacterXML
|
||||
include_relationships = False
|
||||
load_instance = True
|
||||
include_fk = False
|
||||
|
||||
|
||||
class CharacterInfoSchema(ma.SQLAlchemyAutoSchema):
|
||||
class Meta:
|
||||
model = CharacterInfo
|
||||
include_relationships = False
|
||||
load_instance = True
|
||||
include_fk = False
|
||||
|
||||
charxml = ma.Nested(CharacterXMLSchema)
|
||||
properties_owner = ma.Nested(PropertySchema, many=True)
|
||||
pets = ma.Nested(PetNamesSchema, many=True)
|
||||
mail = ma.Nested(MailSchema, many=True)
|
||||
|
||||
|
||||
class AccountSchema(ma.SQLAlchemyAutoSchema):
|
||||
class Meta:
|
||||
model = Account
|
||||
include_relationships = False
|
||||
load_instance = True
|
||||
include_fk = False
|
||||
|
||||
play_key = ma.Nested(PlayKeySchema)
|
||||
charinfo = ma.Nested(CharacterInfoSchema, many=True)
|
||||
|
||||
|
||||
class AccountInvitationSchema(ma.SQLAlchemyAutoSchema): # noqa
|
||||
class Meta:
|
||||
model = AccountInvitation
|
||||
include_relationships = True
|
||||
load_instance = True
|
||||
include_fk = True
|
||||
|
||||
invite_by_user = ma.Nested(AccountSchema)
|
||||
|
||||
|
||||
class ActivityLogSchema(ma.SQLAlchemyAutoSchema): # noqa
|
||||
class Meta:
|
||||
model = ActivityLog
|
||||
include_relationships = True
|
||||
load_instance = True
|
||||
include_fk = True
|
||||
|
||||
character = ma.Nested(CharacterInfoSchema())
|
||||
|
||||
|
||||
class CommandLogSchema(ma.SQLAlchemyAutoSchema): # noqa
|
||||
class Meta:
|
||||
model = CommandLog
|
||||
include_relationships = True
|
||||
load_instance = True
|
||||
include_fk = True
|
||||
|
||||
character = ma.Nested(CharacterInfoSchema())
|
39
app/settings.py
Normal file
39
app/settings.py
Normal file
@ -0,0 +1,39 @@
|
||||
# Settings common to all environments (development|staging|production)
|
||||
|
||||
# Application settings
|
||||
APP_NAME = "Nexus Dashboard"
|
||||
APP_SYSTEM_ERROR_SUBJECT_LINE = APP_NAME + " system error"
|
||||
|
||||
# Flask settings
|
||||
CSRF_ENABLED = True
|
||||
|
||||
# Flask-SQLAlchemy settings
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
WTF_CSRF_TIME_LIMIT = 86400
|
||||
|
||||
# Flask-User settings
|
||||
USER_APP_NAME = APP_NAME
|
||||
USER_ENABLE_CHANGE_PASSWORD = True # Allow users to change their password
|
||||
USER_ENABLE_CHANGE_USERNAME = True # Allow users to change their username
|
||||
USER_ENABLE_REGISTER = False # Allow new users to register
|
||||
|
||||
# Should alwyas be set to true
|
||||
USER_REQUIRE_RETYPE_PASSWORD = True # Prompt for `retype password`
|
||||
USER_ENABLE_USERNAME = True # Register and Login with username
|
||||
|
||||
# Email Related Settings
|
||||
USER_ENABLE_EMAIL = True # Register with Email WILL - DISABLE OTHER THINGS TOO
|
||||
USER_ENABLE_CONFIRM_EMAIL = True # Force users to confirm their email
|
||||
USER_ENABLE_INVITE_USER = False # Allow users to be invited
|
||||
USER_REQUIRE_INVITATION = False # Only invited users may - WILL DISABLE REGISTRATION
|
||||
USER_ENABLE_FORGOT_PASSWORD = True # Allow users to reset their passwords
|
||||
|
||||
# Require Play Key
|
||||
REQUIRE_PLAY_KEY = True
|
||||
|
||||
# Password hashing settings
|
||||
USER_PASSLIB_CRYPTCONTEXT_SCHEMES = ['bcrypt'] # bcrypt for password hashing
|
||||
|
||||
# Flask-User routing settings
|
||||
USER_AFTER_LOGIN_ENDPOINT = "main.index"
|
||||
USER_AFTER_LOGOUT_ENDPOINT = "main.index"
|
7
app/static/bootstrap-4.2.1/js/bootstrap.bundle.min.js
vendored
Normal file
7
app/static/bootstrap-4.2.1/js/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
app/static/bootstrap-4.2.1/js/jquery-3.3.1.min.js
vendored
Normal file
2
app/static/bootstrap-4.2.1/js/jquery-3.3.1.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
51
app/static/bootstrap-4.2.1/scss/_alert.scss
vendored
Normal file
51
app/static/bootstrap-4.2.1/scss/_alert.scss
vendored
Normal file
@ -0,0 +1,51 @@
|
||||
//
|
||||
// Base styles
|
||||
//
|
||||
|
||||
.alert {
|
||||
position: relative;
|
||||
padding: $alert-padding-y $alert-padding-x;
|
||||
margin-bottom: $alert-margin-bottom;
|
||||
border: $alert-border-width solid transparent;
|
||||
@include border-radius($alert-border-radius);
|
||||
}
|
||||
|
||||
// Headings for larger alerts
|
||||
.alert-heading {
|
||||
// Specified to prevent conflicts of changing $headings-color
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
// Provide class for links that match alerts
|
||||
.alert-link {
|
||||
font-weight: $alert-link-font-weight;
|
||||
}
|
||||
|
||||
|
||||
// Dismissible alerts
|
||||
//
|
||||
// Expand the right padding and account for the close button's positioning.
|
||||
|
||||
.alert-dismissible {
|
||||
padding-right: $close-font-size + $alert-padding-x * 2;
|
||||
|
||||
// Adjust close link position
|
||||
.close {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: $alert-padding-y $alert-padding-x;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Alternate styles
|
||||
//
|
||||
// Generate contextual modifier classes for colorizing the alert.
|
||||
|
||||
@each $color, $value in $theme-colors {
|
||||
.alert-#{$color} {
|
||||
@include alert-variant(theme-color-level($color, $alert-bg-level), theme-color-level($color, $alert-border-level), theme-color-level($color, $alert-color-level));
|
||||
}
|
||||
}
|
53
app/static/bootstrap-4.2.1/scss/_badge.scss
vendored
Normal file
53
app/static/bootstrap-4.2.1/scss/_badge.scss
vendored
Normal file
@ -0,0 +1,53 @@
|
||||
// Base class
|
||||
//
|
||||
// Requires one of the contextual, color modifier classes for `color` and
|
||||
// `background-color`.
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: $badge-padding-y $badge-padding-x;
|
||||
font-size: $badge-font-size;
|
||||
font-weight: $badge-font-weight;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
vertical-align: baseline;
|
||||
@include border-radius($badge-border-radius);
|
||||
|
||||
@at-root a#{&} {
|
||||
@include hover-focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Empty badges collapse automatically
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Quick fix for badges in buttons
|
||||
.btn .badge {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
// Pill badges
|
||||
//
|
||||
// Make them extra rounded with a modifier to replace v3's badges.
|
||||
|
||||
.badge-pill {
|
||||
padding-right: $badge-pill-padding-x;
|
||||
padding-left: $badge-pill-padding-x;
|
||||
@include border-radius($badge-pill-border-radius);
|
||||
}
|
||||
|
||||
// Colors
|
||||
//
|
||||
// Contextual variations (linked badges get darker on :hover).
|
||||
|
||||
@each $color, $value in $theme-colors {
|
||||
.badge-#{$color} {
|
||||
@include badge-variant($value);
|
||||
}
|
||||
}
|
41
app/static/bootstrap-4.2.1/scss/_breadcrumb.scss
vendored
Normal file
41
app/static/bootstrap-4.2.1/scss/_breadcrumb.scss
vendored
Normal file
@ -0,0 +1,41 @@
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: $breadcrumb-padding-y $breadcrumb-padding-x;
|
||||
margin-bottom: $breadcrumb-margin-bottom;
|
||||
list-style: none;
|
||||
background-color: $breadcrumb-bg;
|
||||
@include border-radius($breadcrumb-border-radius);
|
||||
}
|
||||
|
||||
.breadcrumb-item {
|
||||
// The separator between breadcrumbs (by default, a forward-slash: "/")
|
||||
+ .breadcrumb-item {
|
||||
padding-left: $breadcrumb-item-padding;
|
||||
|
||||
&::before {
|
||||
display: inline-block; // Suppress underlining of the separator in modern browsers
|
||||
padding-right: $breadcrumb-item-padding;
|
||||
color: $breadcrumb-divider-color;
|
||||
content: $breadcrumb-divider;
|
||||
}
|
||||
}
|
||||
|
||||
// IE9-11 hack to properly handle hyperlink underlines for breadcrumbs built
|
||||
// without `<ul>`s. The `::before` pseudo-element generates an element
|
||||
// *within* the .breadcrumb-item and thereby inherits the `text-decoration`.
|
||||
//
|
||||
// To trick IE into suppressing the underline, we give the pseudo-element an
|
||||
// underline and then immediately remove it.
|
||||
+ .breadcrumb-item:hover::before {
|
||||
text-decoration: underline;
|
||||
}
|
||||
// stylelint-disable-next-line no-duplicate-selectors
|
||||
+ .breadcrumb-item:hover::before {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: $breadcrumb-active-color;
|
||||
}
|
||||
}
|
163
app/static/bootstrap-4.2.1/scss/_button-group.scss
vendored
Normal file
163
app/static/bootstrap-4.2.1/scss/_button-group.scss
vendored
Normal file
@ -0,0 +1,163 @@
|
||||
// stylelint-disable selector-no-qualifying-type
|
||||
|
||||
// Make the div behave like a button
|
||||
.btn-group,
|
||||
.btn-group-vertical {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
vertical-align: middle; // match .btn alignment given font-size hack above
|
||||
|
||||
> .btn {
|
||||
position: relative;
|
||||
flex: 1 1 auto;
|
||||
|
||||
// Bring the hover, focused, and "active" buttons to the front to overlay
|
||||
// the borders properly
|
||||
@include hover {
|
||||
z-index: 1;
|
||||
}
|
||||
&:focus,
|
||||
&:active,
|
||||
&.active {
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Optional: Group multiple button groups together for a toolbar
|
||||
.btn-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
|
||||
.input-group {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
// Prevent double borders when buttons are next to each other
|
||||
> .btn:not(:first-child),
|
||||
> .btn-group:not(:first-child) {
|
||||
margin-left: -$btn-border-width;
|
||||
}
|
||||
|
||||
// Reset rounded corners
|
||||
> .btn:not(:last-child):not(.dropdown-toggle),
|
||||
> .btn-group:not(:last-child) > .btn {
|
||||
@include border-right-radius(0);
|
||||
}
|
||||
|
||||
> .btn:not(:first-child),
|
||||
> .btn-group:not(:first-child) > .btn {
|
||||
@include border-left-radius(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Sizing
|
||||
//
|
||||
// Remix the default button sizing classes into new ones for easier manipulation.
|
||||
|
||||
.btn-group-sm > .btn { @extend .btn-sm; }
|
||||
.btn-group-lg > .btn { @extend .btn-lg; }
|
||||
|
||||
|
||||
//
|
||||
// Split button dropdowns
|
||||
//
|
||||
|
||||
.dropdown-toggle-split {
|
||||
padding-right: $btn-padding-x * .75;
|
||||
padding-left: $btn-padding-x * .75;
|
||||
|
||||
&::after,
|
||||
.dropup &::after,
|
||||
.dropright &::after {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.dropleft &::before {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-sm + .dropdown-toggle-split {
|
||||
padding-right: $btn-padding-x-sm * .75;
|
||||
padding-left: $btn-padding-x-sm * .75;
|
||||
}
|
||||
|
||||
.btn-lg + .dropdown-toggle-split {
|
||||
padding-right: $btn-padding-x-lg * .75;
|
||||
padding-left: $btn-padding-x-lg * .75;
|
||||
}
|
||||
|
||||
|
||||
// The clickable button for toggling the menu
|
||||
// Set the same inset shadow as the :active state
|
||||
.btn-group.show .dropdown-toggle {
|
||||
@include box-shadow($btn-active-box-shadow);
|
||||
|
||||
// Show no shadow for `.btn-link` since it has no other button styles.
|
||||
&.btn-link {
|
||||
@include box-shadow(none);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Vertical button groups
|
||||
//
|
||||
|
||||
.btn-group-vertical {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
|
||||
> .btn,
|
||||
> .btn-group {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
> .btn:not(:first-child),
|
||||
> .btn-group:not(:first-child) {
|
||||
margin-top: -$btn-border-width;
|
||||
}
|
||||
|
||||
// Reset rounded corners
|
||||
> .btn:not(:last-child):not(.dropdown-toggle),
|
||||
> .btn-group:not(:last-child) > .btn {
|
||||
@include border-bottom-radius(0);
|
||||
}
|
||||
|
||||
> .btn:not(:first-child),
|
||||
> .btn-group:not(:first-child) > .btn {
|
||||
@include border-top-radius(0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Checkbox and radio options
|
||||
//
|
||||
// In order to support the browser's form validation feedback, powered by the
|
||||
// `required` attribute, we have to "hide" the inputs via `clip`. We cannot use
|
||||
// `display: none;` or `visibility: hidden;` as that also hides the popover.
|
||||
// Simply visually hiding the inputs via `opacity` would leave them clickable in
|
||||
// certain cases which is prevented by using `clip` and `pointer-events`.
|
||||
// This way, we ensure a DOM element is visible to position the popover from.
|
||||
//
|
||||
// See https://github.com/twbs/bootstrap/pull/12794 and
|
||||
// https://github.com/twbs/bootstrap/pull/14559 for more information.
|
||||
|
||||
.btn-group-toggle {
|
||||
> .btn,
|
||||
> .btn-group > .btn {
|
||||
margin-bottom: 0; // Override default `<label>` value
|
||||
|
||||
input[type="radio"],
|
||||
input[type="checkbox"] {
|
||||
position: absolute;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
140
app/static/bootstrap-4.2.1/scss/_buttons.scss
vendored
Normal file
140
app/static/bootstrap-4.2.1/scss/_buttons.scss
vendored
Normal file
@ -0,0 +1,140 @@
|
||||
// stylelint-disable selector-no-qualifying-type
|
||||
|
||||
//
|
||||
// Base styles
|
||||
//
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
font-weight: $btn-font-weight;
|
||||
color: $body-color;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
user-select: none;
|
||||
background-color: transparent;
|
||||
border: $btn-border-width solid transparent;
|
||||
@include button-size($btn-padding-y, $btn-padding-x, $btn-font-size, $btn-line-height, $btn-border-radius);
|
||||
@include transition($btn-transition);
|
||||
|
||||
@include hover {
|
||||
color: $body-color;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&.focus {
|
||||
outline: 0;
|
||||
box-shadow: $btn-focus-box-shadow;
|
||||
}
|
||||
|
||||
// Disabled comes first so active can properly restyle
|
||||
&.disabled,
|
||||
&:disabled {
|
||||
opacity: $btn-disabled-opacity;
|
||||
@include box-shadow(none);
|
||||
}
|
||||
|
||||
// Opinionated: add "hand" cursor to non-disabled .btn elements
|
||||
&:not(:disabled):not(.disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:not(:disabled):not(.disabled):active,
|
||||
&:not(:disabled):not(.disabled).active {
|
||||
@include box-shadow($btn-active-box-shadow);
|
||||
|
||||
&:focus {
|
||||
@include box-shadow($btn-focus-box-shadow, $btn-active-box-shadow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Future-proof disabling of clicks on `<a>` elements
|
||||
a.btn.disabled,
|
||||
fieldset:disabled a.btn {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Alternate buttons
|
||||
//
|
||||
|
||||
@each $color, $value in $theme-colors {
|
||||
.btn-#{$color} {
|
||||
@include button-variant($value, $value);
|
||||
}
|
||||
}
|
||||
|
||||
@each $color, $value in $theme-colors {
|
||||
.btn-outline-#{$color} {
|
||||
@include button-outline-variant($value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Link buttons
|
||||
//
|
||||
|
||||
// Make a button look and behave like a link
|
||||
.btn-link {
|
||||
font-weight: $font-weight-normal;
|
||||
color: $link-color;
|
||||
|
||||
@include hover {
|
||||
color: $link-hover-color;
|
||||
text-decoration: $link-hover-decoration;
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&.focus {
|
||||
text-decoration: $link-hover-decoration;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&:disabled,
|
||||
&.disabled {
|
||||
color: $btn-link-disabled-color;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// No need for an active state here
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Button Sizes
|
||||
//
|
||||
|
||||
.btn-lg {
|
||||
@include button-size($btn-padding-y-lg, $btn-padding-x-lg, $btn-font-size-lg, $btn-line-height-lg, $btn-border-radius-lg);
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
@include button-size($btn-padding-y-sm, $btn-padding-x-sm, $btn-font-size-sm, $btn-line-height-sm, $btn-border-radius-sm);
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Block button
|
||||
//
|
||||
|
||||
.btn-block {
|
||||
display: block;
|
||||
width: 100%;
|
||||
|
||||
// Vertically space out multiple block buttons
|
||||
+ .btn-block {
|
||||
margin-top: $btn-block-spacing-y;
|
||||
}
|
||||
}
|
||||
|
||||
// Specificity overrides
|
||||
input[type="submit"],
|
||||
input[type="reset"],
|
||||
input[type="button"] {
|
||||
&.btn-block {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
310
app/static/bootstrap-4.2.1/scss/_card.scss
vendored
Normal file
310
app/static/bootstrap-4.2.1/scss/_card.scss
vendored
Normal file
@ -0,0 +1,310 @@
|
||||
//
|
||||
// Base styles
|
||||
//
|
||||
|
||||
.card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
word-wrap: break-word;
|
||||
background-color: $card-bg;
|
||||
background-clip: border-box;
|
||||
border: $card-border-width solid $card-border-color;
|
||||
@include border-radius($card-border-radius);
|
||||
|
||||
> hr {
|
||||
margin-right: 0;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
> .list-group:first-child {
|
||||
.list-group-item:first-child {
|
||||
@include border-top-radius($card-border-radius);
|
||||
}
|
||||
}
|
||||
|
||||
> .list-group:last-child {
|
||||
.list-group-item:last-child {
|
||||
@include border-bottom-radius($card-border-radius);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-body {
|
||||
// Enable `flex-grow: 1` for decks and groups so that card blocks take up
|
||||
// as much space as possible, ensuring footers are aligned to the bottom.
|
||||
flex: 1 1 auto;
|
||||
padding: $card-spacer-x;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
margin-bottom: $card-spacer-y;
|
||||
}
|
||||
|
||||
.card-subtitle {
|
||||
margin-top: -$card-spacer-y / 2;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.card-text:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.card-link {
|
||||
@include hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
+ .card-link {
|
||||
margin-left: $card-spacer-x;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Optional textual caps
|
||||
//
|
||||
|
||||
.card-header {
|
||||
padding: $card-spacer-y $card-spacer-x;
|
||||
margin-bottom: 0; // Removes the default margin-bottom of <hN>
|
||||
color: $card-cap-color;
|
||||
background-color: $card-cap-bg;
|
||||
border-bottom: $card-border-width solid $card-border-color;
|
||||
|
||||
&:first-child {
|
||||
@include border-radius($card-inner-border-radius $card-inner-border-radius 0 0);
|
||||
}
|
||||
|
||||
+ .list-group {
|
||||
.list-group-item:first-child {
|
||||
border-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
padding: $card-spacer-y $card-spacer-x;
|
||||
background-color: $card-cap-bg;
|
||||
border-top: $card-border-width solid $card-border-color;
|
||||
|
||||
&:last-child {
|
||||
@include border-radius(0 0 $card-inner-border-radius $card-inner-border-radius);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Header navs
|
||||
//
|
||||
|
||||
.card-header-tabs {
|
||||
margin-right: -$card-spacer-x / 2;
|
||||
margin-bottom: -$card-spacer-y;
|
||||
margin-left: -$card-spacer-x / 2;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.card-header-pills {
|
||||
margin-right: -$card-spacer-x / 2;
|
||||
margin-left: -$card-spacer-x / 2;
|
||||
}
|
||||
|
||||
// Card image
|
||||
.card-img-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding: $card-img-overlay-padding;
|
||||
}
|
||||
|
||||
.card-img {
|
||||
width: 100%; // Required because we use flexbox and this inherently applies align-self: stretch
|
||||
@include border-radius($card-inner-border-radius);
|
||||
}
|
||||
|
||||
// Card image caps
|
||||
.card-img-top {
|
||||
width: 100%; // Required because we use flexbox and this inherently applies align-self: stretch
|
||||
@include border-top-radius($card-inner-border-radius);
|
||||
}
|
||||
|
||||
.card-img-bottom {
|
||||
width: 100%; // Required because we use flexbox and this inherently applies align-self: stretch
|
||||
@include border-bottom-radius($card-inner-border-radius);
|
||||
}
|
||||
|
||||
|
||||
// Card deck
|
||||
|
||||
.card-deck {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.card {
|
||||
margin-bottom: $card-deck-margin;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
flex-flow: row wrap;
|
||||
margin-right: -$card-deck-margin;
|
||||
margin-left: -$card-deck-margin;
|
||||
|
||||
.card {
|
||||
display: flex;
|
||||
// Flexbugs #4: https://github.com/philipwalton/flexbugs#flexbug-4
|
||||
flex: 1 0 0%;
|
||||
flex-direction: column;
|
||||
margin-right: $card-deck-margin;
|
||||
margin-bottom: 0; // Override the default
|
||||
margin-left: $card-deck-margin;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Card groups
|
||||
//
|
||||
|
||||
.card-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
// The child selector allows nested `.card` within `.card-group`
|
||||
// to display properly.
|
||||
> .card {
|
||||
margin-bottom: $card-group-margin;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
flex-flow: row wrap;
|
||||
// The child selector allows nested `.card` within `.card-group`
|
||||
// to display properly.
|
||||
> .card {
|
||||
// Flexbugs #4: https://github.com/philipwalton/flexbugs#flexbug-4
|
||||
flex: 1 0 0%;
|
||||
margin-bottom: 0;
|
||||
|
||||
+ .card {
|
||||
margin-left: 0;
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
// Handle rounded corners
|
||||
@if $enable-rounded {
|
||||
&:first-child {
|
||||
@include border-right-radius(0);
|
||||
|
||||
.card-img-top,
|
||||
.card-header {
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
.card-img-bottom,
|
||||
.card-footer {
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
@include border-left-radius(0);
|
||||
|
||||
.card-img-top,
|
||||
.card-header {
|
||||
border-top-left-radius: 0;
|
||||
}
|
||||
.card-img-bottom,
|
||||
.card-footer {
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:only-child {
|
||||
@include border-radius($card-border-radius);
|
||||
|
||||
.card-img-top,
|
||||
.card-header {
|
||||
@include border-top-radius($card-border-radius);
|
||||
}
|
||||
.card-img-bottom,
|
||||
.card-footer {
|
||||
@include border-bottom-radius($card-border-radius);
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:first-child):not(:last-child):not(:only-child) {
|
||||
@include border-radius(0);
|
||||
|
||||
.card-img-top,
|
||||
.card-img-bottom,
|
||||
.card-header,
|
||||
.card-footer {
|
||||
@include border-radius(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Columns
|
||||
//
|
||||
|
||||
.card-columns {
|
||||
.card {
|
||||
margin-bottom: $card-columns-margin;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
column-count: $card-columns-count;
|
||||
column-gap: $card-columns-gap;
|
||||
orphans: 1;
|
||||
widows: 1;
|
||||
|
||||
.card {
|
||||
display: inline-block; // Don't let them vertically span multiple columns
|
||||
width: 100%; // Don't let their width change
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Accordion
|
||||
//
|
||||
|
||||
.accordion {
|
||||
.card {
|
||||
overflow: hidden;
|
||||
|
||||
&:not(:first-of-type) {
|
||||
.card-header:first-child {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
&:not(:last-of-type) {
|
||||
border-bottom: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:first-of-type {
|
||||
border-bottom: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
margin-bottom: -$card-border-width;
|
||||
}
|
||||
}
|
||||
}
|
198
app/static/bootstrap-4.2.1/scss/_carousel.scss
vendored
Normal file
198
app/static/bootstrap-4.2.1/scss/_carousel.scss
vendored
Normal file
@ -0,0 +1,198 @@
|
||||
// Notes on the classes:
|
||||
//
|
||||
// 1. .carousel.pointer-event should ideally be pan-y (to allow for users to scroll vertically)
|
||||
// even when their scroll action started on a carousel, but for compatibility (with Firefox)
|
||||
// we're preventing all actions instead
|
||||
// 2. The .carousel-item-left and .carousel-item-right is used to indicate where
|
||||
// the active slide is heading.
|
||||
// 3. .active.carousel-item is the current slide.
|
||||
// 4. .active.carousel-item-left and .active.carousel-item-right is the current
|
||||
// slide in its in-transition state. Only one of these occurs at a time.
|
||||
// 5. .carousel-item-next.carousel-item-left and .carousel-item-prev.carousel-item-right
|
||||
// is the upcoming slide in transition.
|
||||
|
||||
.carousel {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.carousel.pointer-event {
|
||||
touch-action: pan-y;
|
||||
}
|
||||
|
||||
.carousel-inner {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
@include clearfix();
|
||||
}
|
||||
|
||||
.carousel-item {
|
||||
position: relative;
|
||||
display: none;
|
||||
float: left;
|
||||
width: 100%;
|
||||
margin-right: -100%;
|
||||
backface-visibility: hidden;
|
||||
@include transition($carousel-transition);
|
||||
}
|
||||
|
||||
.carousel-item.active,
|
||||
.carousel-item-next,
|
||||
.carousel-item-prev {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.carousel-item-next:not(.carousel-item-left),
|
||||
.active.carousel-item-right {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
.carousel-item-prev:not(.carousel-item-right),
|
||||
.active.carousel-item-left {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Alternate transitions
|
||||
//
|
||||
|
||||
.carousel-fade {
|
||||
.carousel-item {
|
||||
opacity: 0;
|
||||
transition-property: opacity;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.carousel-item.active,
|
||||
.carousel-item-next.carousel-item-left,
|
||||
.carousel-item-prev.carousel-item-right {
|
||||
z-index: 1;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.active.carousel-item-left,
|
||||
.active.carousel-item-right {
|
||||
z-index: 0;
|
||||
opacity: 0;
|
||||
@include transition(0s $carousel-transition-duration opacity);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Left/right controls for nav
|
||||
//
|
||||
|
||||
.carousel-control-prev,
|
||||
.carousel-control-next {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
// Use flex for alignment (1-3)
|
||||
display: flex; // 1. allow flex styles
|
||||
align-items: center; // 2. vertically center contents
|
||||
justify-content: center; // 3. horizontally center contents
|
||||
width: $carousel-control-width;
|
||||
color: $carousel-control-color;
|
||||
text-align: center;
|
||||
opacity: $carousel-control-opacity;
|
||||
@include transition($carousel-control-transition);
|
||||
|
||||
// Hover/focus state
|
||||
@include hover-focus {
|
||||
color: $carousel-control-color;
|
||||
text-decoration: none;
|
||||
outline: 0;
|
||||
opacity: $carousel-control-hover-opacity;
|
||||
}
|
||||
}
|
||||
.carousel-control-prev {
|
||||
left: 0;
|
||||
@if $enable-gradients {
|
||||
background: linear-gradient(90deg, rgba($black, .25), rgba($black, .001));
|
||||
}
|
||||
}
|
||||
.carousel-control-next {
|
||||
right: 0;
|
||||
@if $enable-gradients {
|
||||
background: linear-gradient(270deg, rgba($black, .25), rgba($black, .001));
|
||||
}
|
||||
}
|
||||
|
||||
// Icons for within
|
||||
.carousel-control-prev-icon,
|
||||
.carousel-control-next-icon {
|
||||
display: inline-block;
|
||||
width: $carousel-control-icon-width;
|
||||
height: $carousel-control-icon-width;
|
||||
background: transparent no-repeat center center;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
.carousel-control-prev-icon {
|
||||
background-image: $carousel-control-prev-icon-bg;
|
||||
}
|
||||
.carousel-control-next-icon {
|
||||
background-image: $carousel-control-next-icon-bg;
|
||||
}
|
||||
|
||||
|
||||
// Optional indicator pips
|
||||
//
|
||||
// Add an ordered list with the following class and add a list item for each
|
||||
// slide your carousel holds.
|
||||
|
||||
.carousel-indicators {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 15;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-left: 0; // override <ol> default
|
||||
// Use the .carousel-control's width as margin so we don't overlay those
|
||||
margin-right: $carousel-control-width;
|
||||
margin-left: $carousel-control-width;
|
||||
list-style: none;
|
||||
|
||||
li {
|
||||
box-sizing: content-box;
|
||||
flex: 0 1 auto;
|
||||
width: $carousel-indicator-width;
|
||||
height: $carousel-indicator-height;
|
||||
margin-right: $carousel-indicator-spacer;
|
||||
margin-left: $carousel-indicator-spacer;
|
||||
text-indent: -999px;
|
||||
cursor: pointer;
|
||||
background-color: $carousel-indicator-active-bg;
|
||||
background-clip: padding-box;
|
||||
// Use transparent borders to increase the hit area by 10px on top and bottom.
|
||||
border-top: $carousel-indicator-hit-area-height solid transparent;
|
||||
border-bottom: $carousel-indicator-hit-area-height solid transparent;
|
||||
opacity: .5;
|
||||
@include transition($carousel-indicator-transition);
|
||||
}
|
||||
|
||||
.active {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Optional captions
|
||||
//
|
||||
//
|
||||
|
||||
.carousel-caption {
|
||||
position: absolute;
|
||||
right: (100% - $carousel-caption-width) / 2;
|
||||
bottom: 20px;
|
||||
left: (100% - $carousel-caption-width) / 2;
|
||||
z-index: 10;
|
||||
padding-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
color: $carousel-caption-color;
|
||||
text-align: center;
|
||||
}
|
44
app/static/bootstrap-4.2.1/scss/_close.scss
vendored
Normal file
44
app/static/bootstrap-4.2.1/scss/_close.scss
vendored
Normal file
@ -0,0 +1,44 @@
|
||||
.close {
|
||||
float: right;
|
||||
font-size: $close-font-size;
|
||||
font-weight: $close-font-weight;
|
||||
line-height: 1;
|
||||
color: $close-color;
|
||||
text-shadow: $close-text-shadow;
|
||||
opacity: .5;
|
||||
|
||||
// Override <a>'s hover style
|
||||
@include hover {
|
||||
color: $close-color;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:not(:disabled):not(.disabled) {
|
||||
@include hover-focus {
|
||||
opacity: .75;
|
||||
}
|
||||
|
||||
// Opinionated: add "hand" cursor to non-disabled .close elements
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
// Additional properties for button version
|
||||
// iOS requires the button element instead of an anchor tag.
|
||||
// If you want the anchor version, it requires `href="#"`.
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile
|
||||
|
||||
// stylelint-disable-next-line selector-no-qualifying-type
|
||||
button.close {
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
// Future-proof disabling of clicks on `<a>` elements
|
||||
|
||||
// stylelint-disable-next-line selector-no-qualifying-type
|
||||
a.close.disabled {
|
||||
pointer-events: none;
|
||||
}
|
48
app/static/bootstrap-4.2.1/scss/_code.scss
vendored
Normal file
48
app/static/bootstrap-4.2.1/scss/_code.scss
vendored
Normal file
@ -0,0 +1,48 @@
|
||||
// Inline code
|
||||
code {
|
||||
font-size: $code-font-size;
|
||||
color: $code-color;
|
||||
word-break: break-word;
|
||||
|
||||
// Streamline the style when inside anchors to avoid broken underline and more
|
||||
a > & {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
// User input typically entered via keyboard
|
||||
kbd {
|
||||
padding: $kbd-padding-y $kbd-padding-x;
|
||||
font-size: $kbd-font-size;
|
||||
color: $kbd-color;
|
||||
background-color: $kbd-bg;
|
||||
@include border-radius($border-radius-sm);
|
||||
@include box-shadow($kbd-box-shadow);
|
||||
|
||||
kbd {
|
||||
padding: 0;
|
||||
font-size: 100%;
|
||||
font-weight: $nested-kbd-font-weight;
|
||||
@include box-shadow(none);
|
||||
}
|
||||
}
|
||||
|
||||
// Blocks of code
|
||||
pre {
|
||||
display: block;
|
||||
font-size: $code-font-size;
|
||||
color: $pre-color;
|
||||
|
||||
// Account for some code outputs that place code tags in pre tags
|
||||
code {
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
word-break: normal;
|
||||
}
|
||||
}
|
||||
|
||||
// Enable scrollable blocks of code
|
||||
.pre-scrollable {
|
||||
max-height: $pre-scrollable-max-height;
|
||||
overflow-y: scroll;
|
||||
}
|
507
app/static/bootstrap-4.2.1/scss/_custom-forms.scss
vendored
Normal file
507
app/static/bootstrap-4.2.1/scss/_custom-forms.scss
vendored
Normal file
@ -0,0 +1,507 @@
|
||||
// Embedded icons from Open Iconic.
|
||||
// Released under MIT and copyright 2014 Waybury.
|
||||
// https://useiconic.com/open
|
||||
|
||||
|
||||
// Checkboxes and radios
|
||||
//
|
||||
// Base class takes care of all the key behavioral aspects.
|
||||
|
||||
.custom-control {
|
||||
position: relative;
|
||||
display: block;
|
||||
min-height: $font-size-base * $line-height-base;
|
||||
padding-left: $custom-control-gutter + $custom-control-indicator-size;
|
||||
}
|
||||
|
||||
.custom-control-inline {
|
||||
display: inline-flex;
|
||||
margin-right: $custom-control-spacer-x;
|
||||
}
|
||||
|
||||
.custom-control-input {
|
||||
position: absolute;
|
||||
z-index: -1; // Put the input behind the label so it doesn't overlay text
|
||||
opacity: 0;
|
||||
|
||||
&:checked ~ .custom-control-label::before {
|
||||
color: $custom-control-indicator-checked-color;
|
||||
border-color: $custom-control-indicator-checked-border-color;
|
||||
@include gradient-bg($custom-control-indicator-checked-bg);
|
||||
@include box-shadow($custom-control-indicator-checked-box-shadow);
|
||||
}
|
||||
|
||||
&:focus ~ .custom-control-label::before {
|
||||
// the mixin is not used here to make sure there is feedback
|
||||
@if $enable-shadows {
|
||||
box-shadow: $input-box-shadow, $input-focus-box-shadow;
|
||||
} @else {
|
||||
box-shadow: $custom-control-indicator-focus-box-shadow;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus:not(:checked) ~ .custom-control-label::before {
|
||||
border-color: $custom-control-indicator-focus-border-color;
|
||||
}
|
||||
|
||||
&:not(:disabled):active ~ .custom-control-label::before {
|
||||
color: $custom-control-indicator-active-color;
|
||||
background-color: $custom-control-indicator-active-bg;
|
||||
border-color: $custom-control-indicator-active-border-color;
|
||||
@include box-shadow($custom-control-indicator-active-box-shadow);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
~ .custom-control-label {
|
||||
color: $custom-control-label-disabled-color;
|
||||
|
||||
&::before {
|
||||
background-color: $custom-control-indicator-disabled-bg;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Custom control indicators
|
||||
//
|
||||
// Build the custom controls out of pseudo-elements.
|
||||
|
||||
.custom-control-label {
|
||||
position: relative;
|
||||
margin-bottom: 0;
|
||||
vertical-align: top;
|
||||
|
||||
// Background-color and (when enabled) gradient
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: ($font-size-base * $line-height-base - $custom-control-indicator-size) / 2;
|
||||
left: -($custom-control-gutter + $custom-control-indicator-size);
|
||||
display: block;
|
||||
width: $custom-control-indicator-size;
|
||||
height: $custom-control-indicator-size;
|
||||
pointer-events: none;
|
||||
content: "";
|
||||
background-color: $custom-control-indicator-bg;
|
||||
border: $custom-control-indicator-border-color solid $custom-control-indicator-border-width;
|
||||
@include box-shadow($custom-control-indicator-box-shadow);
|
||||
}
|
||||
|
||||
// Foreground (icon)
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: ($font-size-base * $line-height-base - $custom-control-indicator-size) / 2;
|
||||
left: -($custom-control-gutter + $custom-control-indicator-size);
|
||||
display: block;
|
||||
width: $custom-control-indicator-size;
|
||||
height: $custom-control-indicator-size;
|
||||
content: "";
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
background-size: $custom-control-indicator-bg-size;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Checkboxes
|
||||
//
|
||||
// Tweak just a few things for checkboxes.
|
||||
|
||||
.custom-checkbox {
|
||||
.custom-control-label::before {
|
||||
@include border-radius($custom-checkbox-indicator-border-radius);
|
||||
}
|
||||
|
||||
.custom-control-input:checked ~ .custom-control-label {
|
||||
&::after {
|
||||
background-image: $custom-checkbox-indicator-icon-checked;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-control-input:indeterminate ~ .custom-control-label {
|
||||
&::before {
|
||||
border-color: $custom-checkbox-indicator-indeterminate-border-color;
|
||||
@include gradient-bg($custom-checkbox-indicator-indeterminate-bg);
|
||||
@include box-shadow($custom-checkbox-indicator-indeterminate-box-shadow);
|
||||
}
|
||||
&::after {
|
||||
background-image: $custom-checkbox-indicator-icon-indeterminate;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-control-input:disabled {
|
||||
&:checked ~ .custom-control-label::before {
|
||||
background-color: $custom-control-indicator-checked-disabled-bg;
|
||||
}
|
||||
&:indeterminate ~ .custom-control-label::before {
|
||||
background-color: $custom-control-indicator-checked-disabled-bg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Radios
|
||||
//
|
||||
// Tweak just a few things for radios.
|
||||
|
||||
.custom-radio {
|
||||
.custom-control-label::before {
|
||||
border-radius: $custom-radio-indicator-border-radius;
|
||||
}
|
||||
|
||||
.custom-control-input:checked ~ .custom-control-label {
|
||||
&::after {
|
||||
background-image: $custom-radio-indicator-icon-checked;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-control-input:disabled {
|
||||
&:checked ~ .custom-control-label::before {
|
||||
background-color: $custom-control-indicator-checked-disabled-bg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// switches
|
||||
//
|
||||
// Tweak a few things for switches
|
||||
|
||||
.custom-switch {
|
||||
padding-left: $custom-switch-width + $custom-control-gutter;
|
||||
|
||||
.custom-control-label {
|
||||
&::before {
|
||||
left: -($custom-switch-width + $custom-control-gutter);
|
||||
width: $custom-switch-width;
|
||||
pointer-events: all;
|
||||
border-radius: $custom-switch-indicator-border-radius;
|
||||
}
|
||||
|
||||
&::after {
|
||||
top: calc(#{(($font-size-base * $line-height-base - $custom-control-indicator-size) / 2)} + #{$custom-control-indicator-border-width * 2});
|
||||
left: calc(#{-($custom-switch-width + $custom-control-gutter)} + #{$custom-control-indicator-border-width * 2});
|
||||
width: $custom-switch-indicator-size;
|
||||
height: $custom-switch-indicator-size;
|
||||
background-color: $custom-control-indicator-border-color;
|
||||
border-radius: $custom-switch-indicator-border-radius;
|
||||
@include transition(transform .15s ease-in-out, $custom-forms-transition);
|
||||
}
|
||||
}
|
||||
|
||||
.custom-control-input:checked ~ .custom-control-label {
|
||||
&::after {
|
||||
background-color: $custom-control-indicator-bg;
|
||||
transform: translateX($custom-switch-width - $custom-control-indicator-size);
|
||||
}
|
||||
}
|
||||
|
||||
.custom-control-input:disabled {
|
||||
&:checked ~ .custom-control-label::before {
|
||||
background-color: $custom-control-indicator-checked-disabled-bg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Select
|
||||
//
|
||||
// Replaces the browser default select with a custom one, mostly pulled from
|
||||
// https://primer.github.io/.
|
||||
//
|
||||
|
||||
.custom-select {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
height: $custom-select-height;
|
||||
padding: $custom-select-padding-y ($custom-select-padding-x + $custom-select-indicator-padding) $custom-select-padding-y $custom-select-padding-x;
|
||||
font-weight: $custom-select-font-weight;
|
||||
line-height: $custom-select-line-height;
|
||||
color: $custom-select-color;
|
||||
vertical-align: middle;
|
||||
background: $custom-select-background;
|
||||
background-color: $custom-select-bg;
|
||||
border: $custom-select-border-width solid $custom-select-border-color;
|
||||
@if $enable-rounded {
|
||||
border-radius: $custom-select-border-radius;
|
||||
} @else {
|
||||
border-radius: 0;
|
||||
}
|
||||
@include box-shadow($custom-select-box-shadow);
|
||||
appearance: none;
|
||||
|
||||
&:focus {
|
||||
border-color: $custom-select-focus-border-color;
|
||||
outline: 0;
|
||||
@if $enable-shadows {
|
||||
box-shadow: $custom-select-box-shadow, $custom-select-focus-box-shadow;
|
||||
} @else {
|
||||
box-shadow: $custom-select-focus-box-shadow;
|
||||
}
|
||||
|
||||
&::-ms-value {
|
||||
// For visual consistency with other platforms/browsers,
|
||||
// suppress the default white text on blue background highlight given to
|
||||
// the selected option text when the (still closed) <select> receives focus
|
||||
// in IE and (under certain conditions) Edge.
|
||||
// See https://github.com/twbs/bootstrap/issues/19398.
|
||||
color: $input-color;
|
||||
background-color: $input-bg;
|
||||
}
|
||||
}
|
||||
|
||||
&[multiple],
|
||||
&[size]:not([size="1"]) {
|
||||
height: auto;
|
||||
padding-right: $custom-select-padding-x;
|
||||
background-image: none;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: $custom-select-disabled-color;
|
||||
background-color: $custom-select-disabled-bg;
|
||||
}
|
||||
|
||||
// Hides the default caret in IE11
|
||||
&::-ms-expand {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-select-sm {
|
||||
height: $custom-select-height-sm;
|
||||
padding-top: $custom-select-padding-y-sm;
|
||||
padding-bottom: $custom-select-padding-y-sm;
|
||||
padding-left: $custom-select-padding-x-sm;
|
||||
font-size: $custom-select-font-size-sm;
|
||||
}
|
||||
|
||||
.custom-select-lg {
|
||||
height: $custom-select-height-lg;
|
||||
padding-top: $custom-select-padding-y-lg;
|
||||
padding-bottom: $custom-select-padding-y-lg;
|
||||
padding-left: $custom-select-padding-x-lg;
|
||||
font-size: $custom-select-font-size-lg;
|
||||
}
|
||||
|
||||
|
||||
// File
|
||||
//
|
||||
// Custom file input.
|
||||
|
||||
.custom-file {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
height: $custom-file-height;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.custom-file-input {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
height: $custom-file-height;
|
||||
margin: 0;
|
||||
opacity: 0;
|
||||
|
||||
&:focus ~ .custom-file-label {
|
||||
border-color: $custom-file-focus-border-color;
|
||||
box-shadow: $custom-file-focus-box-shadow;
|
||||
}
|
||||
|
||||
&:disabled ~ .custom-file-label {
|
||||
background-color: $custom-file-disabled-bg;
|
||||
}
|
||||
|
||||
@each $lang, $value in $custom-file-text {
|
||||
&:lang(#{$lang}) ~ .custom-file-label::after {
|
||||
content: $value;
|
||||
}
|
||||
}
|
||||
|
||||
~ .custom-file-label[data-browse]::after {
|
||||
content: attr(data-browse);
|
||||
}
|
||||
}
|
||||
|
||||
.custom-file-label {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
height: $custom-file-height;
|
||||
padding: $custom-file-padding-y $custom-file-padding-x;
|
||||
font-weight: $custom-file-font-weight;
|
||||
line-height: $custom-file-line-height;
|
||||
color: $custom-file-color;
|
||||
background-color: $custom-file-bg;
|
||||
border: $custom-file-border-width solid $custom-file-border-color;
|
||||
@include border-radius($custom-file-border-radius);
|
||||
@include box-shadow($custom-file-box-shadow);
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 3;
|
||||
display: block;
|
||||
height: $custom-file-height-inner;
|
||||
padding: $custom-file-padding-y $custom-file-padding-x;
|
||||
line-height: $custom-file-line-height;
|
||||
color: $custom-file-button-color;
|
||||
content: "Browse";
|
||||
@include gradient-bg($custom-file-button-bg);
|
||||
border-left: inherit;
|
||||
@include border-radius(0 $custom-file-border-radius $custom-file-border-radius 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Range
|
||||
//
|
||||
// Style range inputs the same across browsers. Vendor-specific rules for pseudo
|
||||
// elements cannot be mixed. As such, there are no shared styles for focus or
|
||||
// active states on prefixed selectors.
|
||||
|
||||
.custom-range {
|
||||
width: 100%;
|
||||
height: calc(#{$custom-range-thumb-height} + #{$custom-range-thumb-focus-box-shadow-width * 2});
|
||||
padding: 0; // Need to reset padding
|
||||
background-color: transparent;
|
||||
appearance: none;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
|
||||
// Pseudo-elements must be split across multiple rulesets to have an effect.
|
||||
// No box-shadow() mixin for focus accessibility.
|
||||
&::-webkit-slider-thumb { box-shadow: $custom-range-thumb-focus-box-shadow; }
|
||||
&::-moz-range-thumb { box-shadow: $custom-range-thumb-focus-box-shadow; }
|
||||
&::-ms-thumb { box-shadow: $custom-range-thumb-focus-box-shadow; }
|
||||
}
|
||||
|
||||
&::-moz-focus-outer {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
&::-webkit-slider-thumb {
|
||||
width: $custom-range-thumb-width;
|
||||
height: $custom-range-thumb-height;
|
||||
margin-top: ($custom-range-track-height - $custom-range-thumb-height) / 2; // Webkit specific
|
||||
@include gradient-bg($custom-range-thumb-bg);
|
||||
border: $custom-range-thumb-border;
|
||||
@include border-radius($custom-range-thumb-border-radius);
|
||||
@include box-shadow($custom-range-thumb-box-shadow);
|
||||
@include transition($custom-forms-transition);
|
||||
appearance: none;
|
||||
|
||||
&:active {
|
||||
@include gradient-bg($custom-range-thumb-active-bg);
|
||||
}
|
||||
}
|
||||
|
||||
&::-webkit-slider-runnable-track {
|
||||
width: $custom-range-track-width;
|
||||
height: $custom-range-track-height;
|
||||
color: transparent; // Why?
|
||||
cursor: $custom-range-track-cursor;
|
||||
background-color: $custom-range-track-bg;
|
||||
border-color: transparent;
|
||||
@include border-radius($custom-range-track-border-radius);
|
||||
@include box-shadow($custom-range-track-box-shadow);
|
||||
}
|
||||
|
||||
&::-moz-range-thumb {
|
||||
width: $custom-range-thumb-width;
|
||||
height: $custom-range-thumb-height;
|
||||
@include gradient-bg($custom-range-thumb-bg);
|
||||
border: $custom-range-thumb-border;
|
||||
@include border-radius($custom-range-thumb-border-radius);
|
||||
@include box-shadow($custom-range-thumb-box-shadow);
|
||||
@include transition($custom-forms-transition);
|
||||
appearance: none;
|
||||
|
||||
&:active {
|
||||
@include gradient-bg($custom-range-thumb-active-bg);
|
||||
}
|
||||
}
|
||||
|
||||
&::-moz-range-track {
|
||||
width: $custom-range-track-width;
|
||||
height: $custom-range-track-height;
|
||||
color: transparent;
|
||||
cursor: $custom-range-track-cursor;
|
||||
background-color: $custom-range-track-bg;
|
||||
border-color: transparent; // Firefox specific?
|
||||
@include border-radius($custom-range-track-border-radius);
|
||||
@include box-shadow($custom-range-track-box-shadow);
|
||||
}
|
||||
|
||||
&::-ms-thumb {
|
||||
width: $custom-range-thumb-width;
|
||||
height: $custom-range-thumb-height;
|
||||
margin-top: 0; // Edge specific
|
||||
margin-right: $custom-range-thumb-focus-box-shadow-width; // Workaround that overflowed box-shadow is hidden.
|
||||
margin-left: $custom-range-thumb-focus-box-shadow-width; // Workaround that overflowed box-shadow is hidden.
|
||||
@include gradient-bg($custom-range-thumb-bg);
|
||||
border: $custom-range-thumb-border;
|
||||
@include border-radius($custom-range-thumb-border-radius);
|
||||
@include box-shadow($custom-range-thumb-box-shadow);
|
||||
@include transition($custom-forms-transition);
|
||||
appearance: none;
|
||||
|
||||
&:active {
|
||||
@include gradient-bg($custom-range-thumb-active-bg);
|
||||
}
|
||||
}
|
||||
|
||||
&::-ms-track {
|
||||
width: $custom-range-track-width;
|
||||
height: $custom-range-track-height;
|
||||
color: transparent;
|
||||
cursor: $custom-range-track-cursor;
|
||||
background-color: transparent;
|
||||
border-color: transparent;
|
||||
border-width: $custom-range-thumb-height / 2;
|
||||
@include box-shadow($custom-range-track-box-shadow);
|
||||
}
|
||||
|
||||
&::-ms-fill-lower {
|
||||
background-color: $custom-range-track-bg;
|
||||
@include border-radius($custom-range-track-border-radius);
|
||||
}
|
||||
|
||||
&::-ms-fill-upper {
|
||||
margin-right: 15px; // arbitrary?
|
||||
background-color: $custom-range-track-bg;
|
||||
@include border-radius($custom-range-track-border-radius);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
&::-webkit-slider-thumb {
|
||||
background-color: $custom-range-thumb-disabled-bg;
|
||||
}
|
||||
|
||||
&::-webkit-slider-runnable-track {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&::-moz-range-thumb {
|
||||
background-color: $custom-range-thumb-disabled-bg;
|
||||
}
|
||||
|
||||
&::-moz-range-track {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&::-ms-thumb {
|
||||
background-color: $custom-range-thumb-disabled-bg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.custom-control-label::before,
|
||||
.custom-file-label,
|
||||
.custom-select {
|
||||
@include transition($custom-forms-transition);
|
||||
}
|
191
app/static/bootstrap-4.2.1/scss/_dropdown.scss
vendored
Normal file
191
app/static/bootstrap-4.2.1/scss/_dropdown.scss
vendored
Normal file
@ -0,0 +1,191 @@
|
||||
// The dropdown wrapper (`<div>`)
|
||||
.dropup,
|
||||
.dropright,
|
||||
.dropdown,
|
||||
.dropleft {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dropdown-toggle {
|
||||
// Generate the caret automatically
|
||||
@include caret;
|
||||
}
|
||||
|
||||
// The dropdown menu
|
||||
.dropdown-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
z-index: $zindex-dropdown;
|
||||
display: none; // none by default, but block on "open" of the menu
|
||||
float: left;
|
||||
min-width: $dropdown-min-width;
|
||||
padding: $dropdown-padding-y 0;
|
||||
margin: $dropdown-spacer 0 0; // override default ul
|
||||
font-size: $font-size-base; // Redeclare because nesting can cause inheritance issues
|
||||
color: $body-color;
|
||||
text-align: left; // Ensures proper alignment if parent has it changed (e.g., modal footer)
|
||||
list-style: none;
|
||||
background-color: $dropdown-bg;
|
||||
background-clip: padding-box;
|
||||
border: $dropdown-border-width solid $dropdown-border-color;
|
||||
@include border-radius($dropdown-border-radius);
|
||||
@include box-shadow($dropdown-box-shadow);
|
||||
}
|
||||
|
||||
@each $breakpoint in map-keys($grid-breakpoints) {
|
||||
@include media-breakpoint-up($breakpoint) {
|
||||
$infix: breakpoint-infix($breakpoint, $grid-breakpoints);
|
||||
|
||||
.dropdown-menu#{$infix}-right {
|
||||
right: 0;
|
||||
left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@each $breakpoint in map-keys($grid-breakpoints) {
|
||||
@include media-breakpoint-up($breakpoint) {
|
||||
$infix: breakpoint-infix($breakpoint, $grid-breakpoints);
|
||||
|
||||
.dropdown-menu#{$infix}-left {
|
||||
right: auto;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Allow for dropdowns to go bottom up (aka, dropup-menu)
|
||||
// Just add .dropup after the standard .dropdown class and you're set.
|
||||
.dropup {
|
||||
.dropdown-menu {
|
||||
top: auto;
|
||||
bottom: 100%;
|
||||
margin-top: 0;
|
||||
margin-bottom: $dropdown-spacer;
|
||||
}
|
||||
|
||||
.dropdown-toggle {
|
||||
@include caret(up);
|
||||
}
|
||||
}
|
||||
|
||||
.dropright {
|
||||
.dropdown-menu {
|
||||
top: 0;
|
||||
right: auto;
|
||||
left: 100%;
|
||||
margin-top: 0;
|
||||
margin-left: $dropdown-spacer;
|
||||
}
|
||||
|
||||
.dropdown-toggle {
|
||||
@include caret(right);
|
||||
&::after {
|
||||
vertical-align: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropleft {
|
||||
.dropdown-menu {
|
||||
top: 0;
|
||||
right: 100%;
|
||||
left: auto;
|
||||
margin-top: 0;
|
||||
margin-right: $dropdown-spacer;
|
||||
}
|
||||
|
||||
.dropdown-toggle {
|
||||
@include caret(left);
|
||||
&::before {
|
||||
vertical-align: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// When enabled Popper.js, reset basic dropdown position
|
||||
// stylelint-disable-next-line no-duplicate-selectors
|
||||
.dropdown-menu {
|
||||
&[x-placement^="top"],
|
||||
&[x-placement^="right"],
|
||||
&[x-placement^="bottom"],
|
||||
&[x-placement^="left"] {
|
||||
right: auto;
|
||||
bottom: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// Dividers (basically an `<hr>`) within the dropdown
|
||||
.dropdown-divider {
|
||||
@include nav-divider($dropdown-divider-bg);
|
||||
}
|
||||
|
||||
// Links, buttons, and more within the dropdown menu
|
||||
//
|
||||
// `<button>`-specific styles are denoted with `// For <button>s`
|
||||
.dropdown-item {
|
||||
display: block;
|
||||
width: 100%; // For `<button>`s
|
||||
padding: $dropdown-item-padding-y $dropdown-item-padding-x;
|
||||
clear: both;
|
||||
font-weight: $font-weight-normal;
|
||||
color: $dropdown-link-color;
|
||||
text-align: inherit; // For `<button>`s
|
||||
white-space: nowrap; // prevent links from randomly breaking onto new lines
|
||||
background-color: transparent; // For `<button>`s
|
||||
border: 0; // For `<button>`s
|
||||
|
||||
&:first-child {
|
||||
@include border-top-radius($dropdown-inner-border-radius);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
@include border-bottom-radius($dropdown-inner-border-radius);
|
||||
}
|
||||
|
||||
@include hover-focus {
|
||||
color: $dropdown-link-hover-color;
|
||||
text-decoration: none;
|
||||
@include gradient-bg($dropdown-link-hover-bg);
|
||||
}
|
||||
|
||||
&.active,
|
||||
&:active {
|
||||
color: $dropdown-link-active-color;
|
||||
text-decoration: none;
|
||||
@include gradient-bg($dropdown-link-active-bg);
|
||||
}
|
||||
|
||||
&.disabled,
|
||||
&:disabled {
|
||||
color: $dropdown-link-disabled-color;
|
||||
pointer-events: none;
|
||||
background-color: transparent;
|
||||
// Remove CSS gradients if they're enabled
|
||||
@if $enable-gradients {
|
||||
background-image: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
// Dropdown section headers
|
||||
.dropdown-header {
|
||||
display: block;
|
||||
padding: $dropdown-padding-y $dropdown-item-padding-x;
|
||||
margin-bottom: 0; // for use with heading elements
|
||||
font-size: $font-size-sm;
|
||||
color: $dropdown-header-color;
|
||||
white-space: nowrap; // as with > li > a
|
||||
}
|
||||
|
||||
// Dropdown text
|
||||
.dropdown-item-text {
|
||||
display: block;
|
||||
padding: $dropdown-item-padding-y $dropdown-item-padding-x;
|
||||
color: $dropdown-link-color;
|
||||
}
|
334
app/static/bootstrap-4.2.1/scss/_forms.scss
vendored
Normal file
334
app/static/bootstrap-4.2.1/scss/_forms.scss
vendored
Normal file
@ -0,0 +1,334 @@
|
||||
// stylelint-disable selector-no-qualifying-type
|
||||
|
||||
//
|
||||
// Textual form controls
|
||||
//
|
||||
|
||||
.form-control {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: $input-height;
|
||||
padding: $input-padding-y $input-padding-x;
|
||||
font-size: $input-font-size;
|
||||
font-weight: $input-font-weight;
|
||||
line-height: $input-line-height;
|
||||
color: $input-color;
|
||||
background-color: $input-bg;
|
||||
background-clip: padding-box;
|
||||
border: $input-border-width solid $input-border-color;
|
||||
|
||||
// Note: This has no effect on <select>s in some browsers, due to the limited stylability of `<select>`s in CSS.
|
||||
@if $enable-rounded {
|
||||
// Manually use the if/else instead of the mixin to account for iOS override
|
||||
border-radius: $input-border-radius;
|
||||
} @else {
|
||||
// Otherwise undo the iOS default
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
@include box-shadow($input-box-shadow);
|
||||
@include transition($input-transition);
|
||||
|
||||
// Unstyle the caret on `<select>`s in IE10+.
|
||||
&::-ms-expand {
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
// Customize the `:focus` state to imitate native WebKit styles.
|
||||
@include form-control-focus();
|
||||
|
||||
// Placeholder
|
||||
&::placeholder {
|
||||
color: $input-placeholder-color;
|
||||
// Override Firefox's unusual default opacity; see https://github.com/twbs/bootstrap/pull/11526.
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
// Disabled and read-only inputs
|
||||
//
|
||||
// HTML5 says that controls under a fieldset > legend:first-child won't be
|
||||
// disabled if the fieldset is disabled. Due to implementation difficulty, we
|
||||
// don't honor that edge case; we style them as disabled anyway.
|
||||
&:disabled,
|
||||
&[readonly] {
|
||||
background-color: $input-disabled-bg;
|
||||
// iOS fix for unreadable disabled content; see https://github.com/twbs/bootstrap/issues/11655.
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
select.form-control {
|
||||
&:focus::-ms-value {
|
||||
// Suppress the nested default white text on blue background highlight given to
|
||||
// the selected option text when the (still closed) <select> receives focus
|
||||
// in IE and (under certain conditions) Edge, as it looks bad and cannot be made to
|
||||
// match the appearance of the native widget.
|
||||
// See https://github.com/twbs/bootstrap/issues/19398.
|
||||
color: $input-color;
|
||||
background-color: $input-bg;
|
||||
}
|
||||
}
|
||||
|
||||
// Make file inputs better match text inputs by forcing them to new lines.
|
||||
.form-control-file,
|
||||
.form-control-range {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Labels
|
||||
//
|
||||
|
||||
// For use with horizontal and inline forms, when you need the label (or legend)
|
||||
// text to align with the form controls.
|
||||
.col-form-label {
|
||||
padding-top: calc(#{$input-padding-y} + #{$input-border-width});
|
||||
padding-bottom: calc(#{$input-padding-y} + #{$input-border-width});
|
||||
margin-bottom: 0; // Override the `<label>/<legend>` default
|
||||
font-size: inherit; // Override the `<legend>` default
|
||||
line-height: $input-line-height;
|
||||
}
|
||||
|
||||
.col-form-label-lg {
|
||||
padding-top: calc(#{$input-padding-y-lg} + #{$input-border-width});
|
||||
padding-bottom: calc(#{$input-padding-y-lg} + #{$input-border-width});
|
||||
font-size: $input-font-size-lg;
|
||||
line-height: $input-line-height-lg;
|
||||
}
|
||||
|
||||
.col-form-label-sm {
|
||||
padding-top: calc(#{$input-padding-y-sm} + #{$input-border-width});
|
||||
padding-bottom: calc(#{$input-padding-y-sm} + #{$input-border-width});
|
||||
font-size: $input-font-size-sm;
|
||||
line-height: $input-line-height-sm;
|
||||
}
|
||||
|
||||
|
||||
// Readonly controls as plain text
|
||||
//
|
||||
// Apply class to a readonly input to make it appear like regular plain
|
||||
// text (without any border, background color, focus indicator)
|
||||
|
||||
.form-control-plaintext {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding-top: $input-padding-y;
|
||||
padding-bottom: $input-padding-y;
|
||||
margin-bottom: 0; // match inputs if this class comes on inputs with default margins
|
||||
line-height: $input-line-height;
|
||||
color: $input-plaintext-color;
|
||||
background-color: transparent;
|
||||
border: solid transparent;
|
||||
border-width: $input-border-width 0;
|
||||
|
||||
&.form-control-sm,
|
||||
&.form-control-lg {
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Form control sizing
|
||||
//
|
||||
// Build on `.form-control` with modifier classes to decrease or increase the
|
||||
// height and font-size of form controls.
|
||||
//
|
||||
// Repeated in `_input_group.scss` to avoid Sass extend issues.
|
||||
|
||||
.form-control-sm {
|
||||
height: $input-height-sm;
|
||||
padding: $input-padding-y-sm $input-padding-x-sm;
|
||||
font-size: $input-font-size-sm;
|
||||
line-height: $input-line-height-sm;
|
||||
@include border-radius($input-border-radius-sm);
|
||||
}
|
||||
|
||||
.form-control-lg {
|
||||
height: $input-height-lg;
|
||||
padding: $input-padding-y-lg $input-padding-x-lg;
|
||||
font-size: $input-font-size-lg;
|
||||
line-height: $input-line-height-lg;
|
||||
@include border-radius($input-border-radius-lg);
|
||||
}
|
||||
|
||||
// stylelint-disable-next-line no-duplicate-selectors
|
||||
select.form-control {
|
||||
&[size],
|
||||
&[multiple] {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// stylelint-disable-next-line no-duplicate-selectors
|
||||
textarea.form-control {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
// Form groups
|
||||
//
|
||||
// Designed to help with the organization and spacing of vertical forms. For
|
||||
// horizontal forms, use the predefined grid classes.
|
||||
|
||||
.form-group {
|
||||
margin-bottom: $form-group-margin-bottom;
|
||||
}
|
||||
|
||||
.form-text {
|
||||
display: block;
|
||||
margin-top: $form-text-margin-top;
|
||||
}
|
||||
|
||||
|
||||
// Form grid
|
||||
//
|
||||
// Special replacement for our grid system's `.row` for tighter form layouts.
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-right: -$form-grid-gutter-width / 2;
|
||||
margin-left: -$form-grid-gutter-width / 2;
|
||||
|
||||
> .col,
|
||||
> [class*="col-"] {
|
||||
padding-right: $form-grid-gutter-width / 2;
|
||||
padding-left: $form-grid-gutter-width / 2;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Checkboxes and radios
|
||||
//
|
||||
// Indent the labels to position radios/checkboxes as hanging controls.
|
||||
|
||||
.form-check {
|
||||
position: relative;
|
||||
display: block;
|
||||
padding-left: $form-check-input-gutter;
|
||||
}
|
||||
|
||||
.form-check-input {
|
||||
position: absolute;
|
||||
margin-top: $form-check-input-margin-y;
|
||||
margin-left: -$form-check-input-gutter;
|
||||
|
||||
&:disabled ~ .form-check-label {
|
||||
color: $text-muted;
|
||||
}
|
||||
}
|
||||
|
||||
.form-check-label {
|
||||
margin-bottom: 0; // Override default `<label>` bottom margin
|
||||
}
|
||||
|
||||
.form-check-inline {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding-left: 0; // Override base .form-check
|
||||
margin-right: $form-check-inline-margin-x;
|
||||
|
||||
// Undo .form-check-input defaults and add some `margin-right`.
|
||||
.form-check-input {
|
||||
position: static;
|
||||
margin-top: 0;
|
||||
margin-right: $form-check-inline-input-margin-x;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Form validation
|
||||
//
|
||||
// Provide feedback to users when form field values are valid or invalid. Works
|
||||
// primarily for client-side validation via scoped `:invalid` and `:valid`
|
||||
// pseudo-classes but also includes `.is-invalid` and `.is-valid` classes for
|
||||
// server side validation.
|
||||
|
||||
@include form-validation-state("valid", $form-feedback-valid-color);
|
||||
@include form-validation-state("invalid", $form-feedback-invalid-color);
|
||||
|
||||
// Inline forms
|
||||
//
|
||||
// Make forms appear inline(-block) by adding the `.form-inline` class. Inline
|
||||
// forms begin stacked on extra small (mobile) devices and then go inline when
|
||||
// viewports reach <768px.
|
||||
//
|
||||
// Requires wrapping inputs and labels with `.form-group` for proper display of
|
||||
// default HTML form controls and our custom form controls (e.g., input groups).
|
||||
|
||||
.form-inline {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
align-items: center; // Prevent shorter elements from growing to same height as others (e.g., small buttons growing to normal sized button height)
|
||||
|
||||
// Because we use flex, the initial sizing of checkboxes is collapsed and
|
||||
// doesn't occupy the full-width (which is what we want for xs grid tier),
|
||||
// so we force that here.
|
||||
.form-check {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// Kick in the inline
|
||||
@include media-breakpoint-up(sm) {
|
||||
label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
// Inline-block all the things for "inline"
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
flex-flow: row wrap;
|
||||
align-items: center;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
// Allow folks to *not* use `.form-group`
|
||||
.form-control {
|
||||
display: inline-block;
|
||||
width: auto; // Prevent labels from stacking above inputs in `.form-group`
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
// Make static controls behave like regular ones
|
||||
.form-control-plaintext {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.input-group,
|
||||
.custom-select {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
// Remove default margin on radios/checkboxes that were used for stacking, and
|
||||
// then undo the floating of radios and checkboxes to match.
|
||||
.form-check {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: auto;
|
||||
padding-left: 0;
|
||||
}
|
||||
.form-check-input {
|
||||
position: relative;
|
||||
margin-top: 0;
|
||||
margin-right: $form-check-input-margin-x;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.custom-control {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.custom-control-label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
86
app/static/bootstrap-4.2.1/scss/_functions.scss
vendored
Normal file
86
app/static/bootstrap-4.2.1/scss/_functions.scss
vendored
Normal file
@ -0,0 +1,86 @@
|
||||
// Bootstrap functions
|
||||
//
|
||||
// Utility mixins and functions for evaluating source code across our variables, maps, and mixins.
|
||||
|
||||
// Ascending
|
||||
// Used to evaluate Sass maps like our grid breakpoints.
|
||||
@mixin _assert-ascending($map, $map-name) {
|
||||
$prev-key: null;
|
||||
$prev-num: null;
|
||||
@each $key, $num in $map {
|
||||
@if $prev-num == null or unit($num) == "%" {
|
||||
// Do nothing
|
||||
} @else if not comparable($prev-num, $num) {
|
||||
@warn "Potentially invalid value for #{$map-name}: This map must be in ascending order, but key '#{$key}' has value #{$num} whose unit makes it incomparable to #{$prev-num}, the value of the previous key '#{$prev-key}' !";
|
||||
} @else if $prev-num >= $num {
|
||||
@warn "Invalid value for #{$map-name}: This map must be in ascending order, but key '#{$key}' has value #{$num} which isn't greater than #{$prev-num}, the value of the previous key '#{$prev-key}' !";
|
||||
}
|
||||
$prev-key: $key;
|
||||
$prev-num: $num;
|
||||
}
|
||||
}
|
||||
|
||||
// Starts at zero
|
||||
// Another grid mixin that ensures the min-width of the lowest breakpoint starts at 0.
|
||||
@mixin _assert-starts-at-zero($map) {
|
||||
$values: map-values($map);
|
||||
$first-value: nth($values, 1);
|
||||
@if $first-value != 0 {
|
||||
@warn "First breakpoint in `$grid-breakpoints` must start at 0, but starts at #{$first-value}.";
|
||||
}
|
||||
}
|
||||
|
||||
// Replace `$search` with `$replace` in `$string`
|
||||
// Used on our SVG icon backgrounds for custom forms.
|
||||
//
|
||||
// @author Hugo Giraudel
|
||||
// @param {String} $string - Initial string
|
||||
// @param {String} $search - Substring to replace
|
||||
// @param {String} $replace ('') - New value
|
||||
// @return {String} - Updated string
|
||||
@function str-replace($string, $search, $replace: "") {
|
||||
$index: str-index($string, $search);
|
||||
|
||||
@if $index {
|
||||
@return str-slice($string, 1, $index - 1) + $replace + str-replace(str-slice($string, $index + str-length($search)), $search, $replace);
|
||||
}
|
||||
|
||||
@return $string;
|
||||
}
|
||||
|
||||
// Color contrast
|
||||
@function color-yiq($color, $dark: $yiq-text-dark, $light: $yiq-text-light) {
|
||||
$r: red($color);
|
||||
$g: green($color);
|
||||
$b: blue($color);
|
||||
|
||||
$yiq: (($r * 299) + ($g * 587) + ($b * 114)) / 1000;
|
||||
|
||||
@if ($yiq >= $yiq-contrasted-threshold) {
|
||||
@return $dark;
|
||||
} @else {
|
||||
@return $light;
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve color Sass maps
|
||||
@function color($key: "blue") {
|
||||
@return map-get($colors, $key);
|
||||
}
|
||||
|
||||
@function theme-color($key: "primary") {
|
||||
@return map-get($theme-colors, $key);
|
||||
}
|
||||
|
||||
@function gray($key: "100") {
|
||||
@return map-get($grays, $key);
|
||||
}
|
||||
|
||||
// Request a theme color level
|
||||
@function theme-color-level($color-name: "primary", $level: 0) {
|
||||
$color: theme-color($color-name);
|
||||
$color-base: if($level > 0, $black, $white);
|
||||
$level: abs($level);
|
||||
|
||||
@return mix($color-base, $color, $level * $theme-color-interval);
|
||||
}
|
52
app/static/bootstrap-4.2.1/scss/_grid.scss
vendored
Normal file
52
app/static/bootstrap-4.2.1/scss/_grid.scss
vendored
Normal file
@ -0,0 +1,52 @@
|
||||
// Container widths
|
||||
//
|
||||
// Set the container width, and override it for fixed navbars in media queries.
|
||||
|
||||
@if $enable-grid-classes {
|
||||
.container {
|
||||
@include make-container();
|
||||
@include make-container-max-widths();
|
||||
}
|
||||
}
|
||||
|
||||
// Fluid container
|
||||
//
|
||||
// Utilizes the mixin meant for fixed width containers, but with 100% width for
|
||||
// fluid, full width layouts.
|
||||
|
||||
@if $enable-grid-classes {
|
||||
.container-fluid {
|
||||
@include make-container();
|
||||
}
|
||||
}
|
||||
|
||||
// Row
|
||||
//
|
||||
// Rows contain and clear the floats of your columns.
|
||||
|
||||
@if $enable-grid-classes {
|
||||
.row {
|
||||
@include make-row();
|
||||
}
|
||||
|
||||
// Remove the negative margin from default .row, then the horizontal padding
|
||||
// from all immediate children columns (to prevent runaway style inheritance).
|
||||
.no-gutters {
|
||||
margin-right: 0;
|
||||
margin-left: 0;
|
||||
|
||||
> .col,
|
||||
> [class*="col-"] {
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Columns
|
||||
//
|
||||
// Common styles for small and large grid columns
|
||||
|
||||
@if $enable-grid-classes {
|
||||
@include make-grid-columns();
|
||||
}
|
42
app/static/bootstrap-4.2.1/scss/_images.scss
vendored
Normal file
42
app/static/bootstrap-4.2.1/scss/_images.scss
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
// Responsive images (ensure images don't scale beyond their parents)
|
||||
//
|
||||
// This is purposefully opt-in via an explicit class rather than being the default for all `<img>`s.
|
||||
// We previously tried the "images are responsive by default" approach in Bootstrap v2,
|
||||
// and abandoned it in Bootstrap v3 because it breaks lots of third-party widgets (including Google Maps)
|
||||
// which weren't expecting the images within themselves to be involuntarily resized.
|
||||
// See also https://github.com/twbs/bootstrap/issues/18178
|
||||
.img-fluid {
|
||||
@include img-fluid;
|
||||
}
|
||||
|
||||
|
||||
// Image thumbnails
|
||||
.img-thumbnail {
|
||||
padding: $thumbnail-padding;
|
||||
background-color: $thumbnail-bg;
|
||||
border: $thumbnail-border-width solid $thumbnail-border-color;
|
||||
@include border-radius($thumbnail-border-radius);
|
||||
@include box-shadow($thumbnail-box-shadow);
|
||||
|
||||
// Keep them at most 100% wide
|
||||
@include img-fluid;
|
||||
}
|
||||
|
||||
//
|
||||
// Figures
|
||||
//
|
||||
|
||||
.figure {
|
||||
// Ensures the caption's text aligns with the image.
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.figure-img {
|
||||
margin-bottom: $spacer / 2;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.figure-caption {
|
||||
font-size: $figure-caption-font-size;
|
||||
color: $figure-caption-color;
|
||||
}
|
193
app/static/bootstrap-4.2.1/scss/_input-group.scss
vendored
Normal file
193
app/static/bootstrap-4.2.1/scss/_input-group.scss
vendored
Normal file
@ -0,0 +1,193 @@
|
||||
// stylelint-disable selector-no-qualifying-type
|
||||
|
||||
//
|
||||
// Base styles
|
||||
//
|
||||
|
||||
.input-group {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-wrap: wrap; // For form validation feedback
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
|
||||
> .form-control,
|
||||
> .form-control-plaintext,
|
||||
> .custom-select,
|
||||
> .custom-file {
|
||||
position: relative; // For focus state's z-index
|
||||
flex: 1 1 auto;
|
||||
// Add width 1% and flex-basis auto to ensure that button will not wrap out
|
||||
// the column. Applies to IE Edge+ and Firefox. Chrome does not require this.
|
||||
width: 1%;
|
||||
margin-bottom: 0;
|
||||
|
||||
+ .form-control,
|
||||
+ .custom-select,
|
||||
+ .custom-file {
|
||||
margin-left: -$input-border-width;
|
||||
}
|
||||
}
|
||||
|
||||
// Bring the "active" form control to the top of surrounding elements
|
||||
> .form-control:focus,
|
||||
> .custom-select:focus,
|
||||
> .custom-file .custom-file-input:focus ~ .custom-file-label {
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
// Bring the custom file input above the label
|
||||
> .custom-file .custom-file-input:focus {
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
> .form-control,
|
||||
> .custom-select {
|
||||
&:not(:last-child) { @include border-right-radius(0); }
|
||||
&:not(:first-child) { @include border-left-radius(0); }
|
||||
}
|
||||
|
||||
// Custom file inputs have more complex markup, thus requiring different
|
||||
// border-radius overrides.
|
||||
> .custom-file {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:not(:last-child) .custom-file-label,
|
||||
&:not(:last-child) .custom-file-label::after { @include border-right-radius(0); }
|
||||
&:not(:first-child) .custom-file-label { @include border-left-radius(0); }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Prepend and append
|
||||
//
|
||||
// While it requires one extra layer of HTML for each, dedicated prepend and
|
||||
// append elements allow us to 1) be less clever, 2) simplify our selectors, and
|
||||
// 3) support HTML5 form validation.
|
||||
|
||||
.input-group-prepend,
|
||||
.input-group-append {
|
||||
display: flex;
|
||||
|
||||
// Ensure buttons are always above inputs for more visually pleasing borders.
|
||||
// This isn't needed for `.input-group-text` since it shares the same border-color
|
||||
// as our inputs.
|
||||
.btn {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
&:focus {
|
||||
z-index: 3;
|
||||
}
|
||||
}
|
||||
|
||||
.btn + .btn,
|
||||
.btn + .input-group-text,
|
||||
.input-group-text + .input-group-text,
|
||||
.input-group-text + .btn {
|
||||
margin-left: -$input-border-width;
|
||||
}
|
||||
}
|
||||
|
||||
.input-group-prepend { margin-right: -$input-border-width; }
|
||||
.input-group-append { margin-left: -$input-border-width; }
|
||||
|
||||
|
||||
// Textual addons
|
||||
//
|
||||
// Serves as a catch-all element for any text or radio/checkbox input you wish
|
||||
// to prepend or append to an input.
|
||||
|
||||
.input-group-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: $input-padding-y $input-padding-x;
|
||||
margin-bottom: 0; // Allow use of <label> elements by overriding our default margin-bottom
|
||||
font-size: $font-size-base; // Match inputs
|
||||
font-weight: $font-weight-normal;
|
||||
line-height: $input-line-height;
|
||||
color: $input-group-addon-color;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
background-color: $input-group-addon-bg;
|
||||
border: $input-border-width solid $input-group-addon-border-color;
|
||||
@include border-radius($input-border-radius);
|
||||
|
||||
// Nuke default margins from checkboxes and radios to vertically center within.
|
||||
input[type="radio"],
|
||||
input[type="checkbox"] {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Sizing
|
||||
//
|
||||
// Remix the default form control sizing classes into new ones for easier
|
||||
// manipulation.
|
||||
|
||||
.input-group-lg > .form-control:not(textarea),
|
||||
.input-group-lg > .custom-select {
|
||||
height: $input-height-lg;
|
||||
}
|
||||
|
||||
.input-group-lg > .form-control,
|
||||
.input-group-lg > .custom-select,
|
||||
.input-group-lg > .input-group-prepend > .input-group-text,
|
||||
.input-group-lg > .input-group-append > .input-group-text,
|
||||
.input-group-lg > .input-group-prepend > .btn,
|
||||
.input-group-lg > .input-group-append > .btn {
|
||||
padding: $input-padding-y-lg $input-padding-x-lg;
|
||||
font-size: $input-font-size-lg;
|
||||
line-height: $input-line-height-lg;
|
||||
@include border-radius($input-border-radius-lg);
|
||||
}
|
||||
|
||||
.input-group-sm > .form-control:not(textarea),
|
||||
.input-group-sm > .custom-select {
|
||||
height: $input-height-sm;
|
||||
}
|
||||
|
||||
.input-group-sm > .form-control,
|
||||
.input-group-sm > .custom-select,
|
||||
.input-group-sm > .input-group-prepend > .input-group-text,
|
||||
.input-group-sm > .input-group-append > .input-group-text,
|
||||
.input-group-sm > .input-group-prepend > .btn,
|
||||
.input-group-sm > .input-group-append > .btn {
|
||||
padding: $input-padding-y-sm $input-padding-x-sm;
|
||||
font-size: $input-font-size-sm;
|
||||
line-height: $input-line-height-sm;
|
||||
@include border-radius($input-border-radius-sm);
|
||||
}
|
||||
|
||||
.input-group-lg > .custom-select,
|
||||
.input-group-sm > .custom-select {
|
||||
padding-right: $custom-select-padding-x + $custom-select-indicator-padding;
|
||||
}
|
||||
|
||||
|
||||
// Prepend and append rounded corners
|
||||
//
|
||||
// These rulesets must come after the sizing ones to properly override sm and lg
|
||||
// border-radius values when extending. They're more specific than we'd like
|
||||
// with the `.input-group >` part, but without it, we cannot override the sizing.
|
||||
|
||||
|
||||
.input-group > .input-group-prepend > .btn,
|
||||
.input-group > .input-group-prepend > .input-group-text,
|
||||
.input-group > .input-group-append:not(:last-child) > .btn,
|
||||
.input-group > .input-group-append:not(:last-child) > .input-group-text,
|
||||
.input-group > .input-group-append:last-child > .btn:not(:last-child):not(.dropdown-toggle),
|
||||
.input-group > .input-group-append:last-child > .input-group-text:not(:last-child) {
|
||||
@include border-right-radius(0);
|
||||
}
|
||||
|
||||
.input-group > .input-group-append > .btn,
|
||||
.input-group > .input-group-append > .input-group-text,
|
||||
.input-group > .input-group-prepend:not(:first-child) > .btn,
|
||||
.input-group > .input-group-prepend:not(:first-child) > .input-group-text,
|
||||
.input-group > .input-group-prepend:first-child > .btn:not(:first-child),
|
||||
.input-group > .input-group-prepend:first-child > .input-group-text:not(:first-child) {
|
||||
@include border-left-radius(0);
|
||||
}
|
16
app/static/bootstrap-4.2.1/scss/_jumbotron.scss
vendored
Normal file
16
app/static/bootstrap-4.2.1/scss/_jumbotron.scss
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
.jumbotron {
|
||||
padding: $jumbotron-padding ($jumbotron-padding / 2);
|
||||
margin-bottom: $jumbotron-padding;
|
||||
background-color: $jumbotron-bg;
|
||||
@include border-radius($border-radius-lg);
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
padding: ($jumbotron-padding * 2) $jumbotron-padding;
|
||||
}
|
||||
}
|
||||
|
||||
.jumbotron-fluid {
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
@include border-radius(0);
|
||||
}
|
121
app/static/bootstrap-4.2.1/scss/_list-group.scss
vendored
Normal file
121
app/static/bootstrap-4.2.1/scss/_list-group.scss
vendored
Normal file
@ -0,0 +1,121 @@
|
||||
// Base class
|
||||
//
|
||||
// Easily usable on <ul>, <ol>, or <div>.
|
||||
|
||||
.list-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
// No need to set list-style: none; since .list-group-item is block level
|
||||
padding-left: 0; // reset padding because ul and ol
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
|
||||
// Interactive list items
|
||||
//
|
||||
// Use anchor or button elements instead of `li`s or `div`s to create interactive
|
||||
// list items. Includes an extra `.active` modifier class for selected items.
|
||||
|
||||
.list-group-item-action {
|
||||
width: 100%; // For `<button>`s (anchors become 100% by default though)
|
||||
color: $list-group-action-color;
|
||||
text-align: inherit; // For `<button>`s (anchors inherit)
|
||||
|
||||
// Hover state
|
||||
@include hover-focus {
|
||||
color: $list-group-action-hover-color;
|
||||
text-decoration: none;
|
||||
background-color: $list-group-hover-bg;
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: $list-group-action-active-color;
|
||||
background-color: $list-group-action-active-bg;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Individual list items
|
||||
//
|
||||
// Use on `li`s or `div`s within the `.list-group` parent.
|
||||
|
||||
.list-group-item {
|
||||
position: relative;
|
||||
display: block;
|
||||
padding: $list-group-item-padding-y $list-group-item-padding-x;
|
||||
// Place the border on the list items and negative margin up for better styling
|
||||
margin-bottom: -$list-group-border-width;
|
||||
background-color: $list-group-bg;
|
||||
border: $list-group-border-width solid $list-group-border-color;
|
||||
|
||||
&:first-child {
|
||||
@include border-top-radius($list-group-border-radius);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
@include border-bottom-radius($list-group-border-radius);
|
||||
}
|
||||
|
||||
@include hover-focus {
|
||||
z-index: 1; // Place hover/active items above their siblings for proper border styling
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&.disabled,
|
||||
&:disabled {
|
||||
color: $list-group-disabled-color;
|
||||
pointer-events: none;
|
||||
background-color: $list-group-disabled-bg;
|
||||
}
|
||||
|
||||
// Include both here for `<a>`s and `<button>`s
|
||||
&.active {
|
||||
z-index: 2; // Place active items above their siblings for proper border styling
|
||||
color: $list-group-active-color;
|
||||
background-color: $list-group-active-bg;
|
||||
border-color: $list-group-active-border-color;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Flush list items
|
||||
//
|
||||
// Remove borders and border-radius to keep list group items edge-to-edge. Most
|
||||
// useful within other components (e.g., cards).
|
||||
|
||||
.list-group-flush {
|
||||
.list-group-item {
|
||||
border-right: 0;
|
||||
border-left: 0;
|
||||
@include border-radius(0);
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: -$list-group-border-width;
|
||||
}
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
.list-group-item:first-child {
|
||||
border-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
.list-group-item:last-child {
|
||||
margin-bottom: 0;
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Contextual variants
|
||||
//
|
||||
// Add modifier classes to change text and background color on individual items.
|
||||
// Organizationally, this must come after the `:hover` states.
|
||||
|
||||
@each $color, $value in $theme-colors {
|
||||
@include list-group-item-variant($color, theme-color-level($color, -9), theme-color-level($color, 6));
|
||||
}
|
8
app/static/bootstrap-4.2.1/scss/_media.scss
vendored
Normal file
8
app/static/bootstrap-4.2.1/scss/_media.scss
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
.media {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.media-body {
|
||||
flex: 1;
|
||||
}
|
41
app/static/bootstrap-4.2.1/scss/_mixins.scss
vendored
Normal file
41
app/static/bootstrap-4.2.1/scss/_mixins.scss
vendored
Normal file
@ -0,0 +1,41 @@
|
||||
// Toggles
|
||||
//
|
||||
// Used in conjunction with global variables to enable certain theme features.
|
||||
|
||||
// Utilities
|
||||
@import "mixins/breakpoints";
|
||||
@import "mixins/hover";
|
||||
@import "mixins/image";
|
||||
@import "mixins/badge";
|
||||
@import "mixins/resize";
|
||||
@import "mixins/screen-reader";
|
||||
@import "mixins/size";
|
||||
@import "mixins/reset-text";
|
||||
@import "mixins/text-emphasis";
|
||||
@import "mixins/text-hide";
|
||||
@import "mixins/text-truncate";
|
||||
@import "mixins/visibility";
|
||||
|
||||
// // Components
|
||||
@import "mixins/alert";
|
||||
@import "mixins/buttons";
|
||||
@import "mixins/caret";
|
||||
@import "mixins/pagination";
|
||||
@import "mixins/lists";
|
||||
@import "mixins/list-group";
|
||||
@import "mixins/nav-divider";
|
||||
@import "mixins/forms";
|
||||
@import "mixins/table-row";
|
||||
|
||||
// // Skins
|
||||
@import "mixins/background-variant";
|
||||
@import "mixins/border-radius";
|
||||
@import "mixins/box-shadow";
|
||||
@import "mixins/gradients";
|
||||
@import "mixins/transition";
|
||||
|
||||
// // Layout
|
||||
@import "mixins/clearfix";
|
||||
@import "mixins/grid-framework";
|
||||
@import "mixins/grid";
|
||||
@import "mixins/float";
|
186
app/static/bootstrap-4.2.1/scss/_modal.scss
vendored
Normal file
186
app/static/bootstrap-4.2.1/scss/_modal.scss
vendored
Normal file
@ -0,0 +1,186 @@
|
||||
// .modal-open - body class for killing the scroll
|
||||
// .modal - container to scroll within
|
||||
// .modal-dialog - positioning shell for the actual modal
|
||||
// .modal-content - actual modal w/ bg and corners and stuff
|
||||
|
||||
|
||||
.modal-open {
|
||||
// Kill the scroll on the body
|
||||
overflow: hidden;
|
||||
|
||||
.modal {
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// Container that the modal scrolls within
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: $zindex-modal;
|
||||
display: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
// Prevent Chrome on Windows from adding a focus outline. For details, see
|
||||
// https://github.com/twbs/bootstrap/pull/10951.
|
||||
outline: 0;
|
||||
// We deliberately don't use `-webkit-overflow-scrolling: touch;` due to a
|
||||
// gnarly iOS Safari bug: https://bugs.webkit.org/show_bug.cgi?id=158342
|
||||
// See also https://github.com/twbs/bootstrap/issues/17695
|
||||
}
|
||||
|
||||
// Shell div to position the modal with bottom padding
|
||||
.modal-dialog {
|
||||
position: relative;
|
||||
width: auto;
|
||||
margin: $modal-dialog-margin;
|
||||
// allow clicks to pass through for custom click handling to close modal
|
||||
pointer-events: none;
|
||||
|
||||
// When fading in the modal, animate it to slide down
|
||||
.modal.fade & {
|
||||
@include transition($modal-transition);
|
||||
transform: $modal-fade-transform;
|
||||
}
|
||||
.modal.show & {
|
||||
transform: $modal-show-transform;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-dialog-centered {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: calc(100% - (#{$modal-dialog-margin} * 2));
|
||||
|
||||
// Ensure `modal-dialog-centered` extends the full height of the view (IE10/11)
|
||||
&::before {
|
||||
display: block; // IE10
|
||||
height: calc(100vh - (#{$modal-dialog-margin} * 2));
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
|
||||
// Actual modal
|
||||
.modal-content {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%; // Ensure `.modal-content` extends the full width of the parent `.modal-dialog`
|
||||
// counteract the pointer-events: none; in the .modal-dialog
|
||||
pointer-events: auto;
|
||||
background-color: $modal-content-bg;
|
||||
background-clip: padding-box;
|
||||
border: $modal-content-border-width solid $modal-content-border-color;
|
||||
@include border-radius($modal-content-border-radius);
|
||||
@include box-shadow($modal-content-box-shadow-xs);
|
||||
// Remove focus outline from opened modal
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
// Modal background
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: $zindex-modal-backdrop;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: $modal-backdrop-bg;
|
||||
|
||||
// Fade for backdrop
|
||||
&.fade { opacity: 0; }
|
||||
&.show { opacity: $modal-backdrop-opacity; }
|
||||
}
|
||||
|
||||
// Modal header
|
||||
// Top section of the modal w/ title and dismiss
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: flex-start; // so the close btn always stays on the upper right corner
|
||||
justify-content: space-between; // Put modal header elements (title and dismiss) on opposite ends
|
||||
padding: $modal-header-padding;
|
||||
border-bottom: $modal-header-border-width solid $modal-header-border-color;
|
||||
@include border-top-radius($modal-content-border-radius);
|
||||
|
||||
.close {
|
||||
padding: $modal-header-padding;
|
||||
// auto on the left force icon to the right even when there is no .modal-title
|
||||
margin: (-$modal-header-padding-y) (-$modal-header-padding-x) (-$modal-header-padding-y) auto;
|
||||
}
|
||||
}
|
||||
|
||||
// Title text within header
|
||||
.modal-title {
|
||||
margin-bottom: 0;
|
||||
line-height: $modal-title-line-height;
|
||||
}
|
||||
|
||||
// Modal body
|
||||
// Where all modal content resides (sibling of .modal-header and .modal-footer)
|
||||
.modal-body {
|
||||
position: relative;
|
||||
// Enable `flex-grow: 1` so that the body take up as much space as possible
|
||||
// when should there be a fixed height on `.modal-dialog`.
|
||||
flex: 1 1 auto;
|
||||
padding: $modal-inner-padding;
|
||||
}
|
||||
|
||||
// Footer (for actions)
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
align-items: center; // vertically center
|
||||
justify-content: flex-end; // Right align buttons with flex property because text-align doesn't work on flex items
|
||||
padding: $modal-inner-padding;
|
||||
border-top: $modal-footer-border-width solid $modal-footer-border-color;
|
||||
@include border-bottom-radius($modal-content-border-radius);
|
||||
|
||||
// Easily place margin between footer elements
|
||||
> :not(:first-child) { margin-left: .25rem; }
|
||||
> :not(:last-child) { margin-right: .25rem; }
|
||||
}
|
||||
|
||||
// Measure scrollbar width for padding body during modal show/hide
|
||||
.modal-scrollbar-measure {
|
||||
position: absolute;
|
||||
top: -9999px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
// Scale up the modal
|
||||
@include media-breakpoint-up(sm) {
|
||||
// Automatically set modal's width for larger viewports
|
||||
.modal-dialog {
|
||||
max-width: $modal-md;
|
||||
margin: $modal-dialog-margin-y-sm-up auto;
|
||||
}
|
||||
|
||||
.modal-dialog-centered {
|
||||
min-height: calc(100% - (#{$modal-dialog-margin-y-sm-up} * 2));
|
||||
|
||||
&::before {
|
||||
height: calc(100vh - (#{$modal-dialog-margin-y-sm-up} * 2));
|
||||
}
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
@include box-shadow($modal-content-box-shadow-sm-up);
|
||||
}
|
||||
|
||||
.modal-sm { max-width: $modal-sm; }
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
.modal-lg,
|
||||
.modal-xl {
|
||||
max-width: $modal-lg;
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(xl) {
|
||||
.modal-xl { max-width: $modal-xl; }
|
||||
}
|
120
app/static/bootstrap-4.2.1/scss/_nav.scss
vendored
Normal file
120
app/static/bootstrap-4.2.1/scss/_nav.scss
vendored
Normal file
@ -0,0 +1,120 @@
|
||||
// Base class
|
||||
//
|
||||
// Kickstart any navigation component with a set of style resets. Works with
|
||||
// `<nav>`s or `<ul>`s.
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding-left: 0;
|
||||
margin-bottom: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
display: block;
|
||||
padding: $nav-link-padding-y $nav-link-padding-x;
|
||||
|
||||
@include hover-focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
// Disabled state lightens text
|
||||
&.disabled {
|
||||
color: $nav-link-disabled-color;
|
||||
pointer-events: none;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Tabs
|
||||
//
|
||||
|
||||
.nav-tabs {
|
||||
border-bottom: $nav-tabs-border-width solid $nav-tabs-border-color;
|
||||
|
||||
.nav-item {
|
||||
margin-bottom: -$nav-tabs-border-width;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
border: $nav-tabs-border-width solid transparent;
|
||||
@include border-top-radius($nav-tabs-border-radius);
|
||||
|
||||
@include hover-focus {
|
||||
border-color: $nav-tabs-link-hover-border-color;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
color: $nav-link-disabled-color;
|
||||
background-color: transparent;
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-link.active,
|
||||
.nav-item.show .nav-link {
|
||||
color: $nav-tabs-link-active-color;
|
||||
background-color: $nav-tabs-link-active-bg;
|
||||
border-color: $nav-tabs-link-active-border-color;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
// Make dropdown border overlap tab border
|
||||
margin-top: -$nav-tabs-border-width;
|
||||
// Remove the top rounded corners here since there is a hard edge above the menu
|
||||
@include border-top-radius(0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Pills
|
||||
//
|
||||
|
||||
.nav-pills {
|
||||
.nav-link {
|
||||
@include border-radius($nav-pills-border-radius);
|
||||
}
|
||||
|
||||
.nav-link.active,
|
||||
.show > .nav-link {
|
||||
color: $nav-pills-link-active-color;
|
||||
background-color: $nav-pills-link-active-bg;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Justified variants
|
||||
//
|
||||
|
||||
.nav-fill {
|
||||
.nav-item {
|
||||
flex: 1 1 auto;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-justified {
|
||||
.nav-item {
|
||||
flex-basis: 0;
|
||||
flex-grow: 1;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Tabbable tabs
|
||||
//
|
||||
// Hide tabbable panes to start, show them when `.active`
|
||||
|
||||
.tab-content {
|
||||
> .tab-pane {
|
||||
display: none;
|
||||
}
|
||||
> .active {
|
||||
display: block;
|
||||
}
|
||||
}
|
299
app/static/bootstrap-4.2.1/scss/_navbar.scss
vendored
Normal file
299
app/static/bootstrap-4.2.1/scss/_navbar.scss
vendored
Normal file
@ -0,0 +1,299 @@
|
||||
// Contents
|
||||
//
|
||||
// Navbar
|
||||
// Navbar brand
|
||||
// Navbar nav
|
||||
// Navbar text
|
||||
// Navbar divider
|
||||
// Responsive navbar
|
||||
// Navbar position
|
||||
// Navbar themes
|
||||
|
||||
|
||||
// Navbar
|
||||
//
|
||||
// Provide a static navbar from which we expand to create full-width, fixed, and
|
||||
// other navbar variations.
|
||||
|
||||
.navbar {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-wrap: wrap; // allow us to do the line break for collapsing content
|
||||
align-items: center;
|
||||
justify-content: space-between; // space out brand from logo
|
||||
padding: $navbar-padding-y $navbar-padding-x;
|
||||
|
||||
// Because flex properties aren't inherited, we need to redeclare these first
|
||||
// few properties so that content nested within behave properly.
|
||||
> .container,
|
||||
> .container-fluid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Navbar brand
|
||||
//
|
||||
// Used for brand, project, or site names.
|
||||
|
||||
.navbar-brand {
|
||||
display: inline-block;
|
||||
padding-top: $navbar-brand-padding-y;
|
||||
padding-bottom: $navbar-brand-padding-y;
|
||||
margin-right: $navbar-padding-x;
|
||||
font-size: $navbar-brand-font-size;
|
||||
line-height: inherit;
|
||||
white-space: nowrap;
|
||||
|
||||
@include hover-focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Navbar nav
|
||||
//
|
||||
// Custom navbar navigation (doesn't require `.nav`, but does make use of `.nav-link`).
|
||||
|
||||
.navbar-nav {
|
||||
display: flex;
|
||||
flex-direction: column; // cannot use `inherit` to get the `.navbar`s value
|
||||
padding-left: 0;
|
||||
margin-bottom: 0;
|
||||
list-style: none;
|
||||
|
||||
.nav-link {
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
position: static;
|
||||
float: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Navbar text
|
||||
//
|
||||
//
|
||||
|
||||
.navbar-text {
|
||||
display: inline-block;
|
||||
padding-top: $nav-link-padding-y;
|
||||
padding-bottom: $nav-link-padding-y;
|
||||
}
|
||||
|
||||
|
||||
// Responsive navbar
|
||||
//
|
||||
// Custom styles for responsive collapsing and toggling of navbar contents.
|
||||
// Powered by the collapse Bootstrap JavaScript plugin.
|
||||
|
||||
// When collapsed, prevent the toggleable navbar contents from appearing in
|
||||
// the default flexbox row orientation. Requires the use of `flex-wrap: wrap`
|
||||
// on the `.navbar` parent.
|
||||
.navbar-collapse {
|
||||
flex-basis: 100%;
|
||||
flex-grow: 1;
|
||||
// For always expanded or extra full navbars, ensure content aligns itself
|
||||
// properly vertically. Can be easily overridden with flex utilities.
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
// Button for toggling the navbar when in its collapsed state
|
||||
.navbar-toggler {
|
||||
padding: $navbar-toggler-padding-y $navbar-toggler-padding-x;
|
||||
font-size: $navbar-toggler-font-size;
|
||||
line-height: 1;
|
||||
background-color: transparent; // remove default button style
|
||||
border: $border-width solid transparent; // remove default button style
|
||||
@include border-radius($navbar-toggler-border-radius);
|
||||
|
||||
@include hover-focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
// Opinionated: add "hand" cursor to non-disabled .navbar-toggler elements
|
||||
&:not(:disabled):not(.disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
// Keep as a separate element so folks can easily override it with another icon
|
||||
// or image file as needed.
|
||||
.navbar-toggler-icon {
|
||||
display: inline-block;
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
vertical-align: middle;
|
||||
content: "";
|
||||
background: no-repeat center center;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
// Generate series of `.navbar-expand-*` responsive classes for configuring
|
||||
// where your navbar collapses.
|
||||
.navbar-expand {
|
||||
@each $breakpoint in map-keys($grid-breakpoints) {
|
||||
$next: breakpoint-next($breakpoint, $grid-breakpoints);
|
||||
$infix: breakpoint-infix($next, $grid-breakpoints);
|
||||
|
||||
&#{$infix} {
|
||||
@include media-breakpoint-down($breakpoint) {
|
||||
> .container,
|
||||
> .container-fluid {
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-up($next) {
|
||||
flex-flow: row nowrap;
|
||||
justify-content: flex-start;
|
||||
|
||||
.navbar-nav {
|
||||
flex-direction: row;
|
||||
|
||||
.dropdown-menu {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
padding-right: $navbar-nav-link-padding-x;
|
||||
padding-left: $navbar-nav-link-padding-x;
|
||||
}
|
||||
}
|
||||
|
||||
// For nesting containers, have to redeclare for alignment purposes
|
||||
> .container,
|
||||
> .container-fluid {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.navbar-collapse {
|
||||
display: flex !important; // stylelint-disable-line declaration-no-important
|
||||
|
||||
// Changes flex-bases to auto because of an IE10 bug
|
||||
flex-basis: auto;
|
||||
}
|
||||
|
||||
.navbar-toggler {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Navbar themes
|
||||
//
|
||||
// Styles for switching between navbars with light or dark background.
|
||||
|
||||
// Dark links against a light background
|
||||
.navbar-light {
|
||||
.navbar-brand {
|
||||
color: $navbar-light-brand-color;
|
||||
|
||||
@include hover-focus {
|
||||
color: $navbar-light-brand-hover-color;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-nav {
|
||||
.nav-link {
|
||||
color: $navbar-light-color;
|
||||
|
||||
@include hover-focus {
|
||||
color: $navbar-light-hover-color;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
color: $navbar-light-disabled-color;
|
||||
}
|
||||
}
|
||||
|
||||
.show > .nav-link,
|
||||
.active > .nav-link,
|
||||
.nav-link.show,
|
||||
.nav-link.active {
|
||||
color: $navbar-light-active-color;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-toggler {
|
||||
color: $navbar-light-color;
|
||||
border-color: $navbar-light-toggler-border-color;
|
||||
}
|
||||
|
||||
.navbar-toggler-icon {
|
||||
background-image: $navbar-light-toggler-icon-bg;
|
||||
}
|
||||
|
||||
.navbar-text {
|
||||
color: $navbar-light-color;
|
||||
a {
|
||||
color: $navbar-light-active-color;
|
||||
|
||||
@include hover-focus {
|
||||
color: $navbar-light-active-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// White links against a dark background
|
||||
.navbar-dark {
|
||||
.navbar-brand {
|
||||
color: $navbar-dark-brand-color;
|
||||
|
||||
@include hover-focus {
|
||||
color: $navbar-dark-brand-hover-color;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-nav {
|
||||
.nav-link {
|
||||
color: $navbar-dark-color;
|
||||
|
||||
@include hover-focus {
|
||||
color: $navbar-dark-hover-color;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
color: $navbar-dark-disabled-color;
|
||||
}
|
||||
}
|
||||
|
||||
.show > .nav-link,
|
||||
.active > .nav-link,
|
||||
.nav-link.show,
|
||||
.nav-link.active {
|
||||
color: $navbar-dark-active-color;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-toggler {
|
||||
color: $navbar-dark-color;
|
||||
border-color: $navbar-dark-toggler-border-color;
|
||||
}
|
||||
|
||||
.navbar-toggler-icon {
|
||||
background-image: $navbar-dark-toggler-icon-bg;
|
||||
}
|
||||
|
||||
.navbar-text {
|
||||
color: $navbar-dark-color;
|
||||
a {
|
||||
color: $navbar-dark-active-color;
|
||||
|
||||
@include hover-focus {
|
||||
color: $navbar-dark-active-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
78
app/static/bootstrap-4.2.1/scss/_pagination.scss
vendored
Normal file
78
app/static/bootstrap-4.2.1/scss/_pagination.scss
vendored
Normal file
@ -0,0 +1,78 @@
|
||||
.pagination {
|
||||
display: flex;
|
||||
@include list-unstyled();
|
||||
@include border-radius();
|
||||
}
|
||||
|
||||
.page-link {
|
||||
position: relative;
|
||||
display: block;
|
||||
padding: $pagination-padding-y $pagination-padding-x;
|
||||
margin-left: -$pagination-border-width;
|
||||
line-height: $pagination-line-height;
|
||||
color: $pagination-color;
|
||||
background-color: $pagination-bg;
|
||||
border: $pagination-border-width solid $pagination-border-color;
|
||||
|
||||
&:hover {
|
||||
z-index: 2;
|
||||
color: $pagination-hover-color;
|
||||
text-decoration: none;
|
||||
background-color: $pagination-hover-bg;
|
||||
border-color: $pagination-hover-border-color;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
z-index: 2;
|
||||
outline: $pagination-focus-outline;
|
||||
box-shadow: $pagination-focus-box-shadow;
|
||||
}
|
||||
|
||||
// Opinionated: add "hand" cursor to non-disabled .page-link elements
|
||||
&:not(:disabled):not(.disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.page-item {
|
||||
&:first-child {
|
||||
.page-link {
|
||||
margin-left: 0;
|
||||
@include border-left-radius($border-radius);
|
||||
}
|
||||
}
|
||||
&:last-child {
|
||||
.page-link {
|
||||
@include border-right-radius($border-radius);
|
||||
}
|
||||
}
|
||||
|
||||
&.active .page-link {
|
||||
z-index: 1;
|
||||
color: $pagination-active-color;
|
||||
background-color: $pagination-active-bg;
|
||||
border-color: $pagination-active-border-color;
|
||||
}
|
||||
|
||||
&.disabled .page-link {
|
||||
color: $pagination-disabled-color;
|
||||
pointer-events: none;
|
||||
// Opinionated: remove the "hand" cursor set previously for .page-link
|
||||
cursor: auto;
|
||||
background-color: $pagination-disabled-bg;
|
||||
border-color: $pagination-disabled-border-color;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Sizing
|
||||
//
|
||||
|
||||
.pagination-lg {
|
||||
@include pagination-size($pagination-padding-y-lg, $pagination-padding-x-lg, $font-size-lg, $line-height-lg, $border-radius-lg);
|
||||
}
|
||||
|
||||
.pagination-sm {
|
||||
@include pagination-size($pagination-padding-y-sm, $pagination-padding-x-sm, $font-size-sm, $line-height-sm, $border-radius-sm);
|
||||
}
|
183
app/static/bootstrap-4.2.1/scss/_popover.scss
vendored
Normal file
183
app/static/bootstrap-4.2.1/scss/_popover.scss
vendored
Normal file
@ -0,0 +1,183 @@
|
||||
.popover {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: $zindex-popover;
|
||||
display: block;
|
||||
max-width: $popover-max-width;
|
||||
// Our parent element can be arbitrary since tooltips are by default inserted as a sibling of their target element.
|
||||
// So reset our font and text properties to avoid inheriting weird values.
|
||||
@include reset-text();
|
||||
font-size: $popover-font-size;
|
||||
// Allow breaking very long words so they don't overflow the popover's bounds
|
||||
word-wrap: break-word;
|
||||
background-color: $popover-bg;
|
||||
background-clip: padding-box;
|
||||
border: $popover-border-width solid $popover-border-color;
|
||||
@include border-radius($popover-border-radius);
|
||||
@include box-shadow($popover-box-shadow);
|
||||
|
||||
.arrow {
|
||||
position: absolute;
|
||||
display: block;
|
||||
width: $popover-arrow-width;
|
||||
height: $popover-arrow-height;
|
||||
margin: 0 $border-radius-lg;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
position: absolute;
|
||||
display: block;
|
||||
content: "";
|
||||
border-color: transparent;
|
||||
border-style: solid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bs-popover-top {
|
||||
margin-bottom: $popover-arrow-height;
|
||||
|
||||
.arrow {
|
||||
bottom: calc((#{$popover-arrow-height} + #{$popover-border-width}) * -1);
|
||||
}
|
||||
|
||||
.arrow::before,
|
||||
.arrow::after {
|
||||
border-width: $popover-arrow-height ($popover-arrow-width / 2) 0;
|
||||
}
|
||||
|
||||
.arrow::before {
|
||||
bottom: 0;
|
||||
border-top-color: $popover-arrow-outer-color;
|
||||
}
|
||||
|
||||
.arrow::after {
|
||||
bottom: $popover-border-width;
|
||||
border-top-color: $popover-arrow-color;
|
||||
}
|
||||
}
|
||||
|
||||
.bs-popover-right {
|
||||
margin-left: $popover-arrow-height;
|
||||
|
||||
.arrow {
|
||||
left: calc((#{$popover-arrow-height} + #{$popover-border-width}) * -1);
|
||||
width: $popover-arrow-height;
|
||||
height: $popover-arrow-width;
|
||||
margin: $border-radius-lg 0; // make sure the arrow does not touch the popover's rounded corners
|
||||
}
|
||||
|
||||
.arrow::before,
|
||||
.arrow::after {
|
||||
border-width: ($popover-arrow-width / 2) $popover-arrow-height ($popover-arrow-width / 2) 0;
|
||||
}
|
||||
|
||||
.arrow::before {
|
||||
left: 0;
|
||||
border-right-color: $popover-arrow-outer-color;
|
||||
}
|
||||
|
||||
.arrow::after {
|
||||
left: $popover-border-width;
|
||||
border-right-color: $popover-arrow-color;
|
||||
}
|
||||
}
|
||||
|
||||
.bs-popover-bottom {
|
||||
margin-top: $popover-arrow-height;
|
||||
|
||||
.arrow {
|
||||
top: calc((#{$popover-arrow-height} + #{$popover-border-width}) * -1);
|
||||
}
|
||||
|
||||
.arrow::before,
|
||||
.arrow::after {
|
||||
border-width: 0 ($popover-arrow-width / 2) $popover-arrow-height ($popover-arrow-width / 2);
|
||||
}
|
||||
|
||||
.arrow::before {
|
||||
top: 0;
|
||||
border-bottom-color: $popover-arrow-outer-color;
|
||||
}
|
||||
|
||||
.arrow::after {
|
||||
top: $popover-border-width;
|
||||
border-bottom-color: $popover-arrow-color;
|
||||
}
|
||||
|
||||
// This will remove the popover-header's border just below the arrow
|
||||
.popover-header::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
display: block;
|
||||
width: $popover-arrow-width;
|
||||
margin-left: -$popover-arrow-width / 2;
|
||||
content: "";
|
||||
border-bottom: $popover-border-width solid $popover-header-bg;
|
||||
}
|
||||
}
|
||||
|
||||
.bs-popover-left {
|
||||
margin-right: $popover-arrow-height;
|
||||
|
||||
.arrow {
|
||||
right: calc((#{$popover-arrow-height} + #{$popover-border-width}) * -1);
|
||||
width: $popover-arrow-height;
|
||||
height: $popover-arrow-width;
|
||||
margin: $border-radius-lg 0; // make sure the arrow does not touch the popover's rounded corners
|
||||
}
|
||||
|
||||
.arrow::before,
|
||||
.arrow::after {
|
||||
border-width: ($popover-arrow-width / 2) 0 ($popover-arrow-width / 2) $popover-arrow-height;
|
||||
}
|
||||
|
||||
.arrow::before {
|
||||
right: 0;
|
||||
border-left-color: $popover-arrow-outer-color;
|
||||
}
|
||||
|
||||
.arrow::after {
|
||||
right: $popover-border-width;
|
||||
border-left-color: $popover-arrow-color;
|
||||
}
|
||||
}
|
||||
|
||||
.bs-popover-auto {
|
||||
&[x-placement^="top"] {
|
||||
@extend .bs-popover-top;
|
||||
}
|
||||
&[x-placement^="right"] {
|
||||
@extend .bs-popover-right;
|
||||
}
|
||||
&[x-placement^="bottom"] {
|
||||
@extend .bs-popover-bottom;
|
||||
}
|
||||
&[x-placement^="left"] {
|
||||
@extend .bs-popover-left;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Offset the popover to account for the popover arrow
|
||||
.popover-header {
|
||||
padding: $popover-header-padding-y $popover-header-padding-x;
|
||||
margin-bottom: 0; // Reset the default from Reboot
|
||||
font-size: $font-size-base;
|
||||
color: $popover-header-color;
|
||||
background-color: $popover-header-bg;
|
||||
border-bottom: $popover-border-width solid darken($popover-header-bg, 5%);
|
||||
$offset-border-width: calc(#{$border-radius-lg} - #{$popover-border-width});
|
||||
@include border-top-radius($offset-border-width);
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.popover-body {
|
||||
padding: $popover-body-padding-y $popover-body-padding-x;
|
||||
color: $popover-body-color;
|
||||
}
|
141
app/static/bootstrap-4.2.1/scss/_print.scss
vendored
Normal file
141
app/static/bootstrap-4.2.1/scss/_print.scss
vendored
Normal file
@ -0,0 +1,141 @@
|
||||
// stylelint-disable declaration-no-important, selector-no-qualifying-type
|
||||
|
||||
// Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css
|
||||
|
||||
// ==========================================================================
|
||||
// Print styles.
|
||||
// Inlined to avoid the additional HTTP request:
|
||||
// https://www.phpied.com/delay-loading-your-print-css/
|
||||
// ==========================================================================
|
||||
|
||||
@if $enable-print-styles {
|
||||
@media print {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
// Bootstrap specific; comment out `color` and `background`
|
||||
//color: $black !important; // Black prints faster
|
||||
text-shadow: none !important;
|
||||
//background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
a {
|
||||
&:not(.btn) {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
// Bootstrap specific; comment the following selector out
|
||||
//a[href]::after {
|
||||
// content: " (" attr(href) ")";
|
||||
//}
|
||||
|
||||
abbr[title]::after {
|
||||
content: " (" attr(title) ")";
|
||||
}
|
||||
|
||||
// Bootstrap specific; comment the following selector out
|
||||
//
|
||||
// Don't show links that are fragment identifiers,
|
||||
// or use the `javascript:` pseudo protocol
|
||||
//
|
||||
|
||||
//a[href^="#"]::after,
|
||||
//a[href^="javascript:"]::after {
|
||||
// content: "";
|
||||
//}
|
||||
|
||||
pre {
|
||||
white-space: pre-wrap !important;
|
||||
}
|
||||
pre,
|
||||
blockquote {
|
||||
border: $border-width solid $gray-500; // Bootstrap custom code; using `$border-width` instead of 1px
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
//
|
||||
// Printing Tables:
|
||||
// http://css-discuss.incutio.com/wiki/Printing_Tables
|
||||
//
|
||||
|
||||
thead {
|
||||
display: table-header-group;
|
||||
}
|
||||
|
||||
tr,
|
||||
img {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
p,
|
||||
h2,
|
||||
h3 {
|
||||
orphans: 3;
|
||||
widows: 3;
|
||||
}
|
||||
|
||||
h2,
|
||||
h3 {
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
// Bootstrap specific changes start
|
||||
|
||||
// Specify a size and min-width to make printing closer across browsers.
|
||||
// We don't set margin here because it breaks `size` in Chrome. We also
|
||||
// don't use `!important` on `size` as it breaks in Chrome.
|
||||
@page {
|
||||
size: $print-page-size;
|
||||
}
|
||||
body {
|
||||
min-width: $print-body-min-width !important;
|
||||
}
|
||||
.container {
|
||||
min-width: $print-body-min-width !important;
|
||||
}
|
||||
|
||||
// Bootstrap components
|
||||
.navbar {
|
||||
display: none;
|
||||
}
|
||||
.badge {
|
||||
border: $border-width solid $black;
|
||||
}
|
||||
|
||||
.table {
|
||||
border-collapse: collapse !important;
|
||||
|
||||
td,
|
||||
th {
|
||||
background-color: $white !important;
|
||||
}
|
||||
}
|
||||
|
||||
.table-bordered {
|
||||
th,
|
||||
td {
|
||||
border: 1px solid $gray-300 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.table-dark {
|
||||
color: inherit;
|
||||
|
||||
th,
|
||||
td,
|
||||
thead th,
|
||||
tbody + tbody {
|
||||
border-color: $table-border-color;
|
||||
}
|
||||
}
|
||||
|
||||
.table .thead-dark th {
|
||||
color: inherit;
|
||||
border-color: $table-border-color;
|
||||
}
|
||||
|
||||
// Bootstrap specific changes end
|
||||
}
|
||||
}
|
34
app/static/bootstrap-4.2.1/scss/_progress.scss
vendored
Normal file
34
app/static/bootstrap-4.2.1/scss/_progress.scss
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
@keyframes progress-bar-stripes {
|
||||
from { background-position: $progress-height 0; }
|
||||
to { background-position: 0 0; }
|
||||
}
|
||||
|
||||
.progress {
|
||||
display: flex;
|
||||
height: $progress-height;
|
||||
overflow: hidden; // force rounded corners by cropping it
|
||||
font-size: $progress-font-size;
|
||||
background-color: $progress-bg;
|
||||
@include border-radius($progress-border-radius);
|
||||
@include box-shadow($progress-box-shadow);
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
color: $progress-bar-color;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
background-color: $progress-bar-bg;
|
||||
@include transition($progress-bar-transition);
|
||||
}
|
||||
|
||||
.progress-bar-striped {
|
||||
@include gradient-striped();
|
||||
background-size: $progress-height $progress-height;
|
||||
}
|
||||
|
||||
.progress-bar-animated {
|
||||
animation: progress-bar-stripes $progress-bar-animation-timing;
|
||||
}
|
462
app/static/bootstrap-4.2.1/scss/_reboot.scss
vendored
Normal file
462
app/static/bootstrap-4.2.1/scss/_reboot.scss
vendored
Normal file
@ -0,0 +1,462 @@
|
||||
// stylelint-disable at-rule-no-vendor-prefix, declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix
|
||||
|
||||
// Reboot
|
||||
//
|
||||
// Normalization of HTML elements, manually forked from Normalize.css to remove
|
||||
// styles targeting irrelevant browsers while applying new styles.
|
||||
//
|
||||
// Normalize is licensed MIT. https://github.com/necolas/normalize.css
|
||||
|
||||
|
||||
// Document
|
||||
//
|
||||
// 1. Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.
|
||||
// 2. Change the default font family in all browsers.
|
||||
// 3. Correct the line height in all browsers.
|
||||
// 4. Prevent adjustments of font size after orientation changes in IE on Windows Phone and in iOS.
|
||||
// 5. Change the default tap highlight to be completely transparent in iOS.
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box; // 1
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: sans-serif; // 2
|
||||
line-height: 1.15; // 3
|
||||
-webkit-text-size-adjust: 100%; // 4
|
||||
-webkit-tap-highlight-color: rgba($black, 0); // 5
|
||||
}
|
||||
|
||||
// Shim for "new" HTML5 structural elements to display correctly (IE10, older browsers)
|
||||
// TODO: remove in v5
|
||||
// stylelint-disable-next-line selector-list-comma-newline-after
|
||||
article, aside, figcaption, figure, footer, header, hgroup, main, nav, section {
|
||||
display: block;
|
||||
}
|
||||
|
||||
// Body
|
||||
//
|
||||
// 1. Remove the margin in all browsers.
|
||||
// 2. As a best practice, apply a default `background-color`.
|
||||
// 3. Set an explicit initial text-align value so that we can later use
|
||||
// the `inherit` value on things like `<th>` elements.
|
||||
|
||||
body {
|
||||
margin: 0; // 1
|
||||
font-family: $font-family-base;
|
||||
font-size: $font-size-base;
|
||||
font-weight: $font-weight-base;
|
||||
line-height: $line-height-base;
|
||||
color: $body-color;
|
||||
text-align: left; // 3
|
||||
background-color: $body-bg; // 2
|
||||
}
|
||||
|
||||
// Suppress the focus outline on elements that cannot be accessed via keyboard.
|
||||
// This prevents an unwanted focus outline from appearing around elements that
|
||||
// might still respond to pointer events.
|
||||
//
|
||||
// Credit: https://github.com/suitcss/base
|
||||
[tabindex="-1"]:focus {
|
||||
outline: 0 !important;
|
||||
}
|
||||
|
||||
|
||||
// Content grouping
|
||||
//
|
||||
// 1. Add the correct box sizing in Firefox.
|
||||
// 2. Show the overflow in Edge and IE.
|
||||
|
||||
hr {
|
||||
box-sizing: content-box; // 1
|
||||
height: 0; // 1
|
||||
overflow: visible; // 2
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Typography
|
||||
//
|
||||
|
||||
// Remove top margins from headings
|
||||
//
|
||||
// By default, `<h1>`-`<h6>` all receive top and bottom margins. We nuke the top
|
||||
// margin for easier control within type scales as it avoids margin collapsing.
|
||||
// stylelint-disable-next-line selector-list-comma-newline-after
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin-top: 0;
|
||||
margin-bottom: $headings-margin-bottom;
|
||||
}
|
||||
|
||||
// Reset margins on paragraphs
|
||||
//
|
||||
// Similarly, the top margin on `<p>`s get reset. However, we also reset the
|
||||
// bottom margin to use `rem` units instead of `em`.
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: $paragraph-margin-bottom;
|
||||
}
|
||||
|
||||
// Abbreviations
|
||||
//
|
||||
// 1. Duplicate behavior to the data-* attribute for our tooltip plugin
|
||||
// 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
|
||||
// 3. Add explicit cursor to indicate changed behavior.
|
||||
// 4. Remove the bottom border in Firefox 39-.
|
||||
// 5. Prevent the text-decoration to be skipped.
|
||||
|
||||
abbr[title],
|
||||
abbr[data-original-title] { // 1
|
||||
text-decoration: underline; // 2
|
||||
text-decoration: underline dotted; // 2
|
||||
cursor: help; // 3
|
||||
border-bottom: 0; // 4
|
||||
text-decoration-skip-ink: none; // 5
|
||||
}
|
||||
|
||||
address {
|
||||
margin-bottom: 1rem;
|
||||
font-style: normal;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
dl {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ol ol,
|
||||
ul ul,
|
||||
ol ul,
|
||||
ul ol {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: $dt-font-weight;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-bottom: .5rem;
|
||||
margin-left: 0; // Undo browser default
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: $font-weight-bolder; // Add the correct font weight in Chrome, Edge, and Safari
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 80%; // Add the correct font size in all browsers
|
||||
}
|
||||
|
||||
//
|
||||
// Prevent `sub` and `sup` elements from affecting the line height in
|
||||
// all browsers.
|
||||
//
|
||||
|
||||
sub,
|
||||
sup {
|
||||
position: relative;
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub { bottom: -.25em; }
|
||||
sup { top: -.5em; }
|
||||
|
||||
|
||||
//
|
||||
// Links
|
||||
//
|
||||
|
||||
a {
|
||||
color: $link-color;
|
||||
text-decoration: $link-decoration;
|
||||
background-color: transparent; // Remove the gray background on active links in IE 10.
|
||||
|
||||
@include hover {
|
||||
color: $link-hover-color;
|
||||
text-decoration: $link-hover-decoration;
|
||||
}
|
||||
}
|
||||
|
||||
// And undo these styles for placeholder links/named anchors (without href)
|
||||
// which have not been made explicitly keyboard-focusable (without tabindex).
|
||||
// It would be more straightforward to just use a[href] in previous block, but that
|
||||
// causes specificity issues in many other styles that are too complex to fix.
|
||||
// See https://github.com/twbs/bootstrap/issues/19402
|
||||
|
||||
a:not([href]):not([tabindex]) {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
|
||||
@include hover-focus {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Code
|
||||
//
|
||||
|
||||
pre,
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: $font-family-monospace;
|
||||
font-size: 1em; // Correct the odd `em` font sizing in all browsers.
|
||||
}
|
||||
|
||||
pre {
|
||||
// Remove browser default top margin
|
||||
margin-top: 0;
|
||||
// Reset browser default of `1em` to use `rem`s
|
||||
margin-bottom: 1rem;
|
||||
// Don't allow content to break outside
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Figures
|
||||
//
|
||||
|
||||
figure {
|
||||
// Apply a consistent margin strategy (matches our type styles).
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Images and content
|
||||
//
|
||||
|
||||
img {
|
||||
vertical-align: middle;
|
||||
border-style: none; // Remove the border on images inside links in IE 10-.
|
||||
}
|
||||
|
||||
svg {
|
||||
// Workaround for the SVG overflow bug in IE10/11 is still required.
|
||||
// See https://github.com/twbs/bootstrap/issues/26878
|
||||
overflow: hidden;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Tables
|
||||
//
|
||||
|
||||
table {
|
||||
border-collapse: collapse; // Prevent double borders
|
||||
}
|
||||
|
||||
caption {
|
||||
padding-top: $table-cell-padding;
|
||||
padding-bottom: $table-cell-padding;
|
||||
color: $table-caption-color;
|
||||
text-align: left;
|
||||
caption-side: bottom;
|
||||
}
|
||||
|
||||
th {
|
||||
// Matches default `<td>` alignment by inheriting from the `<body>`, or the
|
||||
// closest parent with a set `text-align`.
|
||||
text-align: inherit;
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Forms
|
||||
//
|
||||
|
||||
label {
|
||||
// Allow labels to use `margin` for spacing.
|
||||
display: inline-block;
|
||||
margin-bottom: $label-margin-bottom;
|
||||
}
|
||||
|
||||
// Remove the default `border-radius` that macOS Chrome adds.
|
||||
//
|
||||
// Details at https://github.com/twbs/bootstrap/issues/24093
|
||||
button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
// Work around a Firefox/IE bug where the transparent `button` background
|
||||
// results in a loss of the default `button` focus styles.
|
||||
//
|
||||
// Credit: https://github.com/suitcss/base/
|
||||
button:focus {
|
||||
outline: 1px dotted;
|
||||
outline: 5px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
optgroup,
|
||||
textarea {
|
||||
margin: 0; // Remove the margin in Firefox and Safari
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
input {
|
||||
overflow: visible; // Show the overflow in Edge
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none; // Remove the inheritance of text transform in Firefox
|
||||
}
|
||||
|
||||
// 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`
|
||||
// controls in Android 4.
|
||||
// 2. Correct the inability to style clickable types in iOS and Safari.
|
||||
button,
|
||||
[type="button"], // 1
|
||||
[type="reset"],
|
||||
[type="submit"] {
|
||||
-webkit-appearance: button; // 2
|
||||
}
|
||||
|
||||
// Remove inner border and padding from Firefox, but don't restore the outline like Normalize.
|
||||
button::-moz-focus-inner,
|
||||
[type="button"]::-moz-focus-inner,
|
||||
[type="reset"]::-moz-focus-inner,
|
||||
[type="submit"]::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
input[type="radio"],
|
||||
input[type="checkbox"] {
|
||||
box-sizing: border-box; // 1. Add the correct box sizing in IE 10-
|
||||
padding: 0; // 2. Remove the padding in IE 10-
|
||||
}
|
||||
|
||||
|
||||
input[type="date"],
|
||||
input[type="time"],
|
||||
input[type="datetime-local"],
|
||||
input[type="month"] {
|
||||
// Remove the default appearance of temporal inputs to avoid a Mobile Safari
|
||||
// bug where setting a custom line-height prevents text from being vertically
|
||||
// centered within the input.
|
||||
// See https://bugs.webkit.org/show_bug.cgi?id=139848
|
||||
// and https://github.com/twbs/bootstrap/issues/11266
|
||||
-webkit-appearance: listbox;
|
||||
}
|
||||
|
||||
textarea {
|
||||
overflow: auto; // Remove the default vertical scrollbar in IE.
|
||||
// Textareas should really only resize vertically so they don't break their (horizontal) containers.
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
// Browsers set a default `min-width: min-content;` on fieldsets,
|
||||
// unlike e.g. `<div>`s, which have `min-width: 0;` by default.
|
||||
// So we reset that to ensure fieldsets behave more like a standard block element.
|
||||
// See https://github.com/twbs/bootstrap/issues/12359
|
||||
// and https://html.spec.whatwg.org/multipage/#the-fieldset-and-legend-elements
|
||||
min-width: 0;
|
||||
// Reset the default outline behavior of fieldsets so they don't affect page layout.
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
// 1. Correct the text wrapping in Edge and IE.
|
||||
// 2. Correct the color inheritance from `fieldset` elements in IE.
|
||||
legend {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 100%; // 1
|
||||
padding: 0;
|
||||
margin-bottom: .5rem;
|
||||
font-size: 1.5rem;
|
||||
line-height: inherit;
|
||||
color: inherit; // 2
|
||||
white-space: normal; // 1
|
||||
}
|
||||
|
||||
progress {
|
||||
vertical-align: baseline; // Add the correct vertical alignment in Chrome, Firefox, and Opera.
|
||||
}
|
||||
|
||||
// Correct the cursor style of increment and decrement buttons in Chrome.
|
||||
[type="number"]::-webkit-inner-spin-button,
|
||||
[type="number"]::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[type="search"] {
|
||||
// This overrides the extra rounded corners on search inputs in iOS so that our
|
||||
// `.form-control` class can properly style them. Note that this cannot simply
|
||||
// be added to `.form-control` as it's not specific enough. For details, see
|
||||
// https://github.com/twbs/bootstrap/issues/11586.
|
||||
outline-offset: -2px; // 2. Correct the outline style in Safari.
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
//
|
||||
// Remove the inner padding and cancel buttons in Chrome and Safari on macOS.
|
||||
//
|
||||
|
||||
[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
//
|
||||
// 1. Correct the inability to style clickable types in iOS and Safari.
|
||||
// 2. Change font properties to `inherit` in Safari.
|
||||
//
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
font: inherit; // 2
|
||||
-webkit-appearance: button; // 1
|
||||
}
|
||||
|
||||
//
|
||||
// Correct element displays
|
||||
//
|
||||
|
||||
output {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item; // Add the correct display in all browsers
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
template {
|
||||
display: none; // Add the correct display in IE
|
||||
}
|
||||
|
||||
// Always hide an element with the `hidden` HTML attribute (from PureCSS).
|
||||
// Needed for proper display in IE 10-.
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
19
app/static/bootstrap-4.2.1/scss/_root.scss
vendored
Normal file
19
app/static/bootstrap-4.2.1/scss/_root.scss
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
:root {
|
||||
// Custom variable values only support SassScript inside `#{}`.
|
||||
@each $color, $value in $colors {
|
||||
--#{$color}: #{$value};
|
||||
}
|
||||
|
||||
@each $color, $value in $theme-colors {
|
||||
--#{$color}: #{$value};
|
||||
}
|
||||
|
||||
@each $bp, $value in $grid-breakpoints {
|
||||
--breakpoint-#{$bp}: #{$value};
|
||||
}
|
||||
|
||||
// Use `inspect` for lists so that quoted items keep the quotes.
|
||||
// See https://github.com/sass/sass/issues/2383#issuecomment-336349172
|
||||
--font-family-sans-serif: #{inspect($font-family-sans-serif)};
|
||||
--font-family-monospace: #{inspect($font-family-monospace)};
|
||||
}
|
53
app/static/bootstrap-4.2.1/scss/_spinners.scss
vendored
Normal file
53
app/static/bootstrap-4.2.1/scss/_spinners.scss
vendored
Normal file
@ -0,0 +1,53 @@
|
||||
//
|
||||
// Rotating border
|
||||
//
|
||||
|
||||
@keyframes spinner-border {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.spinner-border {
|
||||
display: inline-block;
|
||||
width: $spinner-width;
|
||||
height: $spinner-height;
|
||||
vertical-align: text-bottom;
|
||||
border: $spinner-border-width solid currentColor;
|
||||
border-right-color: transparent;
|
||||
border-radius: 50%;
|
||||
animation: spinner-border .75s linear infinite;
|
||||
}
|
||||
|
||||
.spinner-border-sm {
|
||||
width: $spinner-width-sm;
|
||||
height: $spinner-height-sm;
|
||||
border-width: $spinner-border-width-sm;
|
||||
}
|
||||
|
||||
//
|
||||
// Growing circle
|
||||
//
|
||||
|
||||
@keyframes spinner-grow {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.spinner-grow {
|
||||
display: inline-block;
|
||||
width: $spinner-width;
|
||||
height: $spinner-height;
|
||||
vertical-align: text-bottom;
|
||||
background-color: currentColor;
|
||||
border-radius: 50%;
|
||||
opacity: 0;
|
||||
animation: spinner-grow .75s linear infinite;
|
||||
}
|
||||
|
||||
.spinner-grow-sm {
|
||||
width: $spinner-width-sm;
|
||||
height: $spinner-height-sm;
|
||||
}
|
187
app/static/bootstrap-4.2.1/scss/_tables.scss
vendored
Normal file
187
app/static/bootstrap-4.2.1/scss/_tables.scss
vendored
Normal file
@ -0,0 +1,187 @@
|
||||
//
|
||||
// Basic Bootstrap table
|
||||
//
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
margin-bottom: $spacer;
|
||||
background-color: $table-bg; // Reset for nesting within parents with `background-color`.
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: $table-cell-padding;
|
||||
vertical-align: top;
|
||||
border-top: $table-border-width solid $table-border-color;
|
||||
}
|
||||
|
||||
thead th {
|
||||
vertical-align: bottom;
|
||||
border-bottom: (2 * $table-border-width) solid $table-border-color;
|
||||
}
|
||||
|
||||
tbody + tbody {
|
||||
border-top: (2 * $table-border-width) solid $table-border-color;
|
||||
}
|
||||
|
||||
.table {
|
||||
background-color: $body-bg;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Condensed table w/ half padding
|
||||
//
|
||||
|
||||
.table-sm {
|
||||
th,
|
||||
td {
|
||||
padding: $table-cell-padding-sm;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Border versions
|
||||
//
|
||||
// Add or remove borders all around the table and between all the columns.
|
||||
|
||||
.table-bordered {
|
||||
border: $table-border-width solid $table-border-color;
|
||||
|
||||
th,
|
||||
td {
|
||||
border: $table-border-width solid $table-border-color;
|
||||
}
|
||||
|
||||
thead {
|
||||
th,
|
||||
td {
|
||||
border-bottom-width: 2 * $table-border-width;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-borderless {
|
||||
th,
|
||||
td,
|
||||
thead th,
|
||||
tbody + tbody {
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Zebra-striping
|
||||
//
|
||||
// Default zebra-stripe styles (alternating gray and transparent backgrounds)
|
||||
|
||||
.table-striped {
|
||||
tbody tr:nth-of-type(#{$table-striped-order}) {
|
||||
background-color: $table-accent-bg;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Hover effect
|
||||
//
|
||||
// Placed here since it has to come after the potential zebra striping
|
||||
|
||||
.table-hover {
|
||||
tbody tr {
|
||||
@include hover {
|
||||
background-color: $table-hover-bg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Table backgrounds
|
||||
//
|
||||
// Exact selectors below required to override `.table-striped` and prevent
|
||||
// inheritance to nested tables.
|
||||
|
||||
@each $color, $value in $theme-colors {
|
||||
@include table-row-variant($color, theme-color-level($color, $table-bg-level), theme-color-level($color, $table-border-level));
|
||||
}
|
||||
|
||||
@include table-row-variant(active, $table-active-bg);
|
||||
|
||||
|
||||
// Dark styles
|
||||
//
|
||||
// Same table markup, but inverted color scheme: dark background and light text.
|
||||
|
||||
// stylelint-disable-next-line no-duplicate-selectors
|
||||
.table {
|
||||
.thead-dark {
|
||||
th {
|
||||
color: $table-dark-color;
|
||||
background-color: $table-dark-bg;
|
||||
border-color: $table-dark-border-color;
|
||||
}
|
||||
}
|
||||
|
||||
.thead-light {
|
||||
th {
|
||||
color: $table-head-color;
|
||||
background-color: $table-head-bg;
|
||||
border-color: $table-border-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-dark {
|
||||
color: $table-dark-color;
|
||||
background-color: $table-dark-bg;
|
||||
|
||||
th,
|
||||
td,
|
||||
thead th {
|
||||
border-color: $table-dark-border-color;
|
||||
}
|
||||
|
||||
&.table-bordered {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
&.table-striped {
|
||||
tbody tr:nth-of-type(odd) {
|
||||
background-color: $table-dark-accent-bg;
|
||||
}
|
||||
}
|
||||
|
||||
&.table-hover {
|
||||
tbody tr {
|
||||
@include hover {
|
||||
background-color: $table-dark-hover-bg;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Responsive tables
|
||||
//
|
||||
// Generate series of `.table-responsive-*` classes for configuring the screen
|
||||
// size of where your table will overflow.
|
||||
|
||||
.table-responsive {
|
||||
@each $breakpoint in map-keys($grid-breakpoints) {
|
||||
$next: breakpoint-next($breakpoint, $grid-breakpoints);
|
||||
$infix: breakpoint-infix($next, $grid-breakpoints);
|
||||
|
||||
&#{$infix} {
|
||||
@include media-breakpoint-down($breakpoint) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
-ms-overflow-style: -ms-autohiding-scrollbar; // See https://github.com/twbs/bootstrap/pull/10057
|
||||
|
||||
// Prevent double border on horizontal scroll due to use of `display: block;`
|
||||
> .table-bordered {
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
43
app/static/bootstrap-4.2.1/scss/_toasts.scss
vendored
Normal file
43
app/static/bootstrap-4.2.1/scss/_toasts.scss
vendored
Normal file
@ -0,0 +1,43 @@
|
||||
.toast {
|
||||
max-width: $toast-max-width;
|
||||
overflow: hidden; // cheap rounded corners on nested items
|
||||
font-size: $toast-font-size; // knock it down to 14px
|
||||
background-color: $toast-background-color;
|
||||
background-clip: padding-box;
|
||||
border: $toast-border-width solid $toast-border-color;
|
||||
border-radius: $toast-border-radius;
|
||||
box-shadow: $toast-box-shadow;
|
||||
backdrop-filter: blur(10px);
|
||||
opacity: 0;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: $toast-padding-x;
|
||||
}
|
||||
|
||||
&.showing {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.show {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.hide {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.toast-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: $toast-padding-y $toast-padding-x;
|
||||
color: $toast-header-color;
|
||||
background-color: $toast-header-background-color;
|
||||
background-clip: padding-box;
|
||||
border-bottom: $toast-border-width solid $toast-header-border-color;
|
||||
}
|
||||
|
||||
.toast-body {
|
||||
padding: $toast-padding-x; // apply to both vertical and horizontal
|
||||
}
|
115
app/static/bootstrap-4.2.1/scss/_tooltip.scss
vendored
Normal file
115
app/static/bootstrap-4.2.1/scss/_tooltip.scss
vendored
Normal file
@ -0,0 +1,115 @@
|
||||
// Base class
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
z-index: $zindex-tooltip;
|
||||
display: block;
|
||||
margin: $tooltip-margin;
|
||||
// Our parent element can be arbitrary since tooltips are by default inserted as a sibling of their target element.
|
||||
// So reset our font and text properties to avoid inheriting weird values.
|
||||
@include reset-text();
|
||||
font-size: $tooltip-font-size;
|
||||
// Allow breaking very long words so they don't overflow the tooltip's bounds
|
||||
word-wrap: break-word;
|
||||
opacity: 0;
|
||||
|
||||
&.show { opacity: $tooltip-opacity; }
|
||||
|
||||
.arrow {
|
||||
position: absolute;
|
||||
display: block;
|
||||
width: $tooltip-arrow-width;
|
||||
height: $tooltip-arrow-height;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
border-color: transparent;
|
||||
border-style: solid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bs-tooltip-top {
|
||||
padding: $tooltip-arrow-height 0;
|
||||
|
||||
.arrow {
|
||||
bottom: 0;
|
||||
|
||||
&::before {
|
||||
top: 0;
|
||||
border-width: $tooltip-arrow-height ($tooltip-arrow-width / 2) 0;
|
||||
border-top-color: $tooltip-arrow-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bs-tooltip-right {
|
||||
padding: 0 $tooltip-arrow-height;
|
||||
|
||||
.arrow {
|
||||
left: 0;
|
||||
width: $tooltip-arrow-height;
|
||||
height: $tooltip-arrow-width;
|
||||
|
||||
&::before {
|
||||
right: 0;
|
||||
border-width: ($tooltip-arrow-width / 2) $tooltip-arrow-height ($tooltip-arrow-width / 2) 0;
|
||||
border-right-color: $tooltip-arrow-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bs-tooltip-bottom {
|
||||
padding: $tooltip-arrow-height 0;
|
||||
|
||||
.arrow {
|
||||
top: 0;
|
||||
|
||||
&::before {
|
||||
bottom: 0;
|
||||
border-width: 0 ($tooltip-arrow-width / 2) $tooltip-arrow-height;
|
||||
border-bottom-color: $tooltip-arrow-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bs-tooltip-left {
|
||||
padding: 0 $tooltip-arrow-height;
|
||||
|
||||
.arrow {
|
||||
right: 0;
|
||||
width: $tooltip-arrow-height;
|
||||
height: $tooltip-arrow-width;
|
||||
|
||||
&::before {
|
||||
left: 0;
|
||||
border-width: ($tooltip-arrow-width / 2) 0 ($tooltip-arrow-width / 2) $tooltip-arrow-height;
|
||||
border-left-color: $tooltip-arrow-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bs-tooltip-auto {
|
||||
&[x-placement^="top"] {
|
||||
@extend .bs-tooltip-top;
|
||||
}
|
||||
&[x-placement^="right"] {
|
||||
@extend .bs-tooltip-right;
|
||||
}
|
||||
&[x-placement^="bottom"] {
|
||||
@extend .bs-tooltip-bottom;
|
||||
}
|
||||
&[x-placement^="left"] {
|
||||
@extend .bs-tooltip-left;
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapper for the tooltip content
|
||||
.tooltip-inner {
|
||||
max-width: $tooltip-max-width;
|
||||
padding: $tooltip-padding-y $tooltip-padding-x;
|
||||
color: $tooltip-color;
|
||||
text-align: center;
|
||||
background-color: $tooltip-bg;
|
||||
@include border-radius($tooltip-border-radius);
|
||||
}
|
22
app/static/bootstrap-4.2.1/scss/_transitions.scss
vendored
Normal file
22
app/static/bootstrap-4.2.1/scss/_transitions.scss
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
// stylelint-disable selector-no-qualifying-type
|
||||
|
||||
.fade {
|
||||
@include transition($transition-fade);
|
||||
|
||||
&:not(.show) {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.collapse {
|
||||
&:not(.show) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.collapsing {
|
||||
position: relative;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
@include transition($transition-collapse);
|
||||
}
|
125
app/static/bootstrap-4.2.1/scss/_type.scss
vendored
Normal file
125
app/static/bootstrap-4.2.1/scss/_type.scss
vendored
Normal file
@ -0,0 +1,125 @@
|
||||
// stylelint-disable declaration-no-important, selector-list-comma-newline-after
|
||||
|
||||
//
|
||||
// Headings
|
||||
//
|
||||
|
||||
h1, h2, h3, h4, h5, h6,
|
||||
.h1, .h2, .h3, .h4, .h5, .h6 {
|
||||
margin-bottom: $headings-margin-bottom;
|
||||
font-family: $headings-font-family;
|
||||
font-weight: $headings-font-weight;
|
||||
line-height: $headings-line-height;
|
||||
color: $headings-color;
|
||||
}
|
||||
|
||||
h1, .h1 { font-size: $h1-font-size; }
|
||||
h2, .h2 { font-size: $h2-font-size; }
|
||||
h3, .h3 { font-size: $h3-font-size; }
|
||||
h4, .h4 { font-size: $h4-font-size; }
|
||||
h5, .h5 { font-size: $h5-font-size; }
|
||||
h6, .h6 { font-size: $h6-font-size; }
|
||||
|
||||
.lead {
|
||||
font-size: $lead-font-size;
|
||||
font-weight: $lead-font-weight;
|
||||
}
|
||||
|
||||
// Type display classes
|
||||
.display-1 {
|
||||
font-size: $display1-size;
|
||||
font-weight: $display1-weight;
|
||||
line-height: $display-line-height;
|
||||
}
|
||||
.display-2 {
|
||||
font-size: $display2-size;
|
||||
font-weight: $display2-weight;
|
||||
line-height: $display-line-height;
|
||||
}
|
||||
.display-3 {
|
||||
font-size: $display3-size;
|
||||
font-weight: $display3-weight;
|
||||
line-height: $display-line-height;
|
||||
}
|
||||
.display-4 {
|
||||
font-size: $display4-size;
|
||||
font-weight: $display4-weight;
|
||||
line-height: $display-line-height;
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Horizontal rules
|
||||
//
|
||||
|
||||
hr {
|
||||
margin-top: $hr-margin-y;
|
||||
margin-bottom: $hr-margin-y;
|
||||
border: 0;
|
||||
border-top: $hr-border-width solid $hr-border-color;
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Emphasis
|
||||
//
|
||||
|
||||
small,
|
||||
.small {
|
||||
font-size: $small-font-size;
|
||||
font-weight: $font-weight-normal;
|
||||
}
|
||||
|
||||
mark,
|
||||
.mark {
|
||||
padding: $mark-padding;
|
||||
background-color: $mark-bg;
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Lists
|
||||
//
|
||||
|
||||
.list-unstyled {
|
||||
@include list-unstyled;
|
||||
}
|
||||
|
||||
// Inline turns list items into inline-block
|
||||
.list-inline {
|
||||
@include list-unstyled;
|
||||
}
|
||||
.list-inline-item {
|
||||
display: inline-block;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: $list-inline-padding;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Misc
|
||||
//
|
||||
|
||||
// Builds on `abbr`
|
||||
.initialism {
|
||||
font-size: 90%;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
// Blockquotes
|
||||
.blockquote {
|
||||
margin-bottom: $spacer;
|
||||
font-size: $blockquote-font-size;
|
||||
}
|
||||
|
||||
.blockquote-footer {
|
||||
display: block;
|
||||
font-size: $blockquote-small-font-size;
|
||||
color: $blockquote-small-color;
|
||||
|
||||
&::before {
|
||||
content: "\2014\00A0"; // em dash, nbsp
|
||||
}
|
||||
}
|
16
app/static/bootstrap-4.2.1/scss/_utilities.scss
vendored
Normal file
16
app/static/bootstrap-4.2.1/scss/_utilities.scss
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
@import "utilities/align";
|
||||
@import "utilities/background";
|
||||
@import "utilities/borders";
|
||||
@import "utilities/clearfix";
|
||||
@import "utilities/display";
|
||||
@import "utilities/embed";
|
||||
@import "utilities/flex";
|
||||
@import "utilities/float";
|
||||
@import "utilities/overflow";
|
||||
@import "utilities/position";
|
||||
@import "utilities/screenreaders";
|
||||
@import "utilities/shadows";
|
||||
@import "utilities/sizing";
|
||||
@import "utilities/spacing";
|
||||
@import "utilities/text";
|
||||
@import "utilities/visibility";
|
1091
app/static/bootstrap-4.2.1/scss/_variables.scss
vendored
Normal file
1091
app/static/bootstrap-4.2.1/scss/_variables.scss
vendored
Normal file
File diff suppressed because it is too large
Load Diff
29
app/static/bootstrap-4.2.1/scss/bootstrap-grid.scss
vendored
Normal file
29
app/static/bootstrap-4.2.1/scss/bootstrap-grid.scss
vendored
Normal file
@ -0,0 +1,29 @@
|
||||
/*!
|
||||
* Bootstrap Grid v4.2.1 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2018 The Bootstrap Authors
|
||||
* Copyright 2011-2018 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||
*/
|
||||
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
-ms-overflow-style: scrollbar;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
@import "functions";
|
||||
@import "variables";
|
||||
|
||||
@import "mixins/breakpoints";
|
||||
@import "mixins/grid-framework";
|
||||
@import "mixins/grid";
|
||||
|
||||
@import "grid";
|
||||
@import "utilities/display";
|
||||
@import "utilities/flex";
|
||||
@import "utilities/spacing";
|
12
app/static/bootstrap-4.2.1/scss/bootstrap-reboot.scss
vendored
Normal file
12
app/static/bootstrap-4.2.1/scss/bootstrap-reboot.scss
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v4.2.1 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2018 The Bootstrap Authors
|
||||
* Copyright 2011-2018 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
|
||||
*/
|
||||
|
||||
@import "functions";
|
||||
@import "variables";
|
||||
@import "mixins";
|
||||
@import "reboot";
|
44
app/static/bootstrap-4.2.1/scss/bootstrap.scss
vendored
Normal file
44
app/static/bootstrap-4.2.1/scss/bootstrap.scss
vendored
Normal file
@ -0,0 +1,44 @@
|
||||
/*!
|
||||
* Bootstrap v4.2.1 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2018 The Bootstrap Authors
|
||||
* Copyright 2011-2018 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||
*/
|
||||
|
||||
@import "functions";
|
||||
@import "variables";
|
||||
@import "mixins";
|
||||
@import "root";
|
||||
@import "reboot";
|
||||
@import "type";
|
||||
@import "images";
|
||||
@import "code";
|
||||
@import "grid";
|
||||
@import "tables";
|
||||
@import "forms";
|
||||
@import "buttons";
|
||||
@import "transitions";
|
||||
@import "dropdown";
|
||||
@import "button-group";
|
||||
@import "input-group";
|
||||
@import "custom-forms";
|
||||
@import "nav";
|
||||
@import "navbar";
|
||||
@import "card";
|
||||
@import "breadcrumb";
|
||||
@import "pagination";
|
||||
@import "badge";
|
||||
@import "jumbotron";
|
||||
@import "alert";
|
||||
@import "progress";
|
||||
@import "media";
|
||||
@import "list-group";
|
||||
@import "close";
|
||||
@import "toasts";
|
||||
@import "modal";
|
||||
@import "tooltip";
|
||||
@import "popover";
|
||||
@import "carousel";
|
||||
@import "spinners";
|
||||
@import "utilities";
|
||||
@import "print";
|
13
app/static/bootstrap-4.2.1/scss/mixins/_alert.scss
vendored
Normal file
13
app/static/bootstrap-4.2.1/scss/mixins/_alert.scss
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
@mixin alert-variant($background, $border, $color) {
|
||||
color: $color;
|
||||
@include gradient-bg($background);
|
||||
border-color: $border;
|
||||
|
||||
hr {
|
||||
border-top-color: darken($border, 5%);
|
||||
}
|
||||
|
||||
.alert-link {
|
||||
color: darken($color, 10%);
|
||||
}
|
||||
}
|
21
app/static/bootstrap-4.2.1/scss/mixins/_background-variant.scss
vendored
Normal file
21
app/static/bootstrap-4.2.1/scss/mixins/_background-variant.scss
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
// stylelint-disable declaration-no-important
|
||||
|
||||
// Contextual backgrounds
|
||||
|
||||
@mixin bg-variant($parent, $color) {
|
||||
#{$parent} {
|
||||
background-color: $color !important;
|
||||
}
|
||||
a#{$parent},
|
||||
button#{$parent} {
|
||||
@include hover-focus {
|
||||
background-color: darken($color, 10%) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin bg-gradient-variant($parent, $color) {
|
||||
#{$parent} {
|
||||
background: $color linear-gradient(180deg, mix($body-bg, $color, 15%), $color) repeat-x !important;
|
||||
}
|
||||
}
|
11
app/static/bootstrap-4.2.1/scss/mixins/_badge.scss
vendored
Normal file
11
app/static/bootstrap-4.2.1/scss/mixins/_badge.scss
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
@mixin badge-variant($bg) {
|
||||
color: color-yiq($bg);
|
||||
background-color: $bg;
|
||||
|
||||
@at-root a#{&} {
|
||||
@include hover-focus {
|
||||
color: color-yiq($bg);
|
||||
background-color: darken($bg, 10%);
|
||||
}
|
||||
}
|
||||
}
|
35
app/static/bootstrap-4.2.1/scss/mixins/_border-radius.scss
vendored
Normal file
35
app/static/bootstrap-4.2.1/scss/mixins/_border-radius.scss
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
// Single side border-radius
|
||||
|
||||
@mixin border-radius($radius: $border-radius) {
|
||||
@if $enable-rounded {
|
||||
border-radius: $radius;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin border-top-radius($radius) {
|
||||
@if $enable-rounded {
|
||||
border-top-left-radius: $radius;
|
||||
border-top-right-radius: $radius;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin border-right-radius($radius) {
|
||||
@if $enable-rounded {
|
||||
border-top-right-radius: $radius;
|
||||
border-bottom-right-radius: $radius;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin border-bottom-radius($radius) {
|
||||
@if $enable-rounded {
|
||||
border-bottom-right-radius: $radius;
|
||||
border-bottom-left-radius: $radius;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin border-left-radius($radius) {
|
||||
@if $enable-rounded {
|
||||
border-top-left-radius: $radius;
|
||||
border-bottom-left-radius: $radius;
|
||||
}
|
||||
}
|
5
app/static/bootstrap-4.2.1/scss/mixins/_box-shadow.scss
vendored
Normal file
5
app/static/bootstrap-4.2.1/scss/mixins/_box-shadow.scss
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
@mixin box-shadow($shadow...) {
|
||||
@if $enable-shadows {
|
||||
box-shadow: $shadow;
|
||||
}
|
||||
}
|
123
app/static/bootstrap-4.2.1/scss/mixins/_breakpoints.scss
vendored
Normal file
123
app/static/bootstrap-4.2.1/scss/mixins/_breakpoints.scss
vendored
Normal file
@ -0,0 +1,123 @@
|
||||
// Breakpoint viewport sizes and media queries.
|
||||
//
|
||||
// Breakpoints are defined as a map of (name: minimum width), order from small to large:
|
||||
//
|
||||
// (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px)
|
||||
//
|
||||
// The map defined in the `$grid-breakpoints` global variable is used as the `$breakpoints` argument by default.
|
||||
|
||||
// Name of the next breakpoint, or null for the last breakpoint.
|
||||
//
|
||||
// >> breakpoint-next(sm)
|
||||
// md
|
||||
// >> breakpoint-next(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))
|
||||
// md
|
||||
// >> breakpoint-next(sm, $breakpoint-names: (xs sm md lg xl))
|
||||
// md
|
||||
@function breakpoint-next($name, $breakpoints: $grid-breakpoints, $breakpoint-names: map-keys($breakpoints)) {
|
||||
$n: index($breakpoint-names, $name);
|
||||
@return if($n != null and $n < length($breakpoint-names), nth($breakpoint-names, $n + 1), null);
|
||||
}
|
||||
|
||||
// Minimum breakpoint width. Null for the smallest (first) breakpoint.
|
||||
//
|
||||
// >> breakpoint-min(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))
|
||||
// 576px
|
||||
@function breakpoint-min($name, $breakpoints: $grid-breakpoints) {
|
||||
$min: map-get($breakpoints, $name);
|
||||
@return if($min != 0, $min, null);
|
||||
}
|
||||
|
||||
// Maximum breakpoint width. Null for the largest (last) breakpoint.
|
||||
// The maximum value is calculated as the minimum of the next one less 0.02px
|
||||
// to work around the limitations of `min-` and `max-` prefixes and viewports with fractional widths.
|
||||
// See https://www.w3.org/TR/mediaqueries-4/#mq-min-max
|
||||
// Uses 0.02px rather than 0.01px to work around a current rounding bug in Safari.
|
||||
// See https://bugs.webkit.org/show_bug.cgi?id=178261
|
||||
//
|
||||
// >> breakpoint-max(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))
|
||||
// 767.98px
|
||||
@function breakpoint-max($name, $breakpoints: $grid-breakpoints) {
|
||||
$next: breakpoint-next($name, $breakpoints);
|
||||
@return if($next, breakpoint-min($next, $breakpoints) - .02, null);
|
||||
}
|
||||
|
||||
// Returns a blank string if smallest breakpoint, otherwise returns the name with a dash in front.
|
||||
// Useful for making responsive utilities.
|
||||
//
|
||||
// >> breakpoint-infix(xs, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))
|
||||
// "" (Returns a blank string)
|
||||
// >> breakpoint-infix(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))
|
||||
// "-sm"
|
||||
@function breakpoint-infix($name, $breakpoints: $grid-breakpoints) {
|
||||
@return if(breakpoint-min($name, $breakpoints) == null, "", "-#{$name}");
|
||||
}
|
||||
|
||||
// Media of at least the minimum breakpoint width. No query for the smallest breakpoint.
|
||||
// Makes the @content apply to the given breakpoint and wider.
|
||||
@mixin media-breakpoint-up($name, $breakpoints: $grid-breakpoints) {
|
||||
$min: breakpoint-min($name, $breakpoints);
|
||||
@if $min {
|
||||
@media (min-width: $min) {
|
||||
@content;
|
||||
}
|
||||
} @else {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
// Media of at most the maximum breakpoint width. No query for the largest breakpoint.
|
||||
// Makes the @content apply to the given breakpoint and narrower.
|
||||
@mixin media-breakpoint-down($name, $breakpoints: $grid-breakpoints) {
|
||||
$max: breakpoint-max($name, $breakpoints);
|
||||
@if $max {
|
||||
@media (max-width: $max) {
|
||||
@content;
|
||||
}
|
||||
} @else {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
// Media that spans multiple breakpoint widths.
|
||||
// Makes the @content apply between the min and max breakpoints
|
||||
@mixin media-breakpoint-between($lower, $upper, $breakpoints: $grid-breakpoints) {
|
||||
$min: breakpoint-min($lower, $breakpoints);
|
||||
$max: breakpoint-max($upper, $breakpoints);
|
||||
|
||||
@if $min != null and $max != null {
|
||||
@media (min-width: $min) and (max-width: $max) {
|
||||
@content;
|
||||
}
|
||||
} @else if $max == null {
|
||||
@include media-breakpoint-up($lower, $breakpoints) {
|
||||
@content;
|
||||
}
|
||||
} @else if $min == null {
|
||||
@include media-breakpoint-down($upper, $breakpoints) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Media between the breakpoint's minimum and maximum widths.
|
||||
// No minimum for the smallest breakpoint, and no maximum for the largest one.
|
||||
// Makes the @content apply only to the given breakpoint, not viewports any wider or narrower.
|
||||
@mixin media-breakpoint-only($name, $breakpoints: $grid-breakpoints) {
|
||||
$min: breakpoint-min($name, $breakpoints);
|
||||
$max: breakpoint-max($name, $breakpoints);
|
||||
|
||||
@if $min != null and $max != null {
|
||||
@media (min-width: $min) and (max-width: $max) {
|
||||
@content;
|
||||
}
|
||||
} @else if $max == null {
|
||||
@include media-breakpoint-up($name, $breakpoints) {
|
||||
@content;
|
||||
}
|
||||
} @else if $min == null {
|
||||
@include media-breakpoint-down($name, $breakpoints) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
}
|
111
app/static/bootstrap-4.2.1/scss/mixins/_buttons.scss
vendored
Normal file
111
app/static/bootstrap-4.2.1/scss/mixins/_buttons.scss
vendored
Normal file
@ -0,0 +1,111 @@
|
||||
// Button variants
|
||||
//
|
||||
// Easily pump out default styles, as well as :hover, :focus, :active,
|
||||
// and disabled options for all buttons
|
||||
|
||||
@mixin button-variant($background, $border, $hover-background: darken($background, 7.5%), $hover-border: darken($border, 10%), $active-background: darken($background, 10%), $active-border: darken($border, 12.5%)) {
|
||||
color: color-yiq($background);
|
||||
@include gradient-bg($background);
|
||||
border-color: $border;
|
||||
@include box-shadow($btn-box-shadow);
|
||||
|
||||
@include hover {
|
||||
color: color-yiq($hover-background);
|
||||
@include gradient-bg($hover-background);
|
||||
border-color: $hover-border;
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&.focus {
|
||||
// Avoid using mixin so we can pass custom focus shadow properly
|
||||
@if $enable-shadows {
|
||||
box-shadow: $btn-box-shadow, 0 0 0 $btn-focus-width rgba(mix(color-yiq($background), $border, 15%), .5);
|
||||
} @else {
|
||||
box-shadow: 0 0 0 $btn-focus-width rgba(mix(color-yiq($background), $border, 15%), .5);
|
||||
}
|
||||
}
|
||||
|
||||
// Disabled comes first so active can properly restyle
|
||||
&.disabled,
|
||||
&:disabled {
|
||||
color: color-yiq($background);
|
||||
background-color: $background;
|
||||
border-color: $border;
|
||||
// Remove CSS gradients if they're enabled
|
||||
@if $enable-gradients {
|
||||
background-image: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:disabled):not(.disabled):active,
|
||||
&:not(:disabled):not(.disabled).active,
|
||||
.show > &.dropdown-toggle {
|
||||
color: color-yiq($active-background);
|
||||
background-color: $active-background;
|
||||
@if $enable-gradients {
|
||||
background-image: none; // Remove the gradient for the pressed/active state
|
||||
}
|
||||
border-color: $active-border;
|
||||
|
||||
&:focus {
|
||||
// Avoid using mixin so we can pass custom focus shadow properly
|
||||
@if $enable-shadows {
|
||||
box-shadow: $btn-active-box-shadow, 0 0 0 $btn-focus-width rgba(mix(color-yiq($background), $border, 15%), .5);
|
||||
} @else {
|
||||
box-shadow: 0 0 0 $btn-focus-width rgba(mix(color-yiq($background), $border, 15%), .5);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin button-outline-variant($color, $color-hover: color-yiq($color), $active-background: $color, $active-border: $color) {
|
||||
color: $color;
|
||||
border-color: $color;
|
||||
|
||||
@include hover {
|
||||
color: $color-hover;
|
||||
background-color: $active-background;
|
||||
border-color: $active-border;
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&.focus {
|
||||
box-shadow: 0 0 0 $btn-focus-width rgba($color, .5);
|
||||
}
|
||||
|
||||
&.disabled,
|
||||
&:disabled {
|
||||
color: $color;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&:not(:disabled):not(.disabled):active,
|
||||
&:not(:disabled):not(.disabled).active,
|
||||
.show > &.dropdown-toggle {
|
||||
color: color-yiq($active-background);
|
||||
background-color: $active-background;
|
||||
border-color: $active-border;
|
||||
|
||||
&:focus {
|
||||
// Avoid using mixin so we can pass custom focus shadow properly
|
||||
@if $enable-shadows and $btn-active-box-shadow != none {
|
||||
box-shadow: $btn-active-box-shadow, 0 0 0 $btn-focus-width rgba($color, .5);
|
||||
} @else {
|
||||
box-shadow: 0 0 0 $btn-focus-width rgba($color, .5);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Button sizes
|
||||
@mixin button-size($padding-y, $padding-x, $font-size, $line-height, $border-radius) {
|
||||
padding: $padding-y $padding-x;
|
||||
font-size: $font-size;
|
||||
line-height: $line-height;
|
||||
// Manually declare to provide an override to the browser default
|
||||
@if $enable-rounded {
|
||||
border-radius: $border-radius;
|
||||
} @else {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
62
app/static/bootstrap-4.2.1/scss/mixins/_caret.scss
vendored
Normal file
62
app/static/bootstrap-4.2.1/scss/mixins/_caret.scss
vendored
Normal file
@ -0,0 +1,62 @@
|
||||
@mixin caret-down {
|
||||
border-top: $caret-width solid;
|
||||
border-right: $caret-width solid transparent;
|
||||
border-bottom: 0;
|
||||
border-left: $caret-width solid transparent;
|
||||
}
|
||||
|
||||
@mixin caret-up {
|
||||
border-top: 0;
|
||||
border-right: $caret-width solid transparent;
|
||||
border-bottom: $caret-width solid;
|
||||
border-left: $caret-width solid transparent;
|
||||
}
|
||||
|
||||
@mixin caret-right {
|
||||
border-top: $caret-width solid transparent;
|
||||
border-right: 0;
|
||||
border-bottom: $caret-width solid transparent;
|
||||
border-left: $caret-width solid;
|
||||
}
|
||||
|
||||
@mixin caret-left {
|
||||
border-top: $caret-width solid transparent;
|
||||
border-right: $caret-width solid;
|
||||
border-bottom: $caret-width solid transparent;
|
||||
}
|
||||
|
||||
@mixin caret($direction: down) {
|
||||
@if $enable-caret {
|
||||
&::after {
|
||||
display: inline-block;
|
||||
margin-left: $caret-width * .85;
|
||||
vertical-align: $caret-width * .85;
|
||||
content: "";
|
||||
@if $direction == down {
|
||||
@include caret-down;
|
||||
} @else if $direction == up {
|
||||
@include caret-up;
|
||||
} @else if $direction == right {
|
||||
@include caret-right;
|
||||
}
|
||||
}
|
||||
|
||||
@if $direction == left {
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&::before {
|
||||
display: inline-block;
|
||||
margin-right: $caret-width * .85;
|
||||
vertical-align: $caret-width * .85;
|
||||
content: "";
|
||||
@include caret-left;
|
||||
}
|
||||
}
|
||||
|
||||
&:empty::after {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
7
app/static/bootstrap-4.2.1/scss/mixins/_clearfix.scss
vendored
Normal file
7
app/static/bootstrap-4.2.1/scss/mixins/_clearfix.scss
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
@mixin clearfix() {
|
||||
&::after {
|
||||
display: block;
|
||||
clear: both;
|
||||
content: "";
|
||||
}
|
||||
}
|
11
app/static/bootstrap-4.2.1/scss/mixins/_float.scss
vendored
Normal file
11
app/static/bootstrap-4.2.1/scss/mixins/_float.scss
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
// stylelint-disable declaration-no-important
|
||||
|
||||
@mixin float-left {
|
||||
float: left !important;
|
||||
}
|
||||
@mixin float-right {
|
||||
float: right !important;
|
||||
}
|
||||
@mixin float-none {
|
||||
float: none !important;
|
||||
}
|
198
app/static/bootstrap-4.2.1/scss/mixins/_forms.scss
vendored
Normal file
198
app/static/bootstrap-4.2.1/scss/mixins/_forms.scss
vendored
Normal file
@ -0,0 +1,198 @@
|
||||
// Form control focus state
|
||||
//
|
||||
// Generate a customized focus state and for any input with the specified color,
|
||||
// which defaults to the `$input-focus-border-color` variable.
|
||||
//
|
||||
// We highly encourage you to not customize the default value, but instead use
|
||||
// this to tweak colors on an as-needed basis. This aesthetic change is based on
|
||||
// WebKit's default styles, but applicable to a wider range of browsers. Its
|
||||
// usability and accessibility should be taken into account with any change.
|
||||
//
|
||||
// Example usage: change the default blue border and shadow to white for better
|
||||
// contrast against a dark gray background.
|
||||
@mixin form-control-focus() {
|
||||
&:focus {
|
||||
color: $input-focus-color;
|
||||
background-color: $input-focus-bg;
|
||||
border-color: $input-focus-border-color;
|
||||
outline: 0;
|
||||
// Avoid using mixin so we can pass custom focus shadow properly
|
||||
@if $enable-shadows {
|
||||
box-shadow: $input-box-shadow, $input-focus-box-shadow;
|
||||
} @else {
|
||||
box-shadow: $input-focus-box-shadow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@mixin form-validation-state($state, $color) {
|
||||
.#{$state}-feedback {
|
||||
display: none;
|
||||
width: 100%;
|
||||
margin-top: $form-feedback-margin-top;
|
||||
font-size: $form-feedback-font-size;
|
||||
color: $color;
|
||||
}
|
||||
|
||||
.#{$state}-tooltip {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
z-index: 5;
|
||||
display: none;
|
||||
max-width: 100%; // Contain to parent when possible
|
||||
padding: $form-feedback-tooltip-padding-y $form-feedback-tooltip-padding-x;
|
||||
margin-top: .1rem;
|
||||
font-size: $form-feedback-tooltip-font-size;
|
||||
line-height: $form-feedback-tooltip-line-height;
|
||||
color: color-yiq($color);
|
||||
background-color: rgba($color, $form-feedback-tooltip-opacity);
|
||||
@include border-radius($form-feedback-tooltip-border-radius);
|
||||
}
|
||||
|
||||
.form-control {
|
||||
.was-validated &:#{$state},
|
||||
&.is-#{$state} {
|
||||
border-color: $color;
|
||||
|
||||
@if $enable-validation-icons {
|
||||
padding-right: $input-height-inner;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center right calc(#{$input-height-inner} / 4);
|
||||
background-size: calc(#{$input-height-inner} / 2) calc(#{$input-height-inner} / 2);
|
||||
|
||||
@if $state == "valid" {
|
||||
background-image: $form-feedback-icon-valid;
|
||||
} @else {
|
||||
background-image: $form-feedback-icon-invalid;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: $color;
|
||||
box-shadow: 0 0 0 $input-focus-width rgba($color, .25);
|
||||
}
|
||||
|
||||
~ .#{$state}-feedback,
|
||||
~ .#{$state}-tooltip {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// stylelint-disable-next-line selector-no-qualifying-type
|
||||
textarea.form-control {
|
||||
.was-validated &:#{$state},
|
||||
&.is-#{$state} {
|
||||
@if $enable-validation-icons {
|
||||
padding-right: $input-height-inner;
|
||||
background-position: top calc(#{$input-height-inner} / 4) right calc(#{$input-height-inner} / 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.custom-select {
|
||||
.was-validated &:#{$state},
|
||||
&.is-#{$state} {
|
||||
border-color: $color;
|
||||
|
||||
@if $enable-validation-icons {
|
||||
$form-feedback-icon: if($state == "valid", $form-feedback-icon-valid, $form-feedback-icon-invalid);
|
||||
padding-right: $custom-select-feedback-icon-padding-right;
|
||||
background: $custom-select-background, $form-feedback-icon no-repeat $custom-select-feedback-icon-position / $custom-select-feedback-icon-size;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: $color;
|
||||
box-shadow: 0 0 0 $input-focus-width rgba($color, .25);
|
||||
}
|
||||
|
||||
~ .#{$state}-feedback,
|
||||
~ .#{$state}-tooltip {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.form-control-file {
|
||||
.was-validated &:#{$state},
|
||||
&.is-#{$state} {
|
||||
~ .#{$state}-feedback,
|
||||
~ .#{$state}-tooltip {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-check-input {
|
||||
.was-validated &:#{$state},
|
||||
&.is-#{$state} {
|
||||
~ .form-check-label {
|
||||
color: $color;
|
||||
}
|
||||
|
||||
~ .#{$state}-feedback,
|
||||
~ .#{$state}-tooltip {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.custom-control-input {
|
||||
.was-validated &:#{$state},
|
||||
&.is-#{$state} {
|
||||
~ .custom-control-label {
|
||||
color: $color;
|
||||
|
||||
&::before {
|
||||
border-color: $color;
|
||||
}
|
||||
}
|
||||
|
||||
~ .#{$state}-feedback,
|
||||
~ .#{$state}-tooltip {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&:checked {
|
||||
~ .custom-control-label::before {
|
||||
border-color: lighten($color, 10%);
|
||||
@include gradient-bg(lighten($color, 10%));
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
~ .custom-control-label::before {
|
||||
box-shadow: 0 0 0 $input-focus-width rgba($color, .25);
|
||||
}
|
||||
|
||||
&:not(:checked) ~ .custom-control-label::before {
|
||||
border-color: $color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// custom file
|
||||
.custom-file-input {
|
||||
.was-validated &:#{$state},
|
||||
&.is-#{$state} {
|
||||
~ .custom-file-label {
|
||||
border-color: $color;
|
||||
}
|
||||
|
||||
~ .#{$state}-feedback,
|
||||
~ .#{$state}-tooltip {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
~ .custom-file-label {
|
||||
border-color: $color;
|
||||
box-shadow: 0 0 0 $input-focus-width rgba($color, .25);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
45
app/static/bootstrap-4.2.1/scss/mixins/_gradients.scss
vendored
Normal file
45
app/static/bootstrap-4.2.1/scss/mixins/_gradients.scss
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
// Gradients
|
||||
|
||||
@mixin gradient-bg($color) {
|
||||
@if $enable-gradients {
|
||||
background: $color linear-gradient(180deg, mix($body-bg, $color, 15%), $color) repeat-x;
|
||||
} @else {
|
||||
background-color: $color;
|
||||
}
|
||||
}
|
||||
|
||||
// Horizontal gradient, from left to right
|
||||
//
|
||||
// Creates two color stops, start and end, by specifying a color and position for each color stop.
|
||||
@mixin gradient-x($start-color: $gray-700, $end-color: $gray-800, $start-percent: 0%, $end-percent: 100%) {
|
||||
background-image: linear-gradient(to right, $start-color $start-percent, $end-color $end-percent);
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
|
||||
// Vertical gradient, from top to bottom
|
||||
//
|
||||
// Creates two color stops, start and end, by specifying a color and position for each color stop.
|
||||
@mixin gradient-y($start-color: $gray-700, $end-color: $gray-800, $start-percent: 0%, $end-percent: 100%) {
|
||||
background-image: linear-gradient(to bottom, $start-color $start-percent, $end-color $end-percent);
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
|
||||
@mixin gradient-directional($start-color: $gray-700, $end-color: $gray-800, $deg: 45deg) {
|
||||
background-image: linear-gradient($deg, $start-color, $end-color);
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
@mixin gradient-x-three-colors($start-color: $blue, $mid-color: $purple, $color-stop: 50%, $end-color: $red) {
|
||||
background-image: linear-gradient(to right, $start-color, $mid-color $color-stop, $end-color);
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
@mixin gradient-y-three-colors($start-color: $blue, $mid-color: $purple, $color-stop: 50%, $end-color: $red) {
|
||||
background-image: linear-gradient($start-color, $mid-color $color-stop, $end-color);
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
@mixin gradient-radial($inner-color: $gray-700, $outer-color: $gray-800) {
|
||||
background-image: radial-gradient(circle, $inner-color, $outer-color);
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
@mixin gradient-striped($color: rgba($white, .15), $angle: 45deg) {
|
||||
background-image: linear-gradient($angle, $color 25%, transparent 25%, transparent 50%, $color 50%, $color 75%, transparent 75%, transparent);
|
||||
}
|
66
app/static/bootstrap-4.2.1/scss/mixins/_grid-framework.scss
vendored
Normal file
66
app/static/bootstrap-4.2.1/scss/mixins/_grid-framework.scss
vendored
Normal file
@ -0,0 +1,66 @@
|
||||
// Framework grid generation
|
||||
//
|
||||
// Used only by Bootstrap to generate the correct number of grid classes given
|
||||
// any value of `$grid-columns`.
|
||||
|
||||
@mixin make-grid-columns($columns: $grid-columns, $gutter: $grid-gutter-width, $breakpoints: $grid-breakpoints) {
|
||||
// Common properties for all breakpoints
|
||||
%grid-column {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-right: $gutter / 2;
|
||||
padding-left: $gutter / 2;
|
||||
}
|
||||
|
||||
@each $breakpoint in map-keys($breakpoints) {
|
||||
$infix: breakpoint-infix($breakpoint, $breakpoints);
|
||||
|
||||
// Allow columns to stretch full width below their breakpoints
|
||||
@for $i from 1 through $columns {
|
||||
.col#{$infix}-#{$i} {
|
||||
@extend %grid-column;
|
||||
}
|
||||
}
|
||||
.col#{$infix},
|
||||
.col#{$infix}-auto {
|
||||
@extend %grid-column;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up($breakpoint, $breakpoints) {
|
||||
// Provide basic `.col-{bp}` classes for equal-width flexbox columns
|
||||
.col#{$infix} {
|
||||
flex-basis: 0;
|
||||
flex-grow: 1;
|
||||
max-width: 100%;
|
||||
}
|
||||
.col#{$infix}-auto {
|
||||
flex: 0 0 auto;
|
||||
width: auto;
|
||||
max-width: 100%; // Reset earlier grid tiers
|
||||
}
|
||||
|
||||
@for $i from 1 through $columns {
|
||||
.col#{$infix}-#{$i} {
|
||||
@include make-col($i, $columns);
|
||||
}
|
||||
}
|
||||
|
||||
.order#{$infix}-first { order: -1; }
|
||||
|
||||
.order#{$infix}-last { order: $columns + 1; }
|
||||
|
||||
@for $i from 0 through $columns {
|
||||
.order#{$infix}-#{$i} { order: $i; }
|
||||
}
|
||||
|
||||
// `$columns - 1` because offsetting by the width of an entire row isn't possible
|
||||
@for $i from 0 through ($columns - 1) {
|
||||
@if not ($infix == "" and $i == 0) { // Avoid emitting useless .offset-0
|
||||
.offset#{$infix}-#{$i} {
|
||||
@include make-col-offset($i, $columns);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
51
app/static/bootstrap-4.2.1/scss/mixins/_grid.scss
vendored
Normal file
51
app/static/bootstrap-4.2.1/scss/mixins/_grid.scss
vendored
Normal file
@ -0,0 +1,51 @@
|
||||
/// Grid system
|
||||
//
|
||||
// Generate semantic grid columns with these mixins.
|
||||
|
||||
@mixin make-container($gutter: $grid-gutter-width) {
|
||||
width: 100%;
|
||||
padding-right: $gutter / 2;
|
||||
padding-left: $gutter / 2;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
|
||||
// For each breakpoint, define the maximum width of the container in a media query
|
||||
@mixin make-container-max-widths($max-widths: $container-max-widths, $breakpoints: $grid-breakpoints) {
|
||||
@each $breakpoint, $container-max-width in $max-widths {
|
||||
@include media-breakpoint-up($breakpoint, $breakpoints) {
|
||||
max-width: $container-max-width;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin make-row($gutter: $grid-gutter-width) {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-right: -$gutter / 2;
|
||||
margin-left: -$gutter / 2;
|
||||
}
|
||||
|
||||
@mixin make-col-ready($gutter: $grid-gutter-width) {
|
||||
position: relative;
|
||||
// Prevent columns from becoming too narrow when at smaller grid tiers by
|
||||
// always setting `width: 100%;`. This works because we use `flex` values
|
||||
// later on to override this initial width.
|
||||
width: 100%;
|
||||
padding-right: $gutter / 2;
|
||||
padding-left: $gutter / 2;
|
||||
}
|
||||
|
||||
@mixin make-col($size, $columns: $grid-columns) {
|
||||
flex: 0 0 percentage($size / $columns);
|
||||
// Add a `max-width` to ensure content within each column does not blow out
|
||||
// the width of the column. Applies to IE10+ and Firefox. Chrome and Safari
|
||||
// do not appear to require this.
|
||||
max-width: percentage($size / $columns);
|
||||
}
|
||||
|
||||
@mixin make-col-offset($size, $columns: $grid-columns) {
|
||||
$num: $size / $columns;
|
||||
margin-left: if($num == 0, 0, percentage($num));
|
||||
}
|
37
app/static/bootstrap-4.2.1/scss/mixins/_hover.scss
vendored
Normal file
37
app/static/bootstrap-4.2.1/scss/mixins/_hover.scss
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
// Hover mixin and `$enable-hover-media-query` are deprecated.
|
||||
//
|
||||
// Originally added during our alphas and maintained during betas, this mixin was
|
||||
// designed to prevent `:hover` stickiness on iOS-an issue where hover styles
|
||||
// would persist after initial touch.
|
||||
//
|
||||
// For backward compatibility, we've kept these mixins and updated them to
|
||||
// always return their regular pseudo-classes instead of a shimmed media query.
|
||||
//
|
||||
// Issue: https://github.com/twbs/bootstrap/issues/25195
|
||||
|
||||
@mixin hover {
|
||||
&:hover { @content; }
|
||||
}
|
||||
|
||||
@mixin hover-focus {
|
||||
&:hover,
|
||||
&:focus {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin plain-hover-focus {
|
||||
&,
|
||||
&:hover,
|
||||
&:focus {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin hover-focus-active {
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
@content;
|
||||
}
|
||||
}
|
36
app/static/bootstrap-4.2.1/scss/mixins/_image.scss
vendored
Normal file
36
app/static/bootstrap-4.2.1/scss/mixins/_image.scss
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
// Image Mixins
|
||||
// - Responsive image
|
||||
// - Retina image
|
||||
|
||||
|
||||
// Responsive image
|
||||
//
|
||||
// Keep images from scaling beyond the width of their parents.
|
||||
|
||||
@mixin img-fluid {
|
||||
// Part 1: Set a maximum relative to the parent
|
||||
max-width: 100%;
|
||||
// Part 2: Override the height to auto, otherwise images will be stretched
|
||||
// when setting a width and height attribute on the img element.
|
||||
height: auto;
|
||||
}
|
||||
|
||||
|
||||
// Retina image
|
||||
//
|
||||
// Short retina mixin for setting background-image and -size.
|
||||
|
||||
// stylelint-disable indentation, media-query-list-comma-newline-after
|
||||
@mixin img-retina($file-1x, $file-2x, $width-1x, $height-1x) {
|
||||
background-image: url($file-1x);
|
||||
|
||||
// Autoprefixer takes care of adding -webkit-min-device-pixel-ratio and -o-min-device-pixel-ratio,
|
||||
// but doesn't convert dppx=>dpi.
|
||||
// There's no such thing as unprefixed min-device-pixel-ratio since it's nonstandard.
|
||||
// Compatibility info: https://caniuse.com/#feat=css-media-resolution
|
||||
@media only screen and (min-resolution: 192dpi), // IE9-11 don't support dppx
|
||||
only screen and (min-resolution: 2dppx) { // Standardized
|
||||
background-image: url($file-2x);
|
||||
background-size: $width-1x $height-1x;
|
||||
}
|
||||
}
|
21
app/static/bootstrap-4.2.1/scss/mixins/_list-group.scss
vendored
Normal file
21
app/static/bootstrap-4.2.1/scss/mixins/_list-group.scss
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
// List Groups
|
||||
|
||||
@mixin list-group-item-variant($state, $background, $color) {
|
||||
.list-group-item-#{$state} {
|
||||
color: $color;
|
||||
background-color: $background;
|
||||
|
||||
&.list-group-item-action {
|
||||
@include hover-focus {
|
||||
color: $color;
|
||||
background-color: darken($background, 5%);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: $white;
|
||||
background-color: $color;
|
||||
border-color: $color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
7
app/static/bootstrap-4.2.1/scss/mixins/_lists.scss
vendored
Normal file
7
app/static/bootstrap-4.2.1/scss/mixins/_lists.scss
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
// Lists
|
||||
|
||||
// Unstyled keeps list items block level, just removes default browser padding and list-style
|
||||
@mixin list-unstyled {
|
||||
padding-left: 0;
|
||||
list-style: none;
|
||||
}
|
10
app/static/bootstrap-4.2.1/scss/mixins/_nav-divider.scss
vendored
Normal file
10
app/static/bootstrap-4.2.1/scss/mixins/_nav-divider.scss
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
// Horizontal dividers
|
||||
//
|
||||
// Dividers (basically an hr) within dropdowns and nav lists
|
||||
|
||||
@mixin nav-divider($color: $nav-divider-color, $margin-y: $nav-divider-margin-y) {
|
||||
height: 0;
|
||||
margin: $margin-y 0;
|
||||
overflow: hidden;
|
||||
border-top: 1px solid $color;
|
||||
}
|
22
app/static/bootstrap-4.2.1/scss/mixins/_pagination.scss
vendored
Normal file
22
app/static/bootstrap-4.2.1/scss/mixins/_pagination.scss
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
// Pagination
|
||||
|
||||
@mixin pagination-size($padding-y, $padding-x, $font-size, $line-height, $border-radius) {
|
||||
.page-link {
|
||||
padding: $padding-y $padding-x;
|
||||
font-size: $font-size;
|
||||
line-height: $line-height;
|
||||
}
|
||||
|
||||
.page-item {
|
||||
&:first-child {
|
||||
.page-link {
|
||||
@include border-left-radius($border-radius);
|
||||
}
|
||||
}
|
||||
&:last-child {
|
||||
.page-link {
|
||||
@include border-right-radius($border-radius);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
17
app/static/bootstrap-4.2.1/scss/mixins/_reset-text.scss
vendored
Normal file
17
app/static/bootstrap-4.2.1/scss/mixins/_reset-text.scss
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
@mixin reset-text {
|
||||
font-family: $font-family-base;
|
||||
// We deliberately do NOT reset font-size or word-wrap.
|
||||
font-style: normal;
|
||||
font-weight: $font-weight-normal;
|
||||
line-height: $line-height-base;
|
||||
text-align: left; // Fallback for where `start` is not supported
|
||||
text-align: start; // stylelint-disable-line declaration-block-no-duplicate-properties
|
||||
text-decoration: none;
|
||||
text-shadow: none;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
word-break: normal;
|
||||
word-spacing: normal;
|
||||
white-space: normal;
|
||||
line-break: auto;
|
||||
}
|
6
app/static/bootstrap-4.2.1/scss/mixins/_resize.scss
vendored
Normal file
6
app/static/bootstrap-4.2.1/scss/mixins/_resize.scss
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
// Resize anything
|
||||
|
||||
@mixin resizable($direction) {
|
||||
overflow: auto; // Per CSS3 UI, `resize` only applies when `overflow` isn't `visible`
|
||||
resize: $direction; // Options: horizontal, vertical, both
|
||||
}
|
33
app/static/bootstrap-4.2.1/scss/mixins/_screen-reader.scss
vendored
Normal file
33
app/static/bootstrap-4.2.1/scss/mixins/_screen-reader.scss
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
// Only display content to screen readers
|
||||
//
|
||||
// See: https://a11yproject.com/posts/how-to-hide-content/
|
||||
// See: https://hugogiraudel.com/2016/10/13/css-hide-and-seek/
|
||||
|
||||
@mixin sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
// Use in conjunction with .sr-only to only display content when it's focused.
|
||||
//
|
||||
// Useful for "Skip to main content" links; see https://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1
|
||||
//
|
||||
// Credit: HTML5 Boilerplate
|
||||
|
||||
@mixin sr-only-focusable {
|
||||
&:active,
|
||||
&:focus {
|
||||
position: static;
|
||||
width: auto;
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
clip: auto;
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
6
app/static/bootstrap-4.2.1/scss/mixins/_size.scss
vendored
Normal file
6
app/static/bootstrap-4.2.1/scss/mixins/_size.scss
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
// Sizing shortcuts
|
||||
|
||||
@mixin size($width, $height: $width) {
|
||||
width: $width;
|
||||
height: $height;
|
||||
}
|
39
app/static/bootstrap-4.2.1/scss/mixins/_table-row.scss
vendored
Normal file
39
app/static/bootstrap-4.2.1/scss/mixins/_table-row.scss
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
// Tables
|
||||
|
||||
@mixin table-row-variant($state, $background, $border: null) {
|
||||
// Exact selectors below required to override `.table-striped` and prevent
|
||||
// inheritance to nested tables.
|
||||
.table-#{$state} {
|
||||
&,
|
||||
> th,
|
||||
> td {
|
||||
background-color: $background;
|
||||
}
|
||||
|
||||
@if $border != null {
|
||||
th,
|
||||
td,
|
||||
thead th,
|
||||
tbody + tbody {
|
||||
border-color: $border;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hover states for `.table-hover`
|
||||
// Note: this is not available for cells or rows within `thead` or `tfoot`.
|
||||
.table-hover {
|
||||
$hover-background: darken($background, 5%);
|
||||
|
||||
.table-#{$state} {
|
||||
@include hover {
|
||||
background-color: $hover-background;
|
||||
|
||||
> td,
|
||||
> th {
|
||||
background-color: $hover-background;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
14
app/static/bootstrap-4.2.1/scss/mixins/_text-emphasis.scss
vendored
Normal file
14
app/static/bootstrap-4.2.1/scss/mixins/_text-emphasis.scss
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
// stylelint-disable declaration-no-important
|
||||
|
||||
// Typography
|
||||
|
||||
@mixin text-emphasis-variant($parent, $color) {
|
||||
#{$parent} {
|
||||
color: $color !important;
|
||||
}
|
||||
a#{$parent} {
|
||||
@include hover-focus {
|
||||
color: darken($color, $emphasized-link-hover-darken-percentage) !important;
|
||||
}
|
||||
}
|
||||
}
|
13
app/static/bootstrap-4.2.1/scss/mixins/_text-hide.scss
vendored
Normal file
13
app/static/bootstrap-4.2.1/scss/mixins/_text-hide.scss
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
// CSS image replacement
|
||||
@mixin text-hide($ignore-warning: false) {
|
||||
// stylelint-disable-next-line font-family-no-missing-generic-family-keyword
|
||||
font: 0/0 a;
|
||||
color: transparent;
|
||||
text-shadow: none;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
|
||||
@if ($ignore-warning != true) {
|
||||
@warn "The `text-hide()` mixin has been deprecated as of v4.1.0. It will be removed entirely in v5.";
|
||||
}
|
||||
}
|
8
app/static/bootstrap-4.2.1/scss/mixins/_text-truncate.scss
vendored
Normal file
8
app/static/bootstrap-4.2.1/scss/mixins/_text-truncate.scss
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
// Text truncate
|
||||
// Requires inline-block or block for proper styling
|
||||
|
||||
@mixin text-truncate() {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
16
app/static/bootstrap-4.2.1/scss/mixins/_transition.scss
vendored
Normal file
16
app/static/bootstrap-4.2.1/scss/mixins/_transition.scss
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
// stylelint-disable property-blacklist
|
||||
@mixin transition($transition...) {
|
||||
@if $enable-transitions {
|
||||
@if length($transition) == 0 {
|
||||
transition: $transition-base;
|
||||
} @else {
|
||||
transition: $transition;
|
||||
}
|
||||
}
|
||||
|
||||
@if $enable-prefers-reduced-motion-media-query {
|
||||
@media screen and (prefers-reduced-motion: reduce) {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
}
|
7
app/static/bootstrap-4.2.1/scss/mixins/_visibility.scss
vendored
Normal file
7
app/static/bootstrap-4.2.1/scss/mixins/_visibility.scss
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
// stylelint-disable declaration-no-important
|
||||
|
||||
// Visibility
|
||||
|
||||
@mixin invisible($visibility) {
|
||||
visibility: $visibility !important;
|
||||
}
|
8
app/static/bootstrap-4.2.1/scss/utilities/_align.scss
vendored
Normal file
8
app/static/bootstrap-4.2.1/scss/utilities/_align.scss
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
// stylelint-disable declaration-no-important
|
||||
|
||||
.align-baseline { vertical-align: baseline !important; } // Browser default
|
||||
.align-top { vertical-align: top !important; }
|
||||
.align-middle { vertical-align: middle !important; }
|
||||
.align-bottom { vertical-align: bottom !important; }
|
||||
.align-text-bottom { vertical-align: text-bottom !important; }
|
||||
.align-text-top { vertical-align: text-top !important; }
|
19
app/static/bootstrap-4.2.1/scss/utilities/_background.scss
vendored
Normal file
19
app/static/bootstrap-4.2.1/scss/utilities/_background.scss
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
// stylelint-disable declaration-no-important
|
||||
|
||||
@each $color, $value in $theme-colors {
|
||||
@include bg-variant(".bg-#{$color}", $value);
|
||||
}
|
||||
|
||||
@if $enable-gradients {
|
||||
@each $color, $value in $theme-colors {
|
||||
@include bg-gradient-variant(".bg-gradient-#{$color}", $value);
|
||||
}
|
||||
}
|
||||
|
||||
.bg-white {
|
||||
background-color: $white !important;
|
||||
}
|
||||
|
||||
.bg-transparent {
|
||||
background-color: transparent !important;
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user