Neda/call/lib/ui/screens/home_screen.dart

355 lines
12 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import '../../services/call_session.dart';
import '../widgets/status_indicator.dart';
import 'call_screen.dart';
import 'settings_screen.dart';
/// Home screen — pre-call setup and call initiation.
///
/// Responsibilities:
/// 1. Request RECORD_AUDIO permission on first launch.
/// 2. Show current key status (fingerprint loaded / none).
/// 3. Allow user to navigate to Settings to enter passphrase.
/// 4. Start / end the secure call.
class HomeScreen extends StatefulWidget {
final CallSession session;
const HomeScreen({super.key, required this.session});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
StreamSubscription<SessionState>? _stateSub;
SessionState _state = SessionState.noKey;
String? _fingerprint;
bool _permGranted = false;
@override
void initState() {
super.initState();
_state = widget.session.state;
_stateSub = widget.session.onStateChange
.listen((s) { if (mounted) setState(() => _state = s); });
_init();
}
Future<void> _init() async {
await _checkPermission();
await _loadFingerprint();
}
Future<void> _checkPermission() async {
final status = await Permission.microphone.status;
if (status.isGranted) {
if (mounted) setState(() => _permGranted = true);
return;
}
final result = await Permission.microphone.request();
if (mounted) setState(() => _permGranted = result.isGranted);
}
Future<void> _loadFingerprint() async {
final fp = await widget.session.getSavedFingerprint();
if (mounted) setState(() => _fingerprint = fp);
}
Future<void> _openSettings() async {
await Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => SettingsScreen(session: widget.session),
),
);
// Refresh fingerprint after returning from settings.
await _loadFingerprint();
}
Future<void> _startCall() async {
if (!_permGranted) {
await _checkPermission();
if (!_permGranted) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text('Microphone permission is required'),
backgroundColor: Colors.redAccent,
));
}
return;
}
}
if (!widget.session.hasKey) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text('Set a passphrase in Settings first'),
backgroundColor: Colors.amber,
));
}
return;
}
await widget.session.startCall();
if (!mounted) return;
await Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => CallScreen(session: widget.session),
),
);
}
@override
void dispose() {
_stateSub?.cancel();
super.dispose();
}
// ── Build ──────────────────────────────────────────────────────────
@override
Widget build(BuildContext context) {
final canCall = _permGranted &&
(_state == SessionState.keyLoaded ||
_state == SessionState.inCall);
return Scaffold(
backgroundColor: const Color(0xFF0D0D1A),
appBar: AppBar(
backgroundColor: const Color(0xFF0D0D1A),
title: const Text('SecureCall',
style: TextStyle(color: Colors.white, letterSpacing: 2,
fontWeight: FontWeight.w300)),
actions: [
IconButton(
icon: const Icon(Icons.settings_outlined, color: Colors.white54),
tooltip: 'Settings',
onPressed: _openSettings,
),
],
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// ── Status bar ────────────────────────────────────────
Center(child: StatusIndicator(state: _state)),
const SizedBox(height: 32),
// ── Key fingerprint card ──────────────────────────────
_KeyCard(fingerprint: _fingerprint, onTap: _openSettings),
const SizedBox(height: 20),
// ── Permission card ───────────────────────────────────
if (!_permGranted)
_WarningCard(
icon: Icons.mic_off,
message:
'Microphone permission denied.\nTap to request again.',
onTap: _checkPermission,
),
const Spacer(),
// ── How it works ──────────────────────────────────────
_HowItWorksSection(),
const SizedBox(height: 28),
// ── Start call button ─────────────────────────────────
ElevatedButton.icon(
onPressed: canCall ? _startCall : null,
icon: const Icon(Icons.lock_rounded, size: 20),
label: const Text('START SECURE CALL',
style: TextStyle(
fontSize: 16,
letterSpacing: 2,
fontWeight: FontWeight.bold)),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF00C853),
disabledBackgroundColor: const Color(0xFF1A3020),
padding: const EdgeInsets.symmetric(vertical: 18),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14)),
),
),
const SizedBox(height: 16),
Center(
child: Text(
canCall
? 'First start a normal phone call, then press the button'
: _fingerprint == null
? 'Set a passphrase in ⚙ Settings to enable'
: 'Microphone permission required',
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.white38, fontSize: 12, height: 1.5),
),
),
const SizedBox(height: 12),
],
),
),
),
);
}
}
// ── Sub-widgets ────────────────────────────────────────────────────────
class _KeyCard extends StatelessWidget {
final String? fingerprint;
final VoidCallback onTap;
const _KeyCard({required this.fingerprint, required this.onTap});
@override
Widget build(BuildContext context) {
final hasKey = fingerprint != null;
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF1A1A2E),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: hasKey
? const Color(0xFF00C853).withValues(alpha: 0.5)
: Colors.white12,
),
),
child: Row(
children: [
Icon(
hasKey ? Icons.vpn_key_rounded : Icons.vpn_key_off_outlined,
color: hasKey ? const Color(0xFF00C853) : Colors.white38,
size: 28,
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
hasKey ? 'Encryption key loaded' : 'No encryption key',
style: TextStyle(
color: hasKey
? const Color(0xFF00C853)
: Colors.white54,
fontWeight: FontWeight.w600,
),
),
if (hasKey) ...[
const SizedBox(height: 4),
Text(
'Fingerprint: $fingerprint',
style: const TextStyle(
color: Colors.white38,
fontSize: 12,
fontFamily: 'monospace',
letterSpacing: 2),
),
] else ...[
const SizedBox(height: 4),
const Text('Tap to open Settings and set passphrase',
style: TextStyle(color: Colors.white30, fontSize: 12)),
],
],
),
),
const Icon(Icons.chevron_right, color: Colors.white24),
],
),
),
);
}
}
class _WarningCard extends StatelessWidget {
final IconData icon;
final String message;
final VoidCallback onTap;
const _WarningCard(
{required this.icon, required this.message, required this.onTap});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: Colors.amber.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(12),
border:
Border.all(color: Colors.amber.withValues(alpha: 0.4)),
),
child: Row(
children: [
Icon(icon, color: Colors.amber, size: 22),
const SizedBox(width: 12),
Expanded(
child: Text(message,
style: const TextStyle(
color: Colors.amber, fontSize: 13, height: 1.4)),
),
],
),
),
);
}
}
class _HowItWorksSection extends StatelessWidget {
@override
Widget build(BuildContext context) {
const steps = [
(Icons.phone_in_talk_rounded,
'Start a normal cellular voice call with your peer'),
(Icons.vpn_key_rounded,
'Both devices must have the same passphrase loaded'),
(Icons.mic,
'Press PTT to speak — FSK audio transmits over the call'),
(Icons.hearing,
'Tap LISTEN to receive and decode incoming voice'),
];
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF0F0F1E),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white10),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('HOW TO USE',
style: TextStyle(
color: Colors.white38, fontSize: 11, letterSpacing: 2)),
const SizedBox(height: 12),
...steps.map((s) => Padding(
padding: const EdgeInsets.only(bottom: 10),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(s.$1, color: Colors.white38, size: 16),
const SizedBox(width: 10),
Expanded(
child: Text(s.$2,
style: const TextStyle(
color: Colors.white54,
fontSize: 13,
height: 1.4))),
],
),
)),
],
),
);
}
}