// Copyright 2021 The Flutter team. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:linting_tool/model/profile.dart'; import 'package:linting_tool/model/profiles_store.dart'; import 'package:linting_tool/model/rule.dart'; import 'package:linting_tool/theme/app_theme.dart'; import 'package:linting_tool/theme/colors.dart'; import 'package:provider/provider.dart'; class LintExpansionTile extends StatefulWidget { final Rule rule; const LintExpansionTile({ required this.rule, super.key, }); @override State createState() => _LintExpansionTileState(); } class _LintExpansionTileState extends State { var isExpanded = false; @override Widget build(BuildContext context) { final theme = Theme.of(context); final textTheme = theme.textTheme; final rule = widget.rule; final incompatibleString = rule.incompatible.isNotEmpty ? rule.incompatible.join(', ') : 'none'; final setsString = rule.sets.isNotEmpty ? rule.sets.join(', ') : 'none'; return ExpansionTile( collapsedBackgroundColor: AppColors.white50, title: Text( rule.name, style: textTheme.titleMedium!.copyWith( fontWeight: FontWeight.w700, ), ), subtitle: Text( rule.description, style: textTheme.bodySmall!, ), initiallyExpanded: isExpanded, onExpansionChanged: (value) { setState(() { isExpanded = value; }); }, expandedAlignment: Alignment.centerLeft, childrenPadding: const EdgeInsets.symmetric( horizontal: 16.0, vertical: 8.0, ), backgroundColor: AppColors.white50, maintainState: true, expandedCrossAxisAlignment: CrossAxisAlignment.start, children: [ Text.rich( TextSpan( children: [ TextSpan( text: 'Group:', style: textTheme.titleSmall, ), TextSpan( text: ' ${rule.group}', ), ], ), textAlign: TextAlign.left, ), Text.rich( TextSpan( children: [ TextSpan( text: 'State:', style: textTheme.titleSmall, ), TextSpan( text: ' ${rule.state}', ), ], ), textAlign: TextAlign.left, ), Text.rich( TextSpan( children: [ TextSpan( text: 'Incompatible:', style: textTheme.titleSmall, ), TextSpan( text: ' $incompatibleString', ), ], ), textAlign: TextAlign.left, ), Text.rich( TextSpan( children: [ TextSpan( text: 'Sets:', style: textTheme.titleSmall, ), TextSpan( text: ' $setsString', ), ], ), textAlign: TextAlign.left, ), const SizedBox( height: 16.0, ), MarkdownBody( data: rule.details, selectable: true, styleSheet: AppTheme.buildMarkDownTheme(theme), ), const SizedBox( height: 8.0, ), Align( alignment: Alignment.centerRight, child: ElevatedButton( child: const Text('Add to profile'), onPressed: () async { ProfileType? destinationProfileType = await showDialog( context: context, builder: (context) { return const _ProfileTypeDialog(); }, ); if (!context.mounted) return; if (destinationProfileType == ProfileType.newProfile) { await showDialog( context: context, builder: (context) { return NewProfileDialog(rule: rule); }, ); } else if (destinationProfileType == ProfileType.existingProfile) { await showDialog( context: context, builder: (context) { return ExistingProfileDialog(rule: rule); }, ); } }, ), ), const SizedBox( height: 16.0, ), ], ); } } enum ProfileType { newProfile, existingProfile, } class _ProfileTypeDialog extends StatelessWidget { const _ProfileTypeDialog(); @override Widget build(BuildContext context) { var profilesStore = context.watch(); return AlertDialog( actionsPadding: const EdgeInsets.only( left: 16.0, right: 16.0, bottom: 16.0, ), title: const Text('Select Profile Type'), actions: [ if (profilesStore.savedProfiles.isNotEmpty) ElevatedButton( onPressed: () { Navigator.pop(context, ProfileType.existingProfile); }, child: const Text('Existing'), ), TextButton( onPressed: () { Navigator.pop(context, ProfileType.newProfile); }, child: const Text('New'), ), ], ); } } class NewProfileDialog extends StatefulWidget { final Rule rule; const NewProfileDialog({ required this.rule, super.key, }); @override State createState() => _NewProfileDialogState(); } class _NewProfileDialogState extends State { @override Widget build(BuildContext context) { String name = ''; final formKey = GlobalKey(); return AlertDialog( title: const Text('Create new lint profile'), content: Form( key: formKey, autovalidateMode: AutovalidateMode.onUserInteraction, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Profile Name'), TextFormField( onChanged: (value) { name = value; }, validator: (value) { if (value == null || value.isEmpty) { return 'Name cannot be empty.'; } return null; }, ), ], ), ), actionsPadding: const EdgeInsets.all(16.0), actions: [ TextButton( onPressed: () { Navigator.pop(context); }, child: const Text('Cancel'), ), ElevatedButton( onPressed: () async { if (formKey.currentState!.validate()) { var newProfile = RulesProfile( name: name, rules: [widget.rule], ); await Provider.of(context, listen: false) .addToNewProfile(newProfile); if (!context.mounted) return; Navigator.pop(context); } }, child: const Text('Save'), ), ], ); } } class ExistingProfileDialog extends StatefulWidget { const ExistingProfileDialog({ super.key, required this.rule, }); final Rule rule; @override State createState() => _ExistingProfileDialogState(); } class _ExistingProfileDialogState extends State { @override Widget build(BuildContext context) { final profilesStore = Provider.of(context); final savedProfiles = profilesStore.savedProfiles; return AlertDialog( title: const Text('Select a lint profile'), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: List.generate( savedProfiles.length, (index) => ListTile( title: Text(savedProfiles[index].name), onTap: () async { await profilesStore.addToExistingProfile( savedProfiles[index], widget.rule); if (!context.mounted) return; Navigator.pop(context); }, ), ), ), actionsPadding: const EdgeInsets.all(16.0), actions: [ TextButton( onPressed: () { Navigator.pop(context); }, child: const Text('Cancel'), ), ], ); } }