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 createState() => _JobDetailScreenState(); } enum _JobDetailHeaderAction { delete } class _JobDetailScreenState extends State { 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 _selectedWeekdays = {1}; String _contextSource = 'latest_chat'; String _contextWindowMode = 'day'; int _contextWindowCount = 2; final Set _selectedTools = {'memory.write', 'memory.forget'}; ColorScheme get _colorScheme => Theme.of(context).colorScheme; @override void initState() { super.initState(); _cubit = JobDetailCubit(sl()); 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( 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 _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 = { 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 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 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 _pickScheduleType() async { final l10n = context.l10n; final picked = await showAppSelectionSheet( 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 _pickTimezone() async { final picked = await showAppSelectionSheet( 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 _pickContextSource() async { final l10n = context.l10n; final picked = await showAppSelectionSheet( context, title: l10n.settingsJobPickContextSource, selectedValue: _contextSource, items: [ AppSelectionItem( value: 'latest_chat', label: l10n.settingsJobContextSourceLatestChat, ), ], ); if (picked != null) { setState(() { _contextSource = picked; }); } } Future _pickWindowMode() async { final l10n = context.l10n; final picked = await showAppSelectionSheet( 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 _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 _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); } } }