├── migrations ├── README ├── script.py.mako ├── alembic.ini ├── versions │ └── 02defd0699aa_initial.py └── env.py ├── .gitignore ├── MANIFEST.in ├── README.md ├── app ├── templates │ ├── upload.html │ ├── delete.html │ ├── edit_version.html │ ├── plugins.html │ ├── install.html │ ├── edit_plugin.html │ ├── base.html │ └── plugin.html ├── well-known │ ├── apple-app-site-association │ └── assetlinks.json ├── __init__.py ├── api.py ├── auth.py ├── database.py ├── countries.json ├── plugins.py └── static │ └── bootstrap.min.js ├── pyproject.toml └── LICENSE /migrations/README: -------------------------------------------------------------------------------- 1 | Single-database configuration for Flask. 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | instance/ 2 | __pycache__/ 3 | .venv/ 4 | build/ 5 | *.egg-info/ 6 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft app/templates 2 | graft app/static 3 | graft app/well-known 4 | graft migrations 5 | include app/countries.json 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Every Door Plugin Repository 2 | 3 | Written in Flask, have fun reading the code lol. 4 | 5 | ## Author and License 6 | 7 | Written by Ilya Zverev, published under the ISC License. 8 | -------------------------------------------------------------------------------- /migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /app/templates/upload.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 |

Upload a Plugin

4 |

You can also update an existing plugin from here by uploading a new version.

5 |
6 | {{ form.csrf_token }} 7 |
8 | {{ form.package.label(class_='form-label') }} {{ form.package(class_='form-control') }} 9 | {% for error in form.package.errors %}
{{ error }}
{% endfor %} 10 |
11 | 12 |
13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /app/well-known/apple-app-site-association: -------------------------------------------------------------------------------- 1 | { 2 | "applinks": { 3 | "details": [ 4 | { 5 | "appIDs": ["TM4TW7N79V.info.zverev.everydoor"], 6 | "components": [ 7 | { 8 | "/": "/i/*", 9 | "comment": "Install a plugin" 10 | }, 11 | { 12 | "/": "/nav/*", 13 | "comment": "Navigate to a location or an OSM object" 14 | } 15 | ] 16 | } 17 | ] 18 | }, 19 | "webcredentials": { 20 | "apps": [ "TM4TW7N79V.info.zverev.everydoor" ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/templates/delete.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block title %}Deleting {{ plugin.title }} — Every Door Plugins{% endblock %} 3 | {% block content %} 4 |

Really delete "{{ plugin.title }}"{% if version %} version {{ version.version_str }}{% endif %}?

5 |

This is irreversible. It's not too late to go back!

6 |
7 | 8 | No thanks 9 | 10 |
11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "everydoor-plugin-repo" 3 | version = "0.1.18" 4 | description = "Every Door Plugin Repository" 5 | readme = "README.md" 6 | requires-python = ">=3.10" 7 | dependencies = [ 8 | "authlib>=1.5.2", 9 | "flask>=3.1.1", 10 | "flask-migrate>=4.1.0", 11 | "flask-sqlalchemy>=3.1.1", 12 | "flask-wtf>=1.2.2", 13 | "gunicorn>=23.0.0", 14 | "markdown2>=2.5.3", 15 | "pyyaml>=6.0.2", 16 | "qrcode>=8.2", 17 | "requests>=2.32.3", 18 | ] 19 | 20 | [dependency-groups] 21 | dev = [ 22 | "mypy>=1.15.0", 23 | ] 24 | 25 | [build-system] 26 | requires = ["setuptools >= 77.0.3"] 27 | build-backend = "setuptools.build_meta" 28 | 29 | [tool.setuptools] 30 | packages = ["app", "migrations"] 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright 2025 Ilya Zverev 4 | 5 | Permission to use, copy, modify, and/or distribute this software 6 | for any purpose with or without fee is hereby granted, provided 7 | that the above copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, 12 | DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER 13 | RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, 14 | NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH 15 | THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /app/templates/edit_version.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block title %}{{ plugin.title }} {{version.version }} — Every Door Plugins{% endblock %} 3 | {% block content %} 4 |

← back to the plugin

5 |

{{ plugin.title }} version {{ version.version_str }}

6 |
7 | {{ form.csrf_token }} 8 |
9 | {{ form.changelog.label(class_='form-label') }} {{ form.changelog(rows=7, class_='form-control') }} 10 | {% for error in form.changelog.errors %}
{{ error }}
{% endfor %} 11 |
12 | 13 | Cancel 14 | Delete 15 |
16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /app/well-known/assetlinks.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "relation": ["delegate_permission/common.handle_all_urls"], 3 | "target": { 4 | "namespace": "android_app", 5 | "package_name": "info.zverev.ilya.every_door", 6 | "sha256_cert_fingerprints": [ 7 | "9F:FC:A7:0F:77:A8:5E:F6:82:90:27:66:02:BB:5B:4C:92:A6:54:67:C5:4F:F6:FB:89:C4:B2:02:39:50:62:03", 8 | "16:C8:1C:74:3F:75:20:F5:85:9D:EB:25:C0:DA:DB:63:1F:A3:97:3C:D2:B2:88:A9:54:6D:C3:4D:8B:71:C6:97", 9 | "29:90:21:68:8B:04:21:91:A0:90:65:B9:A7:46:3B:72:62:85:40:72:8C:31:C6:C5:2D:8D:BE:B8:21:53:31:FB", 10 | "61:5D:43:AC:81:08:3C:D9:1D:98:16:50:74:76:36:F6:7C:7C:EC:02:33:5A:F2:51:A4:DD:B2:53:53:35:4C:02" 11 | ] 12 | } 13 | }, 14 | { 15 | "relation": ["delegate_permission/common.handle_all_urls"], 16 | "target": { 17 | "namespace": "android_app", 18 | "package_name": "info.zverev.ilya.every_door.beta", 19 | "sha256_cert_fingerprints": [ 20 | "29:90:21:68:8B:04:21:91:A0:90:65:B9:A7:46:3B:72:62:85:40:72:8C:31:C6:C5:2D:8D:BE:B8:21:53:31:FB" 21 | ] 22 | } 23 | }] 24 | -------------------------------------------------------------------------------- /migrations/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # template used to generate migration files 5 | # file_template = %%(rev)s_%%(slug)s 6 | 7 | # set to 'true' to run the environment during 8 | # the 'revision' command, regardless of autogenerate 9 | # revision_environment = false 10 | 11 | 12 | # Logging configuration 13 | [loggers] 14 | keys = root,sqlalchemy,alembic,flask_migrate 15 | 16 | [handlers] 17 | keys = console 18 | 19 | [formatters] 20 | keys = generic 21 | 22 | [logger_root] 23 | level = WARN 24 | handlers = console 25 | qualname = 26 | 27 | [logger_sqlalchemy] 28 | level = WARN 29 | handlers = 30 | qualname = sqlalchemy.engine 31 | 32 | [logger_alembic] 33 | level = INFO 34 | handlers = 35 | qualname = alembic 36 | 37 | [logger_flask_migrate] 38 | level = INFO 39 | handlers = 40 | qualname = flask_migrate 41 | 42 | [handler_console] 43 | class = StreamHandler 44 | args = (sys.stderr,) 45 | level = NOTSET 46 | formatter = generic 47 | 48 | [formatter_generic] 49 | format = %(levelname)-5.5s [%(name)s] %(message)s 50 | datefmt = %H:%M:%S 51 | -------------------------------------------------------------------------------- /app/templates/plugins.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 |

{% if mine %}My {% endif %}Plugins

4 | 5 | 6 | 7 | 8 | 9 | 10 | {% if not mine %}{% endif %} 11 | 12 | 13 | 14 | 15 | 16 | 17 | {% for p in plugins %} 18 | 19 | 20 | 21 | 22 | {% if not mine %}{% endif %} 23 | 24 | 25 | 26 | 27 | {% endfor %} 28 | 29 |
TitleDownloadsAuthorUpdatedVersionLocal
{% if p.icon_file %}{% endif %}{{ p.title }}{{ p.downloads or '-' }}{{ p.created_by.name }}{{ p.last_eversion.created_on | ago }}{{ p.last_eversion.version_str }}{{ p.country or '-' }}
30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /app/templates/install.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 |

Installing "{{ name }}"

4 | 5 |

You have come here from a direct link to install an Every Door plugin. 6 | Your app must be opening right now, so ideally, you do not have to do 7 | anything, just wait a sec.

8 | 9 |

If nothing happens, try downloading the package 10 | manually and opening it in Every Door.

11 | 12 |

If you are reading this on a device without Every Door installed, open 13 | the editor on your phone, tap the three-striped Settings button, then 14 | "Plugins", and the QR code button in the top right corner. Point the camera 15 | at this code:

16 | 17 |

{{ qrcode | safe }}

18 | 19 | {% if plugin and plugin.last_version %} 20 |

Based on the supplied plugin identifier, this is the plugin we have 21 | in the database (you can find it on the "Add a plugin" list in the app):

22 |
23 |
24 |
{{ plugin.title }}
25 |
Version {{ plugin.last_version.version_str }} uploaded on {{ plugin.last_version.created_on }}
26 |

{{ plugin.description | markdown }}

27 | View plugin 28 |
29 |
30 | {% endif %} 31 | 32 |

If nothing works, please contact the person who gave you this link, or 33 | open an issue 34 | in the Every Door project, pasting the link. We are sorry.

35 | {% endblock %} 36 | -------------------------------------------------------------------------------- /app/templates/edit_plugin.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block title %}{{ plugin.title }} — Every Door Plugins{% endblock %} 3 | {% block content %} 4 |

← back to the plugin

5 |

{{ plugin.title }}

