feat: add frontend app, improve static file serving with lazy loading and root redirect
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../api/client.dart';
|
||||
import '../api/models.dart';
|
||||
import '../widgets/pipeline_card.dart';
|
||||
import 'pipeline_editor.dart';
|
||||
import 'pipeline_detail.dart';
|
||||
|
||||
class PipelineListScreen extends StatefulWidget {
|
||||
const PipelineListScreen({super.key});
|
||||
|
||||
@override
|
||||
State<PipelineListScreen> createState() => _PipelineListScreenState();
|
||||
}
|
||||
|
||||
class _PipelineListScreenState extends State<PipelineListScreen> {
|
||||
late APIClient _api;
|
||||
List<Pipeline> _pipelines = [];
|
||||
bool _loading = true;
|
||||
String? _error;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final url = _parseBaseUrl();
|
||||
_api = APIClient(baseUrl: url);
|
||||
_load();
|
||||
}
|
||||
|
||||
static String _parseBaseUrl() {
|
||||
final uri = Uri.base;
|
||||
return '${uri.scheme}://${uri.host}${uri.port != 80 && uri.port != 443 ? ':${uri.port}' : ''}';
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
setState(() {
|
||||
_loading = true;
|
||||
_error = null;
|
||||
});
|
||||
try {
|
||||
final pipelines = await _api.listPipelines();
|
||||
setState(() {
|
||||
_pipelines = pipelines;
|
||||
_loading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_error = e.toString();
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _createPipeline() async {
|
||||
final result = await Navigator.push<Pipeline>(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const PipelineEditorScreen()),
|
||||
);
|
||||
if (result != null) {
|
||||
_pipelines.add(result);
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
void _navigateToDetail(Pipeline p) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => PipelineDetailScreen(pipeline: p)),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _deletePipeline(Pipeline p) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('Delete Pipeline'),
|
||||
content: Text('Are you sure you want to delete "${p.name}"?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
child: const Text('Delete'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed != true) return;
|
||||
|
||||
try {
|
||||
await _api.deletePipeline(p.id);
|
||||
setState(() {
|
||||
_pipelines.removeWhere((pl) => pl.id == p.id);
|
||||
});
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(e.toString())));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Zaloopipe'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: _load,
|
||||
tooltip: 'Refresh',
|
||||
),
|
||||
],
|
||||
),
|
||||
body: _loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: _error != null
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.error_outline, size: 48, color: Colors.red),
|
||||
const SizedBox(height: 16),
|
||||
Text(_error!, textAlign: TextAlign.center),
|
||||
const SizedBox(height: 16),
|
||||
FilledButton(onPressed: _load, child: const Text('Retry')),
|
||||
],
|
||||
),
|
||||
)
|
||||
: _pipelines.isEmpty
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.api_outlined, size: 64, color: Colors.grey),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'No pipelines yet',
|
||||
style: TextStyle(fontSize: 18, color: Colors.grey),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
FilledButton.icon(
|
||||
onPressed: _createPipeline,
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Create Pipeline'),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: RefreshIndicator(
|
||||
onRefresh: _load,
|
||||
child: ListView.builder(
|
||||
itemCount: _pipelines.length + 1,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
itemBuilder: (ctx, i) {
|
||||
if (i == _pipelines.length) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: SizedBox(
|
||||
height: 48,
|
||||
width: double.infinity,
|
||||
child: FilledButton.icon(
|
||||
onPressed: _createPipeline,
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Create Pipeline'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
final p = _pipelines[i];
|
||||
return PipelineCard(
|
||||
pipeline: p,
|
||||
onTap: () => _navigateToDetail(p),
|
||||
onDelete: () => _deletePipeline(p),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user