296 lines
10 KiB
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),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|