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>
78 lines
2.3 KiB
Dart
78 lines
2.3 KiB
Dart
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)}',
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|