d6956395c8
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>
299 lines
8.3 KiB
Dart
299 lines
8.3 KiB
Dart
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),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|