first commit
This commit is contained in:
commit
2a26c396bd
|
|
@ -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
|
||||
|
|
@ -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'
|
||||
|
|
@ -0,0 +1 @@
|
|||
include: package:flutter_lints/flutter.yaml
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
import '../models/instruction.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
abstract class InstructionRepository {
|
||||
Future<List<Instruction>> getInstructions();
|
||||
Future<Instruction> addInstruction(Instruction instruction);
|
||||
Future<void> updateInstruction(Instruction instruction);
|
||||
Future<void> deleteInstruction(String id);
|
||||
}
|
||||
|
||||
|
||||
// ЛОКАЛЬНАЯ РЕАЛИЗАЦИЯ, имитирующая работу с базой данных.
|
||||
class LocalInstructionRepository implements InstructionRepository {
|
||||
final List<Instruction> _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<List<Instruction>> getInstructions() async {
|
||||
// Имитация задержки сети
|
||||
await Future.delayed(const Duration(milliseconds: 400));
|
||||
_instructions.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
return List.of(_instructions);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Instruction> 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<void> 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<void> deleteInstruction(String id) async {
|
||||
await Future.delayed(const Duration(milliseconds: 200));
|
||||
_instructions.removeWhere((i) => i.id == id);
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
class Instruction {
|
||||
final String? id;
|
||||
final String title;
|
||||
final String content;
|
||||
final List<String> 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<String>? 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<String, dynamic> json) {
|
||||
return Instruction(
|
||||
id: json['id'],
|
||||
title: json['title'],
|
||||
content: json['content'],
|
||||
tags: List<String>.from(json['tags'] ?? []),
|
||||
createdAt: DateTime.parse(json['createdAt']),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'title': title,
|
||||
'content': content,
|
||||
'tags': tags,
|
||||
'createdAt': createdAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Instruction> _instructions = [];
|
||||
Set<String> _selectedTags = {};
|
||||
|
||||
bool get isLoading => _isLoading;
|
||||
List<Instruction> get instructions => _instructions;
|
||||
|
||||
List<Instruction> get filteredInstructions {
|
||||
if (_selectedTags.isEmpty) {
|
||||
return _instructions;
|
||||
}
|
||||
return _instructions
|
||||
.where((instr) => _selectedTags.every((tag) => instr.tags.contains(tag)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
Set<String> get allAvailableTags {
|
||||
return _instructions.fold<Set<String>>(
|
||||
<String>{}, (prev, element) => prev..addAll(element.tags));
|
||||
}
|
||||
|
||||
Set<String> get selectedTags => _selectedTags;
|
||||
|
||||
|
||||
Future<void> loadInstructions() async {
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
_instructions = await _repository.getInstructions();
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> addInstruction(String title, String content, List<String> tags) async {
|
||||
final newInstruction = Instruction(
|
||||
title: title,
|
||||
content: content,
|
||||
tags: tags,
|
||||
createdAt: DateTime.now(),
|
||||
);
|
||||
await _repository.addInstruction(newInstruction);
|
||||
await loadInstructions();
|
||||
}
|
||||
|
||||
Future<void> updateInstruction(Instruction instruction) async {
|
||||
await _repository.updateInstruction(instruction);
|
||||
await loadInstructions();
|
||||
}
|
||||
|
||||
Future<void> 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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<AddEditInstructionPage> createState() => _AddEditInstructionPageState();
|
||||
}
|
||||
|
||||
class _AddEditInstructionPageState extends State<AddEditInstructionPage> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
late TextEditingController _titleController;
|
||||
late TextEditingController _contentController;
|
||||
late List<String> _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<String>.from(widget.initialInstruction?.tags ?? []);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_titleController.dispose();
|
||||
_contentController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _showTagSelectionDialog() async {
|
||||
final provider = context.read<InstructionProvider>();
|
||||
final result = await showDialog<List<String>>(
|
||||
context: context,
|
||||
builder: (ctx) => TagSelectionDialog(
|
||||
allTags: provider.allAvailableTags,
|
||||
initialSelectedTags: _selectedTags.toSet(),
|
||||
),
|
||||
);
|
||||
|
||||
if (result != null) {
|
||||
setState(() {
|
||||
_selectedTags = result;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveForm() async {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
final provider = context.read<InstructionProvider>();
|
||||
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(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<AddInstructionPage> createState() => _AddInstructionPageState();
|
||||
}
|
||||
|
||||
class _AddInstructionPageState extends State<AddInstructionPage> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _titleController = TextEditingController();
|
||||
final _contentController = TextEditingController();
|
||||
final _tagController = TextEditingController();
|
||||
|
||||
final List<String> _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<void> _saveInstruction() async {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
final provider = Provider.of<InstructionProvider>(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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<InstructionProvider>().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)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<InstructionListPage> createState() => _InstructionListPageState();
|
||||
}
|
||||
|
||||
class _InstructionListPageState extends State<InstructionListPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
Future.microtask(() =>
|
||||
Provider.of<InstructionProvider>(context, listen: false)
|
||||
.loadInstructions());
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Список инструктажей'),
|
||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
),
|
||||
body: Consumer<InstructionProvider>(
|
||||
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),
|
||||
));
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class TagSelectionDialog extends StatefulWidget {
|
||||
final Set<String> allTags;
|
||||
final Set<String> initialSelectedTags;
|
||||
|
||||
const TagSelectionDialog({
|
||||
super.key,
|
||||
required this.allTags,
|
||||
required this.initialSelectedTags,
|
||||
});
|
||||
|
||||
@override
|
||||
State<TagSelectionDialog> createState() => _TagSelectionDialogState();
|
||||
}
|
||||
|
||||
class _TagSelectionDialogState extends State<TagSelectionDialog> {
|
||||
late Set<String> _selectedTags;
|
||||
late Set<String> _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('Применить'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 917 B |
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 8.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
|
|
@ -0,0 +1,38 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<!--
|
||||
If you are serving your web app in a path other than the root, change the
|
||||
href value below to reflect the base path you are serving from.
|
||||
|
||||
The path provided below has to start and end with a slash "/" in order for
|
||||
it to work correctly.
|
||||
|
||||
For more details:
|
||||
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
|
||||
|
||||
This is a placeholder for base href that will be replaced by the value of
|
||||
the `--base-href` argument provided to `flutter build`.
|
||||
-->
|
||||
<base href="$FLUTTER_BASE_HREF">
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||
<meta name="description" content="A new Flutter project.">
|
||||
|
||||
<!-- iOS meta tags & icons -->
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="apple-mobile-web-app-title" content="instruction_app">
|
||||
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||
|
||||
<title>instruction_app</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
</head>
|
||||
<body>
|
||||
<script src="flutter_bootstrap.js" async></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Reference in New Issue