Initial commit
This commit is contained in:
commit
c0b4ca1021
21 changed files with 677 additions and 0 deletions
23
app/__init__.py
Normal file
23
app/__init__.py
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
from flask import Flask
|
||||||
|
from config import Config
|
||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
from flask_migrate import Migrate
|
||||||
|
from flask_login import LoginManager
|
||||||
|
from flask import request
|
||||||
|
from flask_babel import Babel
|
||||||
|
from flask_babel import lazy_gettext as _l
|
||||||
|
|
||||||
|
def get_locale():
|
||||||
|
return request.accept_languages.best_match(app.config['LANGUAGES'])
|
||||||
|
app = Flask(__name__)
|
||||||
|
babel = Babel(app, locale_selector=get_locale)
|
||||||
|
app.config.from_object(Config)
|
||||||
|
db = SQLAlchemy(app)
|
||||||
|
migrate = Migrate(app, db)
|
||||||
|
login = LoginManager(app)
|
||||||
|
login.login_view = 'login'
|
||||||
|
login.login_message = _l('Please log in to access this page.')
|
||||||
|
from app import routes, models
|
||||||
|
|
||||||
|
from app.commands import seed_settings_command
|
||||||
|
app.cli.add_command(seed_settings_command)
|
||||||
49
app/commands.py
Normal file
49
app/commands.py
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
import click
|
||||||
|
from app import db
|
||||||
|
from flask import current_app
|
||||||
|
from app.models import Setting, User
|
||||||
|
|
||||||
|
|
||||||
|
def seed_defaults():
|
||||||
|
defaults = {
|
||||||
|
"butterbox_name": current_app.config["BUTTERBOX_NAME"],
|
||||||
|
"butterbox_logo": current_app.config["BUTTERBOX_LOGO"],
|
||||||
|
"ssid": current_app.config["BUTTERBOX_SSID"],
|
||||||
|
"wifi_password": current_app.config["BUTTERBOX_WIFI_PASSWORD"],
|
||||||
|
"disable_access_point": current_app.config["DISABLE_ACCESS_POINT"],
|
||||||
|
"apply_changes": "false",
|
||||||
|
"onboarding_complete": "false",
|
||||||
|
"lock_root_password": "false",
|
||||||
|
"disable_file_viewer": current_app.config["DISABLE_FILE_VIEWER"],
|
||||||
|
"disable_map_viewer": current_app.config["DISABLE_MAP_VIEWER"],
|
||||||
|
"disable_chat": current_app.config["DISABLE_CHAT"],
|
||||||
|
"disable_app_store": current_app.config["DISABLE_APP_STORE"],
|
||||||
|
"ssh_password": "",
|
||||||
|
"admin_password": current_app.config["ADMIN_PASSWORD"],
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value in defaults.items():
|
||||||
|
exists = Setting.query.filter_by(key=key).first()
|
||||||
|
if not exists:
|
||||||
|
db.session.add(Setting(key=key, value=value))
|
||||||
|
click.echo("Created new setting {}".format(key))
|
||||||
|
else:
|
||||||
|
click.echo("Found existing setting {}".format(key))
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
admin_user_exists = User.query.filter_by(username='admin').first()
|
||||||
|
if not admin_user_exists:
|
||||||
|
u = User(username='admin')
|
||||||
|
u.set_password('admin')
|
||||||
|
db.session.add(u)
|
||||||
|
db.session.commit()
|
||||||
|
click.echo("Created new admin user")
|
||||||
|
else:
|
||||||
|
click.echo("Found existing admin user")
|
||||||
|
|
||||||
|
|
||||||
|
@click.command("seed-settings")
|
||||||
|
def seed_settings_command():
|
||||||
|
"""Seed default settings into the database (only if missing)."""
|
||||||
|
seed_defaults()
|
||||||
|
click.echo("Finished seeding default settings.")
|
||||||
32
app/forms.py
Normal file
32
app/forms.py
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
from flask_wtf import FlaskForm
|
||||||
|
from flask_wtf.file import FileAllowed, FileRequired
|
||||||
|
from wtforms import StringField, PasswordField, SubmitField, BooleanField, FileField
|
||||||
|
from wtforms.validators import DataRequired
|
||||||
|
from flask_babel import lazy_gettext as _l
|
||||||
|
|
||||||
|
class LoginForm(FlaskForm):
|
||||||
|
username = StringField(_l('Username'), validators=[DataRequired()])
|
||||||
|
password = PasswordField(_l('Password'), validators=[DataRequired()])
|
||||||
|
submit = SubmitField(_l('Sign In'))
|
||||||
|
remember_me = BooleanField('Remember Me')
|
||||||
|
|
||||||
|
|
||||||
|
class SettingsForm(FlaskForm):
|
||||||
|
# Access point settings
|
||||||
|
ssid = StringField('SSID', validators=[DataRequired()])
|
||||||
|
wifi_password = PasswordField(_l('WiFi Password'))
|
||||||
|
disable_access_point = BooleanField(_l('Disable Access Point'))
|
||||||
|
# Customisation settings
|
||||||
|
butterbox_name = StringField(_l('Butterbox Name'), validators=[DataRequired()])
|
||||||
|
butterbox_logo = FileField((_l('Butterbox Logo')), validators=[FileAllowed(['jpg', 'png', 'svg'], 'Images only!')])
|
||||||
|
# Services settings
|
||||||
|
disable_file_viewer = BooleanField(_l('Disable File Viewer'))
|
||||||
|
disable_map_viewer = BooleanField(_l('Disable Map Viewer'))
|
||||||
|
disable_chat = BooleanField(_l('Disable Chat'))
|
||||||
|
disable_app_store = BooleanField(_l('Disable App Store'))
|
||||||
|
# Access Settings
|
||||||
|
admin_password = PasswordField(_l('Admin Password'))
|
||||||
|
ssh_password = PasswordField(_l('SSH Password'))
|
||||||
|
|
||||||
|
submit = SubmitField(_l('Submit'))
|
||||||
|
apply_changes = SubmitField(_l('Apply Changes'))
|
||||||
35
app/models.py
Normal file
35
app/models.py
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
from typing import Optional
|
||||||
|
import sqlalchemy as sa
|
||||||
|
import sqlalchemy.orm as so
|
||||||
|
from app import db
|
||||||
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
|
from flask_login import UserMixin
|
||||||
|
from app import login
|
||||||
|
|
||||||
|
class User(UserMixin, db.Model):
|
||||||
|
id: so.Mapped[int] = so.mapped_column(primary_key=True)
|
||||||
|
username: so.Mapped[str] = so.mapped_column(sa.String(64), index=True,
|
||||||
|
unique=True)
|
||||||
|
password_hash: so.Mapped[Optional[str]] = so.mapped_column(sa.String(256))
|
||||||
|
|
||||||
|
def set_password(self, password: str) -> None:
|
||||||
|
self.password_hash = generate_password_hash(password)
|
||||||
|
|
||||||
|
def check_password(self, password: str) -> bool:
|
||||||
|
return check_password_hash(self.password_hash, password)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '<User {}>'.format(self.username)
|
||||||
|
|
||||||
|
class Setting(db.Model):
|
||||||
|
id: so.Mapped[int] = so.mapped_column(primary_key=True)
|
||||||
|
key: so.Mapped[str] = so.mapped_column(sa.String(255), index=True,
|
||||||
|
unique=True, nullable=False)
|
||||||
|
value: so.Mapped[str] = so.mapped_column(sa.String(255))
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '<Setting {}>'.format(self.key)
|
||||||
|
|
||||||
|
@login.user_loader
|
||||||
|
def load_user(user_id: str) -> User:
|
||||||
|
return db.session.get(User, int(user_id))
|
||||||
140
app/routes.py
Normal file
140
app/routes.py
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
from email.mime import image
|
||||||
|
|
||||||
|
from alembic.util import obfuscate_url_pw
|
||||||
|
|
||||||
|
from app import app
|
||||||
|
from flask import render_template, flash, redirect, url_for, request, session
|
||||||
|
from app.forms import LoginForm, SettingsForm
|
||||||
|
from flask_login import login_user, current_user, logout_user, login_required
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from app import db
|
||||||
|
from app.models import User, Setting
|
||||||
|
from werkzeug.datastructures import FileStorage
|
||||||
|
import json
|
||||||
|
from flask_babel import _
|
||||||
|
import base64
|
||||||
|
|
||||||
|
def get_setting(name) -> str:
|
||||||
|
setting = db.session.scalar(sa.select(Setting).where(Setting.key == name))
|
||||||
|
return str(setting.value)
|
||||||
|
|
||||||
|
def set_setting(name: str, value: str):
|
||||||
|
setting = db.session.scalar(sa.select(Setting).where(Setting.key == name))
|
||||||
|
print(f"I have changed {setting.key}")
|
||||||
|
setting.value = value
|
||||||
|
db.session.add(setting)
|
||||||
|
|
||||||
|
def dump_settings(filename: str) -> None:
|
||||||
|
settings = db.session.execute(sa.select(Setting)).scalars().all()
|
||||||
|
settings_dict = {s.key: s.value for s in settings}
|
||||||
|
print(settings_dict)
|
||||||
|
with open(filename, "w") as f:
|
||||||
|
json.dump(settings_dict, f, indent=4)
|
||||||
|
@app.route('/')
|
||||||
|
@app.route('/index')
|
||||||
|
def index():
|
||||||
|
disable_chat = get_setting("disable_chat")
|
||||||
|
disable_app_store = get_setting("disable_app_store")
|
||||||
|
disable_map_viewer = get_setting("disable_map_viewer")
|
||||||
|
disable_file_viewer = get_setting("disable_file_viewer")
|
||||||
|
service_array = []
|
||||||
|
usb_inserted = False # actual test of whether USB is inserted
|
||||||
|
usb_has_maps = False # actual test of whether USB has maps folder
|
||||||
|
usb_has_appstore = False # actual test of whether USB has an appstore
|
||||||
|
if disable_chat == 'false':
|
||||||
|
service_array.append({"name": "Message Board", "image": url_for("static", filename="images/chat-icon.png"), "url": app.config["CONVENE_INSTALL_PATH"] })
|
||||||
|
if disable_app_store == 'false' and usb_has_appstore:
|
||||||
|
service_array.append({"name": "Apps", "image": url_for("static", filename="images/appstore-icon.svg")})
|
||||||
|
if disable_map_viewer == 'false' and usb_has_maps:
|
||||||
|
service_array.append({"name": "Offline Maps", "image": url_for("static", filename="images/maps-icon.png")})
|
||||||
|
if disable_file_viewer == 'false':
|
||||||
|
name = "Files"
|
||||||
|
if not usb_inserted:
|
||||||
|
name = "Insert USB to browse files"
|
||||||
|
service_array.append({
|
||||||
|
"name": name,
|
||||||
|
"image": url_for("static", filename="images/explore-icon.svg"),
|
||||||
|
"url": url_for("usb")})
|
||||||
|
return render_template('index.html', title='Home', get_setting=get_setting, services=service_array)
|
||||||
|
|
||||||
|
@app.route('/usb')
|
||||||
|
def usb():
|
||||||
|
return render_template('usb-file-viewer.html', title='File Viewer')
|
||||||
|
|
||||||
|
@app.route('/login', methods=['GET', 'POST'])
|
||||||
|
def login():
|
||||||
|
if current_user.is_authenticated:
|
||||||
|
return redirect(url_for('admin'))
|
||||||
|
form = LoginForm()
|
||||||
|
form.remember_me.data = False
|
||||||
|
if form.validate_on_submit():
|
||||||
|
user = db.session.scalar(sa.select(User).where(User.username == form.username.data))
|
||||||
|
if user is None or not user.check_password(form.password.data):
|
||||||
|
flash(_('Invalid username or password'))
|
||||||
|
return redirect(url_for('login'))
|
||||||
|
login_user(user)
|
||||||
|
return redirect(url_for('admin'))
|
||||||
|
return render_template('login.html', title='Sign in', form=form, get_setting=get_setting)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/logout')
|
||||||
|
def logout():
|
||||||
|
logout_user()
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
@app.route('/admin', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
def admin():
|
||||||
|
form = SettingsForm()
|
||||||
|
populate_settings = ['butterbox_name', 'wifi_password', 'disable_access_point', 'ssid', 'disable_file_viewer', 'disable_map_viewer', 'disable_app_store', 'disable_chat' ]
|
||||||
|
|
||||||
|
bool_settings = ['disable_access_point','disable_file_viewer', 'disable_map_viewer', 'disable_app_store', 'disable_chat']
|
||||||
|
|
||||||
|
if not form.is_submitted():
|
||||||
|
for s in populate_settings:
|
||||||
|
if s in bool_settings:
|
||||||
|
getattr(form, s).data = (get_setting(s) == "true")
|
||||||
|
else:
|
||||||
|
getattr(form, s).data = get_setting(s)
|
||||||
|
|
||||||
|
if form.validate_on_submit():
|
||||||
|
if form.submit.data:
|
||||||
|
for s in populate_settings:
|
||||||
|
new_value = getattr(form, s).data
|
||||||
|
if s in bool_settings:
|
||||||
|
new_value = str(new_value).lower() # all settings are str fow now
|
||||||
|
existing_value = get_setting(s)
|
||||||
|
if new_value != existing_value:
|
||||||
|
print(f"New value was changed for {s}. Existing value was {existing_value}, new value was {new_value}")
|
||||||
|
set_setting(s, new_value)
|
||||||
|
if s in ['butterbox_name', 'wifi_password', 'ssid', 'disable_access_point']:
|
||||||
|
app.config['SETTINGS_CHANGED'] = True
|
||||||
|
|
||||||
|
new_logo = form.butterbox_logo.data
|
||||||
|
if new_logo.filename:
|
||||||
|
logo_stream = form.butterbox_logo.data.stream
|
||||||
|
b64_logo = base64.b64encode(logo_stream.read()).decode('utf-8')
|
||||||
|
file_mimetype = form.butterbox_logo.data.mimetype
|
||||||
|
new_value = f"data:{file_mimetype};base64,{b64_logo}"
|
||||||
|
existing_value = get_setting('butterbox_logo')
|
||||||
|
if new_value != existing_value:
|
||||||
|
print( f"New value was changed for logo")
|
||||||
|
set_setting('butterbox_logo', new_value)
|
||||||
|
new_admin_password = form.admin_password.data
|
||||||
|
if new_admin_password:
|
||||||
|
existing_admin_password = get_setting('admin_password')
|
||||||
|
if new_admin_password != existing_admin_password:
|
||||||
|
print( f"New value was changed for admin password")
|
||||||
|
set_setting('admin_password', new_admin_password)
|
||||||
|
print(get_setting('admin_password'))
|
||||||
|
|
||||||
|
if app.config['SETTINGS_CHANGED']:
|
||||||
|
flash(_("⚠️ Some settings won't take effect until the Butter Box restarts. Click 'Apply Changes' to restart."))
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
if form.apply_changes.data:
|
||||||
|
set_setting('apply_changes', "true")
|
||||||
|
dump_settings("settings.txt")
|
||||||
|
flash(_("⚠️ Changes applied! Please wait for the box to restart."))
|
||||||
|
|
||||||
|
|
||||||
|
return render_template('admin.html', get_setting=get_setting, form=form)
|
||||||
21
app/static/butter_styles.css
Normal file
21
app/static/butter_styles.css
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
.butter-title {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.butter-service {
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.butter-service__image {
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.butter-service__content {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
html {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
app/static/images/appstore-icon.svg
Normal file
15
app/static/images/appstore-icon.svg
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M11.0003 10.4969H23.0011C24.3819 10.4969 25.5014 11.6163 25.5014 12.9971V24.998C25.5014 26.3788 24.3819 27.4983 23.0011 27.4983H11.0003C9.61944 27.4983 8.5 26.3789 8.5 24.998V12.9971C8.5 11.6163 9.61947 10.4969 11.0003 10.4969Z"
|
||||||
|
fill="white" stroke="black" stroke-width="3" />
|
||||||
|
<path
|
||||||
|
d="M42.7053 9.20295C42.7056 9.2032 42.7058 9.20344 42.7061 9.20368L50.7965 17.2921C50.7968 17.2924 50.7971 17.2927 50.7974 17.293C51.2482 17.7461 51.5014 18.3593 51.5014 18.9986C51.5014 19.6377 51.2483 20.2509 50.7974 20.7042C50.7971 20.7046 50.7967 20.705 50.7963 20.7053L42.7061 28.7936C42.7057 28.7939 42.7054 28.7942 42.7051 28.7946C42.2526 29.2447 41.6406 29.4973 41.0023 29.4973C40.364 29.4973 39.7519 29.2446 39.2995 28.7944C39.2992 28.7942 39.2989 28.7939 39.2986 28.7936L31.2086 20.7036C31.2083 20.7032 31.2079 20.7029 31.2075 20.7025C30.7571 20.2494 30.5042 19.6366 30.5042 18.9976C30.5042 18.3587 30.757 17.7459 31.2074 17.2929C31.2078 17.2925 31.2082 17.2921 31.2086 17.2917L39.2986 9.20368C39.2989 9.20339 39.2992 9.2031 39.2995 9.2028C39.752 8.75265 40.364 8.5 41.0023 8.5C41.6408 8.5 42.2529 8.7527 42.7053 9.20295Z"
|
||||||
|
fill="#FFDF3F" stroke="black" stroke-width="3" />
|
||||||
|
<path
|
||||||
|
d="M35.002 34.4986H47.0028C48.3837 34.4986 49.5031 35.618 49.5031 36.9989V48.9997C49.5031 50.3806 48.3837 51.5 47.0028 51.5H35.002C33.6211 51.5 32.5017 50.3806 32.5017 48.9997V36.9989C32.5017 35.618 33.621 34.4986 35.002 34.4986Z"
|
||||||
|
fill="white" stroke="black" stroke-width="3" />
|
||||||
|
<path
|
||||||
|
d="M11.0003 34.4986H23.0011C24.382 34.4986 25.5014 35.618 25.5014 36.9989V48.9997C25.5014 50.3805 24.3819 51.5 23.0011 51.5H11.0003C9.61947 51.5 8.5 50.3805 8.5 48.9997V36.9989C8.5 35.618 9.61944 34.4986 11.0003 34.4986Z"
|
||||||
|
fill="white" stroke="black" stroke-width="3" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
BIN
app/static/images/chat-icon.png
Normal file
BIN
app/static/images/chat-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.4 KiB |
5
app/static/images/explore-icon.svg
Normal file
5
app/static/images/explore-icon.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M17.0001 10.5H42C43.3807 10.5 44.5 11.6193 44.5 13V24C44.5 25.3807 43.3807 26.5 42 26.5H17.0001C15.6201 26.5 14.5001 25.38 14.5001 23.9987L14.5001 12.9961C14.5001 11.6173 15.6175 10.5 17.0001 10.5Z" fill="#FFDF3F" stroke="black" stroke-width="3"/>
|
||||||
|
<path d="M8.52257 19.9388V19.9375C8.52257 18.3397 9.84533 17 11.5142 17H24.4417L28.5887 21.0866L29.0266 21.5182H29.6415H47.6132C49.2716 21.5182 50.5 22.7668 50.5 24.375V46.5442C50.5 48.2031 49.2358 49.5 47.6274 49.5H11.5142C9.83565 49.5 8.50029 48.1509 8.5 46.563C8.5 46.5628 8.5 46.5627 8.5 46.5625L8.52257 19.9388Z" fill="white" stroke="black" stroke-width="3"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 741 B |
BIN
app/static/images/maps-icon.png
Normal file
BIN
app/static/images/maps-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 59 KiB |
64
app/templates/admin.html
Normal file
64
app/templates/admin.html
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>{{ _('Application Settings') }}</h1>
|
||||||
|
{% import "bulma_wtf.html" as wtf %}
|
||||||
|
<form action="" method="post" enctype="multipart/form-data" novalidate >
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
{% if config['SETTINGS_CHANGED'] %}
|
||||||
|
<p>{{ form.apply_changes(class="button is-warning") }}</p>
|
||||||
|
{% endif %}
|
||||||
|
<div class="field">
|
||||||
|
{{ wtf.form_input_field(form.ssid) }}
|
||||||
|
<p class="help"> This is the name of the advertised Wi-Fi network. Current SSID: {{ get_setting('ssid') }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="password">
|
||||||
|
{{ wtf.form_input_field(form.wifi_password) }}
|
||||||
|
<p class="help"> This is the secret key needed to connect to the Wi-Fi network. By default, this is not set and everyone can join.
|
||||||
|
Current password: {{ get_setting('wifi_password') or 'Not set' }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
{{ wtf.form_input_field(form.butterbox_name) }}
|
||||||
|
<p class="help">This is the name shown in the UI, and used to access the box locally by adding .local or .lan in your browser.
|
||||||
|
Current name: {{ get_setting('butterbox_name') }}, accessed at {{ get_setting('butterbox_name') }}.local.</p>
|
||||||
|
</div>
|
||||||
|
<div class="checkbox">
|
||||||
|
{{ wtf.form_bool_field(form.disable_access_point) }}
|
||||||
|
<p class="help">Whether this box will advertise a WiFi network.</p>
|
||||||
|
</div>
|
||||||
|
<div class="checkbox">
|
||||||
|
{{ wtf.form_bool_field(form.disable_map_viewer) }}
|
||||||
|
<p class="help">Whether map services are enabled.</p>
|
||||||
|
</div>
|
||||||
|
<div class="checkbox">
|
||||||
|
{{ wtf.form_bool_field(form.disable_chat) }}
|
||||||
|
<p class="help">Whether chat services are enabled.</p>
|
||||||
|
</div>
|
||||||
|
<div class="checkbox">
|
||||||
|
{{ wtf.form_bool_field(form.disable_file_viewer) }}
|
||||||
|
<p class="help">Whether files services via USB are enabled.</p>
|
||||||
|
</div>
|
||||||
|
<div class="checkbox">
|
||||||
|
{{ wtf.form_bool_field(form.disable_app_store) }}
|
||||||
|
<p class="help">Whether app store services are enabled.</p>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
{{ wtf.form_input_field(form.admin_password) }}
|
||||||
|
<p class="help">Password for accessing this interface.</p>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">{{ form.butterbox_logo.label }} </label>
|
||||||
|
<div class="control">{{ form.butterbox_logo(class='label', style="width: 280px") }}</div>
|
||||||
|
{% for error in form.butterbox_logo.errors %}
|
||||||
|
<p class="help is-danger">{{ error }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
<p class="help">This is the logo shown in the UI. Current logo: <br>
|
||||||
|
<img src="{{ get_setting('butterbox_logo') }}" style="height: 50px"> </p>
|
||||||
|
</div>
|
||||||
|
<p>{{ form.submit( class="button is-link") }}</p>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<a href="{{ url_for('logout') }}">Logout</a>
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
36
app/templates/base.html
Normal file
36
app/templates/base.html
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
{% if title %}
|
||||||
|
<title>{{ title }}</title>
|
||||||
|
{% else %}
|
||||||
|
<title>"{{ get_setting('butterbox_name') }}"</title>
|
||||||
|
{% endif %}
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='butter_styles.css') }}">
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header" style="display: inline">
|
||||||
|
<a href="{{ url_for('index') }}"><img class="image is-32x32" style="display: inline;" src="{{ get_setting('butterbox_logo') }}"></a>
|
||||||
|
<p style="display: inline; padding-inline-start: 10px;">{{ get_setting('butterbox_name') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content"> {% block content %}{% endblock %} </div>
|
||||||
|
{% with messages = get_flashed_messages() %}
|
||||||
|
{% if messages %}
|
||||||
|
<div class="notification">
|
||||||
|
<ul>
|
||||||
|
{% for message in messages %}
|
||||||
|
<li>{{ message }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
20
app/templates/bulma_wtf.html
Normal file
20
app/templates/bulma_wtf.html
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
{% macro form_input_field(field) %}
|
||||||
|
<div class="control">
|
||||||
|
{{ field.label(class='label')}}
|
||||||
|
{{ field(class='input' + (' is-danger' if field.errors else ' is_success'), type="text") }}
|
||||||
|
{% for error in field.errors %}
|
||||||
|
<p class="help is-danger">{{ error }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
|
||||||
|
{% macro form_bool_field(field) %}
|
||||||
|
<div class="control">
|
||||||
|
{{ field.label(class='label')}}
|
||||||
|
{{ field(class='checkbox', type="checkbox") }}
|
||||||
|
{% for error in field.errors %}
|
||||||
|
<p class="help is-danger">{{ error }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
||||||
17
app/templates/index.html
Normal file
17
app/templates/index.html
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1 class="title is-large butter-title">Hi, welcome to the {{get_setting('butterbox_name')}}.</h1>
|
||||||
|
<p class="subtitle butter-title"> View and download the information you want from this offline box.</p>
|
||||||
|
<div class="grid">
|
||||||
|
{% for service in services %}
|
||||||
|
<a class="cell button is-large is-responsive butter-service" href={{ service.url }}>
|
||||||
|
<div class="butter-service__content"> {{ service.name }} <br>
|
||||||
|
<img class="image is-64x64 butter-service__image" src={{ service.image }}>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
14
app/templates/login.html
Normal file
14
app/templates/login.html
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% import "bulma_wtf.html" as wtf %}
|
||||||
|
|
||||||
|
<h1>Sign In</h1>
|
||||||
|
<form action="" method="post" novalidate>
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
<div class="field">{{ wtf.form_input_field(form.username) }}</div>
|
||||||
|
<div class="field">{{ wtf.form_input_field(form.password) }}</div>
|
||||||
|
|
||||||
|
<div>{{ form.submit(class="button is-link") }}</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
6
app/translation_refs.py
Normal file
6
app/translation_refs.py
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
from flask_babel import gettext as _
|
||||||
|
|
||||||
|
_('ssid_description')
|
||||||
|
_('butterbox_name_description')
|
||||||
|
_('wifi_password_description')
|
||||||
|
_('logo_description')
|
||||||
88
app/translations/en/LC_MESSAGES/messages.po
Normal file
88
app/translations/en/LC_MESSAGES/messages.po
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
# English translations for PROJECT.
|
||||||
|
# Copyright (C) 2026 ORGANIZATION
|
||||||
|
# This file is distributed under the same license as the PROJECT project.
|
||||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, 2026.
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PROJECT VERSION\n"
|
||||||
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
|
"POT-Creation-Date: 2026-02-10 15:23+0000\n"
|
||||||
|
"PO-Revision-Date: 2026-02-10 15:29+0000\n"
|
||||||
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
|
"Language: en\n"
|
||||||
|
"Language-Team: en <LL@li.org>\n"
|
||||||
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=utf-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Generated-By: Babel 2.18.0\n"
|
||||||
|
|
||||||
|
#: app/__init__.py:19
|
||||||
|
msgid "Please log in to access this page."
|
||||||
|
msgstr "Please log in to access this page."
|
||||||
|
|
||||||
|
#: app/forms.py:6
|
||||||
|
msgid "Username"
|
||||||
|
msgstr "Username"
|
||||||
|
|
||||||
|
#: app/forms.py:7
|
||||||
|
msgid "Password"
|
||||||
|
msgstr "Password"
|
||||||
|
|
||||||
|
#: app/forms.py:8
|
||||||
|
msgid "Sign In"
|
||||||
|
msgstr "Sign In"
|
||||||
|
|
||||||
|
#: app/forms.py:14
|
||||||
|
msgid "WiFi Password"
|
||||||
|
msgstr "WiFi Password"
|
||||||
|
|
||||||
|
#: app/forms.py:15
|
||||||
|
msgid "Butterbox Name"
|
||||||
|
msgstr "Butterbox Name"
|
||||||
|
|
||||||
|
#: app/forms.py:16
|
||||||
|
msgid "Butterbox Logo"
|
||||||
|
msgstr "Butterbox Logo"
|
||||||
|
|
||||||
|
#: app/forms.py:18
|
||||||
|
msgid "Submit"
|
||||||
|
msgstr "Submit"
|
||||||
|
|
||||||
|
#: app/routes.py:39
|
||||||
|
msgid "Invalid username or password"
|
||||||
|
msgstr "Invalid username or password"
|
||||||
|
|
||||||
|
#: app/translation_refs.py:3
|
||||||
|
msgid "ssid_description"
|
||||||
|
msgstr "This is the name of the advertised Wi-Fi network."
|
||||||
|
|
||||||
|
#: app/translation_refs.py:4
|
||||||
|
msgid "butterbox_name_description"
|
||||||
|
msgstr "This is the secret key needed to connect to the Wi-Fi network. By default, this is not set and everyone can join."
|
||||||
|
|
||||||
|
#: app/translation_refs.py:5
|
||||||
|
msgid "wifi_password_description"
|
||||||
|
msgstr "This is the name shown in the UI, and used to access the box locally by adding .local or .lan in your browser."
|
||||||
|
|
||||||
|
#: app/translation_refs.py:6
|
||||||
|
msgid "logo_description"
|
||||||
|
msgstr "An image that will be used as the logo."
|
||||||
|
|
||||||
|
#: app/templates/admin.html:4
|
||||||
|
msgid "Application Settings"
|
||||||
|
msgstr "Application Settings"
|
||||||
|
|
||||||
|
#: app/templates/base.html:15
|
||||||
|
msgid "Home"
|
||||||
|
msgstr "Home"
|
||||||
|
|
||||||
|
#: app/templates/base.html:32
|
||||||
|
msgid ""
|
||||||
|
"Admin settings have changed! Restart the butterbox for changes to take "
|
||||||
|
"effect."
|
||||||
|
msgstr ""
|
||||||
|
"Admin settings have changed! Restart the butterbox for changes to take "
|
||||||
|
"effect."
|
||||||
|
|
||||||
87
app/translations/ro/LC_MESSAGES/messages.po
Normal file
87
app/translations/ro/LC_MESSAGES/messages.po
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
# Romanian translations for PROJECT.
|
||||||
|
# Copyright (C) 2026 ORGANIZATION
|
||||||
|
# This file is distributed under the same license as the PROJECT project.
|
||||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, 2026.
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PROJECT VERSION\n"
|
||||||
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
|
"POT-Creation-Date: 2026-02-10 15:23+0000\n"
|
||||||
|
"PO-Revision-Date: 2026-02-10 16:28+0000\n"
|
||||||
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
|
"Language: ro\n"
|
||||||
|
"Language-Team: ro <LL@li.org>\n"
|
||||||
|
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : (n==0 || (n%100 > 0 && n%100"
|
||||||
|
" < 20)) ? 1 : 2);\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=utf-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Generated-By: Babel 2.18.0\n"
|
||||||
|
|
||||||
|
#: app/__init__.py:19
|
||||||
|
msgid "Please log in to access this page."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/forms.py:6
|
||||||
|
msgid "Username"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/forms.py:7
|
||||||
|
msgid "Password"
|
||||||
|
msgstr "Password"
|
||||||
|
|
||||||
|
#: app/forms.py:8
|
||||||
|
msgid "Sign In"
|
||||||
|
msgstr "Sign In"
|
||||||
|
|
||||||
|
#: app/forms.py:14
|
||||||
|
msgid "WiFi Password"
|
||||||
|
msgstr "WiFi Password"
|
||||||
|
|
||||||
|
#: app/forms.py:15
|
||||||
|
msgid "Butterbox Name"
|
||||||
|
msgstr "Butterbox Name"
|
||||||
|
|
||||||
|
#: app/forms.py:16
|
||||||
|
msgid "Butterbox Logo"
|
||||||
|
msgstr "Butterbox Logo"
|
||||||
|
|
||||||
|
#: app/forms.py:18
|
||||||
|
msgid "Submit"
|
||||||
|
msgstr "Submit"
|
||||||
|
|
||||||
|
#: app/routes.py:39
|
||||||
|
msgid "Invalid username or password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/translation_refs.py:3
|
||||||
|
msgid "ssid_description"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/translation_refs.py:4
|
||||||
|
msgid "butterbox_name_description"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/translation_refs.py:5
|
||||||
|
msgid "wifi_password_description"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/translation_refs.py:6
|
||||||
|
msgid "logo_description"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/admin.html:4
|
||||||
|
msgid "Application Settings"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/base.html:15
|
||||||
|
msgid "Home"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/templates/base.html:32
|
||||||
|
msgid ""
|
||||||
|
"Admin settings have changed! Restart the butterbox for changes to take "
|
||||||
|
"effect."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
2
babel.cfg
Normal file
2
babel.cfg
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
[python: app/**.py]
|
||||||
|
[jinja2: app/templates/**.html]
|
||||||
1
butter-portal.py
Normal file
1
butter-portal.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
from app import app
|
||||||
22
config.py
Normal file
22
config.py
Normal file
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue