Rebrand CommGate to gsmnode and adopt new design system
Rename the whole project from CommGate to gsmnode and re-skin every surface onto the new signal-green + ink design system (Space Grotesk / IBM Plex Sans / JetBrains Mono, flat surfaces, two-arrow routing mark, lowercase gsm+node wordmark). - Web App + API panel: rewrite token layer, gn-* utilities, data-gsm-theme, new logo/favicon assets; both builds verified. - Phone App (Flutter): green theme, new mark/wordmark widgets, adaptive launcher icons; rename Android applicationId/package to app.gsmnode.phone; bump AGP to 8.6.0 and google_fonts to 8.1.0; debug APK build verified. - API Server (Go): panel routes, User-Agent, health service id, branding. - Home Assistant Plugin: rename integration domain sms_gateway to gsmnode (folder, DOMAIN, services, entity ids, classes); py_compile verified. - Update all READMEs; add .gitignore (excludes Design/, build artifacts). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,298 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
import '../main.dart';
|
||||
import '../theme.dart';
|
||||
import '../widgets/gsmnode_mark.dart';
|
||||
import 'login_screen.dart';
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
const HomeScreen({super.key});
|
||||
|
||||
@override
|
||||
State<HomeScreen> createState() => _HomeScreenState();
|
||||
}
|
||||
|
||||
class _HomeScreenState extends State<HomeScreen> {
|
||||
bool _permsGranted = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
gateway.addListener(_onChange);
|
||||
_checkPermissions();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
gateway.removeListener(_onChange);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onChange() => setState(() {});
|
||||
|
||||
Future<void> _checkPermissions() async {
|
||||
final sms = await Permission.sms.status;
|
||||
final phone = await Permission.phone.status;
|
||||
setState(() => _permsGranted = sms.isGranted && phone.isGranted);
|
||||
}
|
||||
|
||||
Future<void> _requestPermissions() async {
|
||||
// SMS + phone gate the gateway; notification lets the foreground service
|
||||
// post its ongoing notification on Android 13+.
|
||||
await [Permission.sms, Permission.phone, Permission.notification].request();
|
||||
await _checkPermissions();
|
||||
}
|
||||
|
||||
void _toggle() {
|
||||
if (gateway.running) {
|
||||
gateway.stop();
|
||||
} else {
|
||||
gateway.start();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _logout() async {
|
||||
gateway.stop();
|
||||
await storage.clearSession();
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (_) => const LoginScreen()),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final running = gateway.running;
|
||||
final cg = context.cg;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
GsmNodeMark(size: 22),
|
||||
SizedBox(width: 8),
|
||||
GsmNodeWordmark(size: 17),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: _logout,
|
||||
icon: const Icon(Icons.logout),
|
||||
tooltip: 'Sign out',
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_heroCard(running),
|
||||
const SizedBox(height: 12),
|
||||
if (!_permsGranted) ...[
|
||||
_permissionCard(),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
_infoCard(),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'ACTIVITY',
|
||||
style: gsmMono(size: 10, color: cg.textMuted, letterSpacing: 1.4),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(child: _activityLog()),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Hero per the mobile UI kit: brand ring + mark when routing, ink when
|
||||
/// paused. The primary action carries the brand glow.
|
||||
Widget _heroCard(bool running) {
|
||||
final cg = context.cg;
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: running ? cg.brandTint : cg.sunkenBg,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: running
|
||||
? GsmColors.green500.withValues(alpha: 0.22)
|
||||
: cg.borderSubtle,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 84,
|
||||
height: 84,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: running ? GsmColors.green500 : cg.textMuted,
|
||||
boxShadow: running
|
||||
? [
|
||||
BoxShadow(
|
||||
color: GsmColors.green500.withValues(alpha: 0.28),
|
||||
blurRadius: 18,
|
||||
offset: const Offset(0, 6),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
child: const Center(
|
||||
child: GsmNodeMark(size: 44, color: Colors.white),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Text(
|
||||
running ? 'Gateway active' : 'Gateway paused',
|
||||
style: gsmDisplay(size: 20, color: cg.textPrimary),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
running
|
||||
? 'routing sms · mms · calls'
|
||||
: 'not routing — messages will queue',
|
||||
style: gsmMono(
|
||||
size: 12,
|
||||
color: running ? GsmColors.green500 : cg.textMuted,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
FilledButton.icon(
|
||||
onPressed: _permsGranted ? _toggle : null,
|
||||
icon: Icon(running ? Icons.stop : Icons.play_arrow),
|
||||
label: Text(running ? 'Stop gateway' : 'Start gateway'),
|
||||
style: running
|
||||
? FilledButton.styleFrom(
|
||||
backgroundColor: cg.danger,
|
||||
minimumSize: const Size.fromHeight(48),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _permissionCard() {
|
||||
final cg = context.cg;
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: cg.warningTint,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.warning_amber_rounded, color: cg.warning),
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(
|
||||
child: Text('SMS & phone permissions are required to send and '
|
||||
'receive messages.'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: _requestPermissions,
|
||||
child: const Text('Grant'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _infoCard() {
|
||||
final cg = context.cg;
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Column(
|
||||
children: [
|
||||
_infoRow('DEVICE', storage.deviceName ?? '—'),
|
||||
Divider(height: 16, color: cg.borderSubtle),
|
||||
_infoRow('ACCOUNT', storage.userEmail ?? '—'),
|
||||
Divider(height: 16, color: cg.borderSubtle),
|
||||
_infoRow('SERVER', storage.apiBase ?? '—'),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _infoRow(String label, String value) {
|
||||
final cg = context.cg;
|
||||
return Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 76,
|
||||
child: Text(
|
||||
label,
|
||||
style: gsmMono(size: 9, color: cg.textMuted, letterSpacing: 1.3),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: gsmMono(size: 12, color: cg.textPrimary),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.right,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _activityLog() {
|
||||
final cg = context.cg;
|
||||
final entries = gateway.log;
|
||||
if (entries.isEmpty) {
|
||||
return Center(
|
||||
child: Text(
|
||||
'No activity yet.',
|
||||
style: TextStyle(color: cg.textMuted),
|
||||
),
|
||||
);
|
||||
}
|
||||
return ListView.separated(
|
||||
itemCount: entries.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 6),
|
||||
itemBuilder: (_, i) {
|
||||
final e = entries[i];
|
||||
final t = e.time;
|
||||
final hh = t.hour.toString().padLeft(2, '0');
|
||||
final mm = t.minute.toString().padLeft(2, '0');
|
||||
final ss = t.second.toString().padLeft(2, '0');
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: cg.card,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: cg.borderSubtle),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: e.error ? cg.danger : cg.success,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(e.text, style: const TextStyle(fontSize: 13)),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
'$hh:$mm:$ss',
|
||||
style: gsmMono(size: 10, color: cg.textMuted),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user