966 lines
28 KiB
Dart
966 lines
28 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:social_app/core/l10n/l10n.dart';
|
|
|
|
import '../../../../app/di/injection.dart';
|
|
import '../../../../core/theme/design_tokens.dart';
|
|
import '../../../../shared/widgets/app_button.dart';
|
|
import '../../../../shared/widgets/app_input.dart';
|
|
import '../../../../shared/widgets/app_loading_indicator.dart';
|
|
import '../../../../shared/widgets/app_pressable.dart';
|
|
import '../../../../shared/widgets/app_selection_sheet.dart';
|
|
import '../../../../shared/widgets/detail_header_action_menu.dart';
|
|
import '../../../../shared/widgets/destructive_action_sheet.dart';
|
|
import '../../../../shared/widgets/toast/toast.dart';
|
|
import '../../../../shared/widgets/toast/toast_type.dart';
|
|
import '../../../../core/utils/tool_name_localizer.dart';
|
|
import '../../data/models/automation_job_model.dart';
|
|
import '../../data/apis/automation_jobs_api.dart';
|
|
import '../../presentation/cubits/job_detail_cubit.dart';
|
|
import '../widgets/settings_page_scaffold.dart';
|
|
|
|
class JobDetailScreen extends StatefulWidget {
|
|
const JobDetailScreen({super.key, this.jobId});
|
|
|
|
final String? jobId;
|
|
|
|
@override
|
|
State<JobDetailScreen> createState() => _JobDetailScreenState();
|
|
}
|
|
|
|
enum _JobDetailHeaderAction { delete }
|
|
|
|
class _JobDetailScreenState extends State<JobDetailScreen> {
|
|
late final JobDetailCubit _cubit;
|
|
final TextEditingController _titleController = TextEditingController();
|
|
final TextEditingController _templateController = TextEditingController();
|
|
|
|
String _scheduleType = 'daily';
|
|
String _timezone = 'Asia/Shanghai';
|
|
TimeOfDay _runAt = const TimeOfDay(hour: 8, minute: 0);
|
|
final Set<int> _selectedWeekdays = <int>{1};
|
|
String _contextSource = 'latest_chat';
|
|
String _contextWindowMode = 'day';
|
|
int _contextWindowCount = 2;
|
|
final Set<String> _selectedTools = <String>{'memory.write', 'memory.forget'};
|
|
|
|
ColorScheme get _colorScheme => Theme.of(context).colorScheme;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_cubit = JobDetailCubit(sl<AutomationJobsApi>());
|
|
if (widget.jobId != null) {
|
|
_cubit.loadJob(widget.jobId!);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_titleController.dispose();
|
|
_templateController.dispose();
|
|
_cubit.close();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return BlocProvider.value(
|
|
value: _cubit,
|
|
child: BlocConsumer<JobDetailCubit, JobDetailState>(
|
|
listener: (context, state) {
|
|
if (state.error != null) {
|
|
Toast.show(context, state.error!, type: ToastType.error);
|
|
}
|
|
},
|
|
builder: (context, state) {
|
|
final l10n = context.l10n;
|
|
if (state.isLoading) {
|
|
return SettingsPageScaffold(
|
|
title: l10n.commonLoading,
|
|
body: const Center(child: AppLoadingIndicator()),
|
|
);
|
|
}
|
|
|
|
final job = state.job;
|
|
final isEditMode = widget.jobId != null;
|
|
if (isEditMode && job == null && state.error != null) {
|
|
return SettingsPageScaffold(
|
|
title: l10n.settingsJobDetailTitle,
|
|
onBack: () => context.pop(),
|
|
body: _buildLoadFailedView(state.error!),
|
|
);
|
|
}
|
|
|
|
return SettingsPageScaffold(
|
|
title: job?.title ?? l10n.settingsJobCreatePageTitle,
|
|
onBack: () => context.pop(),
|
|
trailing: job != null && !job.isSystem
|
|
? _buildHeaderActions(job.id, state)
|
|
: null,
|
|
body: job == null
|
|
? _buildCreateForm(state)
|
|
: _buildDetailPage(job, state),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildLoadFailedView(String error) {
|
|
final l10n = context.l10n;
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_buildSectionTitle(l10n.settingsJobLoadFailed),
|
|
const SizedBox(height: AppSpacing.sm),
|
|
Text(
|
|
error,
|
|
style: TextStyle(
|
|
color: _colorScheme.error,
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
const SizedBox(height: AppSpacing.md),
|
|
AppButton(
|
|
text: l10n.settingsJobRetry,
|
|
onPressed: () => _cubit.loadJob(widget.jobId!),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildDetailPage(AutomationJobModel job, JobDetailState state) {
|
|
final l10n = context.l10n;
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_buildOverviewCard(job),
|
|
const SizedBox(height: AppSpacing.lg),
|
|
_buildSectionTitle(l10n.settingsJobPlanConfig),
|
|
const SizedBox(height: AppSpacing.sm),
|
|
_buildInfoCard([
|
|
_buildInfoRow(
|
|
l10n.settingsJobCycle,
|
|
_scheduleLabel(job.config.schedule.type),
|
|
),
|
|
_buildInfoRow(
|
|
l10n.settingsJobRunAt,
|
|
_displayRunAt(job.config.schedule),
|
|
),
|
|
_buildInfoRow(l10n.settingsJobTimezone, job.timezone),
|
|
_buildInfoRow(
|
|
l10n.settingsJobStatusLabel,
|
|
job.isActive
|
|
? l10n.settingsJobStatusEnabled
|
|
: l10n.settingsJobStatusDisabled,
|
|
),
|
|
]),
|
|
const SizedBox(height: AppSpacing.lg),
|
|
_buildSectionTitle(l10n.settingsJobInputTemplate),
|
|
const SizedBox(height: AppSpacing.sm),
|
|
_buildTextBlock(job.config.inputTemplate),
|
|
const SizedBox(height: AppSpacing.lg),
|
|
_buildSectionTitle(l10n.settingsJobEnabledTools),
|
|
const SizedBox(height: AppSpacing.sm),
|
|
_buildToolWrap(job.config.enabledTools),
|
|
const SizedBox(height: AppSpacing.lg),
|
|
_buildSectionTitle(l10n.settingsJobContextMode),
|
|
const SizedBox(height: AppSpacing.sm),
|
|
_buildInfoCard([
|
|
_buildInfoRow(
|
|
l10n.settingsJobContextSource,
|
|
_contextSourceLabel(job.config.context.source),
|
|
),
|
|
_buildInfoRow(
|
|
l10n.settingsJobWindowMode,
|
|
_windowModeLabel(job.config.context.windowMode),
|
|
),
|
|
_buildInfoRow(
|
|
l10n.settingsJobWindowCount,
|
|
l10n.settingsJobWindowCountValue(job.config.context.windowCount),
|
|
),
|
|
]),
|
|
if (!job.isSystem && state.isSaving)
|
|
const Padding(
|
|
padding: EdgeInsets.only(top: AppSpacing.lg),
|
|
child: Center(child: AppLoadingIndicator(size: AppSpacing.lg)),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildHeaderActions(String jobId, JobDetailState state) {
|
|
final l10n = context.l10n;
|
|
return DetailHeaderActionMenu<_JobDetailHeaderAction>(
|
|
items: [
|
|
DetailHeaderActionItem<_JobDetailHeaderAction>(
|
|
value: _JobDetailHeaderAction.delete,
|
|
label: l10n.commonDelete,
|
|
icon: Icons.delete_outline,
|
|
isDestructive: true,
|
|
),
|
|
],
|
|
onSelected: (action) {
|
|
if (state.isSaving) {
|
|
return;
|
|
}
|
|
if (action == _JobDetailHeaderAction.delete) {
|
|
unawaited(_confirmAndDelete(jobId));
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
Future<void> _confirmAndDelete(String jobId) async {
|
|
final l10n = context.l10n;
|
|
final confirmed = await showDestructiveActionSheet(
|
|
context,
|
|
title: l10n.settingsJobDeleteTitle,
|
|
message: l10n.settingsJobDeleteMessage,
|
|
confirmText: l10n.settingsJobDeleteConfirm,
|
|
);
|
|
if (!confirmed) {
|
|
return;
|
|
}
|
|
final success = await _cubit.deleteJob(jobId);
|
|
if (!mounted) {
|
|
return;
|
|
}
|
|
if (success) {
|
|
Toast.show(
|
|
context,
|
|
l10n.settingsJobDeleteSuccess,
|
|
type: ToastType.success,
|
|
);
|
|
context.pop();
|
|
}
|
|
}
|
|
|
|
Widget _buildOverviewCard(AutomationJobModel job) {
|
|
final l10n = context.l10n;
|
|
return Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(AppSpacing.lg),
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
colors: [_colorScheme.surface, _colorScheme.surfaceContainerLow],
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
),
|
|
borderRadius: BorderRadius.circular(AppRadius.xl),
|
|
border: Border.all(color: _colorScheme.outlineVariant),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
job.title,
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w700,
|
|
color: _colorScheme.onSurface,
|
|
),
|
|
),
|
|
const SizedBox(height: AppSpacing.sm),
|
|
Wrap(
|
|
spacing: AppSpacing.sm,
|
|
runSpacing: AppSpacing.sm,
|
|
children: [
|
|
_buildBadge(
|
|
job.isSystem
|
|
? l10n.settingsJobSourceSystem
|
|
: l10n.settingsJobSourceCustom,
|
|
),
|
|
_buildBadge(
|
|
job.isActive
|
|
? l10n.settingsJobStatusEnabled
|
|
: l10n.settingsJobStatusDisabled,
|
|
),
|
|
_buildBadge(_scheduleLabel(job.config.schedule.type)),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildBadge(String text) {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: AppSpacing.md,
|
|
vertical: AppSpacing.xs,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: _colorScheme.surface,
|
|
borderRadius: BorderRadius.circular(AppRadius.full),
|
|
border: Border.all(color: _colorScheme.outlineVariant),
|
|
),
|
|
child: Text(
|
|
text,
|
|
style: TextStyle(
|
|
color: _colorScheme.onSurfaceVariant,
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildCreateForm(JobDetailState state) {
|
|
final l10n = context.l10n;
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_buildCreateBasicSection(),
|
|
const SizedBox(height: AppSpacing.lg),
|
|
_buildCreateRuleSection(),
|
|
const SizedBox(height: AppSpacing.lg),
|
|
_buildCreateToolSection(),
|
|
const SizedBox(height: AppSpacing.lg),
|
|
_buildCreateContextSection(),
|
|
const SizedBox(height: AppSpacing.xl),
|
|
AppButton(
|
|
text: l10n.settingsCreateJob,
|
|
isLoading: state.isSaving,
|
|
onPressed: state.isSaving ? null : _submitCreate,
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildCreateBasicSection() {
|
|
final l10n = context.l10n;
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_buildSectionTitle(l10n.settingsJobBasicInfo),
|
|
const SizedBox(height: AppSpacing.sm),
|
|
AppInput(
|
|
label: l10n.settingsJobName,
|
|
hint: l10n.settingsJobNameHint,
|
|
controller: _titleController,
|
|
),
|
|
const SizedBox(height: AppSpacing.md),
|
|
AppInput(
|
|
label: l10n.settingsJobInputTemplate,
|
|
hint: l10n.settingsJobTemplateHint,
|
|
controller: _templateController,
|
|
maxLines: 4,
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildCreateRuleSection() {
|
|
final l10n = context.l10n;
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_buildSectionTitle(l10n.settingsJobExecutionRules),
|
|
const SizedBox(height: AppSpacing.sm),
|
|
_buildPickerTile(
|
|
label: l10n.settingsJobCycle,
|
|
value: _scheduleLabel(_scheduleType),
|
|
onTap: _pickScheduleType,
|
|
),
|
|
if (_scheduleType == 'weekly') ...[
|
|
const SizedBox(height: AppSpacing.sm),
|
|
_buildWeekdaySelector(),
|
|
],
|
|
const SizedBox(height: AppSpacing.sm),
|
|
_buildPickerTile(
|
|
label: l10n.settingsJobRunAt,
|
|
value: _formatTime(_runAt),
|
|
onTap: _pickRunAt,
|
|
),
|
|
const SizedBox(height: AppSpacing.sm),
|
|
_buildPickerTile(
|
|
label: l10n.settingsJobTimezone,
|
|
value: _timezone,
|
|
onTap: _pickTimezone,
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildCreateToolSection() {
|
|
final l10n = context.l10n;
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_buildSectionTitle(l10n.settingsJobToolSelection),
|
|
const SizedBox(height: AppSpacing.sm),
|
|
_buildToolSelector(),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildCreateContextSection() {
|
|
final l10n = context.l10n;
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_buildSectionTitle(l10n.settingsJobContextMode),
|
|
const SizedBox(height: AppSpacing.sm),
|
|
_buildPickerTile(
|
|
label: l10n.settingsJobContextSource,
|
|
value: _contextSourceLabel(_contextSource),
|
|
onTap: _pickContextSource,
|
|
),
|
|
const SizedBox(height: AppSpacing.sm),
|
|
_buildPickerTile(
|
|
label: l10n.settingsJobWindowMode,
|
|
value: _windowModeLabel(_contextWindowMode),
|
|
onTap: _pickWindowMode,
|
|
),
|
|
const SizedBox(height: AppSpacing.sm),
|
|
_buildCounterTile(
|
|
label: l10n.settingsJobWindowCount,
|
|
value: _contextWindowCount,
|
|
onMinus: _contextWindowCount > 1
|
|
? () {
|
|
setState(() {
|
|
_contextWindowCount -= 1;
|
|
});
|
|
}
|
|
: null,
|
|
onPlus: _contextWindowCount < 200
|
|
? () {
|
|
setState(() {
|
|
_contextWindowCount += 1;
|
|
});
|
|
}
|
|
: null,
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildPickerTile({
|
|
required String label,
|
|
required String value,
|
|
required VoidCallback onTap,
|
|
}) {
|
|
return AppPressable(
|
|
onTap: onTap,
|
|
borderRadius: BorderRadius.circular(AppRadius.lg),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: AppSpacing.md,
|
|
vertical: AppSpacing.md,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: _colorScheme.surface,
|
|
borderRadius: BorderRadius.circular(AppRadius.lg),
|
|
border: Border.all(color: _colorScheme.outlineVariant),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
label,
|
|
style: TextStyle(
|
|
color: _colorScheme.onSurfaceVariant,
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
const SizedBox(height: AppSpacing.xs),
|
|
Text(
|
|
value,
|
|
style: TextStyle(
|
|
color: _colorScheme.onSurface,
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Icon(
|
|
Icons.keyboard_arrow_down,
|
|
color: _colorScheme.onSurfaceVariant,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildCounterTile({
|
|
required String label,
|
|
required int value,
|
|
required VoidCallback? onMinus,
|
|
required VoidCallback? onPlus,
|
|
}) {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: AppSpacing.md,
|
|
vertical: AppSpacing.md,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: _colorScheme.surface,
|
|
borderRadius: BorderRadius.circular(AppRadius.lg),
|
|
border: Border.all(color: _colorScheme.outlineVariant),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
context.l10n.settingsJobCounterValue(label, value),
|
|
style: TextStyle(
|
|
color: _colorScheme.onSurface,
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
_buildCounterAction(icon: Icons.remove, onTap: onMinus),
|
|
const SizedBox(width: AppSpacing.sm),
|
|
_buildCounterAction(icon: Icons.add, onTap: onPlus),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildCounterAction({
|
|
required IconData icon,
|
|
required VoidCallback? onTap,
|
|
}) {
|
|
return AppPressable(
|
|
onTap: onTap,
|
|
borderRadius: BorderRadius.circular(AppRadius.full),
|
|
child: Container(
|
|
width: AppSpacing.xxl + AppSpacing.md,
|
|
height: AppSpacing.xxl + AppSpacing.md,
|
|
decoration: BoxDecoration(
|
|
color: onTap == null
|
|
? _colorScheme.surfaceContainerHighest
|
|
: _colorScheme.surfaceContainerLow,
|
|
borderRadius: BorderRadius.circular(AppRadius.full),
|
|
border: Border.all(color: _colorScheme.outlineVariant),
|
|
),
|
|
child: Icon(
|
|
icon,
|
|
size: AppSpacing.lg,
|
|
color: onTap == null
|
|
? _colorScheme.onSurfaceVariant
|
|
: _colorScheme.primary,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildToolSelector() {
|
|
return Wrap(
|
|
spacing: AppSpacing.sm,
|
|
runSpacing: AppSpacing.sm,
|
|
children: automationToolOptions.map((toolName) {
|
|
final selected = _selectedTools.contains(toolName);
|
|
return AppPressable(
|
|
onTap: () {
|
|
setState(() {
|
|
if (selected) {
|
|
_selectedTools.remove(toolName);
|
|
} else {
|
|
_selectedTools.add(toolName);
|
|
}
|
|
});
|
|
},
|
|
borderRadius: BorderRadius.circular(AppRadius.full),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: AppSpacing.md,
|
|
vertical: AppSpacing.sm,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: selected
|
|
? _colorScheme.primaryContainer
|
|
: _colorScheme.surface,
|
|
borderRadius: BorderRadius.circular(AppRadius.full),
|
|
border: Border.all(
|
|
color: selected
|
|
? _colorScheme.primary
|
|
: _colorScheme.outlineVariant,
|
|
),
|
|
),
|
|
child: Text(
|
|
localizeToolName(toolName),
|
|
style: TextStyle(
|
|
color: selected
|
|
? _colorScheme.primary
|
|
: _colorScheme.onSurfaceVariant,
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}).toList(),
|
|
);
|
|
}
|
|
|
|
Widget _buildWeekdaySelector() {
|
|
final l10n = context.l10n;
|
|
final weekdayLabels = <int, String>{
|
|
1: l10n.settingsJobWeekdayMon,
|
|
2: l10n.settingsJobWeekdayTue,
|
|
3: l10n.settingsJobWeekdayWed,
|
|
4: l10n.settingsJobWeekdayThu,
|
|
5: l10n.settingsJobWeekdayFri,
|
|
6: l10n.settingsJobWeekdaySat,
|
|
7: l10n.settingsJobWeekdaySun,
|
|
};
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.all(AppSpacing.md),
|
|
decoration: BoxDecoration(
|
|
color: _colorScheme.surface,
|
|
borderRadius: BorderRadius.circular(AppRadius.lg),
|
|
border: Border.all(color: _colorScheme.outlineVariant),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
l10n.settingsJobRunDays,
|
|
style: TextStyle(
|
|
color: _colorScheme.onSurfaceVariant,
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
const SizedBox(height: AppSpacing.sm),
|
|
Wrap(
|
|
spacing: AppSpacing.sm,
|
|
runSpacing: AppSpacing.sm,
|
|
children: weekdayLabels.entries.map((entry) {
|
|
final selected = _selectedWeekdays.contains(entry.key);
|
|
return AppPressable(
|
|
onTap: () {
|
|
setState(() {
|
|
if (selected) {
|
|
if (_selectedWeekdays.length > 1) {
|
|
_selectedWeekdays.remove(entry.key);
|
|
}
|
|
} else {
|
|
_selectedWeekdays.add(entry.key);
|
|
}
|
|
});
|
|
},
|
|
borderRadius: BorderRadius.circular(AppRadius.full),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: AppSpacing.md,
|
|
vertical: AppSpacing.sm,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: selected
|
|
? _colorScheme.primaryContainer
|
|
: _colorScheme.surface,
|
|
borderRadius: BorderRadius.circular(AppRadius.full),
|
|
border: Border.all(
|
|
color: selected
|
|
? _colorScheme.primary
|
|
: _colorScheme.outlineVariant,
|
|
),
|
|
),
|
|
child: Text(
|
|
entry.value,
|
|
style: TextStyle(
|
|
color: selected
|
|
? _colorScheme.primary
|
|
: _colorScheme.onSurfaceVariant,
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}).toList(),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildToolWrap(List<String> tools) {
|
|
if (tools.isEmpty) {
|
|
return _buildTextBlock(context.l10n.settingsJobNoToolsEnabled);
|
|
}
|
|
return Wrap(
|
|
spacing: AppSpacing.sm,
|
|
runSpacing: AppSpacing.sm,
|
|
children: tools
|
|
.map((tool) => _buildBadge(localizeToolName(tool)))
|
|
.toList(),
|
|
);
|
|
}
|
|
|
|
Widget _buildTextBlock(String text) {
|
|
return Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(AppSpacing.md),
|
|
decoration: BoxDecoration(
|
|
color: _colorScheme.surface,
|
|
borderRadius: BorderRadius.circular(AppRadius.lg),
|
|
border: Border.all(color: _colorScheme.outlineVariant),
|
|
),
|
|
child: Text(
|
|
text,
|
|
style: TextStyle(
|
|
color: _colorScheme.onSurface,
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w500,
|
|
height: 1.5,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSectionTitle(String title) {
|
|
return Text(
|
|
title,
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w600,
|
|
color: _colorScheme.onSurfaceVariant,
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildInfoCard(List<Widget> children) {
|
|
return Container(
|
|
padding: const EdgeInsets.all(AppSpacing.md),
|
|
decoration: BoxDecoration(
|
|
color: _colorScheme.surface,
|
|
borderRadius: BorderRadius.circular(AppRadius.lg),
|
|
border: Border.all(color: _colorScheme.outlineVariant),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: children,
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildInfoRow(String label, String value) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: AppSpacing.xs),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
label,
|
|
style: TextStyle(
|
|
color: _colorScheme.onSurfaceVariant,
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
const SizedBox(width: AppSpacing.md),
|
|
Expanded(
|
|
child: Text(
|
|
value,
|
|
textAlign: TextAlign.right,
|
|
style: TextStyle(
|
|
color: _colorScheme.onSurface,
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _pickScheduleType() async {
|
|
final l10n = context.l10n;
|
|
final picked = await showAppSelectionSheet<String>(
|
|
context,
|
|
title: l10n.settingsJobPickCycle,
|
|
selectedValue: _scheduleType,
|
|
items: [
|
|
AppSelectionItem(value: 'daily', label: l10n.settingsJobScheduleDaily),
|
|
AppSelectionItem(
|
|
value: 'weekly',
|
|
label: l10n.settingsJobScheduleWeekly,
|
|
),
|
|
],
|
|
);
|
|
if (picked != null) {
|
|
setState(() {
|
|
_scheduleType = picked;
|
|
if (_scheduleType == 'weekly' && _selectedWeekdays.isEmpty) {
|
|
_selectedWeekdays.add(1);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
Future<void> _pickTimezone() async {
|
|
final picked = await showAppSelectionSheet<String>(
|
|
context,
|
|
title: context.l10n.settingsJobPickTimezone,
|
|
selectedValue: _timezone,
|
|
items: const [
|
|
AppSelectionItem(value: 'Asia/Shanghai', label: 'Asia/Shanghai'),
|
|
AppSelectionItem(value: 'UTC', label: 'UTC'),
|
|
],
|
|
);
|
|
if (picked != null) {
|
|
setState(() {
|
|
_timezone = picked;
|
|
});
|
|
}
|
|
}
|
|
|
|
Future<void> _pickContextSource() async {
|
|
final l10n = context.l10n;
|
|
final picked = await showAppSelectionSheet<String>(
|
|
context,
|
|
title: l10n.settingsJobPickContextSource,
|
|
selectedValue: _contextSource,
|
|
items: [
|
|
AppSelectionItem(
|
|
value: 'latest_chat',
|
|
label: l10n.settingsJobContextSourceLatestChat,
|
|
),
|
|
],
|
|
);
|
|
if (picked != null) {
|
|
setState(() {
|
|
_contextSource = picked;
|
|
});
|
|
}
|
|
}
|
|
|
|
Future<void> _pickWindowMode() async {
|
|
final l10n = context.l10n;
|
|
final picked = await showAppSelectionSheet<String>(
|
|
context,
|
|
title: l10n.settingsJobPickWindowMode,
|
|
selectedValue: _contextWindowMode,
|
|
items: [
|
|
AppSelectionItem(value: 'day', label: l10n.settingsJobWindowModeByDay),
|
|
AppSelectionItem(
|
|
value: 'number',
|
|
label: l10n.settingsJobWindowModeByNumber,
|
|
),
|
|
],
|
|
);
|
|
if (picked != null) {
|
|
setState(() {
|
|
_contextWindowMode = picked;
|
|
});
|
|
}
|
|
}
|
|
|
|
Future<void> _pickRunAt() async {
|
|
final picked = await showTimePicker(context: context, initialTime: _runAt);
|
|
if (picked != null) {
|
|
setState(() {
|
|
_runAt = picked;
|
|
});
|
|
}
|
|
}
|
|
|
|
String _formatTime(TimeOfDay time) {
|
|
final hour = time.hour.toString().padLeft(2, '0');
|
|
final minute = time.minute.toString().padLeft(2, '0');
|
|
return '$hour:$minute:00';
|
|
}
|
|
|
|
String _displayRunAt(ScheduleConfigModel schedule) {
|
|
final hour = schedule.runAt.hour.toString().padLeft(2, '0');
|
|
final minute = schedule.runAt.minute.toString().padLeft(2, '0');
|
|
return '$hour:$minute';
|
|
}
|
|
|
|
String _scheduleLabel(String scheduleType) {
|
|
final l10n = context.l10n;
|
|
final normalized = scheduleType.toLowerCase();
|
|
if (normalized == 'daily') {
|
|
return l10n.settingsJobScheduleDaily;
|
|
}
|
|
if (normalized == 'weekly') {
|
|
return l10n.settingsJobScheduleWeekly;
|
|
}
|
|
return scheduleType;
|
|
}
|
|
|
|
String _contextSourceLabel(String source) {
|
|
if (source == 'latest_chat') {
|
|
return context.l10n.settingsJobContextSourceLatestChat;
|
|
}
|
|
return source;
|
|
}
|
|
|
|
String _windowModeLabel(String mode) {
|
|
if (mode == 'day') {
|
|
return context.l10n.settingsJobWindowModeByDay;
|
|
}
|
|
if (mode == 'number') {
|
|
return context.l10n.settingsJobWindowModeByNumber;
|
|
}
|
|
return mode;
|
|
}
|
|
|
|
Future<void> _submitCreate() async {
|
|
final title = _titleController.text.trim();
|
|
final template = _templateController.text.trim();
|
|
if (title.isEmpty || template.isEmpty) {
|
|
Toast.show(
|
|
context,
|
|
context.l10n.settingsJobFillRequired,
|
|
type: ToastType.error,
|
|
);
|
|
return;
|
|
}
|
|
|
|
final request = AutomationJobCreateRequest(
|
|
title: title,
|
|
timezone: _timezone,
|
|
status: 'active',
|
|
config: AutomationJobConfigModel(
|
|
inputTemplate: template,
|
|
enabledTools: _selectedTools.toList(),
|
|
context: MessageContextConfigModel(
|
|
source: _contextSource,
|
|
windowMode: _contextWindowMode,
|
|
windowCount: _contextWindowCount,
|
|
),
|
|
schedule: ScheduleConfigModel(
|
|
type: _scheduleType,
|
|
runAt: ScheduleRunAtModel(hour: _runAt.hour, minute: _runAt.minute),
|
|
weekdays: _scheduleType == 'weekly'
|
|
? (_selectedWeekdays.toList()..sort())
|
|
: null,
|
|
),
|
|
),
|
|
);
|
|
final success = await _cubit.createJob(request);
|
|
if (!mounted) {
|
|
return;
|
|
}
|
|
if (success) {
|
|
Toast.show(
|
|
context,
|
|
context.l10n.settingsJobCreateSuccess,
|
|
type: ToastType.success,
|
|
);
|
|
context.pop(true);
|
|
}
|
|
}
|
|
}
|