Initial commit

This commit is contained in:
Ana Custura 2026-02-17 08:42:33 +00:00
commit c0b4ca1021
21 changed files with 677 additions and 0 deletions

23
app/__init__.py Normal file
View 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
View 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
View 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
View 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
View 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)

View 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;
}
}

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

64
app/templates/admin.html Normal file
View 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
View 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>

View 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
View 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
View 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
View file

@ -0,0 +1,6 @@
from flask_babel import gettext as _
_('ssid_description')
_('butterbox_name_description')
_('wifi_password_description')
_('logo_description')

View 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."

View 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
View file

@ -0,0 +1,2 @@
[python: app/**.py]
[jinja2: app/templates/**.html]

1
butter-portal.py Normal file
View file

@ -0,0 +1 @@
from app import app

22
config.py Normal file

File diff suppressed because one or more lines are too long