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