Fladder/lib/screens/metadata/edit_screens/edit_fields.dart
PartyDonut 7b3e733b76
[Setup] Added build.yaml and check.yaml (#1)
Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
2024-09-21 11:59:28 +02:00

738 lines
42 KiB
Dart

import 'package:collection/collection.dart';
import 'package:ficonsax/ficonsax.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<String, dynamic> fields;
final Map<String, dynamic>? json;
const EditFields({
required this.fields,
required this.json,
super.key,
});
@override
ConsumerState<ConsumerStatefulWidget> createState() => _EditGeneralState();
}
class _EditGeneralState extends ConsumerState<EditFields> {
TextEditingController? currentController = TextEditingController();
String? currentEditingKey;
List<String> 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<String, bool> _ => Builder(builder: (context) {
final map = e.value as Map<String, bool>;
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<String> _ => Padding(
padding: const EdgeInsets.symmetric(vertical: 21),
child: Builder(builder: (context) {
final expanded = expandedKeys.contains(e.key);
final list = e.value as List<String>;
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<Person> _ => Padding(
padding: const EdgeInsets.symmetric(vertical: 21),
child: Builder(builder: (context) {
final expanded = expandedKeys.contains(e.key);
final list = e.value as List<Person>;
List<Map<String, dynamic>> listToMap(List<Person> 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<PersonKind>(
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<ExternalUrls> _ => Padding(
padding: const EdgeInsets.symmetric(vertical: 21),
child: Builder(builder: (context) {
final expanded = expandedKeys.contains(e.key);
final list = e.value as List<ExternalUrls>;
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<Studio> _ => Padding(
padding: const EdgeInsets.symmetric(vertical: 21),
child: Builder(builder: (context) {
final expanded = expandedKeys.contains(e.key);
final list = e.value as List<Studio>;
void setMapping(List<Studio> 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(IconsaxOutline.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<EditorLockedFields, bool> _ => Builder(builder: (context) {
final map = e.value as Map<EditorLockedFields, bool>;
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),
],
),
),
],
);
}
}