From ec4445839443d96f483dc0416931f3f2b813e490 Mon Sep 17 00:00:00 2001 From: myDan Date: Tue, 31 Mar 2026 17:58:35 +0300 Subject: [PATCH 01/22] conf-(ci) add CI workflow for tests and Dart docs deployment ->27 --- .github/workflows/ci.yml | 25 +++++++++++++++++++ .github/workflows/pages.yml | 48 +++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/pages.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d6130f2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,25 @@ +name: CI + +on: + pull_request: + branches: + - main + +jobs: + test: + name: Run tests + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Flutter + uses: subosito/flutter-action@v2 + + - name: Install dependencies + working-directory: manylines_editor + run: flutter pub get + + - name: Run tests + working-directory: manylines_editor + run: flutter test \ No newline at end of file diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..8888aac --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,48 @@ +name: Deploy Dart docs to GitHub Pages + +on: + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + + +jobs: + build: + name: Build documentation + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Flutter + uses: subosito/flutter-action@v2 + + - name: Install dependencies + working-directory: manylines_editor + run: flutter pub get + + - name: Generate docs + working-directory: manylines_editor + run: dart doc . + + - name: Upload Pages artifact from Dart docs + uses: actions/upload-pages-artifact@v3 + with: + path: manylines_editor/doc/api + deploy: + name: Deploy to GitHub Pages + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 From 58df2046fce017dfd518f3d6a4c6f988d19e94b1 Mon Sep 17 00:00:00 2001 From: myDan Date: Tue, 31 Mar 2026 18:22:38 +0300 Subject: [PATCH 02/22] fix-(ci) fix pages pipeline, run workflow on PR ->27 --- .github/workflows/pages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 8888aac..164073e 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -1,7 +1,7 @@ name: Deploy Dart docs to GitHub Pages on: - push: + pull_request: branches: - main workflow_dispatch: From e1fe3d3a8f0bf802d553545f27d69b8a1ab3e593 Mon Sep 17 00:00:00 2001 From: gitmaxlla <146841763+gitmaxlla@users.noreply.github.com> Date: Tue, 31 Mar 2026 21:53:40 +0300 Subject: [PATCH 03/22] fix-(ci) changed trigger from PR to Push -> 27 --- .github/workflows/pages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 164073e..8888aac 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -1,7 +1,7 @@ name: Deploy Dart docs to GitHub Pages on: - pull_request: + push: branches: - main workflow_dispatch: From 8b76594dc904c081e37aad239321cfdb9514d299 Mon Sep 17 00:00:00 2001 From: Ulquiorra-Keesre Date: Mon, 6 Apr 2026 12:04:13 +0300 Subject: [PATCH 04/22] Home Page1 --- manylines_editor/lib/main.dart | 756 +++++++++++++++--- .../flutter/generated_plugin_registrant.cc | 4 + .../linux/flutter/generated_plugins.cmake | 1 + .../Flutter/GeneratedPluginRegistrant.swift | 4 + manylines_editor/pubspec.lock | 396 ++++++++- manylines_editor/pubspec.yaml | 6 + .../flutter/generated_plugin_registrant.cc | 6 + .../windows/flutter/generated_plugins.cmake | 2 + 8 files changed, 1079 insertions(+), 96 deletions(-) diff --git a/manylines_editor/lib/main.dart b/manylines_editor/lib/main.dart index 6ae3dee..7a0fdee 100644 --- a/manylines_editor/lib/main.dart +++ b/manylines_editor/lib/main.dart @@ -1,123 +1,697 @@ import 'package:flutter/material.dart'; +import 'package:flutter_quill/flutter_quill.dart' as quill; +import 'package:provider/provider.dart'; +import 'package:dart_quill_delta/dart_quill_delta.dart'; +// ==================== МОДЕЛИ ==================== +class Project { + final String id; + final String name; + final List documents; + + Project({required this.id, required this.name, required this.documents}); +} + +class AppDocument { + final String id; + final String name; + int viewCount; + Delta content; + + AppDocument({ + required this.id, + required this.name, + this.viewCount = 0, + required this.content, + }); + + // Вычисляемое свойство + bool get isMostUsed => viewCount > 0; + + static AppDocument? getMostUsed(List docs) { + if (docs.isEmpty) return null; + final sorted = List.from(docs) + ..sort((a, b) => b.viewCount.compareTo(a.viewCount)); + return sorted.first; + } +} + +// ==================== STATE ==================== +// ==================== STATE ==================== +class AppState extends ChangeNotifier { + final List _projects = [ + Project( + id: 'p1', + name: 'Project 1', + documents: [ + AppDocument( + id: 'd1', + name: 'Main Document', + viewCount: 15, + content: Delta()..insert('Welcome to Project 1!\n'), + ), + AppDocument( + id: 'd2', + name: 'Specifications', + viewCount: 8, + content: Delta()..insert('Technical specifications...\n'), + ), + ], + ), + Project( + id: 'p2', + name: 'Project 2', + documents: [ + AppDocument( + id: 'd3', + name: 'Overview', + viewCount: 23, + content: Delta()..insert('Project overview...\n'), + ), + ], + ), + ]; + + // ✅ Настройки (список для перетаскивания) + List> _settings = [ + {'id': 'setting1', 'name': 'Setting 1', 'expanded': true, 'enabled': false}, + {'id': 'setting2', 'name': 'Setting 2', 'expanded': false, 'enabled': false}, + {'id': 'setting3', 'name': 'Setting 3', 'expanded': true, 'enabled': false}, + ]; + + // ✅ Переключатель Switchable + bool _switchableValue = true; + + Project? _selectedProject; + AppDocument? _selectedDocument; + + // Хранилище контроллеров для сохранения изменений + final Map _editors = {}; + + // ==================== GETTERS ==================== + List get projects => _projects; + List> get settings => _settings; // ✅ Добавлен геттер + bool get switchableValue => _switchableValue; // ✅ Добавлен геттер + Project? get selectedProject => _selectedProject; + AppDocument? get selectedDocument => _selectedDocument; + + // ==================== METHODS ==================== + + // Увеличить счётчик просмотров + void incrementViewCount(AppDocument doc) { + doc.viewCount++; + notifyListeners(); + } + + // Сохранить изменения документа + void saveDocumentContent(AppDocument doc, Delta content) { + doc.content = content; + notifyListeners(); + } + + // Получить или создать контроллер для документа + quill.QuillController getOrCreateController(AppDocument doc) { + if (!_editors.containsKey(doc.id)) { + _editors[doc.id] = quill.QuillController( + document: quill.Document.fromJson(doc.content.toJson()), + selection: const TextSelection.collapsed(offset: 0), + ); + + _editors[doc.id]!.changes.listen((_) { + saveDocumentContent(doc, _editors[doc.id]!.document.toDelta()); + }); + } + return _editors[doc.id]!; + } + + void addProject() { + final newId = 'p${_projects.length + 1}'; + _projects.add(Project( + id: newId, + name: 'Project ${_projects.length + 1}', + documents: [ + AppDocument( + id: 'd${DateTime.now().millisecondsSinceEpoch}', + name: 'New Document', + viewCount: 1, + content: Delta()..insert('Start typing...\n'), + ), + ], + )); + notifyListeners(); + } + + void selectProject(Project project) { + _selectedProject = project; + final mostUsed = AppDocument.getMostUsed(project.documents); + _selectedDocument = mostUsed ?? project.documents.first; + if (_selectedDocument != null) { + incrementViewCount(_selectedDocument!); + } + notifyListeners(); + } + + void selectDocument(AppDocument document) { + _selectedDocument = document; + incrementViewCount(document); + notifyListeners(); + } + + void clearSelectedProject() { + _selectedProject = null; + _selectedDocument = null; + notifyListeners(); + } + + // ✅ Метод для переключения Switchable + void setSwitchableValue(bool value) { + _switchableValue = value; + notifyListeners(); + } + + // ✅ Метод для перетаскивания настроек + void reorderSettings(int oldIndex, int newIndex) { + if (newIndex > oldIndex) { + newIndex -= 1; + } + final item = _settings.removeAt(oldIndex); + _settings.insert(newIndex, item); + notifyListeners(); + } + + // ✅ Метод для разворачивания/сворачивания настройки + void toggleSettingExpansion(String id) { + for (var setting in _settings) { + if (setting['id'] == id) { + setting['expanded'] = !setting['expanded']; + notifyListeners(); + break; + } + } + } + + // ✅ Метод для включения/выключения настройки + void toggleSettingEnabled(String id, bool value) { + for (var setting in _settings) { + if (setting['id'] == id) { + setting['enabled'] = value; + notifyListeners(); + break; + } + } + } + + @override + void dispose() { + for (var controller in _editors.values) { + controller.dispose(); + } + super.dispose(); + } +} + +// ==================== APP ==================== void main() { - runApp(const MyApp()); + runApp( + ChangeNotifierProvider( + create: (_) => AppState(), + child: MaterialApp( + title: 'Manyllines', + theme: ThemeData( + useMaterial3: true, + colorSchemeSeed: Colors.green, + fontFamily: 'Roboto', + ), + home: AppShell(), + ), + ), + ); } -class MyApp extends StatelessWidget { - const MyApp({super.key}); +class AppShell extends StatelessWidget { + const AppShell({super.key}); - // This widget is the root of your application. @override Widget build(BuildContext context) { - return MaterialApp( - title: 'Flutter Demo', - theme: ThemeData( - // This is the theme of your application. - // - // TRY THIS: Try running your application with "flutter run". You'll see - // the application has a purple toolbar. Then, without quitting the app, - // try changing the seedColor in the colorScheme below to Colors.green - // and then invoke "hot reload" (save your changes or press the "hot - // reload" button in a Flutter-supported IDE, or press "r" if you used - // the command line to start the app). - // - // Notice that the counter didn't reset back to zero; the application - // state is not lost during the reload. To reset the state, use hot - // restart instead. - // - // This works for code too, not just values: Most code changes can be - // tested with just a hot reload. - colorScheme: .fromSeed(seedColor: Colors.deepPurple), + return Selector( + selector: (_, state) => state.selectedProject, + builder: (context, selectedProject, _) { + return selectedProject == null + ? const ProjectsScreen() + : const ProjectWorkspace(); + }, + ); + } +} + +// ==================== ЭКРАН ПРОЕКТОВ (как на скрине) ==================== +class ProjectsScreen extends StatelessWidget { + const ProjectsScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Column( + children: [ + // Header с Logo и Manyllines + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + border: Border(bottom: BorderSide(color: Colors.grey[300]!)), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey[400]!), + color: Colors.white, + ), + child: const Text( + 'Logo', + style: TextStyle(fontWeight: FontWeight.bold), + ), + ), + const SizedBox(width: 12), + const Expanded( + child: Text( + 'Manyllines', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ), + + // Список проектов (зелёный фон) + Container( + color: Colors.green[50], + child: Consumer( + builder: (context, state, _) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: state.projects.map((project) { + return Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide(color: Colors.green[200]!), + ), + ), + child: CheckboxListTile( + value: false, + onChanged: (_) => state.selectProject(project), + title: Text(project.name), + controlAffinity: ListTileControlAffinity.trailing, + contentPadding: const EdgeInsets.symmetric(horizontal: 16), + ), + ); + }).toList(), + ); + }, + ), + ), + + // Настройки (голубой фон) + // Настройки с возможностью перетаскивания (голубой фон) +Container( + color: Colors.blue[50], + height: 200, + child: Consumer( + builder: (context, state, _) { + // Если Switchable = true → показываем ReorderableListView + // Если Switchable = false → показываем обычный Column + if (state.switchableValue) { + return ReorderableListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: state.settings.length, + onReorder: state.reorderSettings, + itemBuilder: (context, index) { + final setting = state.settings[index]; + return _buildSettingRow( + setting['name'], + setting['expanded'], + setting['enabled'], + setting['id'], + state, + isDraggable: true, // ← Разрешаем перетаскивание + ); + }, + ); + } else { + // Обычный список без перетаскивания + return ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: state.settings.length, + itemBuilder: (context, index) { + final setting = state.settings[index]; + return _buildSettingRow( + setting['name'], + setting['expanded'], + setting['enabled'], + setting['id'], + state, + isDraggable: false, // ← Запрещаем перетаскивание + ); + }, + ); + } + }, + ), +), + + // Description с кнопками A, B, C + Container( + padding: const EdgeInsets.all(16), + alignment: Alignment.centerLeft, + child: Text( + 'Description', + style: TextStyle(fontSize: 14, color: Colors.grey[700]), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + Expanded(child: _buildButton('A')), + const SizedBox(width: 8), + Expanded(child: _buildButton('B')), + const SizedBox(width: 8), + Expanded(child: _buildButton('C')), + ], + ), + ), + const SizedBox(height: 16), + + // Switchable + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Consumer( // ← Оберните в Consumer + builder: (context, state, _) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Switchable'), + Switch( + value: state.switchableValue, // ← Используйте состояние + onChanged: (value) { + state.setSwitchableValue(value); // ← Сохраняйте изменение + }, + ), + ], + ); + }, + ), + ), + + // Listable + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Listable'), + const Icon(Icons.arrow_drop_down), + ], + ), + ), + + // Additional settings (голубой фон) + Expanded( + child: Container( + color: Colors.blue[50], + padding: const EdgeInsets.all(16), + child: const Center( + child: Text( + 'Setting ...', + style: TextStyle(color: Colors.black54), + ), + ), + ), + ), + ], + ), + floatingActionButton: FloatingActionButton( + onPressed: () => context.read().addProject(), + child: const Icon(Icons.add), ), - home: const MyHomePage(title: 'Flutter Demo Home Page'), ); } + + Widget _buildSettingRow( + String title, + bool expanded, + bool enabled, + String id, + AppState state, { + bool isDraggable = false, // ← Новый параметр +}) { + return Container( + key: ValueKey(id), // Обязательно для ReorderableListView! + decoration: BoxDecoration( + border: Border(bottom: BorderSide(color: Colors.blue[200]!)), + color: Colors.white, + ), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(title), + Row( + children: [ + IconButton( + icon: Icon(expanded ? Icons.arrow_drop_down : Icons.arrow_drop_up), + onPressed: () => state.toggleSettingExpansion(id), + ), + const SizedBox(width: 8), + Checkbox( + value: enabled, + onChanged: (value) { + state.toggleSettingEnabled(id, value ?? false); + }, + ), + if (isDraggable) + IconButton( + icon: const Icon(Icons.drag_handle, color: Colors.grey), + onPressed: () {}, + tooltip: 'Перетащить', + ), + ], + ), + ], + ), + ); } -class MyHomePage extends StatefulWidget { - const MyHomePage({super.key, required this.title}); + Widget _buildButton(String label) { + return OutlinedButton( + onPressed: () {}, + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + side: BorderSide(color: Colors.grey[400]!), + ), + child: Text(label), + ); + } +} - // This widget is the home page of your application. It is stateful, meaning - // that it has a State object (defined below) that contains fields that affect - // how it looks. +// ==================== РАБОЧЕЕ ПРОСТРАНСТВО ==================== +class ProjectWorkspace extends StatelessWidget { + const ProjectWorkspace({super.key}); - // This class is the configuration for the state. It holds the values (in this - // case the title) provided by the parent (in this case the App widget) and - // used by the build method of the State. Fields in a Widget subclass are - // always marked "final". + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final isWide = constraints.maxWidth >= 700; + final state = context.read(); + + if (!isWide) { + return state.selectedDocument == null + ? const _MobileDocList() + : _MobileEditorView(document: state.selectedDocument!); + } + + return Row( + children: [ + // Левая панель - список документов (30% ширины) + Container( + width: 300, + decoration: BoxDecoration( + border: Border(right: BorderSide(color: Colors.grey[300]!)), + ), + child: Column( + children: [ + // Header + Container( + padding: const EdgeInsets.all(16), + color: Colors.green[50], + child: Row( + children: [ + IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => state.clearSelectedProject(), + tooltip: 'Back to projects', + ), + Expanded( + child: Text( + state.selectedProject!.name, + style: Theme.of(context).textTheme.titleMedium, + ), + ), + ], + ), + ), + // Список документов + const Expanded(child: _DocumentsList()), + ], + ), + ), + // Правая панель - редактор (70% ширины) + Expanded( + child: state.selectedDocument != null + ? QuillEditorView(document: state.selectedDocument!) + : const Center(child: Text('Select a document')), + ), + ], + ); + }, + ); + } +} + +// ==================== КОМПОНЕНТЫ ==================== +class _DocumentsList extends StatelessWidget { + const _DocumentsList(); + + @override + Widget build(BuildContext context) { + final state = context.watch(); + final docs = state.selectedProject!.documents; + + return ListView.builder( + itemCount: docs.length, + itemBuilder: (context, index) { + final doc = docs[index]; + final isSelected = state.selectedDocument?.id == doc.id; + return ListTile( + selected: isSelected, + selectedTileColor: Theme.of(context).colorScheme.secondaryContainer, + leading: Icon( + doc.isMostUsed ? Icons.star : Icons.insert_drive_file, + color: isSelected + ? Theme.of(context).colorScheme.onSecondaryContainer + : Colors.grey[600], + ), + title: Text(doc.name), + subtitle: isSelected + ? null + : Text('Views: ${doc.viewCount}', style: TextStyle(fontSize: 12, color: Colors.grey[600])), + onTap: () => state.selectDocument(doc), + ); + }, + ); + } +} - final String title; +class QuillEditorView extends StatefulWidget { + final AppDocument document; + const QuillEditorView({super.key, required this.document}); @override - State createState() => _MyHomePageState(); + State createState() => _QuillEditorViewState(); } -class _MyHomePageState extends State { - int _counter = 0; +class _QuillEditorViewState extends State { + late quill.QuillController _controller; - void _incrementCounter() { - setState(() { - // This call to setState tells the Flutter framework that something has - // changed in this State, which causes it to rerun the build method below - // so that the display can reflect the updated values. If we changed - // _counter without calling setState(), then the build method would not be - // called again, and so nothing would appear to happen. - _counter++; - }); + @override + void initState() { + super.initState(); + final state = context.read(); + _controller = state.getOrCreateController(widget.document); + } + + @override + void dispose() { + super.dispose(); } @override Widget build(BuildContext context) { - // This method is rerun every time setState is called, for instance as done - // by the _incrementCounter method above. - // - // The Flutter framework has been optimized to make rerunning build methods - // fast, so that you can just rebuild anything that needs updating rather - // than having to individually change instances of widgets. + return Column( + children: [ + quill.QuillSimpleToolbar( + controller: _controller, + config: const quill.QuillSimpleToolbarConfig( + showBoldButton: true, + showItalicButton: true, + showUnderLineButton: true, + showStrikeThrough: true, + showFontSize: true, + showFontFamily: true, + showColorButton: true, + showBackgroundColorButton: true, + showAlignmentButtons: true, + showListNumbers: true, + showListBullets: true, + showIndent: true, + ), + ), + Expanded( + child: quill.QuillEditor( + controller: _controller, + config: const quill.QuillEditorConfig( + placeholder: 'Начните печатать...', + padding: EdgeInsets.all(16), + ), + scrollController: ScrollController(), + focusNode: FocusNode(), + ), + ), + ], + ); + } +} + +class _MobileDocList extends StatelessWidget { + const _MobileDocList(); + + @override + Widget build(BuildContext context) { + final state = context.read(); return Scaffold( appBar: AppBar( - // TRY THIS: Try changing the color here to a specific color (to - // Colors.amber, perhaps?) and trigger a hot reload to see the AppBar - // change color while the other colors stay the same. - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - // Here we take the value from the MyHomePage object that was created by - // the App.build method, and use it to set our appbar title. - title: Text(widget.title), - ), - body: Center( - // Center is a layout widget. It takes a single child and positions it - // in the middle of the parent. - child: Column( - // Column is also a layout widget. It takes a list of children and - // arranges them vertically. By default, it sizes itself to fit its - // children horizontally, and tries to be as tall as its parent. - // - // Column has various properties to control how it sizes itself and - // how it positions its children. Here we use mainAxisAlignment to - // center the children vertically; the main axis here is the vertical - // axis because Columns are vertical (the cross axis would be - // horizontal). - // - // TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint" - // action in the IDE, or press "p" in the console), to see the - // wireframe for each widget. - mainAxisAlignment: .center, - children: [ - const Text('Hello, world!'), - const Text('You have pushed the button this many times:'), - Text( - '$_counter', - style: Theme.of(context).textTheme.headlineMedium, - ), - ], + title: Text(state.selectedProject!.name), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => state.clearSelectedProject(), ), ), - floatingActionButton: FloatingActionButton( - onPressed: _incrementCounter, - tooltip: 'Increment', - child: const Icon(Icons.add), - ), + body: const _DocumentsList(), ); } } + +class _MobileEditorView extends StatelessWidget { + final AppDocument document; + const _MobileEditorView({required this.document}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(document.name), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () {}, + ), + ), + body: QuillEditorView(document: document), + ); + } +} \ No newline at end of file diff --git a/manylines_editor/linux/flutter/generated_plugin_registrant.cc b/manylines_editor/linux/flutter/generated_plugin_registrant.cc index e71a16d..f6f23bf 100644 --- a/manylines_editor/linux/flutter/generated_plugin_registrant.cc +++ b/manylines_editor/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,10 @@ #include "generated_plugin_registrant.h" +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } diff --git a/manylines_editor/linux/flutter/generated_plugins.cmake b/manylines_editor/linux/flutter/generated_plugins.cmake index 2e1de87..f16b4c3 100644 --- a/manylines_editor/linux/flutter/generated_plugins.cmake +++ b/manylines_editor/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/manylines_editor/macos/Flutter/GeneratedPluginRegistrant.swift b/manylines_editor/macos/Flutter/GeneratedPluginRegistrant.swift index cccf817..392fb08 100644 --- a/manylines_editor/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/manylines_editor/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,10 @@ import FlutterMacOS import Foundation +import quill_native_bridge_macos +import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + QuillNativeBridgePlugin.register(with: registry.registrar(forPlugin: "QuillNativeBridgePlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/manylines_editor/pubspec.lock b/manylines_editor/pubspec.lock index 3cf1fef..3fdf408 100644 --- a/manylines_editor/pubspec.lock +++ b/manylines_editor/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" async: dependency: transitive description: @@ -25,6 +33,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + charcode: + dependency: transitive + description: + name: charcode + sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a + url: "https://pub.dev" + source: hosted + version: "1.4.0" clock: dependency: transitive description: @@ -41,6 +57,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" + url: "https://pub.dev" + source: hosted + version: "0.3.5+2" + csslib: + dependency: transitive + description: + name: csslib + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" + url: "https://pub.dev" + source: hosted + version: "1.0.2" cupertino_icons: dependency: "direct main" description: @@ -49,6 +81,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + dart_quill_delta: + dependency: "direct main" + description: + name: dart_quill_delta + sha256: bddb0b2948bd5b5a328f1651764486d162c59a8ccffd4c63e8b2c5e44be1dac4 + url: "https://pub.dev" + source: hosted + version: "10.8.3" + diff_match_patch: + dependency: transitive + description: + name: diff_match_patch + sha256: "2efc9e6e8f449d0abe15be240e2c2a3bcd977c8d126cfd70598aee60af35c0a4" + url: "https://pub.dev" + source: hosted + version: "0.4.1" fake_async: dependency: transitive description: @@ -57,11 +105,83 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85" + url: "https://pub.dev" + source: hosted + version: "2.7.0" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd" + url: "https://pub.dev" + source: hosted + version: "0.9.3+5" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_colorpicker: + dependency: transitive + description: + name: flutter_colorpicker + sha256: "969de5f6f9e2a570ac660fb7b501551451ea2a1ab9e2097e89475f60e07816ea" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + flutter_keyboard_visibility_linux: + dependency: transitive + description: + name: flutter_keyboard_visibility_linux + sha256: "6fba7cd9bb033b6ddd8c2beb4c99ad02d728f1e6e6d9b9446667398b2ac39f08" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + flutter_keyboard_visibility_macos: + dependency: transitive + description: + name: flutter_keyboard_visibility_macos + sha256: c5c49b16fff453dfdafdc16f26bdd8fb8d55812a1d50b0ce25fc8d9f2e53d086 + url: "https://pub.dev" + source: hosted + version: "1.0.0" + flutter_keyboard_visibility_platform_interface: + dependency: transitive + description: + name: flutter_keyboard_visibility_platform_interface + sha256: e43a89845873f7be10cb3884345ceb9aebf00a659f479d1c8f4293fcb37022a4 + url: "https://pub.dev" + source: hosted + version: "2.0.0" + flutter_keyboard_visibility_temp_fork: + dependency: transitive + description: + name: flutter_keyboard_visibility_temp_fork + sha256: e3d02900640fbc1129245540db16944a0898b8be81694f4bf04b6c985bed9048 + url: "https://pub.dev" + source: hosted + version: "0.1.5" + flutter_keyboard_visibility_windows: + dependency: transitive + description: + name: flutter_keyboard_visibility_windows + sha256: fc4b0f0b6be9b93ae527f3d527fb56ee2d918cd88bbca438c478af7bcfd0ef73 + url: "https://pub.dev" + source: hosted + version: "1.0.0" flutter_lints: dependency: "direct dev" description: @@ -70,11 +190,77 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_quill: + dependency: "direct main" + description: + name: flutter_quill + sha256: b96bb8525afdeaaea52f5d02f525e05cc34acd176467ab6d6f35d434cf14fde2 + url: "https://pub.dev" + source: hosted + version: "11.5.0" + flutter_quill_delta_from_html: + dependency: transitive + description: + name: flutter_quill_delta_from_html + sha256: "0eb801ea8dd498cadc057507af5da794d4c9599ce58b2569cb3d4bb53ba8bed2" + url: "https://pub.dev" + source: hosted + version: "1.5.3" + flutter_riverpod: + dependency: "direct main" + description: + name: flutter_riverpod + sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1" + url: "https://pub.dev" + source: hosted + version: "2.6.1" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + html: + dependency: transitive + description: + name: html + sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" + url: "https://pub.dev" + source: hosted + version: "0.15.6" + http: + dependency: transitive + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + intl: + dependency: transitive + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" leak_tracker: dependency: transitive description: @@ -107,14 +293,22 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.0" + markdown: + dependency: transitive + description: + name: markdown + sha256: ee85086ad7698b42522c6ad42fe195f1b9898e4d974a1af4576c1a3a176cada9 + url: "https://pub.dev" + source: hosted + version: "7.3.1" matcher: dependency: transitive description: name: matcher - sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.19" + version: "0.12.18" material_color_utilities: dependency: transitive description: @@ -131,6 +325,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.17.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" path: dependency: transitive description: @@ -139,6 +341,94 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + provider: + dependency: "direct main" + description: + name: provider + sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" + url: "https://pub.dev" + source: hosted + version: "6.1.5+1" + quill_native_bridge: + dependency: transitive + description: + name: quill_native_bridge + sha256: "76a16512e398e84216f3f659f7cb18a89ec1e141ea908e954652b4ce6cf15b18" + url: "https://pub.dev" + source: hosted + version: "11.1.0" + quill_native_bridge_android: + dependency: transitive + description: + name: quill_native_bridge_android + sha256: b75c7e6ede362a7007f545118e756b1f19053994144ec9eda932ce5e54a57569 + url: "https://pub.dev" + source: hosted + version: "0.0.1+2" + quill_native_bridge_ios: + dependency: transitive + description: + name: quill_native_bridge_ios + sha256: d23de3cd7724d482fe2b514617f8eedc8f296e120fb297368917ac3b59d8099f + url: "https://pub.dev" + source: hosted + version: "0.0.1" + quill_native_bridge_macos: + dependency: transitive + description: + name: quill_native_bridge_macos + sha256: "1c0631bd1e2eee765a8b06017c5286a4e829778f4585736e048eb67c97af8a77" + url: "https://pub.dev" + source: hosted + version: "0.0.1" + quill_native_bridge_platform_interface: + dependency: transitive + description: + name: quill_native_bridge_platform_interface + sha256: "8264a2bdb8a294c31377a27b46c0f8717fa9f968cf113f7dc52d332ed9c84526" + url: "https://pub.dev" + source: hosted + version: "0.0.2+1" + quill_native_bridge_web: + dependency: transitive + description: + name: quill_native_bridge_web + sha256: "7c723f6824b0250d7f33e8b6c23f2f8eb0103fe48ee7ebf47ab6786b64d5c05d" + url: "https://pub.dev" + source: hosted + version: "0.0.2" + quill_native_bridge_windows: + dependency: transitive + description: + name: quill_native_bridge_windows + sha256: "3f96ced19e3206ddf4f6f7dde3eb16bdd05e10294964009ea3a806d995aa7caa" + url: "https://pub.dev" + source: hosted + version: "0.0.2" + quiver: + dependency: transitive + description: + name: quiver + sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + riverpod: + dependency: transitive + description: + name: riverpod + sha256: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959" + url: "https://pub.dev" + source: hosted + version: "2.6.1" sky_engine: dependency: transitive description: flutter @@ -160,6 +450,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.12.1" + state_notifier: + dependency: transitive + description: + name: state_notifier + sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb + url: "https://pub.dev" + source: hosted + version: "1.0.0" stream_channel: dependency: transitive description: @@ -188,10 +486,82 @@ packages: dependency: transitive description: name: test_api - sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" + url: "https://pub.dev" + source: hosted + version: "0.7.9" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + url_launcher: + dependency: transitive + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "3bb000251e55d4a209aa0e2e563309dc9bb2befea2295fd0cec1f51760aac572" + url: "https://pub.dev" + source: hosted + version: "6.3.29" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" + url: "https://pub.dev" + source: hosted + version: "6.4.1" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "3.2.2" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + url: "https://pub.dev" + source: hosted + version: "3.2.5" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f + url: "https://pub.dev" + source: hosted + version: "2.4.2" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + url: "https://pub.dev" + source: hosted + version: "3.1.5" vector_math: dependency: transitive description: @@ -208,6 +578,22 @@ packages: url: "https://pub.dev" source: hosted version: "15.0.2" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + win32: + dependency: transitive + description: + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.dev" + source: hosted + version: "5.15.0" sdks: dart: ">=3.11.1 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + flutter: ">=3.38.0" diff --git a/manylines_editor/pubspec.yaml b/manylines_editor/pubspec.yaml index 363d8cd..330714e 100644 --- a/manylines_editor/pubspec.yaml +++ b/manylines_editor/pubspec.yaml @@ -30,6 +30,12 @@ environment: dependencies: flutter: sdk: flutter + flutter_quill: ^11.5.0 + dart_quill_delta: ^10.8.3 + provider: ^6.1.2 + flutter_riverpod: ^2.4.9 + flutter_localizations: + sdk: flutter # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. diff --git a/manylines_editor/windows/flutter/generated_plugin_registrant.cc b/manylines_editor/windows/flutter/generated_plugin_registrant.cc index 8b6d468..043a96f 100644 --- a/manylines_editor/windows/flutter/generated_plugin_registrant.cc +++ b/manylines_editor/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,12 @@ #include "generated_plugin_registrant.h" +#include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + FileSelectorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSelectorWindows")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/manylines_editor/windows/flutter/generated_plugins.cmake b/manylines_editor/windows/flutter/generated_plugins.cmake index b93c4c3..a95e267 100644 --- a/manylines_editor/windows/flutter/generated_plugins.cmake +++ b/manylines_editor/windows/flutter/generated_plugins.cmake @@ -3,6 +3,8 @@ # list(APPEND FLUTTER_PLUGIN_LIST + file_selector_windows + url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST From 090fc166e174ce63f3c2f4b6f552aa7e49f1e554 Mon Sep 17 00:00:00 2001 From: Ulquiorra-Keesre Date: Mon, 6 Apr 2026 12:49:34 +0300 Subject: [PATCH 05/22] Homa Page -draggable --- manylines_editor/lib/main.dart | 438 +++++++++++++++++++-------------- 1 file changed, 254 insertions(+), 184 deletions(-) diff --git a/manylines_editor/lib/main.dart b/manylines_editor/lib/main.dart index 7a0fdee..d5a67f1 100644 --- a/manylines_editor/lib/main.dart +++ b/manylines_editor/lib/main.dart @@ -25,7 +25,6 @@ class AppDocument { required this.content, }); - // Вычисляемое свойство bool get isMostUsed => viewCount > 0; static AppDocument? getMostUsed(List docs) { @@ -36,7 +35,6 @@ class AppDocument { } } -// ==================== STATE ==================== // ==================== STATE ==================== class AppState extends ChangeNotifier { final List _projects = [ @@ -72,51 +70,42 @@ class AppState extends ChangeNotifier { ), ]; - // ✅ Настройки (список для перетаскивания) List> _settings = [ {'id': 'setting1', 'name': 'Setting 1', 'expanded': true, 'enabled': false}, {'id': 'setting2', 'name': 'Setting 2', 'expanded': false, 'enabled': false}, {'id': 'setting3', 'name': 'Setting 3', 'expanded': true, 'enabled': false}, ]; - // ✅ Переключатель Switchable bool _switchableValue = true; - Project? _selectedProject; AppDocument? _selectedDocument; - - // Хранилище контроллеров для сохранения изменений final Map _editors = {}; // ==================== GETTERS ==================== List get projects => _projects; - List> get settings => _settings; // ✅ Добавлен геттер - bool get switchableValue => _switchableValue; // ✅ Добавлен геттер + List> get settings => _settings; + bool get switchableValue => _switchableValue; Project? get selectedProject => _selectedProject; AppDocument? get selectedDocument => _selectedDocument; // ==================== METHODS ==================== - // Увеличить счётчик просмотров void incrementViewCount(AppDocument doc) { doc.viewCount++; notifyListeners(); } - // Сохранить изменения документа void saveDocumentContent(AppDocument doc, Delta content) { doc.content = content; notifyListeners(); } - // Получить или создать контроллер для документа quill.QuillController getOrCreateController(AppDocument doc) { if (!_editors.containsKey(doc.id)) { _editors[doc.id] = quill.QuillController( document: quill.Document.fromJson(doc.content.toJson()), selection: const TextSelection.collapsed(offset: 0), ); - _editors[doc.id]!.changes.listen((_) { saveDocumentContent(doc, _editors[doc.id]!.document.toDelta()); }); @@ -163,13 +152,11 @@ class AppState extends ChangeNotifier { notifyListeners(); } - // ✅ Метод для переключения Switchable void setSwitchableValue(bool value) { _switchableValue = value; notifyListeners(); } - // ✅ Метод для перетаскивания настроек void reorderSettings(int oldIndex, int newIndex) { if (newIndex > oldIndex) { newIndex -= 1; @@ -179,7 +166,16 @@ class AppState extends ChangeNotifier { notifyListeners(); } - // ✅ Метод для разворачивания/сворачивания настройки + // ✅ Добавлен метод для перетаскивания проектов + void reorderProjects(int oldIndex, int newIndex) { + if (newIndex > oldIndex) { + newIndex -= 1; + } + final item = _projects.removeAt(oldIndex); + _projects.insert(newIndex, item); + notifyListeners(); + } + void toggleSettingExpansion(String id) { for (var setting in _settings) { if (setting['id'] == id) { @@ -190,7 +186,6 @@ class AppState extends ChangeNotifier { } } - // ✅ Метод для включения/выключения настройки void toggleSettingEnabled(String id, bool value) { for (var setting in _settings) { if (setting['id'] == id) { @@ -244,7 +239,7 @@ class AppShell extends StatelessWidget { } } -// ==================== ЭКРАН ПРОЕКТОВ (как на скрине) ==================== +// ==================== ЭКРАН ПРОЕКТОВ ==================== class ProjectsScreen extends StatelessWidget { const ProjectsScreen({super.key}); @@ -286,140 +281,128 @@ class ProjectsScreen extends StatelessWidget { ), ), - // Список проектов (зелёный фон) + // ✅ Список проектов с перетаскиванием (управляется Switchable) Container( color: Colors.green[50], child: Consumer( builder: (context, state, _) { - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: state.projects.map((project) { - return Container( - decoration: BoxDecoration( - border: Border( - bottom: BorderSide(color: Colors.green[200]!), + if (state.switchableValue) { + return ReorderableListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: state.projects.length, + onReorder: state.reorderProjects, + itemBuilder: (context, index) { + final project = state.projects[index]; + return Container( + key: ValueKey(project.id), // ✅ Обязательно для ReorderableListView + decoration: BoxDecoration( + border: Border( + bottom: BorderSide(color: Colors.green[200]!), + ), + ), + child: ListTile( + title: Text(project.name), + trailing: state.switchableValue + ? const Icon(Icons.drag_handle, color: Colors.grey) + : null, + onTap: () => state.selectProject(project), ), - ), - child: CheckboxListTile( - value: false, - onChanged: (_) => state.selectProject(project), - title: Text(project.name), - controlAffinity: ListTileControlAffinity.trailing, - contentPadding: const EdgeInsets.symmetric(horizontal: 16), - ), - ); - }).toList(), - ); + ); + }, + ); + } else { + return ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: state.projects.length, + itemBuilder: (context, index) { + final project = state.projects[index]; + return Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide(color: Colors.green[200]!), + ), + ), + child: ListTile( + title: Text(project.name), + onTap: () => state.selectProject(project), + ), + ); + }, + ); + } }, ), ), - // Настройки (голубой фон) - // Настройки с возможностью перетаскивания (голубой фон) -Container( - color: Colors.blue[50], - height: 200, - child: Consumer( - builder: (context, state, _) { - // Если Switchable = true → показываем ReorderableListView - // Если Switchable = false → показываем обычный Column - if (state.switchableValue) { - return ReorderableListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: state.settings.length, - onReorder: state.reorderSettings, - itemBuilder: (context, index) { - final setting = state.settings[index]; - return _buildSettingRow( - setting['name'], - setting['expanded'], - setting['enabled'], - setting['id'], - state, - isDraggable: true, // ← Разрешаем перетаскивание - ); - }, - ); - } else { - // Обычный список без перетаскивания - return ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: state.settings.length, - itemBuilder: (context, index) { - final setting = state.settings[index]; - return _buildSettingRow( - setting['name'], - setting['expanded'], - setting['enabled'], - setting['id'], - state, - isDraggable: false, // ← Запрещаем перетаскивание - ); - }, - ); - } - }, - ), -), - - // Description с кнопками A, B, C + // ✅ Настройки с перетаскиванием (управляется Switchable) Container( - padding: const EdgeInsets.all(16), - alignment: Alignment.centerLeft, - child: Text( - 'Description', - style: TextStyle(fontSize: 14, color: Colors.grey[700]), - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - children: [ - Expanded(child: _buildButton('A')), - const SizedBox(width: 8), - Expanded(child: _buildButton('B')), - const SizedBox(width: 8), - Expanded(child: _buildButton('C')), - ], - ), - ), - const SizedBox(height: 16), - - // Switchable - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Consumer( // ← Оберните в Consumer + color: Colors.blue[50], + child: Consumer( builder: (context, state, _) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text('Switchable'), - Switch( - value: state.switchableValue, // ← Используйте состояние - onChanged: (value) { - state.setSwitchableValue(value); // ← Сохраняйте изменение - }, - ), - ], - ); + final settings = state.settings; + + if (state.switchableValue) { + return ReorderableListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: settings.length, + onReorder: state.reorderSettings, + itemBuilder: (context, index) { + final setting = settings[index]; + final isExpanded = setting['expanded'] ?? false; + + return Column( + key: ValueKey(setting['id']), + mainAxisSize: MainAxisSize.min, + children: [ + _buildSettingRow( + setting['name'], + isExpanded, + setting['enabled'] ?? false, + setting['id'], + state, + isDraggable: true, + ), + if (setting['id'] == 'setting3' && isExpanded) + _buildDescriptionSection(), + ], + ); + }, + ); + } else { + return ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: settings.length, + itemBuilder: (context, index) { + final setting = settings[index]; + final isExpanded = setting['expanded'] ?? false; + + return Column( + key: ValueKey(setting['id']), + mainAxisSize: MainAxisSize.min, + children: [ + _buildSettingRow( + setting['name'], + isExpanded, + setting['enabled'] ?? false, + setting['id'], + state, + isDraggable: false, + ), + if (setting['id'] == 'setting3' && isExpanded) + _buildDescriptionSection(), + ], + ); + }, + ); + } }, ), ), - // Listable - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text('Listable'), - const Icon(Icons.arrow_drop_down), - ], - ), - ), - // Additional settings (голубой фон) Expanded( child: Container( @@ -442,50 +425,140 @@ Container( ); } - Widget _buildSettingRow( - String title, - bool expanded, - bool enabled, - String id, - AppState state, { - bool isDraggable = false, // ← Новый параметр -}) { - return Container( - key: ValueKey(id), // Обязательно для ReorderableListView! - decoration: BoxDecoration( - border: Border(bottom: BorderSide(color: Colors.blue[200]!)), - color: Colors.white, - ), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(title), - Row( - children: [ - IconButton( - icon: Icon(expanded ? Icons.arrow_drop_down : Icons.arrow_drop_up), - onPressed: () => state.toggleSettingExpansion(id), - ), - const SizedBox(width: 8), - Checkbox( - value: enabled, - onChanged: (value) { - state.toggleSettingEnabled(id, value ?? false); - }, - ), - if (isDraggable) + // ✅ Метод для построения секции Description (выпадает из Setting 3) + Widget _buildDescriptionSection() { + return Container( + color: Colors.grey[50], + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + 'Description', + style: TextStyle(fontSize: 14, color: Colors.grey[700]), + ), + const SizedBox(height: 12), + + // Кнопки A, B, C + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () {}, + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + side: BorderSide(color: Colors.grey[400]!), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: const Text('A'), + ), + ), + const SizedBox(width: 8), + Expanded( + child: OutlinedButton( + onPressed: () {}, + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + side: BorderSide(color: Colors.grey[400]!), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: const Text('B'), + ), + ), + const SizedBox(width: 8), + Expanded( + child: OutlinedButton( + onPressed: () {}, + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + side: BorderSide(color: Colors.grey[400]!), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: const Text('C'), + ), + ), + ], + ), + const SizedBox(height: 16), + + // Switchable + Consumer( + builder: (context, state, _) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Switchable'), + Switch( + value: state.switchableValue, + onChanged: (value) { + state.setSwitchableValue(value); + }, + ), + ], + ); + }, + ), + + // Listable + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Listable'), IconButton( - icon: const Icon(Icons.drag_handle, color: Colors.grey), + icon: const Icon(Icons.arrow_drop_down), onPressed: () {}, - tooltip: 'Перетащить', ), - ], - ), - ], - ), - ); -} + ], + ), + ], + ), + ); + } + + Widget _buildSettingRow( + String title, + bool expanded, + bool enabled, + String id, + AppState state, { + bool isDraggable = false, + }) { + return Container( + decoration: BoxDecoration( + border: Border(bottom: BorderSide(color: Colors.blue[200]!)), + color: Colors.white, + ), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(title), + Row( + children: [ + IconButton( + icon: Icon(expanded ? Icons.arrow_drop_down : Icons.arrow_drop_up), + onPressed: () => state.toggleSettingExpansion(id), + ), + const SizedBox(width: 8), + // ✅ Чекбокс удалён + if (isDraggable) + IconButton( + icon: const Icon(Icons.drag_handle, color: Colors.grey), + onPressed: () {}, + tooltip: 'Перетащить', + ), + ], + ), + ], + ), + ); + } Widget _buildButton(String label) { return OutlinedButton( @@ -518,7 +591,6 @@ class ProjectWorkspace extends StatelessWidget { return Row( children: [ - // Левая панель - список документов (30% ширины) Container( width: 300, decoration: BoxDecoration( @@ -526,7 +598,6 @@ class ProjectWorkspace extends StatelessWidget { ), child: Column( children: [ - // Header Container( padding: const EdgeInsets.all(16), color: Colors.green[50], @@ -546,12 +617,10 @@ class ProjectWorkspace extends StatelessWidget { ], ), ), - // Список документов const Expanded(child: _DocumentsList()), ], ), ), - // Правая панель - редактор (70% ширины) Expanded( child: state.selectedDocument != null ? QuillEditorView(document: state.selectedDocument!) @@ -618,6 +687,7 @@ class _QuillEditorViewState extends State { @override void dispose() { + _controller.dispose(); super.dispose(); } @@ -645,9 +715,9 @@ class _QuillEditorViewState extends State { Expanded( child: quill.QuillEditor( controller: _controller, - config: const quill.QuillEditorConfig( + config: quill.QuillEditorConfig( placeholder: 'Начните печатать...', - padding: EdgeInsets.all(16), + padding: const EdgeInsets.all(16), ), scrollController: ScrollController(), focusNode: FocusNode(), From 097595333a230778f5390d2a659c4d9e2dd4e5db Mon Sep 17 00:00:00 2001 From: Ulquiorra-Keesre Date: Mon, 6 Apr 2026 16:02:47 +0300 Subject: [PATCH 06/22] added dark theme --- manylines_editor/lib/main.dart | 702 +++++++++++++++++++++------------ 1 file changed, 444 insertions(+), 258 deletions(-) diff --git a/manylines_editor/lib/main.dart b/manylines_editor/lib/main.dart index d5a67f1..a1f51d5 100644 --- a/manylines_editor/lib/main.dart +++ b/manylines_editor/lib/main.dart @@ -81,6 +81,9 @@ class AppState extends ChangeNotifier { AppDocument? _selectedDocument; final Map _editors = {}; + bool _isDarkMode = false; + + // ==================== GETTERS ==================== List get projects => _projects; List> get settings => _settings; @@ -88,7 +91,14 @@ class AppState extends ChangeNotifier { Project? get selectedProject => _selectedProject; AppDocument? get selectedDocument => _selectedDocument; + bool get isDarkMode => _isDarkMode; + // ==================== METHODS ==================== + + void toggleDarkMode(bool value) { + _isDarkMode = value; + notifyListeners(); + } void incrementViewCount(AppDocument doc) { doc.viewCount++; @@ -210,14 +220,26 @@ void main() { runApp( ChangeNotifierProvider( create: (_) => AppState(), - child: MaterialApp( - title: 'Manyllines', - theme: ThemeData( - useMaterial3: true, - colorSchemeSeed: Colors.green, - fontFamily: 'Roboto', - ), - home: AppShell(), + child: Consumer( + builder: (context, state, _) { + return MaterialApp( + title: 'Manyllines', + theme: state.isDarkMode + ? ThemeData( + useMaterial3: true, + colorSchemeSeed: Colors.green, + brightness: Brightness.dark, + fontFamily: 'Roboto', + ) + : ThemeData( + useMaterial3: true, + colorSchemeSeed: Colors.green, + brightness: Brightness.light, + fontFamily: 'Roboto', + ), + home: AppShell(), + ); + }, ), ), ); @@ -282,139 +304,208 @@ class ProjectsScreen extends StatelessWidget { ), // ✅ Список проектов с перетаскиванием (управляется Switchable) - Container( - color: Colors.green[50], - child: Consumer( - builder: (context, state, _) { - if (state.switchableValue) { - return ReorderableListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: state.projects.length, - onReorder: state.reorderProjects, - itemBuilder: (context, index) { - final project = state.projects[index]; - return Container( - key: ValueKey(project.id), // ✅ Обязательно для ReorderableListView - decoration: BoxDecoration( - border: Border( - bottom: BorderSide(color: Colors.green[200]!), - ), - ), - child: ListTile( - title: Text(project.name), - trailing: state.switchableValue - ? const Icon(Icons.drag_handle, color: Colors.grey) - : null, - onTap: () => state.selectProject(project), - ), - ); - }, - ); - } else { - return ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: state.projects.length, - itemBuilder: (context, index) { - final project = state.projects[index]; - return Container( - decoration: BoxDecoration( - border: Border( - bottom: BorderSide(color: Colors.green[200]!), - ), - ), - child: ListTile( - title: Text(project.name), - onTap: () => state.selectProject(project), - ), - ); - }, - ); - } - }, + // Список проектов с перетаскиванием (управляется Switchable) + // ✅ Список проектов с перетаскиванием (управляется Switchable) +Consumer( + builder: (context, state, _) { + final bgColor = state.isDarkMode ? Colors.green[900] : Colors.green[50]; + final borderColor = state.isDarkMode + ? const Color.fromARGB(255, 0, 47, 22) + : Colors.green.shade200; + final textColor = state.isDarkMode ? Colors.white : Colors.black87; + + if (state.switchableValue) { + // ✅ Режим с перетаскиванием (ReorderableListView) + return ReorderableListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: state.projects.length, + onReorder: state.reorderProjects, + itemBuilder: (context, index) { + final project = state.projects[index]; + return Container( + key: ValueKey(project.id), + decoration: BoxDecoration( + color: bgColor, + border: Border( + bottom: BorderSide(color: borderColor), + ), ), + child: ListTile( + title: Text(project.name, style: TextStyle(color: textColor)), + trailing: Icon(Icons.drag_handle, + color: state.isDarkMode ? Colors.white54 : Colors.grey), + onTap: () => state.selectProject(project), + ), + ); + }, + ); + } else { + // ✅ Режим БЕЗ перетаскивания (обычный список) — ПРОЕКТЫ ОТОБРАЖАЮТСЯ! + return Container( + decoration: BoxDecoration( + color: bgColor, + border: Border( + bottom: BorderSide(color: borderColor), ), + ), + child: ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: state.projects.length, + itemBuilder: (context, index) { + final project = state.projects[index]; // ← Получаем проект + return Container( // ← Возвращаем виджет! + key: ValueKey(project.id), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide(color: borderColor), + ), + ), + child: ListTile( + title: Text(project.name, style: TextStyle(color: textColor)), + // ❌ Без иконки drag_handle + onTap: () => state.selectProject(project), + ), + ); + }, + ), + ); + } + }, +), // ✅ Настройки с перетаскиванием (управляется Switchable) - Container( - color: Colors.blue[50], - child: Consumer( - builder: (context, state, _) { - final settings = state.settings; - - if (state.switchableValue) { - return ReorderableListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: settings.length, - onReorder: state.reorderSettings, - itemBuilder: (context, index) { - final setting = settings[index]; - final isExpanded = setting['expanded'] ?? false; - - return Column( - key: ValueKey(setting['id']), - mainAxisSize: MainAxisSize.min, - children: [ - _buildSettingRow( - setting['name'], - isExpanded, - setting['enabled'] ?? false, - setting['id'], - state, - isDraggable: true, + // Настройки с перетаскиванием (управляется Switchable) + Consumer( + builder: (context, state, _) { + // ✅ Выбираем цвет в зависимости от темы + final bgColor = state.isDarkMode ? Colors.blue[900] : Colors.blue[50]; + final borderColor = state.isDarkMode ? Colors.blue[700] : Colors.blue[200]; + final textColor = state.isDarkMode ? Colors.white : Colors.black87; + + final settings = state.settings; + + if (state.switchableValue) { + return ReorderableListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: settings.length, + onReorder: state.reorderSettings, + itemBuilder: (context, index) { + final setting = settings[index]; + final isExpanded = setting['expanded'] ?? false; + + return Column( + key: ValueKey(setting['id']), + mainAxisSize: MainAxisSize.min, + children: [ + Container( + decoration: BoxDecoration( + border: Border(bottom: BorderSide(color: borderColor!)), + color: state.isDarkMode ? Colors.blue[800] : Colors.white, ), - if (setting['id'] == 'setting3' && isExpanded) - _buildDescriptionSection(), - ], - ); - }, - ); - } else { - return ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: settings.length, - itemBuilder: (context, index) { - final setting = settings[index]; - final isExpanded = setting['expanded'] ?? false; - - return Column( - key: ValueKey(setting['id']), - mainAxisSize: MainAxisSize.min, - children: [ - _buildSettingRow( - setting['name'], - isExpanded, - setting['enabled'] ?? false, - setting['id'], - state, - isDraggable: false, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + setting['name'], + style: TextStyle(color: textColor), + ), + Row( + children: [ + IconButton( + icon: Icon( + isExpanded ? Icons.arrow_drop_down : Icons.arrow_drop_up, + color: textColor, + ), + onPressed: () => state.toggleSettingExpansion(setting['id']), + ), + if (state.switchableValue) + Icon(Icons.drag_handle, + color: state.isDarkMode ? Colors.white54 : Colors.grey), + ], + ), + ], ), - if (setting['id'] == 'setting3' && isExpanded) - _buildDescriptionSection(), - ], - ); - }, - ); - } - }, - ), + ), + if (setting['id'] == 'setting2' && isExpanded) + _buildDescriptionSection2(state.isDarkMode), + if (setting['id'] == 'setting3' && isExpanded) + _buildDescriptionSection3(state.isDarkMode), + ], + ); + }, + ); + } else { + return ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: settings.length, + itemBuilder: (context, index) { + final setting = settings[index]; + final isExpanded = setting['expanded'] ?? false; + + return Column( + key: ValueKey(setting['id']), + mainAxisSize: MainAxisSize.min, + children: [ + Container( + decoration: BoxDecoration( + border: Border(bottom: BorderSide(color: borderColor!)), + color: state.isDarkMode ? Colors.blue[800] : Colors.white, + ), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + setting['name'], + style: TextStyle(color: textColor), + ), + IconButton( + icon: Icon( + isExpanded ? Icons.arrow_drop_down : Icons.arrow_drop_up, + color: textColor, + ), + onPressed: () => state.toggleSettingExpansion(setting['id']), + ), + ], + ), + ), + if (setting['id'] == 'setting2' && isExpanded) + _buildDescriptionSection2(state.isDarkMode), + if (setting['id'] == 'setting3' && isExpanded) + _buildDescriptionSection3(state.isDarkMode), + ], + ); + }, + ); + } + }, ), // Additional settings (голубой фон) - Expanded( - child: Container( - color: Colors.blue[50], - padding: const EdgeInsets.all(16), - child: const Center( - child: Text( - 'Setting ...', - style: TextStyle(color: Colors.black54), + // Additional settings (голубой фон) — с поддержкой тёмной темы + Consumer( + builder: (context, state, _) { + final bgColor = state.isDarkMode ? const Color.fromARGB(255, 6, 58, 137) : Colors.blue[50]; + final textColor = state.isDarkMode ? Colors.white54 : Colors.black54; + + return Expanded( + child: Container( + color: bgColor, + padding: const EdgeInsets.all(16), + child: Center( + child: Text( + 'Other Settings ...', + style: TextStyle(color: textColor), + ), + ), ), - ), - ), + ); + }, ), ], ), @@ -426,150 +517,245 @@ class ProjectsScreen extends StatelessWidget { } // ✅ Метод для построения секции Description (выпадает из Setting 3) - Widget _buildDescriptionSection() { - return Container( - color: Colors.grey[50], - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - 'Description', - style: TextStyle(fontSize: 14, color: Colors.grey[700]), + // ✅ Метод для построения секции Description для Setting 2 (с переключением темы) +Widget _buildDescriptionSection2(bool isDarkMode) { + return Container( + color: isDarkMode ? Colors.grey[850] : Colors.grey[50], + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + 'Description', + style: TextStyle( + fontSize: 14, + color: isDarkMode ? Colors.grey[300] : Colors.grey[700], ), - const SizedBox(height: 12), - - // Кнопки A, B, C - Row( - children: [ - Expanded( - child: OutlinedButton( - onPressed: () {}, - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 12), - side: BorderSide(color: Colors.grey[400]!), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), + ), + const SizedBox(height: 12), + + // Кнопки A, B, C + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () {}, + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + side: BorderSide( + color: isDarkMode ? Color.fromARGB(255, 54, 107, 232)! : Colors.grey[400]!, ), - child: const Text('A'), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + foregroundColor: isDarkMode ? Colors.white : Colors.black87, ), + child: Text('A'), ), - const SizedBox(width: 8), - Expanded( - child: OutlinedButton( - onPressed: () {}, - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 12), - side: BorderSide(color: Colors.grey[400]!), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), + ), + const SizedBox(width: 8), + Expanded( + child: OutlinedButton( + onPressed: () {}, + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + side: BorderSide( + color: isDarkMode ? Color.fromARGB(255, 54, 107, 232)! : Colors.grey[400]!, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), ), - child: const Text('B'), + foregroundColor: isDarkMode ? Colors.white : Colors.black87, ), + child: Text('B'), ), - const SizedBox(width: 8), - Expanded( - child: OutlinedButton( - onPressed: () {}, - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 12), - side: BorderSide(color: Colors.grey[400]!), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), + ), + const SizedBox(width: 8), + Expanded( + child: OutlinedButton( + onPressed: () {}, + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + side: BorderSide( + color: isDarkMode ? const Color.fromARGB(255, 54, 107, 232)! : Colors.grey[400]!, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), ), - child: const Text('C'), + foregroundColor: isDarkMode ? Colors.white : Colors.black87, ), + child: Text('C'), ), - ], + ), + ], + ), + const SizedBox(height: 16), + + // Switchable для переключения темы + Consumer( + builder: (context, state, _) { + final isDark = state.isDarkMode; + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Icon( + isDark ? Icons.dark_mode : Icons.light_mode, + size: 20, + color: isDark ? Colors.yellow[200] : Colors.orange, + ), + const SizedBox(width: 8), + Text( + isDark ? 'Тёмная тема' : 'Светлая тема', + style: TextStyle(color: isDarkMode ? Colors.white : Colors.black87), + ), + ], + ), + Switch( + value: isDark, + onChanged: (value) { + state.toggleDarkMode(value); + }, + ), + ], + ); + }, + ), + ], + ), + ); +} + +// ✅ Метод для построения секции Description для Setting 3 +Widget _buildDescriptionSection3(bool isDarkMode) { + return Container( + color: isDarkMode ? Colors.grey[850] : Colors.grey[50], + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + 'Description', + style: TextStyle( + fontSize: 14, + color: isDarkMode ? Colors.grey[300] : Colors.grey[700], ), - const SizedBox(height: 16), - - // Switchable - Consumer( - builder: (context, state, _) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text('Switchable'), - Switch( - value: state.switchableValue, - onChanged: (value) { - state.setSwitchableValue(value); - }, + ), + const SizedBox(height: 12), + + // Кнопки A, B, C + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () {}, + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + side: BorderSide( + color: isDarkMode ? Color.fromARGB(255, 54, 107, 232)! : Colors.grey[400]!, ), - ], - ); - }, - ), - - // Listable - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text('Listable'), - IconButton( - icon: const Icon(Icons.arrow_drop_down), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + foregroundColor: isDarkMode ? Colors.white : Colors.black87, + ), + child: Text('A'), + ), + ), + const SizedBox(width: 8), + Expanded( + child: OutlinedButton( onPressed: () {}, + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + side: BorderSide( + color: isDarkMode ? Color.fromARGB(255, 54, 107, 232)! : Colors.grey[400]!, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + foregroundColor: isDarkMode ? Colors.white : Colors.black87, + ), + child: Text('B'), ), - ], - ), - ], - ), - ); - } - - Widget _buildSettingRow( - String title, - bool expanded, - bool enabled, - String id, - AppState state, { - bool isDraggable = false, - }) { - return Container( - decoration: BoxDecoration( - border: Border(bottom: BorderSide(color: Colors.blue[200]!)), - color: Colors.white, - ), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(title), - Row( - children: [ - IconButton( - icon: Icon(expanded ? Icons.arrow_drop_down : Icons.arrow_drop_up), - onPressed: () => state.toggleSettingExpansion(id), + ), + const SizedBox(width: 8), + Expanded( + child: OutlinedButton( + onPressed: () {}, + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + side: BorderSide( + color: isDarkMode ? Color.fromARGB(255, 54, 107, 232)! : Colors.grey[400]!, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + foregroundColor: isDarkMode ? Colors.white : Colors.black87, + ), + child: Text('C'), ), - const SizedBox(width: 8), - // ✅ Чекбокс удалён - if (isDraggable) - IconButton( - icon: const Icon(Icons.drag_handle, color: Colors.grey), - onPressed: () {}, - tooltip: 'Перетащить', + ), + ], + ), + const SizedBox(height: 16), + + // Switchable + Consumer( + builder: (context, state, _) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Switchable', + style: TextStyle(color: isDarkMode ? Colors.white : Colors.black87), ), - ], - ), - ], - ), - ); - } + Switch( + value: state.switchableValue, + onChanged: (value) { + state.setSwitchableValue(value); + }, + ), + ], + ); + }, + ), - Widget _buildButton(String label) { - return OutlinedButton( - onPressed: () {}, - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 12), - side: BorderSide(color: Colors.grey[400]!), - ), - child: Text(label), - ); - } + + + // Listable + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Listable', + style: TextStyle(color: isDarkMode ? Colors.white : Colors.black87), + ), + IconButton( + icon: Icon(Icons.arrow_drop_down, + color: isDarkMode ? Colors.white : Colors.black87), + onPressed: () {}, + ), + ], + ), + ], + ), + ); +} + + // Widget _buildButton(String label) { + // return OutlinedButton( + // onPressed: () {}, + // style: OutlinedButton.styleFrom( + // padding: const EdgeInsets.symmetric(vertical: 12), + // side: BorderSide(color: Colors.grey[400]!), + // ), + // child: Text(label), + // ); + // } + + } // ==================== РАБОЧЕЕ ПРОСТРАНСТВО ==================== From b6ec52aaf8f33e3492722d52ffd9301ee28ace32 Mon Sep 17 00:00:00 2001 From: Ulquiorra-Keesre Date: Mon, 6 Apr 2026 16:24:30 +0300 Subject: [PATCH 07/22] Dark Mode complete --- manylines_editor/lib/main.dart | 290 ++++++++++++++++++++++----------- 1 file changed, 194 insertions(+), 96 deletions(-) diff --git a/manylines_editor/lib/main.dart b/manylines_editor/lib/main.dart index a1f51d5..57d42fc 100644 --- a/manylines_editor/lib/main.dart +++ b/manylines_editor/lib/main.dart @@ -270,110 +270,127 @@ class ProjectsScreen extends StatelessWidget { return Scaffold( body: Column( children: [ + // Header с Logo и Manyllines - Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - decoration: BoxDecoration( - border: Border(bottom: BorderSide(color: Colors.grey[300]!)), - ), - child: Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - border: Border.all(color: Colors.grey[400]!), - color: Colors.white, - ), - child: const Text( - 'Logo', - style: TextStyle(fontWeight: FontWeight.bold), - ), + Consumer( + builder: (context, state, _) { + + final headerBg = state.isDarkMode ? Colors.grey[850] : Colors.white; + final logoBg = state.isDarkMode ? Colors.grey[800] : Colors.white; + final borderColor = state.isDarkMode ? Colors.grey[700]! : Colors.grey[300]!; + final logoBorderColor = state.isDarkMode ? Colors.grey[600]! : Colors.grey[400]!; + final textColor = state.isDarkMode ? Colors.white : Colors.black87; + final logoTextColor = state.isDarkMode ? Colors.white : Colors.black; + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + border: Border(bottom: BorderSide(color: borderColor)), + color: headerBg, ), - const SizedBox(width: 12), - const Expanded( - child: Text( - 'Manyllines', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w600, + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + border: Border.all(color: logoBorderColor), + color: logoBg, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + 'Logo', + style: TextStyle( + fontWeight: FontWeight.bold, + color: logoTextColor, + ), + ), ), - ), + const SizedBox(width: 12), + Expanded( + child: Text( + 'Manyllines', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w600, + color: textColor, + ), + ), + ), + ], ), - ], - ), + ); + }, ), - // ✅ Список проектов с перетаскиванием (управляется Switchable) // Список проектов с перетаскиванием (управляется Switchable) - // ✅ Список проектов с перетаскиванием (управляется Switchable) -Consumer( - builder: (context, state, _) { - final bgColor = state.isDarkMode ? Colors.green[900] : Colors.green[50]; - final borderColor = state.isDarkMode - ? const Color.fromARGB(255, 0, 47, 22) - : Colors.green.shade200; - final textColor = state.isDarkMode ? Colors.white : Colors.black87; - - if (state.switchableValue) { - // ✅ Режим с перетаскиванием (ReorderableListView) - return ReorderableListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: state.projects.length, - onReorder: state.reorderProjects, - itemBuilder: (context, index) { - final project = state.projects[index]; - return Container( - key: ValueKey(project.id), - decoration: BoxDecoration( - color: bgColor, - border: Border( - bottom: BorderSide(color: borderColor), - ), - ), - child: ListTile( - title: Text(project.name, style: TextStyle(color: textColor)), - trailing: Icon(Icons.drag_handle, - color: state.isDarkMode ? Colors.white54 : Colors.grey), - onTap: () => state.selectProject(project), - ), - ); - }, - ); - } else { - // ✅ Режим БЕЗ перетаскивания (обычный список) — ПРОЕКТЫ ОТОБРАЖАЮТСЯ! - return Container( - decoration: BoxDecoration( - color: bgColor, - border: Border( - bottom: BorderSide(color: borderColor), + Consumer( + builder: (context, state, _) { + final bgColor = state.isDarkMode ? Colors.green[900] : Colors.green[50]; + final borderColor = state.isDarkMode + ? const Color.fromARGB(255, 0, 47, 22) + : Colors.green.shade200; + final textColor = state.isDarkMode ? Colors.white : Colors.black87; + + if (state.switchableValue) { + //Режим с перетаскиванием (ReorderableListView) + return ReorderableListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: state.projects.length, + onReorder: state.reorderProjects, + itemBuilder: (context, index) { + final project = state.projects[index]; + return Container( + key: ValueKey(project.id), + decoration: BoxDecoration( + color: bgColor, + border: Border( + bottom: BorderSide(color: borderColor), + ), + ), + child: ListTile( + title: Text(project.name, style: TextStyle(color: textColor)), + trailing: Icon(Icons.drag_handle, + color: state.isDarkMode ? Colors.white54 : Colors.grey), + onTap: () => state.selectProject(project), + ), + ); + }, + ); + } else { + // ✅ Режим БЕЗ перетаскивания (обычный список) — ПРОЕКТЫ ОТОБРАЖАЮТСЯ! + return Container( + decoration: BoxDecoration( + color: bgColor, + border: Border( + bottom: BorderSide(color: borderColor), + ), + ), + child: ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: state.projects.length, + itemBuilder: (context, index) { + final project = state.projects[index]; // ← Получаем проект + return Container( // ← Возвращаем виджет! + key: ValueKey(project.id), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide(color: borderColor), + ), + ), + child: ListTile( + title: Text(project.name, style: TextStyle(color: textColor)), + // ❌ Без иконки drag_handle + onTap: () => state.selectProject(project), + ), + ); + }, + ), + ); + } + }, ), - ), - child: ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: state.projects.length, - itemBuilder: (context, index) { - final project = state.projects[index]; // ← Получаем проект - return Container( // ← Возвращаем виджет! - key: ValueKey(project.id), - decoration: BoxDecoration( - border: Border( - bottom: BorderSide(color: borderColor), - ), - ), - child: ListTile( - title: Text(project.name, style: TextStyle(color: textColor)), - // ❌ Без иконки drag_handle - onTap: () => state.selectProject(project), - ), - ); - }, - ), - ); - } - }, -), // ✅ Настройки с перетаскиванием (управляется Switchable) // Настройки с перетаскиванием (управляется Switchable) @@ -430,6 +447,8 @@ Consumer( ], ), ), + if (setting['id'] == 'setting1' && isExpanded) + _buildDescriptionSection1(state.isDarkMode), if (setting['id'] == 'setting2' && isExpanded) _buildDescriptionSection2(state.isDarkMode), if (setting['id'] == 'setting3' && isExpanded) @@ -474,6 +493,8 @@ Consumer( ], ), ), + if (setting['id'] == 'setting1' && isExpanded) + _buildDescriptionSection1(state.isDarkMode), if (setting['id'] == 'setting2' && isExpanded) _buildDescriptionSection2(state.isDarkMode), if (setting['id'] == 'setting3' && isExpanded) @@ -744,6 +765,83 @@ Widget _buildDescriptionSection3(bool isDarkMode) { ); } +Widget _buildDescriptionSection1(bool isDarkMode) { + return Container( + color: isDarkMode ? Colors.grey[850] : Colors.grey[50], + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + 'Description', + style: TextStyle( + fontSize: 14, + color: isDarkMode ? Colors.grey[300] : Colors.grey[700], + ), + ), + const SizedBox(height: 12), + + // Кнопки A, B, C + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () {}, + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + side: BorderSide( + color: isDarkMode ? Color.fromARGB(255, 54, 107, 232)! : Colors.grey[400]!, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + foregroundColor: isDarkMode ? Colors.white : Colors.black87, + ), + child: Text('A'), + ), + ), + const SizedBox(width: 8), + Expanded( + child: OutlinedButton( + onPressed: () {}, + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + side: BorderSide( + color: isDarkMode ? Color.fromARGB(255, 54, 107, 232)! : Colors.grey[400]!, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + foregroundColor: isDarkMode ? Colors.white : Colors.black87, + ), + child: Text('B'), + ), + ), + const SizedBox(width: 8), + Expanded( + child: OutlinedButton( + onPressed: () {}, + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + side: BorderSide( + color: isDarkMode ? Color.fromARGB(255, 54, 107, 232)! : Colors.grey[400]!, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + foregroundColor: isDarkMode ? Colors.white : Colors.black87, + ), + child: Text('C'), + ), + ), + ], + ), + const SizedBox(height: 16), + ], + ), + ); +} + // Widget _buildButton(String label) { // return OutlinedButton( // onPressed: () {}, From 6b33e3ebd993824869ed0f3929e0727b997c6517 Mon Sep 17 00:00:00 2001 From: Ulquiorra-Keesre Date: Mon, 6 Apr 2026 17:08:01 +0300 Subject: [PATCH 08/22] Editor try1 --- manylines_editor/lib/main.dart | 601 +++++++++++---------------------- 1 file changed, 194 insertions(+), 407 deletions(-) diff --git a/manylines_editor/lib/main.dart b/manylines_editor/lib/main.dart index 57d42fc..b57d717 100644 --- a/manylines_editor/lib/main.dart +++ b/manylines_editor/lib/main.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_quill/flutter_quill.dart' as quill; import 'package:provider/provider.dart'; import 'package:dart_quill_delta/dart_quill_delta.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; // ==================== МОДЕЛИ ==================== class Project { @@ -80,9 +81,7 @@ class AppState extends ChangeNotifier { Project? _selectedProject; AppDocument? _selectedDocument; final Map _editors = {}; - bool _isDarkMode = false; - // ==================== GETTERS ==================== List get projects => _projects; @@ -90,11 +89,10 @@ class AppState extends ChangeNotifier { bool get switchableValue => _switchableValue; Project? get selectedProject => _selectedProject; AppDocument? get selectedDocument => _selectedDocument; - bool get isDarkMode => _isDarkMode; // ==================== METHODS ==================== - + void toggleDarkMode(bool value) { _isDarkMode = value; notifyListeners(); @@ -140,6 +138,22 @@ class AppState extends ChangeNotifier { notifyListeners(); } + // ✅ Метод для добавления документа в текущий проект + void addDocumentToCurrentProject() { + if (_selectedProject == null) return; + + final newDoc = AppDocument( + id: 'd${DateTime.now().millisecondsSinceEpoch}', + name: 'Document ${_selectedProject!.documents.length + 1}', + viewCount: 0, + content: Delta()..insert('New document content...\n'), + ); + + _selectedProject!.documents.add(newDoc); + _selectedDocument = newDoc; // Сразу открываем новый документ + notifyListeners(); + } + void selectProject(Project project) { _selectedProject = project; final mostUsed = AppDocument.getMostUsed(project.documents); @@ -168,19 +182,14 @@ class AppState extends ChangeNotifier { } void reorderSettings(int oldIndex, int newIndex) { - if (newIndex > oldIndex) { - newIndex -= 1; - } + if (newIndex > oldIndex) newIndex -= 1; final item = _settings.removeAt(oldIndex); _settings.insert(newIndex, item); notifyListeners(); } - // ✅ Добавлен метод для перетаскивания проектов void reorderProjects(int oldIndex, int newIndex) { - if (newIndex > oldIndex) { - newIndex -= 1; - } + if (newIndex > oldIndex) newIndex -= 1; final item = _projects.removeAt(oldIndex); _projects.insert(newIndex, item); notifyListeners(); @@ -224,6 +233,14 @@ void main() { builder: (context, state, _) { return MaterialApp( title: 'Manyllines', + localizationsDelegates: const [ + quill.FlutterQuillLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru', 'RU'), Locale('en', 'US')], + locale: const Locale('ru', 'RU'), theme: state.isDarkMode ? ThemeData( useMaterial3: true, @@ -270,11 +287,9 @@ class ProjectsScreen extends StatelessWidget { return Scaffold( body: Column( children: [ - // Header с Logo и Manyllines Consumer( builder: (context, state, _) { - final headerBg = state.isDarkMode ? Colors.grey[850] : Colors.white; final logoBg = state.isDarkMode ? Colors.grey[800] : Colors.white; final borderColor = state.isDarkMode ? Colors.grey[700]! : Colors.grey[300]!; @@ -299,21 +314,14 @@ class ProjectsScreen extends StatelessWidget { ), child: Text( 'Logo', - style: TextStyle( - fontWeight: FontWeight.bold, - color: logoTextColor, - ), + style: TextStyle(fontWeight: FontWeight.bold, color: logoTextColor), ), ), const SizedBox(width: 12), Expanded( child: Text( 'Manyllines', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w600, - color: textColor, - ), + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: textColor), ), ), ], @@ -322,7 +330,7 @@ class ProjectsScreen extends StatelessWidget { }, ), - // Список проектов с перетаскиванием (управляется Switchable) + // Список проектов с перетаскиванием Consumer( builder: (context, state, _) { final bgColor = state.isDarkMode ? Colors.green[900] : Colors.green[50]; @@ -332,7 +340,6 @@ class ProjectsScreen extends StatelessWidget { final textColor = state.isDarkMode ? Colors.white : Colors.black87; if (state.switchableValue) { - //Режим с перетаскиванием (ReorderableListView) return ReorderableListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), @@ -344,9 +351,7 @@ class ProjectsScreen extends StatelessWidget { key: ValueKey(project.id), decoration: BoxDecoration( color: bgColor, - border: Border( - bottom: BorderSide(color: borderColor), - ), + border: Border(bottom: BorderSide(color: borderColor)), ), child: ListTile( title: Text(project.name, style: TextStyle(color: textColor)), @@ -358,30 +363,24 @@ class ProjectsScreen extends StatelessWidget { }, ); } else { - // ✅ Режим БЕЗ перетаскивания (обычный список) — ПРОЕКТЫ ОТОБРАЖАЮТСЯ! return Container( decoration: BoxDecoration( color: bgColor, - border: Border( - bottom: BorderSide(color: borderColor), - ), + border: Border(bottom: BorderSide(color: borderColor)), ), child: ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: state.projects.length, itemBuilder: (context, index) { - final project = state.projects[index]; // ← Получаем проект - return Container( // ← Возвращаем виджет! + final project = state.projects[index]; + return Container( key: ValueKey(project.id), decoration: BoxDecoration( - border: Border( - bottom: BorderSide(color: borderColor), - ), + border: Border(bottom: BorderSide(color: borderColor)), ), child: ListTile( title: Text(project.name, style: TextStyle(color: textColor)), - // ❌ Без иконки drag_handle onTap: () => state.selectProject(project), ), ); @@ -392,15 +391,12 @@ class ProjectsScreen extends StatelessWidget { }, ), - // ✅ Настройки с перетаскиванием (управляется Switchable) - // Настройки с перетаскиванием (управляется Switchable) + // Настройки с перетаскиванием Consumer( builder: (context, state, _) { - // ✅ Выбираем цвет в зависимости от темы final bgColor = state.isDarkMode ? Colors.blue[900] : Colors.blue[50]; final borderColor = state.isDarkMode ? Colors.blue[700] : Colors.blue[200]; final textColor = state.isDarkMode ? Colors.white : Colors.black87; - final settings = state.settings; if (state.switchableValue) { @@ -412,7 +408,6 @@ class ProjectsScreen extends StatelessWidget { itemBuilder: (context, index) { final setting = settings[index]; final isExpanded = setting['expanded'] ?? false; - return Column( key: ValueKey(setting['id']), mainAxisSize: MainAxisSize.min, @@ -426,33 +421,23 @@ class ProjectsScreen extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - setting['name'], - style: TextStyle(color: textColor), - ), + Text(setting['name'], style: TextStyle(color: textColor)), Row( children: [ IconButton( - icon: Icon( - isExpanded ? Icons.arrow_drop_down : Icons.arrow_drop_up, - color: textColor, - ), + icon: Icon(isExpanded ? Icons.arrow_drop_down : Icons.arrow_drop_up, color: textColor), onPressed: () => state.toggleSettingExpansion(setting['id']), ), if (state.switchableValue) - Icon(Icons.drag_handle, - color: state.isDarkMode ? Colors.white54 : Colors.grey), + Icon(Icons.drag_handle, color: state.isDarkMode ? Colors.white54 : Colors.grey), ], ), ], ), ), - if (setting['id'] == 'setting1' && isExpanded) - _buildDescriptionSection1(state.isDarkMode), - if (setting['id'] == 'setting2' && isExpanded) - _buildDescriptionSection2(state.isDarkMode), - if (setting['id'] == 'setting3' && isExpanded) - _buildDescriptionSection3(state.isDarkMode), + if (setting['id'] == 'setting1' && isExpanded) _buildDescriptionSection1(state.isDarkMode), + if (setting['id'] == 'setting2' && isExpanded) _buildDescriptionSection2(state.isDarkMode), + if (setting['id'] == 'setting3' && isExpanded) _buildDescriptionSection3(state.isDarkMode), ], ); }, @@ -465,7 +450,6 @@ class ProjectsScreen extends StatelessWidget { itemBuilder: (context, index) { final setting = settings[index]; final isExpanded = setting['expanded'] ?? false; - return Column( key: ValueKey(setting['id']), mainAxisSize: MainAxisSize.min, @@ -479,26 +463,17 @@ class ProjectsScreen extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - setting['name'], - style: TextStyle(color: textColor), - ), + Text(setting['name'], style: TextStyle(color: textColor)), IconButton( - icon: Icon( - isExpanded ? Icons.arrow_drop_down : Icons.arrow_drop_up, - color: textColor, - ), + icon: Icon(isExpanded ? Icons.arrow_drop_down : Icons.arrow_drop_up, color: textColor), onPressed: () => state.toggleSettingExpansion(setting['id']), ), ], ), ), - if (setting['id'] == 'setting1' && isExpanded) - _buildDescriptionSection1(state.isDarkMode), - if (setting['id'] == 'setting2' && isExpanded) - _buildDescriptionSection2(state.isDarkMode), - if (setting['id'] == 'setting3' && isExpanded) - _buildDescriptionSection3(state.isDarkMode), + if (setting['id'] == 'setting1' && isExpanded) _buildDescriptionSection1(state.isDarkMode), + if (setting['id'] == 'setting2' && isExpanded) _buildDescriptionSection2(state.isDarkMode), + if (setting['id'] == 'setting3' && isExpanded) _buildDescriptionSection3(state.isDarkMode), ], ); }, @@ -507,22 +482,17 @@ class ProjectsScreen extends StatelessWidget { }, ), - // Additional settings (голубой фон) - // Additional settings (голубой фон) — с поддержкой тёмной темы + // Additional settings Consumer( builder: (context, state, _) { final bgColor = state.isDarkMode ? const Color.fromARGB(255, 6, 58, 137) : Colors.blue[50]; final textColor = state.isDarkMode ? Colors.white54 : Colors.black54; - return Expanded( child: Container( color: bgColor, padding: const EdgeInsets.all(16), child: Center( - child: Text( - 'Other Settings ...', - style: TextStyle(color: textColor), - ), + child: Text('Other Settings ...', style: TextStyle(color: textColor)), ), ), ); @@ -537,323 +507,128 @@ class ProjectsScreen extends StatelessWidget { ); } - // ✅ Метод для построения секции Description (выпадает из Setting 3) - // ✅ Метод для построения секции Description для Setting 2 (с переключением темы) -Widget _buildDescriptionSection2(bool isDarkMode) { - return Container( - color: isDarkMode ? Colors.grey[850] : Colors.grey[50], - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - 'Description', - style: TextStyle( - fontSize: 14, - color: isDarkMode ? Colors.grey[300] : Colors.grey[700], + // ==================== ОПИСАНИЯ ДЛЯ НАСТРОЕК ==================== + Widget _buildDescriptionSection2(bool isDarkMode) { + return Container( + color: isDarkMode ? Colors.grey[850] : Colors.grey[50], + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text('Description', style: TextStyle(fontSize: 14, color: isDarkMode ? Colors.grey[300] : Colors.grey[700])), + const SizedBox(height: 12), + Row( + children: [ + _buildOutlinedButton('A', isDarkMode), + const SizedBox(width: 8), + _buildOutlinedButton('B', isDarkMode), + const SizedBox(width: 8), + _buildOutlinedButton('C', isDarkMode), + ], ), - ), - const SizedBox(height: 12), - - // Кнопки A, B, C - Row( - children: [ - Expanded( - child: OutlinedButton( - onPressed: () {}, - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 12), - side: BorderSide( - color: isDarkMode ? Color.fromARGB(255, 54, 107, 232)! : Colors.grey[400]!, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - foregroundColor: isDarkMode ? Colors.white : Colors.black87, - ), - child: Text('A'), - ), - ), - const SizedBox(width: 8), - Expanded( - child: OutlinedButton( - onPressed: () {}, - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 12), - side: BorderSide( - color: isDarkMode ? Color.fromARGB(255, 54, 107, 232)! : Colors.grey[400]!, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - foregroundColor: isDarkMode ? Colors.white : Colors.black87, - ), - child: Text('B'), - ), - ), - const SizedBox(width: 8), - Expanded( - child: OutlinedButton( - onPressed: () {}, - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 12), - side: BorderSide( - color: isDarkMode ? const Color.fromARGB(255, 54, 107, 232)! : Colors.grey[400]!, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), + const SizedBox(height: 16), + Consumer( + builder: (context, state, _) { + final isDark = state.isDarkMode; + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Icon(isDark ? Icons.dark_mode : Icons.light_mode, size: 20, color: isDark ? Colors.yellow[200] : Colors.orange), + const SizedBox(width: 8), + Text(isDark ? 'Тёмная тема' : 'Светлая тема', style: TextStyle(color: isDarkMode ? Colors.white : Colors.black87)), + ], ), - foregroundColor: isDarkMode ? Colors.white : Colors.black87, - ), - child: Text('C'), - ), - ), - ], - ), - const SizedBox(height: 16), - - // Switchable для переключения темы - Consumer( - builder: (context, state, _) { - final isDark = state.isDarkMode; - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Icon( - isDark ? Icons.dark_mode : Icons.light_mode, - size: 20, - color: isDark ? Colors.yellow[200] : Colors.orange, - ), - const SizedBox(width: 8), - Text( - isDark ? 'Тёмная тема' : 'Светлая тема', - style: TextStyle(color: isDarkMode ? Colors.white : Colors.black87), - ), - ], - ), - Switch( - value: isDark, - onChanged: (value) { - state.toggleDarkMode(value); - }, - ), - ], - ); - }, - ), - ], - ), - ); -} - -// ✅ Метод для построения секции Description для Setting 3 -Widget _buildDescriptionSection3(bool isDarkMode) { - return Container( - color: isDarkMode ? Colors.grey[850] : Colors.grey[50], - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - 'Description', - style: TextStyle( - fontSize: 14, - color: isDarkMode ? Colors.grey[300] : Colors.grey[700], + Switch(value: isDark, onChanged: (value) => state.toggleDarkMode(value)), + ], + ); + }, ), - ), - const SizedBox(height: 12), - - // Кнопки A, B, C - Row( - children: [ - Expanded( - child: OutlinedButton( - onPressed: () {}, - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 12), - side: BorderSide( - color: isDarkMode ? Color.fromARGB(255, 54, 107, 232)! : Colors.grey[400]!, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - foregroundColor: isDarkMode ? Colors.white : Colors.black87, - ), - child: Text('A'), - ), - ), - const SizedBox(width: 8), - Expanded( - child: OutlinedButton( - onPressed: () {}, - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 12), - side: BorderSide( - color: isDarkMode ? Color.fromARGB(255, 54, 107, 232)! : Colors.grey[400]!, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - foregroundColor: isDarkMode ? Colors.white : Colors.black87, - ), - child: Text('B'), - ), - ), - const SizedBox(width: 8), - Expanded( - child: OutlinedButton( - onPressed: () {}, - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 12), - side: BorderSide( - color: isDarkMode ? Color.fromARGB(255, 54, 107, 232)! : Colors.grey[400]!, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - foregroundColor: isDarkMode ? Colors.white : Colors.black87, - ), - child: Text('C'), - ), - ), - ], - ), - const SizedBox(height: 16), - - // Switchable - Consumer( - builder: (context, state, _) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'Switchable', - style: TextStyle(color: isDarkMode ? Colors.white : Colors.black87), - ), - Switch( - value: state.switchableValue, - onChanged: (value) { - state.setSwitchableValue(value); - }, - ), - ], - ); - }, - ), - - - - // Listable - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'Listable', - style: TextStyle(color: isDarkMode ? Colors.white : Colors.black87), - ), - IconButton( - icon: Icon(Icons.arrow_drop_down, - color: isDarkMode ? Colors.white : Colors.black87), - onPressed: () {}, - ), - ], - ), - ], - ), - ); -} + ], + ), + ); + } -Widget _buildDescriptionSection1(bool isDarkMode) { - return Container( - color: isDarkMode ? Colors.grey[850] : Colors.grey[50], - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - 'Description', - style: TextStyle( - fontSize: 14, - color: isDarkMode ? Colors.grey[300] : Colors.grey[700], + Widget _buildDescriptionSection3(bool isDarkMode) { + return Container( + color: isDarkMode ? Colors.grey[850] : Colors.grey[50], + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text('Description', style: TextStyle(fontSize: 14, color: isDarkMode ? Colors.grey[300] : Colors.grey[700])), + const SizedBox(height: 12), + Row( + children: [ + _buildOutlinedButton('A', isDarkMode), + const SizedBox(width: 8), + _buildOutlinedButton('B', isDarkMode), + const SizedBox(width: 8), + _buildOutlinedButton('C', isDarkMode), + ], ), - ), - const SizedBox(height: 12), - - // Кнопки A, B, C - Row( - children: [ - Expanded( - child: OutlinedButton( - onPressed: () {}, - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 12), - side: BorderSide( - color: isDarkMode ? Color.fromARGB(255, 54, 107, 232)! : Colors.grey[400]!, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - foregroundColor: isDarkMode ? Colors.white : Colors.black87, - ), - child: Text('A'), - ), - ), - const SizedBox(width: 8), - Expanded( - child: OutlinedButton( - onPressed: () {}, - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 12), - side: BorderSide( - color: isDarkMode ? Color.fromARGB(255, 54, 107, 232)! : Colors.grey[400]!, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - foregroundColor: isDarkMode ? Colors.white : Colors.black87, - ), - child: Text('B'), - ), - ), - const SizedBox(width: 8), - Expanded( - child: OutlinedButton( - onPressed: () {}, - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 12), - side: BorderSide( - color: isDarkMode ? Color.fromARGB(255, 54, 107, 232)! : Colors.grey[400]!, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - foregroundColor: isDarkMode ? Colors.white : Colors.black87, - ), - child: Text('C'), - ), - ), - ], - ), - const SizedBox(height: 16), - ], - ), - ); -} + const SizedBox(height: 16), + Consumer( + builder: (context, state, _) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Switchable', style: TextStyle(color: isDarkMode ? Colors.white : Colors.black87)), + Switch(value: state.switchableValue, onChanged: (value) => state.setSwitchableValue(value)), + ], + ); + }, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Listable', style: TextStyle(color: isDarkMode ? Colors.white : Colors.black87)), + IconButton(icon: Icon(Icons.arrow_drop_down, color: isDarkMode ? Colors.white : Colors.black87), onPressed: () {}), + ], + ), + ], + ), + ); + } - // Widget _buildButton(String label) { - // return OutlinedButton( - // onPressed: () {}, - // style: OutlinedButton.styleFrom( - // padding: const EdgeInsets.symmetric(vertical: 12), - // side: BorderSide(color: Colors.grey[400]!), - // ), - // child: Text(label), - // ); - // } + Widget _buildDescriptionSection1(bool isDarkMode) { + return Container( + color: isDarkMode ? Colors.grey[850] : Colors.grey[50], + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text('Description', style: TextStyle(fontSize: 14, color: isDarkMode ? Colors.grey[300] : Colors.grey[700])), + const SizedBox(height: 12), + Row( + children: [ + _buildOutlinedButton('A', isDarkMode), + const SizedBox(width: 8), + _buildOutlinedButton('B', isDarkMode), + const SizedBox(width: 8), + _buildOutlinedButton('C', isDarkMode), + ], + ), + ], + ), + ); + } - + Widget _buildOutlinedButton(String label, bool isDarkMode) { + return Expanded( + child: OutlinedButton( + onPressed: () {}, + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + side: BorderSide(color: isDarkMode ? const Color.fromARGB(255, 54, 107, 232) : Colors.grey[400]!), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + foregroundColor: isDarkMode ? Colors.white : Colors.black87, + ), + child: Text(label), + ), + ); + } } // ==================== РАБОЧЕЕ ПРОСТРАНСТВО ==================== @@ -873,18 +648,24 @@ class ProjectWorkspace extends StatelessWidget { : _MobileEditorView(document: state.selectedDocument!); } + // ✅ Цвета для тёмной темы в левой панели + final leftPanelBg = state.isDarkMode ? Colors.grey[900] : Colors.white; + final headerBg = state.isDarkMode ? Colors.green[900] : Colors.green[50]; + final textColor = state.isDarkMode ? Colors.white : Colors.black87; + return Row( children: [ + // Левая панель - список документов Container( width: 300, decoration: BoxDecoration( - border: Border(right: BorderSide(color: Colors.grey[300]!)), + border: Border(right: BorderSide(color: state.isDarkMode ? Colors.grey[700]! : Colors.grey[300]!)), ), child: Column( children: [ Container( padding: const EdgeInsets.all(16), - color: Colors.green[50], + color: headerBg, child: Row( children: [ IconButton( @@ -895,20 +676,32 @@ class ProjectWorkspace extends StatelessWidget { Expanded( child: Text( state.selectedProject!.name, - style: Theme.of(context).textTheme.titleMedium, + style: Theme.of(context).textTheme.titleMedium?.copyWith(color: textColor), ), ), ], ), ), - const Expanded(child: _DocumentsList()), + // ✅ Material wrapper для ListTile + Expanded( + child: Material( + color: leftPanelBg, + child: _DocumentsList(), + ), + ), ], ), ), + // Правая панель - редактор Expanded( child: state.selectedDocument != null ? QuillEditorView(document: state.selectedDocument!) - : const Center(child: Text('Select a document')), + : Center( + child: Text( + 'Выберите документ', + style: TextStyle(color: state.isDarkMode ? Colors.white70 : Colors.black54), + ), + ), ), ], ); @@ -924,7 +717,9 @@ class _DocumentsList extends StatelessWidget { @override Widget build(BuildContext context) { final state = context.watch(); - final docs = state.selectedProject!.documents; + // ✅ Сортируем документы по viewCount (самые популярные сверху) + final docs = List.from(state.selectedProject!.documents) + ..sort((a, b) => b.viewCount.compareTo(a.viewCount)); return ListView.builder( itemCount: docs.length, @@ -941,9 +736,7 @@ class _DocumentsList extends StatelessWidget { : Colors.grey[600], ), title: Text(doc.name), - subtitle: isSelected - ? null - : Text('Views: ${doc.viewCount}', style: TextStyle(fontSize: 12, color: Colors.grey[600])), + subtitle: isSelected ? null : Text('Views: ${doc.viewCount}', style: TextStyle(fontSize: 12, color: Colors.grey[600])), onTap: () => state.selectDocument(doc), ); }, @@ -1021,10 +814,7 @@ class _MobileDocList extends StatelessWidget { return Scaffold( appBar: AppBar( title: Text(state.selectedProject!.name), - leading: IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () => state.clearSelectedProject(), - ), + leading: IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => state.clearSelectedProject()), ), body: const _DocumentsList(), ); @@ -1040,10 +830,7 @@ class _MobileEditorView extends StatelessWidget { return Scaffold( appBar: AppBar( title: Text(document.name), - leading: IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () {}, - ), + leading: IconButton(icon: const Icon(Icons.arrow_back), onPressed: () {}), ), body: QuillEditorView(document: document), ); From 028b5986ac867333880a22439a0726dca2c12745 Mon Sep 17 00:00:00 2001 From: Ulquiorra-Keesre Date: Mon, 6 Apr 2026 19:51:29 +0300 Subject: [PATCH 09/22] isMostUsed fix --- manylines_editor/lib/main.dart | 220 ++++++++++++++++++++------------- 1 file changed, 134 insertions(+), 86 deletions(-) diff --git a/manylines_editor/lib/main.dart b/manylines_editor/lib/main.dart index b57d717..2a18040 100644 --- a/manylines_editor/lib/main.dart +++ b/manylines_editor/lib/main.dart @@ -10,7 +10,22 @@ class Project { final String name; final List documents; - Project({required this.id, required this.name, required this.documents}); + Project({ + required this.id, + required this.name, + required this.documents, + }); + + // ✅ Метод для получения максимального количества просмотров в проекте + int get maxViewCount { + if (documents.isEmpty) return 0; + return documents.map((doc) => doc.viewCount).reduce((a, b) => a > b ? a : b); + } + + // ✅ Метод для проверки, является ли документ самым популярным + bool isDocumentMostUsed(AppDocument doc) { + return doc.viewCount >= maxViewCount && maxViewCount > 0; + } } class AppDocument { @@ -25,15 +40,6 @@ class AppDocument { this.viewCount = 0, required this.content, }); - - bool get isMostUsed => viewCount > 0; - - static AppDocument? getMostUsed(List docs) { - if (docs.isEmpty) return null; - final sorted = List.from(docs) - ..sort((a, b) => b.viewCount.compareTo(a.viewCount)); - return sorted.first; - } } // ==================== STATE ==================== @@ -80,7 +86,6 @@ class AppState extends ChangeNotifier { bool _switchableValue = true; Project? _selectedProject; AppDocument? _selectedDocument; - final Map _editors = {}; bool _isDarkMode = false; // ==================== GETTERS ==================== @@ -103,22 +108,18 @@ class AppState extends ChangeNotifier { notifyListeners(); } - void saveDocumentContent(AppDocument doc, Delta content) { - doc.content = content; - notifyListeners(); - } - + // ✅ Создаёт НОВЫЙ контроллер для каждого документа quill.QuillController getOrCreateController(AppDocument doc) { - if (!_editors.containsKey(doc.id)) { - _editors[doc.id] = quill.QuillController( - document: quill.Document.fromJson(doc.content.toJson()), - selection: const TextSelection.collapsed(offset: 0), - ); - _editors[doc.id]!.changes.listen((_) { - saveDocumentContent(doc, _editors[doc.id]!.document.toDelta()); - }); - } - return _editors[doc.id]!; + final controller = quill.QuillController( + document: quill.Document.fromJson(doc.content.toJson()), + selection: const TextSelection.collapsed(offset: 0), + ); + + controller.changes.listen((change) { + doc.content = controller.document.toDelta(); + }); + + return controller; } void addProject() { @@ -138,7 +139,6 @@ class AppState extends ChangeNotifier { notifyListeners(); } - // ✅ Метод для добавления документа в текущий проект void addDocumentToCurrentProject() { if (_selectedProject == null) return; @@ -150,13 +150,15 @@ class AppState extends ChangeNotifier { ); _selectedProject!.documents.add(newDoc); - _selectedDocument = newDoc; // Сразу открываем новый документ + _selectedDocument = newDoc; notifyListeners(); } void selectProject(Project project) { _selectedProject = project; - final mostUsed = AppDocument.getMostUsed(project.documents); + final mostUsed = project.documents.isNotEmpty + ? project.documents.reduce((a, b) => a.viewCount > b.viewCount ? a : b) + : null; _selectedDocument = mostUsed ?? project.documents.first; if (_selectedDocument != null) { incrementViewCount(_selectedDocument!); @@ -217,9 +219,6 @@ class AppState extends ChangeNotifier { @override void dispose() { - for (var controller in _editors.values) { - controller.dispose(); - } super.dispose(); } } @@ -648,62 +647,89 @@ class ProjectWorkspace extends StatelessWidget { : _MobileEditorView(document: state.selectedDocument!); } - // ✅ Цвета для тёмной темы в левой панели final leftPanelBg = state.isDarkMode ? Colors.grey[900] : Colors.white; final headerBg = state.isDarkMode ? Colors.green[900] : Colors.green[50]; final textColor = state.isDarkMode ? Colors.white : Colors.black87; + final borderColor = state.isDarkMode ? Colors.grey[700]! : Colors.grey[300]!; - return Row( - children: [ - // Левая панель - список документов - Container( - width: 300, - decoration: BoxDecoration( - border: Border(right: BorderSide(color: state.isDarkMode ? Colors.grey[700]! : Colors.grey[300]!)), - ), - child: Column( - children: [ - Container( - padding: const EdgeInsets.all(16), - color: headerBg, - child: Row( - children: [ - IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () => state.clearSelectedProject(), - tooltip: 'Back to projects', - ), - Expanded( - child: Text( - state.selectedProject!.name, - style: Theme.of(context).textTheme.titleMedium?.copyWith(color: textColor), + return Scaffold( + body: Row( + children: [ + Container( + width: 300, + decoration: BoxDecoration( + border: Border(right: BorderSide(color: borderColor)), + ), + child: Column( + children: [ + Container( + padding: const EdgeInsets.all(16), + color: headerBg, + child: Row( + children: [ + IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => state.clearSelectedProject(), + tooltip: 'Back to projects', ), - ), - ], + Expanded( + child: Text( + state.selectedProject!.name, + style: Theme.of(context).textTheme.titleMedium?.copyWith(color: textColor), + ), + ), + ], + ), ), - ), - // ✅ Material wrapper для ListTile - Expanded( - child: Material( - color: leftPanelBg, - child: _DocumentsList(), + Expanded( + child: Material( + color: leftPanelBg, + child: Column( + children: [ + Expanded(child: _DocumentsList()), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + border: Border(top: BorderSide(color: borderColor)), + ), + child: SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + onPressed: () => state.addDocumentToCurrentProject(), + icon: const Icon(Icons.add, size: 18), + label: const Text('Новый документ'), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + foregroundColor: state.isDarkMode ? Colors.white : Colors.green[700], + side: BorderSide(color: state.isDarkMode ? Colors.green[400]! : Colors.green[700]!), + ), + ), + ), + ), + ], + ), + ), ), - ), - ], + ], + ), ), - ), - // Правая панель - редактор - Expanded( - child: state.selectedDocument != null - ? QuillEditorView(document: state.selectedDocument!) - : Center( - child: Text( - 'Выберите документ', - style: TextStyle(color: state.isDarkMode ? Colors.white70 : Colors.black54), + Expanded( + child: state.selectedDocument != null + ? QuillEditorView(document: state.selectedDocument!) + : Center( + child: Text( + 'Выберите документ или создайте новый', + style: TextStyle(color: state.isDarkMode ? Colors.white70 : Colors.black54), + ), ), - ), - ), - ], + ), + ], + ), + floatingActionButton: FloatingActionButton( + onPressed: () => state.addDocumentToCurrentProject(), + tooltip: 'Создать документ', + child: const Icon(Icons.add), + ), ); }, ); @@ -717,8 +743,8 @@ class _DocumentsList extends StatelessWidget { @override Widget build(BuildContext context) { final state = context.watch(); - // ✅ Сортируем документы по viewCount (самые популярные сверху) - final docs = List.from(state.selectedProject!.documents) + final project = state.selectedProject!; + final docs = List.from(project.documents) ..sort((a, b) => b.viewCount.compareTo(a.viewCount)); return ListView.builder( @@ -726,11 +752,15 @@ class _DocumentsList extends StatelessWidget { itemBuilder: (context, index) { final doc = docs[index]; final isSelected = state.selectedDocument?.id == doc.id; + + // ✅ Вычисляем isMostUsed через проект + final isMostUsed = project.isDocumentMostUsed(doc); + return ListTile( selected: isSelected, selectedTileColor: Theme.of(context).colorScheme.secondaryContainer, leading: Icon( - doc.isMostUsed ? Icons.star : Icons.insert_drive_file, + isMostUsed ? Icons.star : Icons.insert_drive_file, color: isSelected ? Theme.of(context).colorScheme.onSecondaryContainer : Colors.grey[600], @@ -753,27 +783,45 @@ class QuillEditorView extends StatefulWidget { } class _QuillEditorViewState extends State { - late quill.QuillController _controller; + quill.QuillController? _controller; @override void initState() { super.initState(); + _initializeController(); + } + + @override + void didUpdateWidget(QuillEditorView oldWidget) { + super.didUpdateWidget(oldWidget); + // ✅ Если документ изменился - создаём НОВЫЙ контроллер + if (oldWidget.document.id != widget.document.id) { + _controller?.dispose(); + _initializeController(); + } + } + + void _initializeController() { final state = context.read(); _controller = state.getOrCreateController(widget.document); } @override void dispose() { - _controller.dispose(); + _controller?.dispose(); super.dispose(); } @override Widget build(BuildContext context) { + if (_controller == null) { + return const Center(child: CircularProgressIndicator()); + } + return Column( children: [ quill.QuillSimpleToolbar( - controller: _controller, + controller: _controller!, config: const quill.QuillSimpleToolbarConfig( showBoldButton: true, showItalicButton: true, @@ -791,7 +839,7 @@ class _QuillEditorViewState extends State { ), Expanded( child: quill.QuillEditor( - controller: _controller, + controller: _controller!, config: quill.QuillEditorConfig( placeholder: 'Начните печатать...', padding: const EdgeInsets.all(16), From 100281139180f04048451f7900899e8c9a9eb0d3 Mon Sep 17 00:00:00 2001 From: Ulquiorra-Keesre Date: Mon, 6 Apr 2026 20:17:02 +0300 Subject: [PATCH 10/22] Text Editor fix --- manylines_editor/lib/main.dart | 312 ++++++++++++++++++++------------- 1 file changed, 187 insertions(+), 125 deletions(-) diff --git a/manylines_editor/lib/main.dart b/manylines_editor/lib/main.dart index 2a18040..d601002 100644 --- a/manylines_editor/lib/main.dart +++ b/manylines_editor/lib/main.dart @@ -16,13 +16,11 @@ class Project { required this.documents, }); - // ✅ Метод для получения максимального количества просмотров в проекте int get maxViewCount { if (documents.isEmpty) return 0; return documents.map((doc) => doc.viewCount).reduce((a, b) => a > b ? a : b); } - // ✅ Метод для проверки, является ли документ самым популярным bool isDocumentMostUsed(AppDocument doc) { return doc.viewCount >= maxViewCount && maxViewCount > 0; } @@ -45,35 +43,16 @@ class AppDocument { // ==================== STATE ==================== class AppState extends ChangeNotifier { final List _projects = [ + // ✅ Проекты создаются ПУСТЫМИ (без документов) Project( id: 'p1', name: 'Project 1', - documents: [ - AppDocument( - id: 'd1', - name: 'Main Document', - viewCount: 15, - content: Delta()..insert('Welcome to Project 1!\n'), - ), - AppDocument( - id: 'd2', - name: 'Specifications', - viewCount: 8, - content: Delta()..insert('Technical specifications...\n'), - ), - ], + documents: [], // ← Пустой список! ), Project( id: 'p2', name: 'Project 2', - documents: [ - AppDocument( - id: 'd3', - name: 'Overview', - viewCount: 23, - content: Delta()..insert('Project overview...\n'), - ), - ], + documents: [], // ← Пустой список! ), ]; @@ -122,23 +101,18 @@ class AppState extends ChangeNotifier { return controller; } + // ✅ Project создаётся БЕЗ документов void addProject() { final newId = 'p${_projects.length + 1}'; _projects.add(Project( id: newId, name: 'Project ${_projects.length + 1}', - documents: [ - AppDocument( - id: 'd${DateTime.now().millisecondsSinceEpoch}', - name: 'New Document', - viewCount: 1, - content: Delta()..insert('Start typing...\n'), - ), - ], + documents: [], // ← Пустой список! )); notifyListeners(); } + // ✅ Добавляет документ в текущий проект void addDocumentToCurrentProject() { if (_selectedProject == null) return; @@ -150,18 +124,23 @@ class AppState extends ChangeNotifier { ); _selectedProject!.documents.add(newDoc); - _selectedDocument = newDoc; + _selectedDocument = newDoc; // ✅ Сразу открываем новый документ notifyListeners(); } void selectProject(Project project) { _selectedProject = project; - final mostUsed = project.documents.isNotEmpty - ? project.documents.reduce((a, b) => a.viewCount > b.viewCount ? a : b) - : null; - _selectedDocument = mostUsed ?? project.documents.first; - if (_selectedDocument != null) { - incrementViewCount(_selectedDocument!); + + // ✅ Если есть документы - выбираем самый популярный + if (project.documents.isNotEmpty) { + final mostUsed = project.documents.reduce((a, b) => + a.viewCount > b.viewCount ? a : b + ); + _selectedDocument = mostUsed; + incrementViewCount(mostUsed); + } else { + // ✅ Если документов нет - очищаем selectedDocument + _selectedDocument = null; } notifyListeners(); } @@ -329,7 +308,7 @@ class ProjectsScreen extends StatelessWidget { }, ), - // Список проектов с перетаскиванием + // Список проектов Consumer( builder: (context, state, _) { final bgColor = state.isDarkMode ? Colors.green[900] : Colors.green[50]; @@ -354,6 +333,10 @@ class ProjectsScreen extends StatelessWidget { ), child: ListTile( title: Text(project.name, style: TextStyle(color: textColor)), + subtitle: Text( + '${project.documents.length} документов', + style: TextStyle(fontSize: 12, color: textColor.withOpacity(0.7)), + ), trailing: Icon(Icons.drag_handle, color: state.isDarkMode ? Colors.white54 : Colors.grey), onTap: () => state.selectProject(project), @@ -380,6 +363,10 @@ class ProjectsScreen extends StatelessWidget { ), child: ListTile( title: Text(project.name, style: TextStyle(color: textColor)), + subtitle: Text( + '${project.documents.length} документов', + style: TextStyle(fontSize: 12, color: textColor.withOpacity(0.7)), + ), onTap: () => state.selectProject(project), ), ); @@ -390,7 +377,7 @@ class ProjectsScreen extends StatelessWidget { }, ), - // Настройки с перетаскиванием + // Настройки Consumer( builder: (context, state, _) { final bgColor = state.isDarkMode ? Colors.blue[900] : Colors.blue[50]; @@ -506,7 +493,6 @@ class ProjectsScreen extends StatelessWidget { ); } - // ==================== ОПИСАНИЯ ДЛЯ НАСТРОЕК ==================== Widget _buildDescriptionSection2(bool isDarkMode) { return Container( color: isDarkMode ? Colors.grey[850] : Colors.grey[50], @@ -630,6 +616,7 @@ class ProjectsScreen extends StatelessWidget { } } +// ==================== РАБОЧЕЕ ПРОСТРАНСТВО ==================== // ==================== РАБОЧЕЕ ПРОСТРАНСТВО ==================== class ProjectWorkspace extends StatelessWidget { const ProjectWorkspace({super.key}); @@ -639,99 +626,144 @@ class ProjectWorkspace extends StatelessWidget { return LayoutBuilder( builder: (context, constraints) { final isWide = constraints.maxWidth >= 700; - final state = context.read(); if (!isWide) { - return state.selectedDocument == null - ? const _MobileDocList() - : _MobileEditorView(document: state.selectedDocument!); + // ✅ Используем watch для мобильных + return Consumer( + builder: (context, state, _) { + return state.selectedDocument == null + ? const _MobileDocList() + : _MobileEditorView(document: state.selectedDocument!); + }, + ); } - final leftPanelBg = state.isDarkMode ? Colors.grey[900] : Colors.white; - final headerBg = state.isDarkMode ? Colors.green[900] : Colors.green[50]; - final textColor = state.isDarkMode ? Colors.white : Colors.black87; - final borderColor = state.isDarkMode ? Colors.grey[700]! : Colors.grey[300]!; + // ✅ Используем Selector для отслеживания selectedDocument + return Selector( + selector: (_, state) => state.selectedDocument, + builder: (context, selectedDocument, _) { + return _buildDesktopLayout(context, selectedDocument); + }, + ); + }, + ); + } - return Scaffold( - body: Row( - children: [ - Container( - width: 300, - decoration: BoxDecoration( - border: Border(right: BorderSide(color: borderColor)), + Widget _buildDesktopLayout(BuildContext context, AppDocument? selectedDocument) { + final state = context.read(); + final leftPanelBg = state.isDarkMode ? Colors.grey[900] : Colors.white; + final headerBg = state.isDarkMode ? Colors.green[900] : Colors.green[50]; + final textColor = state.isDarkMode ? Colors.white : Colors.black87; + final borderColor = state.isDarkMode ? Colors.grey[700]! : Colors.grey[300]!; + + return Scaffold( + body: Row( + children: [ + Container( + width: 300, + decoration: BoxDecoration( + border: Border(right: BorderSide(color: borderColor)), + ), + child: Column( + children: [ + Container( + padding: const EdgeInsets.all(16), + color: headerBg, + child: Row( + children: [ + IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => state.clearSelectedProject(), + tooltip: 'Back to projects', + ), + Expanded( + child: Text( + state.selectedProject!.name, + style: Theme.of(context).textTheme.titleMedium?.copyWith(color: textColor), + ), + ), + ], + ), ), - child: Column( - children: [ - Container( - padding: const EdgeInsets.all(16), - color: headerBg, - child: Row( - children: [ - IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () => state.clearSelectedProject(), - tooltip: 'Back to projects', - ), - Expanded( - child: Text( - state.selectedProject!.name, - style: Theme.of(context).textTheme.titleMedium?.copyWith(color: textColor), - ), + Expanded( + child: Material( + color: leftPanelBg, + child: Column( + children: [ + const Expanded(child: _DocumentsList()), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + border: Border(top: BorderSide(color: borderColor)), ), - ], - ), - ), - Expanded( - child: Material( - color: leftPanelBg, - child: Column( - children: [ - Expanded(child: _DocumentsList()), - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - border: Border(top: BorderSide(color: borderColor)), - ), - child: SizedBox( - width: double.infinity, - child: OutlinedButton.icon( - onPressed: () => state.addDocumentToCurrentProject(), - icon: const Icon(Icons.add, size: 18), - label: const Text('Новый документ'), - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 12), - foregroundColor: state.isDarkMode ? Colors.white : Colors.green[700], - side: BorderSide(color: state.isDarkMode ? Colors.green[400]! : Colors.green[700]!), - ), - ), + child: SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + onPressed: () => state.addDocumentToCurrentProject(), + icon: const Icon(Icons.add, size: 18), + label: const Text('Новый документ'), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + foregroundColor: state.isDarkMode ? Colors.white : Colors.green[700], + side: BorderSide(color: state.isDarkMode ? Colors.green[400]! : Colors.green[700]!), ), ), - ], + ), ), - ), + ], ), - ], + ), ), - ), - Expanded( - child: state.selectedDocument != null - ? QuillEditorView(document: state.selectedDocument!) - : Center( - child: Text( - 'Выберите документ или создайте новый', - style: TextStyle(color: state.isDarkMode ? Colors.white70 : Colors.black54), - ), - ), - ), - ], + ], + ), ), - floatingActionButton: FloatingActionButton( - onPressed: () => state.addDocumentToCurrentProject(), - tooltip: 'Создать документ', - child: const Icon(Icons.add), + Expanded( + child: selectedDocument != null + ? QuillEditorView(document: selectedDocument) + : Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.description_outlined, + size: 64, + color: state.isDarkMode ? Colors.white30 : Colors.black26, + ), + const SizedBox(height: 16), + Text( + 'В проекте нет документов', + style: TextStyle( + fontSize: 18, + color: state.isDarkMode ? Colors.white70 : Colors.black54, + ), + ), + const SizedBox(height: 8), + Text( + 'Нажмите кнопку "+ Новый документ" чтобы создать', + style: TextStyle( + color: state.isDarkMode ? Colors.white54 : Colors.black45, + ), + ), + const SizedBox(height: 24), + ElevatedButton.icon( + onPressed: () => state.addDocumentToCurrentProject(), + icon: const Icon(Icons.add), + label: const Text('Создать первый документ'), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + ), + ), + ], + ), + ), ), - ); - }, + ], + ), + floatingActionButton: FloatingActionButton( + onPressed: () => state.addDocumentToCurrentProject(), + tooltip: 'Создать документ', + child: const Icon(Icons.add), + ), ); } } @@ -747,13 +779,31 @@ class _DocumentsList extends StatelessWidget { final docs = List.from(project.documents) ..sort((a, b) => b.viewCount.compareTo(a.viewCount)); + if (docs.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.folder_open_outlined, + size: 48, + color: Colors.grey[400], + ), + const SizedBox(height: 8), + Text( + 'Нет документов', + style: TextStyle(color: Colors.grey[600]), + ), + ], + ), + ); + } + return ListView.builder( itemCount: docs.length, itemBuilder: (context, index) { final doc = docs[index]; final isSelected = state.selectedDocument?.id == doc.id; - - // ✅ Вычисляем isMostUsed через проект final isMostUsed = project.isDocumentMostUsed(doc); return ListTile( @@ -766,7 +816,9 @@ class _DocumentsList extends StatelessWidget { : Colors.grey[600], ), title: Text(doc.name), - subtitle: isSelected ? null : Text('Views: ${doc.viewCount}', style: TextStyle(fontSize: 12, color: Colors.grey[600])), + subtitle: isSelected + ? null + : Text('Views: ${doc.viewCount}', style: TextStyle(fontSize: 12, color: Colors.grey[600])), onTap: () => state.selectDocument(doc), ); }, @@ -794,7 +846,6 @@ class _QuillEditorViewState extends State { @override void didUpdateWidget(QuillEditorView oldWidget) { super.didUpdateWidget(oldWidget); - // ✅ Если документ изменился - создаём НОВЫЙ контроллер if (oldWidget.document.id != widget.document.id) { _controller?.dispose(); _initializeController(); @@ -863,8 +914,19 @@ class _MobileDocList extends StatelessWidget { appBar: AppBar( title: Text(state.selectedProject!.name), leading: IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => state.clearSelectedProject()), + actions: [ + IconButton( + icon: const Icon(Icons.add), + onPressed: () => state.addDocumentToCurrentProject(), + tooltip: 'Новый документ', + ), + ], ), body: const _DocumentsList(), + floatingActionButton: FloatingActionButton( + onPressed: () => state.addDocumentToCurrentProject(), + child: const Icon(Icons.add), + ), ); } } From 3952d350837b41cea52355cda7e38eb1f8ec1580 Mon Sep 17 00:00:00 2001 From: Ulquiorra-Keesre Date: Mon, 6 Apr 2026 20:32:43 +0300 Subject: [PATCH 11/22] small color fix --- manylines_editor/lib/main.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manylines_editor/lib/main.dart b/manylines_editor/lib/main.dart index d601002..748ee7d 100644 --- a/manylines_editor/lib/main.dart +++ b/manylines_editor/lib/main.dart @@ -606,7 +606,7 @@ class ProjectsScreen extends StatelessWidget { onPressed: () {}, style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 12), - side: BorderSide(color: isDarkMode ? const Color.fromARGB(255, 54, 107, 232) : Colors.grey[400]!), + side: BorderSide(color: isDarkMode ? const Color.fromARGB(163, 162, 164, 170) : Colors.grey[400]!), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), foregroundColor: isDarkMode ? Colors.white : Colors.black87, ), From 503c81a65c128a70f69f8c56c6edcea601a81bcd Mon Sep 17 00:00:00 2001 From: Ulquiorra-Keesre Date: Thu, 9 Apr 2026 15:58:59 +0300 Subject: [PATCH 12/22] Name creaction added --- manylines_editor/lib/main.dart | 257 +++++++++++++++++++++++++++++---- 1 file changed, 232 insertions(+), 25 deletions(-) diff --git a/manylines_editor/lib/main.dart b/manylines_editor/lib/main.dart index 748ee7d..41f8158 100644 --- a/manylines_editor/lib/main.dart +++ b/manylines_editor/lib/main.dart @@ -43,16 +43,15 @@ class AppDocument { // ==================== STATE ==================== class AppState extends ChangeNotifier { final List _projects = [ - // ✅ Проекты создаются ПУСТЫМИ (без документов) Project( id: 'p1', name: 'Project 1', - documents: [], // ← Пустой список! + documents: [], ), Project( id: 'p2', name: 'Project 2', - documents: [], // ← Пустой список! + documents: [], ), ]; @@ -87,7 +86,6 @@ class AppState extends ChangeNotifier { notifyListeners(); } - // ✅ Создаёт НОВЫЙ контроллер для каждого документа quill.QuillController getOrCreateController(AppDocument doc) { final controller = quill.QuillController( document: quill.Document.fromJson(doc.content.toJson()), @@ -101,37 +99,36 @@ class AppState extends ChangeNotifier { return controller; } - // ✅ Project создаётся БЕЗ документов - void addProject() { + // ✅ Создаёт проект с указанным именем + void addProject(String name) { final newId = 'p${_projects.length + 1}'; _projects.add(Project( id: newId, - name: 'Project ${_projects.length + 1}', - documents: [], // ← Пустой список! + name: name, + documents: [], )); notifyListeners(); } - // ✅ Добавляет документ в текущий проект - void addDocumentToCurrentProject() { + // ✅ Создаёт документ с указанным именем + void addDocumentToCurrentProject(String name) { if (_selectedProject == null) return; final newDoc = AppDocument( id: 'd${DateTime.now().millisecondsSinceEpoch}', - name: 'Document ${_selectedProject!.documents.length + 1}', + name: name, viewCount: 0, content: Delta()..insert('New document content...\n'), ); _selectedProject!.documents.add(newDoc); - _selectedDocument = newDoc; // ✅ Сразу открываем новый документ + _selectedDocument = newDoc; notifyListeners(); } void selectProject(Project project) { _selectedProject = project; - // ✅ Если есть документы - выбираем самый популярный if (project.documents.isNotEmpty) { final mostUsed = project.documents.reduce((a, b) => a.viewCount > b.viewCount ? a : b @@ -139,7 +136,6 @@ class AppState extends ChangeNotifier { _selectedDocument = mostUsed; incrementViewCount(mostUsed); } else { - // ✅ Если документов нет - очищаем selectedDocument _selectedDocument = null; } notifyListeners(); @@ -265,7 +261,7 @@ class ProjectsScreen extends StatelessWidget { return Scaffold( body: Column( children: [ - // Header с Logo и Manyllines + // Header Consumer( builder: (context, state, _) { final headerBg = state.isDarkMode ? Colors.grey[850] : Colors.white; @@ -487,12 +483,86 @@ class ProjectsScreen extends StatelessWidget { ], ), floatingActionButton: FloatingActionButton( - onPressed: () => context.read().addProject(), + onPressed: () => _showCreateProjectDialog(context), child: const Icon(Icons.add), ), ); } + // ✅ Диалог создания проекта + void _showCreateProjectDialog(BuildContext context) { + final controller = TextEditingController(); + final formKey = GlobalKey(); + + showDialog( + context: context, + builder: (context) => Consumer( + builder: (context, state, _) { + final isDarkMode = state.isDarkMode; + return AlertDialog( + backgroundColor: isDarkMode ? Colors.grey[850] : Colors.white, + title: Text( + 'Новый проект', + style: TextStyle(color: isDarkMode ? Colors.white : Colors.black87), + ), + content: Form( + key: formKey, + child: TextFormField( + controller: controller, + autofocus: true, + style: TextStyle(color: isDarkMode ? Colors.white : Colors.black87), + decoration: InputDecoration( + labelText: 'Название проекта', + labelStyle: TextStyle(color: isDarkMode ? Colors.grey[400] : Colors.grey[600]), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide(color: Colors.green[700]!), + ), + prefixIcon: Icon(Icons.folder, color: Colors.green[700]), + ), + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'Введите название проекта'; + } + return null; + }, + onFieldSubmitted: (_) { + if (formKey.currentState!.validate()) { + state.addProject(controller.text.trim()); + Navigator.pop(context); + } + }, + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text('Отмена', style: TextStyle(color: Colors.grey[600])), + ), + ElevatedButton( + onPressed: () { + if (formKey.currentState!.validate()) { + state.addProject(controller.text.trim()); + Navigator.pop(context); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green[700], + foregroundColor: Colors.white, + ), + child: const Text('Создать'), + ), + ], + ); + }, + ), + ); + } + + // ==================== ОПИСАНИЯ ДЛЯ НАСТРОЕК ==================== Widget _buildDescriptionSection2(bool isDarkMode) { return Container( color: isDarkMode ? Colors.grey[850] : Colors.grey[50], @@ -606,7 +676,7 @@ class ProjectsScreen extends StatelessWidget { onPressed: () {}, style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 12), - side: BorderSide(color: isDarkMode ? const Color.fromARGB(163, 162, 164, 170) : Colors.grey[400]!), + side: BorderSide(color: isDarkMode ? const Color.fromARGB(255, 54, 107, 232) : Colors.grey[400]!), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), foregroundColor: isDarkMode ? Colors.white : Colors.black87, ), @@ -616,7 +686,6 @@ class ProjectsScreen extends StatelessWidget { } } -// ==================== РАБОЧЕЕ ПРОСТРАНСТВО ==================== // ==================== РАБОЧЕЕ ПРОСТРАНСТВО ==================== class ProjectWorkspace extends StatelessWidget { const ProjectWorkspace({super.key}); @@ -628,7 +697,6 @@ class ProjectWorkspace extends StatelessWidget { final isWide = constraints.maxWidth >= 700; if (!isWide) { - // ✅ Используем watch для мобильных return Consumer( builder: (context, state, _) { return state.selectedDocument == null @@ -638,7 +706,6 @@ class ProjectWorkspace extends StatelessWidget { ); } - // ✅ Используем Selector для отслеживания selectedDocument return Selector( selector: (_, state) => state.selectedDocument, builder: (context, selectedDocument, _) { @@ -699,7 +766,7 @@ class ProjectWorkspace extends StatelessWidget { child: SizedBox( width: double.infinity, child: OutlinedButton.icon( - onPressed: () => state.addDocumentToCurrentProject(), + onPressed: () => _showCreateDocumentDialog(context), icon: const Icon(Icons.add, size: 18), label: const Text('Новый документ'), style: OutlinedButton.styleFrom( @@ -746,7 +813,7 @@ class ProjectWorkspace extends StatelessWidget { ), const SizedBox(height: 24), ElevatedButton.icon( - onPressed: () => state.addDocumentToCurrentProject(), + onPressed: () => _showCreateDocumentDialog(context), icon: const Icon(Icons.add), label: const Text('Создать первый документ'), style: ElevatedButton.styleFrom( @@ -760,12 +827,85 @@ class ProjectWorkspace extends StatelessWidget { ], ), floatingActionButton: FloatingActionButton( - onPressed: () => state.addDocumentToCurrentProject(), + onPressed: () => _showCreateDocumentDialog(context), tooltip: 'Создать документ', child: const Icon(Icons.add), ), ); } + + // ✅ Диалог создания документа + void _showCreateDocumentDialog(BuildContext context) { + final controller = TextEditingController(); + final formKey = GlobalKey(); + + showDialog( + context: context, + builder: (context) => Consumer( + builder: (context, state, _) { + final isDarkMode = state.isDarkMode; + return AlertDialog( + backgroundColor: isDarkMode ? Colors.grey[850] : Colors.white, + title: Text( + 'Новый документ', + style: TextStyle(color: isDarkMode ? Colors.white : Colors.black87), + ), + content: Form( + key: formKey, + child: TextFormField( + controller: controller, + autofocus: true, + style: TextStyle(color: isDarkMode ? Colors.white : Colors.black87), + decoration: InputDecoration( + labelText: 'Название документа', + labelStyle: TextStyle(color: isDarkMode ? Colors.grey[400] : Colors.grey[600]), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide(color: Colors.green[700]!), + ), + prefixIcon: Icon(Icons.description, color: Colors.green[700]), + ), + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'Введите название документа'; + } + return null; + }, + onFieldSubmitted: (_) { + if (formKey.currentState!.validate()) { + state.addDocumentToCurrentProject(controller.text.trim()); + Navigator.pop(context); + } + }, + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text('Отмена', style: TextStyle(color: Colors.grey[600])), + ), + ElevatedButton( + onPressed: () { + if (formKey.currentState!.validate()) { + state.addDocumentToCurrentProject(controller.text.trim()); + Navigator.pop(context); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green[700], + foregroundColor: Colors.white, + ), + child: const Text('Создать'), + ), + ], + ); + }, + ), + ); + } } // ==================== КОМПОНЕНТЫ ==================== @@ -917,18 +1057,85 @@ class _MobileDocList extends StatelessWidget { actions: [ IconButton( icon: const Icon(Icons.add), - onPressed: () => state.addDocumentToCurrentProject(), + onPressed: () => _showCreateDocumentDialog(context), tooltip: 'Новый документ', ), ], ), body: const _DocumentsList(), floatingActionButton: FloatingActionButton( - onPressed: () => state.addDocumentToCurrentProject(), + onPressed: () => _showCreateDocumentDialog(context), child: const Icon(Icons.add), ), ); } + + void _showCreateDocumentDialog(BuildContext context) { + final controller = TextEditingController(); + final formKey = GlobalKey(); + final state = context.read(); + final isDarkMode = state.isDarkMode; + + showDialog( + context: context, + builder: (context) => AlertDialog( + backgroundColor: isDarkMode ? Colors.grey[850] : Colors.white, + title: Text( + 'Новый документ', + style: TextStyle(color: isDarkMode ? Colors.white : Colors.black87), + ), + content: Form( + key: formKey, + child: TextFormField( + controller: controller, + autofocus: true, + style: TextStyle(color: isDarkMode ? Colors.white : Colors.black87), + decoration: InputDecoration( + labelText: 'Название документа', + labelStyle: TextStyle(color: isDarkMode ? Colors.grey[400] : Colors.grey[600]), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide(color: Colors.green[700]!), + ), + prefixIcon: Icon(Icons.description, color: Colors.green[700]), + ), + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'Введите название документа'; + } + return null; + }, + onFieldSubmitted: (_) { + if (formKey.currentState!.validate()) { + state.addDocumentToCurrentProject(controller.text.trim()); + Navigator.pop(context); + } + }, + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text('Отмена', style: TextStyle(color: Colors.grey[600])), + ), + ElevatedButton( + onPressed: () { + if (formKey.currentState!.validate()) { + state.addDocumentToCurrentProject(controller.text.trim()); + Navigator.pop(context); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green[700], + foregroundColor: Colors.white, + ), + child: const Text('Создать'), + ), + ], + ), + ); + } } class _MobileEditorView extends StatelessWidget { From 17dd4ce643fd28858e3a142a7bae7ab24ce0586f Mon Sep 17 00:00:00 2001 From: Ulquiorra-Keesre Date: Fri, 10 Apr 2026 14:24:13 +0300 Subject: [PATCH 13/22] pinned and subdocuments --- manylines_editor/lib/main.dart | 640 ++++++++++++++++----------------- 1 file changed, 306 insertions(+), 334 deletions(-) diff --git a/manylines_editor/lib/main.dart b/manylines_editor/lib/main.dart index 41f8158..12e6537 100644 --- a/manylines_editor/lib/main.dart +++ b/manylines_editor/lib/main.dart @@ -24,35 +24,39 @@ class Project { bool isDocumentMostUsed(AppDocument doc) { return doc.viewCount >= maxViewCount && maxViewCount > 0; } + + List get pinnedDocuments => + documents.where((doc) => doc.isPinned).toList(); + + List get unpinnedDocuments => + documents.where((doc) => !doc.isPinned).toList(); } class AppDocument { final String id; final String name; int viewCount; + bool isPinned; + String? parentId; Delta content; AppDocument({ required this.id, required this.name, this.viewCount = 0, + this.isPinned = false, + this.parentId, required this.content, }); + + bool get isChild => parentId != null; } // ==================== STATE ==================== class AppState extends ChangeNotifier { final List _projects = [ - Project( - id: 'p1', - name: 'Project 1', - documents: [], - ), - Project( - id: 'p2', - name: 'Project 2', - documents: [], - ), + Project(id: 'p1', name: 'Project 1', documents: []), + Project(id: 'p2', name: 'Project 2', documents: []), ]; List> _settings = [ @@ -66,7 +70,6 @@ class AppState extends ChangeNotifier { AppDocument? _selectedDocument; bool _isDarkMode = false; - // ==================== GETTERS ==================== List get projects => _projects; List> get settings => _settings; bool get switchableValue => _switchableValue; @@ -74,13 +77,11 @@ class AppState extends ChangeNotifier { AppDocument? get selectedDocument => _selectedDocument; bool get isDarkMode => _isDarkMode; - // ==================== METHODS ==================== - void toggleDarkMode(bool value) { _isDarkMode = value; notifyListeners(); } - + void incrementViewCount(AppDocument doc) { doc.viewCount++; notifyListeners(); @@ -91,48 +92,88 @@ class AppState extends ChangeNotifier { document: quill.Document.fromJson(doc.content.toJson()), selection: const TextSelection.collapsed(offset: 0), ); - controller.changes.listen((change) { doc.content = controller.document.toDelta(); }); - return controller; } - // ✅ Создаёт проект с указанным именем void addProject(String name) { - final newId = 'p${_projects.length + 1}'; _projects.add(Project( - id: newId, + id: 'p${_projects.length + 1}', name: name, documents: [], )); notifyListeners(); } - // ✅ Создаёт документ с указанным именем void addDocumentToCurrentProject(String name) { if (_selectedProject == null) return; - final newDoc = AppDocument( id: 'd${DateTime.now().millisecondsSinceEpoch}', name: name, viewCount: 0, + isPinned: false, + parentId: null, content: Delta()..insert('New document content...\n'), ); - _selectedProject!.documents.add(newDoc); _selectedDocument = newDoc; notifyListeners(); } + void toggleDocumentPin(AppDocument doc) { + doc.isPinned = !doc.isPinned; + notifyListeners(); + } + + void reorderPinnedDocuments(int oldIndex, int newIndex) { + if (_selectedProject == null) return; + final pinnedDocs = _selectedProject!.pinnedDocuments; + if (newIndex > oldIndex) newIndex -= 1; + + final doc = pinnedDocs.removeAt(oldIndex); + final targetDoc = pinnedDocs[newIndex]; + + final docMainIndex = _selectedProject!.documents.indexOf(doc); + final targetMainIndex = _selectedProject!.documents.indexOf(targetDoc); + + _selectedProject!.documents.removeAt(docMainIndex); + _selectedProject!.documents.insert(targetMainIndex, doc); + + notifyListeners(); + } + + // ✅ Исправлено: теперь ищем родителя среди документов ВЫШЕ текущего + void indentDocument(int index) { + if (_selectedProject == null || index <= 0) return; + final docs = _selectedProject!.documents; + + // Ищем последний документ с parentId == null и !isPinned выше текущего + AppDocument? parentDoc; + for (int i = index - 1; i >= 0; i--) { + if (docs[i].parentId == null && !docs[i].isPinned) { + parentDoc = docs[i]; + break; + } + } + + if (parentDoc != null) { + docs[index].parentId = parentDoc.id; + notifyListeners(); + } + } + + void outdentDocument(int index) { + if (_selectedProject == null) return; + _selectedProject!.documents[index].parentId = null; + notifyListeners(); + } + void selectProject(Project project) { _selectedProject = project; - if (project.documents.isNotEmpty) { - final mostUsed = project.documents.reduce((a, b) => - a.viewCount > b.viewCount ? a : b - ); + final mostUsed = project.documents.reduce((a, b) => a.viewCount > b.viewCount ? a : b); _selectedDocument = mostUsed; incrementViewCount(mostUsed); } else { @@ -215,7 +256,7 @@ void main() { ], supportedLocales: const [Locale('ru', 'RU'), Locale('en', 'US')], locale: const Locale('ru', 'RU'), - theme: state.isDarkMode + theme: state.isDarkMode ? ThemeData( useMaterial3: true, colorSchemeSeed: Colors.green, @@ -238,15 +279,12 @@ void main() { class AppShell extends StatelessWidget { const AppShell({super.key}); - @override Widget build(BuildContext context) { return Selector( selector: (_, state) => state.selectedProject, builder: (context, selectedProject, _) { - return selectedProject == null - ? const ProjectsScreen() - : const ProjectWorkspace(); + return selectedProject == null ? const ProjectsScreen() : const ProjectWorkspace(); }, ); } @@ -255,13 +293,11 @@ class AppShell extends StatelessWidget { // ==================== ЭКРАН ПРОЕКТОВ ==================== class ProjectsScreen extends StatelessWidget { const ProjectsScreen({super.key}); - @override Widget build(BuildContext context) { return Scaffold( body: Column( children: [ - // Header Consumer( builder: (context, state, _) { final headerBg = state.isDarkMode ? Colors.grey[850] : Colors.white; @@ -270,13 +306,9 @@ class ProjectsScreen extends StatelessWidget { final logoBorderColor = state.isDarkMode ? Colors.grey[600]! : Colors.grey[400]!; final textColor = state.isDarkMode ? Colors.white : Colors.black87; final logoTextColor = state.isDarkMode ? Colors.white : Colors.black; - return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - decoration: BoxDecoration( - border: Border(bottom: BorderSide(color: borderColor)), - color: headerBg, - ), + decoration: BoxDecoration(border: Border(bottom: BorderSide(color: borderColor)), color: headerBg), child: Row( children: [ Container( @@ -286,33 +318,20 @@ class ProjectsScreen extends StatelessWidget { color: logoBg, borderRadius: BorderRadius.circular(4), ), - child: Text( - 'Logo', - style: TextStyle(fontWeight: FontWeight.bold, color: logoTextColor), - ), + child: Text('Logo', style: TextStyle(fontWeight: FontWeight.bold, color: logoTextColor)), ), const SizedBox(width: 12), - Expanded( - child: Text( - 'Manyllines', - style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: textColor), - ), - ), + Expanded(child: Text('Manyllines', style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: textColor))), ], ), ); }, ), - - // Список проектов Consumer( builder: (context, state, _) { final bgColor = state.isDarkMode ? Colors.green[900] : Colors.green[50]; - final borderColor = state.isDarkMode - ? const Color.fromARGB(255, 0, 47, 22) - : Colors.green.shade200; + final borderColor = state.isDarkMode ? const Color.fromARGB(255, 0, 47, 22) : Colors.green.shade200; final textColor = state.isDarkMode ? Colors.white : Colors.black87; - if (state.switchableValue) { return ReorderableListView.builder( shrinkWrap: true, @@ -323,18 +342,11 @@ class ProjectsScreen extends StatelessWidget { final project = state.projects[index]; return Container( key: ValueKey(project.id), - decoration: BoxDecoration( - color: bgColor, - border: Border(bottom: BorderSide(color: borderColor)), - ), + decoration: BoxDecoration(color: bgColor, border: Border(bottom: BorderSide(color: borderColor))), child: ListTile( title: Text(project.name, style: TextStyle(color: textColor)), - subtitle: Text( - '${project.documents.length} документов', - style: TextStyle(fontSize: 12, color: textColor.withOpacity(0.7)), - ), - trailing: Icon(Icons.drag_handle, - color: state.isDarkMode ? Colors.white54 : Colors.grey), + subtitle: Text('${project.documents.length} документов', style: TextStyle(fontSize: 12, color: textColor.withOpacity(0.7))), + trailing: Icon(Icons.drag_handle, color: state.isDarkMode ? Colors.white54 : Colors.grey), onTap: () => state.selectProject(project), ), ); @@ -342,10 +354,7 @@ class ProjectsScreen extends StatelessWidget { ); } else { return Container( - decoration: BoxDecoration( - color: bgColor, - border: Border(bottom: BorderSide(color: borderColor)), - ), + decoration: BoxDecoration(color: bgColor, border: Border(bottom: BorderSide(color: borderColor))), child: ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), @@ -354,15 +363,10 @@ class ProjectsScreen extends StatelessWidget { final project = state.projects[index]; return Container( key: ValueKey(project.id), - decoration: BoxDecoration( - border: Border(bottom: BorderSide(color: borderColor)), - ), + decoration: BoxDecoration(border: Border(bottom: BorderSide(color: borderColor))), child: ListTile( title: Text(project.name, style: TextStyle(color: textColor)), - subtitle: Text( - '${project.documents.length} документов', - style: TextStyle(fontSize: 12, color: textColor.withOpacity(0.7)), - ), + subtitle: Text('${project.documents.length} документов', style: TextStyle(fontSize: 12, color: textColor.withOpacity(0.7))), onTap: () => state.selectProject(project), ), ); @@ -372,15 +376,12 @@ class ProjectsScreen extends StatelessWidget { } }, ), - - // Настройки Consumer( builder: (context, state, _) { final bgColor = state.isDarkMode ? Colors.blue[900] : Colors.blue[50]; final borderColor = state.isDarkMode ? Colors.blue[700] : Colors.blue[200]; final textColor = state.isDarkMode ? Colors.white : Colors.black87; final settings = state.settings; - if (state.switchableValue) { return ReorderableListView.builder( shrinkWrap: true, @@ -395,10 +396,7 @@ class ProjectsScreen extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ Container( - decoration: BoxDecoration( - border: Border(bottom: BorderSide(color: borderColor!)), - color: state.isDarkMode ? Colors.blue[800] : Colors.white, - ), + decoration: BoxDecoration(border: Border(bottom: BorderSide(color: borderColor!)), color: state.isDarkMode ? Colors.blue[800] : Colors.white), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -406,12 +404,8 @@ class ProjectsScreen extends StatelessWidget { Text(setting['name'], style: TextStyle(color: textColor)), Row( children: [ - IconButton( - icon: Icon(isExpanded ? Icons.arrow_drop_down : Icons.arrow_drop_up, color: textColor), - onPressed: () => state.toggleSettingExpansion(setting['id']), - ), - if (state.switchableValue) - Icon(Icons.drag_handle, color: state.isDarkMode ? Colors.white54 : Colors.grey), + IconButton(icon: Icon(isExpanded ? Icons.arrow_drop_down : Icons.arrow_drop_up, color: textColor), onPressed: () => state.toggleSettingExpansion(setting['id'])), + if (state.switchableValue) Icon(Icons.drag_handle, color: state.isDarkMode ? Colors.white54 : Colors.grey), ], ), ], @@ -437,19 +431,13 @@ class ProjectsScreen extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ Container( - decoration: BoxDecoration( - border: Border(bottom: BorderSide(color: borderColor!)), - color: state.isDarkMode ? Colors.blue[800] : Colors.white, - ), + decoration: BoxDecoration(border: Border(bottom: BorderSide(color: borderColor!)), color: state.isDarkMode ? Colors.blue[800] : Colors.white), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(setting['name'], style: TextStyle(color: textColor)), - IconButton( - icon: Icon(isExpanded ? Icons.arrow_drop_down : Icons.arrow_drop_up, color: textColor), - onPressed: () => state.toggleSettingExpansion(setting['id']), - ), + IconButton(icon: Icon(isExpanded ? Icons.arrow_drop_down : Icons.arrow_drop_up, color: textColor), onPressed: () => state.toggleSettingExpansion(setting['id'])), ], ), ), @@ -463,37 +451,24 @@ class ProjectsScreen extends StatelessWidget { } }, ), - - // Additional settings Consumer( builder: (context, state, _) { final bgColor = state.isDarkMode ? const Color.fromARGB(255, 6, 58, 137) : Colors.blue[50]; final textColor = state.isDarkMode ? Colors.white54 : Colors.black54; return Expanded( - child: Container( - color: bgColor, - padding: const EdgeInsets.all(16), - child: Center( - child: Text('Other Settings ...', style: TextStyle(color: textColor)), - ), - ), + child: Container(color: bgColor, padding: const EdgeInsets.all(16), child: Center(child: Text('Other Settings ...', style: TextStyle(color: textColor)))), ); }, ), ], ), - floatingActionButton: FloatingActionButton( - onPressed: () => _showCreateProjectDialog(context), - child: const Icon(Icons.add), - ), + floatingActionButton: FloatingActionButton(onPressed: () => _showCreateProjectDialog(context), child: const Icon(Icons.add)), ); } - // ✅ Диалог создания проекта void _showCreateProjectDialog(BuildContext context) { final controller = TextEditingController(); final formKey = GlobalKey(); - showDialog( context: context, builder: (context) => Consumer( @@ -501,10 +476,7 @@ class ProjectsScreen extends StatelessWidget { final isDarkMode = state.isDarkMode; return AlertDialog( backgroundColor: isDarkMode ? Colors.grey[850] : Colors.white, - title: Text( - 'Новый проект', - style: TextStyle(color: isDarkMode ? Colors.white : Colors.black87), - ), + title: Text('Новый проект', style: TextStyle(color: isDarkMode ? Colors.white : Colors.black87)), content: Form( key: formKey, child: TextFormField( @@ -514,19 +486,12 @@ class ProjectsScreen extends StatelessWidget { decoration: InputDecoration( labelText: 'Название проекта', labelStyle: TextStyle(color: isDarkMode ? Colors.grey[400] : Colors.grey[600]), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide(color: Colors.green[700]!), - ), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), + focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: BorderSide(color: Colors.green[700]!)), prefixIcon: Icon(Icons.folder, color: Colors.green[700]), ), validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'Введите название проекта'; - } + if (value == null || value.trim().isEmpty) return 'Введите название проекта'; return null; }, onFieldSubmitted: (_) { @@ -538,10 +503,7 @@ class ProjectsScreen extends StatelessWidget { ), ), actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text('Отмена', style: TextStyle(color: Colors.grey[600])), - ), + TextButton(onPressed: () => Navigator.pop(context), child: Text('Отмена', style: TextStyle(color: Colors.grey[600]))), ElevatedButton( onPressed: () { if (formKey.currentState!.validate()) { @@ -549,10 +511,7 @@ class ProjectsScreen extends StatelessWidget { Navigator.pop(context); } }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green[700], - foregroundColor: Colors.white, - ), + style: ElevatedButton.styleFrom(backgroundColor: Colors.green[700], foregroundColor: Colors.white), child: const Text('Создать'), ), ], @@ -562,7 +521,6 @@ class ProjectsScreen extends StatelessWidget { ); } - // ==================== ОПИСАНИЯ ДЛЯ НАСТРОЕК ==================== Widget _buildDescriptionSection2(bool isDarkMode) { return Container( color: isDarkMode ? Colors.grey[850] : Colors.grey[50], @@ -572,15 +530,7 @@ class ProjectsScreen extends StatelessWidget { children: [ Text('Description', style: TextStyle(fontSize: 14, color: isDarkMode ? Colors.grey[300] : Colors.grey[700])), const SizedBox(height: 12), - Row( - children: [ - _buildOutlinedButton('A', isDarkMode), - const SizedBox(width: 8), - _buildOutlinedButton('B', isDarkMode), - const SizedBox(width: 8), - _buildOutlinedButton('C', isDarkMode), - ], - ), + Row(children: [_buildOutlinedButton('A', isDarkMode), const SizedBox(width: 8), _buildOutlinedButton('B', isDarkMode), const SizedBox(width: 8), _buildOutlinedButton('C', isDarkMode)]), const SizedBox(height: 16), Consumer( builder: (context, state, _) { @@ -588,13 +538,7 @@ class ProjectsScreen extends StatelessWidget { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - children: [ - Icon(isDark ? Icons.dark_mode : Icons.light_mode, size: 20, color: isDark ? Colors.yellow[200] : Colors.orange), - const SizedBox(width: 8), - Text(isDark ? 'Тёмная тема' : 'Светлая тема', style: TextStyle(color: isDarkMode ? Colors.white : Colors.black87)), - ], - ), + Row(children: [Icon(isDark ? Icons.dark_mode : Icons.light_mode, size: 20, color: isDark ? Colors.yellow[200] : Colors.orange), const SizedBox(width: 8), Text(isDark ? 'Тёмная тема' : 'Светлая тема', style: TextStyle(color: isDarkMode ? Colors.white : Colors.black87))]), Switch(value: isDark, onChanged: (value) => state.toggleDarkMode(value)), ], ); @@ -614,15 +558,7 @@ class ProjectsScreen extends StatelessWidget { children: [ Text('Description', style: TextStyle(fontSize: 14, color: isDarkMode ? Colors.grey[300] : Colors.grey[700])), const SizedBox(height: 12), - Row( - children: [ - _buildOutlinedButton('A', isDarkMode), - const SizedBox(width: 8), - _buildOutlinedButton('B', isDarkMode), - const SizedBox(width: 8), - _buildOutlinedButton('C', isDarkMode), - ], - ), + Row(children: [_buildOutlinedButton('A', isDarkMode), const SizedBox(width: 8), _buildOutlinedButton('B', isDarkMode), const SizedBox(width: 8), _buildOutlinedButton('C', isDarkMode)]), const SizedBox(height: 16), Consumer( builder: (context, state, _) { @@ -635,13 +571,7 @@ class ProjectsScreen extends StatelessWidget { ); }, ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('Listable', style: TextStyle(color: isDarkMode ? Colors.white : Colors.black87)), - IconButton(icon: Icon(Icons.arrow_drop_down, color: isDarkMode ? Colors.white : Colors.black87), onPressed: () {}), - ], - ), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text('Listable', style: TextStyle(color: isDarkMode ? Colors.white : Colors.black87)), IconButton(icon: Icon(Icons.arrow_drop_down, color: isDarkMode ? Colors.white : Colors.black87), onPressed: () {})]), ], ), ); @@ -651,22 +581,11 @@ class ProjectsScreen extends StatelessWidget { return Container( color: isDarkMode ? Colors.grey[850] : Colors.grey[50], padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text('Description', style: TextStyle(fontSize: 14, color: isDarkMode ? Colors.grey[300] : Colors.grey[700])), - const SizedBox(height: 12), - Row( - children: [ - _buildOutlinedButton('A', isDarkMode), - const SizedBox(width: 8), - _buildOutlinedButton('B', isDarkMode), - const SizedBox(width: 8), - _buildOutlinedButton('C', isDarkMode), - ], - ), - ], - ), + child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + Text('Description', style: TextStyle(fontSize: 14, color: isDarkMode ? Colors.grey[300] : Colors.grey[700])), + const SizedBox(height: 12), + Row(children: [_buildOutlinedButton('A', isDarkMode), const SizedBox(width: 8), _buildOutlinedButton('B', isDarkMode), const SizedBox(width: 8), _buildOutlinedButton('C', isDarkMode)]), + ]), ); } @@ -689,23 +608,18 @@ class ProjectsScreen extends StatelessWidget { // ==================== РАБОЧЕЕ ПРОСТРАНСТВО ==================== class ProjectWorkspace extends StatelessWidget { const ProjectWorkspace({super.key}); - @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { final isWide = constraints.maxWidth >= 700; - if (!isWide) { return Consumer( builder: (context, state, _) { - return state.selectedDocument == null - ? const _MobileDocList() - : _MobileEditorView(document: state.selectedDocument!); + return state.selectedDocument == null ? const _MobileDocList() : _MobileEditorView(document: state.selectedDocument!); }, ); } - return Selector( selector: (_, state) => state.selectedDocument, builder: (context, selectedDocument, _) { @@ -722,15 +636,12 @@ class ProjectWorkspace extends StatelessWidget { final headerBg = state.isDarkMode ? Colors.green[900] : Colors.green[50]; final textColor = state.isDarkMode ? Colors.white : Colors.black87; final borderColor = state.isDarkMode ? Colors.grey[700]! : Colors.grey[300]!; - return Scaffold( body: Row( children: [ Container( width: 300, - decoration: BoxDecoration( - border: Border(right: BorderSide(color: borderColor)), - ), + decoration: BoxDecoration(border: Border(right: BorderSide(color: borderColor))), child: Column( children: [ Container( @@ -738,17 +649,8 @@ class ProjectWorkspace extends StatelessWidget { color: headerBg, child: Row( children: [ - IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () => state.clearSelectedProject(), - tooltip: 'Back to projects', - ), - Expanded( - child: Text( - state.selectedProject!.name, - style: Theme.of(context).textTheme.titleMedium?.copyWith(color: textColor), - ), - ), + IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => state.clearSelectedProject(), tooltip: 'Back to projects'), + Expanded(child: Text(state.selectedProject!.name, style: Theme.of(context).textTheme.titleMedium?.copyWith(color: textColor))), ], ), ), @@ -757,12 +659,10 @@ class ProjectWorkspace extends StatelessWidget { color: leftPanelBg, child: Column( children: [ - const Expanded(child: _DocumentsList()), + Expanded(child: _DocumentsList()), Container( padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - border: Border(top: BorderSide(color: borderColor)), - ), + decoration: BoxDecoration(border: Border(top: BorderSide(color: borderColor))), child: SizedBox( width: double.infinity, child: OutlinedButton.icon( @@ -791,34 +691,17 @@ class ProjectWorkspace extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - Icons.description_outlined, - size: 64, - color: state.isDarkMode ? Colors.white30 : Colors.black26, - ), + Icon(Icons.description_outlined, size: 64, color: state.isDarkMode ? Colors.white30 : Colors.black26), const SizedBox(height: 16), - Text( - 'В проекте нет документов', - style: TextStyle( - fontSize: 18, - color: state.isDarkMode ? Colors.white70 : Colors.black54, - ), - ), + Text('В проекте нет документов', style: TextStyle(fontSize: 18, color: state.isDarkMode ? Colors.white70 : Colors.black54)), const SizedBox(height: 8), - Text( - 'Нажмите кнопку "+ Новый документ" чтобы создать', - style: TextStyle( - color: state.isDarkMode ? Colors.white54 : Colors.black45, - ), - ), + Text('Нажмите кнопку "+ Новый документ" чтобы создать', style: TextStyle(color: state.isDarkMode ? Colors.white54 : Colors.black45)), const SizedBox(height: 24), ElevatedButton.icon( onPressed: () => _showCreateDocumentDialog(context), icon: const Icon(Icons.add), label: const Text('Создать первый документ'), - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), - ), + style: ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12)), ), ], ), @@ -826,19 +709,13 @@ class ProjectWorkspace extends StatelessWidget { ), ], ), - floatingActionButton: FloatingActionButton( - onPressed: () => _showCreateDocumentDialog(context), - tooltip: 'Создать документ', - child: const Icon(Icons.add), - ), + floatingActionButton: FloatingActionButton(onPressed: () => _showCreateDocumentDialog(context), tooltip: 'Создать документ', child: const Icon(Icons.add)), ); } - // ✅ Диалог создания документа void _showCreateDocumentDialog(BuildContext context) { final controller = TextEditingController(); final formKey = GlobalKey(); - showDialog( context: context, builder: (context) => Consumer( @@ -846,10 +723,7 @@ class ProjectWorkspace extends StatelessWidget { final isDarkMode = state.isDarkMode; return AlertDialog( backgroundColor: isDarkMode ? Colors.grey[850] : Colors.white, - title: Text( - 'Новый документ', - style: TextStyle(color: isDarkMode ? Colors.white : Colors.black87), - ), + title: Text('Новый документ', style: TextStyle(color: isDarkMode ? Colors.white : Colors.black87)), content: Form( key: formKey, child: TextFormField( @@ -859,19 +733,12 @@ class ProjectWorkspace extends StatelessWidget { decoration: InputDecoration( labelText: 'Название документа', labelStyle: TextStyle(color: isDarkMode ? Colors.grey[400] : Colors.grey[600]), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide(color: Colors.green[700]!), - ), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), + focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: BorderSide(color: Colors.green[700]!)), prefixIcon: Icon(Icons.description, color: Colors.green[700]), ), validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'Введите название документа'; - } + if (value == null || value.trim().isEmpty) return 'Введите название документа'; return null; }, onFieldSubmitted: (_) { @@ -883,10 +750,7 @@ class ProjectWorkspace extends StatelessWidget { ), ), actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text('Отмена', style: TextStyle(color: Colors.grey[600])), - ), + TextButton(onPressed: () => Navigator.pop(context), child: Text('Отмена', style: TextStyle(color: Colors.grey[600]))), ElevatedButton( onPressed: () { if (formKey.currentState!.validate()) { @@ -894,10 +758,7 @@ class ProjectWorkspace extends StatelessWidget { Navigator.pop(context); } }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green[700], - foregroundColor: Colors.white, - ), + style: ElevatedButton.styleFrom(backgroundColor: Colors.green[700], foregroundColor: Colors.white), child: const Text('Создать'), ), ], @@ -911,55 +772,200 @@ class ProjectWorkspace extends StatelessWidget { // ==================== КОМПОНЕНТЫ ==================== class _DocumentsList extends StatelessWidget { const _DocumentsList(); - + @override Widget build(BuildContext context) { final state = context.watch(); final project = state.selectedProject!; - final docs = List.from(project.documents) - ..sort((a, b) => b.viewCount.compareTo(a.viewCount)); + final pinnedDocs = project.pinnedDocuments; + final unpinnedDocs = project.unpinnedDocuments; + final isDarkMode = state.isDarkMode; - if (docs.isEmpty) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.folder_open_outlined, - size: 48, - color: Colors.grey[400], - ), - const SizedBox(height: 8), - Text( - 'Нет документов', - style: TextStyle(color: Colors.grey[600]), + return Column( + children: [ + // ✅ Закреплённые документы (зелёный фон, перетаскивание, чекбоксы) + if (pinnedDocs.isNotEmpty) + Container( + color: isDarkMode ? Colors.green[900]!.withOpacity(0.3) : Colors.green[50], + child: ReorderableListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: pinnedDocs.length, + onReorder: state.reorderPinnedDocuments, + itemBuilder: (context, index) { + final doc = pinnedDocs[index]; + final isSelected = state.selectedDocument?.id == doc.id; + + return Container( + key: ValueKey(doc.id), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: isDarkMode ? Colors.green[700]! : Colors.green[200]!, + ), + ), + ), + child: ListTile( + selected: isSelected, + selectedTileColor: Theme.of(context).colorScheme.secondaryContainer, + leading: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '${index + 1}.', + style: TextStyle( + fontSize: 12, + color: Colors.green[700], + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(width: 8), + Icon( + Icons.push_pin, + size: 16, + color: Colors.green[700], + ), + ], + ), + title: Text(doc.name), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Checkbox( + value: doc.isPinned, + activeColor: Colors.green[700], + onChanged: (value) { + state.toggleDocumentPin(doc); + }, + ), + const SizedBox(width: 4), + Icon( + Icons.drag_handle, + color: isDarkMode ? Colors.white54 : Colors.grey, + ), + ], + ), + onTap: () => state.selectDocument(doc), + ), + ); + }, ), - ], + ), + + // ✅ Обычные документы (синий фон, скроллинг, свайп для поддокументов, чекбоксы) + Expanded( + child: Container( + color: isDarkMode ? Colors.blue[900]!.withOpacity(0.2) : Colors.blue[50], + child: _buildDismissibleList(project, unpinnedDocs, state, isDarkMode, context), + ), ), - ); - } + ], + ); + } + + // ✅ Исправлено: передаём project для получения реального индекса + Widget _buildDismissibleList(Project project, List docs, AppState state, bool isDarkMode, BuildContext context) { + int mainIndex = 0; + int childIndex = 0; return ListView.builder( itemCount: docs.length, itemBuilder: (context, index) { final doc = docs[index]; final isSelected = state.selectedDocument?.id == doc.id; - final isMostUsed = project.isDocumentMostUsed(doc); - return ListTile( - selected: isSelected, - selectedTileColor: Theme.of(context).colorScheme.secondaryContainer, - leading: Icon( - isMostUsed ? Icons.star : Icons.insert_drive_file, - color: isSelected - ? Theme.of(context).colorScheme.onSecondaryContainer - : Colors.grey[600], + // ✅ Находим РЕАЛЬНЫЙ индекс в project.documents + final actualIndex = project.documents.indexOf(doc); + + String number; + if (doc.parentId == null) { + mainIndex++; + childIndex = 0; + number = '$mainIndex.'; + } else { + childIndex++; + number = '$mainIndex.$childIndex'; + } + + return Dismissible( + key: ValueKey(doc.id), + direction: DismissDirection.horizontal, + background: Container( + color: Colors.blue, + alignment: Alignment.centerLeft, + padding: const EdgeInsets.only(left: 16), + child: const Icon(Icons.arrow_back, color: Colors.white), + ), + secondaryBackground: Container( + color: Colors.green, + alignment: Alignment.centerRight, + padding: const EdgeInsets.only(right: 16), + child: const Icon(Icons.arrow_forward, color: Colors.white), + ), + confirmDismiss: (direction) async { + // ✅ Используем actualIndex вместо index + if (direction == DismissDirection.startToEnd) { + state.indentDocument(actualIndex); + } else if (direction == DismissDirection.endToStart) { + state.outdentDocument(actualIndex); + } + return false; + }, + child: Container( + decoration: BoxDecoration( + color: doc.parentId == null + ? (isDarkMode ? Colors.blue[900]!.withOpacity(0.2) : Colors.blue[50]) + : (isDarkMode ? Colors.green[900]!.withOpacity(0.3) : Colors.green[50]), + border: Border( + bottom: BorderSide( + color: isDarkMode + ? (doc.parentId == null ? Colors.blue[700]! : Colors.green[700]!) + : (doc.parentId == null ? Colors.blue[200]! : Colors.green[200]!), + ), + ), + ), + child: ListTile( + selected: isSelected, + selectedTileColor: Theme.of(context).colorScheme.secondaryContainer, + leading: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + number, + style: TextStyle( + fontSize: 12, + color: doc.parentId == null ? Colors.blue[700] : Colors.green[700], + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(width: 8), + Icon( + doc.parentId == null ? Icons.insert_drive_file : Icons.subdirectory_arrow_right, + size: 16, + color: doc.parentId == null ? Colors.blue[700] : Colors.green[700], + ), + ], + ), + title: Text(doc.name), + subtitle: doc.parentId != null ? Text('Поддокумент', style: TextStyle(fontSize: 10, color: Colors.grey[600])) : null, + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Checkbox( + value: doc.isPinned, + activeColor: Colors.green[700], + onChanged: (value) { + state.toggleDocumentPin(doc); + }, + ), + const SizedBox(width: 4), + if (doc.parentId != null) + Icon(Icons.swipe, size: 16, color: Colors.grey[500]), + ], + ), + onTap: () => state.selectDocument(doc), + ), ), - title: Text(doc.name), - subtitle: isSelected - ? null - : Text('Views: ${doc.viewCount}', style: TextStyle(fontSize: 12, color: Colors.grey[600])), - onTap: () => state.selectDocument(doc), ); }, ); @@ -969,14 +975,12 @@ class _DocumentsList extends StatelessWidget { class QuillEditorView extends StatefulWidget { final AppDocument document; const QuillEditorView({super.key, required this.document}); - @override State createState() => _QuillEditorViewState(); } class _QuillEditorViewState extends State { quill.QuillController? _controller; - @override void initState() { super.initState(); @@ -1005,10 +1009,7 @@ class _QuillEditorViewState extends State { @override Widget build(BuildContext context) { - if (_controller == null) { - return const Center(child: CircularProgressIndicator()); - } - + if (_controller == null) return const Center(child: CircularProgressIndicator()); return Column( children: [ quill.QuillSimpleToolbar( @@ -1046,7 +1047,6 @@ class _QuillEditorViewState extends State { class _MobileDocList extends StatelessWidget { const _MobileDocList(); - @override Widget build(BuildContext context) { final state = context.read(); @@ -1054,19 +1054,10 @@ class _MobileDocList extends StatelessWidget { appBar: AppBar( title: Text(state.selectedProject!.name), leading: IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => state.clearSelectedProject()), - actions: [ - IconButton( - icon: const Icon(Icons.add), - onPressed: () => _showCreateDocumentDialog(context), - tooltip: 'Новый документ', - ), - ], + actions: [IconButton(icon: const Icon(Icons.add), onPressed: () => _showCreateDocumentDialog(context), tooltip: 'Новый документ')], ), body: const _DocumentsList(), - floatingActionButton: FloatingActionButton( - onPressed: () => _showCreateDocumentDialog(context), - child: const Icon(Icons.add), - ), + floatingActionButton: FloatingActionButton(onPressed: () => _showCreateDocumentDialog(context), child: const Icon(Icons.add)), ); } @@ -1075,15 +1066,11 @@ class _MobileDocList extends StatelessWidget { final formKey = GlobalKey(); final state = context.read(); final isDarkMode = state.isDarkMode; - showDialog( context: context, builder: (context) => AlertDialog( backgroundColor: isDarkMode ? Colors.grey[850] : Colors.white, - title: Text( - 'Новый документ', - style: TextStyle(color: isDarkMode ? Colors.white : Colors.black87), - ), + title: Text('Новый документ', style: TextStyle(color: isDarkMode ? Colors.white : Colors.black87)), content: Form( key: formKey, child: TextFormField( @@ -1094,16 +1081,11 @@ class _MobileDocList extends StatelessWidget { labelText: 'Название документа', labelStyle: TextStyle(color: isDarkMode ? Colors.grey[400] : Colors.grey[600]), border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide(color: Colors.green[700]!), - ), + focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: BorderSide(color: Colors.green[700]!)), prefixIcon: Icon(Icons.description, color: Colors.green[700]), ), validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'Введите название документа'; - } + if (value == null || value.trim().isEmpty) return 'Введите название документа'; return null; }, onFieldSubmitted: (_) { @@ -1115,10 +1097,7 @@ class _MobileDocList extends StatelessWidget { ), ), actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text('Отмена', style: TextStyle(color: Colors.grey[600])), - ), + TextButton(onPressed: () => Navigator.pop(context), child: Text('Отмена', style: TextStyle(color: Colors.grey[600]))), ElevatedButton( onPressed: () { if (formKey.currentState!.validate()) { @@ -1126,10 +1105,7 @@ class _MobileDocList extends StatelessWidget { Navigator.pop(context); } }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green[700], - foregroundColor: Colors.white, - ), + style: ElevatedButton.styleFrom(backgroundColor: Colors.green[700], foregroundColor: Colors.white), child: const Text('Создать'), ), ], @@ -1141,14 +1117,10 @@ class _MobileDocList extends StatelessWidget { class _MobileEditorView extends StatelessWidget { final AppDocument document; const _MobileEditorView({required this.document}); - @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: Text(document.name), - leading: IconButton(icon: const Icon(Icons.arrow_back), onPressed: () {}), - ), + appBar: AppBar(title: Text(document.name), leading: IconButton(icon: const Icon(Icons.arrow_back), onPressed: () {})), body: QuillEditorView(document: document), ); } From 6fc932a172e21150c5e9043041c6f4af4f08b29e Mon Sep 17 00:00:00 2001 From: Ulquiorra-Keesre Date: Fri, 10 Apr 2026 17:55:54 +0300 Subject: [PATCH 14/22] Graph view --- manylines_editor/lib/main.dart | 222 ++++++++++++++++++++++++++++++--- 1 file changed, 205 insertions(+), 17 deletions(-) diff --git a/manylines_editor/lib/main.dart b/manylines_editor/lib/main.dart index 12e6537..d7a19cc 100644 --- a/manylines_editor/lib/main.dart +++ b/manylines_editor/lib/main.dart @@ -69,6 +69,7 @@ class AppState extends ChangeNotifier { Project? _selectedProject; AppDocument? _selectedDocument; bool _isDarkMode = false; + bool _isGraphView = false; // ✅ Режим просмотра (список/граф) List get projects => _projects; List> get settings => _settings; @@ -76,12 +77,19 @@ class AppState extends ChangeNotifier { Project? get selectedProject => _selectedProject; AppDocument? get selectedDocument => _selectedDocument; bool get isDarkMode => _isDarkMode; + bool get isGraphView => _isGraphView; // ✅ Геттер для режима просмотра void toggleDarkMode(bool value) { _isDarkMode = value; notifyListeners(); } + // ✅ Переключение между списком и графом + void toggleViewMode() { + _isGraphView = !_isGraphView; + notifyListeners(); + } + void incrementViewCount(AppDocument doc) { doc.viewCount++; notifyListeners(); @@ -144,12 +152,10 @@ class AppState extends ChangeNotifier { notifyListeners(); } - // ✅ Исправлено: теперь ищем родителя среди документов ВЫШЕ текущего void indentDocument(int index) { if (_selectedProject == null || index <= 0) return; final docs = _selectedProject!.documents; - // Ищем последний документ с parentId == null и !isPinned выше текущего AppDocument? parentDoc; for (int i = index - 1; i >= 0; i--) { if (docs[i].parentId == null && !docs[i].isPinned) { @@ -659,7 +665,17 @@ class ProjectWorkspace extends StatelessWidget { color: leftPanelBg, child: Column( children: [ - Expanded(child: _DocumentsList()), + // ✅ Переключение между списком и графом + Expanded( + child: Selector( + selector: (_, state) => state.isGraphView, + builder: (context, isGraphView, _) { + return isGraphView + ? _DocumentsGraph() // ✅ Графовое представление + : _DocumentsList(); // ✅ Список + }, + ), + ), Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration(border: Border(top: BorderSide(color: borderColor))), @@ -709,9 +725,27 @@ class ProjectWorkspace extends StatelessWidget { ), ], ), - floatingActionButton: FloatingActionButton(onPressed: () => _showCreateDocumentDialog(context), tooltip: 'Создать документ', child: const Icon(Icons.add)), - ); - } + floatingActionButton: Selector( + selector: (_, state) => state.isGraphView, + builder: (context, isGraphView, _) { + return FloatingActionButton( + onPressed: () => context.read().toggleViewMode(), + tooltip: isGraphView ? 'Список' : 'Граф', + child: Icon(isGraphView ? Icons.list : Icons.account_tree), + ); + }, + ), + + persistentFooterButtons: [ + FloatingActionButton( + heroTag: 'createDoc', + onPressed: () => _showCreateDocumentDialog(context), + tooltip: 'Новый документ', + child: const Icon(Icons.add), + ), + ], + ); +} void _showCreateDocumentDialog(BuildContext context) { final controller = TextEditingController(); @@ -769,7 +803,7 @@ class ProjectWorkspace extends StatelessWidget { } } -// ==================== КОМПОНЕНТЫ ==================== +// ==================== СПИСОК ДОКУМЕНТОВ ==================== class _DocumentsList extends StatelessWidget { const _DocumentsList(); @@ -783,7 +817,6 @@ class _DocumentsList extends StatelessWidget { return Column( children: [ - // ✅ Закреплённые документы (зелёный фон, перетаскивание, чекбоксы) if (pinnedDocs.isNotEmpty) Container( color: isDarkMode ? Colors.green[900]!.withOpacity(0.3) : Colors.green[50], @@ -851,8 +884,6 @@ class _DocumentsList extends StatelessWidget { }, ), ), - - // ✅ Обычные документы (синий фон, скроллинг, свайп для поддокументов, чекбоксы) Expanded( child: Container( color: isDarkMode ? Colors.blue[900]!.withOpacity(0.2) : Colors.blue[50], @@ -863,7 +894,6 @@ class _DocumentsList extends StatelessWidget { ); } - // ✅ Исправлено: передаём project для получения реального индекса Widget _buildDismissibleList(Project project, List docs, AppState state, bool isDarkMode, BuildContext context) { int mainIndex = 0; int childIndex = 0; @@ -873,8 +903,6 @@ class _DocumentsList extends StatelessWidget { itemBuilder: (context, index) { final doc = docs[index]; final isSelected = state.selectedDocument?.id == doc.id; - - // ✅ Находим РЕАЛЬНЫЙ индекс в project.documents final actualIndex = project.documents.indexOf(doc); String number; @@ -903,7 +931,6 @@ class _DocumentsList extends StatelessWidget { child: const Icon(Icons.arrow_forward, color: Colors.white), ), confirmDismiss: (direction) async { - // ✅ Используем actualIndex вместо index if (direction == DismissDirection.startToEnd) { state.indentDocument(actualIndex); } else if (direction == DismissDirection.endToStart) { @@ -972,6 +999,152 @@ class _DocumentsList extends StatelessWidget { } } +// ==================== ГРАФОВОЕ ПРЕДСТАВЛЕНИЕ ==================== +class _DocumentsGraph extends StatelessWidget { + const _DocumentsGraph(); + + @override + Widget build(BuildContext context) { + final state = context.watch(); + final project = state.selectedProject!; + final docs = project.documents; + final isDarkMode = state.isDarkMode; + + if (docs.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.account_tree_outlined, size: 64, color: Colors.grey[400]), + const SizedBox(height: 16), + Text( + 'Нет документов', + style: TextStyle(color: Colors.grey[600]), + ), + ], + ), + ); + } + + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // ✅ Строим граф документов + ..._buildDocumentNodes(docs, state, isDarkMode), + ], + ), + ), + ); + } + + List _buildDocumentNodes(List docs, AppState state, bool isDarkMode) { + final widgets = []; + final rootDocs = docs.where((d) => d.parentId == null).toList(); + + for (var doc in rootDocs) { + widgets.add(_buildDocumentNode(doc, docs, state, isDarkMode)); + widgets.add(const SizedBox(height: 20)); + } + + return widgets; + } + + Widget _buildDocumentNode(AppDocument doc, List allDocs, AppState state, bool isDarkMode) { + final isSelected = state.selectedDocument?.id == doc.id; + final children = allDocs.where((d) => d.parentId == doc.id).toList(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // ✅ Узел документа + GestureDetector( + onTap: () => state.selectDocument(doc), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + decoration: BoxDecoration( + color: isSelected + ? (isDarkMode ? Colors.green[800] : Colors.green[100]) + : (isDarkMode ? Colors.grey[800] : Colors.white), + border: Border.all( + color: isSelected + ? Colors.green[700]! + : (isDarkMode ? Colors.grey[600]! : Colors.grey[400]!), + width: 2, + ), + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + doc.isPinned ? Icons.push_pin : Icons.insert_drive_file, + color: isSelected ? Colors.green[700] : Colors.grey[600], + size: 20, + ), + const SizedBox(width: 8), + Text( + doc.name, + style: TextStyle( + fontSize: 14, + fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, + color: isDarkMode ? Colors.white : Colors.black87, + ), + ), + ], + ), + ), + ), + + // ✅ Стрелки и поддокументы + if (children.isNotEmpty) ...[ + const SizedBox(height: 8), + ...children.asMap().entries.map((entry) { + final isLast = entry.key == children.length - 1; + final childDoc = entry.value; + return Padding( + padding: const EdgeInsets.only(left: 40), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // ✅ Стрелка + Row( + children: [ + Container( + width: 30, + height: 2, + color: isDarkMode ? Colors.grey[600] : Colors.grey[400], + ), + Icon( + Icons.arrow_forward, + size: 16, + color: isDarkMode ? Colors.grey[600] : Colors.grey[400], + ), + ], + ), + const SizedBox(height: 8), + // ✅ Рекурсивно строим поддокументы + _buildDocumentNode(childDoc, allDocs, state, isDarkMode), + ], + ), + ); + }).toList(), + ], + ], + ); + } +} + +// ==================== РЕДАКТОР ==================== class QuillEditorView extends StatefulWidget { final AppDocument document; const QuillEditorView({super.key, required this.document}); @@ -1054,10 +1227,25 @@ class _MobileDocList extends StatelessWidget { appBar: AppBar( title: Text(state.selectedProject!.name), leading: IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => state.clearSelectedProject()), - actions: [IconButton(icon: const Icon(Icons.add), onPressed: () => _showCreateDocumentDialog(context), tooltip: 'Новый документ')], + actions: [ + IconButton( + icon: Icon(state.isGraphView ? Icons.list : Icons.account_tree), + onPressed: () => state.toggleViewMode(), + tooltip: state.isGraphView ? 'Список' : 'Граф', + ), + IconButton(icon: const Icon(Icons.add), onPressed: () => _showCreateDocumentDialog(context), tooltip: 'Новый документ'), + ], + ), + body: Selector( + selector: (_, state) => state.isGraphView, + builder: (context, isGraphView, _) { + return isGraphView ? const _DocumentsGraph() : const _DocumentsList(); + }, + ), + floatingActionButton: FloatingActionButton( + onPressed: () => _showCreateDocumentDialog(context), + child: const Icon(Icons.add), ), - body: const _DocumentsList(), - floatingActionButton: FloatingActionButton(onPressed: () => _showCreateDocumentDialog(context), child: const Icon(Icons.add)), ); } From e38499dde3182d0a65b756c8c701f608f4f2d90b Mon Sep 17 00:00:00 2001 From: Ulquiorra-Keesre Date: Fri, 10 Apr 2026 20:53:10 +0300 Subject: [PATCH 15/22] SecondEditor added --- manylines_editor/lib/main.dart | 530 +++++++++++++++++++++++++-------- 1 file changed, 408 insertions(+), 122 deletions(-) diff --git a/manylines_editor/lib/main.dart b/manylines_editor/lib/main.dart index d7a19cc..d6755c9 100644 --- a/manylines_editor/lib/main.dart +++ b/manylines_editor/lib/main.dart @@ -68,23 +68,29 @@ class AppState extends ChangeNotifier { bool _switchableValue = true; Project? _selectedProject; AppDocument? _selectedDocument; + AppDocument? _secondSelectedDocument; bool _isDarkMode = false; - bool _isGraphView = false; // ✅ Режим просмотра (список/граф) + bool _isGraphView = false; List get projects => _projects; List> get settings => _settings; bool get switchableValue => _switchableValue; Project? get selectedProject => _selectedProject; AppDocument? get selectedDocument => _selectedDocument; + AppDocument? get secondSelectedDocument => _secondSelectedDocument; bool get isDarkMode => _isDarkMode; - bool get isGraphView => _isGraphView; // ✅ Геттер для режима просмотра + bool get isGraphView => _isGraphView; void toggleDarkMode(bool value) { _isDarkMode = value; notifyListeners(); } - // ✅ Переключение между списком и графом + void closeFirstEditor() { + _selectedDocument = null; + notifyListeners(); + } + void toggleViewMode() { _isGraphView = !_isGraphView; notifyListeners(); @@ -130,6 +136,18 @@ class AppState extends ChangeNotifier { notifyListeners(); } + // ✅ Удалить документ + void deleteDocument(AppDocument doc) { + if (_selectedProject == null) return; + + // Если удаляем открытый документ — закрываем редактор + if (_selectedDocument?.id == doc.id) _selectedDocument = null; + if (_secondSelectedDocument?.id == doc.id) _secondSelectedDocument = null; + + _selectedProject!.documents.remove(doc); + notifyListeners(); + } + void toggleDocumentPin(AppDocument doc) { doc.isPinned = !doc.isPinned; notifyListeners(); @@ -194,9 +212,23 @@ class AppState extends ChangeNotifier { notifyListeners(); } + // ✅ Открыть документ во втором редакторе + void selectSecondDocument(AppDocument document) { + _secondSelectedDocument = document; + incrementViewCount(document); + notifyListeners(); + } + + // ✅ Закрыть второй редактор + void closeSecondEditor() { + _secondSelectedDocument = null; + notifyListeners(); + } + void clearSelectedProject() { _selectedProject = null; _selectedDocument = null; + _secondSelectedDocument = null; notifyListeners(); } @@ -637,105 +669,86 @@ class ProjectWorkspace extends StatelessWidget { } Widget _buildDesktopLayout(BuildContext context, AppDocument? selectedDocument) { - final state = context.read(); - final leftPanelBg = state.isDarkMode ? Colors.grey[900] : Colors.white; - final headerBg = state.isDarkMode ? Colors.green[900] : Colors.green[50]; - final textColor = state.isDarkMode ? Colors.white : Colors.black87; - final borderColor = state.isDarkMode ? Colors.grey[700]! : Colors.grey[300]!; - return Scaffold( - body: Row( - children: [ - Container( - width: 300, - decoration: BoxDecoration(border: Border(right: BorderSide(color: borderColor))), - child: Column( - children: [ - Container( - padding: const EdgeInsets.all(16), - color: headerBg, - child: Row( - children: [ - IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => state.clearSelectedProject(), tooltip: 'Back to projects'), - Expanded(child: Text(state.selectedProject!.name, style: Theme.of(context).textTheme.titleMedium?.copyWith(color: textColor))), - ], - ), + final state = context.watch(); + final leftPanelBg = state.isDarkMode ? Colors.grey[900] : Colors.white; + final headerBg = state.isDarkMode ? Colors.green[900] : Colors.green[50]; + final textColor = state.isDarkMode ? Colors.white : Colors.black87; + final borderColor = state.isDarkMode ? Colors.grey[700]! : Colors.grey[300]!; + + final showTwoEditors = state.secondSelectedDocument != null; + + return Scaffold( + body: Row( + children: [ + Container( + width: 300, + decoration: BoxDecoration(border: Border(right: BorderSide(color: borderColor))), + child: Column( + children: [ + Container( + padding: const EdgeInsets.all(16), + color: headerBg, + child: Row( + children: [ + IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => state.clearSelectedProject(), tooltip: 'Back to projects'), + Expanded(child: Text(state.selectedProject!.name, style: Theme.of(context).textTheme.titleMedium?.copyWith(color: textColor))), + ], ), - Expanded( - child: Material( - color: leftPanelBg, - child: Column( - children: [ - // ✅ Переключение между списком и графом - Expanded( - child: Selector( - selector: (_, state) => state.isGraphView, - builder: (context, isGraphView, _) { - return isGraphView - ? _DocumentsGraph() // ✅ Графовое представление - : _DocumentsList(); // ✅ Список - }, - ), + ), + Expanded( + child: Material( + color: leftPanelBg, + child: Column( + children: [ + Expanded( + child: Selector( + selector: (_, state) => state.isGraphView, + builder: (context, isGraphView, _) { + return isGraphView ? _DocumentsGraph() : _DocumentsList(); + }, ), - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration(border: Border(top: BorderSide(color: borderColor))), - child: SizedBox( - width: double.infinity, - child: OutlinedButton.icon( - onPressed: () => _showCreateDocumentDialog(context), - icon: const Icon(Icons.add, size: 18), - label: const Text('Новый документ'), - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 12), - foregroundColor: state.isDarkMode ? Colors.white : Colors.green[700], - side: BorderSide(color: state.isDarkMode ? Colors.green[400]! : Colors.green[700]!), - ), + ), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration(border: Border(top: BorderSide(color: borderColor))), + child: SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + onPressed: () => _showCreateDocumentDialog(context), + icon: const Icon(Icons.add, size: 18), + label: const Text('Новый документ'), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + foregroundColor: state.isDarkMode ? Colors.white : Colors.green[700], + side: BorderSide(color: state.isDarkMode ? Colors.green[400]! : Colors.green[700]!), ), ), ), - ], - ), + ), + ], ), ), - ], - ), - ), - Expanded( - child: selectedDocument != null - ? QuillEditorView(document: selectedDocument) - : Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.description_outlined, size: 64, color: state.isDarkMode ? Colors.white30 : Colors.black26), - const SizedBox(height: 16), - Text('В проекте нет документов', style: TextStyle(fontSize: 18, color: state.isDarkMode ? Colors.white70 : Colors.black54)), - const SizedBox(height: 8), - Text('Нажмите кнопку "+ Новый документ" чтобы создать', style: TextStyle(color: state.isDarkMode ? Colors.white54 : Colors.black45)), - const SizedBox(height: 24), - ElevatedButton.icon( - onPressed: () => _showCreateDocumentDialog(context), - icon: const Icon(Icons.add), - label: const Text('Создать первый документ'), - style: ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12)), - ), - ], - ), - ), + ), + ], ), - ], - ), - floatingActionButton: Selector( + ), + Expanded( + child: showTwoEditors + ? _buildTwoEditorsLayout(context, state, borderColor, textColor) + : _buildSingleEditorLayout(context, selectedDocument, state, textColor), + ), + ], + ), + floatingActionButton: Selector( selector: (_, state) => state.isGraphView, builder: (context, isGraphView, _) { return FloatingActionButton( - onPressed: () => context.read().toggleViewMode(), + onPressed: () => state.toggleViewMode(), tooltip: isGraphView ? 'Список' : 'Граф', child: Icon(isGraphView ? Icons.list : Icons.account_tree), ); }, ), - persistentFooterButtons: [ FloatingActionButton( heroTag: 'createDoc', @@ -747,6 +760,163 @@ class ProjectWorkspace extends StatelessWidget { ); } + // ✅ Макет с одним редактором (с toolbar) +// ✅ Макет с одним редактором (с toolbar) +Widget _buildSingleEditorLayout(BuildContext context, AppDocument? selectedDocument, AppState state, Color textColor) { + final project = state.selectedProject; + final hasDocuments = project != null && project.documents.isNotEmpty; + + if (selectedDocument == null) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + hasDocuments ? Icons.touch_app : Icons.description_outlined, + size: 64, + color: state.isDarkMode ? Colors.white30 : Colors.black26, + ), + const SizedBox(height: 16), + Text( + // ✅ Разные сообщения в зависимости от наличия документов + hasDocuments ? 'Выберите документ' : 'В проекте нет документов', + style: TextStyle(fontSize: 18, color: state.isDarkMode ? Colors.white70 : Colors.black54), + ), + const SizedBox(height: 8), + Text( + hasDocuments + ? 'Кликните на документ в списке слева' + : 'Нажмите кнопку "+ Новый документ" чтобы создать', + style: TextStyle(color: state.isDarkMode ? Colors.white54 : Colors.black45), + ), + const SizedBox(height: 24), + if (!hasDocuments) + ElevatedButton.icon( + onPressed: () => _showCreateDocumentDialog(context), + icon: const Icon(Icons.add), + label: const Text('Создать первый документ'), + style: ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12)), + ), + ], + ), + ); + } + + // ✅ Редактор с toolbar + return Column( + children: [ + // Toolbar первого редактора + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + color: state.isDarkMode ? Colors.grey[850] : Colors.grey[100], + child: Row( + children: [ + Expanded( + child: Text( + selectedDocument.name, + style: TextStyle(fontWeight: FontWeight.w500, color: textColor), + overflow: TextOverflow.ellipsis, + ), + ), + IconButton( + icon: const Icon(Icons.close, size: 20), + onPressed: () => state.closeFirstEditor(), + tooltip: 'Закрыть', + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + ], + ), + ), + // Контент редактора + Expanded( + child: QuillEditorView(document: selectedDocument, editorIndex: 1), + ), + ], + ); +} + + // ✅ Макет с двумя редакторами +Widget _buildTwoEditorsLayout(BuildContext context, AppState state, Color borderColor, Color textColor) { + return Row( + children: [ + // ✅ Первый редактор (50% ширины) с toolbar + Expanded( + child: Column( + children: [ + // Toolbar первого редактора + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + color: state.isDarkMode ? Colors.grey[850] : Colors.grey[100], + child: Row( + children: [ + Expanded( + child: Text( + state.selectedDocument?.name ?? 'Первый редактор', + style: TextStyle(fontWeight: FontWeight.w500, color: textColor), + overflow: TextOverflow.ellipsis, + ), + ), + IconButton( + icon: const Icon(Icons.close, size: 20), + onPressed: () => state.closeFirstEditor(), + tooltip: 'Закрыть', + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + ], + ), + ), + // Контент первого редактора + Expanded( + child: Container( + decoration: BoxDecoration(border: Border(right: BorderSide(color: borderColor))), + child: state.selectedDocument != null + ? QuillEditorView(document: state.selectedDocument!, editorIndex: 1) + : Center(child: Text('Выберите документ', style: TextStyle(color: textColor))), + ), + ), + ], + ), + ), + // ✅ Второй редактор (50% ширины) с toolbar + Expanded( + child: Column( + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + color: state.isDarkMode ? Colors.grey[850] : Colors.grey[100], + child: Row( + children: [ + Expanded( + child: Text( + state.secondSelectedDocument?.name ?? 'Второй редактор', + style: TextStyle(fontWeight: FontWeight.w500, color: textColor), + overflow: TextOverflow.ellipsis, + ), + ), + IconButton( + icon: const Icon(Icons.close, size: 20), + onPressed: () => state.closeSecondEditor(), + tooltip: 'Закрыть', + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + ], + ), + ), + Expanded( + child: state.secondSelectedDocument != null + ? QuillEditorView(document: state.secondSelectedDocument!, editorIndex: 2) + : Center(child: Text('Выберите документ', style: TextStyle(color: textColor))), + ), + ], + ), + ), + ], + ); +} + void _showCreateDocumentDialog(BuildContext context) { final controller = TextEditingController(); final formKey = GlobalKey(); @@ -827,7 +997,7 @@ class _DocumentsList extends StatelessWidget { onReorder: state.reorderPinnedDocuments, itemBuilder: (context, index) { final doc = pinnedDocs[index]; - final isSelected = state.selectedDocument?.id == doc.id; + final isSelected = state.selectedDocument?.id == doc.id || state.secondSelectedDocument?.id == doc.id; return Container( key: ValueKey(doc.id), @@ -867,18 +1037,26 @@ class _DocumentsList extends StatelessWidget { Checkbox( value: doc.isPinned, activeColor: Colors.green[700], - onChanged: (value) { - state.toggleDocumentPin(doc); - }, + onChanged: (value) => state.toggleDocumentPin(doc), ), const SizedBox(width: 4), + // ✅ Кнопка меню для удаления + IconButton( + icon: const Icon(Icons.more_vert, size: 20), + onPressed: () => _showDeleteMenu(context, doc), + tooltip: 'Меню', + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), Icon( Icons.drag_handle, color: isDarkMode ? Colors.white54 : Colors.grey, ), ], ), + // ✅ Долгое нажатие → сразу второй редактор onTap: () => state.selectDocument(doc), + onLongPress: () => state.selectSecondDocument(doc), ), ); }, @@ -902,7 +1080,7 @@ class _DocumentsList extends StatelessWidget { itemCount: docs.length, itemBuilder: (context, index) { final doc = docs[index]; - final isSelected = state.selectedDocument?.id == doc.id; + final isSelected = state.selectedDocument?.id == doc.id || state.secondSelectedDocument?.id == doc.id; final actualIndex = project.documents.indexOf(doc); String number; @@ -981,22 +1159,79 @@ class _DocumentsList extends StatelessWidget { Checkbox( value: doc.isPinned, activeColor: Colors.green[700], - onChanged: (value) { - state.toggleDocumentPin(doc); - }, + onChanged: (value) => state.toggleDocumentPin(doc), ), const SizedBox(width: 4), if (doc.parentId != null) Icon(Icons.swipe, size: 16, color: Colors.grey[500]), + // ✅ Кнопка меню для удаления + IconButton( + icon: const Icon(Icons.more_vert, size: 20), + onPressed: () => _showDeleteMenu(context, doc), + tooltip: 'Меню', + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), ], ), + // ✅ Долгое нажатие → сразу второй редактор onTap: () => state.selectDocument(doc), + onLongPress: () => state.selectSecondDocument(doc), ), ), ); }, ); } + + // ✅ Меню с опцией удаления + void _showDeleteMenu(BuildContext context, AppDocument doc) { + final state = context.read(); + final isDarkMode = state.isDarkMode; + + showMenu( + context: context, + position: RelativeRect.fill, + items: [ + PopupMenuItem( + child: Row( + children: [ + Icon(Icons.delete_outline, size: 20, color: Colors.red), + const SizedBox(width: 8), + Text('Удалить документ', style: TextStyle(color: Colors.red)), + ], + ), + onTap: () { + // ✅ Подтверждение удаления + showDialog( + context: context, + builder: (ctx) => AlertDialog( + backgroundColor: isDarkMode ? Colors.grey[850] : Colors.white, + title: const Text('Удалить документ?'), + content: Text('Документ "${doc.name}" будет удалён без возможности восстановления.', + style: TextStyle(color: isDarkMode ? Colors.white70 : Colors.black87)), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: Text('Отмена', style: TextStyle(color: isDarkMode ? Colors.grey[400] : Colors.grey[600])), + ), + ElevatedButton( + onPressed: () { + state.deleteDocument(doc); + Navigator.pop(ctx); + Navigator.pop(context); + }, + style: ElevatedButton.styleFrom(backgroundColor: Colors.red, foregroundColor: Colors.white), + child: const Text('Удалить'), + ), + ], + ), + ); + }, + ), + ], + ); + } } // ==================== ГРАФОВОЕ ПРЕДСТАВЛЕНИЕ ==================== @@ -1017,10 +1252,7 @@ class _DocumentsGraph extends StatelessWidget { children: [ Icon(Icons.account_tree_outlined, size: 64, color: Colors.grey[400]), const SizedBox(height: 16), - Text( - 'Нет документов', - style: TextStyle(color: Colors.grey[600]), - ), + Text('Нет документов', style: TextStyle(color: Colors.grey[600])), ], ), ); @@ -1032,36 +1264,36 @@ class _DocumentsGraph extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // ✅ Строим граф документов - ..._buildDocumentNodes(docs, state, isDarkMode), + ..._buildDocumentNodes(docs, state, isDarkMode, context), ], ), ), ); } - List _buildDocumentNodes(List docs, AppState state, bool isDarkMode) { + List _buildDocumentNodes(List docs, AppState state, bool isDarkMode, BuildContext context) { final widgets = []; final rootDocs = docs.where((d) => d.parentId == null).toList(); for (var doc in rootDocs) { - widgets.add(_buildDocumentNode(doc, docs, state, isDarkMode)); + widgets.add(_buildDocumentNode(doc, docs, state, isDarkMode, context)); widgets.add(const SizedBox(height: 20)); } return widgets; } - Widget _buildDocumentNode(AppDocument doc, List allDocs, AppState state, bool isDarkMode) { - final isSelected = state.selectedDocument?.id == doc.id; + Widget _buildDocumentNode(AppDocument doc, List allDocs, AppState state, bool isDarkMode, BuildContext context) { + final isSelected = state.selectedDocument?.id == doc.id || state.secondSelectedDocument?.id == doc.id; final children = allDocs.where((d) => d.parentId == doc.id).toList(); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // ✅ Узел документа GestureDetector( onTap: () => state.selectDocument(doc), + // ✅ Долгое нажатие → сразу второй редактор + onLongPress: () => state.selectSecondDocument(doc), child: Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), decoration: BoxDecoration( @@ -1100,40 +1332,36 @@ class _DocumentsGraph extends StatelessWidget { color: isDarkMode ? Colors.white : Colors.black87, ), ), + // ✅ Кнопка меню для удаления в графе + IconButton( + icon: const Icon(Icons.more_vert, size: 18), + onPressed: () => _showDeleteMenuInGraph(context, doc), + tooltip: 'Меню', + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), ], ), ), ), - // ✅ Стрелки и поддокументы if (children.isNotEmpty) ...[ const SizedBox(height: 8), ...children.asMap().entries.map((entry) { - final isLast = entry.key == children.length - 1; final childDoc = entry.value; return Padding( padding: const EdgeInsets.only(left: 40), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // ✅ Стрелка Row( children: [ - Container( - width: 30, - height: 2, - color: isDarkMode ? Colors.grey[600] : Colors.grey[400], - ), - Icon( - Icons.arrow_forward, - size: 16, - color: isDarkMode ? Colors.grey[600] : Colors.grey[400], - ), + Container(width: 30, height: 2, color: isDarkMode ? Colors.grey[600] : Colors.grey[400]), + Icon(Icons.arrow_forward, size: 16, color: isDarkMode ? Colors.grey[600] : Colors.grey[400]), ], ), const SizedBox(height: 8), - // ✅ Рекурсивно строим поддокументы - _buildDocumentNode(childDoc, allDocs, state, isDarkMode), + _buildDocumentNode(childDoc, allDocs, state, isDarkMode, context), ], ), ); @@ -1142,18 +1370,74 @@ class _DocumentsGraph extends StatelessWidget { ], ); } + + // ✅ Меню с опцией удаления для графа + void _showDeleteMenuInGraph(BuildContext context, AppDocument doc) { + final state = context.read(); + final isDarkMode = state.isDarkMode; + + showMenu( + context: context, + position: RelativeRect.fill, + items: [ + PopupMenuItem( + child: Row( + children: [ + Icon(Icons.delete_outline, size: 20, color: Colors.red), + const SizedBox(width: 8), + Text('Удалить документ', style: TextStyle(color: Colors.red)), + ], + ), + onTap: () { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + backgroundColor: isDarkMode ? Colors.grey[850] : Colors.white, + title: const Text('Удалить документ?'), + content: Text('Документ "${doc.name}" будет удалён без возможности восстановления.', + style: TextStyle(color: isDarkMode ? Colors.white70 : Colors.black87)), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: Text('Отмена', style: TextStyle(color: isDarkMode ? Colors.grey[400] : Colors.grey[600])), + ), + ElevatedButton( + onPressed: () { + state.deleteDocument(doc); + Navigator.pop(ctx); + Navigator.pop(context); + }, + style: ElevatedButton.styleFrom(backgroundColor: Colors.red, foregroundColor: Colors.white), + child: const Text('Удалить'), + ), + ], + ), + ); + }, + ), + ], + ); + } } // ==================== РЕДАКТОР ==================== class QuillEditorView extends StatefulWidget { final AppDocument document; - const QuillEditorView({super.key, required this.document}); + final int editorIndex; + + const QuillEditorView({ + super.key, + required this.document, + this.editorIndex = 1, + }); + @override State createState() => _QuillEditorViewState(); } class _QuillEditorViewState extends State { quill.QuillController? _controller; + @override void initState() { super.initState(); @@ -1176,16 +1460,17 @@ class _QuillEditorViewState extends State { @override void dispose() { - _controller?.dispose(); super.dispose(); } @override Widget build(BuildContext context) { if (_controller == null) return const Center(child: CircularProgressIndicator()); + return Column( children: [ quill.QuillSimpleToolbar( + key: ValueKey('toolbar_${widget.editorIndex}_${widget.document.id}'), controller: _controller!, config: const quill.QuillSimpleToolbarConfig( showBoldButton: true, @@ -1204,6 +1489,7 @@ class _QuillEditorViewState extends State { ), Expanded( child: quill.QuillEditor( + key: ValueKey('editor_${widget.editorIndex}_${widget.document.id}'), controller: _controller!, config: quill.QuillEditorConfig( placeholder: 'Начните печатать...', From 3be3b574162d6c0141f2dfd6ae0efcfe32f77e67 Mon Sep 17 00:00:00 2001 From: Ulquiorra-Keesre Date: Sat, 11 Apr 2026 15:14:27 +0300 Subject: [PATCH 16/22] side panel modification --- manylines_editor/lib/main.dart | 502 +++++++++++++++---------- manylines_editor/test/widget_test.dart | 24 +- 2 files changed, 302 insertions(+), 224 deletions(-) diff --git a/manylines_editor/lib/main.dart b/manylines_editor/lib/main.dart index d6755c9..8766939 100644 --- a/manylines_editor/lib/main.dart +++ b/manylines_editor/lib/main.dart @@ -71,6 +71,7 @@ class AppState extends ChangeNotifier { AppDocument? _secondSelectedDocument; bool _isDarkMode = false; bool _isGraphView = false; + bool _isSidePanelCollapsed = false; List get projects => _projects; List> get settings => _settings; @@ -80,12 +81,18 @@ class AppState extends ChangeNotifier { AppDocument? get secondSelectedDocument => _secondSelectedDocument; bool get isDarkMode => _isDarkMode; bool get isGraphView => _isGraphView; + bool get isSidePanelCollapsed => _isSidePanelCollapsed; void toggleDarkMode(bool value) { _isDarkMode = value; notifyListeners(); } + void toggleSidePanel() { + _isSidePanelCollapsed = !_isSidePanelCollapsed; + notifyListeners(); + } + void closeFirstEditor() { _selectedDocument = null; notifyListeners(); @@ -136,37 +143,49 @@ class AppState extends ChangeNotifier { notifyListeners(); } - // ✅ Удалить документ - void deleteDocument(AppDocument doc) { - if (_selectedProject == null) return; - - // Если удаляем открытый документ — закрываем редактор - if (_selectedDocument?.id == doc.id) _selectedDocument = null; - if (_secondSelectedDocument?.id == doc.id) _secondSelectedDocument = null; - - _selectedProject!.documents.remove(doc); - notifyListeners(); - } - + // ✅ Закрепить/открепить документ void toggleDocumentPin(AppDocument doc) { doc.isPinned = !doc.isPinned; notifyListeners(); } - void reorderPinnedDocuments(int oldIndex, int newIndex) { + // ✅ В классе AppState добавьте этот метод: +void reorderPinnedDocuments(int oldIndex, int newIndex) { + if (_selectedProject == null) return; + + final pinnedDocs = _selectedProject!.pinnedDocuments; + if (newIndex > oldIndex) newIndex -= 1; + + final doc = pinnedDocs.removeAt(oldIndex); + final targetDoc = pinnedDocs[newIndex]; + + // Находим индексы в основном списке + final docMainIndex = _selectedProject!.documents.indexOf(doc); + final targetMainIndex = _selectedProject!.documents.indexOf(targetDoc); + + // Перемещаем в основном списке + _selectedProject!.documents.removeAt(docMainIndex); + _selectedProject!.documents.insert(targetMainIndex, doc); + + notifyListeners(); +} + + // ✅ Удалить документ + void deleteDocument(AppDocument doc) { if (_selectedProject == null) return; - final pinnedDocs = _selectedProject!.pinnedDocuments; - if (newIndex > oldIndex) newIndex -= 1; - - final doc = pinnedDocs.removeAt(oldIndex); - final targetDoc = pinnedDocs[newIndex]; - final docMainIndex = _selectedProject!.documents.indexOf(doc); - final targetMainIndex = _selectedProject!.documents.indexOf(targetDoc); + // ✅ Сначала удаляем документ из проекта + _selectedProject!.documents.remove(doc); - _selectedProject!.documents.removeAt(docMainIndex); - _selectedProject!.documents.insert(targetMainIndex, doc); + // ✅ Затем закрываем редакторы, если они используют этот документ + if (_selectedDocument?.id == doc.id) { + _selectedDocument = null; + } + if (_secondSelectedDocument?.id == doc.id) { + _secondSelectedDocument = null; + } + // ✅ И только потом уведомляем слушателей notifyListeners(); } @@ -676,62 +695,117 @@ class ProjectWorkspace extends StatelessWidget { final borderColor = state.isDarkMode ? Colors.grey[700]! : Colors.grey[300]!; final showTwoEditors = state.secondSelectedDocument != null; + final isPanelCollapsed = state.isSidePanelCollapsed; // ✅ Получаем состояние return Scaffold( body: Row( children: [ - Container( - width: 300, - decoration: BoxDecoration(border: Border(right: BorderSide(color: borderColor))), - child: Column( - children: [ - Container( - padding: const EdgeInsets.all(16), - color: headerBg, - child: Row( + // ✅ Боковая панель с анимацией сворачивания + AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + width: isPanelCollapsed ? 0 : 300, + decoration: BoxDecoration( + border: Border(right: BorderSide(color: borderColor)), + ), + child: isPanelCollapsed + ? const SizedBox.shrink() + : Column( children: [ - IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => state.clearSelectedProject(), tooltip: 'Back to projects'), - Expanded(child: Text(state.selectedProject!.name, style: Theme.of(context).textTheme.titleMedium?.copyWith(color: textColor))), - ], - ), - ), - Expanded( - child: Material( - color: leftPanelBg, - child: Column( - children: [ - Expanded( - child: Selector( - selector: (_, state) => state.isGraphView, - builder: (context, isGraphView, _) { - return isGraphView ? _DocumentsGraph() : _DocumentsList(); - }, - ), - ), - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration(border: Border(top: BorderSide(color: borderColor))), - child: SizedBox( - width: double.infinity, - child: OutlinedButton.icon( - onPressed: () => _showCreateDocumentDialog(context), - icon: const Icon(Icons.add, size: 18), - label: const Text('Новый документ'), - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 12), - foregroundColor: state.isDarkMode ? Colors.white : Colors.green[700], - side: BorderSide(color: state.isDarkMode ? Colors.green[400]! : Colors.green[700]!), + Container( + padding: const EdgeInsets.all(16), + color: headerBg, + child: Row( + children: [ + IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => state.clearSelectedProject(), + tooltip: 'Back to projects', + ), + Expanded( + child: Text( + state.selectedProject!.name, + style: Theme.of(context).textTheme.titleMedium?.copyWith(color: textColor), ), ), + ], + ), + ), + Expanded( + child: Material( + color: leftPanelBg, + child: Column( + children: [ + Expanded( + child: Selector( + selector: (_, state) => state.isGraphView, + builder: (context, isGraphView, _) { + return isGraphView ? _DocumentsGraph() : _DocumentsList(); + }, + ), + ), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration(border: Border(top: BorderSide(color: borderColor))), + child: SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + onPressed: () => _showCreateDocumentDialog(context), + icon: const Icon(Icons.add, size: 18), + label: const Text('Новый документ'), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + foregroundColor: state.isDarkMode ? Colors.white : Colors.green[700], + side: BorderSide(color: state.isDarkMode ? Colors.green[400]! : Colors.green[700]!), + ), + ), + ), + ), + ], ), ), - ], + ), + ], + ), + ), + + // ✅ Кнопка-вкладка для сворачивания/разворачивания + Container( + width: 24, + decoration: BoxDecoration( + color: isPanelCollapsed ? (state.isDarkMode ? Colors.grey[800] : Colors.grey[200]) : Colors.transparent, + border: Border(right: BorderSide(color: borderColor)), + ), + child: Column( + children: [ + // ✅ Регулируйте высоту верхнего отступа + const SizedBox(height: 100), // Было: Spacer() + + // ✅ Кнопка сворачивания/разворачивания + GestureDetector( + onTap: () => state.toggleSidePanel(), + child: Container( + width: 24, + height: 82, + decoration: BoxDecoration( + color: state.isDarkMode ? Colors.grey[700] : Colors.grey[300], + borderRadius: BorderRadius.zero, + ), + child: Icon( + isPanelCollapsed ? Icons.chevron_right : Icons.chevron_left, + size: 20, + color: textColor, + ), ), ), - ), - ], + + // ✅ Оставшееся пространство + const Expanded(child: SizedBox()), // Было: Spacer() + ], + ), ), - ), + + // ✅ Редакторы занимают всё оставшееся пространство Expanded( child: showTwoEditors ? _buildTwoEditorsLayout(context, state, borderColor, textColor) @@ -761,161 +835,159 @@ class ProjectWorkspace extends StatelessWidget { } // ✅ Макет с одним редактором (с toolbar) -// ✅ Макет с одним редактором (с toolbar) -Widget _buildSingleEditorLayout(BuildContext context, AppDocument? selectedDocument, AppState state, Color textColor) { - final project = state.selectedProject; - final hasDocuments = project != null && project.documents.isNotEmpty; - - if (selectedDocument == null) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - hasDocuments ? Icons.touch_app : Icons.description_outlined, - size: 64, - color: state.isDarkMode ? Colors.white30 : Colors.black26, - ), - const SizedBox(height: 16), - Text( - // ✅ Разные сообщения в зависимости от наличия документов - hasDocuments ? 'Выберите документ' : 'В проекте нет документов', - style: TextStyle(fontSize: 18, color: state.isDarkMode ? Colors.white70 : Colors.black54), - ), - const SizedBox(height: 8), - Text( - hasDocuments - ? 'Кликните на документ в списке слева' - : 'Нажмите кнопку "+ Новый документ" чтобы создать', - style: TextStyle(color: state.isDarkMode ? Colors.white54 : Colors.black45), - ), - const SizedBox(height: 24), - if (!hasDocuments) - ElevatedButton.icon( - onPressed: () => _showCreateDocumentDialog(context), - icon: const Icon(Icons.add), - label: const Text('Создать первый документ'), - style: ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12)), - ), - ], - ), - ); - } - - // ✅ Редактор с toolbar - return Column( - children: [ - // Toolbar первого редактора - Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - color: state.isDarkMode ? Colors.grey[850] : Colors.grey[100], - child: Row( + Widget _buildSingleEditorLayout(BuildContext context, AppDocument? selectedDocument, AppState state, Color textColor) { + final project = state.selectedProject; + final hasDocuments = project != null && project.documents.isNotEmpty; + + if (selectedDocument == null) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, children: [ - Expanded( - child: Text( - selectedDocument.name, - style: TextStyle(fontWeight: FontWeight.w500, color: textColor), - overflow: TextOverflow.ellipsis, - ), + Icon( + hasDocuments ? Icons.touch_app : Icons.description_outlined, + size: 64, + color: state.isDarkMode ? Colors.white30 : Colors.black26, ), - IconButton( - icon: const Icon(Icons.close, size: 20), - onPressed: () => state.closeFirstEditor(), - tooltip: 'Закрыть', - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), + const SizedBox(height: 16), + Text( + hasDocuments ? 'Выберите документ' : 'В проекте нет документов', + style: TextStyle(fontSize: 18, color: state.isDarkMode ? Colors.white70 : Colors.black54), + ), + const SizedBox(height: 8), + Text( + hasDocuments + ? 'Кликните на документ в списке слева' + : 'Нажмите кнопку "+ Новый документ" чтобы создать', + style: TextStyle(color: state.isDarkMode ? Colors.white54 : Colors.black45), ), + const SizedBox(height: 24), + if (!hasDocuments) + ElevatedButton.icon( + onPressed: () => _showCreateDocumentDialog(context), + icon: const Icon(Icons.add), + label: const Text('Создать первый документ'), + style: ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12)), + ), ], ), - ), - // Контент редактора - Expanded( - child: QuillEditorView(document: selectedDocument, editorIndex: 1), - ), - ], - ); -} + ); + } + + // ✅ Редактор с toolbar + return Column( + children: [ + // Toolbar первого редактора + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + color: state.isDarkMode ? Colors.grey[850] : Colors.grey[100], + child: Row( + children: [ + Expanded( + child: Text( + selectedDocument.name, + style: TextStyle(fontWeight: FontWeight.w500, color: textColor), + overflow: TextOverflow.ellipsis, + ), + ), + IconButton( + icon: const Icon(Icons.close, size: 20), + onPressed: () => state.closeFirstEditor(), + tooltip: 'Закрыть', + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + ], + ), + ), + // Контент редактора + Expanded( + child: QuillEditorView(document: selectedDocument, editorIndex: 1), + ), + ], + ); + } // ✅ Макет с двумя редакторами -Widget _buildTwoEditorsLayout(BuildContext context, AppState state, Color borderColor, Color textColor) { - return Row( - children: [ - // ✅ Первый редактор (50% ширины) с toolbar - Expanded( - child: Column( - children: [ - // Toolbar первого редактора - Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - color: state.isDarkMode ? Colors.grey[850] : Colors.grey[100], - child: Row( - children: [ - Expanded( - child: Text( - state.selectedDocument?.name ?? 'Первый редактор', - style: TextStyle(fontWeight: FontWeight.w500, color: textColor), - overflow: TextOverflow.ellipsis, + Widget _buildTwoEditorsLayout(BuildContext context, AppState state, Color borderColor, Color textColor) { + return Row( + children: [ + // ✅ Первый редактор (50% ширины) с toolbar + Expanded( + child: Column( + children: [ + // Toolbar первого редактора + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + color: state.isDarkMode ? Colors.grey[850] : Colors.grey[100], + child: Row( + children: [ + Expanded( + child: Text( + state.selectedDocument?.name ?? 'Первый редактор', + style: TextStyle(fontWeight: FontWeight.w500, color: textColor), + overflow: TextOverflow.ellipsis, + ), ), - ), - IconButton( - icon: const Icon(Icons.close, size: 20), - onPressed: () => state.closeFirstEditor(), - tooltip: 'Закрыть', - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - ), - ], + IconButton( + icon: const Icon(Icons.close, size: 20), + onPressed: () => state.closeFirstEditor(), + tooltip: 'Закрыть', + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + ], + ), ), - ), - // Контент первого редактора - Expanded( - child: Container( - decoration: BoxDecoration(border: Border(right: BorderSide(color: borderColor))), - child: state.selectedDocument != null - ? QuillEditorView(document: state.selectedDocument!, editorIndex: 1) - : Center(child: Text('Выберите документ', style: TextStyle(color: textColor))), + // Контент первого редактора + Expanded( + child: Container( + decoration: BoxDecoration(border: Border(right: BorderSide(color: borderColor))), + child: state.selectedDocument != null + ? QuillEditorView(document: state.selectedDocument!, editorIndex: 1) + : Center(child: Text('Выберите документ', style: TextStyle(color: textColor))), + ), ), - ), - ], + ], + ), ), - ), - // ✅ Второй редактор (50% ширины) с toolbar - Expanded( - child: Column( - children: [ - Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - color: state.isDarkMode ? Colors.grey[850] : Colors.grey[100], - child: Row( - children: [ - Expanded( - child: Text( - state.secondSelectedDocument?.name ?? 'Второй редактор', - style: TextStyle(fontWeight: FontWeight.w500, color: textColor), - overflow: TextOverflow.ellipsis, + // ✅ Второй редактор (50% ширины) с toolbar + Expanded( + child: Column( + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + color: state.isDarkMode ? Colors.grey[850] : Colors.grey[100], + child: Row( + children: [ + Expanded( + child: Text( + state.secondSelectedDocument?.name ?? 'Второй редактор', + style: TextStyle(fontWeight: FontWeight.w500, color: textColor), + overflow: TextOverflow.ellipsis, + ), ), - ), - IconButton( - icon: const Icon(Icons.close, size: 20), - onPressed: () => state.closeSecondEditor(), - tooltip: 'Закрыть', - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - ), - ], + IconButton( + icon: const Icon(Icons.close, size: 20), + onPressed: () => state.closeSecondEditor(), + tooltip: 'Закрыть', + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + ], + ), ), - ), - Expanded( - child: state.secondSelectedDocument != null - ? QuillEditorView(document: state.secondSelectedDocument!, editorIndex: 2) - : Center(child: Text('Выберите документ', style: TextStyle(color: textColor))), - ), - ], + Expanded( + child: state.secondSelectedDocument != null + ? QuillEditorView(document: state.secondSelectedDocument!, editorIndex: 2) + : Center(child: Text('Выберите документ', style: TextStyle(color: textColor))), + ), + ], + ), ), - ), - ], - ); -} + ], + ); + } void _showCreateDocumentDialog(BuildContext context) { final controller = TextEditingController(); @@ -1598,4 +1670,20 @@ class _MobileEditorView extends StatelessWidget { body: QuillEditorView(document: document), ); } +} + +class TestApp extends StatelessWidget { + const TestApp({super.key}); + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (_) => AppState(), + child: MaterialApp( + title: 'Manyllines Test', + // ✅ Не используем локализации в тестах - они не инициализируются в test environment + home: const AppShell(), + ), + ); + } } \ No newline at end of file diff --git a/manylines_editor/test/widget_test.dart b/manylines_editor/test/widget_test.dart index 57295e3..eafff7d 100644 --- a/manylines_editor/test/widget_test.dart +++ b/manylines_editor/test/widget_test.dart @@ -7,24 +7,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; - -import 'package:manylines_editor/main.dart'; +import 'package:manylines_editor/main.dart'; // Импортируем и MyApp, и TestApp void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); + testWidgets('App loads without crashing', (WidgetTester tester) async { + // ✅ Используем упрощённый TestApp вместо MyApp + await tester.pumpWidget(const TestApp()); + await tester.pumpAndSettle(); - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); + }); -} +} \ No newline at end of file From c5ee94b236387649fffbc69f03ba21691887d84c Mon Sep 17 00:00:00 2001 From: Ulquiorra-Keesre Date: Sat, 11 Apr 2026 22:04:37 +0300 Subject: [PATCH 17/22] Glossary created --- manylines_editor/lib/main.dart | 816 ++++++++++++++++++++----- manylines_editor/test/widget_test.dart | 77 ++- 2 files changed, 748 insertions(+), 145 deletions(-) diff --git a/manylines_editor/lib/main.dart b/manylines_editor/lib/main.dart index 8766939..c0e0b7d 100644 --- a/manylines_editor/lib/main.dart +++ b/manylines_editor/lib/main.dart @@ -5,6 +5,22 @@ import 'package:dart_quill_delta/dart_quill_delta.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; // ==================== МОДЕЛИ ==================== +class GlossaryEntry { + final String id; + final String term; + String definition; + bool isExpanded; + DateTime createdAt; + + GlossaryEntry({ + required this.id, + required this.term, + this.definition = '', + this.isExpanded = false, + DateTime? createdAt, + }) : createdAt = createdAt ?? DateTime.now(); +} + class Project { final String id; final String name; @@ -39,6 +55,7 @@ class AppDocument { bool isPinned; String? parentId; Delta content; + List glossary; AppDocument({ required this.id, @@ -47,6 +64,7 @@ class AppDocument { this.isPinned = false, this.parentId, required this.content, + this.glossary = const [], }); bool get isChild => parentId != null; @@ -72,6 +90,8 @@ class AppState extends ChangeNotifier { bool _isDarkMode = false; bool _isGraphView = false; bool _isSidePanelCollapsed = false; + bool _isGlossaryPanelOpen = false; + String? _selectedTextForGlossary; List get projects => _projects; List> get settings => _settings; @@ -82,27 +102,173 @@ class AppState extends ChangeNotifier { bool get isDarkMode => _isDarkMode; bool get isGraphView => _isGraphView; bool get isSidePanelCollapsed => _isSidePanelCollapsed; + bool get isGlossaryPanelOpen => _isGlossaryPanelOpen; + String? get selectedTextForGlossary => _selectedTextForGlossary; void toggleDarkMode(bool value) { _isDarkMode = value; notifyListeners(); } + void closeFirstEditor() { + _selectedDocument = null; + notifyListeners(); + } + + void toggleViewMode() { + _isGraphView = !_isGraphView; + notifyListeners(); + } + void toggleSidePanel() { _isSidePanelCollapsed = !_isSidePanelCollapsed; notifyListeners(); } - void closeFirstEditor() { - _selectedDocument = null; + void toggleGlossaryPanel() { + _isGlossaryPanelOpen = !_isGlossaryPanelOpen; notifyListeners(); } - void toggleViewMode() { - _isGraphView = !_isGraphView; + void setSelectedTextForGlossary(String text) { + _selectedTextForGlossary = text; + notifyListeners(); + } + + void clearSelectedTextForGlossary() { + _selectedTextForGlossary = null; + notifyListeners(); + } + + // ✅ Обновить документ в проекте + void _updateDocumentInProject(AppDocument updatedDocument) { + if (_selectedProject == null) return; + + final index = _selectedProject!.documents.indexWhere((d) => d.id == updatedDocument.id); + if (index != -1) { + _selectedProject!.documents[index] = updatedDocument; + notifyListeners(); + } + } + + // ✅ Добавить запись в глоссарий + void addGlossaryEntry(String term) { + if (_selectedDocument == null) return; + + final newEntry = GlossaryEntry( + id: 'g${DateTime.now().millisecondsSinceEpoch}', + term: term, + definition: '', + isExpanded: true, + ); + + final updatedGlossary = List.from(_selectedDocument!.glossary) + ..add(newEntry); + + final updatedDocument = AppDocument( + id: _selectedDocument!.id, + name: _selectedDocument!.name, + viewCount: _selectedDocument!.viewCount, + isPinned: _selectedDocument!.isPinned, + parentId: _selectedDocument!.parentId, + content: _selectedDocument!.content, + glossary: updatedGlossary, + ); + + _selectedDocument = updatedDocument; + _updateDocumentInProject(updatedDocument); + + _selectedTextForGlossary = null; + } + + // ✅ Автоматически добавить и открыть глоссарий + void addAndOpenGlossary(String term) { + addGlossaryEntry(term); + _isGlossaryPanelOpen = true; notifyListeners(); } + void updateGlossaryDefinition(String entryId, String definition) { + if (_selectedDocument == null) return; + + final updatedGlossary = _selectedDocument!.glossary.map((entry) { + if (entry.id == entryId) { + return GlossaryEntry( + id: entry.id, + term: entry.term, + definition: definition, + isExpanded: entry.isExpanded, + createdAt: entry.createdAt, + ); + } + return entry; + }).toList(); + + final updatedDocument = AppDocument( + id: _selectedDocument!.id, + name: _selectedDocument!.name, + viewCount: _selectedDocument!.viewCount, + isPinned: _selectedDocument!.isPinned, + parentId: _selectedDocument!.parentId, + content: _selectedDocument!.content, + glossary: updatedGlossary, + ); + + _selectedDocument = updatedDocument; + _updateDocumentInProject(updatedDocument); + } + + void toggleGlossaryEntry(String entryId) { + if (_selectedDocument == null) return; + + final updatedGlossary = _selectedDocument!.glossary.map((entry) { + if (entry.id == entryId) { + return GlossaryEntry( + id: entry.id, + term: entry.term, + definition: entry.definition, + isExpanded: !entry.isExpanded, + createdAt: entry.createdAt, + ); + } + return entry; + }).toList(); + + final updatedDocument = AppDocument( + id: _selectedDocument!.id, + name: _selectedDocument!.name, + viewCount: _selectedDocument!.viewCount, + isPinned: _selectedDocument!.isPinned, + parentId: _selectedDocument!.parentId, + content: _selectedDocument!.content, + glossary: updatedGlossary, + ); + + _selectedDocument = updatedDocument; + _updateDocumentInProject(updatedDocument); + } + + void deleteGlossaryEntry(String entryId) { + if (_selectedDocument == null) return; + + final updatedGlossary = _selectedDocument!.glossary + .where((entry) => entry.id != entryId) + .toList(); + + final updatedDocument = AppDocument( + id: _selectedDocument!.id, + name: _selectedDocument!.name, + viewCount: _selectedDocument!.viewCount, + isPinned: _selectedDocument!.isPinned, + parentId: _selectedDocument!.parentId, + content: _selectedDocument!.content, + glossary: updatedGlossary, + ); + + _selectedDocument = updatedDocument; + _updateDocumentInProject(updatedDocument); + } + void incrementViewCount(AppDocument doc) { doc.viewCount++; notifyListeners(); @@ -143,41 +309,34 @@ class AppState extends ChangeNotifier { notifyListeners(); } - // ✅ Закрепить/открепить документ void toggleDocumentPin(AppDocument doc) { doc.isPinned = !doc.isPinned; notifyListeners(); } - // ✅ В классе AppState добавьте этот метод: -void reorderPinnedDocuments(int oldIndex, int newIndex) { - if (_selectedProject == null) return; - - final pinnedDocs = _selectedProject!.pinnedDocuments; - if (newIndex > oldIndex) newIndex -= 1; - - final doc = pinnedDocs.removeAt(oldIndex); - final targetDoc = pinnedDocs[newIndex]; - - // Находим индексы в основном списке - final docMainIndex = _selectedProject!.documents.indexOf(doc); - final targetMainIndex = _selectedProject!.documents.indexOf(targetDoc); - - // Перемещаем в основном списке - _selectedProject!.documents.removeAt(docMainIndex); - _selectedProject!.documents.insert(targetMainIndex, doc); - - notifyListeners(); -} + void reorderPinnedDocuments(int oldIndex, int newIndex) { + if (_selectedProject == null) return; + + final pinnedDocs = _selectedProject!.pinnedDocuments; + if (newIndex > oldIndex) newIndex -= 1; + + final doc = pinnedDocs.removeAt(oldIndex); + final targetDoc = pinnedDocs[newIndex]; + + final docMainIndex = _selectedProject!.documents.indexOf(doc); + final targetMainIndex = _selectedProject!.documents.indexOf(targetDoc); + + _selectedProject!.documents.removeAt(docMainIndex); + _selectedProject!.documents.insert(targetMainIndex, doc); + + notifyListeners(); + } - // ✅ Удалить документ void deleteDocument(AppDocument doc) { if (_selectedProject == null) return; - // ✅ Сначала удаляем документ из проекта _selectedProject!.documents.remove(doc); - // ✅ Затем закрываем редакторы, если они используют этот документ if (_selectedDocument?.id == doc.id) { _selectedDocument = null; } @@ -185,7 +344,6 @@ void reorderPinnedDocuments(int oldIndex, int newIndex) { _secondSelectedDocument = null; } - // ✅ И только потом уведомляем слушателей notifyListeners(); } @@ -222,23 +380,25 @@ void reorderPinnedDocuments(int oldIndex, int newIndex) { } else { _selectedDocument = null; } + _isGlossaryPanelOpen = false; + _selectedTextForGlossary = null; notifyListeners(); } void selectDocument(AppDocument document) { _selectedDocument = document; incrementViewCount(document); + _isGlossaryPanelOpen = false; + _selectedTextForGlossary = null; notifyListeners(); } - // ✅ Открыть документ во втором редакторе void selectSecondDocument(AppDocument document) { _secondSelectedDocument = document; incrementViewCount(document); notifyListeners(); } - // ✅ Закрыть второй редактор void closeSecondEditor() { _secondSelectedDocument = null; notifyListeners(); @@ -248,6 +408,8 @@ void reorderPinnedDocuments(int oldIndex, int newIndex) { _selectedProject = null; _selectedDocument = null; _secondSelectedDocument = null; + _isGlossaryPanelOpen = false; + _selectedTextForGlossary = null; notifyListeners(); } @@ -294,6 +456,27 @@ void reorderPinnedDocuments(int oldIndex, int newIndex) { void dispose() { super.dispose(); } + + String? getSelectedTextFromController(quill.QuillController? controller) { + if (controller == null) return null; + + final selection = controller.selection; + if (selection.isCollapsed) return null; + + final text = controller.document.toPlainText(); + if (selection.baseOffset >= text.length || selection.extentOffset >= text.length) return null; + + final start = selection.baseOffset < selection.extentOffset + ? selection.baseOffset + : selection.extentOffset; + final end = selection.baseOffset < selection.extentOffset + ? selection.extentOffset + : selection.baseOffset; + + final selectedText = text.substring(start, end); + + return selectedText.trim().isNotEmpty ? selectedText.trim() : null; +} } // ==================== APP ==================== @@ -695,12 +878,13 @@ class ProjectWorkspace extends StatelessWidget { final borderColor = state.isDarkMode ? Colors.grey[700]! : Colors.grey[300]!; final showTwoEditors = state.secondSelectedDocument != null; - final isPanelCollapsed = state.isSidePanelCollapsed; // ✅ Получаем состояние + final isPanelCollapsed = state.isSidePanelCollapsed; + final isGlossaryOpen = state.isGlossaryPanelOpen; return Scaffold( body: Row( children: [ - // ✅ Боковая панель с анимацией сворачивания + // ✅ Левая панель (сворачиваемая) AnimatedContainer( duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, @@ -717,17 +901,8 @@ class ProjectWorkspace extends StatelessWidget { color: headerBg, child: Row( children: [ - IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () => state.clearSelectedProject(), - tooltip: 'Back to projects', - ), - Expanded( - child: Text( - state.selectedProject!.name, - style: Theme.of(context).textTheme.titleMedium?.copyWith(color: textColor), - ), - ), + IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => state.clearSelectedProject(), tooltip: 'Back to projects'), + Expanded(child: Text(state.selectedProject!.name, style: Theme.of(context).textTheme.titleMedium?.copyWith(color: textColor))), ], ), ), @@ -769,50 +944,114 @@ class ProjectWorkspace extends StatelessWidget { ), ), - // ✅ Кнопка-вкладка для сворачивания/разворачивания + // ✅ Кнопка сворачивания левой панели Container( + width: 24, + decoration: BoxDecoration( + color: isPanelCollapsed ? (state.isDarkMode ? Colors.grey[800] : Colors.grey[200]) : Colors.transparent, + border: Border(right: BorderSide(color: borderColor)), + ), + child: Column( + children: [ + // ✅ Регулируйте высоту верхнего отступа (100px от верха) + const SizedBox(height: 100), // ← Меняйте это значение + + // ✅ Кнопка сворачивания/разворачивания + GestureDetector( + onTap: () => state.toggleSidePanel(), + child: Container( + width: 24, + height: 82, + decoration: BoxDecoration( + color: state.isDarkMode ? Colors.grey[700] : Colors.grey[300], + ), + child: Icon( + isPanelCollapsed ? Icons.chevron_right : Icons.chevron_left, + size: 20, + color: textColor, + ), + ), + ), + + // ✅ Оставшееся пространство + const Expanded(child: SizedBox()), // Было: Spacer() + ], + ), + ), + + // ✅ Редакторы + // ✅ Редакторы +Expanded( + child: showTwoEditors + ? _buildTwoEditorsLayout(context, state, borderColor, textColor) + : _buildSingleEditorLayout(context, selectedDocument, state, textColor), +), + +// ✅ Панель глоссария + вкладка слева +if (isGlossaryOpen) ...[ + // ✅ Вкладка для ЗАКРЫТИЯ глоссария (слева от панели) + Container( + width: 24, + decoration: BoxDecoration( + color: state.isDarkMode ? Colors.grey[800] : Colors.grey[200], + border: Border(right: BorderSide(color: borderColor)), + ), + child: Column( + children: [ + const SizedBox(height: 100), + GestureDetector( + onTap: () => state.toggleGlossaryPanel(), + child: Container( width: 24, + height: 82, decoration: BoxDecoration( - color: isPanelCollapsed ? (state.isDarkMode ? Colors.grey[800] : Colors.grey[200]) : Colors.transparent, - border: Border(right: BorderSide(color: borderColor)), + color: state.isDarkMode ? Colors.grey[700] : Colors.grey[300], ), - child: Column( - children: [ - // ✅ Регулируйте высоту верхнего отступа - const SizedBox(height: 100), // Было: Spacer() - - // ✅ Кнопка сворачивания/разворачивания - GestureDetector( - onTap: () => state.toggleSidePanel(), - child: Container( - width: 24, - height: 82, - decoration: BoxDecoration( - color: state.isDarkMode ? Colors.grey[700] : Colors.grey[300], - borderRadius: BorderRadius.zero, - ), - child: Icon( - isPanelCollapsed ? Icons.chevron_right : Icons.chevron_left, - size: 20, - color: textColor, - ), - ), - ), - - // ✅ Оставшееся пространство - const Expanded(child: SizedBox()), // Было: Spacer() - ], + child: Icon( + Icons.chevron_right, // Стрелка вправо = закрыть панель + size: 20, + color: textColor, ), ), - - // ✅ Редакторы занимают всё оставшееся пространство - Expanded( - child: showTwoEditors - ? _buildTwoEditorsLayout(context, state, borderColor, textColor) - : _buildSingleEditorLayout(context, selectedDocument, state, textColor), ), + const Expanded(child: SizedBox()), ], ), + ), + // ✅ Панель глоссария + const _GlossaryPanel(), +] else + // ✅ Вкладка для ОТКРЫТИЯ глоссария (слева, когда панель закрыта) + Container( + width: 24, + decoration: BoxDecoration( + color: state.isDarkMode ? Colors.grey[800] : Colors.grey[200], + border: Border(right: BorderSide(color: borderColor)), + ), + child: Column( + children: [ + const SizedBox(height: 100), + GestureDetector( + onTap: () => state.toggleGlossaryPanel(), + child: Container( + width: 24, + height: 82, + decoration: BoxDecoration( + color: state.isDarkMode ? Colors.blue[700] : Colors.blue[300], + ), + child: const Icon( + Icons.chevron_left, // Стрелка влево = открыть панель + size: 20, + color: Colors.white, + ), + ), + ), + const Expanded(child: SizedBox()), + ], + ), + ), + ], + ), floatingActionButton: Selector( selector: (_, state) => state.isGraphView, builder: (context, isGraphView, _) { @@ -834,7 +1073,6 @@ class ProjectWorkspace extends StatelessWidget { ); } - // ✅ Макет с одним редактором (с toolbar) Widget _buildSingleEditorLayout(BuildContext context, AppDocument? selectedDocument, AppState state, Color textColor) { final project = state.selectedProject; final hasDocuments = project != null && project.documents.isNotEmpty; @@ -874,10 +1112,8 @@ class ProjectWorkspace extends StatelessWidget { ); } - // ✅ Редактор с toolbar return Column( children: [ - // Toolbar первого редактора Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), color: state.isDarkMode ? Colors.grey[850] : Colors.grey[100], @@ -900,7 +1136,6 @@ class ProjectWorkspace extends StatelessWidget { ], ), ), - // Контент редактора Expanded( child: QuillEditorView(document: selectedDocument, editorIndex: 1), ), @@ -908,15 +1143,12 @@ class ProjectWorkspace extends StatelessWidget { ); } - // ✅ Макет с двумя редакторами Widget _buildTwoEditorsLayout(BuildContext context, AppState state, Color borderColor, Color textColor) { return Row( children: [ - // ✅ Первый редактор (50% ширины) с toolbar Expanded( child: Column( children: [ - // Toolbar первого редактора Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), color: state.isDarkMode ? Colors.grey[850] : Colors.grey[100], @@ -939,7 +1171,6 @@ class ProjectWorkspace extends StatelessWidget { ], ), ), - // Контент первого редактора Expanded( child: Container( decoration: BoxDecoration(border: Border(right: BorderSide(color: borderColor))), @@ -951,7 +1182,6 @@ class ProjectWorkspace extends StatelessWidget { ], ), ), - // ✅ Второй редактор (50% ширины) с toolbar Expanded( child: Column( children: [ @@ -1045,6 +1275,258 @@ class ProjectWorkspace extends StatelessWidget { } } +// ==================== ПАНЕЛЬ ГЛОССАРИЯ ==================== +class _GlossaryPanel extends StatelessWidget { + const _GlossaryPanel(); + + @override + Widget build(BuildContext context) { + final state = context.watch(); + final document = state.selectedDocument; + final isDarkMode = state.isDarkMode; + final textColor = isDarkMode ? Colors.white : Colors.black87; + final borderColor = isDarkMode ? Colors.grey[700]! : Colors.grey[300]!; + + if (document == null) { + return Container( + width: 300, + color: isDarkMode ? Colors.grey[900] : Colors.grey[50], + child: const Center(child: Text('Выберите документ')), + ); + } + + return Container( + width: 300, + decoration: BoxDecoration( + border: Border(left: BorderSide(color: borderColor)), + color: isDarkMode ? Colors.grey[900] : Colors.grey[50], + ), + child: Column( + children: [ + // ✅ Заголовок глоссария — название документа, БЕЗ крестика + Container( + padding: const EdgeInsets.all(12), + color: isDarkMode ? Colors.grey[800] : Colors.grey[200], + child: Row( + children: [ + const Icon(Icons.book, size: 20), + const SizedBox(width: 8), + Expanded( + child: Text( + document.name, // ✅ Название документа + style: const TextStyle(fontWeight: FontWeight.bold), + overflow: TextOverflow.ellipsis, + ), + ), + // ❌ Крестик убран — закрытие только через вкладку справа + ], + ), + ), + + // ✅ Кнопка добавления записи (если есть выделенный текст) + if (state.selectedTextForGlossary != null) + Container( + padding: const EdgeInsets.all(8), + color: isDarkMode ? Colors.green[900]!.withOpacity(0.3) : Colors.green[50], + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Выделенный текст:', + style: TextStyle(fontSize: 12, color: Colors.grey[600]), + ), + const SizedBox(height: 4), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[800] : Colors.white, + border: Border.all(color: Colors.green[700]!), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + state.selectedTextForGlossary!, + style: TextStyle(color: textColor), + ), + ), + const SizedBox(height: 8), + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: () => state.addGlossaryEntry(state.selectedTextForGlossary!), + icon: const Icon(Icons.add, size: 18), + label: const Text('Добавить в глоссарий'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green[700], + foregroundColor: Colors.white, + ), + ), + ), + ], + ), + ), + + // ✅ Список записей глоссария + Expanded( + child: document.glossary.isEmpty + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.book_outlined, size: 48, color: Colors.grey[400]), + const SizedBox(height: 16), + Text( + 'Глоссарий пуст', + style: TextStyle(color: Colors.grey[600]), + ), + const SizedBox(height: 8), + Text( + 'Выделите текст и свайпните влево', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 12, color: Colors.grey[500]), + ), + ], + ), + ) + : ListView.builder( + itemCount: document.glossary.length, + itemBuilder: (context, index) { + final entry = document.glossary[index]; + return _GlossaryEntryTile( + entry: entry, + isDarkMode: isDarkMode, + textColor: textColor, + borderColor: borderColor, + onUpdateDefinition: (definition) => + state.updateGlossaryDefinition(entry.id, definition), + onToggleExpand: () => state.toggleGlossaryEntry(entry.id), + onDelete: () => state.deleteGlossaryEntry(entry.id), + ); + }, + ), + ), + ], + ), + ); + } +} + +class _GlossaryEntryTile extends StatefulWidget { + final GlossaryEntry entry; + final bool isDarkMode; + final Color textColor; + final Color borderColor; + final Function(String) onUpdateDefinition; + final VoidCallback onToggleExpand; + final VoidCallback onDelete; + + const _GlossaryEntryTile({ + required this.entry, + required this.isDarkMode, + required this.textColor, + required this.borderColor, + required this.onUpdateDefinition, + required this.onToggleExpand, + required this.onDelete, + }); + + @override + State<_GlossaryEntryTile> createState() => _GlossaryEntryTileState(); +} + +class _GlossaryEntryTileState extends State<_GlossaryEntryTile> { + late TextEditingController _definitionController; + + @override + void initState() { + super.initState(); + _definitionController = TextEditingController(text: widget.entry.definition); + } + + @override + void dispose() { + _definitionController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + border: Border(bottom: BorderSide(color: widget.borderColor)), + color: widget.entry.isExpanded + ? (widget.isDarkMode ? Colors.blue[900]!.withOpacity(0.2) : Colors.blue[50]) + : Colors.transparent, + ), + child: Column( + children: [ + ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + title: Row( + children: [ + Icon( + widget.entry.isExpanded ? Icons.arrow_drop_down : Icons.arrow_right, + size: 20, + color: widget.textColor, + ), + const SizedBox(width: 4), + Expanded( + child: Text( + widget.entry.term, + style: const TextStyle(fontWeight: FontWeight.w500), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + IconButton( + icon: const Icon(Icons.delete_outline, size: 18), + onPressed: widget.onDelete, + tooltip: 'Удалить', + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + ], + ), + onTap: widget.onToggleExpand, + ), + if (widget.entry.isExpanded) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Определение:', + style: TextStyle( + fontSize: 12, + color: widget.isDarkMode ? Colors.grey[400] : Colors.grey[600], + ), + ), + const SizedBox(height: 8), + TextField( + controller: _definitionController, + maxLines: 5, + minLines: 3, + style: TextStyle(color: widget.textColor), + decoration: InputDecoration( + hintText: 'Введите определение...', + hintStyle: TextStyle(color: Colors.grey[500]), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + filled: true, + fillColor: widget.isDarkMode ? Colors.grey[800] : Colors.white, + ), + onChanged: widget.onUpdateDefinition, + ), + ], + ), + ), + ], + ), + ); + } +} + // ==================== СПИСОК ДОКУМЕНТОВ ==================== class _DocumentsList extends StatelessWidget { const _DocumentsList(); @@ -1112,7 +1594,6 @@ class _DocumentsList extends StatelessWidget { onChanged: (value) => state.toggleDocumentPin(doc), ), const SizedBox(width: 4), - // ✅ Кнопка меню для удаления IconButton( icon: const Icon(Icons.more_vert, size: 20), onPressed: () => _showDeleteMenu(context, doc), @@ -1126,7 +1607,6 @@ class _DocumentsList extends StatelessWidget { ), ], ), - // ✅ Долгое нажатие → сразу второй редактор onTap: () => state.selectDocument(doc), onLongPress: () => state.selectSecondDocument(doc), ), @@ -1236,7 +1716,6 @@ class _DocumentsList extends StatelessWidget { const SizedBox(width: 4), if (doc.parentId != null) Icon(Icons.swipe, size: 16, color: Colors.grey[500]), - // ✅ Кнопка меню для удаления IconButton( icon: const Icon(Icons.more_vert, size: 20), onPressed: () => _showDeleteMenu(context, doc), @@ -1246,7 +1725,6 @@ class _DocumentsList extends StatelessWidget { ), ], ), - // ✅ Долгое нажатие → сразу второй редактор onTap: () => state.selectDocument(doc), onLongPress: () => state.selectSecondDocument(doc), ), @@ -1256,7 +1734,6 @@ class _DocumentsList extends StatelessWidget { ); } - // ✅ Меню с опцией удаления void _showDeleteMenu(BuildContext context, AppDocument doc) { final state = context.read(); final isDarkMode = state.isDarkMode; @@ -1274,7 +1751,6 @@ class _DocumentsList extends StatelessWidget { ], ), onTap: () { - // ✅ Подтверждение удаления showDialog( context: context, builder: (ctx) => AlertDialog( @@ -1364,7 +1840,6 @@ class _DocumentsGraph extends StatelessWidget { children: [ GestureDetector( onTap: () => state.selectDocument(doc), - // ✅ Долгое нажатие → сразу второй редактор onLongPress: () => state.selectSecondDocument(doc), child: Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), @@ -1404,7 +1879,6 @@ class _DocumentsGraph extends StatelessWidget { color: isDarkMode ? Colors.white : Colors.black87, ), ), - // ✅ Кнопка меню для удаления в графе IconButton( icon: const Icon(Icons.more_vert, size: 18), onPressed: () => _showDeleteMenuInGraph(context, doc), @@ -1443,7 +1917,6 @@ class _DocumentsGraph extends StatelessWidget { ); } - // ✅ Меню с опцией удаления для графа void _showDeleteMenuInGraph(BuildContext context, AppDocument doc) { final state = context.read(); final isDarkMode = state.isDarkMode; @@ -1532,46 +2005,119 @@ class _QuillEditorViewState extends State { @override void dispose() { + _controller?.dispose(); super.dispose(); } + // ✅ Получение выделенного текста + String? _getSelectedText() { + if (_controller == null) return null; + + final selection = _controller!.selection; + if (selection.isCollapsed) return null; + + final text = _controller!.document.toPlainText(); + if (selection.baseOffset >= text.length || selection.extentOffset >= text.length) return null; + + final start = selection.baseOffset < selection.extentOffset + ? selection.baseOffset + : selection.extentOffset; + final end = selection.baseOffset < selection.extentOffset + ? selection.extentOffset + : selection.baseOffset; + + final selectedText = text.substring(start, end); + + return selectedText.trim().isNotEmpty ? selectedText.trim() : null; + } + + // ✅ Кнопка: добавить выделенный текст и открыть глоссарий + void _addSelectedToGlossary() { + final selectedText = _getSelectedText(); + if (selectedText != null) { + context.read().addAndOpenGlossary(selectedText); + } else { + // Если нет выделения, просто открыть панель + context.read().toggleGlossaryPanel(); + } + } + + // ✅ Свайп: просто открыть глоссарий + void _handleSwipeLeft() { + final selectedText = _getSelectedText(); + if (selectedText != null) { + // Сохраняем текст для отображения в панели + context.read().setSelectedTextForGlossary(selectedText); + } + context.read().toggleGlossaryPanel(); + } + @override Widget build(BuildContext context) { if (_controller == null) return const Center(child: CircularProgressIndicator()); - return Column( - children: [ - quill.QuillSimpleToolbar( - key: ValueKey('toolbar_${widget.editorIndex}_${widget.document.id}'), - controller: _controller!, - config: const quill.QuillSimpleToolbarConfig( - showBoldButton: true, - showItalicButton: true, - showUnderLineButton: true, - showStrikeThrough: true, - showFontSize: true, - showFontFamily: true, - showColorButton: true, - showBackgroundColorButton: true, - showAlignmentButtons: true, - showListNumbers: true, - showListBullets: true, - showIndent: true, + return GestureDetector( + onPanEnd: (details) { + if (details.velocity.pixelsPerSecond.dx < -500) { + _handleSwipeLeft(); + } + }, + child: Column( + children: [ + Container( + decoration: BoxDecoration( + border: Border(bottom: BorderSide(color: Colors.grey[300]!)), + ), + child: Row( + children: [ + Expanded( + child: quill.QuillSimpleToolbar( + key: ValueKey('toolbar_${widget.editorIndex}_${widget.document.id}'), + controller: _controller!, + config: const quill.QuillSimpleToolbarConfig( + showBoldButton: true, + showItalicButton: true, + showUnderLineButton: true, + showStrikeThrough: true, + showFontSize: true, + showFontFamily: true, + showColorButton: true, + showBackgroundColorButton: true, + showAlignmentButtons: true, + showListNumbers: true, + showListBullets: true, + showIndent: true, + ), + ), + ), + Container( + decoration: BoxDecoration( + border: Border(left: BorderSide(color: Colors.grey[300]!)), + ), + child: IconButton( + icon: const Icon(Icons.book, size: 22), + onPressed: _addSelectedToGlossary, // ✅ Кнопка добавляет и открывает + tooltip: 'Глоссарий', + color: Colors.blue[700], + ), + ), + ], + ), ), - ), - Expanded( - child: quill.QuillEditor( - key: ValueKey('editor_${widget.editorIndex}_${widget.document.id}'), - controller: _controller!, - config: quill.QuillEditorConfig( - placeholder: 'Начните печатать...', - padding: const EdgeInsets.all(16), + Expanded( + child: quill.QuillEditor( + key: ValueKey('editor_${widget.editorIndex}_${widget.document.id}'), + controller: _controller!, + config: quill.QuillEditorConfig( + placeholder: 'Начните печатать...', + padding: const EdgeInsets.all(16), + ), + scrollController: ScrollController(), + focusNode: FocusNode(), ), - scrollController: ScrollController(), - focusNode: FocusNode(), ), - ), - ], + ], + ), ); } } @@ -1670,20 +2216,4 @@ class _MobileEditorView extends StatelessWidget { body: QuillEditorView(document: document), ); } -} - -class TestApp extends StatelessWidget { - const TestApp({super.key}); - - @override - Widget build(BuildContext context) { - return ChangeNotifierProvider( - create: (_) => AppState(), - child: MaterialApp( - title: 'Manyllines Test', - // ✅ Не используем локализации в тестах - они не инициализируются в test environment - home: const AppShell(), - ), - ); - } } \ No newline at end of file diff --git a/manylines_editor/test/widget_test.dart b/manylines_editor/test/widget_test.dart index eafff7d..b2b53aa 100644 --- a/manylines_editor/test/widget_test.dart +++ b/manylines_editor/test/widget_test.dart @@ -7,14 +7,87 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:manylines_editor/main.dart'; // Импортируем и MyApp, и TestApp +import 'package:provider/provider.dart'; +import 'package:manylines_editor/main.dart'; void main() { + // ✅ Простой тест: приложение загружается без крашей testWidgets('App loads without crashing', (WidgetTester tester) async { - // ✅ Используем упрощённый TestApp вместо MyApp + // Запускаем упрощённый TestApp вместо MyApp + await tester.pumpWidget(const TestApp()); + + // Даём время на отрисовку всех виджетов + await tester.pumpAndSettle(); + + // ✅ Проверка 1: приложение содержит Scaffold + expect(find.byType(Scaffold), findsOneWidget); + + // ✅ Проверка 2: заголовок приложения отрисовался + expect(find.text('Manyllines'), findsOneWidget); + + // ✅ Проверка 3: экран проектов загрузился (ищем текст из ProjectsScreen) + expect(find.text('Logo'), findsOneWidget); + }); + + // ✅ Тест: создание проекта работает + testWidgets('Can create a new project', (WidgetTester tester) async { await tester.pumpWidget(const TestApp()); await tester.pumpAndSettle(); + // Находим и нажимаем FAB для создания проекта + final fabFinder = find.byType(FloatingActionButton); + expect(fabFinder, findsOneWidget); + + await tester.tap(fabFinder); + await tester.pumpAndSettle(); + + // Проверяем, что диалог создания проекта открылся + expect(find.text('Новый проект'), findsOneWidget); + + // Вводим название проекта + await tester.enterText(find.byType(TextFormField), 'Test Project'); + await tester.pump(); + + // Нажимаем "Создать" + await tester.tap(find.text('Создать')); + await tester.pumpAndSettle(); + + // Проверяем, что проект появился в списке + expect(find.text('Test Project'), findsOneWidget); + }); + + // ✅ Тест: переключение темы работает (исправленная версия) + testWidgets('Can toggle dark mode', (WidgetTester tester) async { + await tester.pumpWidget(const TestApp()); + await tester.pumpAndSettle(); + + // ✅ Правильный способ получить AppState в тесте: + // Используем Provider.of через контекст виджета + final context = tester.element(find.byType(AppShell).first); + final state = Provider.of(context, listen: false); + + // Меняем тему + state.toggleDarkMode(true); + await tester.pump(); + + // Проверяем, что тема изменилась (проверяем, что состояние обновилось) + expect(state.isDarkMode, isTrue); + }); + + // ✅ Тест: выбор проекта работает + testWidgets('Can select a project', (WidgetTester tester) async { + await tester.pumpWidget(const TestApp()); + await tester.pumpAndSettle(); + + // Находим первый проект в списке + final projectTile = find.text('Project 1'); + expect(projectTile, findsOneWidget); + + // Нажимаем на проект + await tester.tap(projectTile); + await tester.pumpAndSettle(); + // Проверяем, что открылось рабочее пространство + expect(find.byType(ProjectWorkspace), findsOneWidget); }); } \ No newline at end of file From 52f4602049e3c5099cf656541de83e9331eaf290 Mon Sep 17 00:00:00 2001 From: Ulquiorra-Keesre Date: Sun, 12 Apr 2026 15:08:46 +0300 Subject: [PATCH 18/22] 16:9 --- manylines_editor/lib/main.dart | 336 +++++++++++++++++---------------- 1 file changed, 178 insertions(+), 158 deletions(-) diff --git a/manylines_editor/lib/main.dart b/manylines_editor/lib/main.dart index c0e0b7d..ee80ee6 100644 --- a/manylines_editor/lib/main.dart +++ b/manylines_editor/lib/main.dart @@ -536,176 +536,196 @@ class ProjectsScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - body: Column( - children: [ - Consumer( - builder: (context, state, _) { - final headerBg = state.isDarkMode ? Colors.grey[850] : Colors.white; - final logoBg = state.isDarkMode ? Colors.grey[800] : Colors.white; - final borderColor = state.isDarkMode ? Colors.grey[700]! : Colors.grey[300]!; - final logoBorderColor = state.isDarkMode ? Colors.grey[600]! : Colors.grey[400]!; - final textColor = state.isDarkMode ? Colors.white : Colors.black87; - final logoTextColor = state.isDarkMode ? Colors.white : Colors.black; - return Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - decoration: BoxDecoration(border: Border(bottom: BorderSide(color: borderColor)), color: headerBg), - child: Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - border: Border.all(color: logoBorderColor), - color: logoBg, - borderRadius: BorderRadius.circular(4), - ), - child: Text('Logo', style: TextStyle(fontWeight: FontWeight.bold, color: logoTextColor)), - ), - const SizedBox(width: 12), - Expanded(child: Text('Manyllines', style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: textColor))), - ], - ), - ); - }, + body: Center( + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width * 0.7, + maxHeight: MediaQuery.of(context).size.height * 0.9, // ✅ Изменил на 90% высоты экрана ), - Consumer( - builder: (context, state, _) { - final bgColor = state.isDarkMode ? Colors.green[900] : Colors.green[50]; - final borderColor = state.isDarkMode ? const Color.fromARGB(255, 0, 47, 22) : Colors.green.shade200; - final textColor = state.isDarkMode ? Colors.white : Colors.black87; - if (state.switchableValue) { - return ReorderableListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: state.projects.length, - onReorder: state.reorderProjects, - itemBuilder: (context, index) { - final project = state.projects[index]; - return Container( - key: ValueKey(project.id), - decoration: BoxDecoration(color: bgColor, border: Border(bottom: BorderSide(color: borderColor))), - child: ListTile( - title: Text(project.name, style: TextStyle(color: textColor)), - subtitle: Text('${project.documents.length} документов', style: TextStyle(fontSize: 12, color: textColor.withOpacity(0.7))), - trailing: Icon(Icons.drag_handle, color: state.isDarkMode ? Colors.white54 : Colors.grey), - onTap: () => state.selectProject(project), - ), - ); - }, - ); - } else { - return Container( - decoration: BoxDecoration(color: bgColor, border: Border(bottom: BorderSide(color: borderColor))), - child: ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: state.projects.length, - itemBuilder: (context, index) { - final project = state.projects[index]; - return Container( - key: ValueKey(project.id), - decoration: BoxDecoration(border: Border(bottom: BorderSide(color: borderColor))), - child: ListTile( - title: Text(project.name, style: TextStyle(color: textColor)), - subtitle: Text('${project.documents.length} документов', style: TextStyle(fontSize: 12, color: textColor.withOpacity(0.7))), - onTap: () => state.selectProject(project), - ), - ); - }, - ), - ); - } - }, - ), - Consumer( - builder: (context, state, _) { - final bgColor = state.isDarkMode ? Colors.blue[900] : Colors.blue[50]; - final borderColor = state.isDarkMode ? Colors.blue[700] : Colors.blue[200]; - final textColor = state.isDarkMode ? Colors.white : Colors.black87; - final settings = state.settings; - if (state.switchableValue) { - return ReorderableListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: settings.length, - onReorder: state.reorderSettings, - itemBuilder: (context, index) { - final setting = settings[index]; - final isExpanded = setting['expanded'] ?? false; - return Column( - key: ValueKey(setting['id']), - mainAxisSize: MainAxisSize.min, - children: [ - Container( - decoration: BoxDecoration(border: Border(bottom: BorderSide(color: borderColor!)), color: state.isDarkMode ? Colors.blue[800] : Colors.white), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(setting['name'], style: TextStyle(color: textColor)), - Row( - children: [ - IconButton(icon: Icon(isExpanded ? Icons.arrow_drop_down : Icons.arrow_drop_up, color: textColor), onPressed: () => state.toggleSettingExpansion(setting['id'])), - if (state.switchableValue) Icon(Icons.drag_handle, color: state.isDarkMode ? Colors.white54 : Colors.grey), - ], - ), - ], - ), - ), - if (setting['id'] == 'setting1' && isExpanded) _buildDescriptionSection1(state.isDarkMode), - if (setting['id'] == 'setting2' && isExpanded) _buildDescriptionSection2(state.isDarkMode), - if (setting['id'] == 'setting3' && isExpanded) _buildDescriptionSection3(state.isDarkMode), - ], - ); - }, - ); - } else { - return ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: settings.length, - itemBuilder: (context, index) { - final setting = settings[index]; - final isExpanded = setting['expanded'] ?? false; - return Column( - key: ValueKey(setting['id']), - mainAxisSize: MainAxisSize.min, + child: Column( + children: [ + Consumer( + builder: (context, state, _) { + final headerBg = state.isDarkMode ? Colors.grey[850] : Colors.white; + final logoBg = state.isDarkMode ? Colors.grey[800] : Colors.white; + final borderColor = state.isDarkMode ? Colors.grey[700]! : Colors.grey[300]!; + final logoBorderColor = state.isDarkMode ? Colors.grey[600]! : Colors.grey[400]!; + final textColor = state.isDarkMode ? Colors.white : Colors.black87; + final logoTextColor = state.isDarkMode ? Colors.white : Colors.black; + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration(border: Border(bottom: BorderSide(color: borderColor)), color: headerBg), + child: Row( children: [ Container( - decoration: BoxDecoration(border: Border(bottom: BorderSide(color: borderColor!)), color: state.isDarkMode ? Colors.blue[800] : Colors.white), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(setting['name'], style: TextStyle(color: textColor)), - IconButton(icon: Icon(isExpanded ? Icons.arrow_drop_down : Icons.arrow_drop_up, color: textColor), onPressed: () => state.toggleSettingExpansion(setting['id'])), - ], + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + border: Border.all(color: logoBorderColor), + color: logoBg, + borderRadius: BorderRadius.circular(4), ), + child: Text('Logo', style: TextStyle(fontWeight: FontWeight.bold, color: logoTextColor)), ), - if (setting['id'] == 'setting1' && isExpanded) _buildDescriptionSection1(state.isDarkMode), - if (setting['id'] == 'setting2' && isExpanded) _buildDescriptionSection2(state.isDarkMode), - if (setting['id'] == 'setting3' && isExpanded) _buildDescriptionSection3(state.isDarkMode), + const SizedBox(width: 12), + Expanded(child: Text('Manyllines', style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: textColor))), ], - ); - }, - ); - } - }, - ), - Consumer( - builder: (context, state, _) { - final bgColor = state.isDarkMode ? const Color.fromARGB(255, 6, 58, 137) : Colors.blue[50]; - final textColor = state.isDarkMode ? Colors.white54 : Colors.black54; - return Expanded( - child: Container(color: bgColor, padding: const EdgeInsets.all(16), child: Center(child: Text('Other Settings ...', style: TextStyle(color: textColor)))), - ); - }, + ), + ); + }, + ), + // ✅ Оборачиваем прокручиваемый контент + Expanded( + child: SingleChildScrollView( + child: Column( + children: [ + Consumer( + builder: (context, state, _) { + final bgColor = state.isDarkMode ? Colors.green[900] : Colors.green[50]; + final borderColor = state.isDarkMode ? const Color.fromARGB(255, 0, 47, 22) : Colors.green.shade200; + final textColor = state.isDarkMode ? Colors.white : Colors.black87; + if (state.switchableValue) { + return ReorderableListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: state.projects.length, + onReorder: state.reorderProjects, + itemBuilder: (context, index) { + final project = state.projects[index]; + return Container( + key: ValueKey(project.id), + decoration: BoxDecoration(color: bgColor, border: Border(bottom: BorderSide(color: borderColor))), + child: ListTile( + title: Text(project.name, style: TextStyle(color: textColor)), + subtitle: Text('${project.documents.length} документов', style: TextStyle(fontSize: 12, color: textColor.withOpacity(0.7))), + trailing: Icon(Icons.drag_handle, color: state.isDarkMode ? Colors.white54 : Colors.grey), + onTap: () => state.selectProject(project), + ), + ); + }, + ); + } else { + return Container( + decoration: BoxDecoration(color: bgColor, border: Border(bottom: BorderSide(color: borderColor))), + child: ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: state.projects.length, + itemBuilder: (context, index) { + final project = state.projects[index]; + return Container( + key: ValueKey(project.id), + decoration: BoxDecoration(border: Border(bottom: BorderSide(color: borderColor))), + child: ListTile( + title: Text(project.name, style: TextStyle(color: textColor)), + subtitle: Text('${project.documents.length} документов', style: TextStyle(fontSize: 12, color: textColor.withOpacity(0.7))), + onTap: () => state.selectProject(project), + ), + ); + }, + ), + ); + } + }, + ), + Consumer( + builder: (context, state, _) { + final bgColor = state.isDarkMode ? Colors.blue[900] : Colors.blue[50]; + final borderColor = state.isDarkMode ? Colors.blue[700] : Colors.blue[200]; + final textColor = state.isDarkMode ? Colors.white : Colors.black87; + final settings = state.settings; + if (state.switchableValue) { + return ReorderableListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: settings.length, + onReorder: state.reorderSettings, + itemBuilder: (context, index) { + final setting = settings[index]; + final isExpanded = setting['expanded'] ?? false; + return Column( + key: ValueKey(setting['id']), + mainAxisSize: MainAxisSize.min, + children: [ + Container( + decoration: BoxDecoration(border: Border(bottom: BorderSide(color: borderColor!)), color: state.isDarkMode ? Colors.blue[800] : Colors.white), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(setting['name'], style: TextStyle(color: textColor)), + Row( + children: [ + IconButton(icon: Icon(isExpanded ? Icons.arrow_drop_down : Icons.arrow_drop_up, color: textColor), onPressed: () => state.toggleSettingExpansion(setting['id'])), + if (state.switchableValue) Icon(Icons.drag_handle, color: state.isDarkMode ? Colors.white54 : Colors.grey), + ], + ), + ], + ), + ), + if (setting['id'] == 'setting1' && isExpanded) _buildDescriptionSection1(state.isDarkMode), + if (setting['id'] == 'setting2' && isExpanded) _buildDescriptionSection2(state.isDarkMode), + if (setting['id'] == 'setting3' && isExpanded) _buildDescriptionSection3(state.isDarkMode), + ], + ); + }, + ); + } else { + return ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: settings.length, + itemBuilder: (context, index) { + final setting = settings[index]; + final isExpanded = setting['expanded'] ?? false; + return Column( + key: ValueKey(setting['id']), + mainAxisSize: MainAxisSize.min, + children: [ + Container( + decoration: BoxDecoration(border: Border(bottom: BorderSide(color: borderColor!)), color: state.isDarkMode ? Colors.blue[800] : Colors.white), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(setting['name'], style: TextStyle(color: textColor)), + IconButton(icon: Icon(isExpanded ? Icons.arrow_drop_down : Icons.arrow_drop_up, color: textColor), onPressed: () => state.toggleSettingExpansion(setting['id'])), + ], + ), + ), + if (setting['id'] == 'setting1' && isExpanded) _buildDescriptionSection1(state.isDarkMode), + if (setting['id'] == 'setting2' && isExpanded) _buildDescriptionSection2(state.isDarkMode), + if (setting['id'] == 'setting3' && isExpanded) _buildDescriptionSection3(state.isDarkMode), + ], + ); + }, + ); + } + }, + ), + Consumer( + builder: (context, state, _) { + final bgColor = state.isDarkMode ? const Color.fromARGB(255, 6, 58, 137) : Colors.blue[50]; + final textColor = state.isDarkMode ? Colors.white54 : Colors.black54; + return Container( + color: bgColor, + padding: const EdgeInsets.all(16), + child: Center(child: Text('Other Settings ...', style: TextStyle(color: textColor))), + ); + }, + ), + ], + ), + ), + ), + ], ), - ], + ), ), floatingActionButton: FloatingActionButton(onPressed: () => _showCreateProjectDialog(context), child: const Icon(Icons.add)), ); } + void _showCreateProjectDialog(BuildContext context) { final controller = TextEditingController(); final formKey = GlobalKey(); From 61a182928c91add5ab1da89b3f89e6a1311b9d36 Mon Sep 17 00:00:00 2001 From: Ulquiorra-Keesre Date: Sun, 12 Apr 2026 17:12:18 +0300 Subject: [PATCH 19/22] 16:9 2 --- manylines_editor/lib/main.dart | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/manylines_editor/lib/main.dart b/manylines_editor/lib/main.dart index ee80ee6..27a2ded 100644 --- a/manylines_editor/lib/main.dart +++ b/manylines_editor/lib/main.dart @@ -2236,4 +2236,19 @@ class _MobileEditorView extends StatelessWidget { body: QuillEditorView(document: document), ); } +} + +class TestApp extends StatelessWidget { + const TestApp({super.key}); + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (_) => AppState(), + child: MaterialApp( + title: 'Manyllines Test', + home: const AppShell(), + ), + ); + } } \ No newline at end of file From e2d273289b8fc7e501ab5967816860b1de9e5809 Mon Sep 17 00:00:00 2001 From: Ulquiorra-Keesre Date: Sun, 12 Apr 2026 17:16:03 +0300 Subject: [PATCH 20/22] something --- manylines_editor/pubspec.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/manylines_editor/pubspec.lock b/manylines_editor/pubspec.lock index 3cf1fef..798ce27 100644 --- a/manylines_editor/pubspec.lock +++ b/manylines_editor/pubspec.lock @@ -111,10 +111,10 @@ packages: dependency: transitive description: name: matcher - sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.19" + version: "0.12.18" material_color_utilities: dependency: transitive description: @@ -188,10 +188,10 @@ packages: dependency: transitive description: name: test_api - sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.7.9" vector_math: dependency: transitive description: From 598ae4bdf1101ef809f09cc0a5e49d26306db793 Mon Sep 17 00:00:00 2001 From: Ulquiorra-Keesre Date: Tue, 14 Apr 2026 16:25:51 +0300 Subject: [PATCH 21/22] FSD complete --- manylines_editor/lib/app/providers.dart | 11 +- .../lib/entities/document/document.dart | 7 +- .../document/document_repository.dart | 197 +- .../entities/project/project_repository.dart | 31 +- .../features/document/create_document.dart | 7 + .../features/document/delete_document.dart | 14 +- .../features/document/outdent_document.dart | 12 +- .../editor/handle_text_selection.dart | 2 +- .../lib/features/glossary/add_entry.dart | 2 +- manylines_editor/lib/main.dart | 2258 +---------------- .../pages/projects/widgets/settings_list.dart | 4 +- .../pages/workspace/widgets/editor_area.dart | 12 +- .../widgets/glossary_entry_tile.dart | 17 +- .../workspace/widgets/glossary_panel.dart | 66 +- .../pages/workspace/widgets/side_panel.dart | 393 ++- .../lib/pages/workspace/workspace_page.dart | 286 ++- .../lib/widgets/quill_editor_wrapper.dart | 110 +- manylines_editor/test/widget_test.dart | 68 +- 18 files changed, 1059 insertions(+), 2438 deletions(-) diff --git a/manylines_editor/lib/app/providers.dart b/manylines_editor/lib/app/providers.dart index c6f7996..c02aac5 100644 --- a/manylines_editor/lib/app/providers.dart +++ b/manylines_editor/lib/app/providers.dart @@ -11,11 +11,16 @@ class AppProviders extends StatelessWidget { @override Widget build(BuildContext context) { + + final projectRepo = ProjectRepository(); + final documentRepo = DocumentRepository(projectRepo); + final settingRepo = SettingRepository(); + return MultiProvider( providers: [ - ChangeNotifierProvider(create: (_) => ProjectRepository()), - ChangeNotifierProvider(create: (_) => DocumentRepository()), - ChangeNotifierProvider(create: (_) => SettingRepository()), + ChangeNotifierProvider.value(value: projectRepo), + ChangeNotifierProvider.value(value: documentRepo), + ChangeNotifierProvider.value(value: settingRepo), ], child: child, ); diff --git a/manylines_editor/lib/entities/document/document.dart b/manylines_editor/lib/entities/document/document.dart index 8cf26cd..7138006 100644 --- a/manylines_editor/lib/entities/document/document.dart +++ b/manylines_editor/lib/entities/document/document.dart @@ -8,7 +8,7 @@ class AppDocument { bool isPinned; String? parentId; Delta content; - List glossary; + List glossary; // ✅ Изменяемый список AppDocument({ required this.id, @@ -17,8 +17,9 @@ class AppDocument { this.isPinned = false, this.parentId, required this.content, - this.glossary = const [], - }); + List? glossary, // ✅ Изменили на nullable + }) : glossary = glossary ?? []; // ✅ Создаём mutable список + bool get isChild => parentId != null; } \ No newline at end of file diff --git a/manylines_editor/lib/entities/document/document_repository.dart b/manylines_editor/lib/entities/document/document_repository.dart index b1dcfe1..a25f1a2 100644 --- a/manylines_editor/lib/entities/document/document_repository.dart +++ b/manylines_editor/lib/entities/document/document_repository.dart @@ -4,12 +4,20 @@ import 'package:flutter_quill/flutter_quill.dart' as quill; import 'package:dart_quill_delta/dart_quill_delta.dart'; import 'document.dart'; import '../glossary_entry/glossary_entry.dart'; +import '../project/project_repository.dart'; class DocumentRepository extends ChangeNotifier { + final ProjectRepository _projectRepo; + AppDocument? _selectedDocument; AppDocument? _secondSelectedDocument; bool _isGlossaryPanelOpen = false; String? _selectedTextForGlossary; + + // ✅ Хранилище контроллеров + final Map _controllers = {}; + + DocumentRepository(this._projectRepo); AppDocument? get selectedDocument => _selectedDocument; AppDocument? get secondSelectedDocument => _secondSelectedDocument; @@ -17,47 +25,65 @@ class DocumentRepository extends ChangeNotifier { String? get selectedTextForGlossary => _selectedTextForGlossary; AppDocument createDocument({ - required String name, - required Delta content, - String? parentId, - }) { - return AppDocument( - id: 'd${DateTime.now().millisecondsSinceEpoch}', - name: name, - content: content, - parentId: parentId, - ); - } - - quill.QuillController getOrCreateController(String documentId) { - final doc = _findDocument(documentId); - if (doc == null) { - return quill.QuillController( - document: quill.Document(), - selection: const TextSelection.collapsed(offset: 0), - ); + required String name, + required Delta content, + String? parentId, +}) { + return AppDocument( + id: 'd${DateTime.now().millisecondsSinceEpoch}', + name: name, + content: content, + parentId: parentId, + glossary: [], // ✅ Явно передаём mutable список + ); +} + + // ✅ Создаём или получаем контроллер + quill.QuillController getOrCreateController(AppDocument document) { + if (_controllers.containsKey(document.id)) { + return _controllers[document.id]!; } final controller = quill.QuillController( - document: quill.Document.fromJson(doc.content.toJson()), + document: quill.Document.fromJson(document.content.toJson()), selection: const TextSelection.collapsed(offset: 0), ); + // ✅ Сохраняем изменения в документ controller.changes.listen((change) { - doc.content = controller.document.toDelta(); - notifyListeners(); + document.content = controller.document.toDelta(); }); + _controllers[document.id] = controller; return controller; } + // ✅ Вызывать только при удалении документа или закрытии приложения + void disposeController(String documentId) { + final controller = _controllers[documentId]; + if (controller != null) { + controller.dispose(); + _controllers.remove(documentId); + } + } + + // ✅ Вызывать при закрытии приложения + void disposeAll() { + for (var controller in _controllers.values) { + controller.dispose(); + } + _controllers.clear(); + } + void selectDocument(AppDocument document) { _selectedDocument = document; + incrementViewCount(document); notifyListeners(); } void selectSecondDocument(AppDocument document) { _secondSelectedDocument = document; + incrementViewCount(document); notifyListeners(); } @@ -83,58 +109,129 @@ class DocumentRepository extends ChangeNotifier { } void togglePin(AppDocument doc) { - doc.isPinned = !doc.isPinned; - notifyListeners(); - } + doc.isPinned = !doc.isPinned; + notifyListeners(); // ✅ Обязательно! +} void indentDocument(String documentId, String parentId) { - final doc = _findDocument(documentId); - if (doc != null) { - doc.parentId = parentId; - notifyListeners(); - } + final project = _projectRepo.selectedProject; + if (project == null) return; + + final doc = project.documents.firstWhere((d) => d.id == documentId); + doc.parentId = parentId; + notifyListeners(); } - void outdentDocument(int index) { + void outdentDocument(String documentId) { + final project = _projectRepo.selectedProject; + if (project == null) return; + + final doc = project.documents.firstWhere((d) => d.id == documentId); + doc.parentId = null; notifyListeners(); } - void addGlossaryEntry(String documentId, GlossaryEntry entry) { - final doc = _findDocument(documentId); - if (doc != null) { - doc.glossary.add(entry); - notifyListeners(); - } + void toggleGlossaryPanel() { + _isGlossaryPanelOpen = !_isGlossaryPanelOpen; + notifyListeners(); } - void updateGlossaryDefinition(String entryId, String definition) { + void setSelectedTextForGlossary(String text) { + _selectedTextForGlossary = text; notifyListeners(); } - void toggleGlossaryEntry(String entryId) { + void clearSelectedTextForGlossary() { + _selectedTextForGlossary = null; notifyListeners(); } - void deleteGlossaryEntry(String entryId) { + void addGlossaryEntry(String documentId, String term) { + print('🔍 addGlossaryEntry вызван'); + print(' - documentId: $documentId'); + print(' - term: $term'); + + final project = _projectRepo.selectedProject; + print(' - selectedProject: ${project?.name ?? "null"}'); + + if (project == null) { + print('❌ Проект не выбран'); + return; + } + + try { + final doc = project.documents.firstWhere((d) => d.id == documentId); + print(' - Найден документ: ${doc.name}'); + print(' - Глоссарий до: ${doc.glossary.length} записей'); + + final entry = GlossaryEntry( + id: 'g${DateTime.now().millisecondsSinceEpoch}', + term: term, + definition: '', + isExpanded: true, + ); + + doc.glossary.add(entry); + print(' - Глоссарий после: ${doc.glossary.length} записей'); + print('✅ Термин добавлен успешно'); + notifyListeners(); + } catch (e) { + print('❌ Ошибка: $e'); } +} - void toggleGlossaryPanel() { - _isGlossaryPanelOpen = !_isGlossaryPanelOpen; - notifyListeners(); + void updateGlossaryDefinition(String entryId, String definition) { + final project = _projectRepo.selectedProject; + if (project == null) return; + + // ✅ Ищем во ВСЕХ документах проекта + for (var doc in project.documents) { + final index = doc.glossary.indexWhere((e) => e.id == entryId); + if (index != -1) { + // ✅ Обновляем только найденную запись + doc.glossary[index].definition = definition; + notifyListeners(); + return; // ✅ Выходим после первого нахождения + } } +} - void setSelectedTextForGlossary(String text) { - _selectedTextForGlossary = text; - notifyListeners(); + // lib/entities/document/document_repository.dart + +void toggleGlossaryEntry(String entryId) { + final project = _projectRepo.selectedProject; + if (project == null) return; + + // ✅ Ищем во ВСЕХ документах + for (var doc in project.documents) { + final index = doc.glossary.indexWhere((e) => e.id == entryId); + if (index != -1) { + // ✅ Переключаем только найденную запись + doc.glossary[index].isExpanded = !doc.glossary[index].isExpanded; + notifyListeners(); + return; // ✅ Выходим после первого нахождения + } } +} - void clearSelectedTextForGlossary() { - _selectedTextForGlossary = null; +void openGlossaryPanel() { + _isGlossaryPanelOpen = true; + notifyListeners(); +} + + void deleteGlossaryEntry(String entryId) { + final project = _projectRepo.selectedProject; + if (project == null) return; + + for (var doc in project.documents) { + doc.glossary.removeWhere((e) => e.id == entryId); + } notifyListeners(); } - AppDocument? _findDocument(String id) { - return null; + // ✅ Вызывать при удалении документа + void deleteDocumentControllers(String documentId) { + disposeController(documentId); } } \ No newline at end of file diff --git a/manylines_editor/lib/entities/project/project_repository.dart b/manylines_editor/lib/entities/project/project_repository.dart index ac6e7eb..ffb5a5e 100644 --- a/manylines_editor/lib/entities/project/project_repository.dart +++ b/manylines_editor/lib/entities/project/project_repository.dart @@ -41,20 +41,43 @@ class ProjectRepository extends ChangeNotifier { notifyListeners(); } + void reorderPinnedDocuments(int oldIndex, int newIndex) { + if (_selectedProject == null) return; + + final pinnedDocs = _selectedProject!.pinnedDocuments; + if (newIndex > oldIndex) newIndex -= 1; + + final doc = pinnedDocs.removeAt(oldIndex); + final targetDoc = pinnedDocs[newIndex]; + + final docMainIndex = _selectedProject!.documents.indexOf(doc); + final targetMainIndex = _selectedProject!.documents.indexOf(targetDoc); + + _selectedProject!.documents.removeAt(docMainIndex); + _selectedProject!.documents.insert(targetMainIndex, doc); + + notifyListeners(); + } + void toggleViewMode() { _isGraphView = !_isGraphView; notifyListeners(); } void addDocumentToProject(AppDocument document) { - if (_selectedProject == null) return; - _selectedProject!.documents.add(document); - notifyListeners(); - } + if (_selectedProject == null) return; + _selectedProject!.documents.add(document); + notifyListeners(); +} void deleteDocument(AppDocument doc) { if (_selectedProject == null) return; _selectedProject!.documents.remove(doc); notifyListeners(); } + + void togglePin(AppDocument doc) { + doc.isPinned = !doc.isPinned; + notifyListeners(); // ✅ Уведомляем о изменении +} } \ No newline at end of file diff --git a/manylines_editor/lib/features/document/create_document.dart b/manylines_editor/lib/features/document/create_document.dart index 4644e53..50a4f86 100644 --- a/manylines_editor/lib/features/document/create_document.dart +++ b/manylines_editor/lib/features/document/create_document.dart @@ -29,6 +29,12 @@ class CreateDocumentFeature { if (value == null || value.trim().isEmpty) return 'Введите название документа'; return null; }, + onFieldSubmitted: (_) { + if (formKey.currentState!.validate()) { + execute(context, controller.text.trim()); + Navigator.pop(context); + } + }, ), ), actions: [ @@ -67,6 +73,7 @@ class CreateDocumentFeature { ); projectRepo.addDocumentToProject(newDoc); + documentRepo.selectDocument(newDoc); } } \ No newline at end of file diff --git a/manylines_editor/lib/features/document/delete_document.dart b/manylines_editor/lib/features/document/delete_document.dart index 2cadfff..35da7a3 100644 --- a/manylines_editor/lib/features/document/delete_document.dart +++ b/manylines_editor/lib/features/document/delete_document.dart @@ -35,10 +35,12 @@ class DeleteDocumentFeature { } static void execute(BuildContext context, AppDocument doc) { - final projectRepo = Provider.of(context, listen: false); - final documentRepo = Provider.of(context, listen: false); - - projectRepo.deleteDocument(doc); - documentRepo.closeEditorIfOpen(doc.id); - } + final projectRepo = Provider.of(context, listen: false); + final documentRepo = Provider.of(context, listen: false); + + documentRepo.deleteDocumentControllers(doc.id); + + projectRepo.deleteDocument(doc); + documentRepo.closeEditorIfOpen(doc.id); +} } \ No newline at end of file diff --git a/manylines_editor/lib/features/document/outdent_document.dart b/manylines_editor/lib/features/document/outdent_document.dart index 58b2f18..ca10dff 100644 --- a/manylines_editor/lib/features/document/outdent_document.dart +++ b/manylines_editor/lib/features/document/outdent_document.dart @@ -1,10 +1,18 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../entities/document/document_repository.dart'; +import '../../entities/project/project_repository.dart'; class OutdentDocumentFeature { static void execute(BuildContext context, int index) { - final repo = Provider.of(context, listen: false); - repo.outdentDocument(index); + final projectRepo = Provider.of(context, listen: false); + final documentRepo = Provider.of(context, listen: false); + + if (projectRepo.selectedProject == null) return; + + final docs = projectRepo.selectedProject!.documents; + if (index >= 0 && index < docs.length) { + documentRepo.outdentDocument(docs[index].id); + } } } \ No newline at end of file diff --git a/manylines_editor/lib/features/editor/handle_text_selection.dart b/manylines_editor/lib/features/editor/handle_text_selection.dart index 1f3b82b..add81b6 100644 --- a/manylines_editor/lib/features/editor/handle_text_selection.dart +++ b/manylines_editor/lib/features/editor/handle_text_selection.dart @@ -23,7 +23,7 @@ class HandleTextSelectionFeature { if (selectedText.trim().isNotEmpty) { final repo = Provider.of(context, listen: false); repo.setSelectedTextForGlossary(selectedText.trim()); - repo.toggleGlossaryPanel(); + repo.openGlossaryPanel(); } } } \ No newline at end of file diff --git a/manylines_editor/lib/features/glossary/add_entry.dart b/manylines_editor/lib/features/glossary/add_entry.dart index 30f4c00..4a050df 100644 --- a/manylines_editor/lib/features/glossary/add_entry.dart +++ b/manylines_editor/lib/features/glossary/add_entry.dart @@ -18,6 +18,6 @@ class AddGlossaryEntryFeature { isExpanded: true, ); - repo.addGlossaryEntry(documentId, newEntry); + repo.addGlossaryEntry(documentId, term); } } \ No newline at end of file diff --git a/manylines_editor/lib/main.dart b/manylines_editor/lib/main.dart index 27a2ded..81d6803 100644 --- a/manylines_editor/lib/main.dart +++ b/manylines_editor/lib/main.dart @@ -1,2254 +1,34 @@ import 'package:flutter/material.dart'; -import 'package:flutter_quill/flutter_quill.dart' as quill; import 'package:provider/provider.dart'; -import 'package:dart_quill_delta/dart_quill_delta.dart'; -import 'package:flutter_localizations/flutter_localizations.dart'; +import 'app/providers.dart'; +import 'app/theme.dart'; +import 'app/router.dart'; +import 'entities/setting/setting_repository.dart'; -// ==================== МОДЕЛИ ==================== -class GlossaryEntry { - final String id; - final String term; - String definition; - bool isExpanded; - DateTime createdAt; - - GlossaryEntry({ - required this.id, - required this.term, - this.definition = '', - this.isExpanded = false, - DateTime? createdAt, - }) : createdAt = createdAt ?? DateTime.now(); -} - -class Project { - final String id; - final String name; - final List documents; - - Project({ - required this.id, - required this.name, - required this.documents, - }); - - int get maxViewCount { - if (documents.isEmpty) return 0; - return documents.map((doc) => doc.viewCount).reduce((a, b) => a > b ? a : b); - } - - bool isDocumentMostUsed(AppDocument doc) { - return doc.viewCount >= maxViewCount && maxViewCount > 0; - } - - List get pinnedDocuments => - documents.where((doc) => doc.isPinned).toList(); - - List get unpinnedDocuments => - documents.where((doc) => !doc.isPinned).toList(); -} - -class AppDocument { - final String id; - final String name; - int viewCount; - bool isPinned; - String? parentId; - Delta content; - List glossary; - - AppDocument({ - required this.id, - required this.name, - this.viewCount = 0, - this.isPinned = false, - this.parentId, - required this.content, - this.glossary = const [], - }); - - bool get isChild => parentId != null; -} - -// ==================== STATE ==================== -class AppState extends ChangeNotifier { - final List _projects = [ - Project(id: 'p1', name: 'Project 1', documents: []), - Project(id: 'p2', name: 'Project 2', documents: []), - ]; - - List> _settings = [ - {'id': 'setting1', 'name': 'Setting 1', 'expanded': true, 'enabled': false}, - {'id': 'setting2', 'name': 'Setting 2', 'expanded': false, 'enabled': false}, - {'id': 'setting3', 'name': 'Setting 3', 'expanded': true, 'enabled': false}, - ]; - - bool _switchableValue = true; - Project? _selectedProject; - AppDocument? _selectedDocument; - AppDocument? _secondSelectedDocument; - bool _isDarkMode = false; - bool _isGraphView = false; - bool _isSidePanelCollapsed = false; - bool _isGlossaryPanelOpen = false; - String? _selectedTextForGlossary; - - List get projects => _projects; - List> get settings => _settings; - bool get switchableValue => _switchableValue; - Project? get selectedProject => _selectedProject; - AppDocument? get selectedDocument => _selectedDocument; - AppDocument? get secondSelectedDocument => _secondSelectedDocument; - bool get isDarkMode => _isDarkMode; - bool get isGraphView => _isGraphView; - bool get isSidePanelCollapsed => _isSidePanelCollapsed; - bool get isGlossaryPanelOpen => _isGlossaryPanelOpen; - String? get selectedTextForGlossary => _selectedTextForGlossary; - - void toggleDarkMode(bool value) { - _isDarkMode = value; - notifyListeners(); - } - - void closeFirstEditor() { - _selectedDocument = null; - notifyListeners(); - } - - void toggleViewMode() { - _isGraphView = !_isGraphView; - notifyListeners(); - } - - void toggleSidePanel() { - _isSidePanelCollapsed = !_isSidePanelCollapsed; - notifyListeners(); - } - - void toggleGlossaryPanel() { - _isGlossaryPanelOpen = !_isGlossaryPanelOpen; - notifyListeners(); - } - - void setSelectedTextForGlossary(String text) { - _selectedTextForGlossary = text; - notifyListeners(); - } - - void clearSelectedTextForGlossary() { - _selectedTextForGlossary = null; - notifyListeners(); - } - - // ✅ Обновить документ в проекте - void _updateDocumentInProject(AppDocument updatedDocument) { - if (_selectedProject == null) return; - - final index = _selectedProject!.documents.indexWhere((d) => d.id == updatedDocument.id); - if (index != -1) { - _selectedProject!.documents[index] = updatedDocument; - notifyListeners(); - } - } - - // ✅ Добавить запись в глоссарий - void addGlossaryEntry(String term) { - if (_selectedDocument == null) return; - - final newEntry = GlossaryEntry( - id: 'g${DateTime.now().millisecondsSinceEpoch}', - term: term, - definition: '', - isExpanded: true, - ); - - final updatedGlossary = List.from(_selectedDocument!.glossary) - ..add(newEntry); - - final updatedDocument = AppDocument( - id: _selectedDocument!.id, - name: _selectedDocument!.name, - viewCount: _selectedDocument!.viewCount, - isPinned: _selectedDocument!.isPinned, - parentId: _selectedDocument!.parentId, - content: _selectedDocument!.content, - glossary: updatedGlossary, - ); - - _selectedDocument = updatedDocument; - _updateDocumentInProject(updatedDocument); - - _selectedTextForGlossary = null; - } - - // ✅ Автоматически добавить и открыть глоссарий - void addAndOpenGlossary(String term) { - addGlossaryEntry(term); - _isGlossaryPanelOpen = true; - notifyListeners(); - } - - void updateGlossaryDefinition(String entryId, String definition) { - if (_selectedDocument == null) return; - - final updatedGlossary = _selectedDocument!.glossary.map((entry) { - if (entry.id == entryId) { - return GlossaryEntry( - id: entry.id, - term: entry.term, - definition: definition, - isExpanded: entry.isExpanded, - createdAt: entry.createdAt, - ); - } - return entry; - }).toList(); - - final updatedDocument = AppDocument( - id: _selectedDocument!.id, - name: _selectedDocument!.name, - viewCount: _selectedDocument!.viewCount, - isPinned: _selectedDocument!.isPinned, - parentId: _selectedDocument!.parentId, - content: _selectedDocument!.content, - glossary: updatedGlossary, - ); - - _selectedDocument = updatedDocument; - _updateDocumentInProject(updatedDocument); - } - - void toggleGlossaryEntry(String entryId) { - if (_selectedDocument == null) return; - - final updatedGlossary = _selectedDocument!.glossary.map((entry) { - if (entry.id == entryId) { - return GlossaryEntry( - id: entry.id, - term: entry.term, - definition: entry.definition, - isExpanded: !entry.isExpanded, - createdAt: entry.createdAt, - ); - } - return entry; - }).toList(); - - final updatedDocument = AppDocument( - id: _selectedDocument!.id, - name: _selectedDocument!.name, - viewCount: _selectedDocument!.viewCount, - isPinned: _selectedDocument!.isPinned, - parentId: _selectedDocument!.parentId, - content: _selectedDocument!.content, - glossary: updatedGlossary, - ); - - _selectedDocument = updatedDocument; - _updateDocumentInProject(updatedDocument); - } - - void deleteGlossaryEntry(String entryId) { - if (_selectedDocument == null) return; - - final updatedGlossary = _selectedDocument!.glossary - .where((entry) => entry.id != entryId) - .toList(); - - final updatedDocument = AppDocument( - id: _selectedDocument!.id, - name: _selectedDocument!.name, - viewCount: _selectedDocument!.viewCount, - isPinned: _selectedDocument!.isPinned, - parentId: _selectedDocument!.parentId, - content: _selectedDocument!.content, - glossary: updatedGlossary, - ); - - _selectedDocument = updatedDocument; - _updateDocumentInProject(updatedDocument); - } - - void incrementViewCount(AppDocument doc) { - doc.viewCount++; - notifyListeners(); - } - - quill.QuillController getOrCreateController(AppDocument doc) { - final controller = quill.QuillController( - document: quill.Document.fromJson(doc.content.toJson()), - selection: const TextSelection.collapsed(offset: 0), - ); - controller.changes.listen((change) { - doc.content = controller.document.toDelta(); - }); - return controller; - } - - void addProject(String name) { - _projects.add(Project( - id: 'p${_projects.length + 1}', - name: name, - documents: [], - )); - notifyListeners(); - } - - void addDocumentToCurrentProject(String name) { - if (_selectedProject == null) return; - final newDoc = AppDocument( - id: 'd${DateTime.now().millisecondsSinceEpoch}', - name: name, - viewCount: 0, - isPinned: false, - parentId: null, - content: Delta()..insert('New document content...\n'), - ); - _selectedProject!.documents.add(newDoc); - _selectedDocument = newDoc; - notifyListeners(); - } - - void toggleDocumentPin(AppDocument doc) { - doc.isPinned = !doc.isPinned; - notifyListeners(); - } - - void reorderPinnedDocuments(int oldIndex, int newIndex) { - if (_selectedProject == null) return; - - final pinnedDocs = _selectedProject!.pinnedDocuments; - if (newIndex > oldIndex) newIndex -= 1; - - final doc = pinnedDocs.removeAt(oldIndex); - final targetDoc = pinnedDocs[newIndex]; - - final docMainIndex = _selectedProject!.documents.indexOf(doc); - final targetMainIndex = _selectedProject!.documents.indexOf(targetDoc); - - _selectedProject!.documents.removeAt(docMainIndex); - _selectedProject!.documents.insert(targetMainIndex, doc); - - notifyListeners(); - } - - void deleteDocument(AppDocument doc) { - if (_selectedProject == null) return; - - _selectedProject!.documents.remove(doc); - - if (_selectedDocument?.id == doc.id) { - _selectedDocument = null; - } - if (_secondSelectedDocument?.id == doc.id) { - _secondSelectedDocument = null; - } - - notifyListeners(); - } - - void indentDocument(int index) { - if (_selectedProject == null || index <= 0) return; - final docs = _selectedProject!.documents; - - AppDocument? parentDoc; - for (int i = index - 1; i >= 0; i--) { - if (docs[i].parentId == null && !docs[i].isPinned) { - parentDoc = docs[i]; - break; - } - } - - if (parentDoc != null) { - docs[index].parentId = parentDoc.id; - notifyListeners(); - } - } - - void outdentDocument(int index) { - if (_selectedProject == null) return; - _selectedProject!.documents[index].parentId = null; - notifyListeners(); - } - - void selectProject(Project project) { - _selectedProject = project; - if (project.documents.isNotEmpty) { - final mostUsed = project.documents.reduce((a, b) => a.viewCount > b.viewCount ? a : b); - _selectedDocument = mostUsed; - incrementViewCount(mostUsed); - } else { - _selectedDocument = null; - } - _isGlossaryPanelOpen = false; - _selectedTextForGlossary = null; - notifyListeners(); - } - - void selectDocument(AppDocument document) { - _selectedDocument = document; - incrementViewCount(document); - _isGlossaryPanelOpen = false; - _selectedTextForGlossary = null; - notifyListeners(); - } - - void selectSecondDocument(AppDocument document) { - _secondSelectedDocument = document; - incrementViewCount(document); - notifyListeners(); - } - - void closeSecondEditor() { - _secondSelectedDocument = null; - notifyListeners(); - } - - void clearSelectedProject() { - _selectedProject = null; - _selectedDocument = null; - _secondSelectedDocument = null; - _isGlossaryPanelOpen = false; - _selectedTextForGlossary = null; - notifyListeners(); - } - - void setSwitchableValue(bool value) { - _switchableValue = value; - notifyListeners(); - } - - void reorderSettings(int oldIndex, int newIndex) { - if (newIndex > oldIndex) newIndex -= 1; - final item = _settings.removeAt(oldIndex); - _settings.insert(newIndex, item); - notifyListeners(); - } - - void reorderProjects(int oldIndex, int newIndex) { - if (newIndex > oldIndex) newIndex -= 1; - final item = _projects.removeAt(oldIndex); - _projects.insert(newIndex, item); - notifyListeners(); - } - - void toggleSettingExpansion(String id) { - for (var setting in _settings) { - if (setting['id'] == id) { - setting['expanded'] = !setting['expanded']; - notifyListeners(); - break; - } - } - } - - void toggleSettingEnabled(String id, bool value) { - for (var setting in _settings) { - if (setting['id'] == id) { - setting['enabled'] = value; - notifyListeners(); - break; - } - } - } - - @override - void dispose() { - super.dispose(); - } - - String? getSelectedTextFromController(quill.QuillController? controller) { - if (controller == null) return null; - - final selection = controller.selection; - if (selection.isCollapsed) return null; - - final text = controller.document.toPlainText(); - if (selection.baseOffset >= text.length || selection.extentOffset >= text.length) return null; - - final start = selection.baseOffset < selection.extentOffset - ? selection.baseOffset - : selection.extentOffset; - final end = selection.baseOffset < selection.extentOffset - ? selection.extentOffset - : selection.baseOffset; - - final selectedText = text.substring(start, end); - - return selectedText.trim().isNotEmpty ? selectedText.trim() : null; -} -} - -// ==================== APP ==================== void main() { - runApp( - ChangeNotifierProvider( - create: (_) => AppState(), - child: Consumer( - builder: (context, state, _) { - return MaterialApp( - title: 'Manyllines', - localizationsDelegates: const [ - quill.FlutterQuillLocalizations.delegate, - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: const [Locale('ru', 'RU'), Locale('en', 'US')], - locale: const Locale('ru', 'RU'), - theme: state.isDarkMode - ? ThemeData( - useMaterial3: true, - colorSchemeSeed: Colors.green, - brightness: Brightness.dark, - fontFamily: 'Roboto', - ) - : ThemeData( - useMaterial3: true, - colorSchemeSeed: Colors.green, - brightness: Brightness.light, - fontFamily: 'Roboto', - ), - home: AppShell(), - ); - }, - ), - ), - ); + runApp(const ManyllinesApp()); } -class AppShell extends StatelessWidget { - const AppShell({super.key}); - @override - Widget build(BuildContext context) { - return Selector( - selector: (_, state) => state.selectedProject, - builder: (context, selectedProject, _) { - return selectedProject == null ? const ProjectsScreen() : const ProjectWorkspace(); - }, - ); - } -} - -// ==================== ЭКРАН ПРОЕКТОВ ==================== -class ProjectsScreen extends StatelessWidget { - const ProjectsScreen({super.key}); - @override - Widget build(BuildContext context) { - return Scaffold( - body: Center( - child: ConstrainedBox( - constraints: BoxConstraints( - maxWidth: MediaQuery.of(context).size.width * 0.7, - maxHeight: MediaQuery.of(context).size.height * 0.9, // ✅ Изменил на 90% высоты экрана - ), - child: Column( - children: [ - Consumer( - builder: (context, state, _) { - final headerBg = state.isDarkMode ? Colors.grey[850] : Colors.white; - final logoBg = state.isDarkMode ? Colors.grey[800] : Colors.white; - final borderColor = state.isDarkMode ? Colors.grey[700]! : Colors.grey[300]!; - final logoBorderColor = state.isDarkMode ? Colors.grey[600]! : Colors.grey[400]!; - final textColor = state.isDarkMode ? Colors.white : Colors.black87; - final logoTextColor = state.isDarkMode ? Colors.white : Colors.black; - return Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - decoration: BoxDecoration(border: Border(bottom: BorderSide(color: borderColor)), color: headerBg), - child: Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - border: Border.all(color: logoBorderColor), - color: logoBg, - borderRadius: BorderRadius.circular(4), - ), - child: Text('Logo', style: TextStyle(fontWeight: FontWeight.bold, color: logoTextColor)), - ), - const SizedBox(width: 12), - Expanded(child: Text('Manyllines', style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: textColor))), - ], - ), - ); - }, - ), - // ✅ Оборачиваем прокручиваемый контент - Expanded( - child: SingleChildScrollView( - child: Column( - children: [ - Consumer( - builder: (context, state, _) { - final bgColor = state.isDarkMode ? Colors.green[900] : Colors.green[50]; - final borderColor = state.isDarkMode ? const Color.fromARGB(255, 0, 47, 22) : Colors.green.shade200; - final textColor = state.isDarkMode ? Colors.white : Colors.black87; - if (state.switchableValue) { - return ReorderableListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: state.projects.length, - onReorder: state.reorderProjects, - itemBuilder: (context, index) { - final project = state.projects[index]; - return Container( - key: ValueKey(project.id), - decoration: BoxDecoration(color: bgColor, border: Border(bottom: BorderSide(color: borderColor))), - child: ListTile( - title: Text(project.name, style: TextStyle(color: textColor)), - subtitle: Text('${project.documents.length} документов', style: TextStyle(fontSize: 12, color: textColor.withOpacity(0.7))), - trailing: Icon(Icons.drag_handle, color: state.isDarkMode ? Colors.white54 : Colors.grey), - onTap: () => state.selectProject(project), - ), - ); - }, - ); - } else { - return Container( - decoration: BoxDecoration(color: bgColor, border: Border(bottom: BorderSide(color: borderColor))), - child: ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: state.projects.length, - itemBuilder: (context, index) { - final project = state.projects[index]; - return Container( - key: ValueKey(project.id), - decoration: BoxDecoration(border: Border(bottom: BorderSide(color: borderColor))), - child: ListTile( - title: Text(project.name, style: TextStyle(color: textColor)), - subtitle: Text('${project.documents.length} документов', style: TextStyle(fontSize: 12, color: textColor.withOpacity(0.7))), - onTap: () => state.selectProject(project), - ), - ); - }, - ), - ); - } - }, - ), - Consumer( - builder: (context, state, _) { - final bgColor = state.isDarkMode ? Colors.blue[900] : Colors.blue[50]; - final borderColor = state.isDarkMode ? Colors.blue[700] : Colors.blue[200]; - final textColor = state.isDarkMode ? Colors.white : Colors.black87; - final settings = state.settings; - if (state.switchableValue) { - return ReorderableListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: settings.length, - onReorder: state.reorderSettings, - itemBuilder: (context, index) { - final setting = settings[index]; - final isExpanded = setting['expanded'] ?? false; - return Column( - key: ValueKey(setting['id']), - mainAxisSize: MainAxisSize.min, - children: [ - Container( - decoration: BoxDecoration(border: Border(bottom: BorderSide(color: borderColor!)), color: state.isDarkMode ? Colors.blue[800] : Colors.white), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(setting['name'], style: TextStyle(color: textColor)), - Row( - children: [ - IconButton(icon: Icon(isExpanded ? Icons.arrow_drop_down : Icons.arrow_drop_up, color: textColor), onPressed: () => state.toggleSettingExpansion(setting['id'])), - if (state.switchableValue) Icon(Icons.drag_handle, color: state.isDarkMode ? Colors.white54 : Colors.grey), - ], - ), - ], - ), - ), - if (setting['id'] == 'setting1' && isExpanded) _buildDescriptionSection1(state.isDarkMode), - if (setting['id'] == 'setting2' && isExpanded) _buildDescriptionSection2(state.isDarkMode), - if (setting['id'] == 'setting3' && isExpanded) _buildDescriptionSection3(state.isDarkMode), - ], - ); - }, - ); - } else { - return ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: settings.length, - itemBuilder: (context, index) { - final setting = settings[index]; - final isExpanded = setting['expanded'] ?? false; - return Column( - key: ValueKey(setting['id']), - mainAxisSize: MainAxisSize.min, - children: [ - Container( - decoration: BoxDecoration(border: Border(bottom: BorderSide(color: borderColor!)), color: state.isDarkMode ? Colors.blue[800] : Colors.white), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(setting['name'], style: TextStyle(color: textColor)), - IconButton(icon: Icon(isExpanded ? Icons.arrow_drop_down : Icons.arrow_drop_up, color: textColor), onPressed: () => state.toggleSettingExpansion(setting['id'])), - ], - ), - ), - if (setting['id'] == 'setting1' && isExpanded) _buildDescriptionSection1(state.isDarkMode), - if (setting['id'] == 'setting2' && isExpanded) _buildDescriptionSection2(state.isDarkMode), - if (setting['id'] == 'setting3' && isExpanded) _buildDescriptionSection3(state.isDarkMode), - ], - ); - }, - ); - } - }, - ), - Consumer( - builder: (context, state, _) { - final bgColor = state.isDarkMode ? const Color.fromARGB(255, 6, 58, 137) : Colors.blue[50]; - final textColor = state.isDarkMode ? Colors.white54 : Colors.black54; - return Container( - color: bgColor, - padding: const EdgeInsets.all(16), - child: Center(child: Text('Other Settings ...', style: TextStyle(color: textColor))), - ); - }, - ), - ], - ), - ), - ), - ], - ), - ), - ), - floatingActionButton: FloatingActionButton(onPressed: () => _showCreateProjectDialog(context), child: const Icon(Icons.add)), - ); - } - - - void _showCreateProjectDialog(BuildContext context) { - final controller = TextEditingController(); - final formKey = GlobalKey(); - showDialog( - context: context, - builder: (context) => Consumer( - builder: (context, state, _) { - final isDarkMode = state.isDarkMode; - return AlertDialog( - backgroundColor: isDarkMode ? Colors.grey[850] : Colors.white, - title: Text('Новый проект', style: TextStyle(color: isDarkMode ? Colors.white : Colors.black87)), - content: Form( - key: formKey, - child: TextFormField( - controller: controller, - autofocus: true, - style: TextStyle(color: isDarkMode ? Colors.white : Colors.black87), - decoration: InputDecoration( - labelText: 'Название проекта', - labelStyle: TextStyle(color: isDarkMode ? Colors.grey[400] : Colors.grey[600]), - border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), - focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: BorderSide(color: Colors.green[700]!)), - prefixIcon: Icon(Icons.folder, color: Colors.green[700]), - ), - validator: (value) { - if (value == null || value.trim().isEmpty) return 'Введите название проекта'; - return null; - }, - onFieldSubmitted: (_) { - if (formKey.currentState!.validate()) { - state.addProject(controller.text.trim()); - Navigator.pop(context); - } - }, - ), - ), - actions: [ - TextButton(onPressed: () => Navigator.pop(context), child: Text('Отмена', style: TextStyle(color: Colors.grey[600]))), - ElevatedButton( - onPressed: () { - if (formKey.currentState!.validate()) { - state.addProject(controller.text.trim()); - Navigator.pop(context); - } - }, - style: ElevatedButton.styleFrom(backgroundColor: Colors.green[700], foregroundColor: Colors.white), - child: const Text('Создать'), - ), - ], - ); - }, - ), - ); - } - - Widget _buildDescriptionSection2(bool isDarkMode) { - return Container( - color: isDarkMode ? Colors.grey[850] : Colors.grey[50], - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text('Description', style: TextStyle(fontSize: 14, color: isDarkMode ? Colors.grey[300] : Colors.grey[700])), - const SizedBox(height: 12), - Row(children: [_buildOutlinedButton('A', isDarkMode), const SizedBox(width: 8), _buildOutlinedButton('B', isDarkMode), const SizedBox(width: 8), _buildOutlinedButton('C', isDarkMode)]), - const SizedBox(height: 16), - Consumer( - builder: (context, state, _) { - final isDark = state.isDarkMode; - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row(children: [Icon(isDark ? Icons.dark_mode : Icons.light_mode, size: 20, color: isDark ? Colors.yellow[200] : Colors.orange), const SizedBox(width: 8), Text(isDark ? 'Тёмная тема' : 'Светлая тема', style: TextStyle(color: isDarkMode ? Colors.white : Colors.black87))]), - Switch(value: isDark, onChanged: (value) => state.toggleDarkMode(value)), - ], - ); - }, - ), - ], - ), - ); - } - - Widget _buildDescriptionSection3(bool isDarkMode) { - return Container( - color: isDarkMode ? Colors.grey[850] : Colors.grey[50], - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text('Description', style: TextStyle(fontSize: 14, color: isDarkMode ? Colors.grey[300] : Colors.grey[700])), - const SizedBox(height: 12), - Row(children: [_buildOutlinedButton('A', isDarkMode), const SizedBox(width: 8), _buildOutlinedButton('B', isDarkMode), const SizedBox(width: 8), _buildOutlinedButton('C', isDarkMode)]), - const SizedBox(height: 16), - Consumer( - builder: (context, state, _) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('Switchable', style: TextStyle(color: isDarkMode ? Colors.white : Colors.black87)), - Switch(value: state.switchableValue, onChanged: (value) => state.setSwitchableValue(value)), - ], - ); - }, - ), - Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text('Listable', style: TextStyle(color: isDarkMode ? Colors.white : Colors.black87)), IconButton(icon: Icon(Icons.arrow_drop_down, color: isDarkMode ? Colors.white : Colors.black87), onPressed: () {})]), - ], - ), - ); - } +class ManyllinesApp extends StatelessWidget { + const ManyllinesApp({super.key}); - Widget _buildDescriptionSection1(bool isDarkMode) { - return Container( - color: isDarkMode ? Colors.grey[850] : Colors.grey[50], - padding: const EdgeInsets.all(16), - child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Text('Description', style: TextStyle(fontSize: 14, color: isDarkMode ? Colors.grey[300] : Colors.grey[700])), - const SizedBox(height: 12), - Row(children: [_buildOutlinedButton('A', isDarkMode), const SizedBox(width: 8), _buildOutlinedButton('B', isDarkMode), const SizedBox(width: 8), _buildOutlinedButton('C', isDarkMode)]), - ]), - ); - } - - Widget _buildOutlinedButton(String label, bool isDarkMode) { - return Expanded( - child: OutlinedButton( - onPressed: () {}, - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 12), - side: BorderSide(color: isDarkMode ? const Color.fromARGB(255, 54, 107, 232) : Colors.grey[400]!), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), - foregroundColor: isDarkMode ? Colors.white : Colors.black87, - ), - child: Text(label), - ), - ); - } -} - -// ==================== РАБОЧЕЕ ПРОСТРАНСТВО ==================== -class ProjectWorkspace extends StatelessWidget { - const ProjectWorkspace({super.key}); @override Widget build(BuildContext context) { - return LayoutBuilder( - builder: (context, constraints) { - final isWide = constraints.maxWidth >= 700; - if (!isWide) { - return Consumer( - builder: (context, state, _) { - return state.selectedDocument == null ? const _MobileDocList() : _MobileEditorView(document: state.selectedDocument!); - }, - ); - } - return Selector( - selector: (_, state) => state.selectedDocument, - builder: (context, selectedDocument, _) { - return _buildDesktopLayout(context, selectedDocument); - }, - ); - }, - ); - } - - Widget _buildDesktopLayout(BuildContext context, AppDocument? selectedDocument) { - final state = context.watch(); - final leftPanelBg = state.isDarkMode ? Colors.grey[900] : Colors.white; - final headerBg = state.isDarkMode ? Colors.green[900] : Colors.green[50]; - final textColor = state.isDarkMode ? Colors.white : Colors.black87; - final borderColor = state.isDarkMode ? Colors.grey[700]! : Colors.grey[300]!; - - final showTwoEditors = state.secondSelectedDocument != null; - final isPanelCollapsed = state.isSidePanelCollapsed; - final isGlossaryOpen = state.isGlossaryPanelOpen; - - return Scaffold( - body: Row( - children: [ - // ✅ Левая панель (сворачиваемая) - AnimatedContainer( - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - width: isPanelCollapsed ? 0 : 300, - decoration: BoxDecoration( - border: Border(right: BorderSide(color: borderColor)), - ), - child: isPanelCollapsed - ? const SizedBox.shrink() - : Column( - children: [ - Container( - padding: const EdgeInsets.all(16), - color: headerBg, - child: Row( - children: [ - IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => state.clearSelectedProject(), tooltip: 'Back to projects'), - Expanded(child: Text(state.selectedProject!.name, style: Theme.of(context).textTheme.titleMedium?.copyWith(color: textColor))), - ], - ), - ), - Expanded( - child: Material( - color: leftPanelBg, - child: Column( - children: [ - Expanded( - child: Selector( - selector: (_, state) => state.isGraphView, - builder: (context, isGraphView, _) { - return isGraphView ? _DocumentsGraph() : _DocumentsList(); - }, - ), - ), - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration(border: Border(top: BorderSide(color: borderColor))), - child: SizedBox( - width: double.infinity, - child: OutlinedButton.icon( - onPressed: () => _showCreateDocumentDialog(context), - icon: const Icon(Icons.add, size: 18), - label: const Text('Новый документ'), - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 12), - foregroundColor: state.isDarkMode ? Colors.white : Colors.green[700], - side: BorderSide(color: state.isDarkMode ? Colors.green[400]! : Colors.green[700]!), - ), - ), - ), - ), - ], - ), - ), - ), - ], - ), - ), - - // ✅ Кнопка сворачивания левой панели - Container( - width: 24, - decoration: BoxDecoration( - color: isPanelCollapsed ? (state.isDarkMode ? Colors.grey[800] : Colors.grey[200]) : Colors.transparent, - border: Border(right: BorderSide(color: borderColor)), - ), - child: Column( - children: [ - // ✅ Регулируйте высоту верхнего отступа (100px от верха) - const SizedBox(height: 100), // ← Меняйте это значение - - // ✅ Кнопка сворачивания/разворачивания - GestureDetector( - onTap: () => state.toggleSidePanel(), - child: Container( - width: 24, - height: 82, - decoration: BoxDecoration( - color: state.isDarkMode ? Colors.grey[700] : Colors.grey[300], - ), - child: Icon( - isPanelCollapsed ? Icons.chevron_right : Icons.chevron_left, - size: 20, - color: textColor, - ), - ), - ), - - // ✅ Оставшееся пространство - const Expanded(child: SizedBox()), // Было: Spacer() - ], - ), - ), - - // ✅ Редакторы - // ✅ Редакторы -Expanded( - child: showTwoEditors - ? _buildTwoEditorsLayout(context, state, borderColor, textColor) - : _buildSingleEditorLayout(context, selectedDocument, state, textColor), -), - -// ✅ Панель глоссария + вкладка слева -if (isGlossaryOpen) ...[ - // ✅ Вкладка для ЗАКРЫТИЯ глоссария (слева от панели) - Container( - width: 24, - decoration: BoxDecoration( - color: state.isDarkMode ? Colors.grey[800] : Colors.grey[200], - border: Border(right: BorderSide(color: borderColor)), - ), - child: Column( - children: [ - const SizedBox(height: 100), - GestureDetector( - onTap: () => state.toggleGlossaryPanel(), - child: Container( - width: 24, - height: 82, - decoration: BoxDecoration( - color: state.isDarkMode ? Colors.grey[700] : Colors.grey[300], - ), - child: Icon( - Icons.chevron_right, // Стрелка вправо = закрыть панель - size: 20, - color: textColor, - ), - ), - ), - const Expanded(child: SizedBox()), - ], - ), - ), - // ✅ Панель глоссария - const _GlossaryPanel(), -] else - // ✅ Вкладка для ОТКРЫТИЯ глоссария (слева, когда панель закрыта) - Container( - width: 24, - decoration: BoxDecoration( - color: state.isDarkMode ? Colors.grey[800] : Colors.grey[200], - border: Border(right: BorderSide(color: borderColor)), - ), - child: Column( - children: [ - const SizedBox(height: 100), - GestureDetector( - onTap: () => state.toggleGlossaryPanel(), - child: Container( - width: 24, - height: 82, - decoration: BoxDecoration( - color: state.isDarkMode ? Colors.blue[700] : Colors.blue[300], - ), - child: const Icon( - Icons.chevron_left, // Стрелка влево = открыть панель - size: 20, - color: Colors.white, - ), - ), - ), - const Expanded(child: SizedBox()), - ], - ), - ), - ], - ), - floatingActionButton: Selector( - selector: (_, state) => state.isGraphView, - builder: (context, isGraphView, _) { - return FloatingActionButton( - onPressed: () => state.toggleViewMode(), - tooltip: isGraphView ? 'Список' : 'Граф', - child: Icon(isGraphView ? Icons.list : Icons.account_tree), - ); - }, - ), - persistentFooterButtons: [ - FloatingActionButton( - heroTag: 'createDoc', - onPressed: () => _showCreateDocumentDialog(context), - tooltip: 'Новый документ', - child: const Icon(Icons.add), - ), - ], - ); -} - - Widget _buildSingleEditorLayout(BuildContext context, AppDocument? selectedDocument, AppState state, Color textColor) { - final project = state.selectedProject; - final hasDocuments = project != null && project.documents.isNotEmpty; - - if (selectedDocument == null) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - hasDocuments ? Icons.touch_app : Icons.description_outlined, - size: 64, - color: state.isDarkMode ? Colors.white30 : Colors.black26, - ), - const SizedBox(height: 16), - Text( - hasDocuments ? 'Выберите документ' : 'В проекте нет документов', - style: TextStyle(fontSize: 18, color: state.isDarkMode ? Colors.white70 : Colors.black54), - ), - const SizedBox(height: 8), - Text( - hasDocuments - ? 'Кликните на документ в списке слева' - : 'Нажмите кнопку "+ Новый документ" чтобы создать', - style: TextStyle(color: state.isDarkMode ? Colors.white54 : Colors.black45), - ), - const SizedBox(height: 24), - if (!hasDocuments) - ElevatedButton.icon( - onPressed: () => _showCreateDocumentDialog(context), - icon: const Icon(Icons.add), - label: const Text('Создать первый документ'), - style: ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12)), - ), - ], - ), - ); - } - - return Column( - children: [ - Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - color: state.isDarkMode ? Colors.grey[850] : Colors.grey[100], - child: Row( - children: [ - Expanded( - child: Text( - selectedDocument.name, - style: TextStyle(fontWeight: FontWeight.w500, color: textColor), - overflow: TextOverflow.ellipsis, - ), - ), - IconButton( - icon: const Icon(Icons.close, size: 20), - onPressed: () => state.closeFirstEditor(), - tooltip: 'Закрыть', - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - ), - ], - ), - ), - Expanded( - child: QuillEditorView(document: selectedDocument, editorIndex: 1), - ), - ], - ); - } - - Widget _buildTwoEditorsLayout(BuildContext context, AppState state, Color borderColor, Color textColor) { - return Row( - children: [ - Expanded( - child: Column( - children: [ - Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - color: state.isDarkMode ? Colors.grey[850] : Colors.grey[100], - child: Row( - children: [ - Expanded( - child: Text( - state.selectedDocument?.name ?? 'Первый редактор', - style: TextStyle(fontWeight: FontWeight.w500, color: textColor), - overflow: TextOverflow.ellipsis, - ), - ), - IconButton( - icon: const Icon(Icons.close, size: 20), - onPressed: () => state.closeFirstEditor(), - tooltip: 'Закрыть', - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - ), - ], - ), - ), - Expanded( - child: Container( - decoration: BoxDecoration(border: Border(right: BorderSide(color: borderColor))), - child: state.selectedDocument != null - ? QuillEditorView(document: state.selectedDocument!, editorIndex: 1) - : Center(child: Text('Выберите документ', style: TextStyle(color: textColor))), - ), - ), - ], - ), - ), - Expanded( - child: Column( - children: [ - Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - color: state.isDarkMode ? Colors.grey[850] : Colors.grey[100], - child: Row( - children: [ - Expanded( - child: Text( - state.secondSelectedDocument?.name ?? 'Второй редактор', - style: TextStyle(fontWeight: FontWeight.w500, color: textColor), - overflow: TextOverflow.ellipsis, - ), - ), - IconButton( - icon: const Icon(Icons.close, size: 20), - onPressed: () => state.closeSecondEditor(), - tooltip: 'Закрыть', - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - ), - ], - ), - ), - Expanded( - child: state.secondSelectedDocument != null - ? QuillEditorView(document: state.secondSelectedDocument!, editorIndex: 2) - : Center(child: Text('Выберите документ', style: TextStyle(color: textColor))), - ), - ], - ), - ), - ], - ); - } - - void _showCreateDocumentDialog(BuildContext context) { - final controller = TextEditingController(); - final formKey = GlobalKey(); - showDialog( - context: context, - builder: (context) => Consumer( - builder: (context, state, _) { - final isDarkMode = state.isDarkMode; - return AlertDialog( - backgroundColor: isDarkMode ? Colors.grey[850] : Colors.white, - title: Text('Новый документ', style: TextStyle(color: isDarkMode ? Colors.white : Colors.black87)), - content: Form( - key: formKey, - child: TextFormField( - controller: controller, - autofocus: true, - style: TextStyle(color: isDarkMode ? Colors.white : Colors.black87), - decoration: InputDecoration( - labelText: 'Название документа', - labelStyle: TextStyle(color: isDarkMode ? Colors.grey[400] : Colors.grey[600]), - border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), - focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: BorderSide(color: Colors.green[700]!)), - prefixIcon: Icon(Icons.description, color: Colors.green[700]), - ), - validator: (value) { - if (value == null || value.trim().isEmpty) return 'Введите название документа'; - return null; - }, - onFieldSubmitted: (_) { - if (formKey.currentState!.validate()) { - state.addDocumentToCurrentProject(controller.text.trim()); - Navigator.pop(context); - } - }, - ), - ), - actions: [ - TextButton(onPressed: () => Navigator.pop(context), child: Text('Отмена', style: TextStyle(color: Colors.grey[600]))), - ElevatedButton( - onPressed: () { - if (formKey.currentState!.validate()) { - state.addDocumentToCurrentProject(controller.text.trim()); - Navigator.pop(context); - } - }, - style: ElevatedButton.styleFrom(backgroundColor: Colors.green[700], foregroundColor: Colors.white), - child: const Text('Создать'), - ), - ], + return AppProviders( + child: Consumer( + builder: (context, settingState, _) { + return MaterialApp( + title: 'Manylines', + theme: AppTheme.light, + darkTheme: AppTheme.dark, + themeMode: settingState.isDarkMode ? ThemeMode.dark : ThemeMode.light, + localizationsDelegates: AppLocalizations.delegates, + supportedLocales: AppLocalizations.supportedLocales, + locale: const Locale('ru', 'RU'), + home: const AppRouter(), ); }, ), ); } -} - -// ==================== ПАНЕЛЬ ГЛОССАРИЯ ==================== -class _GlossaryPanel extends StatelessWidget { - const _GlossaryPanel(); - - @override - Widget build(BuildContext context) { - final state = context.watch(); - final document = state.selectedDocument; - final isDarkMode = state.isDarkMode; - final textColor = isDarkMode ? Colors.white : Colors.black87; - final borderColor = isDarkMode ? Colors.grey[700]! : Colors.grey[300]!; - - if (document == null) { - return Container( - width: 300, - color: isDarkMode ? Colors.grey[900] : Colors.grey[50], - child: const Center(child: Text('Выберите документ')), - ); - } - - return Container( - width: 300, - decoration: BoxDecoration( - border: Border(left: BorderSide(color: borderColor)), - color: isDarkMode ? Colors.grey[900] : Colors.grey[50], - ), - child: Column( - children: [ - // ✅ Заголовок глоссария — название документа, БЕЗ крестика - Container( - padding: const EdgeInsets.all(12), - color: isDarkMode ? Colors.grey[800] : Colors.grey[200], - child: Row( - children: [ - const Icon(Icons.book, size: 20), - const SizedBox(width: 8), - Expanded( - child: Text( - document.name, // ✅ Название документа - style: const TextStyle(fontWeight: FontWeight.bold), - overflow: TextOverflow.ellipsis, - ), - ), - // ❌ Крестик убран — закрытие только через вкладку справа - ], - ), - ), - - // ✅ Кнопка добавления записи (если есть выделенный текст) - if (state.selectedTextForGlossary != null) - Container( - padding: const EdgeInsets.all(8), - color: isDarkMode ? Colors.green[900]!.withOpacity(0.3) : Colors.green[50], - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Выделенный текст:', - style: TextStyle(fontSize: 12, color: Colors.grey[600]), - ), - const SizedBox(height: 4), - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: isDarkMode ? Colors.grey[800] : Colors.white, - border: Border.all(color: Colors.green[700]!), - borderRadius: BorderRadius.circular(4), - ), - child: Text( - state.selectedTextForGlossary!, - style: TextStyle(color: textColor), - ), - ), - const SizedBox(height: 8), - SizedBox( - width: double.infinity, - child: ElevatedButton.icon( - onPressed: () => state.addGlossaryEntry(state.selectedTextForGlossary!), - icon: const Icon(Icons.add, size: 18), - label: const Text('Добавить в глоссарий'), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green[700], - foregroundColor: Colors.white, - ), - ), - ), - ], - ), - ), - - // ✅ Список записей глоссария - Expanded( - child: document.glossary.isEmpty - ? Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.book_outlined, size: 48, color: Colors.grey[400]), - const SizedBox(height: 16), - Text( - 'Глоссарий пуст', - style: TextStyle(color: Colors.grey[600]), - ), - const SizedBox(height: 8), - Text( - 'Выделите текст и свайпните влево', - textAlign: TextAlign.center, - style: TextStyle(fontSize: 12, color: Colors.grey[500]), - ), - ], - ), - ) - : ListView.builder( - itemCount: document.glossary.length, - itemBuilder: (context, index) { - final entry = document.glossary[index]; - return _GlossaryEntryTile( - entry: entry, - isDarkMode: isDarkMode, - textColor: textColor, - borderColor: borderColor, - onUpdateDefinition: (definition) => - state.updateGlossaryDefinition(entry.id, definition), - onToggleExpand: () => state.toggleGlossaryEntry(entry.id), - onDelete: () => state.deleteGlossaryEntry(entry.id), - ); - }, - ), - ), - ], - ), - ); - } -} - -class _GlossaryEntryTile extends StatefulWidget { - final GlossaryEntry entry; - final bool isDarkMode; - final Color textColor; - final Color borderColor; - final Function(String) onUpdateDefinition; - final VoidCallback onToggleExpand; - final VoidCallback onDelete; - - const _GlossaryEntryTile({ - required this.entry, - required this.isDarkMode, - required this.textColor, - required this.borderColor, - required this.onUpdateDefinition, - required this.onToggleExpand, - required this.onDelete, - }); - - @override - State<_GlossaryEntryTile> createState() => _GlossaryEntryTileState(); -} - -class _GlossaryEntryTileState extends State<_GlossaryEntryTile> { - late TextEditingController _definitionController; - - @override - void initState() { - super.initState(); - _definitionController = TextEditingController(text: widget.entry.definition); - } - - @override - void dispose() { - _definitionController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - border: Border(bottom: BorderSide(color: widget.borderColor)), - color: widget.entry.isExpanded - ? (widget.isDarkMode ? Colors.blue[900]!.withOpacity(0.2) : Colors.blue[50]) - : Colors.transparent, - ), - child: Column( - children: [ - ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), - title: Row( - children: [ - Icon( - widget.entry.isExpanded ? Icons.arrow_drop_down : Icons.arrow_right, - size: 20, - color: widget.textColor, - ), - const SizedBox(width: 4), - Expanded( - child: Text( - widget.entry.term, - style: const TextStyle(fontWeight: FontWeight.w500), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ), - IconButton( - icon: const Icon(Icons.delete_outline, size: 18), - onPressed: widget.onDelete, - tooltip: 'Удалить', - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - ), - ], - ), - onTap: widget.onToggleExpand, - ), - if (widget.entry.isExpanded) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Определение:', - style: TextStyle( - fontSize: 12, - color: widget.isDarkMode ? Colors.grey[400] : Colors.grey[600], - ), - ), - const SizedBox(height: 8), - TextField( - controller: _definitionController, - maxLines: 5, - minLines: 3, - style: TextStyle(color: widget.textColor), - decoration: InputDecoration( - hintText: 'Введите определение...', - hintStyle: TextStyle(color: Colors.grey[500]), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - filled: true, - fillColor: widget.isDarkMode ? Colors.grey[800] : Colors.white, - ), - onChanged: widget.onUpdateDefinition, - ), - ], - ), - ), - ], - ), - ); - } -} - -// ==================== СПИСОК ДОКУМЕНТОВ ==================== -class _DocumentsList extends StatelessWidget { - const _DocumentsList(); - - @override - Widget build(BuildContext context) { - final state = context.watch(); - final project = state.selectedProject!; - final pinnedDocs = project.pinnedDocuments; - final unpinnedDocs = project.unpinnedDocuments; - final isDarkMode = state.isDarkMode; - - return Column( - children: [ - if (pinnedDocs.isNotEmpty) - Container( - color: isDarkMode ? Colors.green[900]!.withOpacity(0.3) : Colors.green[50], - child: ReorderableListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: pinnedDocs.length, - onReorder: state.reorderPinnedDocuments, - itemBuilder: (context, index) { - final doc = pinnedDocs[index]; - final isSelected = state.selectedDocument?.id == doc.id || state.secondSelectedDocument?.id == doc.id; - - return Container( - key: ValueKey(doc.id), - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - color: isDarkMode ? Colors.green[700]! : Colors.green[200]!, - ), - ), - ), - child: ListTile( - selected: isSelected, - selectedTileColor: Theme.of(context).colorScheme.secondaryContainer, - leading: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - '${index + 1}.', - style: TextStyle( - fontSize: 12, - color: Colors.green[700], - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(width: 8), - Icon( - Icons.push_pin, - size: 16, - color: Colors.green[700], - ), - ], - ), - title: Text(doc.name), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Checkbox( - value: doc.isPinned, - activeColor: Colors.green[700], - onChanged: (value) => state.toggleDocumentPin(doc), - ), - const SizedBox(width: 4), - IconButton( - icon: const Icon(Icons.more_vert, size: 20), - onPressed: () => _showDeleteMenu(context, doc), - tooltip: 'Меню', - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - ), - Icon( - Icons.drag_handle, - color: isDarkMode ? Colors.white54 : Colors.grey, - ), - ], - ), - onTap: () => state.selectDocument(doc), - onLongPress: () => state.selectSecondDocument(doc), - ), - ); - }, - ), - ), - Expanded( - child: Container( - color: isDarkMode ? Colors.blue[900]!.withOpacity(0.2) : Colors.blue[50], - child: _buildDismissibleList(project, unpinnedDocs, state, isDarkMode, context), - ), - ), - ], - ); - } - - Widget _buildDismissibleList(Project project, List docs, AppState state, bool isDarkMode, BuildContext context) { - int mainIndex = 0; - int childIndex = 0; - - return ListView.builder( - itemCount: docs.length, - itemBuilder: (context, index) { - final doc = docs[index]; - final isSelected = state.selectedDocument?.id == doc.id || state.secondSelectedDocument?.id == doc.id; - final actualIndex = project.documents.indexOf(doc); - - String number; - if (doc.parentId == null) { - mainIndex++; - childIndex = 0; - number = '$mainIndex.'; - } else { - childIndex++; - number = '$mainIndex.$childIndex'; - } - - return Dismissible( - key: ValueKey(doc.id), - direction: DismissDirection.horizontal, - background: Container( - color: Colors.blue, - alignment: Alignment.centerLeft, - padding: const EdgeInsets.only(left: 16), - child: const Icon(Icons.arrow_back, color: Colors.white), - ), - secondaryBackground: Container( - color: Colors.green, - alignment: Alignment.centerRight, - padding: const EdgeInsets.only(right: 16), - child: const Icon(Icons.arrow_forward, color: Colors.white), - ), - confirmDismiss: (direction) async { - if (direction == DismissDirection.startToEnd) { - state.indentDocument(actualIndex); - } else if (direction == DismissDirection.endToStart) { - state.outdentDocument(actualIndex); - } - return false; - }, - child: Container( - decoration: BoxDecoration( - color: doc.parentId == null - ? (isDarkMode ? Colors.blue[900]!.withOpacity(0.2) : Colors.blue[50]) - : (isDarkMode ? Colors.green[900]!.withOpacity(0.3) : Colors.green[50]), - border: Border( - bottom: BorderSide( - color: isDarkMode - ? (doc.parentId == null ? Colors.blue[700]! : Colors.green[700]!) - : (doc.parentId == null ? Colors.blue[200]! : Colors.green[200]!), - ), - ), - ), - child: ListTile( - selected: isSelected, - selectedTileColor: Theme.of(context).colorScheme.secondaryContainer, - leading: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - number, - style: TextStyle( - fontSize: 12, - color: doc.parentId == null ? Colors.blue[700] : Colors.green[700], - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(width: 8), - Icon( - doc.parentId == null ? Icons.insert_drive_file : Icons.subdirectory_arrow_right, - size: 16, - color: doc.parentId == null ? Colors.blue[700] : Colors.green[700], - ), - ], - ), - title: Text(doc.name), - subtitle: doc.parentId != null ? Text('Поддокумент', style: TextStyle(fontSize: 10, color: Colors.grey[600])) : null, - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Checkbox( - value: doc.isPinned, - activeColor: Colors.green[700], - onChanged: (value) => state.toggleDocumentPin(doc), - ), - const SizedBox(width: 4), - if (doc.parentId != null) - Icon(Icons.swipe, size: 16, color: Colors.grey[500]), - IconButton( - icon: const Icon(Icons.more_vert, size: 20), - onPressed: () => _showDeleteMenu(context, doc), - tooltip: 'Меню', - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - ), - ], - ), - onTap: () => state.selectDocument(doc), - onLongPress: () => state.selectSecondDocument(doc), - ), - ), - ); - }, - ); - } - - void _showDeleteMenu(BuildContext context, AppDocument doc) { - final state = context.read(); - final isDarkMode = state.isDarkMode; - - showMenu( - context: context, - position: RelativeRect.fill, - items: [ - PopupMenuItem( - child: Row( - children: [ - Icon(Icons.delete_outline, size: 20, color: Colors.red), - const SizedBox(width: 8), - Text('Удалить документ', style: TextStyle(color: Colors.red)), - ], - ), - onTap: () { - showDialog( - context: context, - builder: (ctx) => AlertDialog( - backgroundColor: isDarkMode ? Colors.grey[850] : Colors.white, - title: const Text('Удалить документ?'), - content: Text('Документ "${doc.name}" будет удалён без возможности восстановления.', - style: TextStyle(color: isDarkMode ? Colors.white70 : Colors.black87)), - actions: [ - TextButton( - onPressed: () => Navigator.pop(ctx), - child: Text('Отмена', style: TextStyle(color: isDarkMode ? Colors.grey[400] : Colors.grey[600])), - ), - ElevatedButton( - onPressed: () { - state.deleteDocument(doc); - Navigator.pop(ctx); - Navigator.pop(context); - }, - style: ElevatedButton.styleFrom(backgroundColor: Colors.red, foregroundColor: Colors.white), - child: const Text('Удалить'), - ), - ], - ), - ); - }, - ), - ], - ); - } -} - -// ==================== ГРАФОВОЕ ПРЕДСТАВЛЕНИЕ ==================== -class _DocumentsGraph extends StatelessWidget { - const _DocumentsGraph(); - - @override - Widget build(BuildContext context) { - final state = context.watch(); - final project = state.selectedProject!; - final docs = project.documents; - final isDarkMode = state.isDarkMode; - - if (docs.isEmpty) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.account_tree_outlined, size: 64, color: Colors.grey[400]), - const SizedBox(height: 16), - Text('Нет документов', style: TextStyle(color: Colors.grey[600])), - ], - ), - ); - } - - return SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ..._buildDocumentNodes(docs, state, isDarkMode, context), - ], - ), - ), - ); - } - - List _buildDocumentNodes(List docs, AppState state, bool isDarkMode, BuildContext context) { - final widgets = []; - final rootDocs = docs.where((d) => d.parentId == null).toList(); - - for (var doc in rootDocs) { - widgets.add(_buildDocumentNode(doc, docs, state, isDarkMode, context)); - widgets.add(const SizedBox(height: 20)); - } - - return widgets; - } - - Widget _buildDocumentNode(AppDocument doc, List allDocs, AppState state, bool isDarkMode, BuildContext context) { - final isSelected = state.selectedDocument?.id == doc.id || state.secondSelectedDocument?.id == doc.id; - final children = allDocs.where((d) => d.parentId == doc.id).toList(); - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - GestureDetector( - onTap: () => state.selectDocument(doc), - onLongPress: () => state.selectSecondDocument(doc), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), - decoration: BoxDecoration( - color: isSelected - ? (isDarkMode ? Colors.green[800] : Colors.green[100]) - : (isDarkMode ? Colors.grey[800] : Colors.white), - border: Border.all( - color: isSelected - ? Colors.green[700]! - : (isDarkMode ? Colors.grey[600]! : Colors.grey[400]!), - width: 2, - ), - borderRadius: BorderRadius.circular(8), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - doc.isPinned ? Icons.push_pin : Icons.insert_drive_file, - color: isSelected ? Colors.green[700] : Colors.grey[600], - size: 20, - ), - const SizedBox(width: 8), - Text( - doc.name, - style: TextStyle( - fontSize: 14, - fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, - color: isDarkMode ? Colors.white : Colors.black87, - ), - ), - IconButton( - icon: const Icon(Icons.more_vert, size: 18), - onPressed: () => _showDeleteMenuInGraph(context, doc), - tooltip: 'Меню', - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - ), - ], - ), - ), - ), - - if (children.isNotEmpty) ...[ - const SizedBox(height: 8), - ...children.asMap().entries.map((entry) { - final childDoc = entry.value; - return Padding( - padding: const EdgeInsets.only(left: 40), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Container(width: 30, height: 2, color: isDarkMode ? Colors.grey[600] : Colors.grey[400]), - Icon(Icons.arrow_forward, size: 16, color: isDarkMode ? Colors.grey[600] : Colors.grey[400]), - ], - ), - const SizedBox(height: 8), - _buildDocumentNode(childDoc, allDocs, state, isDarkMode, context), - ], - ), - ); - }).toList(), - ], - ], - ); - } - - void _showDeleteMenuInGraph(BuildContext context, AppDocument doc) { - final state = context.read(); - final isDarkMode = state.isDarkMode; - - showMenu( - context: context, - position: RelativeRect.fill, - items: [ - PopupMenuItem( - child: Row( - children: [ - Icon(Icons.delete_outline, size: 20, color: Colors.red), - const SizedBox(width: 8), - Text('Удалить документ', style: TextStyle(color: Colors.red)), - ], - ), - onTap: () { - showDialog( - context: context, - builder: (ctx) => AlertDialog( - backgroundColor: isDarkMode ? Colors.grey[850] : Colors.white, - title: const Text('Удалить документ?'), - content: Text('Документ "${doc.name}" будет удалён без возможности восстановления.', - style: TextStyle(color: isDarkMode ? Colors.white70 : Colors.black87)), - actions: [ - TextButton( - onPressed: () => Navigator.pop(ctx), - child: Text('Отмена', style: TextStyle(color: isDarkMode ? Colors.grey[400] : Colors.grey[600])), - ), - ElevatedButton( - onPressed: () { - state.deleteDocument(doc); - Navigator.pop(ctx); - Navigator.pop(context); - }, - style: ElevatedButton.styleFrom(backgroundColor: Colors.red, foregroundColor: Colors.white), - child: const Text('Удалить'), - ), - ], - ), - ); - }, - ), - ], - ); - } -} - -// ==================== РЕДАКТОР ==================== -class QuillEditorView extends StatefulWidget { - final AppDocument document; - final int editorIndex; - - const QuillEditorView({ - super.key, - required this.document, - this.editorIndex = 1, - }); - - @override - State createState() => _QuillEditorViewState(); -} - -class _QuillEditorViewState extends State { - quill.QuillController? _controller; - - @override - void initState() { - super.initState(); - _initializeController(); - } - - @override - void didUpdateWidget(QuillEditorView oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.document.id != widget.document.id) { - _controller?.dispose(); - _initializeController(); - } - } - - void _initializeController() { - final state = context.read(); - _controller = state.getOrCreateController(widget.document); - } - - @override - void dispose() { - _controller?.dispose(); - super.dispose(); - } - - // ✅ Получение выделенного текста - String? _getSelectedText() { - if (_controller == null) return null; - - final selection = _controller!.selection; - if (selection.isCollapsed) return null; - - final text = _controller!.document.toPlainText(); - if (selection.baseOffset >= text.length || selection.extentOffset >= text.length) return null; - - final start = selection.baseOffset < selection.extentOffset - ? selection.baseOffset - : selection.extentOffset; - final end = selection.baseOffset < selection.extentOffset - ? selection.extentOffset - : selection.baseOffset; - - final selectedText = text.substring(start, end); - - return selectedText.trim().isNotEmpty ? selectedText.trim() : null; - } - - // ✅ Кнопка: добавить выделенный текст и открыть глоссарий - void _addSelectedToGlossary() { - final selectedText = _getSelectedText(); - if (selectedText != null) { - context.read().addAndOpenGlossary(selectedText); - } else { - // Если нет выделения, просто открыть панель - context.read().toggleGlossaryPanel(); - } - } - - // ✅ Свайп: просто открыть глоссарий - void _handleSwipeLeft() { - final selectedText = _getSelectedText(); - if (selectedText != null) { - // Сохраняем текст для отображения в панели - context.read().setSelectedTextForGlossary(selectedText); - } - context.read().toggleGlossaryPanel(); - } - - @override - Widget build(BuildContext context) { - if (_controller == null) return const Center(child: CircularProgressIndicator()); - - return GestureDetector( - onPanEnd: (details) { - if (details.velocity.pixelsPerSecond.dx < -500) { - _handleSwipeLeft(); - } - }, - child: Column( - children: [ - Container( - decoration: BoxDecoration( - border: Border(bottom: BorderSide(color: Colors.grey[300]!)), - ), - child: Row( - children: [ - Expanded( - child: quill.QuillSimpleToolbar( - key: ValueKey('toolbar_${widget.editorIndex}_${widget.document.id}'), - controller: _controller!, - config: const quill.QuillSimpleToolbarConfig( - showBoldButton: true, - showItalicButton: true, - showUnderLineButton: true, - showStrikeThrough: true, - showFontSize: true, - showFontFamily: true, - showColorButton: true, - showBackgroundColorButton: true, - showAlignmentButtons: true, - showListNumbers: true, - showListBullets: true, - showIndent: true, - ), - ), - ), - Container( - decoration: BoxDecoration( - border: Border(left: BorderSide(color: Colors.grey[300]!)), - ), - child: IconButton( - icon: const Icon(Icons.book, size: 22), - onPressed: _addSelectedToGlossary, // ✅ Кнопка добавляет и открывает - tooltip: 'Глоссарий', - color: Colors.blue[700], - ), - ), - ], - ), - ), - Expanded( - child: quill.QuillEditor( - key: ValueKey('editor_${widget.editorIndex}_${widget.document.id}'), - controller: _controller!, - config: quill.QuillEditorConfig( - placeholder: 'Начните печатать...', - padding: const EdgeInsets.all(16), - ), - scrollController: ScrollController(), - focusNode: FocusNode(), - ), - ), - ], - ), - ); - } -} - -class _MobileDocList extends StatelessWidget { - const _MobileDocList(); - @override - Widget build(BuildContext context) { - final state = context.read(); - return Scaffold( - appBar: AppBar( - title: Text(state.selectedProject!.name), - leading: IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => state.clearSelectedProject()), - actions: [ - IconButton( - icon: Icon(state.isGraphView ? Icons.list : Icons.account_tree), - onPressed: () => state.toggleViewMode(), - tooltip: state.isGraphView ? 'Список' : 'Граф', - ), - IconButton(icon: const Icon(Icons.add), onPressed: () => _showCreateDocumentDialog(context), tooltip: 'Новый документ'), - ], - ), - body: Selector( - selector: (_, state) => state.isGraphView, - builder: (context, isGraphView, _) { - return isGraphView ? const _DocumentsGraph() : const _DocumentsList(); - }, - ), - floatingActionButton: FloatingActionButton( - onPressed: () => _showCreateDocumentDialog(context), - child: const Icon(Icons.add), - ), - ); - } - - void _showCreateDocumentDialog(BuildContext context) { - final controller = TextEditingController(); - final formKey = GlobalKey(); - final state = context.read(); - final isDarkMode = state.isDarkMode; - showDialog( - context: context, - builder: (context) => AlertDialog( - backgroundColor: isDarkMode ? Colors.grey[850] : Colors.white, - title: Text('Новый документ', style: TextStyle(color: isDarkMode ? Colors.white : Colors.black87)), - content: Form( - key: formKey, - child: TextFormField( - controller: controller, - autofocus: true, - style: TextStyle(color: isDarkMode ? Colors.white : Colors.black87), - decoration: InputDecoration( - labelText: 'Название документа', - labelStyle: TextStyle(color: isDarkMode ? Colors.grey[400] : Colors.grey[600]), - border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), - focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8), borderSide: BorderSide(color: Colors.green[700]!)), - prefixIcon: Icon(Icons.description, color: Colors.green[700]), - ), - validator: (value) { - if (value == null || value.trim().isEmpty) return 'Введите название документа'; - return null; - }, - onFieldSubmitted: (_) { - if (formKey.currentState!.validate()) { - state.addDocumentToCurrentProject(controller.text.trim()); - Navigator.pop(context); - } - }, - ), - ), - actions: [ - TextButton(onPressed: () => Navigator.pop(context), child: Text('Отмена', style: TextStyle(color: Colors.grey[600]))), - ElevatedButton( - onPressed: () { - if (formKey.currentState!.validate()) { - state.addDocumentToCurrentProject(controller.text.trim()); - Navigator.pop(context); - } - }, - style: ElevatedButton.styleFrom(backgroundColor: Colors.green[700], foregroundColor: Colors.white), - child: const Text('Создать'), - ), - ], - ), - ); - } -} - -class _MobileEditorView extends StatelessWidget { - final AppDocument document; - const _MobileEditorView({required this.document}); - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: Text(document.name), leading: IconButton(icon: const Icon(Icons.arrow_back), onPressed: () {})), - body: QuillEditorView(document: document), - ); - } -} - -class TestApp extends StatelessWidget { - const TestApp({super.key}); - - @override - Widget build(BuildContext context) { - return ChangeNotifierProvider( - create: (_) => AppState(), - child: MaterialApp( - title: 'Manyllines Test', - home: const AppShell(), - ), - ); - } } \ No newline at end of file diff --git a/manylines_editor/lib/pages/projects/widgets/settings_list.dart b/manylines_editor/lib/pages/projects/widgets/settings_list.dart index 84f8312..81d6373 100644 --- a/manylines_editor/lib/pages/projects/widgets/settings_list.dart +++ b/manylines_editor/lib/pages/projects/widgets/settings_list.dart @@ -12,8 +12,8 @@ class SettingsList extends StatelessWidget { Widget build(BuildContext context) { final settingState = context.watch(); final isDarkMode = settingState.isDarkMode; - final bgColor = isDarkMode ? Colors.blue[900]! : Colors.blue[50]!; // ✅ Добавьте ! - final borderColor = isDarkMode ? Colors.blue[700]! : Colors.blue[200]!; // ✅ Добавьте ! + final bgColor = isDarkMode ? Colors.blue[900]! : Colors.blue[50]!; + final borderColor = isDarkMode ? Colors.blue[700]! : Colors.blue[200]!; final textColor = isDarkMode ? Colors.white : Colors.black87; if (settingState.switchableValue) { diff --git a/manylines_editor/lib/pages/workspace/widgets/editor_area.dart b/manylines_editor/lib/pages/workspace/widgets/editor_area.dart index 3e39c25..9d9d403 100644 --- a/manylines_editor/lib/pages/workspace/widgets/editor_area.dart +++ b/manylines_editor/lib/pages/workspace/widgets/editor_area.dart @@ -12,13 +12,13 @@ class EditorArea extends StatelessWidget { @override Widget build(BuildContext context) { - final projectState = context.watch(); final documentState = context.watch(); + final settingState = context.watch(); final showTwoEditors = documentState.secondSelectedDocument != null; - final borderColor = context.watch().isDarkMode + final borderColor = settingState.isDarkMode ? Colors.grey[700]! : Colors.grey[300]!; - final textColor = context.watch().isDarkMode + final textColor = settingState.isDarkMode ? Colors.white : Colors.black87; if (showTwoEditors) { @@ -71,7 +71,7 @@ class EditorArea extends StatelessWidget { ), Expanded( child: QuillEditorWrapper( - documentId: selectedDocument.id, + document: selectedDocument, editorIndex: 1, ), ), @@ -95,7 +95,7 @@ class EditorArea extends StatelessWidget { ), child: documentState.selectedDocument != null ? QuillEditorWrapper( - documentId: documentState.selectedDocument!.id, + document: documentState.selectedDocument!, editorIndex: 1, ) : Center(child: Text('Выберите документ', style: TextStyle(color: textColor))), @@ -111,7 +111,7 @@ class EditorArea extends StatelessWidget { Expanded( child: documentState.secondSelectedDocument != null ? QuillEditorWrapper( - documentId: documentState.secondSelectedDocument!.id, + document: documentState.secondSelectedDocument!, editorIndex: 2, ) : Center(child: Text('Выберите документ', style: TextStyle(color: textColor))), diff --git a/manylines_editor/lib/pages/workspace/widgets/glossary_entry_tile.dart b/manylines_editor/lib/pages/workspace/widgets/glossary_entry_tile.dart index aef885e..6ecd1eb 100644 --- a/manylines_editor/lib/pages/workspace/widgets/glossary_entry_tile.dart +++ b/manylines_editor/lib/pages/workspace/widgets/glossary_entry_tile.dart @@ -1,3 +1,5 @@ +// lib/pages/workspace/widgets/glossary_entry_tile.dart + import 'package:flutter/material.dart'; import '../../../entities/glossary_entry/glossary_entry.dart'; @@ -31,9 +33,20 @@ class _GlossaryEntryTileState extends State { @override void initState() { super.initState(); + // ✅ Создаём контроллер с текущим определением _definitionController = TextEditingController(text: widget.entry.definition); } + @override + void didUpdateWidget(GlossaryEntryTile oldWidget) { + super.didUpdateWidget(oldWidget); + // ✅ Обновляем контроллер если entry изменился + if (oldWidget.entry.id != widget.entry.id || + oldWidget.entry.definition != widget.entry.definition) { + _definitionController.text = widget.entry.definition; + } + } + @override void dispose() { _definitionController.dispose(); @@ -102,7 +115,9 @@ class _GlossaryEntryTileState extends State { decoration: InputDecoration( hintText: 'Введите определение...', hintStyle: TextStyle(color: Colors.grey[500]), - border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), filled: true, fillColor: widget.isDarkMode ? Colors.grey[800] : Colors.white, ), diff --git a/manylines_editor/lib/pages/workspace/widgets/glossary_panel.dart b/manylines_editor/lib/pages/workspace/widgets/glossary_panel.dart index 50792f7..33ea310 100644 --- a/manylines_editor/lib/pages/workspace/widgets/glossary_panel.dart +++ b/manylines_editor/lib/pages/workspace/widgets/glossary_panel.dart @@ -39,9 +39,7 @@ class GlossaryPanel extends StatelessWidget { ), child: Column( children: [ - _buildHeader(document.name, textColor, context), - if (documentState.selectedTextForGlossary != null) - _buildAddEntrySection(documentState.selectedTextForGlossary!, textColor, isDarkMode, context), + _buildHeader(document.name, textColor, isDarkMode, context), Expanded( child: document.glossary.isEmpty ? _buildEmptyState(isDarkMode) @@ -67,10 +65,10 @@ class GlossaryPanel extends StatelessWidget { ); } - Widget _buildHeader(String documentName, Color textColor, BuildContext context) { + Widget _buildHeader(String documentName, Color textColor, bool isDarkMode, BuildContext context) { return Container( padding: const EdgeInsets.all(12), - color: context.watch().isDarkMode ? Colors.grey[800] : Colors.grey[200], + color: isDarkMode ? Colors.grey[800] : Colors.grey[200], child: Row( children: [ const Icon(Icons.book, size: 20), @@ -87,11 +85,36 @@ class GlossaryPanel extends StatelessWidget { ); } + Widget _buildEmptyState(bool isDarkMode) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.book_outlined, size: 48, color: Colors.grey[400]), + const SizedBox(height: 16), + Text('Глоссарий пуст', style: TextStyle(color: Colors.grey[600])), + const SizedBox(height: 8), + Text( + 'Выделите текст и нажмите кнопку 📖', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 12, color: Colors.grey[500]), + ), + ], + ), + ); + } +} + Widget _buildAddEntrySection( - String selectedText, Color textColor, bool isDarkMode, BuildContext context) { + String selectedText, + Color textColor, + bool isDarkMode, + String documentId, + BuildContext context, + ) { return Container( padding: const EdgeInsets.all(8), - color: isDarkMode ? Colors.green[900]!.withOpacity(0.3) : Colors.green[50], + color: isDarkMode ? Colors.blue[900]!.withOpacity(0.3) : Colors.blue[50], child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -101,7 +124,7 @@ class GlossaryPanel extends StatelessWidget { padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: isDarkMode ? Colors.grey[800] : Colors.white, - border: Border.all(color: Colors.green[700]!), + border: Border.all(color: Colors.blue[700]!), borderRadius: BorderRadius.circular(4), ), child: Text(selectedText, style: TextStyle(color: textColor)), @@ -111,11 +134,14 @@ class GlossaryPanel extends StatelessWidget { width: double.infinity, child: ElevatedButton.icon( onPressed: () => AddGlossaryEntryFeature.execute( - context, selectedText, context.watch().selectedDocument!.id), + context, + selectedText, + documentId, + ), icon: const Icon(Icons.add, size: 18), label: const Text('Добавить в глоссарий'), style: ElevatedButton.styleFrom( - backgroundColor: Colors.green[700], + backgroundColor: Colors.blue[700], foregroundColor: Colors.white, ), ), @@ -124,23 +150,3 @@ class GlossaryPanel extends StatelessWidget { ), ); } - - Widget _buildEmptyState(bool isDarkMode) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.book_outlined, size: 48, color: Colors.grey[400]), - const SizedBox(height: 16), - Text('Глоссарий пуст', style: TextStyle(color: Colors.grey[600])), - const SizedBox(height: 8), - Text( - 'Выделите текст и свайпните влево', - textAlign: TextAlign.center, - style: TextStyle(fontSize: 12, color: Colors.grey[500]), - ), - ], - ), - ); - } -} \ No newline at end of file diff --git a/manylines_editor/lib/pages/workspace/widgets/side_panel.dart b/manylines_editor/lib/pages/workspace/widgets/side_panel.dart index 45f3d90..deed296 100644 --- a/manylines_editor/lib/pages/workspace/widgets/side_panel.dart +++ b/manylines_editor/lib/pages/workspace/widgets/side_panel.dart @@ -1,8 +1,15 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import '../../../entities/project/project.dart'; import '../../../entities/project/project_repository.dart'; +import '../../../entities/document/document_repository.dart'; +import '../../../entities/document/document.dart'; import '../../../entities/setting/setting_repository.dart'; import '../../../features/document/create_document.dart'; +import '../../../features/document/delete_document.dart'; +import '../../../features/document/toggle_pin.dart'; +import '../../../features/document/indent_document.dart'; +import '../../../features/document/outdent_document.dart'; class SidePanel extends StatelessWidget { const SidePanel({super.key}); @@ -45,6 +52,14 @@ class SidePanel extends StatelessWidget { style: TextStyle(color: textColor), ), ), + IconButton( + icon: Icon( + settingState.isGraphView ? Icons.list : Icons.account_tree, + color: textColor, + ), + onPressed: () => settingState.toggleViewMode(), + tooltip: settingState.isGraphView ? 'Список' : 'Граф', + ), ], ), ), @@ -54,7 +69,6 @@ class SidePanel extends StatelessWidget { child: Column( children: [ Expanded( - child: Selector( selector: (_, state) => state.isGraphView, builder: (context, isGraphView, _) { @@ -97,12 +111,385 @@ class SidePanel extends StatelessWidget { class _DocumentsList extends StatelessWidget { const _DocumentsList(); + @override - Widget build(BuildContext context) => const Center(child: Text('Documents List')); + Widget build(BuildContext context) { + final projectState = context.watch(); + final project = projectState.selectedProject; + + context.watch(); + + if (project == null) { + return const Center(child: Text('Нет проекта')); + } + + final pinnedDocs = project.pinnedDocuments; + final unpinnedDocs = project.unpinnedDocuments; + final isDarkMode = context.watch().isDarkMode; + + return Column( + children: [ + if (pinnedDocs.isNotEmpty) + Container( + color: isDarkMode ? Colors.green[900]!.withOpacity(0.3) : Colors.green[50], + child: ReorderableListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: pinnedDocs.length, + onReorder: (oldIndex, newIndex) => projectState.reorderPinnedDocuments(oldIndex, newIndex), + itemBuilder: (context, index) { + final doc = pinnedDocs[index]; + final isSelected = context.watch().selectedDocument?.id == doc.id; + + return Container( + key: ValueKey(doc.id), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: isDarkMode ? Colors.green[700]! : Colors.green[200]!, + ), + ), + ), + child: ListTile( + selected: isSelected, + selectedTileColor: Theme.of(context).colorScheme.secondaryContainer, + leading: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '${index + 1}.', + style: TextStyle( + fontSize: 12, + color: Colors.green[700], + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(width: 8), + Icon( + Icons.push_pin, + size: 16, + color: Colors.green[700], + ), + ], + ), + title: Text(doc.name), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + GestureDetector( + onTap: () {}, + child: Checkbox( + value: doc.isPinned, + activeColor: Colors.green[700], + onChanged: (value) { + TogglePinFeature.execute(context, doc); + }, + ), + ), + const SizedBox(width: 4), + IconButton( + icon: const Icon(Icons.more_vert, size: 20), + onPressed: () => _showDeleteMenu(context, doc), + tooltip: 'Меню', + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + Icon( + Icons.drag_handle, + color: isDarkMode ? Colors.white54 : Colors.grey, + ), + ], + ), + onTap: () { + final repo = context.read(); + repo.selectDocument(doc); + }, + onLongPress: () => context.read().selectSecondDocument(doc), + ), + ); + }, + ), + ), + Expanded( + child: Container( + color: isDarkMode ? Colors.blue[900]!.withOpacity(0.2) : Colors.blue[50], + child: _buildDismissibleList(project, unpinnedDocs, context), + ), + ), + ], + ); + } + + Widget _buildDismissibleList(Project project, List docs, BuildContext context) { + final isDarkMode = context.watch().isDarkMode; + + return ListView.builder( + itemCount: docs.length, + itemBuilder: (context, index) { + final doc = docs[index]; + final isSelected = context.watch().selectedDocument?.id == doc.id; + final actualIndex = project.documents.indexOf(doc); + + String number; + int rootCount = 0; + int childCount = 0; + + for (int i = 0; i <= index; i++) { + if (docs[i].parentId == null) { + rootCount++; + childCount = 0; + } else { + childCount++; + } + } + + if (doc.parentId == null) { + number = '$rootCount.'; + } else { + number = '$rootCount.$childCount'; + } + + return Dismissible( + key: ValueKey(doc.id), + direction: DismissDirection.horizontal, + background: Container( + color: Colors.blue, + alignment: Alignment.centerLeft, + padding: const EdgeInsets.only(left: 16), + child: const Icon(Icons.arrow_back, color: Colors.white), + ), + secondaryBackground: Container( + color: Colors.green, + alignment: Alignment.centerRight, + padding: const EdgeInsets.only(right: 16), + child: const Icon(Icons.arrow_forward, color: Colors.white), + ), + confirmDismiss: (direction) async { + if (direction == DismissDirection.startToEnd) { + IndentDocumentFeature.execute(context, actualIndex); + } else if (direction == DismissDirection.endToStart) { + OutdentDocumentFeature.execute(context, actualIndex); + } + return false; + }, + child: Container( + decoration: BoxDecoration( + color: doc.parentId == null + ? (isDarkMode ? Colors.blue[900]!.withOpacity(0.2) : Colors.blue[50]) + : (isDarkMode ? Colors.green[900]!.withOpacity(0.3) : Colors.green[50]), + border: Border( + bottom: BorderSide( + color: isDarkMode + ? (doc.parentId == null ? Colors.blue[700]! : Colors.green[700]!) + : (doc.parentId == null ? Colors.blue[200]! : Colors.green[200]!), + ), + ), + ), + child: ListTile( + selected: isSelected, + selectedTileColor: Theme.of(context).colorScheme.secondaryContainer, + leading: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + number, + style: TextStyle( + fontSize: 12, + color: doc.parentId == null ? Colors.blue[700] : Colors.green[700], + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(width: 8), + Icon( + doc.parentId == null ? Icons.insert_drive_file : Icons.subdirectory_arrow_right, + size: 16, + color: doc.parentId == null ? Colors.blue[700] : Colors.green[700], + ), + ], + ), + title: Text(doc.name), + subtitle: doc.parentId != null ? Text('Поддокумент', style: TextStyle(fontSize: 10, color: Colors.grey[600])) : null, + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + GestureDetector( + onTap: () {}, + child: Checkbox( + value: doc.isPinned, + activeColor: Colors.green[700], + onChanged: (value) { + TogglePinFeature.execute(context, doc); + }, + ), + ), + const SizedBox(width: 4), + if (doc.parentId != null) + Icon(Icons.swipe, size: 16, color: Colors.grey[500]), + IconButton( + icon: const Icon(Icons.more_vert, size: 20), + onPressed: () => _showDeleteMenu(context, doc), + tooltip: 'Меню', + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + ], + ), + onTap: () { + final repo = context.read(); + repo.selectDocument(doc); + }, + onLongPress: () => context.read().selectSecondDocument(doc), + ), + ), + ); + }, + ); + } + + void _showDeleteMenu(BuildContext context, AppDocument doc) { + DeleteDocumentFeature.showConfirmation(context, doc); + } } class _DocumentsGraph extends StatelessWidget { const _DocumentsGraph(); + @override - Widget build(BuildContext context) => const Center(child: Text('Documents Graph')); + Widget build(BuildContext context) { + final projectState = context.watch(); + final project = projectState.selectedProject; + final docs = project?.documents ?? []; + final isDarkMode = context.watch().isDarkMode; + + if (docs.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.account_tree_outlined, size: 64, color: Colors.grey[400]), + const SizedBox(height: 16), + Text('Нет документов', style: TextStyle(color: Colors.grey[600])), + ], + ), + ); + } + + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ..._buildDocumentNodes(docs, context), + ], + ), + ), + ); + } + + List _buildDocumentNodes(List docs, BuildContext context) { + final widgets = []; + final rootDocs = docs.where((d) => d.parentId == null).toList(); + + for (var doc in rootDocs) { + widgets.add(_buildDocumentNode(doc, docs, context)); + widgets.add(const SizedBox(height: 20)); + } + + return widgets; + } + + Widget _buildDocumentNode(AppDocument doc, List allDocs, BuildContext context) { + final isSelected = context.watch().selectedDocument?.id == doc.id; + final children = allDocs.where((d) => d.parentId == doc.id).toList(); + final isDarkMode = context.watch().isDarkMode; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + onTap: () { + final repo = context.read(); + repo.selectDocument(doc); + }, + onLongPress: () => context.read().selectSecondDocument(doc), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + decoration: BoxDecoration( + color: isSelected + ? (isDarkMode ? Colors.green[800] : Colors.green[100]) + : (isDarkMode ? Colors.grey[800] : Colors.white), + border: Border.all( + color: isSelected + ? Colors.green[700]! + : (isDarkMode ? Colors.grey[600]! : Colors.grey[400]!), + width: 2, + ), + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + doc.isPinned ? Icons.push_pin : Icons.insert_drive_file, + color: isSelected ? Colors.green[700] : Colors.grey[600], + size: 20, + ), + const SizedBox(width: 8), + Text( + doc.name, + style: TextStyle( + fontSize: 14, + fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, + color: isDarkMode ? Colors.white : Colors.black87, + ), + ), + IconButton( + icon: const Icon(Icons.more_vert, size: 18), + onPressed: () => _showDeleteMenuInGraph(context, doc), + tooltip: 'Меню', + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + ], + ), + ), + ), + + if (children.isNotEmpty) ...[ + const SizedBox(height: 8), + ...children.asMap().entries.map((entry) { + final childDoc = entry.value; + return Padding( + padding: const EdgeInsets.only(left: 40), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container(width: 30, height: 2, color: isDarkMode ? Colors.grey[600] : Colors.grey[400]), + Icon(Icons.arrow_forward, size: 16, color: isDarkMode ? Colors.grey[600] : Colors.grey[400]), + ], + ), + const SizedBox(height: 8), + _buildDocumentNode(childDoc, allDocs, context), + ], + ), + ); + }).toList(), + ], + ], + ); + } + + void _showDeleteMenuInGraph(BuildContext context, AppDocument doc) { + DeleteDocumentFeature.showConfirmation(context, doc); + } } \ No newline at end of file diff --git a/manylines_editor/lib/pages/workspace/workspace_page.dart b/manylines_editor/lib/pages/workspace/workspace_page.dart index 096f917..2ef5ee4 100644 --- a/manylines_editor/lib/pages/workspace/workspace_page.dart +++ b/manylines_editor/lib/pages/workspace/workspace_page.dart @@ -1,10 +1,12 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../entities/document/document_repository.dart'; +import '../../entities/document/document.dart'; +import '../../entities/setting/setting_repository.dart'; import '../../entities/project/project_repository.dart'; import '../../widgets/quill_editor_wrapper.dart'; +import '../../features/document/create_document.dart'; import 'widgets/side_panel.dart'; -import 'widgets/editor_area.dart'; import 'widgets/glossary_panel.dart'; class WorkspacePage extends StatelessWidget { @@ -18,27 +20,17 @@ class WorkspacePage extends StatelessWidget { if (!isWide) { return const _MobileWorkspace(); } - return const _DesktopWorkspace(); + return Selector( + selector: (_, repo) => repo.selectedDocument, + builder: (context, selectedDocument, _) { + return _buildDesktopLayout(context, selectedDocument); + }, + ); }, ); } } -class _DesktopWorkspace extends StatelessWidget { - const _DesktopWorkspace(); - - @override - Widget build(BuildContext context) { - return const Row( - children: [ - SidePanel(), - EditorArea(), - GlossaryPanel(), - ], - ); - } -} - class _MobileWorkspace extends StatelessWidget { const _MobileWorkspace(); @@ -52,7 +44,7 @@ class _MobileWorkspace extends StatelessWidget { } return QuillEditorWrapper( - documentId: document.id, + document: document, editorIndex: 1, ); } @@ -86,4 +78,262 @@ class _MobileEmptyState extends StatelessWidget { ), ); } +} + +Widget _buildDesktopLayout(BuildContext context, AppDocument? selectedDocument) { + final settingState = context.watch(); + final documentState = context.watch(); + + final leftPanelBg = settingState.isDarkMode ? Colors.grey[900] : Colors.white; + final headerBg = settingState.isDarkMode ? Colors.green[900] : Colors.green[50]; + final textColor = settingState.isDarkMode ? Colors.white : Colors.black87; + final borderColor = settingState.isDarkMode ? Colors.grey[700]! : Colors.grey[300]!; + + final showTwoEditors = documentState.secondSelectedDocument != null; + final isPanelCollapsed = settingState.isSidePanelCollapsed; + final isGlossaryOpen = documentState.isGlossaryPanelOpen; + + return Scaffold( + body: Row( + children: [ + AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + width: isPanelCollapsed ? 0 : 300, + decoration: BoxDecoration( + border: Border(right: BorderSide(color: borderColor)), + ), + child: isPanelCollapsed + ? const SizedBox.shrink() + : const SidePanel(), + ), + + Container( + width: 24, + decoration: BoxDecoration( + color: isPanelCollapsed ? (settingState.isDarkMode ? Colors.grey[800] : Colors.grey[200]) : Colors.transparent, + border: Border(right: BorderSide(color: borderColor)), + ), + child: Column( + children: [ + const SizedBox(height: 100), + GestureDetector( + onTap: () => settingState.toggleSidePanel(), + child: Container( + width: 24, + height: 82, + decoration: BoxDecoration( + color: settingState.isDarkMode ? Colors.grey[700] : Colors.grey[300], + ), + child: Icon( + isPanelCollapsed ? Icons.chevron_right : Icons.chevron_left, + size: 20, + color: textColor, + ), + ), + ), + const Expanded(child: SizedBox()), + ], + ), + ), + + Expanded( + child: showTwoEditors + ? _buildTwoEditorsLayout(context, borderColor, textColor) + : _buildSingleEditorLayout(context, selectedDocument, textColor), + ), + + if (isGlossaryOpen) ...[ + Container( + width: 24, + decoration: BoxDecoration( + color: settingState.isDarkMode ? Colors.grey[800] : Colors.grey[200], + border: Border(right: BorderSide(color: borderColor)), + ), + child: Column( + children: [ + const SizedBox(height: 100), + GestureDetector( + onTap: () => documentState.toggleGlossaryPanel(), + child: Container( + width: 24, + height: 82, + decoration: BoxDecoration( + color: settingState.isDarkMode ? Colors.grey[700] : Colors.grey[300], + ), + child: const Icon(Icons.chevron_right, size: 20, color: Colors.white), + ), + ), + const Expanded(child: SizedBox()), + ], + ), + ), + const GlossaryPanel(), + ] else + Container( + width: 24, + decoration: BoxDecoration( + color: settingState.isDarkMode ? Colors.grey[800] : Colors.grey[200], + border: Border(right: BorderSide(color: borderColor)), + ), + child: Column( + children: [ + const SizedBox(height: 100), + GestureDetector( + onTap: () => documentState.openGlossaryPanel(), + child: Container( + width: 24, + height: 82, + decoration: BoxDecoration( + color: settingState.isDarkMode ? Colors.blue[700] : Colors.blue[300], + ), + child: const Icon(Icons.chevron_left, size: 20, color: Colors.white), + ), + ), + const Expanded(child: SizedBox()), + ], + ), + ), + ], + ), + + persistentFooterButtons: [ + FloatingActionButton( + heroTag: 'createDoc', + onPressed: () => CreateDocumentFeature.show(context), + tooltip: 'Новый документ', + child: const Icon(Icons.add), + ), + ], + + floatingActionButton: Selector( + selector: (_, state) => state.isGraphView, + builder: (context, isGraphView, _) { + return FloatingActionButton( + onPressed: () => settingState.toggleViewMode(), + tooltip: isGraphView ? 'Список' : 'Граф', + child: Icon(isGraphView ? Icons.list : Icons.account_tree), + ); + }, + ), + ); +} + +Widget _buildSingleEditorLayout(BuildContext context, AppDocument? selectedDocument, Color textColor) { + if (selectedDocument == null) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.description_outlined, size: 64, color: Colors.grey[400]), + const SizedBox(height: 16), + Text('Выберите документ', style: TextStyle(color: textColor)), + ], + ), + ); + } + + return Column( + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + color: context.watch().isDarkMode ? Colors.grey[850] : Colors.grey[100], + child: Row( + children: [ + Expanded( + child: Text( + selectedDocument.name, + style: TextStyle(fontWeight: FontWeight.w500, color: textColor), + overflow: TextOverflow.ellipsis, + ), + ), + IconButton( + icon: const Icon(Icons.close, size: 20), + onPressed: () => context.read().closeFirstEditor(), + tooltip: 'Закрыть', + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + ], + ), + ), + Expanded( + child: QuillEditorWrapper( + document: selectedDocument, + editorIndex: 1, + ), + ), + ], + ); +} + +Widget _buildTwoEditorsLayout(BuildContext context, Color borderColor, Color textColor) { + final documentState = context.watch(); + + return Row( + children: [ + Expanded( + child: Column( + children: [ + _buildEditorHeader(context, 1, textColor, documentState.selectedDocument), + Expanded( + child: Container( + decoration: BoxDecoration(border: Border(right: BorderSide(color: borderColor))), + child: documentState.selectedDocument != null + ? QuillEditorWrapper( + document: documentState.selectedDocument!, + editorIndex: 1, + ) + : Center(child: Text('Выберите документ', style: TextStyle(color: textColor))), + ), + ), + ], + ), + ), + Expanded( + child: Column( + children: [ + _buildEditorHeader(context, 2, textColor, documentState.secondSelectedDocument), + Expanded( + child: documentState.secondSelectedDocument != null + ? QuillEditorWrapper( + document: documentState.secondSelectedDocument!, + editorIndex: 2, + ) + : Center(child: Text('Выберите документ', style: TextStyle(color: textColor))), + ), + ], + ), + ), + ], + ); +} + +Widget _buildEditorHeader(BuildContext context, int index, Color textColor, AppDocument? doc) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + color: context.watch().isDarkMode ? Colors.grey[850] : Colors.grey[100], + child: Row( + children: [ + Expanded( + child: Text( + doc?.name ?? (index == 1 ? 'Первый редактор' : 'Второй редактор'), + style: TextStyle(fontWeight: FontWeight.w500, color: textColor), + overflow: TextOverflow.ellipsis, + ), + ), + IconButton( + icon: const Icon(Icons.close, size: 20), + onPressed: () { + if (doc != null) { + context.read().closeEditorIfOpen(doc.id); + } + }, + tooltip: 'Закрыть', + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + ], + ), + ); } \ No newline at end of file diff --git a/manylines_editor/lib/widgets/quill_editor_wrapper.dart b/manylines_editor/lib/widgets/quill_editor_wrapper.dart index 93e1524..39e711e 100644 --- a/manylines_editor/lib/widgets/quill_editor_wrapper.dart +++ b/manylines_editor/lib/widgets/quill_editor_wrapper.dart @@ -1,16 +1,18 @@ import 'package:flutter/material.dart'; import 'package:flutter_quill/flutter_quill.dart' as quill; import 'package:provider/provider.dart'; -import '../../entities/document/document_repository.dart'; +import '../entities/document/document.dart'; +import '../entities/document/document_repository.dart'; +import '../entities/glossary_entry/glossary_entry.dart'; import '../features/editor/handle_text_selection.dart'; class QuillEditorWrapper extends StatefulWidget { - final String documentId; + final AppDocument document; final int editorIndex; const QuillEditorWrapper({ super.key, - required this.documentId, + required this.document, this.editorIndex = 1, }); @@ -20,27 +22,69 @@ class QuillEditorWrapper extends StatefulWidget { class _QuillEditorWrapperState extends State { quill.QuillController? _controller; - late FocusNode _focusNode; - late ScrollController _scrollController; @override void initState() { super.initState(); - _focusNode = FocusNode(); - _scrollController = ScrollController(); _initializeController(); } + @override + void didUpdateWidget(QuillEditorWrapper oldWidget) { + super.didUpdateWidget(oldWidget); + + if (oldWidget.document.id != widget.document.id) { + _initializeController(); + } + } + void _initializeController() { final repo = context.read(); - _controller = repo.getOrCreateController(widget.documentId); + _controller = repo.getOrCreateController(widget.document); + } + + void _addSelectedToGlossary() { + print('🔍 _addSelectedToGlossary вызван'); + + final selectedText = _getSelectedText(); + print('🔍 Выделенный текст: $selectedText'); + + if (selectedText != null) { + print('🔍 Добавляем термин: $selectedText'); + print('🔍 Document ID: ${widget.document.id}'); + + final repo = context.read(); + repo.addGlossaryEntry(widget.document.id, selectedText); + + print('✅ Термин добавлен'); + print('Открываем Глоссарий'); + repo.openGlossaryPanel(); + } else { + print('❌ Текст не выделен'); + } +} + + String? _getSelectedText() { + if (_controller == null) return null; + + final selection = _controller!.selection; + if (selection.isCollapsed) return null; + + final text = _controller!.document.toPlainText(); + if (selection.baseOffset >= text.length || selection.extentOffset >= text.length) return null; + + final start = selection.baseOffset < selection.extentOffset + ? selection.baseOffset : selection.extentOffset; + final end = selection.baseOffset < selection.extentOffset + ? selection.extentOffset : selection.baseOffset; + + final selectedText = text.substring(start, end); + return selectedText.trim().isNotEmpty ? selectedText.trim() : null; } @override void dispose() { - _controller?.dispose(); - _focusNode.dispose(); - _scrollController.dispose(); + _controller = null; super.dispose(); } @@ -58,27 +102,45 @@ class _QuillEditorWrapperState extends State { }, child: Column( children: [ - quill.QuillSimpleToolbar( - controller: _controller!, - config: const quill.QuillSimpleToolbarConfig( - showBoldButton: true, - showItalicButton: true, - showUnderLineButton: true, - showFontSize: true, - showAlignmentButtons: true, - showListNumbers: true, - showListBullets: true, - ), + Row( + children: [ + Expanded( + child: quill.QuillSimpleToolbar( + controller: _controller!, + config: const quill.QuillSimpleToolbarConfig( + showBoldButton: true, + showItalicButton: true, + showUnderLineButton: true, + showFontSize: true, + showAlignmentButtons: true, + showListNumbers: true, + showListBullets: true, + ), + ), + ), + Container( + decoration: BoxDecoration( + border: Border(left: BorderSide(color: Colors.grey[300]!)), + ), + child: IconButton( + icon: const Icon(Icons.book, size: 22), + onPressed: _addSelectedToGlossary, + tooltip: 'Добавить в глоссарий', + color: Colors.blue[700], + ), + ), + ], ), Expanded( child: quill.QuillEditor( + key: ValueKey('editor_${widget.document.id}_${widget.editorIndex}'), controller: _controller!, config: const quill.QuillEditorConfig( placeholder: 'Начните печатать...', padding: EdgeInsets.all(16), ), - focusNode: _focusNode, - scrollController: _scrollController, + focusNode: FocusNode(), + scrollController: ScrollController(), ), ), ], diff --git a/manylines_editor/test/widget_test.dart b/manylines_editor/test/widget_test.dart index b2b53aa..1d580ad 100644 --- a/manylines_editor/test/widget_test.dart +++ b/manylines_editor/test/widget_test.dart @@ -1,93 +1,71 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility in the flutter_test package. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:provider/provider.dart'; import 'package:manylines_editor/main.dart'; +import 'package:manylines_editor/app/providers.dart'; +import 'package:manylines_editor/entities/setting/setting_repository.dart'; +import 'package:manylines_editor/entities/project/project_repository.dart'; void main() { - // ✅ Простой тест: приложение загружается без крашей + testWidgets('App loads without crashing', (WidgetTester tester) async { - // Запускаем упрощённый TestApp вместо MyApp - await tester.pumpWidget(const TestApp()); - - // Даём время на отрисовку всех виджетов + await tester.pumpWidget(const ManyllinesApp()); await tester.pumpAndSettle(); - // ✅ Проверка 1: приложение содержит Scaffold expect(find.byType(Scaffold), findsOneWidget); - // ✅ Проверка 2: заголовок приложения отрисовался - expect(find.text('Manyllines'), findsOneWidget); - - // ✅ Проверка 3: экран проектов загрузился (ищем текст из ProjectsScreen) - expect(find.text('Logo'), findsOneWidget); + expect(find.text('Manylines'), findsOneWidget); }); - // ✅ Тест: создание проекта работает testWidgets('Can create a new project', (WidgetTester tester) async { - await tester.pumpWidget(const TestApp()); + await tester.pumpWidget(const ManyllinesApp()); await tester.pumpAndSettle(); - // Находим и нажимаем FAB для создания проекта - final fabFinder = find.byType(FloatingActionButton); - expect(fabFinder, findsOneWidget); + final fabFinder = find.byWidgetPredicate( + (widget) => widget is FloatingActionButton && widget.tooltip == 'Новый документ', + ); - await tester.tap(fabFinder); + if (fabFinder.evaluate().isEmpty) { + await tester.tap(find.byIcon(Icons.add)); + } else { + await tester.tap(fabFinder); + } await tester.pumpAndSettle(); - // Проверяем, что диалог создания проекта открылся - expect(find.text('Новый проект'), findsOneWidget); + expect(find.text('Новый документ'), findsOneWidget); - // Вводим название проекта await tester.enterText(find.byType(TextFormField), 'Test Project'); await tester.pump(); - // Нажимаем "Создать" await tester.tap(find.text('Создать')); await tester.pumpAndSettle(); - // Проверяем, что проект появился в списке expect(find.text('Test Project'), findsOneWidget); }); - // ✅ Тест: переключение темы работает (исправленная версия) testWidgets('Can toggle dark mode', (WidgetTester tester) async { - await tester.pumpWidget(const TestApp()); + await tester.pumpWidget(const ManyllinesApp()); await tester.pumpAndSettle(); - // ✅ Правильный способ получить AppState в тесте: - // Используем Provider.of через контекст виджета - final context = tester.element(find.byType(AppShell).first); - final state = Provider.of(context, listen: false); + final context = tester.element(find.byType(ManyllinesApp).first); + final settingRepo = Provider.of(context, listen: false); - // Меняем тему - state.toggleDarkMode(true); + settingRepo.toggleDarkMode(true); await tester.pump(); - // Проверяем, что тема изменилась (проверяем, что состояние обновилось) - expect(state.isDarkMode, isTrue); + expect(settingRepo.isDarkMode, isTrue); }); - // ✅ Тест: выбор проекта работает testWidgets('Can select a project', (WidgetTester tester) async { - await tester.pumpWidget(const TestApp()); + await tester.pumpWidget(const ManyllinesApp()); await tester.pumpAndSettle(); - // Находим первый проект в списке final projectTile = find.text('Project 1'); expect(projectTile, findsOneWidget); - // Нажимаем на проект await tester.tap(projectTile); await tester.pumpAndSettle(); - // Проверяем, что открылось рабочее пространство - expect(find.byType(ProjectWorkspace), findsOneWidget); + expect(find.byType(AnimatedContainer), findsOneWidget); }); } \ No newline at end of file From be2d05e9b6a7ecbbbbad6702e38e86cdd8b7c26d Mon Sep 17 00:00:00 2001 From: Ulquiorra-Keesre Date: Tue, 14 Apr 2026 16:41:28 +0300 Subject: [PATCH 22/22] commit for merge --- manylines_editor/pubspec.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/manylines_editor/pubspec.lock b/manylines_editor/pubspec.lock index 3cf1fef..798ce27 100644 --- a/manylines_editor/pubspec.lock +++ b/manylines_editor/pubspec.lock @@ -111,10 +111,10 @@ packages: dependency: transitive description: name: matcher - sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.19" + version: "0.12.18" material_color_utilities: dependency: transitive description: @@ -188,10 +188,10 @@ packages: dependency: transitive description: name: test_api - sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.7.9" vector_math: dependency: transitive description: