Initial phone message board app

This commit is contained in:
Sean McElwain 2026-05-08 12:51:56 -05:00
commit 397f05bc87
4 changed files with 307 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
__pycache__/
*.pyc
messages.txt
.env
.venv/

11
README.md Normal file
View File

@ -0,0 +1,11 @@
# Phone Message Board
A small Flask local message board for transferring text between a phone and laptop over Bluetooth PAN, hotspot, USB tethering, or another local network.
## Run
python app.py
Then open the phone IP from the laptop, for example:
http://192.168.44.1:8080

290
app.py Normal file
View File

@ -0,0 +1,290 @@
from flask import Flask, request, redirect, render_template_string
from datetime import datetime
from pathlib import Path
import uuid
app = Flask(__name__)
DATA_FILE = Path("messages.txt")
PAGE = """
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Phone Message Board</title>
<style>
:root {
--bg: #111827;
--panel: #1f2937;
--card: #263244;
--text: #f9fafb;
--muted: #9ca3af;
--accent: #60a5fa;
--border: #374151;
--button: #2563eb;
--button-hover: #1d4ed8;
}
body {
margin: 0;
background: var(--bg);
color: var(--text);
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
.app {
max-width: 900px;
margin: 0 auto;
padding: 16px;
}
header {
position: sticky;
top: 0;
background: var(--bg);
padding: 12px 0;
border-bottom: 1px solid var(--border);
z-index: 10;
}
h1 {
margin: 0;
font-size: 22px;
}
.subtitle {
color: var(--muted);
font-size: 13px;
margin-top: 4px;
}
form {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 14px;
padding: 12px;
margin: 16px 0;
}
textarea {
width: 100%;
box-sizing: border-box;
min-height: 130px;
resize: vertical;
border-radius: 10px;
border: 1px solid var(--border);
background: #0f172a;
color: var(--text);
padding: 12px;
font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
font-size: 14px;
line-height: 1.45;
}
.form-row {
display: flex;
justify-content: space-between;
gap: 8px;
margin-top: 10px;
align-items: center;
}
button, .button-link {
border: 0;
border-radius: 10px;
background: var(--button);
color: white;
padding: 9px 14px;
font-size: 14px;
cursor: pointer;
text-decoration: none;
display: inline-block;
}
button:hover, .button-link:hover {
background: var(--button-hover);
}
.clear-link {
color: var(--muted);
text-decoration: none;
font-size: 13px;
}
.messages {
display: flex;
flex-direction: column;
gap: 12px;
padding-bottom: 40px;
}
.msg {
background: var(--card);
border: 1px solid var(--border);
border-radius: 14px;
overflow: hidden;
}
.msg-header {
display: flex;
justify-content: space-between;
gap: 10px;
align-items: center;
padding: 9px 12px;
border-bottom: 1px solid var(--border);
color: var(--muted);
font-size: 12px;
}
.msg-body {
padding: 12px;
}
pre {
margin: 0;
white-space: pre-wrap;
word-break: break-word;
font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
font-size: 14px;
line-height: 1.45;
}
.copy-btn {
background: transparent;
border: 1px solid var(--border);
color: var(--text);
padding: 5px 9px;
font-size: 12px;
}
.copy-btn:hover {
background: #334155;
}
.empty {
color: var(--muted);
text-align: center;
padding: 40px 0;
}
</style>
</head>
<body>
<div class="app">
<header>
<h1>Phone Message Board</h1>
<div class="subtitle">Local transfer page for phone laptop text</div>
</header>
<form method="post">
<textarea name="message" placeholder="Paste or type a message, command, note, or code block here..."></textarea>
<div class="form-row">
<button type="submit">Post Message</button>
<a class="clear-link" href="/clear" onclick="return confirm('Clear all messages?')">Clear all</a>
</div>
</form>
<section class="messages">
{% if not messages %}
<div class="empty">No messages yet.</div>
{% endif %}
{% for msg in messages %}
<article class="msg">
<div class="msg-header">
<span>{{ msg.time }}</span>
<button class="copy-btn" onclick="copyMessage('{{ msg.id }}', this)">Copy</button>
</div>
<div class="msg-body">
<pre id="{{ msg.id }}">{{ msg.text }}</pre>
</div>
</article>
{% endfor %}
</section>
</div>
<script>
function copyMessage(id, button) {
const text = document.getElementById(id).innerText;
const area = document.createElement("textarea");
area.value = text;
area.style.position = "fixed";
area.style.left = "-9999px";
area.style.top = "0";
document.body.appendChild(area);
area.focus();
area.select();
let ok = false;
try {
ok = document.execCommand("copy");
} catch (err) {
ok = false;
}
document.body.removeChild(area);
if (ok) {
const old = button.innerText;
button.innerText = "Copied";
setTimeout(() => button.innerText = old, 1200);
} else {
const pre = document.getElementById(id);
const range = document.createRange();
range.selectNodeContents(pre);
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
button.innerText = "Selected";
setTimeout(() => button.innerText = "Copy", 1200);
}
}
</script>
</body>
</html>
"""
def load_messages():
if not DATA_FILE.exists():
return []
raw = DATA_FILE.read_text(encoding="utf-8")
blocks = raw.split("\n---MESSAGE---\n")
messages = []
for block in blocks:
if not block.strip():
continue
try:
time, text = block.split("\n", 1)
except ValueError:
continue
messages.append({
"id": "msg_" + uuid.uuid4().hex,
"time": time.strip(),
"text": text.strip(),
})
return list(reversed(messages))
@app.route("/", methods=["GET", "POST"])
def index():
if request.method == "POST":
message = request.form.get("message", "").strip()
if message:
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
with DATA_FILE.open("a", encoding="utf-8") as f:
f.write(f"{timestamp}\n{message}\n---MESSAGE---\n")
return redirect("/")
return render_template_string(PAGE, messages=load_messages())
@app.route("/clear")
def clear():
DATA_FILE.write_text("", encoding="utf-8")
return redirect("/")
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080)

1
requirements.txt Normal file
View File

@ -0,0 +1 @@
flask