Files
UnityUDP/lib/screens/home_screen.dart
2025-10-15 15:22:51 +02:00

411 lines
15 KiB
Dart

import 'dart:io';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:path/path.dart' as path;
import '../models/project.dart';
import '../services/storage_service.dart';
import 'project_screen.dart';
import '../widgets/project_dialog.dart';
import '../widgets/about_dialog.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
final StorageService _storageService = StorageService();
List<Project> _projects = [];
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadProjects();
// Show web warning after build
if (kIsWeb) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_showWebWarning();
});
}
}
Future<void> _loadProjects() async {
setState(() => _isLoading = true);
final projects = await _storageService.loadProjects();
setState(() {
_projects = projects;
_isLoading = false;
});
}
Future<void> _saveProjects() async {
await _storageService.saveProjects(_projects);
}
void _addProject(Project project) {
setState(() {
_projects.add(project);
});
_saveProjects();
}
void _updateProject(Project updatedProject) {
setState(() {
final index = _projects.indexWhere((p) => p.id == updatedProject.id);
if (index != -1) {
_projects[index] = updatedProject;
}
});
_saveProjects();
}
void _deleteProject(String projectId) {
setState(() {
_projects.removeWhere((p) => p.id == projectId);
});
_saveProjects();
}
void _showProjectDialog({Project? project}) {
showDialog(
context: context,
builder: (context) => ProjectDialog(
project: project,
onSave: (newProject) {
if (project == null) {
_addProject(newProject);
} else {
_updateProject(newProject);
}
},
),
);
}
void _showAboutDialog() {
showDialog(
context: context,
builder: (context) => const AppAboutDialog(),
);
}
Future<void> _openFolder(String filePath) async {
try {
final directory = path.dirname(filePath);
if (Platform.isLinux) {
await Process.run('xdg-open', [directory]);
} else if (Platform.isWindows) {
await Process.run('explorer', [directory]);
} else if (Platform.isMacOS) {
await Process.run('open', [directory]);
}
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to open folder: $e'),
backgroundColor: Colors.red,
behavior: SnackBarBehavior.floating,
),
);
}
}
Future<void> _showStorageInfo() async {
final filePath = await _storageService.getProjectsFilePath();
if (!mounted) return;
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Row(
children: [
Icon(Icons.folder_open),
SizedBox(width: 8),
Text('Storage Location'),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Your projects are saved in:',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
SelectableText(
filePath,
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 12,
),
),
const SizedBox(height: 16),
const Text(
'You can share this file with other users or back it up for safekeeping.',
style: TextStyle(fontSize: 12),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Close'),
),
FilledButton.icon(
onPressed: () {
_openFolder(filePath);
Navigator.pop(context);
},
icon: const Icon(Icons.folder_open),
label: const Text('Open Folder'),
),
],
),
);
}
void _showWebWarning() {
showDialog(
context: context,
barrierDismissible: true,
builder: (context) => AlertDialog(
title: const Row(
children: [
Icon(Icons.warning, color: Colors.orange),
SizedBox(width: 8),
Text('Web Platform Limitation'),
],
),
content: const Text(
'You are running this app in a web browser.\n\n'
'⚠️ UDP networking is NOT supported in web browsers due to security restrictions.\n\n'
'✅ For full functionality, please run this app on:\n'
' • Windows (flutter run -d windows)\n'
' • macOS (flutter run -d macos)\n'
' • Linux (flutter run -d linux)\n'
' • Android or iOS devices\n\n'
'The app will work for creating and managing projects, but UDP packets cannot be sent from a browser.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('I Understand'),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text('UnityUDP'),
Text(
'by Tom Hempel',
style: TextStyle(fontSize: 12, fontWeight: FontWeight.normal),
),
],
),
actions: [
if (kIsWeb)
Padding(
padding: const EdgeInsets.only(right: 8),
child: IconButton(
icon: const Icon(Icons.warning_amber, color: Colors.orange),
onPressed: _showWebWarning,
tooltip: 'Web Platform Limitations',
),
),
if (!kIsWeb)
IconButton(
icon: const Icon(Icons.folder),
onPressed: _showStorageInfo,
tooltip: 'Storage Location',
),
IconButton(
icon: const Icon(Icons.info_outline),
onPressed: _showAboutDialog,
tooltip: 'About',
),
],
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: _projects.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.folder_open,
size: 64,
color: Theme.of(context).colorScheme.primary.withAlpha(128),
),
const SizedBox(height: 16),
Text(
'No projects yet',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Text(
'Create your first project to get started',
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
)
: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: _projects.length,
itemBuilder: (context, index) {
final project = _projects[index];
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () async {
final updatedProject = await Navigator.push<Project>(
context,
MaterialPageRoute(
builder: (context) =>
ProjectScreen(project: project),
),
);
if (updatedProject != null) {
_updateProject(updatedProject);
}
},
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.primaryContainer,
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.folder,
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
project.name,
style: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
project.isBroadcast
? 'Broadcast • Port: ${project.port}${project.packages.length} package(s)'
: project.ipAddresses.length > 1
? '${project.ipAddresses.length} IPs • Port: ${project.port}${project.packages.length} package(s)'
: '${project.ipAddresses.first}:${project.port}${project.packages.length} package(s)',
style: Theme.of(context).textTheme.bodySmall,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
PopupMenuButton(
itemBuilder: (context) => [
const PopupMenuItem(
value: 'edit',
child: Row(
children: [
Icon(Icons.edit),
SizedBox(width: 8),
Text('Edit'),
],
),
),
const PopupMenuItem(
value: 'delete',
child: Row(
children: [
Icon(Icons.delete, color: Colors.red),
SizedBox(width: 8),
Text('Delete',
style:
TextStyle(color: Colors.red)),
],
),
),
],
onSelected: (value) {
if (value == 'edit') {
_showProjectDialog(project: project);
} else if (value == 'delete') {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Delete Project'),
content: Text(
'Are you sure you want to delete "${project.name}"?'),
actions: [
TextButton(
onPressed: () =>
Navigator.pop(context),
child: const Text('Cancel'),
),
TextButton(
onPressed: () {
_deleteProject(project.id);
Navigator.pop(context);
},
style: TextButton.styleFrom(
foregroundColor: Colors.red,
),
child: const Text('Delete'),
),
],
),
);
}
},
),
],
),
),
),
);
},
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () => _showProjectDialog(),
icon: const Icon(Icons.add),
label: const Text('New Project'),
),
);
}
}