feat(client): Flutter app (web target) — Phase 7 start
Single Flutter codebase under client/app/ with web enabled (mobile/desktop can be added later; lib/ is platform-agnostic). Talks to the NestJS backend via a thin REST wrapper; API_BASE is a --dart-define (defaults to the local backend). Screens: - Login: Konfi/guest (invite code), local Teamer password login, Teamer invite redemption. Token persisted in shared_preferences, restored on start; GET /auth/me drives a role-aware home. - Workshop-Wahl (guests): loads /wahl/guest/overview, ordered pick of up to 3 workshops, submits to /wahl/:id/teilnehmer. - Dateien: /files/:kcId list. - Chat: channel + message list (read-only; WS send is a follow-up). State: AppState (ChangeNotifier) exposed via an InheritedNotifier (AppScope) — no third-party state package. flutter analyze clean, flutter build web --release passes, one widget smoke test. Also: interim client/web/ HTML placeholder stays as-is (per plan it is superseded by this Flutter web build). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../api.dart';
|
||||
import '../main.dart';
|
||||
|
||||
class FilesScreen extends StatefulWidget {
|
||||
const FilesScreen({super.key, required this.kcId});
|
||||
final String kcId;
|
||||
|
||||
@override
|
||||
State<FilesScreen> createState() => _FilesScreenState();
|
||||
}
|
||||
|
||||
class _FilesScreenState extends State<FilesScreen> {
|
||||
Future<List<FileEntry>>? _future;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_future ??= AppScope.of(context).api.files(widget.kcId);
|
||||
}
|
||||
|
||||
static const _visibilityLabels = {
|
||||
'ALLE': 'Alle',
|
||||
'ALLE_AUSSER_KONFIS': 'Team (ohne Konfis)',
|
||||
'NUR_LT': 'Nur Leitungsteam',
|
||||
};
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Dateien')),
|
||||
body: FutureBuilder<List<FileEntry>>(
|
||||
future: _future,
|
||||
builder: (context, snap) {
|
||||
if (snap.connectionState != ConnectionState.done) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (snap.hasError) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Text('${snap.error}', textAlign: TextAlign.center),
|
||||
),
|
||||
);
|
||||
}
|
||||
final files = snap.data!;
|
||||
if (files.isEmpty) {
|
||||
return const Center(child: Text('Keine Dateien freigegeben.'));
|
||||
}
|
||||
return ListView.separated(
|
||||
itemCount: files.length,
|
||||
separatorBuilder: (context, index) => const Divider(height: 1),
|
||||
itemBuilder: (context, i) {
|
||||
final f = files[i];
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.insert_drive_file_outlined),
|
||||
title: Text(f.filename),
|
||||
subtitle: Text(_visibilityLabels[f.visibility] ?? f.visibility),
|
||||
trailing: const Icon(Icons.download),
|
||||
onTap: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Download-URL: ${AppScope.of(context).api.fileDownloadUrl(f.id)}',
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user