first commit

This commit is contained in:
NekoLaiS 2025-07-15 13:21:47 +05:00
commit 2a26c396bd
23 changed files with 1153 additions and 0 deletions

45
.gitignore vendored Normal file
View File

@ -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

30
.metadata Normal file
View File

@ -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'

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# instruction_app
A new Flutter project.

1
analysis_options.yaml Normal file
View File

@ -0,0 +1 @@
include: package:flutter_lints/flutter.yaml

View File

View File

@ -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);
}
}

37
lib/main.dart Normal file
View File

@ -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(),
);
}
}

View File

@ -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(),
};
}
}

View File

@ -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();
}
}

View File

@ -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(),
),
),
],
),
),
),
);
}
}

View File

@ -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(),
),
],
),
),
),
);
}
}

View File

@ -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)),
),
],
),
),
);
}
}

View File

@ -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),
));
},
),
);
},
),
);
}
}

View File

@ -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('Применить'),
),
],
);
}
}

176
pubspec.lock Normal file
View File

@ -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"

17
pubspec.yaml Normal file
View File

@ -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

BIN
web/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 917 B

BIN
web/icons/Icon-192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
web/icons/Icon-512.png Normal file

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

38
web/index.html Normal file
View File

@ -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>

35
web/manifest.json Normal file
View File

@ -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"
}
]
}