6 |
7 | {{ form.csrf_token }} 8 |
9 | {{ form.title.label(class_='form-label') }} {{ form.title | fc }} 10 | {% for error in form.title.errors %}
{{ error }}
{% endfor %} 11 |
12 |
13 | {{ form.description.label(class_='form-label') }} {{ form.description(rows=7, class_='form-control') }} 14 | {% for error in form.description.errors %}
{{ error }}
{% endfor %} 15 |
16 |
17 | {{ form.homepage.label(class_='form-label') }} {{ form.homepage | fc }} 18 | {% for error in form.homepage.errors %}
{{ error }}
{% endfor %} 19 |
20 |
21 | {{ form.country.label(class_='form-label') }} {{ form.country | fc }} 22 | {% for error in form.country.errors %}
{{ error }}
{% endfor %} 23 |
See the country coder website for a list
24 |
25 |
26 | {{ form.hidden(class_='form-check-input') }} {{ form.hidden.label(class_='form-check-label') }} 27 |
28 | 29 | Cancel 30 | Delete 31 |
32 | {% endblock %} 33 | -------------------------------------------------------------------------------- /app/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% block title %}Every Door Plugins{% endblock %} 5 | 6 | 7 | 8 | {% block header %}{% endblock %} 9 | 10 | 11 | 12 | 36 |
37 | {% for message in get_flashed_messages() %} 38 | 39 | {% endfor %} 40 | {% block content %}{% endblock %} 41 |
42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import os.path 3 | import re 4 | from datetime import datetime 5 | from flask import Flask, send_from_directory 6 | from werkzeug.middleware.proxy_fix import ProxyFix 7 | from flask_migrate import Migrate 8 | from markupsafe import escape, Markup 9 | 10 | 11 | def markdown_format(s: str) -> str: 12 | return Markup('

'.join(re.split(r'(?:\r\n|\r|\n){2}', escape(s)))) 13 | 14 | 15 | def wtforms_error_class(field): 16 | return field(class_='form-control' if not field.errors 17 | else 'form-control is-invalid') 18 | 19 | 20 | def date_ago(dt: datetime) -> str: 21 | days = datetime.now() - dt 22 | if days.days < 30: 23 | return f'{days.days} days ago' 24 | if days.days < 360: 25 | return f'{days.days // 30} months ago' 26 | return dt.strftime('%b %Y') 27 | 28 | 29 | def serve_well_known(name: str): 30 | return send_from_directory(os.path.join( 31 | os.path.dirname(__file__), 'well-known'), name) 32 | 33 | 34 | def create_app(): 35 | app = Flask(__name__, instance_relative_config=True) 36 | app.config.from_mapping( 37 | SECRET_KEY='sdfsdfsdf', 38 | SQLALCHEMY_DATABASE_URI='sqlite:///edpr.sqlite', 39 | OAUTH_ID='', 40 | OAUTH_SECRET='', 41 | PROXY=False, 42 | MAX_UPLOAD_SIZE_MB=25, 43 | MAX_ICON_SIZE_KB=100, 44 | ) 45 | app.config.from_pyfile('config.py', silent=True) 46 | app.config['MAX_CONTENT_LENGTH'] = ( 47 | app.config['MAX_UPLOAD_SIZE_MB'] * 1024 * 1024) 48 | os.makedirs(app.instance_path, exist_ok=True) 49 | 50 | from .database import db 51 | db.init_app(app) 52 | Migrate(app, db) 53 | app.add_template_filter(markdown_format, 'markdown') 54 | app.add_template_filter(wtforms_error_class, 'fc') 55 | app.add_template_filter(date_ago, 'ago') 56 | app.add_url_rule('/.well-known/', view_func=serve_well_known) 57 | 58 | from . import plugins 59 | app.register_blueprint(plugins.bp) 60 | from . import api 61 | app.register_blueprint(api.bp, url_prefix='/api') 62 | from . import auth 63 | auth.init_app(app) 64 | app.register_blueprint(auth.bp) 65 | 66 | if app.config['PROXY']: 67 | app.wsgi_app = ProxyFix( 68 | app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1) 69 | 70 | return app 71 | -------------------------------------------------------------------------------- /app/api.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, url_for, request 2 | from typing import Any 3 | from sqlalchemy import or_ 4 | from .database import db, Plugin 5 | 6 | 7 | bp = Blueprint('api', __name__) 8 | 9 | 10 | def plugin_to_dict(plugin: Plugin, experimental=False, 11 | version: int | None = None): 12 | result: dict[str, Any] = { 13 | 'id': plugin.id, 14 | 'name': plugin.title, 15 | 'description': plugin.description, 16 | 'author': plugin.created_by.name, 17 | 'url': url_for('plugins.plugin', name=plugin.id, 18 | _external=True), 19 | 'download': url_for('plugins.download', name=plugin.id, 20 | _external=True), 21 | } 22 | if plugin.homepage: 23 | result['homepage'] = plugin.homepage 24 | if plugin.country: 25 | result['country'] = plugin.country 26 | if plugin.hidden: 27 | result['hidden'] = True 28 | 29 | if plugin.icon: 30 | result['icon'] = url_for( 31 | 'plugins.icon', name=plugin.id, ext=plugin.icon, _external=True) 32 | 33 | result['downloads'] = plugin.downloads 34 | 35 | vobj = plugin.last_eversion if experimental else plugin.last_version 36 | if vobj: 37 | result['version'] = vobj.version_str 38 | result['updated'] = vobj.created_on.isoformat(' ') 39 | result['experimental'] = vobj.experimental 40 | result['download'] = url_for( 41 | 'plugins.download', name=plugin.id, version=vobj.version_str, 42 | _external=True) 43 | else: 44 | return None 45 | return result 46 | 47 | 48 | @bp.route('/list', endpoint='list') 49 | def list_plugins(): 50 | countries = [c for c in request.args.get('countries', '').split(',') if c] 51 | q = db.select(Plugin).where(~Plugin.hidden) 52 | if countries: 53 | q = q.where(or_(Plugin.country.is_(None), 54 | Plugin.country.in_(countries))) 55 | else: 56 | q = q.where(Plugin.country.is_(None)) 57 | 58 | plugins = db.session.scalars(q.order_by(Plugin.title)) 59 | exp = request.args.get('exp') == '1' 60 | result = (plugin_to_dict(p, exp) for p in plugins) 61 | return [r for r in result if r] 62 | 63 | 64 | @bp.route('/plugin/') 65 | def plugin(name: str): 66 | plugin: Plugin = db.get_or_404(Plugin, name) 67 | return plugin_to_dict(plugin) 68 | -------------------------------------------------------------------------------- /app/auth.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | from authlib.integrations.flask_client import OAuth 3 | from sqlalchemy.exc import NoResultFound 4 | from flask import ( 5 | Blueprint, request, url_for, redirect, session, g, flash, 6 | ) 7 | from .database import db, User 8 | 9 | 10 | oauth = OAuth() 11 | bp = Blueprint('auth', __name__) 12 | 13 | 14 | def init_app(app): 15 | oauth.register( 16 | 'openstreetmap', 17 | api_base_url='https://api.openstreetmap.org/api/0.6/', 18 | access_token_url='https://www.openstreetmap.org/oauth2/token', 19 | authorize_url='https://www.openstreetmap.org/oauth2/authorize', 20 | client_id=app.config['OAUTH_ID'], 21 | client_secret=app.config['OAUTH_SECRET'], 22 | client_kwargs={'scope': 'read_prefs'}, 23 | ) 24 | oauth.init_app(app) 25 | 26 | 27 | def login_required(f): 28 | @wraps(f) 29 | def decorated(*args, **kwargs): 30 | if 'user_id' not in session: 31 | return redirect(url_for('auth.login', next=request.url)) 32 | return f(*args, **kwargs) 33 | return decorated 34 | 35 | 36 | def get_user(f): 37 | @wraps(f) 38 | def decorated(*args, **kwargs): 39 | if 'user' not in g: 40 | g.user = None 41 | if 'user_id' in session: 42 | try: 43 | g.user = db.session.get_one(User, session['user_id']) 44 | except NoResultFound: 45 | flash('Error: no user in the database. Please re-login') 46 | del session['user_id'] 47 | return redirect(url_for('plugins.list')) 48 | return f(*args, **kwargs) 49 | return decorated 50 | 51 | 52 | @bp.route('/login') 53 | def login(): 54 | url = url_for('auth.authorize', _external=True) 55 | session['next'] = request.args.get('next', '') 56 | return oauth.openstreetmap.authorize_redirect(url) 57 | 58 | 59 | @bp.route('/auth') 60 | def authorize(): 61 | oauth.openstreetmap.authorize_access_token() 62 | resp = oauth.openstreetmap.get('user/details.json') 63 | resp.raise_for_status() 64 | profile = resp.json() 65 | user_id = profile['user']['id'] 66 | 67 | try: 68 | g.user = db.session.get_one(User, user_id) 69 | except NoResultFound: 70 | # Create a new user 71 | user = User( 72 | osm_id=user_id, 73 | name=profile['user']['display_name'], 74 | ) 75 | user.update_token() 76 | db.session.add(user) 77 | db.session.commit() 78 | g.user = user 79 | 80 | session['user_id'] = user_id 81 | nxt = session.pop('next') 82 | return redirect(nxt or url_for('plugins.list')) 83 | 84 | 85 | @bp.route('/logout') 86 | def logout(): 87 | if 'user_id' in session: 88 | del session['user_id'] 89 | return redirect(url_for('plugins.list')) 90 | -------------------------------------------------------------------------------- /migrations/versions/02defd0699aa_initial.py: -------------------------------------------------------------------------------- 1 | """initial 2 | 3 | Revision ID: 02defd0699aa 4 | Revises: 5 | Create Date: 2025-05-27 22:15:03.697240 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '02defd0699aa' 14 | down_revision = None 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('user', 22 | sa.Column('osm_id', sa.Integer(), nullable=False), 23 | sa.Column('name', sa.String(length=255), nullable=False), 24 | sa.Column('is_admin', sa.Boolean(), server_default=sa.text('false'), nullable=False), 25 | sa.Column('token', sa.String(length=64), nullable=False), 26 | sa.PrimaryKeyConstraint('osm_id') 27 | ) 28 | with op.batch_alter_table('user', schema=None) as batch_op: 29 | batch_op.create_index(batch_op.f('ix_user_token'), ['token'], unique=True) 30 | 31 | op.create_table('plugin', 32 | sa.Column('id', sa.String(), nullable=False), 33 | sa.Column('title', sa.String(length=250), nullable=False), 34 | sa.Column('description', sa.String(), nullable=False), 35 | sa.Column('created_by_id', sa.Integer(), nullable=False), 36 | sa.Column('homepage', sa.String(), nullable=True), 37 | sa.Column('country', sa.String(length=32), nullable=True), 38 | sa.Column('hidden', sa.Boolean(), server_default=sa.text('false'), nullable=False), 39 | sa.Column('icon', sa.String(), nullable=True), 40 | sa.ForeignKeyConstraint(['created_by_id'], ['user.osm_id'], ), 41 | sa.PrimaryKeyConstraint('id'), 42 | sa.UniqueConstraint('title') 43 | ) 44 | op.create_table('plugin_version', 45 | sa.Column('pk', sa.Integer(), nullable=False), 46 | sa.Column('plugin_id', sa.String(), nullable=False), 47 | sa.Column('created_on', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), 48 | sa.Column('version', sa.Integer(), nullable=False), 49 | sa.Column('downloads', sa.Integer(), server_default='0', nullable=False), 50 | sa.Column('created_by_id', sa.Integer(), nullable=False), 51 | sa.Column('changelog', sa.String(), nullable=True), 52 | sa.Column('experimental', sa.Boolean(), server_default=sa.text('true'), nullable=False), 53 | sa.ForeignKeyConstraint(['created_by_id'], ['user.osm_id'], ), 54 | sa.ForeignKeyConstraint(['plugin_id'], ['plugin.id'], ), 55 | sa.PrimaryKeyConstraint('pk') 56 | ) 57 | # ### end Alembic commands ### 58 | 59 | 60 | def downgrade(): 61 | # ### commands auto generated by Alembic - please adjust! ### 62 | op.drop_table('plugin_version') 63 | op.drop_table('plugin') 64 | with op.batch_alter_table('user', schema=None) as batch_op: 65 | batch_op.drop_index(batch_op.f('ix_user_token')) 66 | 67 | op.drop_table('user') 68 | # ### end Alembic commands ### 69 | -------------------------------------------------------------------------------- /app/templates/plugin.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block title %}{{ plugin.title }} — Every Door Plugins{% endblock %} 3 | {% block content %} 4 |

← back to the list

5 | 6 |

{% if plugin.icon_file %}{% endif %}{{ plugin.title }}{% if g.user == plugin.created_by %} Edit plugin{% endif %}

7 |

Published by {{ plugin.created_by.name }}

8 |
{{ plugin.description | markdown }}
9 | {% if plugin.homepage %} 10 |

Open plugin home page

11 | {% endif %} 12 | 13 | {% if plugin.versions %} 14 |

Download

15 |

To install a plugin, there are four options:

16 | 22 |
{{ qrcode | safe }}
23 | {% endif %} 24 | 25 |

Versions

26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | {% if g.user == plugin.created_by %}{% endif %} 34 | 35 | 36 | 37 | {% for v in plugin.versions %} 38 | 39 | 40 | 41 | 42 | 43 | {% if g.user == plugin.created_by %}{% endif %} 44 | 45 | 46 | 49 | 50 | {% endfor %} 51 | 52 |
VersionOk?DownloadsUpdatedAction
{{ v.version_str }}{{ '🚧' if v.experimental else '✅' }}{{ v.downloads or '-' }}{{ v.created_on | ago }}edit
47 |
{{ v.changelog or 'no changelog' | markdown }}
48 |
53 | {% endblock %} 54 | -------------------------------------------------------------------------------- /migrations/env.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from logging.config import fileConfig 3 | 4 | from flask import current_app 5 | 6 | from alembic import context 7 | 8 | # this is the Alembic Config object, which provides 9 | # access to the values within the .ini file in use. 10 | config = context.config 11 | 12 | # Interpret the config file for Python logging. 13 | # This line sets up loggers basically. 14 | fileConfig(config.config_file_name) 15 | logger = logging.getLogger('alembic.env') 16 | 17 | 18 | def get_engine(): 19 | try: 20 | # this works with Flask-SQLAlchemy<3 and Alchemical 21 | return current_app.extensions['migrate'].db.get_engine() 22 | except (TypeError, AttributeError): 23 | # this works with Flask-SQLAlchemy>=3 24 | return current_app.extensions['migrate'].db.engine 25 | 26 | 27 | def get_engine_url(): 28 | try: 29 | return get_engine().url.render_as_string(hide_password=False).replace( 30 | '%', '%%') 31 | except AttributeError: 32 | return str(get_engine().url).replace('%', '%%') 33 | 34 | 35 | # add your model's MetaData object here 36 | # for 'autogenerate' support 37 | # from myapp import mymodel 38 | # target_metadata = mymodel.Base.metadata 39 | config.set_main_option('sqlalchemy.url', get_engine_url()) 40 | target_db = current_app.extensions['migrate'].db 41 | 42 | # other values from the config, defined by the needs of env.py, 43 | # can be acquired: 44 | # my_important_option = config.get_main_option("my_important_option") 45 | # ... etc. 46 | 47 | 48 | def get_metadata(): 49 | if hasattr(target_db, 'metadatas'): 50 | return target_db.metadatas[None] 51 | return target_db.metadata 52 | 53 | 54 | def run_migrations_offline(): 55 | """Run migrations in 'offline' mode. 56 | 57 | This configures the context with just a URL 58 | and not an Engine, though an Engine is acceptable 59 | here as well. By skipping the Engine creation 60 | we don't even need a DBAPI to be available. 61 | 62 | Calls to context.execute() here emit the given string to the 63 | script output. 64 | 65 | """ 66 | url = config.get_main_option("sqlalchemy.url") 67 | context.configure( 68 | url=url, target_metadata=get_metadata(), literal_binds=True 69 | ) 70 | 71 | with context.begin_transaction(): 72 | context.run_migrations() 73 | 74 | 75 | def run_migrations_online(): 76 | """Run migrations in 'online' mode. 77 | 78 | In this scenario we need to create an Engine 79 | and associate a connection with the context. 80 | 81 | """ 82 | 83 | # this callback is used to prevent an auto-migration from being generated 84 | # when there are no changes to the schema 85 | # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html 86 | def process_revision_directives(context, revision, directives): 87 | if getattr(config.cmd_opts, 'autogenerate', False): 88 | script = directives[0] 89 | if script.upgrade_ops.is_empty(): 90 | directives[:] = [] 91 | logger.info('No changes in schema detected.') 92 | 93 | conf_args = current_app.extensions['migrate'].configure_args 94 | if conf_args.get("process_revision_directives") is None: 95 | conf_args["process_revision_directives"] = process_revision_directives 96 | 97 | connectable = get_engine() 98 | 99 | with connectable.connect() as connection: 100 | context.configure( 101 | connection=connection, 102 | target_metadata=get_metadata(), 103 | **conf_args 104 | ) 105 | 106 | with context.begin_transaction(): 107 | context.run_migrations() 108 | 109 | 110 | if context.is_offline_mode(): 111 | run_migrations_offline() 112 | else: 113 | run_migrations_online() 114 | -------------------------------------------------------------------------------- /app/database.py: -------------------------------------------------------------------------------- 1 | import string 2 | import random 3 | import os 4 | from flask import current_app 5 | from datetime import datetime 6 | from flask_sqlalchemy import SQLAlchemy 7 | from sqlalchemy import String, ForeignKey, func, sql 8 | from sqlalchemy.orm import ( 9 | DeclarativeBase, Mapped, mapped_column, relationship, aliased, 10 | ) 11 | from sqlalchemy.ext.hybrid import hybrid_property 12 | 13 | 14 | class Base(DeclarativeBase): 15 | pass 16 | 17 | 18 | db = SQLAlchemy(model_class=Base) 19 | 20 | 21 | class User(db.Model): 22 | osm_id: Mapped[int] = mapped_column(primary_key=True) 23 | name: Mapped[str] = mapped_column(String(255)) 24 | is_admin: Mapped[bool] = mapped_column(server_default=sql.false()) 25 | token: Mapped[str] = mapped_column(String(64), unique=True, index=True) 26 | 27 | def update_token(self): 28 | chars = string.ascii_letters + string.digits + '_' 29 | self.token = ''.join(random.choices(chars, k=32)) 30 | 31 | def __repr__(self) -> str: 32 | return f'User(osm_id={self.osm_id!r}, name={self.name!r})' 33 | 34 | 35 | class Plugin(db.Model): 36 | id: Mapped[str] = mapped_column(primary_key=True) 37 | title: Mapped[str] = mapped_column(String(250), unique=True) 38 | description: Mapped[str] 39 | created_by_id: Mapped[int] = mapped_column(ForeignKey('user.osm_id')) 40 | created_by: Mapped[User] = relationship() 41 | homepage: Mapped[str | None] 42 | country: Mapped[str | None] = mapped_column(String(32)) 43 | hidden: Mapped[bool] = mapped_column(server_default=sql.false()) 44 | icon: Mapped[str | None] 45 | 46 | versions: Mapped[list["PluginVersion"]] = relationship( 47 | back_populates='plugin', order_by='desc(PluginVersion.created_on)', 48 | cascade='all, delete', 49 | ) 50 | 51 | @property 52 | def icon_file(self) -> str | None: 53 | if not self.icon: 54 | return None 55 | return os.path.join( 56 | current_app.instance_path, 'plugins', self.id, 57 | f'icon.{self.icon}') 58 | 59 | @hybrid_property 60 | def downloads(self): 61 | return sum(v.downloads for v in self.versions) 62 | 63 | @downloads.expression 64 | def _downloads_expr(cls): 65 | return ( 66 | db.select(func.sum(PluginVersion.downloads)) 67 | .where(PluginVersion.plugin_id == cls.id) 68 | .label('downloads') 69 | ) 70 | 71 | 72 | class PluginVersion(db.Model): 73 | pk: Mapped[int] = mapped_column(primary_key=True) 74 | plugin_id: Mapped[str] = mapped_column(ForeignKey('plugin.id')) 75 | plugin: Mapped[Plugin] = relationship(back_populates='versions') 76 | created_on: Mapped[datetime] = mapped_column( 77 | server_default=func.CURRENT_TIMESTAMP()) 78 | version: Mapped[int] 79 | downloads: Mapped[int] = mapped_column(server_default='0') 80 | created_by_id: Mapped[int] = mapped_column(ForeignKey('user.osm_id')) 81 | created_by: Mapped[User] = relationship() 82 | changelog: Mapped[str | None] 83 | experimental: Mapped[bool] = mapped_column(server_default=sql.true()) 84 | 85 | @property 86 | def filename(self) -> str: 87 | return os.path.join( 88 | current_app.instance_path, 'plugins', self.plugin_id, 89 | f'{self.version}.edp') 90 | 91 | @property 92 | def version_str(self) -> str: 93 | if self.version < 1000: 94 | return str(self.version) 95 | major = self.version // 1000 - 1 96 | minor = self.version % 1000 97 | return f'{major}.{minor}' 98 | 99 | @staticmethod 100 | def parse_version(value: str | int) -> int: 101 | if isinstance(value, int): 102 | if value > 1000: 103 | raise ValueError('Version over 1000 is ambiguous') 104 | return value 105 | 106 | if isinstance(value, float): 107 | value = str(value) 108 | if '.' not in value: 109 | value = f'{value}.0' 110 | 111 | if '.' in value: 112 | parts = [int(p.strip()) for p in value.split('.')] 113 | if parts[1] > 1000: 114 | raise ValueError('Minor version over 1000') 115 | return (parts[0] + 1) * 1000 + parts[1] 116 | 117 | result = int(value) 118 | if result > 1000: 119 | raise ValueError('Version over 1000 is ambiguous') 120 | return result 121 | 122 | 123 | latest_version_subquery = ( 124 | db.select(PluginVersion).distinct(PluginVersion.plugin_id) 125 | .where(~PluginVersion.experimental) 126 | .order_by(PluginVersion.plugin_id, PluginVersion.created_on.desc()) 127 | .alias() 128 | ) 129 | latest_version_alias = aliased(PluginVersion, latest_version_subquery) 130 | Plugin.last_version = relationship( 131 | latest_version_alias, 132 | uselist=False, 133 | lazy='selectin', 134 | ) 135 | 136 | latest_eversion_subquery = ( 137 | db.select(PluginVersion).distinct(PluginVersion.plugin_id) 138 | .order_by(PluginVersion.plugin_id, PluginVersion.created_on.desc()) 139 | .alias() 140 | ) 141 | latest_eversion_alias = aliased(PluginVersion, latest_eversion_subquery) 142 | Plugin.last_eversion = relationship( 143 | latest_eversion_alias, 144 | uselist=False, 145 | lazy='selectin', 146 | ) 147 | -------------------------------------------------------------------------------- /app/countries.json: -------------------------------------------------------------------------------- 1 | ["001", "002", "003", "004", "005", "008", "009", "010", "011", "012", "013", "014", "015", "016", "017", "018", "019", "020", "021", "024", "028", "029", "030", "031", "032", "034", "035", "036", "039", "040", "044", "048", "050", "051", "052", "053", "054", "056", "057", "060", "061", "064", "068", "070", "072", "074", "076", "084", "086", "090", "092", "096", "100", "104", "108", "112", "116", "120", "124", "132", "136", "140", "142", "143", "144", "145", "148", "150", "151", "152", "154", "155", "156", "158", "162", "166", "170", "174", "175", "178", "180", "184", "188", "191", "192", "196", "202", "203", "204", "208", "212", "214", "218", "222", "226", "231", "232", "233", "234", "238", "239", "242", "246", "248", "249", "250", "254", "258", "260", "262", "266", "268", "270", "275", "276", "288", "292", "296", "300", "304", "308", "312", "316", "320", "324", "328", "332", "334", "336", "340", "344", "348", "352", "356", "360", "364", "368", "372", "376", "380", "384", "388", "392", "396", "398", "400", "404", "408", "410", "414", "417", "418", "419", "422", "426", "428", "430", "434", "438", "440", "442", "446", "450", "454", "458", "462", "466", "470", "474", "478", "480", "484", "488", "492", "496", "498", "499", "500", "504", "508", "512", "516", "520", "524", "528", "531", "533", "534", "535", "540", "548", "554", "558", "562", "566", "570", "574", "578", "580", "581", "583", "584", "585", "586", "591", "598", "600", "604", "608", "612", "616", "620", "624", "626", "630", "634", "638", "642", "643", "646", "652", "654", "659", "660", "662", "663", "666", "670", "674", "678", "680", "682", "686", "688", "690", "694", "702", "703", "704", "705", "706", "710", "716", "724", "728", "729", "732", "740", "744", "748", "752", "756", "760", "762", "764", "768", "772", "776", "780", "784", "788", "792", "795", "796", "798", "800", "804", "807", "818", "826", "830", "831", "832", "833", "834", "840", "850", "854", "858", "860", "862", "872", "876", "882", "887", "894", "ABW", "AC", "AD", "AE", "AF", "AFG", "AG", "AGO", "AI", "AIA", "AL", "ALA", "ALB", "AM", "AND", "AO", "AQ", "AR", "ARE", "ARG", "ARM", "AS", "ASC", "ASM", "AT", "ATA", "ATF", "ATG", "AU", "AU-TAS", "AUS", "AUT", "AW", "AX", "AZ", "AZE", "Australasia", "BA", "BB", "BD", "BDI", "BE", "BEL", "BEN", "BES", "BF", "BFA", "BG", "BGD", "BGR", "BH", "BHR", "BHS", "BI", "BIH", "BJ", "BL", "BLM", "BLR", "BLZ", "BM", "BMU", "BN", "BO", "BOL", "BOTS", "BQ", "BQ-BO", "BQ-SA", "BQ-SE", "BR", "BRA", "BRB", "BRN", "BS", "BT", "BTN", "BU", "BV", "BVT", "BW", "BWA", "BY", "BZ", "Burma", "CA", "CAF", "CAN", "CC", "CCK", "CD", "CF", "CG", "CH", "CHE", "CHL", "CHN", "CI", "CIV", "CK", "CL", "CM", "CMR", "CN", "CO", "COD", "COG", "COK", "COL", "COM", "CONUS", "CP", "CPT", "CPV", "CQ", "CR", "CRI", "CRQ", "CU", "CUB", "CUW", "CV", "CW", "CX", "CXR", "CY", "CYM", "CYP", "CZ", "CZE", "DE", "DEU", "DG", "DGA", "DJ", "DJI", "DK", "DM", "DMA", "DNK", "DO", "DOM", "DY", "DZ", "DZA", "EA", "EC", "EC-W", "ECU", "EE", "EG", "EGY", "EH", "EL", "ER", "ERI", "ES", "ES-CE", "ES-IB", "ES-ML", "ESH", "ESP", "EST", "ET", "ETH", "EU", "EUE", "EW", "Earth", "FI", "FIN", "FJ", "FJI", "FK", "FL", "FLK", "FM", "FO", "FR", "FRA", "FRO", "FSM", "FX", "FXX", "GA", "GAB", "GB", "GB-ENG", "GB-NIR", "GB-SCT", "GB-UKM", "GB-WLS", "GBR", "GD", "GE", "GEO", "GF", "GG", "GGY", "GH", "GHA", "GI", "GIB", "GIN", "GL", "GLP", "GM", "GMB", "GN", "GNB", "GNQ", "GP", "GQ", "GR", "GRC", "GRD", "GRL", "GS", "GT", "GTM", "GU", "GUF", "GUM", "GUY", "GW", "GY", "HK", "HKG", "HM", "HMD", "HN", "HND", "HR", "HRV", "HT", "HTI", "HU", "HUN", "Holy See", "IC", "ID", "ID-JW", "ID-KA", "ID-ML", "ID-NU", "ID-PP", "ID-SL", "ID-SM", "IDN", "IE", "IL", "IM", "IMN", "IN", "IN-AN", "IN-LD", "IND", "IO", "IOT", "IQ", "IR", "IRL", "IRN", "IRQ", "IS", "ISL", "ISR", "IT", "ITA", "JA", "JAM", "JE", "JEY", "JM", "JO", "JOR", "JP", "JPN", "JT", "JTN", "JTUM", "KAZ", "KE", "KEN", "KG", "KGZ", "KH", "KHM", "KI", "KIR", "KM", "KN", "KNA", "KOR", "KP", "KR", "KV", "KW", "KWT", "KY", "KZ", "LA", "LAO", "LB", "LBN", "LBR", "LBY", "LC", "LCA", "LI", "LIE", "LK", "LKA", "LR", "LS", "LSO", "LT", "LTU", "LU", "LUX", "LV", "LVA", "LY", "MA", "MAC", "MAF", "MAR", "MC", "MCO", "MD", "MDA", "MDG", "MDV", "ME", "MEX", "MF", "MG", "MH", "MHL", "MI", "MID", "MIUM", "MK", "MKD", "ML", "MLI", "MLT", "MM", "MMR", "MN", "MNE", "MNG", "MNP", "MO", "MOZ", "MP", "MQ", "MR", "MRT", "MS", "MSR", "MT", "MTQ", "MU", "MUS", "MV", "MW", "MWI", "MX", "MY", "MYS", "MYT", "MZ", "Macao", "NA", "NAM", "NC", "NCL", "NE", "NER", "NF", "NFK", "NG", "NGA", "NI", "NIC", "NIU", "NL", "NL-AW", "NL-BQ1", "NL-BQ2", "NL-BQ3", "NL-CW", "NL-SX", "NLD", "NO", "NO-21", "NO-22", "NOR", "NP", "NPL", "NR", "NRU", "NU", "NZ", "NZ-CIT", "NZL", "OCT", "OM", "OMN", "OMR", "PA", "PAK", "PAN", "PCN", "PE", "PER", "PF", "PG", "PH", "PHL", "PI", "PK", "PL", "PLW", "PM", "PN", "PNG", "POL", "PR", "PRI", "PRK", "PRT", "PRY", "PS", "PSE", "PT", "PT-20", "PT-30", "PW", "PY", "PYF", "Planet", "Q1000", "Q1005", "Q1006", "Q1007", "Q1008", "Q1009", "Q1011", "Q1013", "Q1014", "Q1016", "Q1019", "Q1020", "Q1025", "Q1027", "Q1028", "Q1029", "Q1030", "Q1032", "Q1033", "Q1036", "Q1037", "Q1039", "Q1041", "Q1042", "Q1044", "Q1045", "Q1049", "Q1050", "Q105472", "Q1065", "Q1083368", "Q114", "Q114935", "Q115", "Q115459", "Q117", "Q11703", "Q11708", "Q1183", "Q1184963", "Q118863", "Q120755", "Q123076", "Q1246", "Q126125", "Q12837", "Q129003", "Q1298289", "Q130574", "Q130895", "Q131008", "Q131198", "Q131305", "Q132959", "Q13353", "Q133888", "Q1352230", "Q14056", "Q1410", "Q142", "Q145", "Q1451600", "Q14773", "Q148", "Q15", "Q153732", "Q1544253", "Q155", "Q1585511", "Q159", "Q16", "Q161258", "Q16390686", "Q16635", "Q16641", "Q16644", "Q16645", "Q1681727", "Q17", "Q17012", "Q17054", "Q17063", "Q17070", "Q172216", "Q179313", "Q18", "Q18221", "Q183", "Q184", "Q184851", "Q185086", "Q189", "Q1901211", "Q190571", "Q191", "Q191011", "Q191146", "Q19188", "Q192184", "Q1973345", "Q2", "Q20", "Q201698", "Q2017699", "Q2093907", "Q21", "Q211", "Q212", "Q21203", "Q212429", "Q213", "Q214", "Q215", "Q217", "Q218", "Q219", "Q219060", "Q22", "Q220982", "Q221", "Q222", "Q223", "Q224", "Q225", "Q227", "Q228", "Q22890", "Q229", "Q2298216", "Q230", "Q232", "Q233", "Q23408", "Q235", "Q236", "Q23635", "Q23666", "Q23681", "Q237", "Q238", "Q241", "Q242", "Q244", "Q25", "Q252", "Q25228", "Q25230", "Q25231", "Q25263", "Q25279", "Q25305", "Q25359", "Q25362", "Q25396", "Q25528", "Q258", "Q26", "Q26180", "Q262", "Q26253", "Q26273", "Q265", "Q26927", "Q26988", "Q27", "Q27231", "Q27275", "Q27293", "Q27329", "Q27381", "Q27394", "Q27407", "Q27433", "Q27449", "Q27468", "Q27479", "Q27496", "Q27561", "Q27611", "Q28", "Q2872203", "Q28868874", "Q29", "Q2914565", "Q2915956", "Q298", "Q29999", "Q30", "Q30971", "Q31", "Q31057", "Q31063", "Q32", "Q33", "Q3311985", "Q3320166", "Q3336843", "Q334", "Q3359409", "Q33788", "Q34", "Q34020", "Q3405693", "Q34366", "Q34497", "Q34617", "Q347", "Q3492", "Q35", "Q35086", "Q35555", "Q35657", "Q35672", "Q35942", "Q36", "Q36004", "Q36117", "Q36678", "Q36823", "Q37", "Q37362", "Q37394", "Q3757", "Q3769", "Q3795", "Q38", "Q3803", "Q38095", "Q3812", "Q3827", "Q3845", "Q39", "Q39760", "Q398", "Q399", "Q40", "Q403", "Q408", "Q40888", "Q41", "Q414", "Q41684", "Q419", "Q423", "Q42314", "Q424", "Q43", "Q43296", "Q43448", "Q4412", "Q45", "Q45256", "Q458", "Q46", "Q46197", "Q46275", "Q4628", "Q46395", "Q46772", "Q46879", "Q47863", "Q48", "Q49", "Q51", "Q538", "Q55", "Q5689", "Q574", "Q5765", "Q578170", "Q5785", "Q5813", "Q5823", "Q5831", "Q620634", "Q62218", "Q6250", "Q639185", "Q644636", "Q657", "Q664", "Q664609", "Q668", "Q672", "Q6736667", "Q678", "Q683", "Q685", "Q686", "Q691", "Q695", "Q697", "Q702", "Q709", "Q710", "Q711", "Q712", "Q717", "Q72829598", "Q730", "Q733", "Q734", "Q736", "Q739", "Q750", "Q754", "Q756617", "Q757", "Q760", "Q763", "Q766", "Q769", "Q77", "Q771405", "Q774", "Q778", "Q781", "Q782", "Q783", "Q7835", "Q784", "Q785", "Q786", "Q79", "Q790", "Q792", "Q794", "Q796", "Q797", "Q800", "Q801", "Q804", "Q805", "Q810", "Q811", "Q813", "Q817", "Q819", "Q822", "Q826", "Q828", "Q833", "Q836", "Q837", "Q842", "Q842829", "Q843", "Q846", "Q851", "Q851132", "Q854", "Q858", "Q863", "Q8646", "Q865", "Q869", "Q874", "Q875134", "Q878", "Q881", "Q884", "Q889", "Q902", "Q912", "Q9143535", "Q916", "Q917", "Q9206745", "Q921", "Q924", "Q928", "Q929", "Q945", "Q948", "Q953", "Q954", "Q958", "Q96", "Q962", "Q963", "Q9648", "Q965", "Q967", "Q9676", "Q970", "Q971", "Q974", "Q977", "Q98059339", "Q983", "Q98543636", "Q986", "QA", "QAT", "RA", "RC", "RE", "REU", "RH", "RI", "RL", "RM", "RN", "RO", "ROU", "RP", "RS", "RU", "RUS", "RW", "RWA", "SA", "SAU", "SB", "SBA", "SC", "SD", "SDN", "SE", "SEN", "SF", "SG", "SGP", "SGS", "SH", "SH-AC", "SH-HL", "SH-TA", "SHN", "SI", "SJ", "SJM", "SK", "SL", "SLB", "SLE", "SLV", "SM", "SMR", "SN", "SO", "SOM", "SPM", "SR", "SRB", "SS", "SSD", "ST", "STP", "SUR", "SV", "SVK", "SVN", "SWE", "SWZ", "SX", "SXM", "SY", "SYC", "SYR", "SZ", "Swaziland", "TA", "TAA", "TC", "TCA", "TCD", "TD", "TF", "TG", "TGO", "TH", "THA", "TJ", "TJK", "TK", "TKL", "TKM", "TL", "TLS", "TM", "TN", "TO", "TON", "TP", "TR", "TT", "TTO", "TUN", "TUR", "TUV", "TV", "TW", "TWN", "TZ", "TZA", "Timor-Leste", "UA", "UG", "UGA", "UK", "UKOTS", "UKR", "UM", "UM-67", "UM-71", "UM-76", "UM-79", "UM-81", "UM-84", "UM-86", "UM-89", "UM-95", "UMI", "UN", "URY", "US", "US-AK", "US-AS", "US-GU", "US-HI", "US-MP", "US-PR", "US-VI", "USA", "UY", "UZ", "UZB", "VA", "VAT", "VC", "VCT", "VE", "VEN", "VG", "VGB", "VI", "VIR", "VN", "VNM", "VU", "VUT", "WAK", "WF", "WG", "WK", "WKUM", "WL", "WLF", "WS", "WSM", "WV", "XK", "XKX", "YE", "YEM", "YT", "YV", "ZA", "ZAF", "ZM", "ZMB", "ZR", "ZW", "ZWE"] 2 | -------------------------------------------------------------------------------- /app/plugins.py: -------------------------------------------------------------------------------- 1 | import zipfile 2 | import yaml 3 | import re 4 | import os 5 | import os.path 6 | import json 7 | import qrcode 8 | import qrcode.image.svg 9 | import shutil 10 | from flask import ( 11 | Blueprint, url_for, redirect, render_template, g, 12 | current_app, flash, request, abort, send_file, 13 | ) 14 | from flask_wtf import FlaskForm 15 | from flask_wtf.file import FileField, FileRequired 16 | from wtforms.validators import ValidationError 17 | import wtforms.fields as wtf 18 | import wtforms.validators as wtv 19 | from typing import BinaryIO 20 | from sqlalchemy import func 21 | from sqlalchemy.exc import NoResultFound, IntegrityError 22 | from .auth import login_required, get_user 23 | from .database import db, Plugin, PluginVersion 24 | from importlib.resources import read_text 25 | 26 | 27 | bp = Blueprint('plugins', __name__) 28 | countries = json.loads(read_text('app', 'countries.json')) 29 | FORBIDDEN_NAMES = [ 30 | 'my', 'search', 'nav', 'upload', 'edit', 'delete', 'icon', 31 | 'login', 'auth', 'logout', 'api', 32 | ] 33 | 34 | 35 | @bp.route('/', endpoint='list') 36 | @get_user 37 | def plugins_list(): 38 | plugins = db.session.scalars(db.select(Plugin).order_by(Plugin.title)) 39 | plugins = [p for p in plugins if p.last_version] 40 | return render_template('plugins.html', plugins=plugins, mine=False) 41 | 42 | 43 | @bp.route('/my', endpoint='my') 44 | @get_user 45 | @login_required 46 | def plugins_mine(): 47 | plugins = db.session.scalars(db.select(Plugin).where( 48 | Plugin.created_by == g.user).order_by(Plugin.title)) 49 | return render_template('plugins.html', plugins=plugins, mine=True) 50 | 51 | 52 | @bp.route('/search') 53 | @get_user 54 | def search(): 55 | value = request.args.get('q', '').strip() 56 | if not value: 57 | return plugins_list() 58 | plugins = db.session.scalars(db.select(Plugin).where( 59 | Plugin.title.like(f'%{value}%')).order_by(Plugin.title)) 60 | return render_template('plugins.html', plugins=plugins, 61 | mine=False, search=value) 62 | 63 | 64 | def validate_country(form, field): 65 | data = field.data 66 | if not data: 67 | return 68 | if data in countries: 69 | return 70 | raise ValidationError(f'{data} is not a correct country identifier') 71 | 72 | 73 | class UploadForm(FlaskForm): 74 | package = FileField('EDP Package', validators=[FileRequired()]) 75 | 76 | 77 | def unpack_edp(package: BinaryIO) -> dict: 78 | maxsize = current_app.config['MAX_CONTENT_LENGTH'] 79 | package_size = package.seek(0, 2) 80 | package.seek(0) 81 | if package_size > maxsize: 82 | raise ValidationError('File is too big') 83 | 84 | try: 85 | pkg = zipfile.ZipFile(package, mode='r') 86 | except Exception as e: 87 | raise ValidationError(f'Cannot unzip the package: {e}') 88 | 89 | try: 90 | test_result = pkg.testzip() 91 | if test_result: 92 | try: 93 | raise ValidationError(f'Bad zip file: {test_result}') 94 | except UnicodeDecodeError: 95 | raise ValidationError('Bad zip file, maybe charset issue') 96 | 97 | namelist = pkg.namelist() 98 | for name in namelist: 99 | if '..' in name: 100 | raise ValidationError( 101 | f'Found "{name}" in zip file, which is wrong') 102 | 103 | if 'plugin.yaml' not in namelist: 104 | raise ValidationError('Missing plugin.yaml file') 105 | 106 | try: 107 | content = pkg.read('plugin.yaml').decode('utf8') 108 | metadata = yaml.safe_load(content) 109 | except Exception as e: 110 | raise ValidationError( 111 | f'Error loading plugin.yaml, must be broken: {e}') 112 | 113 | if not isinstance(metadata, dict): 114 | raise ValidationError('plugin.yaml should contain a dictionary') 115 | 116 | if 'icon' in metadata: 117 | icon_file = f'icons/{metadata["icon"]}' 118 | if icon_file not in namelist: 119 | raise ValidationError(f'Missing "{icon_file}" file') 120 | m_ext = re.search(r'\.(svg|gif|png|webp)$', icon_file) 121 | if not m_ext: 122 | raise ValidationError( 123 | 'Icon should be an svg, png, gif, or webp') 124 | max_icon_size = current_app.config['MAX_ICON_SIZE_KB'] * 1024 125 | icon_info = pkg.getinfo(icon_file) 126 | if icon_info.file_size > max_icon_size: 127 | raise ValidationError( 128 | f'Icon file is larger than {max_icon_size}') 129 | metadata['icon_ext'] = m_ext.group(1) 130 | metadata['icon_data'] = pkg.read(icon_file) 131 | if len(metadata['icon_data']) > max_icon_size: 132 | raise ValidationError( 133 | f'Icon file real size is larger than {max_icon_size}') 134 | finally: 135 | pkg.close() 136 | package.seek(0) 137 | 138 | req_keys = ('id', 'name', 'version', 'description') 139 | for k in req_keys: 140 | if k not in metadata: 141 | raise ValidationError(f'Key "{k}" is missing in the metadata') 142 | 143 | if not re.match(r'^[a-zA-Z0-9][a-zA-Z0-9_-]+$', metadata['id']): 144 | raise ValidationError( 145 | 'Plugin id must be of latin letters, numbers, ' 146 | 'dashes, or underscores.') 147 | 148 | if metadata['id'] in FORBIDDEN_NAMES: 149 | raise ValidationError( 150 | f'Plugin id is a reserved word: {metadata["id"]}') 151 | 152 | return metadata 153 | 154 | 155 | @bp.route('/upload', methods=['GET', 'POST']) 156 | @get_user 157 | @login_required 158 | def upload(): 159 | form = UploadForm() 160 | if form.validate_on_submit(): 161 | # Uploading a package, finally 162 | try: 163 | metadata = unpack_edp(form.package.data) 164 | 165 | plugin_id = metadata['id'] 166 | version = PluginVersion.parse_version(metadata['version']) 167 | if db.session.scalar( 168 | db.select(func.count(PluginVersion.pk)) 169 | .where(PluginVersion.plugin_id == plugin_id) 170 | .where(PluginVersion.version >= version)) > 0: 171 | raise ValidationError( 172 | f'Version {metadata["version"]} or higher already exists.') 173 | 174 | data = { 175 | 'id': plugin_id, 176 | 'title': metadata['name'], 177 | 'description': metadata['description'], 178 | 'created_by': g.user, 179 | 'homepage': metadata.get('homepage'), 180 | 'country': metadata.get('country'), 181 | 'icon': metadata.get('icon_ext'), 182 | } 183 | if data['country']: 184 | validate_country(data['country']) 185 | 186 | try: 187 | plugin = db.session.get_one(Plugin, plugin_id) 188 | if plugin.created_by != g.user: 189 | raise ValidationError('No permission to update') 190 | plugin.title = data['title'] 191 | plugin.description = data['description'] 192 | plugin.homepage = data['homepage'] 193 | plugin.icon = data['icon'] 194 | except NoResultFound: 195 | if db.session.scalar( 196 | db.select(func.count(Plugin.id)) 197 | .where(Plugin.title == data['title'])) > 0: 198 | raise ValidationError( 199 | f'A plugin with the title "{data["title"]}" ' 200 | 'but a different id already exists.') 201 | plugin = Plugin(**data) 202 | db.session.add(plugin) 203 | 204 | version = PluginVersion( 205 | plugin_id=plugin.id, 206 | plugin=plugin, 207 | version=version, 208 | created_by=g.user, 209 | experimental=metadata.get('experimental', True), 210 | ) 211 | db.session.add(version) 212 | 213 | # Copy the file 214 | path = version.filename 215 | os.makedirs(os.path.dirname(path), exist_ok=True) 216 | form.package.data.save(path) 217 | # And the icon 218 | icon_file = plugin.icon_file 219 | if icon_file and 'icon_data' in metadata: 220 | with open(icon_file, 'wb') as f: 221 | f.write(metadata['icon_data']) 222 | 223 | db.session.commit() 224 | 225 | return redirect(url_for('plugins.plugin', name=plugin_id)) 226 | except ValidationError as e: 227 | flash(e) 228 | except IntegrityError as e: 229 | flash(e) 230 | except OSError as e: 231 | flash(f'Error copying the file: {e}') 232 | return render_template('upload.html', form=form) 233 | 234 | 235 | class PluginForm(FlaskForm): 236 | title = wtf.StringField( 237 | validators=[wtv.DataRequired(), wtv.Length(max=250)]) 238 | description = wtf.TextAreaField() 239 | homepage = wtf.URLField(validators=[wtv.Optional(), wtv.URL()]) 240 | country = wtf.StringField( 241 | validators=[wtv.Optional(), wtv.Length(max=32), 242 | validate_country]) 243 | hidden = wtf.BooleanField() 244 | 245 | 246 | @bp.route('/edit/', endpoint='edit', methods=['GET', 'POST']) 247 | @get_user 248 | @login_required 249 | def edit_plugin(name: str): 250 | plugin: Plugin = db.get_or_404(Plugin, name) 251 | if plugin.created_by != g.user: 252 | raise ValidationError('No permission to update') 253 | form = PluginForm(obj=plugin) 254 | if form.validate_on_submit(): 255 | plugin.title = form.title.data 256 | plugin.description = form.description.data 257 | plugin.homepage = form.homepage.data or None 258 | plugin.hidden = form.hidden.data 259 | plugin.country = form.country.data or None 260 | db.session.commit() 261 | return redirect(url_for('.plugin', name=name)) 262 | return render_template('edit_plugin.html', plugin=plugin, form=form) 263 | 264 | 265 | class VersionForm(FlaskForm): 266 | changelog = wtf.TextAreaField() 267 | experimental = wtf.BooleanField() 268 | 269 | 270 | @bp.route('/edit//', endpoint='version', 271 | methods=['GET', 'POST']) 272 | @get_user 273 | @login_required 274 | def edit_version(name: str, version: str): 275 | plugin: Plugin = db.get_or_404(Plugin, name) 276 | if plugin.created_by != g.user: 277 | raise ValidationError('No permission to update') 278 | vobj = db.session.scalars( 279 | db.select(PluginVersion) 280 | .where(PluginVersion.plugin_id == name) 281 | .where(PluginVersion.version == PluginVersion.parse_version(version)) 282 | .limit(1) 283 | ).one_or_none() 284 | if vobj is None: 285 | flash('No such version') 286 | return redirect(url_for('.plugin', name=name)) 287 | 288 | form = VersionForm(obj=vobj) 289 | if form.validate_on_submit(): 290 | vobj.changelog = form.changelog.data 291 | db.session.commit() 292 | return redirect(url_for('.plugin', name=name)) 293 | return render_template( 294 | 'edit_version.html', plugin=plugin, version=vobj, form=form) 295 | 296 | 297 | @bp.route('/delete/', endpoint='delete', 298 | methods=['GET', 'POST']) 299 | @bp.route('/delete//', endpoint='delete', 300 | methods=['GET', 'POST']) 301 | @get_user 302 | @login_required 303 | def delete_something(name: str, version: str | None = None): 304 | plugin: Plugin = db.get_or_404(Plugin, name) 305 | if plugin.created_by != g.user: 306 | raise ValidationError('No permission to update') 307 | vobj: PluginVersion | None = None 308 | if version is not None: 309 | vobj = db.session.scalars( 310 | db.select(PluginVersion) 311 | .where(PluginVersion.plugin_id == name) 312 | .where(PluginVersion.version == 313 | PluginVersion.parse_version(version)) 314 | .limit(1) 315 | ).one_or_none() 316 | if vobj is None: 317 | flash('No such version') 318 | return redirect(url_for('.plugin', name=name)) 319 | 320 | if request.method == 'POST': 321 | if request.form.get('really_delete') != '1': 322 | return redirect(url_for('.plugin', name=name)) 323 | db.session.delete(vobj or plugin) 324 | db.session.commit() 325 | 326 | # Delete files 327 | try: 328 | if vobj: 329 | os.remove(vobj.filename) 330 | else: 331 | shutil.rmtree( 332 | os.path.join( 333 | current_app.instance_path, 'plugins', name), 334 | ignore_errors=True, 335 | ) 336 | except IOError: 337 | # Oh well 338 | pass 339 | 340 | # Redirect back 341 | if vobj: 342 | return redirect(url_for('.plugin', name=name)) 343 | return redirect(url_for('.list')) 344 | return render_template('delete.html', plugin=plugin, version=vobj) 345 | 346 | 347 | @bp.route('/i/') 348 | def install(name: str): 349 | url = request.args.get('url') 350 | if not url: 351 | return redirect(url_for('.plugin', name=name)) 352 | if not re.match(r'^https?://.+/[a-zA-Z0-9_-]+\.edp$', url): 353 | flash(f'URL {url} does not seem to point to an EDP file.') 354 | return redirect(url_for('.list')) 355 | 356 | qr = qrcode.make( 357 | url, image_factory=qrcode.image.svg.SvgPathImage, 358 | border=1, box_size=20 359 | ) 360 | plugin = db.session.scalars( 361 | db.select(Plugin).where(Plugin.id == name).limit(1) 362 | ).one_or_none() 363 | return render_template( 364 | 'install.html', name=name, url=url, plugin=plugin, 365 | qrcode=qr.to_string().decode()) 366 | 367 | 368 | @bp.route('/.edp') 369 | @bp.route('/.v.edp') 370 | def download(name: str, version: str | None = None): 371 | plugin = db.get_or_404(Plugin, name) 372 | vobj: PluginVersion | None = None 373 | if version is not None: 374 | vobj = db.session.scalars( 375 | db.select(PluginVersion) 376 | .where(PluginVersion.plugin_id == name) 377 | .where(PluginVersion.version == 378 | PluginVersion.parse_version(version)) 379 | .limit(1) 380 | ).one_or_none() 381 | else: 382 | vobj = plugin.last_version 383 | if vobj is None: 384 | # Fall back to experimental versions. 385 | # Let's hope the user knows what they are doing. 386 | vobj = plugin.last_eversion 387 | if vobj is None: 388 | return abort(404, f'Version {version} not found.') 389 | vobj.downloads += 1 390 | db.session.commit() 391 | path = vobj.filename 392 | return send_file( 393 | path, mimetype='application/x.edp+zip', 394 | download_name=f'{name}.v{vobj.version_str}.edp', 395 | as_attachment=True, 396 | ) 397 | 398 | 399 | @bp.route('/icon/') 400 | @bp.route('/icon/.') 401 | def icon(name: str, ext: str | None = None): 402 | plugin = db.get_or_404(Plugin, name) 403 | icon_file = plugin.icon_file 404 | if not icon_file: 405 | return abort(404, 'The plugin has no icon') 406 | if ext and plugin.icon != ext.lower(): 407 | return abort(415, f'Incorrect extension, expected {plugin.icon}') 408 | mime = { 409 | 'svg': 'image/svg+xml', 410 | 'png': 'image/png', 411 | 'gif': 'image/gif', 412 | 'webp': 'image/webp', 413 | } 414 | return send_file( 415 | icon_file, mimetype=mime.get(icon_file.rsplit('.', 1)[-1])) 416 | 417 | 418 | @bp.route('/') 419 | @get_user 420 | def plugin(name: str): 421 | plugin = db.get_or_404(Plugin, name) 422 | plugin_url = url_for('.install', name=name, _external=True) 423 | qr = qrcode.make( 424 | plugin_url, image_factory=qrcode.image.svg.SvgPathImage, 425 | border=1, box_size=20 426 | ) 427 | return render_template( 428 | 'plugin.html', plugin=plugin, qrcode=qr.to_string().decode()) 429 | -------------------------------------------------------------------------------- /app/static/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v5.3.6 (https://getbootstrap.com/) 3 | * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) 5 | */ 6 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("@popperjs/core")):"function"==typeof define&&define.amd?define(["@popperjs/core"],e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e(t.Popper)}(this,(function(t){"use strict";function e(t){const e=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(t)for(const i in t)if("default"!==i){const s=Object.getOwnPropertyDescriptor(t,i);Object.defineProperty(e,i,s.get?s:{enumerable:!0,get:()=>t[i]})}return e.default=t,Object.freeze(e)}const i=e(t),s=new Map,n={set(t,e,i){s.has(t)||s.set(t,new Map);const n=s.get(t);n.has(e)||0===n.size?n.set(e,i):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(n.keys())[0]}.`)},get:(t,e)=>s.has(t)&&s.get(t).get(e)||null,remove(t,e){if(!s.has(t))return;const i=s.get(t);i.delete(e),0===i.size&&s.delete(t)}},o="transitionend",r=t=>(t&&window.CSS&&window.CSS.escape&&(t=t.replace(/#([^\s"#']+)/g,((t,e)=>`#${CSS.escape(e)}`))),t),a=t=>{t.dispatchEvent(new Event(o))},l=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),c=t=>l(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(r(t)):null,h=t=>{if(!l(t)||0===t.getClientRects().length)return!1;const e="visible"===getComputedStyle(t).getPropertyValue("visibility"),i=t.closest("details:not([open])");if(!i)return e;if(i!==t){const e=t.closest("summary");if(e&&e.parentNode!==i)return!1;if(null===e)return!1}return e},d=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),u=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?u(t.parentNode):null},_=()=>{},g=t=>{t.offsetHeight},f=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,m=[],p=()=>"rtl"===document.documentElement.dir,b=t=>{var e;e=()=>{const e=f();if(e){const i=t.NAME,s=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=s,t.jQueryInterface)}},"loading"===document.readyState?(m.length||document.addEventListener("DOMContentLoaded",(()=>{for(const t of m)t()})),m.push(e)):e()},v=(t,e=[],i=t)=>"function"==typeof t?t.call(...e):i,y=(t,e,i=!0)=>{if(!i)return void v(t);const s=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const s=Number.parseFloat(e),n=Number.parseFloat(i);return s||n?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(e)+5;let n=!1;const r=({target:i})=>{i===e&&(n=!0,e.removeEventListener(o,r),v(t))};e.addEventListener(o,r),setTimeout((()=>{n||a(e)}),s)},w=(t,e,i,s)=>{const n=t.length;let o=t.indexOf(e);return-1===o?!i&&s?t[n-1]:t[0]:(o+=i?1:-1,s&&(o=(o+n)%n),t[Math.max(0,Math.min(o,n-1))])},A=/[^.]*(?=\..*)\.|.*/,E=/\..*/,C=/::\d+$/,T={};let k=1;const $={mouseenter:"mouseover",mouseleave:"mouseout"},S=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function L(t,e){return e&&`${e}::${k++}`||t.uidEvent||k++}function O(t){const e=L(t);return t.uidEvent=e,T[e]=T[e]||{},T[e]}function I(t,e,i=null){return Object.values(t).find((t=>t.callable===e&&t.delegationSelector===i))}function D(t,e,i){const s="string"==typeof e,n=s?i:e||i;let o=M(t);return S.has(o)||(o=t),[s,n,o]}function N(t,e,i,s,n){if("string"!=typeof e||!t)return;let[o,r,a]=D(e,i,s);if(e in $){const t=t=>function(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};r=t(r)}const l=O(t),c=l[a]||(l[a]={}),h=I(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&n);const d=L(r,e.replace(A,"")),u=o?function(t,e,i){return function s(n){const o=t.querySelectorAll(e);for(let{target:r}=n;r&&r!==this;r=r.parentNode)for(const a of o)if(a===r)return F(n,{delegateTarget:r}),s.oneOff&&j.off(t,n.type,e,i),i.apply(r,[n])}}(t,i,r):function(t,e){return function i(s){return F(s,{delegateTarget:t}),i.oneOff&&j.off(t,s.type,e),e.apply(t,[s])}}(t,r);u.delegationSelector=o?i:null,u.callable=r,u.oneOff=n,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function P(t,e,i,s,n){const o=I(e[i],s,n);o&&(t.removeEventListener(i,o,Boolean(n)),delete e[i][o.uidEvent])}function x(t,e,i,s){const n=e[i]||{};for(const[o,r]of Object.entries(n))o.includes(s)&&P(t,e,i,r.callable,r.delegationSelector)}function M(t){return t=t.replace(E,""),$[t]||t}const j={on(t,e,i,s){N(t,e,i,s,!1)},one(t,e,i,s){N(t,e,i,s,!0)},off(t,e,i,s){if("string"!=typeof e||!t)return;const[n,o,r]=D(e,i,s),a=r!==e,l=O(t),c=l[r]||{},h=e.startsWith(".");if(void 0===o){if(h)for(const i of Object.keys(l))x(t,l,i,e.slice(1));for(const[i,s]of Object.entries(c)){const n=i.replace(C,"");a&&!e.includes(n)||P(t,l,r,s.callable,s.delegationSelector)}}else{if(!Object.keys(c).length)return;P(t,l,r,o,n?i:null)}},trigger(t,e,i){if("string"!=typeof e||!t)return null;const s=f();let n=null,o=!0,r=!0,a=!1;e!==M(e)&&s&&(n=s.Event(e,i),s(t).trigger(n),o=!n.isPropagationStopped(),r=!n.isImmediatePropagationStopped(),a=n.isDefaultPrevented());const l=F(new Event(e,{bubbles:o,cancelable:!0}),i);return a&&l.preventDefault(),r&&t.dispatchEvent(l),l.defaultPrevented&&n&&n.preventDefault(),l}};function F(t,e={}){for(const[i,s]of Object.entries(e))try{t[i]=s}catch(e){Object.defineProperty(t,i,{configurable:!0,get:()=>s})}return t}function z(t){if("true"===t)return!0;if("false"===t)return!1;if(t===Number(t).toString())return Number(t);if(""===t||"null"===t)return null;if("string"!=typeof t)return t;try{return JSON.parse(decodeURIComponent(t))}catch(e){return t}}function H(t){return t.replace(/[A-Z]/g,(t=>`-${t.toLowerCase()}`))}const B={setDataAttribute(t,e,i){t.setAttribute(`data-bs-${H(e)}`,i)},removeDataAttribute(t,e){t.removeAttribute(`data-bs-${H(e)}`)},getDataAttributes(t){if(!t)return{};const e={},i=Object.keys(t.dataset).filter((t=>t.startsWith("bs")&&!t.startsWith("bsConfig")));for(const s of i){let i=s.replace(/^bs/,"");i=i.charAt(0).toLowerCase()+i.slice(1),e[i]=z(t.dataset[s])}return e},getDataAttribute:(t,e)=>z(t.getAttribute(`data-bs-${H(e)}`))};class q{static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}_getConfig(t){return t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t}_mergeConfigObj(t,e){const i=l(e)?B.getDataAttribute(e,"config"):{};return{...this.constructor.Default,..."object"==typeof i?i:{},...l(e)?B.getDataAttributes(e):{},..."object"==typeof t?t:{}}}_typeCheckConfig(t,e=this.constructor.DefaultType){for(const[s,n]of Object.entries(e)){const e=t[s],o=l(e)?"element":null==(i=e)?`${i}`:Object.prototype.toString.call(i).match(/\s([a-z]+)/i)[1].toLowerCase();if(!new RegExp(n).test(o))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${s}" provided type "${o}" but expected type "${n}".`)}var i}}class W extends q{constructor(t,e){super(),(t=c(t))&&(this._element=t,this._config=this._getConfig(e),n.set(this._element,this.constructor.DATA_KEY,this))}dispose(){n.remove(this._element,this.constructor.DATA_KEY),j.off(this._element,this.constructor.EVENT_KEY);for(const t of Object.getOwnPropertyNames(this))this[t]=null}_queueCallback(t,e,i=!0){y(t,e,i)}_getConfig(t){return t=this._mergeConfigObj(t,this._element),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}static getInstance(t){return n.get(c(t),this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.3.6"}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(t){return`${t}${this.EVENT_KEY}`}}const R=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i=`#${i.split("#")[1]}`),e=i&&"#"!==i?i.trim():null}return e?e.split(",").map((t=>r(t))).join(","):null},K={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter((t=>t.matches(e))),parents(t,e){const i=[];let s=t.parentNode.closest(e);for(;s;)i.push(s),s=s.parentNode.closest(e);return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]},focusableChildren(t){const e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map((t=>`${t}:not([tabindex^="-"])`)).join(",");return this.find(e,t).filter((t=>!d(t)&&h(t)))},getSelectorFromElement(t){const e=R(t);return e&&K.findOne(e)?e:null},getElementFromSelector(t){const e=R(t);return e?K.findOne(e):null},getMultipleElementsFromSelector(t){const e=R(t);return e?K.find(e):[]}},V=(t,e="hide")=>{const i=`click.dismiss${t.EVENT_KEY}`,s=t.NAME;j.on(document,i,`[data-bs-dismiss="${s}"]`,(function(i){if(["A","AREA"].includes(this.tagName)&&i.preventDefault(),d(this))return;const n=K.getElementFromSelector(this)||this.closest(`.${s}`);t.getOrCreateInstance(n)[e]()}))},Q=".bs.alert",X=`close${Q}`,Y=`closed${Q}`;class U extends W{static get NAME(){return"alert"}close(){if(j.trigger(this._element,X).defaultPrevented)return;this._element.classList.remove("show");const t=this._element.classList.contains("fade");this._queueCallback((()=>this._destroyElement()),this._element,t)}_destroyElement(){this._element.remove(),j.trigger(this._element,Y),this.dispose()}static jQueryInterface(t){return this.each((function(){const e=U.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}V(U,"close"),b(U);const G='[data-bs-toggle="button"]';class J extends W{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each((function(){const e=J.getOrCreateInstance(this);"toggle"===t&&e[t]()}))}}j.on(document,"click.bs.button.data-api",G,(t=>{t.preventDefault();const e=t.target.closest(G);J.getOrCreateInstance(e).toggle()})),b(J);const Z=".bs.swipe",tt=`touchstart${Z}`,et=`touchmove${Z}`,it=`touchend${Z}`,st=`pointerdown${Z}`,nt=`pointerup${Z}`,ot={endCallback:null,leftCallback:null,rightCallback:null},rt={endCallback:"(function|null)",leftCallback:"(function|null)",rightCallback:"(function|null)"};class at extends q{constructor(t,e){super(),this._element=t,t&&at.isSupported()&&(this._config=this._getConfig(e),this._deltaX=0,this._supportPointerEvents=Boolean(window.PointerEvent),this._initEvents())}static get Default(){return ot}static get DefaultType(){return rt}static get NAME(){return"swipe"}dispose(){j.off(this._element,Z)}_start(t){this._supportPointerEvents?this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX):this._deltaX=t.touches[0].clientX}_end(t){this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX-this._deltaX),this._handleSwipe(),v(this._config.endCallback)}_move(t){this._deltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this._deltaX}_handleSwipe(){const t=Math.abs(this._deltaX);if(t<=40)return;const e=t/this._deltaX;this._deltaX=0,e&&v(e>0?this._config.rightCallback:this._config.leftCallback)}_initEvents(){this._supportPointerEvents?(j.on(this._element,st,(t=>this._start(t))),j.on(this._element,nt,(t=>this._end(t))),this._element.classList.add("pointer-event")):(j.on(this._element,tt,(t=>this._start(t))),j.on(this._element,et,(t=>this._move(t))),j.on(this._element,it,(t=>this._end(t))))}_eventIsPointerPenTouch(t){return this._supportPointerEvents&&("pen"===t.pointerType||"touch"===t.pointerType)}static isSupported(){return"ontouchstart"in document.documentElement||navigator.maxTouchPoints>0}}const lt=".bs.carousel",ct=".data-api",ht="ArrowLeft",dt="ArrowRight",ut="next",_t="prev",gt="left",ft="right",mt=`slide${lt}`,pt=`slid${lt}`,bt=`keydown${lt}`,vt=`mouseenter${lt}`,yt=`mouseleave${lt}`,wt=`dragstart${lt}`,At=`load${lt}${ct}`,Et=`click${lt}${ct}`,Ct="carousel",Tt="active",kt=".active",$t=".carousel-item",St=kt+$t,Lt={[ht]:ft,[dt]:gt},Ot={interval:5e3,keyboard:!0,pause:"hover",ride:!1,touch:!0,wrap:!0},It={interval:"(number|boolean)",keyboard:"boolean",pause:"(string|boolean)",ride:"(boolean|string)",touch:"boolean",wrap:"boolean"};class Dt extends W{constructor(t,e){super(t,e),this._interval=null,this._activeElement=null,this._isSliding=!1,this.touchTimeout=null,this._swipeHelper=null,this._indicatorsElement=K.findOne(".carousel-indicators",this._element),this._addEventListeners(),this._config.ride===Ct&&this.cycle()}static get Default(){return Ot}static get DefaultType(){return It}static get NAME(){return"carousel"}next(){this._slide(ut)}nextWhenVisible(){!document.hidden&&h(this._element)&&this.next()}prev(){this._slide(_t)}pause(){this._isSliding&&a(this._element),this._clearInterval()}cycle(){this._clearInterval(),this._updateInterval(),this._interval=setInterval((()=>this.nextWhenVisible()),this._config.interval)}_maybeEnableCycle(){this._config.ride&&(this._isSliding?j.one(this._element,pt,(()=>this.cycle())):this.cycle())}to(t){const e=this._getItems();if(t>e.length-1||t<0)return;if(this._isSliding)return void j.one(this._element,pt,(()=>this.to(t)));const i=this._getItemIndex(this._getActive());if(i===t)return;const s=t>i?ut:_t;this._slide(s,e[t])}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_configAfterMerge(t){return t.defaultInterval=t.interval,t}_addEventListeners(){this._config.keyboard&&j.on(this._element,bt,(t=>this._keydown(t))),"hover"===this._config.pause&&(j.on(this._element,vt,(()=>this.pause())),j.on(this._element,yt,(()=>this._maybeEnableCycle()))),this._config.touch&&at.isSupported()&&this._addTouchEventListeners()}_addTouchEventListeners(){for(const t of K.find(".carousel-item img",this._element))j.on(t,wt,(t=>t.preventDefault()));const t={leftCallback:()=>this._slide(this._directionToOrder(gt)),rightCallback:()=>this._slide(this._directionToOrder(ft)),endCallback:()=>{"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout((()=>this._maybeEnableCycle()),500+this._config.interval))}};this._swipeHelper=new at(this._element,t)}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=Lt[t.key];e&&(t.preventDefault(),this._slide(this._directionToOrder(e)))}_getItemIndex(t){return this._getItems().indexOf(t)}_setActiveIndicatorElement(t){if(!this._indicatorsElement)return;const e=K.findOne(kt,this._indicatorsElement);e.classList.remove(Tt),e.removeAttribute("aria-current");const i=K.findOne(`[data-bs-slide-to="${t}"]`,this._indicatorsElement);i&&(i.classList.add(Tt),i.setAttribute("aria-current","true"))}_updateInterval(){const t=this._activeElement||this._getActive();if(!t)return;const e=Number.parseInt(t.getAttribute("data-bs-interval"),10);this._config.interval=e||this._config.defaultInterval}_slide(t,e=null){if(this._isSliding)return;const i=this._getActive(),s=t===ut,n=e||w(this._getItems(),i,s,this._config.wrap);if(n===i)return;const o=this._getItemIndex(n),r=e=>j.trigger(this._element,e,{relatedTarget:n,direction:this._orderToDirection(t),from:this._getItemIndex(i),to:o});if(r(mt).defaultPrevented)return;if(!i||!n)return;const a=Boolean(this._interval);this.pause(),this._isSliding=!0,this._setActiveIndicatorElement(o),this._activeElement=n;const l=s?"carousel-item-start":"carousel-item-end",c=s?"carousel-item-next":"carousel-item-prev";n.classList.add(c),g(n),i.classList.add(l),n.classList.add(l),this._queueCallback((()=>{n.classList.remove(l,c),n.classList.add(Tt),i.classList.remove(Tt,c,l),this._isSliding=!1,r(pt)}),i,this._isAnimated()),a&&this.cycle()}_isAnimated(){return this._element.classList.contains("slide")}_getActive(){return K.findOne(St,this._element)}_getItems(){return K.find($t,this._element)}_clearInterval(){this._interval&&(clearInterval(this._interval),this._interval=null)}_directionToOrder(t){return p()?t===gt?_t:ut:t===gt?ut:_t}_orderToDirection(t){return p()?t===_t?gt:ft:t===_t?ft:gt}static jQueryInterface(t){return this.each((function(){const e=Dt.getOrCreateInstance(this,t);if("number"!=typeof t){if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}else e.to(t)}))}}j.on(document,Et,"[data-bs-slide], [data-bs-slide-to]",(function(t){const e=K.getElementFromSelector(this);if(!e||!e.classList.contains(Ct))return;t.preventDefault();const i=Dt.getOrCreateInstance(e),s=this.getAttribute("data-bs-slide-to");return s?(i.to(s),void i._maybeEnableCycle()):"next"===B.getDataAttribute(this,"slide")?(i.next(),void i._maybeEnableCycle()):(i.prev(),void i._maybeEnableCycle())})),j.on(window,At,(()=>{const t=K.find('[data-bs-ride="carousel"]');for(const e of t)Dt.getOrCreateInstance(e)})),b(Dt);const Nt=".bs.collapse",Pt=`show${Nt}`,xt=`shown${Nt}`,Mt=`hide${Nt}`,jt=`hidden${Nt}`,Ft=`click${Nt}.data-api`,zt="show",Ht="collapse",Bt="collapsing",qt=`:scope .${Ht} .${Ht}`,Wt='[data-bs-toggle="collapse"]',Rt={parent:null,toggle:!0},Kt={parent:"(null|element)",toggle:"boolean"};class Vt extends W{constructor(t,e){super(t,e),this._isTransitioning=!1,this._triggerArray=[];const i=K.find(Wt);for(const t of i){const e=K.getSelectorFromElement(t),i=K.find(e).filter((t=>t===this._element));null!==e&&i.length&&this._triggerArray.push(t)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return Rt}static get DefaultType(){return Kt}static get NAME(){return"collapse"}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t=[];if(this._config.parent&&(t=this._getFirstLevelChildren(".collapse.show, .collapse.collapsing").filter((t=>t!==this._element)).map((t=>Vt.getOrCreateInstance(t,{toggle:!1})))),t.length&&t[0]._isTransitioning)return;if(j.trigger(this._element,Pt).defaultPrevented)return;for(const e of t)e.hide();const e=this._getDimension();this._element.classList.remove(Ht),this._element.classList.add(Bt),this._element.style[e]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const i=`scroll${e[0].toUpperCase()+e.slice(1)}`;this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(Bt),this._element.classList.add(Ht,zt),this._element.style[e]="",j.trigger(this._element,xt)}),this._element,!0),this._element.style[e]=`${this._element[i]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(j.trigger(this._element,Mt).defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,g(this._element),this._element.classList.add(Bt),this._element.classList.remove(Ht,zt);for(const t of this._triggerArray){const e=K.getElementFromSelector(t);e&&!this._isShown(e)&&this._addAriaAndCollapsedClass([t],!1)}this._isTransitioning=!0,this._element.style[t]="",this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(Bt),this._element.classList.add(Ht),j.trigger(this._element,jt)}),this._element,!0)}_isShown(t=this._element){return t.classList.contains(zt)}_configAfterMerge(t){return t.toggle=Boolean(t.toggle),t.parent=c(t.parent),t}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const t=this._getFirstLevelChildren(Wt);for(const e of t){const t=K.getElementFromSelector(e);t&&this._addAriaAndCollapsedClass([e],this._isShown(t))}}_getFirstLevelChildren(t){const e=K.find(qt,this._config.parent);return K.find(t,this._config.parent).filter((t=>!e.includes(t)))}_addAriaAndCollapsedClass(t,e){if(t.length)for(const i of t)i.classList.toggle("collapsed",!e),i.setAttribute("aria-expanded",e)}static jQueryInterface(t){const e={};return"string"==typeof t&&/show|hide/.test(t)&&(e.toggle=!1),this.each((function(){const i=Vt.getOrCreateInstance(this,e);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t]()}}))}}j.on(document,Ft,Wt,(function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();for(const t of K.getMultipleElementsFromSelector(this))Vt.getOrCreateInstance(t,{toggle:!1}).toggle()})),b(Vt);const Qt="dropdown",Xt=".bs.dropdown",Yt=".data-api",Ut="ArrowUp",Gt="ArrowDown",Jt=`hide${Xt}`,Zt=`hidden${Xt}`,te=`show${Xt}`,ee=`shown${Xt}`,ie=`click${Xt}${Yt}`,se=`keydown${Xt}${Yt}`,ne=`keyup${Xt}${Yt}`,oe="show",re='[data-bs-toggle="dropdown"]:not(.disabled):not(:disabled)',ae=`${re}.${oe}`,le=".dropdown-menu",ce=p()?"top-end":"top-start",he=p()?"top-start":"top-end",de=p()?"bottom-end":"bottom-start",ue=p()?"bottom-start":"bottom-end",_e=p()?"left-start":"right-start",ge=p()?"right-start":"left-start",fe={autoClose:!0,boundary:"clippingParents",display:"dynamic",offset:[0,2],popperConfig:null,reference:"toggle"},me={autoClose:"(boolean|string)",boundary:"(string|element)",display:"string",offset:"(array|string|function)",popperConfig:"(null|object|function)",reference:"(string|element|object)"};class pe extends W{constructor(t,e){super(t,e),this._popper=null,this._parent=this._element.parentNode,this._menu=K.next(this._element,le)[0]||K.prev(this._element,le)[0]||K.findOne(le,this._parent),this._inNavbar=this._detectNavbar()}static get Default(){return fe}static get DefaultType(){return me}static get NAME(){return Qt}toggle(){return this._isShown()?this.hide():this.show()}show(){if(d(this._element)||this._isShown())return;const t={relatedTarget:this._element};if(!j.trigger(this._element,te,t).defaultPrevented){if(this._createPopper(),"ontouchstart"in document.documentElement&&!this._parent.closest(".navbar-nav"))for(const t of[].concat(...document.body.children))j.on(t,"mouseover",_);this._element.focus(),this._element.setAttribute("aria-expanded",!0),this._menu.classList.add(oe),this._element.classList.add(oe),j.trigger(this._element,ee,t)}}hide(){if(d(this._element)||!this._isShown())return;const t={relatedTarget:this._element};this._completeHide(t)}dispose(){this._popper&&this._popper.destroy(),super.dispose()}update(){this._inNavbar=this._detectNavbar(),this._popper&&this._popper.update()}_completeHide(t){if(!j.trigger(this._element,Jt,t).defaultPrevented){if("ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))j.off(t,"mouseover",_);this._popper&&this._popper.destroy(),this._menu.classList.remove(oe),this._element.classList.remove(oe),this._element.setAttribute("aria-expanded","false"),B.removeDataAttribute(this._menu,"popper"),j.trigger(this._element,Zt,t),this._element.focus()}}_getConfig(t){if("object"==typeof(t=super._getConfig(t)).reference&&!l(t.reference)&&"function"!=typeof t.reference.getBoundingClientRect)throw new TypeError(`${Qt.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`);return t}_createPopper(){if(void 0===i)throw new TypeError("Bootstrap's dropdowns require Popper (https://popper.js.org/docs/v2/)");let t=this._element;"parent"===this._config.reference?t=this._parent:l(this._config.reference)?t=c(this._config.reference):"object"==typeof this._config.reference&&(t=this._config.reference);const e=this._getPopperConfig();this._popper=i.createPopper(t,this._menu,e)}_isShown(){return this._menu.classList.contains(oe)}_getPlacement(){const t=this._parent;if(t.classList.contains("dropend"))return _e;if(t.classList.contains("dropstart"))return ge;if(t.classList.contains("dropup-center"))return"top";if(t.classList.contains("dropdown-center"))return"bottom";const e="end"===getComputedStyle(this._menu).getPropertyValue("--bs-position").trim();return t.classList.contains("dropup")?e?he:ce:e?ue:de}_detectNavbar(){return null!==this._element.closest(".navbar")}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return(this._inNavbar||"static"===this._config.display)&&(B.setDataAttribute(this._menu,"popper","static"),t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,...v(this._config.popperConfig,[void 0,t])}}_selectMenuItem({key:t,target:e}){const i=K.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter((t=>h(t)));i.length&&w(i,e,t===Gt,!i.includes(e)).focus()}static jQueryInterface(t){return this.each((function(){const e=pe.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}static clearMenus(t){if(2===t.button||"keyup"===t.type&&"Tab"!==t.key)return;const e=K.find(ae);for(const i of e){const e=pe.getInstance(i);if(!e||!1===e._config.autoClose)continue;const s=t.composedPath(),n=s.includes(e._menu);if(s.includes(e._element)||"inside"===e._config.autoClose&&!n||"outside"===e._config.autoClose&&n)continue;if(e._menu.contains(t.target)&&("keyup"===t.type&&"Tab"===t.key||/input|select|option|textarea|form/i.test(t.target.tagName)))continue;const o={relatedTarget:e._element};"click"===t.type&&(o.clickEvent=t),e._completeHide(o)}}static dataApiKeydownHandler(t){const e=/input|textarea/i.test(t.target.tagName),i="Escape"===t.key,s=[Ut,Gt].includes(t.key);if(!s&&!i)return;if(e&&!i)return;t.preventDefault();const n=this.matches(re)?this:K.prev(this,re)[0]||K.next(this,re)[0]||K.findOne(re,t.delegateTarget.parentNode),o=pe.getOrCreateInstance(n);if(s)return t.stopPropagation(),o.show(),void o._selectMenuItem(t);o._isShown()&&(t.stopPropagation(),o.hide(),n.focus())}}j.on(document,se,re,pe.dataApiKeydownHandler),j.on(document,se,le,pe.dataApiKeydownHandler),j.on(document,ie,pe.clearMenus),j.on(document,ne,pe.clearMenus),j.on(document,ie,re,(function(t){t.preventDefault(),pe.getOrCreateInstance(this).toggle()})),b(pe);const be="backdrop",ve="show",ye=`mousedown.bs.${be}`,we={className:"modal-backdrop",clickCallback:null,isAnimated:!1,isVisible:!0,rootElement:"body"},Ae={className:"string",clickCallback:"(function|null)",isAnimated:"boolean",isVisible:"boolean",rootElement:"(element|string)"};class Ee extends q{constructor(t){super(),this._config=this._getConfig(t),this._isAppended=!1,this._element=null}static get Default(){return we}static get DefaultType(){return Ae}static get NAME(){return be}show(t){if(!this._config.isVisible)return void v(t);this._append();const e=this._getElement();this._config.isAnimated&&g(e),e.classList.add(ve),this._emulateAnimation((()=>{v(t)}))}hide(t){this._config.isVisible?(this._getElement().classList.remove(ve),this._emulateAnimation((()=>{this.dispose(),v(t)}))):v(t)}dispose(){this._isAppended&&(j.off(this._element,ye),this._element.remove(),this._isAppended=!1)}_getElement(){if(!this._element){const t=document.createElement("div");t.className=this._config.className,this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_configAfterMerge(t){return t.rootElement=c(t.rootElement),t}_append(){if(this._isAppended)return;const t=this._getElement();this._config.rootElement.append(t),j.on(t,ye,(()=>{v(this._config.clickCallback)})),this._isAppended=!0}_emulateAnimation(t){y(t,this._getElement(),this._config.isAnimated)}}const Ce=".bs.focustrap",Te=`focusin${Ce}`,ke=`keydown.tab${Ce}`,$e="backward",Se={autofocus:!0,trapElement:null},Le={autofocus:"boolean",trapElement:"element"};class Oe extends q{constructor(t){super(),this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}static get Default(){return Se}static get DefaultType(){return Le}static get NAME(){return"focustrap"}activate(){this._isActive||(this._config.autofocus&&this._config.trapElement.focus(),j.off(document,Ce),j.on(document,Te,(t=>this._handleFocusin(t))),j.on(document,ke,(t=>this._handleKeydown(t))),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,j.off(document,Ce))}_handleFocusin(t){const{trapElement:e}=this._config;if(t.target===document||t.target===e||e.contains(t.target))return;const i=K.focusableChildren(e);0===i.length?e.focus():this._lastTabNavDirection===$e?i[i.length-1].focus():i[0].focus()}_handleKeydown(t){"Tab"===t.key&&(this._lastTabNavDirection=t.shiftKey?$e:"forward")}}const Ie=".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",De=".sticky-top",Ne="padding-right",Pe="margin-right";class xe{constructor(){this._element=document.body}getWidth(){const t=document.documentElement.clientWidth;return Math.abs(window.innerWidth-t)}hide(){const t=this.getWidth();this._disableOverFlow(),this._setElementAttributes(this._element,Ne,(e=>e+t)),this._setElementAttributes(Ie,Ne,(e=>e+t)),this._setElementAttributes(De,Pe,(e=>e-t))}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,Ne),this._resetElementAttributes(Ie,Ne),this._resetElementAttributes(De,Pe)}isOverflowing(){return this.getWidth()>0}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const s=this.getWidth();this._applyManipulationCallback(t,(t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+s)return;this._saveInitialAttribute(t,e);const n=window.getComputedStyle(t).getPropertyValue(e);t.style.setProperty(e,`${i(Number.parseFloat(n))}px`)}))}_saveInitialAttribute(t,e){const i=t.style.getPropertyValue(e);i&&B.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,(t=>{const i=B.getDataAttribute(t,e);null!==i?(B.removeDataAttribute(t,e),t.style.setProperty(e,i)):t.style.removeProperty(e)}))}_applyManipulationCallback(t,e){if(l(t))e(t);else for(const i of K.find(t,this._element))e(i)}}const Me=".bs.modal",je=`hide${Me}`,Fe=`hidePrevented${Me}`,ze=`hidden${Me}`,He=`show${Me}`,Be=`shown${Me}`,qe=`resize${Me}`,We=`click.dismiss${Me}`,Re=`mousedown.dismiss${Me}`,Ke=`keydown.dismiss${Me}`,Ve=`click${Me}.data-api`,Qe="modal-open",Xe="show",Ye="modal-static",Ue={backdrop:!0,focus:!0,keyboard:!0},Ge={backdrop:"(boolean|string)",focus:"boolean",keyboard:"boolean"};class Je extends W{constructor(t,e){super(t,e),this._dialog=K.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._isTransitioning=!1,this._scrollBar=new xe,this._addEventListeners()}static get Default(){return Ue}static get DefaultType(){return Ge}static get NAME(){return"modal"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||j.trigger(this._element,He,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isTransitioning=!0,this._scrollBar.hide(),document.body.classList.add(Qe),this._adjustDialog(),this._backdrop.show((()=>this._showElement(t))))}hide(){this._isShown&&!this._isTransitioning&&(j.trigger(this._element,je).defaultPrevented||(this._isShown=!1,this._isTransitioning=!0,this._focustrap.deactivate(),this._element.classList.remove(Xe),this._queueCallback((()=>this._hideModal()),this._element,this._isAnimated())))}dispose(){j.off(window,Me),j.off(this._dialog,Me),this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new Ee({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new Oe({trapElement:this._element})}_showElement(t){document.body.contains(this._element)||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0;const e=K.findOne(".modal-body",this._dialog);e&&(e.scrollTop=0),g(this._element),this._element.classList.add(Xe),this._queueCallback((()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,j.trigger(this._element,Be,{relatedTarget:t})}),this._dialog,this._isAnimated())}_addEventListeners(){j.on(this._element,Ke,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():this._triggerBackdropTransition())})),j.on(window,qe,(()=>{this._isShown&&!this._isTransitioning&&this._adjustDialog()})),j.on(this._element,Re,(t=>{j.one(this._element,We,(e=>{this._element===t.target&&this._element===e.target&&("static"!==this._config.backdrop?this._config.backdrop&&this.hide():this._triggerBackdropTransition())}))}))}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide((()=>{document.body.classList.remove(Qe),this._resetAdjustments(),this._scrollBar.reset(),j.trigger(this._element,ze)}))}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(j.trigger(this._element,Fe).defaultPrevented)return;const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._element.style.overflowY;"hidden"===e||this._element.classList.contains(Ye)||(t||(this._element.style.overflowY="hidden"),this._element.classList.add(Ye),this._queueCallback((()=>{this._element.classList.remove(Ye),this._queueCallback((()=>{this._element.style.overflowY=e}),this._dialog)}),this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;if(i&&!t){const t=p()?"paddingLeft":"paddingRight";this._element.style[t]=`${e}px`}if(!i&&t){const t=p()?"paddingRight":"paddingLeft";this._element.style[t]=`${e}px`}}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each((function(){const i=Je.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}}))}}j.on(document,Ve,'[data-bs-toggle="modal"]',(function(t){const e=K.getElementFromSelector(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),j.one(e,He,(t=>{t.defaultPrevented||j.one(e,ze,(()=>{h(this)&&this.focus()}))}));const i=K.findOne(".modal.show");i&&Je.getInstance(i).hide(),Je.getOrCreateInstance(e).toggle(this)})),V(Je),b(Je);const Ze=".bs.offcanvas",ti=".data-api",ei=`load${Ze}${ti}`,ii="show",si="showing",ni="hiding",oi=".offcanvas.show",ri=`show${Ze}`,ai=`shown${Ze}`,li=`hide${Ze}`,ci=`hidePrevented${Ze}`,hi=`hidden${Ze}`,di=`resize${Ze}`,ui=`click${Ze}${ti}`,_i=`keydown.dismiss${Ze}`,gi={backdrop:!0,keyboard:!0,scroll:!1},fi={backdrop:"(boolean|string)",keyboard:"boolean",scroll:"boolean"};class mi extends W{constructor(t,e){super(t,e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get Default(){return gi}static get DefaultType(){return fi}static get NAME(){return"offcanvas"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||j.trigger(this._element,ri,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._backdrop.show(),this._config.scroll||(new xe).hide(),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add(si),this._queueCallback((()=>{this._config.scroll&&!this._config.backdrop||this._focustrap.activate(),this._element.classList.add(ii),this._element.classList.remove(si),j.trigger(this._element,ai,{relatedTarget:t})}),this._element,!0))}hide(){this._isShown&&(j.trigger(this._element,li).defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.add(ni),this._backdrop.hide(),this._queueCallback((()=>{this._element.classList.remove(ii,ni),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._config.scroll||(new xe).reset(),j.trigger(this._element,hi)}),this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_initializeBackDrop(){const t=Boolean(this._config.backdrop);return new Ee({className:"offcanvas-backdrop",isVisible:t,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:t?()=>{"static"!==this._config.backdrop?this.hide():j.trigger(this._element,ci)}:null})}_initializeFocusTrap(){return new Oe({trapElement:this._element})}_addEventListeners(){j.on(this._element,_i,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():j.trigger(this._element,ci))}))}static jQueryInterface(t){return this.each((function(){const e=mi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}j.on(document,ui,'[data-bs-toggle="offcanvas"]',(function(t){const e=K.getElementFromSelector(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),d(this))return;j.one(e,hi,(()=>{h(this)&&this.focus()}));const i=K.findOne(oi);i&&i!==e&&mi.getInstance(i).hide(),mi.getOrCreateInstance(e).toggle(this)})),j.on(window,ei,(()=>{for(const t of K.find(oi))mi.getOrCreateInstance(t).show()})),j.on(window,di,(()=>{for(const t of K.find("[aria-modal][class*=show][class*=offcanvas-]"))"fixed"!==getComputedStyle(t).position&&mi.getOrCreateInstance(t).hide()})),V(mi),b(mi);const pi={"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],dd:[],div:[],dl:[],dt:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},bi=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),vi=/^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i,yi=(t,e)=>{const i=t.nodeName.toLowerCase();return e.includes(i)?!bi.has(i)||Boolean(vi.test(t.nodeValue)):e.filter((t=>t instanceof RegExp)).some((t=>t.test(i)))},wi={allowList:pi,content:{},extraClass:"",html:!1,sanitize:!0,sanitizeFn:null,template:"
"},Ai={allowList:"object",content:"object",extraClass:"(string|function)",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",template:"string"},Ei={entry:"(string|element|function|null)",selector:"(string|element)"};class Ci extends q{constructor(t){super(),this._config=this._getConfig(t)}static get Default(){return wi}static get DefaultType(){return Ai}static get NAME(){return"TemplateFactory"}getContent(){return Object.values(this._config.content).map((t=>this._resolvePossibleFunction(t))).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(t){return this._checkContent(t),this._config.content={...this._config.content,...t},this}toHtml(){const t=document.createElement("div");t.innerHTML=this._maybeSanitize(this._config.template);for(const[e,i]of Object.entries(this._config.content))this._setContent(t,i,e);const e=t.children[0],i=this._resolvePossibleFunction(this._config.extraClass);return i&&e.classList.add(...i.split(" ")),e}_typeCheckConfig(t){super._typeCheckConfig(t),this._checkContent(t.content)}_checkContent(t){for(const[e,i]of Object.entries(t))super._typeCheckConfig({selector:e,entry:i},Ei)}_setContent(t,e,i){const s=K.findOne(i,t);s&&((e=this._resolvePossibleFunction(e))?l(e)?this._putElementInTemplate(c(e),s):this._config.html?s.innerHTML=this._maybeSanitize(e):s.textContent=e:s.remove())}_maybeSanitize(t){return this._config.sanitize?function(t,e,i){if(!t.length)return t;if(i&&"function"==typeof i)return i(t);const s=(new window.DOMParser).parseFromString(t,"text/html"),n=[].concat(...s.body.querySelectorAll("*"));for(const t of n){const i=t.nodeName.toLowerCase();if(!Object.keys(e).includes(i)){t.remove();continue}const s=[].concat(...t.attributes),n=[].concat(e["*"]||[],e[i]||[]);for(const e of s)yi(e,n)||t.removeAttribute(e.nodeName)}return s.body.innerHTML}(t,this._config.allowList,this._config.sanitizeFn):t}_resolvePossibleFunction(t){return v(t,[void 0,this])}_putElementInTemplate(t,e){if(this._config.html)return e.innerHTML="",void e.append(t);e.textContent=t.textContent}}const Ti=new Set(["sanitize","allowList","sanitizeFn"]),ki="fade",$i="show",Si=".tooltip-inner",Li=".modal",Oi="hide.bs.modal",Ii="hover",Di="focus",Ni={AUTO:"auto",TOP:"top",RIGHT:p()?"left":"right",BOTTOM:"bottom",LEFT:p()?"right":"left"},Pi={allowList:pi,animation:!0,boundary:"clippingParents",container:!1,customClass:"",delay:0,fallbackPlacements:["top","right","bottom","left"],html:!1,offset:[0,6],placement:"top",popperConfig:null,sanitize:!0,sanitizeFn:null,selector:!1,template:'',title:"",trigger:"hover focus"},xi={allowList:"object",animation:"boolean",boundary:"(string|element)",container:"(string|element|boolean)",customClass:"(string|function)",delay:"(number|object)",fallbackPlacements:"array",html:"boolean",offset:"(array|string|function)",placement:"(string|function)",popperConfig:"(null|object|function)",sanitize:"boolean",sanitizeFn:"(null|function)",selector:"(string|boolean)",template:"string",title:"(string|element|function)",trigger:"string"};class Mi extends W{constructor(t,e){if(void 0===i)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org/docs/v2/)");super(t,e),this._isEnabled=!0,this._timeout=0,this._isHovered=null,this._activeTrigger={},this._popper=null,this._templateFactory=null,this._newContent=null,this.tip=null,this._setListeners(),this._config.selector||this._fixTitle()}static get Default(){return Pi}static get DefaultType(){return xi}static get NAME(){return"tooltip"}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(){this._isEnabled&&(this._isShown()?this._leave():this._enter())}dispose(){clearTimeout(this._timeout),j.off(this._element.closest(Li),Oi,this._hideModalHandler),this._element.getAttribute("data-bs-original-title")&&this._element.setAttribute("title",this._element.getAttribute("data-bs-original-title")),this._disposePopper(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this._isWithContent()||!this._isEnabled)return;const t=j.trigger(this._element,this.constructor.eventName("show")),e=(u(this._element)||this._element.ownerDocument.documentElement).contains(this._element);if(t.defaultPrevented||!e)return;this._disposePopper();const i=this._getTipElement();this._element.setAttribute("aria-describedby",i.getAttribute("id"));const{container:s}=this._config;if(this._element.ownerDocument.documentElement.contains(this.tip)||(s.append(i),j.trigger(this._element,this.constructor.eventName("inserted"))),this._popper=this._createPopper(i),i.classList.add($i),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))j.on(t,"mouseover",_);this._queueCallback((()=>{j.trigger(this._element,this.constructor.eventName("shown")),!1===this._isHovered&&this._leave(),this._isHovered=!1}),this.tip,this._isAnimated())}hide(){if(this._isShown()&&!j.trigger(this._element,this.constructor.eventName("hide")).defaultPrevented){if(this._getTipElement().classList.remove($i),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))j.off(t,"mouseover",_);this._activeTrigger.click=!1,this._activeTrigger[Di]=!1,this._activeTrigger[Ii]=!1,this._isHovered=null,this._queueCallback((()=>{this._isWithActiveTrigger()||(this._isHovered||this._disposePopper(),this._element.removeAttribute("aria-describedby"),j.trigger(this._element,this.constructor.eventName("hidden")))}),this.tip,this._isAnimated())}}update(){this._popper&&this._popper.update()}_isWithContent(){return Boolean(this._getTitle())}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(t){const e=this._getTemplateFactory(t).toHtml();if(!e)return null;e.classList.remove(ki,$i),e.classList.add(`bs-${this.constructor.NAME}-auto`);const i=(t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t})(this.constructor.NAME).toString();return e.setAttribute("id",i),this._isAnimated()&&e.classList.add(ki),e}setContent(t){this._newContent=t,this._isShown()&&(this._disposePopper(),this.show())}_getTemplateFactory(t){return this._templateFactory?this._templateFactory.changeContent(t):this._templateFactory=new Ci({...this._config,content:t,extraClass:this._resolvePossibleFunction(this._config.customClass)}),this._templateFactory}_getContentForTemplate(){return{[Si]:this._getTitle()}}_getTitle(){return this._resolvePossibleFunction(this._config.title)||this._element.getAttribute("data-bs-original-title")}_initializeOnDelegatedTarget(t){return this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains(ki)}_isShown(){return this.tip&&this.tip.classList.contains($i)}_createPopper(t){const e=v(this._config.placement,[this,t,this._element]),s=Ni[e.toUpperCase()];return i.createPopper(this._element,t,this._getPopperConfig(s))}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_resolvePossibleFunction(t){return v(t,[this._element,this._element])}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"preSetPlacement",enabled:!0,phase:"beforeMain",fn:t=>{this._getTipElement().setAttribute("data-popper-placement",t.state.placement)}}]};return{...e,...v(this._config.popperConfig,[void 0,e])}}_setListeners(){const t=this._config.trigger.split(" ");for(const e of t)if("click"===e)j.on(this._element,this.constructor.eventName("click"),this._config.selector,(t=>{this._initializeOnDelegatedTarget(t).toggle()}));else if("manual"!==e){const t=e===Ii?this.constructor.eventName("mouseenter"):this.constructor.eventName("focusin"),i=e===Ii?this.constructor.eventName("mouseleave"):this.constructor.eventName("focusout");j.on(this._element,t,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusin"===t.type?Di:Ii]=!0,e._enter()})),j.on(this._element,i,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusout"===t.type?Di:Ii]=e._element.contains(t.relatedTarget),e._leave()}))}this._hideModalHandler=()=>{this._element&&this.hide()},j.on(this._element.closest(Li),Oi,this._hideModalHandler)}_fixTitle(){const t=this._element.getAttribute("title");t&&(this._element.getAttribute("aria-label")||this._element.textContent.trim()||this._element.setAttribute("aria-label",t),this._element.setAttribute("data-bs-original-title",t),this._element.removeAttribute("title"))}_enter(){this._isShown()||this._isHovered?this._isHovered=!0:(this._isHovered=!0,this._setTimeout((()=>{this._isHovered&&this.show()}),this._config.delay.show))}_leave(){this._isWithActiveTrigger()||(this._isHovered=!1,this._setTimeout((()=>{this._isHovered||this.hide()}),this._config.delay.hide))}_setTimeout(t,e){clearTimeout(this._timeout),this._timeout=setTimeout(t,e)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(t){const e=B.getDataAttributes(this._element);for(const t of Object.keys(e))Ti.has(t)&&delete e[t];return t={...e,..."object"==typeof t&&t?t:{}},t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t.container=!1===t.container?document.body:c(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),t}_getDelegateConfig(){const t={};for(const[e,i]of Object.entries(this._config))this.constructor.Default[e]!==i&&(t[e]=i);return t.selector=!1,t.trigger="manual",t}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null),this.tip&&(this.tip.remove(),this.tip=null)}static jQueryInterface(t){return this.each((function(){const e=Mi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}b(Mi);const ji=".popover-header",Fi=".popover-body",zi={...Mi.Default,content:"",offset:[0,8],placement:"right",template:'',trigger:"click"},Hi={...Mi.DefaultType,content:"(null|string|element|function)"};class Bi extends Mi{static get Default(){return zi}static get DefaultType(){return Hi}static get NAME(){return"popover"}_isWithContent(){return this._getTitle()||this._getContent()}_getContentForTemplate(){return{[ji]:this._getTitle(),[Fi]:this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}static jQueryInterface(t){return this.each((function(){const e=Bi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}b(Bi);const qi=".bs.scrollspy",Wi=`activate${qi}`,Ri=`click${qi}`,Ki=`load${qi}.data-api`,Vi="active",Qi="[href]",Xi=".nav-link",Yi=`${Xi}, .nav-item > ${Xi}, .list-group-item`,Ui={offset:null,rootMargin:"0px 0px -25%",smoothScroll:!1,target:null,threshold:[.1,.5,1]},Gi={offset:"(number|null)",rootMargin:"string",smoothScroll:"boolean",target:"element",threshold:"array"};class Ji extends W{constructor(t,e){super(t,e),this._targetLinks=new Map,this._observableSections=new Map,this._rootElement="visible"===getComputedStyle(this._element).overflowY?null:this._element,this._activeTarget=null,this._observer=null,this._previousScrollData={visibleEntryTop:0,parentScrollTop:0},this.refresh()}static get Default(){return Ui}static get DefaultType(){return Gi}static get NAME(){return"scrollspy"}refresh(){this._initializeTargetsAndObservables(),this._maybeEnableSmoothScroll(),this._observer?this._observer.disconnect():this._observer=this._getNewObserver();for(const t of this._observableSections.values())this._observer.observe(t)}dispose(){this._observer.disconnect(),super.dispose()}_configAfterMerge(t){return t.target=c(t.target)||document.body,t.rootMargin=t.offset?`${t.offset}px 0px -30%`:t.rootMargin,"string"==typeof t.threshold&&(t.threshold=t.threshold.split(",").map((t=>Number.parseFloat(t)))),t}_maybeEnableSmoothScroll(){this._config.smoothScroll&&(j.off(this._config.target,Ri),j.on(this._config.target,Ri,Qi,(t=>{const e=this._observableSections.get(t.target.hash);if(e){t.preventDefault();const i=this._rootElement||window,s=e.offsetTop-this._element.offsetTop;if(i.scrollTo)return void i.scrollTo({top:s,behavior:"smooth"});i.scrollTop=s}})))}_getNewObserver(){const t={root:this._rootElement,threshold:this._config.threshold,rootMargin:this._config.rootMargin};return new IntersectionObserver((t=>this._observerCallback(t)),t)}_observerCallback(t){const e=t=>this._targetLinks.get(`#${t.target.id}`),i=t=>{this._previousScrollData.visibleEntryTop=t.target.offsetTop,this._process(e(t))},s=(this._rootElement||document.documentElement).scrollTop,n=s>=this._previousScrollData.parentScrollTop;this._previousScrollData.parentScrollTop=s;for(const o of t){if(!o.isIntersecting){this._activeTarget=null,this._clearActiveClass(e(o));continue}const t=o.target.offsetTop>=this._previousScrollData.visibleEntryTop;if(n&&t){if(i(o),!s)return}else n||t||i(o)}}_initializeTargetsAndObservables(){this._targetLinks=new Map,this._observableSections=new Map;const t=K.find(Qi,this._config.target);for(const e of t){if(!e.hash||d(e))continue;const t=K.findOne(decodeURI(e.hash),this._element);h(t)&&(this._targetLinks.set(decodeURI(e.hash),e),this._observableSections.set(e.hash,t))}}_process(t){this._activeTarget!==t&&(this._clearActiveClass(this._config.target),this._activeTarget=t,t.classList.add(Vi),this._activateParents(t),j.trigger(this._element,Wi,{relatedTarget:t}))}_activateParents(t){if(t.classList.contains("dropdown-item"))K.findOne(".dropdown-toggle",t.closest(".dropdown")).classList.add(Vi);else for(const e of K.parents(t,".nav, .list-group"))for(const t of K.prev(e,Yi))t.classList.add(Vi)}_clearActiveClass(t){t.classList.remove(Vi);const e=K.find(`${Qi}.${Vi}`,t);for(const t of e)t.classList.remove(Vi)}static jQueryInterface(t){return this.each((function(){const e=Ji.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}j.on(window,Ki,(()=>{for(const t of K.find('[data-bs-spy="scroll"]'))Ji.getOrCreateInstance(t)})),b(Ji);const Zi=".bs.tab",ts=`hide${Zi}`,es=`hidden${Zi}`,is=`show${Zi}`,ss=`shown${Zi}`,ns=`click${Zi}`,os=`keydown${Zi}`,rs=`load${Zi}`,as="ArrowLeft",ls="ArrowRight",cs="ArrowUp",hs="ArrowDown",ds="Home",us="End",_s="active",gs="fade",fs="show",ms=".dropdown-toggle",ps=`:not(${ms})`,bs='[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',vs=`.nav-link${ps}, .list-group-item${ps}, [role="tab"]${ps}, ${bs}`,ys=`.${_s}[data-bs-toggle="tab"], .${_s}[data-bs-toggle="pill"], .${_s}[data-bs-toggle="list"]`;class ws extends W{constructor(t){super(t),this._parent=this._element.closest('.list-group, .nav, [role="tablist"]'),this._parent&&(this._setInitialAttributes(this._parent,this._getChildren()),j.on(this._element,os,(t=>this._keydown(t))))}static get NAME(){return"tab"}show(){const t=this._element;if(this._elemIsActive(t))return;const e=this._getActiveElem(),i=e?j.trigger(e,ts,{relatedTarget:t}):null;j.trigger(t,is,{relatedTarget:e}).defaultPrevented||i&&i.defaultPrevented||(this._deactivate(e,t),this._activate(t,e))}_activate(t,e){t&&(t.classList.add(_s),this._activate(K.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.removeAttribute("tabindex"),t.setAttribute("aria-selected",!0),this._toggleDropDown(t,!0),j.trigger(t,ss,{relatedTarget:e})):t.classList.add(fs)}),t,t.classList.contains(gs)))}_deactivate(t,e){t&&(t.classList.remove(_s),t.blur(),this._deactivate(K.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.setAttribute("aria-selected",!1),t.setAttribute("tabindex","-1"),this._toggleDropDown(t,!1),j.trigger(t,es,{relatedTarget:e})):t.classList.remove(fs)}),t,t.classList.contains(gs)))}_keydown(t){if(![as,ls,cs,hs,ds,us].includes(t.key))return;t.stopPropagation(),t.preventDefault();const e=this._getChildren().filter((t=>!d(t)));let i;if([ds,us].includes(t.key))i=e[t.key===ds?0:e.length-1];else{const s=[ls,hs].includes(t.key);i=w(e,t.target,s,!0)}i&&(i.focus({preventScroll:!0}),ws.getOrCreateInstance(i).show())}_getChildren(){return K.find(vs,this._parent)}_getActiveElem(){return this._getChildren().find((t=>this._elemIsActive(t)))||null}_setInitialAttributes(t,e){this._setAttributeIfNotExists(t,"role","tablist");for(const t of e)this._setInitialAttributesOnChild(t)}_setInitialAttributesOnChild(t){t=this._getInnerElement(t);const e=this._elemIsActive(t),i=this._getOuterElement(t);t.setAttribute("aria-selected",e),i!==t&&this._setAttributeIfNotExists(i,"role","presentation"),e||t.setAttribute("tabindex","-1"),this._setAttributeIfNotExists(t,"role","tab"),this._setInitialAttributesOnTargetPanel(t)}_setInitialAttributesOnTargetPanel(t){const e=K.getElementFromSelector(t);e&&(this._setAttributeIfNotExists(e,"role","tabpanel"),t.id&&this._setAttributeIfNotExists(e,"aria-labelledby",`${t.id}`))}_toggleDropDown(t,e){const i=this._getOuterElement(t);if(!i.classList.contains("dropdown"))return;const s=(t,s)=>{const n=K.findOne(t,i);n&&n.classList.toggle(s,e)};s(ms,_s),s(".dropdown-menu",fs),i.setAttribute("aria-expanded",e)}_setAttributeIfNotExists(t,e,i){t.hasAttribute(e)||t.setAttribute(e,i)}_elemIsActive(t){return t.classList.contains(_s)}_getInnerElement(t){return t.matches(vs)?t:K.findOne(vs,t)}_getOuterElement(t){return t.closest(".nav-item, .list-group-item")||t}static jQueryInterface(t){return this.each((function(){const e=ws.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}j.on(document,ns,bs,(function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),d(this)||ws.getOrCreateInstance(this).show()})),j.on(window,rs,(()=>{for(const t of K.find(ys))ws.getOrCreateInstance(t)})),b(ws);const As=".bs.toast",Es=`mouseover${As}`,Cs=`mouseout${As}`,Ts=`focusin${As}`,ks=`focusout${As}`,$s=`hide${As}`,Ss=`hidden${As}`,Ls=`show${As}`,Os=`shown${As}`,Is="hide",Ds="show",Ns="showing",Ps={animation:"boolean",autohide:"boolean",delay:"number"},xs={animation:!0,autohide:!0,delay:5e3};class Ms extends W{constructor(t,e){super(t,e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get Default(){return xs}static get DefaultType(){return Ps}static get NAME(){return"toast"}show(){j.trigger(this._element,Ls).defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove(Is),g(this._element),this._element.classList.add(Ds,Ns),this._queueCallback((()=>{this._element.classList.remove(Ns),j.trigger(this._element,Os),this._maybeScheduleHide()}),this._element,this._config.animation))}hide(){this.isShown()&&(j.trigger(this._element,$s).defaultPrevented||(this._element.classList.add(Ns),this._queueCallback((()=>{this._element.classList.add(Is),this._element.classList.remove(Ns,Ds),j.trigger(this._element,Ss)}),this._element,this._config.animation)))}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove(Ds),super.dispose()}isShown(){return this._element.classList.contains(Ds)}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout((()=>{this.hide()}),this._config.delay)))}_onInteraction(t,e){switch(t.type){case"mouseover":case"mouseout":this._hasMouseInteraction=e;break;case"focusin":case"focusout":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){j.on(this._element,Es,(t=>this._onInteraction(t,!0))),j.on(this._element,Cs,(t=>this._onInteraction(t,!1))),j.on(this._element,Ts,(t=>this._onInteraction(t,!0))),j.on(this._element,ks,(t=>this._onInteraction(t,!1)))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){const e=Ms.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}return V(Ms),b(Ms),{Alert:U,Button:J,Carousel:Dt,Collapse:Vt,Dropdown:pe,Modal:Je,Offcanvas:mi,Popover:Bi,ScrollSpy:Ji,Tab:ws,Toast:Ms,Tooltip:Mi}})); 7 | //# sourceMappingURL=bootstrap.min.js.map --------------------------------------------------------------------------------