import 'package:collection/collection.dart'; import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart'; import 'package:fladder/models/items/item_shared_models.dart'; import 'package:fladder/providers/edit_item_provider.dart'; import 'package:fladder/screens/settings/settings_list_tile.dart'; import 'package:fladder/screens/shared/focused_outlined_text_field.dart'; import 'package:fladder/screens/shared/media/external_urls.dart'; import 'package:fladder/screens/shared/outlined_text_field.dart'; import 'package:fladder/util/jelly_id.dart'; import 'package:fladder/util/list_extensions.dart'; import 'package:fladder/util/string_extensions.dart'; import 'package:fladder/widgets/shared/adaptive_date_picker.dart'; import 'package:fladder/widgets/shared/enum_selection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; class EditFields extends ConsumerStatefulWidget { final Map fields; final Map? json; const EditFields({ required this.fields, required this.json, super.key, }); @override ConsumerState createState() => _EditGeneralState(); } class _EditGeneralState extends ConsumerState { TextEditingController? currentController = TextEditingController(); String? currentEditingKey; List expandedKeys = []; final personName = TextEditingController(); PersonKind personType = PersonKind.actor; final personRole = TextEditingController(); @override Widget build(BuildContext context) { return Column( mainAxisSize: MainAxisSize.min, children: [ Flexible( child: ListView( padding: const EdgeInsets.symmetric(horizontal: 16), shrinkWrap: true, children: [ if (widget.json != null) ...widget.fields.entries.map( (e) { final keyLabel = e.key.toUpperCaseSplit(); return Padding( padding: const EdgeInsets.symmetric(vertical: 12), child: switch (e.value) { Map _ => Builder(builder: (context) { final map = e.value as Map; return SettingsListTile( label: Text(keyLabel), trailing: EnumBox( current: map.entries.firstWhereOrNull((element) => element.value == true)?.key ?? "", itemBuilder: (context) => [ PopupMenuItem( child: const Text(""), onTap: () => ref.read(editItemProvider.notifier).updateField(MapEntry(e.key, "")), ), ...map.entries.map( (mapEntry) => PopupMenuItem( child: Text(mapEntry.key), onTap: () => ref .read(editItemProvider.notifier) .updateField(MapEntry(e.key, mapEntry.key)), ), ) ], ), ); }), List _ => Padding( padding: const EdgeInsets.symmetric(vertical: 21), child: Builder(builder: (context) { final expanded = expandedKeys.contains(e.key); final list = e.value as List; return Card( child: InkWell( onTap: () => setState(() => expandedKeys = expandedKeys.toggle(e.key)), child: Padding( padding: const EdgeInsets.all(12.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( keyLabel, style: Theme.of(context).textTheme.titleLarge, ), const Spacer(), IconButton( onPressed: () => setState(() => expandedKeys = expandedKeys.toggle(e.key)), icon: Icon(expanded ? Icons.keyboard_arrow_up_rounded : Icons.keyboard_arrow_down_rounded), ) ], ), if (expanded) ...{ const SizedBox(height: 6), ...list.map( (genre) => Row( children: [ Text(genre.toString()), const Spacer(), IconButton( onPressed: () => ref.read(editItemProvider.notifier).updateField( MapEntry(e.key, list..remove(genre)), ), icon: const Icon(Icons.remove_rounded)) ], ), ), OutlinedTextField( label: "Add", controller: TextEditingController(), onSubmitted: (value) { ref.read(editItemProvider.notifier).updateField( MapEntry(e.key, list..add(value)), ); }, ) }, ], ), ), ), ); }), ), List _ => Padding( padding: const EdgeInsets.symmetric(vertical: 21), child: Builder(builder: (context) { final expanded = expandedKeys.contains(e.key); final list = e.value as List; List> listToMap(List people) { return people.map((e) => e.toPerson().toJson()).toList(); } return Card( child: InkWell( onTap: () => setState(() => expandedKeys = expandedKeys.toggle(e.key)), child: Padding( padding: const EdgeInsets.all(12.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( keyLabel, style: Theme.of(context).textTheme.titleLarge, ), const Spacer(), IconButton( onPressed: () => setState(() => expandedKeys = expandedKeys.toggle(e.key)), icon: Icon(expanded ? Icons.keyboard_arrow_up_rounded : Icons.keyboard_arrow_down_rounded), ) ], ), if (expanded) ...{ const SizedBox(height: 6), ...list.map( (person) => Padding( padding: const EdgeInsets.symmetric(vertical: 6), child: Row( children: [ SizedBox( height: 50, width: 50, child: Card( elevation: 2, color: Theme.of(context).colorScheme.onPrimary, child: Center( child: Text( person.name.getInitials(), style: Theme.of(context) .textTheme .bodyMedium ?.copyWith(fontWeight: FontWeight.bold), ), ), ), ), const SizedBox(width: 6), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(person.name), Opacity( opacity: 0.65, child: Text(person.role.isNotEmpty ? "${person.role} (${person.type}) " : person.type?.value ?? ""), ), ], ), const Spacer(), IconButton( onPressed: () { ref.read(editItemProvider.notifier).updateField( MapEntry(e.key, listToMap(list..remove(person)))); }, icon: const Icon(Icons.remove_rounded)) ], ), ), ), const SizedBox(height: 16), Row( children: [ Expanded( child: OutlinedTextField( label: "Name", controller: personName, ), ), const SizedBox(width: 16), Flexible( child: EnumBox( current: personType.name.toUpperCaseSplit(), itemBuilder: (context) => [ ...PersonKind.values .whereNot( (element) => element == PersonKind.swaggerGeneratedUnknown) .map( (entry) => PopupMenuItem( child: Text(entry.name.toUpperCaseSplit()), onTap: () { setState(() { personType = entry; }); }, ), ) ], ), ), const SizedBox(width: 16), IconButton( onPressed: () { ref.read(editItemProvider.notifier).updateField(MapEntry( e.key, listToMap(list ..add( Person( id: jellyId, name: personName.text, type: personType, role: personRole.text, ), )))); setState(() { personName.text = ""; personType = PersonKind.actor; personRole.text = ""; }); }, icon: const Icon(Icons.add_rounded), ) ], ), }, ], ), ), ), ); }), ), List _ => Padding( padding: const EdgeInsets.symmetric(vertical: 21), child: Builder(builder: (context) { final expanded = expandedKeys.contains(e.key); final list = e.value as List; final name = TextEditingController(); final url = TextEditingController(); return Card( child: InkWell( onTap: () => setState(() => expandedKeys = expandedKeys.toggle(e.key)), child: Padding( padding: const EdgeInsets.all(12.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( keyLabel, style: Theme.of(context).textTheme.titleLarge, ), const Spacer(), IconButton( onPressed: () => setState(() => expandedKeys = expandedKeys.toggle(e.key)), icon: Icon(expanded ? Icons.keyboard_arrow_up_rounded : Icons.keyboard_arrow_down_rounded), ) ], ), if (expanded) ...{ const SizedBox(height: 6), ...list.map( (externalUrl) => Row( children: [ Text(externalUrl.name), const Spacer(), Tooltip( message: "Open in browser", child: IconButton( onPressed: () => launchUrl(context, externalUrl.url), icon: const Icon(Icons.open_in_browser_rounded)), ), IconButton( onPressed: () { ref.read(editItemProvider.notifier).updateField( MapEntry( e.key, (list..remove(externalUrl)) .map((e) => e.toMap()) .toList()), ); }, icon: const Icon(Icons.remove_rounded)) ], ), ), Row( children: [ Flexible( child: OutlinedTextField( label: "Name", controller: name, ), ), const SizedBox(width: 16), Flexible( child: OutlinedTextField( label: "Url", controller: url, ), ), const SizedBox(width: 16), IconButton( onPressed: () { ref.read(editItemProvider.notifier).updateField( MapEntry( e.key, (list ..add( ExternalUrls(name: name.text, url: url.text), )) .map((e) => e.toMap()) .toList()), ); }, icon: const Icon(Icons.add_rounded), ) ], ), }, ], ), ), ), ); }), ), List _ => Padding( padding: const EdgeInsets.symmetric(vertical: 21), child: Builder(builder: (context) { final expanded = expandedKeys.contains(e.key); final list = e.value as List; void setMapping(List newList) { ref.read(editItemProvider.notifier).updateField( MapEntry(e.key, newList.map((e) => e.toMap()).toList()), ); } return Card( child: InkWell( onTap: () => setState(() => expandedKeys = expandedKeys.toggle(e.key)), child: Padding( padding: const EdgeInsets.all(12.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( keyLabel, style: Theme.of(context).textTheme.titleLarge, ), const Spacer(), IconButton( onPressed: () => setState(() => expandedKeys = expandedKeys.toggle(e.key)), icon: Icon(expanded ? Icons.keyboard_arrow_up_rounded : Icons.keyboard_arrow_down_rounded), ) ], ), if (expanded) ...[ const SizedBox(height: 6), ...list.map( (studio) => Row( children: [ Text(studio.name), const Spacer(), IconButton( onPressed: () => setMapping(list..remove(studio)), icon: const Icon(Icons.remove_rounded)) ], ), ), const SizedBox(height: 6), OutlinedTextField( label: "Add", controller: TextEditingController(), onSubmitted: (value) => setMapping(list..add(Studio(id: jellyId, name: value))), ) ] ], ), ), ), ); }), ), int value => Builder(builder: (context) { final controller = currentEditingKey == e.key ? currentController : TextEditingController(text: value.toString()); return FocusedOutlinedTextField( label: switch (e.key) { "IndexNumber" => "Episode Number", "ParentIndexNumber" => "Season Number", _ => keyLabel, }, controller: controller, inputFormatters: [FilteringTextInputFormatter.digitsOnly], onFocus: (focused) { if (focused) { currentController = controller; currentEditingKey = e.key; } else { currentController = null; currentEditingKey = null; } }, onSubmitted: (value) { final newYear = int.tryParse(value); if (newYear != null) { ref.read(editItemProvider.notifier).updateField( MapEntry(e.key, newYear), ); } }, keyboardType: TextInputType.number, onChanged: (value) { if (currentEditingKey != e.key) { currentEditingKey = e.key; currentController = controller; } final newYear = int.tryParse(value); if (newYear != null) { ref.read(editItemProvider.notifier).updateField( MapEntry(e.key, newYear), ); } }, ); }), double value => Builder(builder: (context) { final controller = currentEditingKey == e.key ? currentController : TextEditingController(text: value.toString()); return FocusedOutlinedTextField( label: keyLabel, controller: controller, onFocus: (focused) { if (focused) { currentController = controller; currentEditingKey = e.key; } else { currentController = null; currentEditingKey = null; } }, onSubmitted: (newValue) { final newRating = double.tryParse(newValue); if (newRating != null) { ref.read(editItemProvider.notifier).updateField( MapEntry(e.key, newRating), ); } else { controller?.text = value.toString(); } currentController = null; }, keyboardType: TextInputType.number, ); }), DateTime _ => Row( children: [ Flexible( child: FocusedOutlinedTextField( label: keyLabel, onTap: () async { FocusScope.of(context).requestFocus(FocusNode()); final newDate = await showAdaptiveDatePicker( context, initialDateTime: e.value, ); if (newDate == null) return; ref .read(editItemProvider.notifier) .updateField(MapEntry(e.key, newDate.toIso8601String())); }, controller: TextEditingController(text: DateFormat.yMMMEd().format((e.value as DateTime))), ), ), const SizedBox(width: 12), IconButton( onPressed: () async { final newDate = await showDatePicker( context: context, currentDate: DateTime.now(), initialDate: e.value, firstDate: DateTime(1950), lastDate: DateTime(2100), ); if (newDate == null) return; ref .read(editItemProvider.notifier) .updateField(MapEntry(e.key, newDate.toIso8601String())); }, icon: const Icon(IconsaxPlusLinear.calendar_2)) ], ), DisplayOrder _ => Builder(builder: (context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SettingsListTile( label: Text(keyLabel), trailing: EnumBox( current: (e.value as DisplayOrder).value.toUpperCaseSplit(), itemBuilder: (context) => DisplayOrder.values .map( (mapEntry) => PopupMenuItem( child: Text(mapEntry.value.toUpperCaseSplit()), onTap: () => ref .read(editItemProvider.notifier) .updateField(MapEntry(e.key, mapEntry.value)), ), ) .toList(), ), ), const Padding( padding: EdgeInsets.symmetric(horizontal: 12), child: Text("Order episodes by air date, DVD order, or absolute numbering."), ) ], ); }), ShowStatus _ => Builder(builder: (context) { return SettingsListTile( label: Text(keyLabel), trailing: EnumBox( current: (e.value as ShowStatus).value, itemBuilder: (context) => ShowStatus.values .map( (mapEntry) => PopupMenuItem( child: Text(mapEntry.value), onTap: () => ref .read(editItemProvider.notifier) .updateField(MapEntry(e.key, mapEntry.value)), ), ) .toList(), ), ); }), bool _ => SettingsListTile( label: Text(keyLabel), trailing: Switch.adaptive( value: e.value as bool, onChanged: (value) => ref.read(editItemProvider.notifier).updateField(MapEntry(e.key, value)), ), ), Duration _ => Builder(builder: (context) { final valueInMinutes = (e.value as Duration).inMinutes.toString(); final controller = currentEditingKey == e.key ? currentController : TextEditingController(text: valueInMinutes); return FocusedOutlinedTextField( label: keyLabel, controller: controller, onFocus: (focused) { if (focused) { currentController = controller; currentEditingKey = e.key; } else { currentController = null; currentEditingKey = null; } }, keyboardType: TextInputType.number, onSubmitted: (value) { final newMinutes = int.tryParse(value); if (newMinutes != null) { ref.read(editItemProvider.notifier).updateField( MapEntry(e.key, Duration(minutes: newMinutes).inMilliseconds * 10000), ); } else { controller?.text = valueInMinutes; } }, ); }), Map _ => Builder(builder: (context) { final map = e.value as Map; return Card( child: InkWell( onTap: () => setState(() => expandedKeys = expandedKeys.toggle(e.key)), child: Padding( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text(keyLabel, style: Theme.of(context).textTheme.titleLarge), const SizedBox(height: 6), const Text( "Uncheck a field to lock it and prevent its data from being changed.", ), const SizedBox(height: 6), Column( children: map.entries .map((values) => Row( children: [ Text(values.key.value), const Spacer(), Switch.adaptive( value: !values.value, onChanged: (value) { final newEntries = map; newEntries.update(values.key, (value) => !value); final newValues = newEntries.entries .where((element) => element.value == true) .map((e) => e.key.value); ref .read(editItemProvider.notifier) .updateField(MapEntry(e.key, newValues.toList())); }, ) ], )) .toList(), ) ], ), ), ), ); }), String value => Builder(builder: (context) { final controller = currentEditingKey == e.key ? currentController : TextEditingController(text: value); return FocusedOutlinedTextField( label: keyLabel, maxLines: e.key == "Overview" ? 5 : 1, controller: controller, onFocus: (focused) { if (focused) { currentEditingKey = e.key; currentController = controller; } else { currentController = null; currentEditingKey = null; } }, onSubmitted: (value) => ref.read(editItemProvider.notifier).updateField(MapEntry(e.key, value)), onChanged: (value) { if (currentEditingKey != e.key) { currentEditingKey = e.key; currentController = controller; } return ref.read(editItemProvider.notifier).updateField(MapEntry(e.key, value)); }, ); }), _ => Text("Not supported ${e.value.runtimeType}: ${e.value}"), }, ); }, ) else const Padding( padding: EdgeInsets.all(8.0), child: Center( child: CircularProgressIndicator.adaptive(strokeCap: StrokeCap.round), ), ), const SizedBox(height: 16), ], ), ), ], ); } }