commit 2a26c396bdc9f1e07a8118cfdc64237002f81f89 Author: NekoLaiS Date: Tue Jul 15 13:21:47 2025 +0500 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..79c113f --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..4387f5e --- /dev/null +++ b/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "35c388afb57ef061d06a39b537336c87e0e3d1b1" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 + base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 + - platform: web + create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 + base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/README.md b/README.md new file mode 100644 index 0000000..fb2856c --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# instruction_app + +A new Flutter project. diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..f9b3034 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1 @@ +include: package:flutter_lints/flutter.yaml diff --git a/lib/data/api_instruction_repository.dart b/lib/data/api_instruction_repository.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/data/instruction_repository.dart b/lib/data/instruction_repository.dart new file mode 100644 index 0000000..6f52736 --- /dev/null +++ b/lib/data/instruction_repository.dart @@ -0,0 +1,75 @@ +import '../models/instruction.dart'; +import 'package:uuid/uuid.dart'; + +abstract class InstructionRepository { + Future> getInstructions(); + Future addInstruction(Instruction instruction); + Future updateInstruction(Instruction instruction); + Future deleteInstruction(String id); +} + + +// ЛОКАЛЬНАЯ РЕАЛИЗАЦИЯ, имитирующая работу с базой данных. +class LocalInstructionRepository implements InstructionRepository { + final List _instructions = [ + Instruction( + id: '1', + title: 'Инструктаж по пожарной безопасности', + content: '1. Не курить в помещении. 2. В случае пожара звонить 101. 3. Использовать огнетушитель, расположенный у выхода.', + tags: ['безопасность', 'офис'], + createdAt: DateTime.now().subtract(const Duration(days: 2)), + ), + Instruction( + id: '2', + title: 'Работа с новым ПО "Феникс"', + content: 'Подробное описание как работать с новым программным обеспечением "Феникс".', + tags: ['it', 'софт'], + createdAt: DateTime.now().subtract(const Duration(days: 1)), + ), + Instruction( + id: '3', + title: 'Правила поведения на новогоднем корпоративе', + content: 'Веселиться, но в меру. Помнить о субординации. Не злоупотреблять напитками.', + tags: ['офис', 'развлечения', 'hr'], + createdAt: DateTime.now(), + ), + ]; + + final _uuid = const Uuid(); + + @override + Future> getInstructions() async { + // Имитация задержки сети + await Future.delayed(const Duration(milliseconds: 400)); + _instructions.sort((a, b) => b.createdAt.compareTo(a.createdAt)); + return List.of(_instructions); + } + + @override + Future addInstruction(Instruction instruction) async { + await Future.delayed(const Duration(milliseconds: 300)); + final newInstruction = instruction.copyWith( + id: _uuid.v4(), + createdAt: DateTime.now(), + ); + _instructions.add(newInstruction); + return newInstruction; + } + + @override + Future updateInstruction(Instruction instruction) async { + await Future.delayed(const Duration(milliseconds: 300)); + final index = _instructions.indexWhere((i) => i.id == instruction.id); + if (index != -1) { + _instructions[index] = instruction; + } else { + throw Exception('Instruction not found'); + } + } + + @override + Future deleteInstruction(String id) async { + await Future.delayed(const Duration(milliseconds: 200)); + _instructions.removeWhere((i) => i.id == id); + } +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..29ce80f --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,37 @@ +// lib/main.dart + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'data/instruction_repository.dart'; +import 'providers/instruction_provider.dart'; +import 'screens/instruction_list_page.dart'; +//import 'data/api_instruction_repository.dart'; + +void main() { + final InstructionRepository repository = LocalInstructionRepository(); + //LocalInstructionRepository(); + + runApp( + // "Предоставляем" наш провайдер всему дереву виджетов + ChangeNotifierProvider( + create: (context) => InstructionProvider(repository), + child: const MyApp(), + ), + ); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Инструктажи', + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), + useMaterial3: true, + ), + home: const InstructionListPage(), + ); + } +} \ No newline at end of file diff --git a/lib/models/instruction.dart b/lib/models/instruction.dart new file mode 100644 index 0000000..3fe8449 --- /dev/null +++ b/lib/models/instruction.dart @@ -0,0 +1,50 @@ +class Instruction { + final String? id; + final String title; + final String content; + final List tags; + final DateTime createdAt; + + Instruction({ + this.id, + required this.title, + required this.content, + required this.tags, + required this.createdAt, + }); + + Instruction copyWith({ + String? id, + String? title, + String? content, + List? tags, + DateTime? createdAt, + }) { + return Instruction( + id: id ?? this.id, + title: title ?? this.title, + content: content ?? this.content, + tags: tags ?? this.tags, + createdAt: createdAt ?? this.createdAt, + ); + } + factory Instruction.fromJson(Map json) { + return Instruction( + id: json['id'], + title: json['title'], + content: json['content'], + tags: List.from(json['tags'] ?? []), + createdAt: DateTime.parse(json['createdAt']), + ); + } + + Map toJson() { + return { + 'id': id, + 'title': title, + 'content': content, + 'tags': tags, + 'createdAt': createdAt.toIso8601String(), + }; + } +} \ No newline at end of file diff --git a/lib/providers/instruction_provider.dart b/lib/providers/instruction_provider.dart new file mode 100644 index 0000000..1f25c88 --- /dev/null +++ b/lib/providers/instruction_provider.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import '../data/instruction_repository.dart'; +import '../models/instruction.dart'; + +class InstructionProvider extends ChangeNotifier { + final InstructionRepository _repository; + + InstructionProvider(this._repository); + + bool _isLoading = false; + List _instructions = []; + Set _selectedTags = {}; + + bool get isLoading => _isLoading; + List get instructions => _instructions; + + List get filteredInstructions { + if (_selectedTags.isEmpty) { + return _instructions; + } + return _instructions + .where((instr) => _selectedTags.every((tag) => instr.tags.contains(tag))) + .toList(); + } + + Set get allAvailableTags { + return _instructions.fold>( + {}, (prev, element) => prev..addAll(element.tags)); + } + + Set get selectedTags => _selectedTags; + + + Future loadInstructions() async { + _isLoading = true; + notifyListeners(); + _instructions = await _repository.getInstructions(); + _isLoading = false; + notifyListeners(); + } + + Future addInstruction(String title, String content, List tags) async { + final newInstruction = Instruction( + title: title, + content: content, + tags: tags, + createdAt: DateTime.now(), + ); + await _repository.addInstruction(newInstruction); + await loadInstructions(); + } + + Future updateInstruction(Instruction instruction) async { + await _repository.updateInstruction(instruction); + await loadInstructions(); + } + + Future deleteInstruction(String id) async { + await _repository.deleteInstruction(id); + _instructions.removeWhere((instr) => instr.id == id); + notifyListeners(); + } + + void toggleTagFilter(String tag) { + if (_selectedTags.contains(tag)) { + _selectedTags.remove(tag); + } else { + _selectedTags.add(tag); + } + notifyListeners(); + } +} \ No newline at end of file diff --git a/lib/screens/add_edit_instruction_page.dart b/lib/screens/add_edit_instruction_page.dart new file mode 100644 index 0000000..26fe43c --- /dev/null +++ b/lib/screens/add_edit_instruction_page.dart @@ -0,0 +1,150 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../models/instruction.dart'; +import '../providers/instruction_provider.dart'; +import '../widgets/tag_selection_dialog.dart'; + +class AddEditInstructionPage extends StatefulWidget { + final Instruction? initialInstruction; + + const AddEditInstructionPage({super.key, this.initialInstruction}); + + @override + State createState() => _AddEditInstructionPageState(); +} + +class _AddEditInstructionPageState extends State { + final _formKey = GlobalKey(); + late TextEditingController _titleController; + late TextEditingController _contentController; + late List _selectedTags; + + bool get _isEditing => widget.initialInstruction != null; + + @override + void initState() { + super.initState(); + _titleController = TextEditingController(text: widget.initialInstruction?.title ?? ''); + _contentController = TextEditingController(text: widget.initialInstruction?.content ?? ''); + _selectedTags = List.from(widget.initialInstruction?.tags ?? []); + } + + @override + void dispose() { + _titleController.dispose(); + _contentController.dispose(); + super.dispose(); + } + + Future _showTagSelectionDialog() async { + final provider = context.read(); + final result = await showDialog>( + context: context, + builder: (ctx) => TagSelectionDialog( + allTags: provider.allAvailableTags, + initialSelectedTags: _selectedTags.toSet(), + ), + ); + + if (result != null) { + setState(() { + _selectedTags = result; + }); + } + } + + Future _saveForm() async { + if (_formKey.currentState!.validate()) { + final provider = context.read(); + final title = _titleController.text; + final content = _contentController.text; + + showDialog( + context: context, + barrierDismissible: false, + builder: (ctx) => const Center(child: CircularProgressIndicator())); + + try { + if (_isEditing) { + final updatedInstruction = widget.initialInstruction!.copyWith( + title: title, + content: content, + tags: _selectedTags, + ); + await provider.updateInstruction(updatedInstruction); + } else { + await provider.addInstruction(title, content, _selectedTags); + } + if (mounted) { + Navigator.of(context)..pop()..pop(); + } + } catch (e) { + if(mounted) { + Navigator.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Ошибка сохранения: $e')), + ); + } + } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(_isEditing ? 'Редактировать' : 'Новый инструктаж'), + actions: [IconButton(icon: const Icon(Icons.save), onPressed: _saveForm)], + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Form( + key: _formKey, + child: ListView( + children: [ + TextFormField( + controller: _titleController, + decoration: const InputDecoration(labelText: 'Название', border: OutlineInputBorder()), + validator: (v) => v == null || v.isEmpty ? 'Название не может быть пустым' : null, + ), + const SizedBox(height: 16), + TextFormField( + controller: _contentController, + decoration: const InputDecoration(labelText: 'Содержание', border: OutlineInputBorder()), + maxLines: 8, + validator: (v) => v == null || v.isEmpty ? 'Содержание не может быть пустым' : null, + ), + const SizedBox(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Теги:', style: TextStyle(fontSize: 16)), + OutlinedButton.icon( + icon: const Icon(Icons.edit_note), + label: const Text('Изменить теги'), + onPressed: _showTagSelectionDialog, + ), + ], + ), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade400), + borderRadius: BorderRadius.circular(4), + ), + child: Wrap( + spacing: 8.0, + runSpacing: 4.0, + children: _selectedTags.isEmpty + ? [const Text('Теги не выбраны')] + : _selectedTags.map((tag) => Chip(label: Text(tag))).toList(), + ), + ), + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/add_instruction_page.dart b/lib/screens/add_instruction_page.dart new file mode 100644 index 0000000..a5f03db --- /dev/null +++ b/lib/screens/add_instruction_page.dart @@ -0,0 +1,118 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../providers/instruction_provider.dart'; + +class AddInstructionPage extends StatefulWidget { + const AddInstructionPage({super.key}); + + @override + State createState() => _AddInstructionPageState(); +} + +class _AddInstructionPageState extends State { + final _formKey = GlobalKey(); + final _titleController = TextEditingController(); + final _contentController = TextEditingController(); + final _tagController = TextEditingController(); + + final List _tags = []; + + void _addTag() { + final tag = _tagController.text.trim().toLowerCase(); + if (tag.isNotEmpty && !_tags.contains(tag)) { + setState(() { + _tags.add(tag); + _tagController.clear(); + }); + } + } + + void _removeTag(String tag) { + setState(() { + _tags.remove(tag); + }); + } + + Future _saveInstruction() async { + if (_formKey.currentState!.validate()) { + final provider = Provider.of(context, listen: false); + await provider.addInstruction( + _titleController.text, + _contentController.text, + _tags, + ); + if (mounted) { + Navigator.of(context).pop(); + } + } + } + + @override + void dispose() { + _titleController.dispose(); + _contentController.dispose(); + _tagController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Новый инструктаж'), + actions: [ + IconButton( + icon: const Icon(Icons.save), + onPressed: _saveInstruction, + ) + ], + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Form( + key: _formKey, + child: ListView( + children: [ + TextFormField( + controller: _titleController, + decoration: const InputDecoration(labelText: 'Название'), + validator: (value) => (value == null || value.isEmpty) ? 'Введите название' : null, + ), + const SizedBox(height: 16), + TextFormField( + controller: _contentController, + decoration: const InputDecoration(labelText: 'Содержание'), + maxLines: 5, + validator: (value) => (value == null || value.isEmpty) ? 'Введите содержание' : null, + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: TextField( + controller: _tagController, + decoration: const InputDecoration(labelText: 'Добавить тег'), + onSubmitted: (_) => _addTag(), + ), + ), + IconButton( + icon: const Icon(Icons.add_circle_outline), + onPressed: _addTag, + ), + ], + ), + const SizedBox(height: 10), + Wrap( + spacing: 8.0, + children: _tags.map((tag) => Chip( + label: Text(tag), + onDeleted: () => _removeTag(tag), + )).toList(), + ), + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/instruction_detail_page.dart b/lib/screens/instruction_detail_page.dart new file mode 100644 index 0000000..78af960 --- /dev/null +++ b/lib/screens/instruction_detail_page.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../models/instruction.dart'; +import '../providers/instruction_provider.dart'; +import 'add_edit_instruction_page.dart'; + +class InstructionDetailPage extends StatelessWidget { + final Instruction instruction; + + const InstructionDetailPage({super.key, required this.instruction}); + + void _deleteInstruction(BuildContext context) { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('Подтверждение'), + content: const Text('Вы уверены, что хотите удалить этот инструктаж?'), + actions: [ + TextButton(onPressed: () => Navigator.of(ctx).pop(), child: const Text('Отмена')), + TextButton( + onPressed: () { + context.read().deleteInstruction(instruction.id!); + Navigator.of(ctx).pop(); + Navigator.of(context).pop(); + }, + child: const Text('Удалить', style: TextStyle(color: Colors.red)), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(instruction.title, overflow: TextOverflow.ellipsis), + actions: [ + IconButton( + icon: const Icon(Icons.edit), + tooltip: 'Редактировать', + onPressed: () => Navigator.of(context).push(MaterialPageRoute( + builder: (ctx) => AddEditInstructionPage(initialInstruction: instruction), + )), + ), + IconButton( + icon: const Icon(Icons.delete), + tooltip: 'Удалить', + onPressed: () => _deleteInstruction(context), + ), + ], + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: ListView( + children: [ + Text('Теги:', style: Theme.of(context).textTheme.titleMedium), + const SizedBox(height: 8), + if (instruction.tags.isEmpty) + const Text('Теги отсутствуют') + else + Wrap( + spacing: 8.0, + runSpacing: 4.0, + children: instruction.tags.map((tag) => Chip(label: Text(tag))).toList(), + ), + const SizedBox(height: 24), + Text('Содержание:', style: Theme.of(context).textTheme.titleMedium), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.05), + borderRadius: BorderRadius.circular(8), + ), + child: Text(instruction.content, style: const TextStyle(fontSize: 16, height: 1.5)), + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/instruction_list_page.dart b/lib/screens/instruction_list_page.dart new file mode 100644 index 0000000..9ed1ff3 --- /dev/null +++ b/lib/screens/instruction_list_page.dart @@ -0,0 +1,112 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../providers/instruction_provider.dart'; +import 'add_edit_instruction_page.dart'; +import 'instruction_detail_page.dart'; + +class InstructionListPage extends StatefulWidget { + const InstructionListPage({super.key}); + + @override + State createState() => _InstructionListPageState(); +} + +class _InstructionListPageState extends State { + @override + void initState() { + super.initState(); + Future.microtask(() => + Provider.of(context, listen: false) + .loadInstructions()); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Список инструктажей'), + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + ), + body: Consumer( + builder: (context, provider, child) { + return Column( + children: [ + _buildFilterChips(provider), + if (provider.isLoading) + const Expanded(child: Center(child: CircularProgressIndicator())) + else + _buildInstructionList(provider), + ], + ); + }, + ), + floatingActionButton: FloatingActionButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute(builder: (context) => const AddEditInstructionPage()), + ); + }, + tooltip: 'Добавить инструктаж', + child: const Icon(Icons.add), + ), + ); + } + + Widget _buildFilterChips(InstructionProvider provider) { + if (provider.allAvailableTags.isEmpty) return const SizedBox.shrink(); + + return Padding( + padding: const EdgeInsets.all(8.0), + child: Wrap( + spacing: 8.0, + runSpacing: 4.0, + children: provider.allAvailableTags.map((tag) { + final isSelected = provider.selectedTags.contains(tag); + return FilterChip( + label: Text(tag), + selected: isSelected, + onSelected: (_) => provider.toggleTagFilter(tag), + selectedColor: Theme.of(context).colorScheme.primaryContainer, + ); + }).toList(), + ), + ); + } + + Widget _buildInstructionList(InstructionProvider provider) { + if (provider.filteredInstructions.isEmpty && !provider.isLoading) { + return const Expanded( + child: Center( + child: Text( + 'Инструктажи не найдены.\nПопробуйте сбросить фильтры или добавить новый.', + textAlign: TextAlign.center, + ), + ), + ); + } + return Expanded( + child: ListView.builder( + itemCount: provider.filteredInstructions.length, + itemBuilder: (context, index) { + final instruction = provider.filteredInstructions[index]; + return Card( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: ListTile( + title: Text(instruction.title), + subtitle: Text( + 'Теги: ${instruction.tags.join(', ')}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + onTap: () { + Navigator.of(context).push(MaterialPageRoute( + builder: (ctx) => InstructionDetailPage(instruction: instruction), + )); + }, + ), + ); + }, + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/tag_selection_dialog.dart b/lib/widgets/tag_selection_dialog.dart new file mode 100644 index 0000000..d72b709 --- /dev/null +++ b/lib/widgets/tag_selection_dialog.dart @@ -0,0 +1,111 @@ +import 'package:flutter/material.dart'; + +class TagSelectionDialog extends StatefulWidget { + final Set allTags; + final Set initialSelectedTags; + + const TagSelectionDialog({ + super.key, + required this.allTags, + required this.initialSelectedTags, + }); + + @override + State createState() => _TagSelectionDialogState(); +} + +class _TagSelectionDialogState extends State { + late Set _selectedTags; + late Set _availableTags; + final _newTagController = TextEditingController(); + + @override + void initState() { + super.initState(); + _selectedTags = {...widget.initialSelectedTags}; + _availableTags = {...widget.allTags}; + } + + @override + void dispose() { + _newTagController.dispose(); + super.dispose(); + } + + void _addNewTag() { + final tag = _newTagController.text.trim().toLowerCase(); + if (tag.isNotEmpty && !tag.contains(' ')) { + setState(() { + _availableTags.add(tag); + _selectedTags.add(tag); // Сразу делаем новый тег выбранным + _newTagController.clear(); + }); + } + } + + @override + Widget build(BuildContext context) { + final sortedTags = _availableTags.toList()..sort(); + + return AlertDialog( + title: const Text('Выберите теги'), + content: SizedBox( + width: double.maxFinite, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: _newTagController, + decoration: InputDecoration( + labelText: 'Добавить новый тег', + suffixIcon: IconButton( + icon: const Icon(Icons.add_circle_outline), + onPressed: _addNewTag, + ), + ), + onSubmitted: (_) => _addNewTag(), + ), + const SizedBox(height: 16), + Expanded( + child: sortedTags.isEmpty + ? const Center(child: Text('Доступных тегов нет')) + : ListView.builder( + shrinkWrap: true, + itemCount: sortedTags.length, + itemBuilder: (context, index) { + final tag = sortedTags[index]; + return CheckboxListTile( + title: Text(tag), + value: _selectedTags.contains(tag), + onChanged: (isSelected) { + setState(() { + if (isSelected ?? false) { + _selectedTags.add(tag); + } else { + _selectedTags.remove(tag); + } + }); + }, + ); + }, + ), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Отмена'), + ), + FilledButton( + onPressed: () { + // Возвращаем результат + Navigator.of(context).pop(_selectedTags.toList()..sort()); + }, + child: const Text('Применить'), + ), + ], + ); + } +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..54856d1 --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,176 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_hooks: + dependency: "direct main" + description: + name: flutter_hooks + sha256: cde36b12f7188c85286fba9b38cc5a902e7279f36dd676967106c041dc9dde70 + url: "https://pub.dev" + source: hosted + version: "0.20.5" + http: + dependency: "direct main" + description: + name: http + sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" + url: "https://pub.dev" + source: hosted + version: "0.13.6" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.dev" + source: hosted + version: "1.16.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + provider: + dependency: "direct main" + description: + name: provider + sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84" + url: "https://pub.dev" + source: hosted + version: "6.1.5" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + uuid: + dependency: "direct main" + description: + name: uuid + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + url: "https://pub.dev" + source: hosted + version: "4.5.1" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" +sdks: + dart: ">=3.7.0-0 <4.0.0" + flutter: ">=3.0.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..a1321c3 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,17 @@ +name: instruction_app +description: A demo app for showing instructions with tags. +version: 1.0.0 + +environment: + sdk: ">=2.17.0 <4.0.0" + +dependencies: + flutter: + sdk: flutter + http: ^0.13.6 + flutter_hooks: ^0.20.5 + provider: ^6.1.2 # Используем provider + uuid: ^4.3.3 + +flutter: + uses-material-design: true \ No newline at end of file diff --git a/web/favicon.png b/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/web/favicon.png differ diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/web/icons/Icon-192.png differ diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/web/icons/Icon-512.png differ diff --git a/web/icons/Icon-maskable-192.png b/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/web/icons/Icon-maskable-192.png differ diff --git a/web/icons/Icon-maskable-512.png b/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/web/icons/Icon-maskable-512.png differ diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..7de7408 --- /dev/null +++ b/web/index.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + instruction_app + + + + + + diff --git a/web/manifest.json b/web/manifest.json new file mode 100644 index 0000000..ac5e5d3 --- /dev/null +++ b/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "instruction_app", + "short_name": "instruction_app", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +}