feat: polish phase 7 forms and schedules
This commit is contained in:
101
lib/features/groups/group_form_logic.dart
Normal file
101
lib/features/groups/group_form_logic.dart
Normal file
@@ -0,0 +1,101 @@
|
||||
import '../../models/ignis_group.dart';
|
||||
|
||||
final RegExp _groupIdAllowedPattern = RegExp(r'^[a-z0-9][a-z0-9_-]*$');
|
||||
final RegExp _groupIdSanitizePattern = RegExp(r'[^a-z0-9_-]+');
|
||||
final RegExp _dashCollapsePattern = RegExp(r'[-_]{2,}');
|
||||
|
||||
const Map<String, String> _transliterationMap = {
|
||||
'а': 'a',
|
||||
'б': 'b',
|
||||
'в': 'v',
|
||||
'г': 'g',
|
||||
'д': 'd',
|
||||
'е': 'e',
|
||||
'ё': 'e',
|
||||
'ж': 'zh',
|
||||
'з': 'z',
|
||||
'и': 'i',
|
||||
'й': 'y',
|
||||
'к': 'k',
|
||||
'л': 'l',
|
||||
'м': 'm',
|
||||
'н': 'n',
|
||||
'о': 'o',
|
||||
'п': 'p',
|
||||
'р': 'r',
|
||||
'с': 's',
|
||||
'т': 't',
|
||||
'у': 'u',
|
||||
'ф': 'f',
|
||||
'х': 'h',
|
||||
'ц': 'ts',
|
||||
'ч': 'ch',
|
||||
'ш': 'sh',
|
||||
'щ': 'sch',
|
||||
'ъ': '',
|
||||
'ы': 'y',
|
||||
'ь': '',
|
||||
'э': 'e',
|
||||
'ю': 'yu',
|
||||
'я': 'ya',
|
||||
};
|
||||
|
||||
String slugifyGroupId(String input) {
|
||||
final lower = input.trim().toLowerCase();
|
||||
final buffer = StringBuffer();
|
||||
for (final rune in lower.runes) {
|
||||
final char = String.fromCharCode(rune);
|
||||
buffer.write(_transliterationMap[char] ?? char);
|
||||
}
|
||||
|
||||
var value = buffer
|
||||
.toString()
|
||||
.replaceAll(RegExp(r'\s+'), '-')
|
||||
.replaceAll(_groupIdSanitizePattern, '-')
|
||||
.replaceAll(_dashCollapsePattern, '-')
|
||||
.replaceAll(RegExp(r'^[-_]+|[-_]+$'), '');
|
||||
|
||||
if (value.length > 32) {
|
||||
value = value.substring(0, 32).replaceAll(RegExp(r'[-_]+$'), '');
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
String? validateGroupId(String value, Iterable<IgnisGroup> existingGroups) {
|
||||
final normalized = value.trim().toLowerCase();
|
||||
if (normalized.isEmpty) {
|
||||
return 'Укажите ID группы';
|
||||
}
|
||||
if (!_groupIdAllowedPattern.hasMatch(normalized)) {
|
||||
return 'Только латиница, цифры, "-" и "_"';
|
||||
}
|
||||
final alreadyExists = existingGroups.any((group) => group.id == normalized);
|
||||
if (alreadyExists) {
|
||||
return 'Группа с таким ID уже существует';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? validateGroupName(String value) {
|
||||
if (value.trim().isEmpty) {
|
||||
return 'Укажите название группы';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
List<IgnisGroup> findGroupMembershipConflicts(
|
||||
Set<String> selectedDeviceIds,
|
||||
Iterable<IgnisGroup> existingGroups,
|
||||
) {
|
||||
if (selectedDeviceIds.isEmpty) {
|
||||
return const [];
|
||||
}
|
||||
|
||||
return existingGroups
|
||||
.where(
|
||||
(group) =>
|
||||
group.macs.any((deviceId) => selectedDeviceIds.contains(deviceId)),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
147
lib/features/schedules/schedule_form_logic.dart
Normal file
147
lib/features/schedules/schedule_form_logic.dart
Normal file
@@ -0,0 +1,147 @@
|
||||
class ScheduleWeekdayOption {
|
||||
final int backendValue;
|
||||
final String shortLabel;
|
||||
final String fullLabel;
|
||||
|
||||
const ScheduleWeekdayOption({
|
||||
required this.backendValue,
|
||||
required this.shortLabel,
|
||||
required this.fullLabel,
|
||||
});
|
||||
}
|
||||
|
||||
const scheduleWeekdayOptions = <ScheduleWeekdayOption>[
|
||||
ScheduleWeekdayOption(
|
||||
backendValue: 1,
|
||||
shortLabel: 'Пн',
|
||||
fullLabel: 'понедельник',
|
||||
),
|
||||
ScheduleWeekdayOption(
|
||||
backendValue: 2,
|
||||
shortLabel: 'Вт',
|
||||
fullLabel: 'вторник',
|
||||
),
|
||||
ScheduleWeekdayOption(backendValue: 3, shortLabel: 'Ср', fullLabel: 'среда'),
|
||||
ScheduleWeekdayOption(
|
||||
backendValue: 4,
|
||||
shortLabel: 'Чт',
|
||||
fullLabel: 'четверг',
|
||||
),
|
||||
ScheduleWeekdayOption(
|
||||
backendValue: 5,
|
||||
shortLabel: 'Пт',
|
||||
fullLabel: 'пятница',
|
||||
),
|
||||
ScheduleWeekdayOption(
|
||||
backendValue: 6,
|
||||
shortLabel: 'Сб',
|
||||
fullLabel: 'суббота',
|
||||
),
|
||||
ScheduleWeekdayOption(
|
||||
backendValue: 0,
|
||||
shortLabel: 'Вс',
|
||||
fullLabel: 'воскресенье',
|
||||
),
|
||||
];
|
||||
|
||||
DateTime defaultOnceScheduleTime({DateTime? now}) {
|
||||
final base = now ?? DateTime.now();
|
||||
final candidate = base.add(const Duration(hours: 4));
|
||||
final roundedMinute = ((candidate.minute + 4) ~/ 5) * 5;
|
||||
final normalized = DateTime(
|
||||
candidate.year,
|
||||
candidate.month,
|
||||
candidate.day,
|
||||
candidate.hour,
|
||||
roundedMinute,
|
||||
);
|
||||
return normalized.isAfter(base)
|
||||
? normalized
|
||||
: normalized.add(const Duration(minutes: 5));
|
||||
}
|
||||
|
||||
String? validateOnceSchedule(DateTime? scheduledAt, {DateTime? now}) {
|
||||
if (scheduledAt == null) {
|
||||
return 'Выберите дату и время';
|
||||
}
|
||||
final threshold = (now ?? DateTime.now()).add(const Duration(minutes: 1));
|
||||
if (!scheduledAt.isAfter(threshold)) {
|
||||
return 'Нужна дата хотя бы на минуту вперёд';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? validateCronSchedule({
|
||||
required int? hour,
|
||||
required int? minute,
|
||||
required Set<int> weekdays,
|
||||
}) {
|
||||
if (hour == null || hour < 0 || hour > 23) {
|
||||
return 'Час должен быть от 0 до 23';
|
||||
}
|
||||
if (minute == null || minute < 0 || minute > 59) {
|
||||
return 'Минута должна быть от 0 до 59';
|
||||
}
|
||||
if (weekdays.isEmpty) {
|
||||
return 'Выберите хотя бы один день недели';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String serializeCronWeekdays(Set<int> weekdays) {
|
||||
if (weekdays.length == scheduleWeekdayOptions.length) {
|
||||
return '*';
|
||||
}
|
||||
final sorted = weekdays.toList()..sort();
|
||||
return sorted.join(',');
|
||||
}
|
||||
|
||||
String describeCronWeekdays(Set<int> weekdays) {
|
||||
if (weekdays.isEmpty) {
|
||||
return 'дни не выбраны';
|
||||
}
|
||||
if (weekdays.length == scheduleWeekdayOptions.length) {
|
||||
return 'каждый день';
|
||||
}
|
||||
final weekdaySet = weekdays.toSet();
|
||||
final selected = scheduleWeekdayOptions
|
||||
.where((option) => weekdaySet.contains(option.backendValue))
|
||||
.map((option) => option.shortLabel)
|
||||
.toList();
|
||||
return selected.join(', ');
|
||||
}
|
||||
|
||||
String describeCronWeekdaysExpression(String? value) {
|
||||
if (value == null || value.isEmpty || value == '*') {
|
||||
return 'каждый день';
|
||||
}
|
||||
|
||||
final weekdays = value
|
||||
.split(',')
|
||||
.map((item) => int.tryParse(item.trim()))
|
||||
.whereType<int>()
|
||||
.toSet();
|
||||
if (weekdays.isEmpty) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return describeCronWeekdays(weekdays);
|
||||
}
|
||||
|
||||
String formatLocalScheduleDateTime(DateTime value) {
|
||||
String pad(int number) => number.toString().padLeft(2, '0');
|
||||
return '${pad(value.day)}.${pad(value.month)}.${value.year} ${pad(value.hour)}:${pad(value.minute)}';
|
||||
}
|
||||
|
||||
String formatRunAtLabel(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'время не указано';
|
||||
}
|
||||
|
||||
final parsed = DateTime.tryParse(value);
|
||||
if (parsed == null) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return formatLocalScheduleDateTime(parsed.toLocal());
|
||||
}
|
||||
Reference in New Issue
Block a user