manager-app/lib/Screens/Resources/resource_body_grid.dart

296 lines
10 KiB
Dart

import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart';
import 'package:manager_app/Components/fetch_resource_icon.dart';
import 'package:manager_app/Components/multi_select_container.dart';
import 'package:manager_app/Components/string_input_container.dart';
import 'package:manager_app/Models/managerContext.dart';
import 'package:manager_app/app_context.dart';
import 'package:manager_app/constants.dart';
import 'package:manager_api_new/api.dart';
import 'package:provider/provider.dart';
import 'package:diacritic/diacritic.dart';
class ResourceBodyGrid extends StatefulWidget {
final List<ResourceDTO> resources;
final Function onSelect;
final bool isAddButton;
final bool isSelectModal;
final List<ResourceType> resourceTypesIn;
const ResourceBodyGrid({
Key? key,
required this.resources,
required this.onSelect,
required this.isAddButton,
required this.resourceTypesIn,
this.isSelectModal = false,
}) : super(key: key);
@override
_ResourceBodyGridState createState() => _ResourceBodyGridState();
}
class _ResourceBodyGridState extends State<ResourceBodyGrid> {
late List<String> filterTypes;
late List<String> currentFilterTypes;
String filterSearch = '';
late List<String> selectedTypes;
late List<ResourceDTO> resourcesToShow;
@override
void initState() {
resourcesToShow = widget.resources;
currentFilterTypes = resource_types
.where((rt) => widget.resourceTypesIn.contains(rt.type))
.map((rt) => rt.label)
.toList();
filterTypes = resource_types
.where((rt) => widget.resourceTypesIn.contains(rt.type))
.map((rt) => rt.label)
.toList();
selectedTypes = resource_types
.where((rt) => widget.resourceTypesIn.contains(rt.type))
.map((rt) => rt.label)
.toList();
super.initState();
}
@override
Widget build(BuildContext context) {
final appContext = Provider.of<AppContext>(context);
return bodyGrid(resourcesToShow, appContext);
}
Widget bodyGrid(List<ResourceDTO> data, AppContext appContext) {
final canEdit = (appContext.getContext() as ManagerAppContext).canEdit;
return Column(
children: [
// Header: search + filter + add button — Wrap pour mobile
Padding(
padding: const EdgeInsets.fromLTRB(8, 8, 8, 4),
child: Wrap(
spacing: 8,
runSpacing: 8,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
StringInputContainer(
label: 'Recherche:',
onChanged: (String value) {
setState(() {
filterSearch = value;
filterResource();
});
},
),
MultiSelectContainer(
label: 'Type:',
initialValue: filterTypes,
values: currentFilterTypes,
isMultiple: true,
onChanged: (result) {
setState(() {
selectedTypes = result as List<String>;
filterResource();
});
},
),
if (widget.isAddButton && canEdit)
_AddButton(onTap: () {
widget.onSelect(
ResourceDTO(id: widget.isSelectModal ? '-1' : null));
}),
],
),
),
// Grid
Expanded(
child: GridView.builder(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 160,
childAspectRatio: 1.0,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
),
itemCount: data.length,
itemBuilder: (BuildContext context, int index) {
return _ResourceCard(
resource: resourcesToShow[index],
onTap: () => widget.onSelect(resourcesToShow[index]),
);
},
),
),
],
);
}
void filterResource() {
resourcesToShow = filterSearch.isEmpty
? widget.resources
: widget.resources
.where((ResourceDTO resource) =>
resource.id == null ||
removeDiacritics(resource.label!.toUpperCase())
.contains(removeDiacritics(filterSearch.toUpperCase())))
.toList();
var getTypesInSelected = resource_types
.where((ft) => selectedTypes.contains(ft.label))
.map((rt) => rt.type)
.toList();
resourcesToShow = resourcesToShow
.where((resource) =>
resource.id == null ||
getTypesInSelected.contains(resource.type))
.toList();
}
}
// ── Add button ────────────────────────────────────────────────────────────────
class _AddButton extends StatelessWidget {
final VoidCallback onTap;
const _AddButton({required this.onTap});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
width: 56,
height: 56,
decoration: BoxDecoration(
color: kSuccess,
borderRadius: BorderRadius.circular(16),
boxShadow: const [kDefaultShadow],
),
child: const Icon(Icons.add, color: kTextLightColor, size: 28),
),
);
}
}
// ── Resource card ─────────────────────────────────────────────────────────────
class _ResourceCard extends StatefulWidget {
final ResourceDTO resource;
final VoidCallback onTap;
const _ResourceCard({required this.resource, required this.onTap});
@override
State<_ResourceCard> createState() => _ResourceCardState();
}
class _ResourceCardState extends State<_ResourceCard> {
bool _hovered = false;
@override
Widget build(BuildContext context) {
final resource = widget.resource;
final isRemove = resource.id == null;
final hasImage = !isRemove &&
(resource.type == ResourceType.Image ||
resource.type == ResourceType.ImageUrl) &&
resource.url != null;
return MouseRegion(
onEnter: (_) => setState(() => _hovered = true),
onExit: (_) => setState(() => _hovered = false),
child: GestureDetector(
onTap: widget.onTap,
child: AnimatedScale(
scale: _hovered ? 1.03 : 1.0,
duration: const Duration(milliseconds: 150),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: kSecond,
spreadRadius: _hovered ? 1 : 0.5,
blurRadius: _hovered ? 10 : 5,
offset: const Offset(0, 2),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Stack(
fit: StackFit.expand,
children: [
// Background
if (hasImage)
Image.network(resource.url!, fit: BoxFit.cover)
else
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: isRemove
? [kSecond, kSecond.withValues(alpha: 0.7)]
: [kPrimaryColor, kPrimaryColor.withValues(alpha: 0.65)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
),
// Gradient scrim (only when there's an image)
if (hasImage)
DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
stops: const [0.35, 1.0],
colors: [
Colors.transparent,
Colors.black.withValues(alpha: 0.65),
],
),
),
),
// Type icon — top right badge
if (!isRemove)
Positioned(
top: 8,
right: 8,
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.35),
borderRadius: BorderRadius.circular(6),
),
child: Icon(
getResourceIcon(resource.type),
color: kTextLightColor,
size: 14,
),
),
),
// Label — bottom left
if (!isRemove)
Positioned(
left: 8,
right: 8,
bottom: 8,
child: AutoSizeText(
resource.label ?? '',
style: kCardTitleStyle.copyWith(fontSize: 13),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
// Remove icon
if (isRemove)
const Center(
child: Icon(Icons.close, color: kTextLightColor, size: 40),
),
],
),
),
),
),
),
);
}
}