From 6cf7326d55182f78eac6cf2ec178d55fa2a49c89 Mon Sep 17 00:00:00 2001 From: Elena G Blanco Date: Sat, 13 Dec 2025 13:31:48 +0100 Subject: [PATCH 1/8] Convert steps to JSON --- README.md | 11 +- src/fireflake/.metadata | 2 +- src/fireflake/lib/author_info_page.dart | 147 +++++--- src/fireflake/lib/data/project_storage.dart | 58 ++++ src/fireflake/lib/info_views/projectinfo.dart | 35 +- src/fireflake/lib/main.dart | 11 +- src/fireflake/lib/models/author_settings.dart | 44 +++ src/fireflake/lib/models/character.dart | 10 + src/fireflake/lib/models/project.dart | 115 +++++++ src/fireflake/lib/models/scene.dart | 12 + src/fireflake/lib/projects_page.dart | 65 +++- src/fireflake/lib/projectwrapper.dart | 21 +- src/fireflake/lib/state/app_cubit.dart | 240 +++++++++++++ src/fireflake/lib/state/author_cubit.dart | 72 ++++ src/fireflake/lib/step_views/step1.dart | 97 +++++- src/fireflake/lib/step_views/step2.dart | 185 +++++++++- src/fireflake/lib/step_views/step3.dart | 10 +- src/fireflake/lib/step_views/step4.dart | 150 ++++++++- src/fireflake/lib/step_views/step5.dart | 191 ++++++++++- src/fireflake/lib/step_views/step6.dart | 127 +++++++ src/fireflake/lib/step_views/step7.dart | 139 ++++++++ src/fireflake/lib/step_views/step8.dart | 209 ++++++++++++ src/fireflake/lib/step_views/step9.dart | 316 ++++++++---------- src/fireflake/pubspec.lock | 201 ++++++++++- src/fireflake/pubspec.yaml | 6 + 25 files changed, 2223 insertions(+), 251 deletions(-) create mode 100644 src/fireflake/lib/data/project_storage.dart create mode 100644 src/fireflake/lib/models/author_settings.dart create mode 100644 src/fireflake/lib/state/app_cubit.dart create mode 100644 src/fireflake/lib/state/author_cubit.dart create mode 100644 src/fireflake/lib/step_views/step6.dart create mode 100644 src/fireflake/lib/step_views/step7.dart create mode 100644 src/fireflake/lib/step_views/step8.dart diff --git a/README.md b/README.md index 7c23ca8..f11c560 100644 --- a/README.md +++ b/README.md @@ -13,13 +13,16 @@ Ejemplo de app: [▶️ vídeo en YouTube](https://youtu.be/5UdYlEhAuTE) Los diferentes pasos incluyen: - 1, el resumen en una sola frase -- 2, ampliación del resulen con actos pricipales y final +- 2, ampliación del resumen con actos pricipales y final - 3, describiendo al protagonista, con los diferentes elementos: - -- 4, convitiendo cada frase del paso 2 y convertirlo en un nuevo párrafo + - motivaciones + - objetivos + - conflicto + - epifanía +- 4, recoge cada frase del paso 2 y convertirlo en un nuevo párrafo - 5, describir a los personajes pricipales - 6, volver al punto cuatro y ampliar argumento -- 7, crea tablas de personaje para los principales, siguiendo la estructura del punto tres +- 7, crea tablas de personaje para los principales, siguiendo la estructura del punto 3 - 8, usando el argumento ampliado, escribe una lista de las escenas que faltan para completar la historia - 9, de cada escena en la lista, escribe un resumen narrativo (con varios párrafos) diff --git a/src/fireflake/.metadata b/src/fireflake/.metadata index e2ff7d7..e8b36fd 100644 --- a/src/fireflake/.metadata +++ b/src/fireflake/.metadata @@ -15,7 +15,7 @@ migration: - platform: root create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 - - platform: web + - platform: windows create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 diff --git a/src/fireflake/lib/author_info_page.dart b/src/fireflake/lib/author_info_page.dart index 2196410..07063e3 100644 --- a/src/fireflake/lib/author_info_page.dart +++ b/src/fireflake/lib/author_info_page.dart @@ -1,7 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'state/author_cubit.dart'; +import 'models/author_settings.dart'; class AuthorInfoPage extends StatefulWidget { - const AuthorInfoPage({super.key}); + const AuthorInfoPage({super.key}); @override State createState() => _AuthorInfoPageState(); @@ -9,15 +12,35 @@ class AuthorInfoPage extends StatefulWidget { class _AuthorInfoPageState extends State { final _formKey = GlobalKey(); - String _name = ''; - String _bio = ''; - String _email = ''; + late TextEditingController _nameController; + late TextEditingController _bioController; + late TextEditingController _emailController; + + @override + void initState() { + super.initState(); + _nameController = TextEditingController(); + _bioController = TextEditingController(); + _emailController = TextEditingController(); + } + + @override + void dispose() { + _nameController.dispose(); + _bioController.dispose(); + _emailController.dispose(); + super.dispose(); + } void _submit() { if (_formKey.currentState!.validate()) { - _formKey.currentState!.save(); + context.read().updateAll( + name: _nameController.text, + bio: _bioController.text, + email: _emailController.text, + ); ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Author info saved')), + const SnackBar(content: Text('Información del autor guardada')), ); } } @@ -25,39 +48,87 @@ class _AuthorInfoPageState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: Text('Add Author Info')), - body: Padding( - padding: const EdgeInsets.all(16.0), - child: Form( - key: _formKey, - child: ListView( - children: [ - TextFormField( - decoration: InputDecoration(labelText: 'Name'), - validator: (value) => - value == null || value.isEmpty ? 'Enter name' : null, - onSaved: (value) => _name = value ?? '', - ), - TextFormField( - decoration: InputDecoration(labelText: 'Bio'), - maxLines: 3, - onSaved: (value) => _bio = value ?? '', - ), - TextFormField( - decoration: InputDecoration(labelText: 'Email'), - keyboardType: TextInputType.emailAddress, - validator: (value) => - value == null || value.isEmpty ? 'Enter email' : null, - onSaved: (value) => _email = value ?? '', - ), - SizedBox(height: 24), - ElevatedButton( - onPressed: _submit, - child: Text('Save'), + appBar: AppBar(title: const Text('Información del Autor')), + body: BlocBuilder( + builder: (context, settings) { + // Actualizar controladores cuando cambie el estado + if (_nameController.text != settings.name) { + _nameController.text = settings.name; + } + if (_bioController.text != settings.bio) { + _bioController.text = settings.bio; + } + if (_emailController.text != settings.email) { + _emailController.text = settings.email; + } + + return Padding( + padding: const EdgeInsets.all(16.0), + child: Form( + key: _formKey, + child: ListView( + children: [ + const Text( + 'Esta configuración se guarda de forma global y será usada en todos los proyectos.', + style: TextStyle( + fontStyle: FontStyle.italic, + color: Colors.grey, + ), + ), + const SizedBox(height: 16), + TextFormField( + controller: _nameController, + decoration: const InputDecoration( + labelText: 'Nombre', + border: OutlineInputBorder(), + ), + validator: (value) => + value == null || value.isEmpty ? 'Ingresa tu nombre' : null, + ), + const SizedBox(height: 16), + TextFormField( + controller: _bioController, + decoration: const InputDecoration( + labelText: 'Biografía', + border: OutlineInputBorder(), + alignLabelWithHint: true, + ), + maxLines: 5, + ), + const SizedBox(height: 16), + TextFormField( + controller: _emailController, + decoration: const InputDecoration( + labelText: 'Email', + border: OutlineInputBorder(), + ), + keyboardType: TextInputType.emailAddress, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Ingresa tu email'; + } + if (!value.contains('@')) { + return 'Ingresa un email válido'; + } + return null; + }, + ), + const SizedBox(height: 24), + ElevatedButton( + onPressed: _submit, + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.all(16), + ), + child: const Text( + 'Guardar', + style: TextStyle(fontSize: 16), + ), + ), + ], ), - ], - ), - ), + ), + ); + }, ), ); } diff --git a/src/fireflake/lib/data/project_storage.dart b/src/fireflake/lib/data/project_storage.dart new file mode 100644 index 0000000..eeb132e --- /dev/null +++ b/src/fireflake/lib/data/project_storage.dart @@ -0,0 +1,58 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart'; + +import '../models/project.dart'; + +class ProjectStorage { + static const String _folderName = 'projects'; + + static Future _baseDir() async { + final dir = await getApplicationDocumentsDirectory(); + final target = Directory(p.join(dir.path, 'fireflake', _folderName)); + if (!await target.exists()) { + await target.create(recursive: true); + } + return target; + } + + static String _fileName(String title) { + final slug = title.toLowerCase().replaceAll(RegExp(r'[^a-z0-9]+'), '-').replaceAll(RegExp(r'-+'), '-').trim(); + final safe = slug.isEmpty ? 'project' : slug; + return '$safe.json'; + } + + static Future> loadProjects() async { + final dir = await _baseDir(); + final files = dir.listSync().whereType().where((f) => f.path.endsWith('.json')); + final projects = []; + for (final file in files) { + try { + final jsonStr = await file.readAsString(); + final map = json.decode(jsonStr) as Map; + projects.add(Project.fromJson(map)); + } catch (_) { + // ignore malformed files + } + } + return projects; + } + + static Future loadProjectByTitle(String title) async { + final dir = await _baseDir(); + final file = File(p.join(dir.path, _fileName(title))); + if (!await file.exists()) return null; + final jsonStr = await file.readAsString(); + final map = json.decode(jsonStr) as Map; + return Project.fromJson(map); + } + + static Future saveProject(Project project) async { + final dir = await _baseDir(); + final file = File(p.join(dir.path, _fileName(project.title))); + final jsonStr = json.encode(project.toJson()); + await file.writeAsString(jsonStr); + } +} diff --git a/src/fireflake/lib/info_views/projectinfo.dart b/src/fireflake/lib/info_views/projectinfo.dart index 90599a0..e193000 100644 --- a/src/fireflake/lib/info_views/projectinfo.dart +++ b/src/fireflake/lib/info_views/projectinfo.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../state/app_cubit.dart'; class ProjectInfoPage extends StatefulWidget { const ProjectInfoPage({super.key}); @@ -13,6 +15,7 @@ class _ProjectInfoPageState extends State { final TextEditingController _titleController = TextEditingController(); final TextEditingController _subtitleController = TextEditingController(); final TextEditingController _wordCountController = TextEditingController(); + bool _initializedFromState = false; @override void dispose() { @@ -24,12 +27,19 @@ class _ProjectInfoPageState extends State { void _saveProject() { if (_formKey.currentState!.validate()) { - // ScaffoldMessenger.of(context).showSnackBar( - // const SnackBar( - // content: Text('Información del proyecto guardada'), - // backgroundColor: Colors.green, - // ), - // ); + final cubit = context.read(); + final wordCount = int.parse(_wordCountController.text); + cubit.saveProject( + title: _titleController.text.trim(), + subtitle: _subtitleController.text.trim(), + expectedWordCount: wordCount, + ); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Proyecto guardado'), + backgroundColor: Colors.green, + ), + ); } } @@ -39,6 +49,19 @@ class _ProjectInfoPageState extends State { _wordCountController.clear(); } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (_initializedFromState) return; + final project = context.read().state.selectedProject; + if (project != null) { + _titleController.text = project.title; + _subtitleController.text = project.subtitle; + _wordCountController.text = project.expectedWordCount.toString(); + } + _initializedFromState = true; + } + @override Widget build(BuildContext context) { return Scaffold( diff --git a/src/fireflake/lib/main.dart b/src/fireflake/lib/main.dart index efde0bd..6c6ee92 100644 --- a/src/fireflake/lib/main.dart +++ b/src/fireflake/lib/main.dart @@ -1,4 +1,7 @@ import 'package:fireflake/projects_page.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'state/app_cubit.dart'; +import 'state/author_cubit.dart'; import 'package:flutter/material.dart'; void main() { @@ -10,11 +13,17 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - return MaterialApp( + return MultiBlocProvider( + providers: [ + BlocProvider(create: (_) => AppCubit()), + BlocProvider(create: (_) => AuthorCubit()), + ], + child: MaterialApp( debugShowCheckedModeBanner: false, title: 'Fireflake', theme: ThemeData(brightness: Brightness.light), home: HomePage(title: 'Fireflake'), + ), ); } } diff --git a/src/fireflake/lib/models/author_settings.dart b/src/fireflake/lib/models/author_settings.dart new file mode 100644 index 0000000..0febe35 --- /dev/null +++ b/src/fireflake/lib/models/author_settings.dart @@ -0,0 +1,44 @@ +import 'package:equatable/equatable.dart'; + +class AuthorSettings extends Equatable { + final String name; + final String bio; + final String email; + + const AuthorSettings({ + this.name = '', + this.bio = '', + this.email = '', + }); + + AuthorSettings copyWith({ + String? name, + String? bio, + String? email, + }) { + return AuthorSettings( + name: name ?? this.name, + bio: bio ?? this.bio, + email: email ?? this.email, + ); + } + + Map toJson() { + return { + 'name': name, + 'bio': bio, + 'email': email, + }; + } + + factory AuthorSettings.fromJson(Map json) { + return AuthorSettings( + name: json['name'] as String? ?? '', + bio: json['bio'] as String? ?? '', + email: json['email'] as String? ?? '', + ); + } + + @override + List get props => [name, bio, email]; +} diff --git a/src/fireflake/lib/models/character.dart b/src/fireflake/lib/models/character.dart index 2c2b3d9..5053590 100644 --- a/src/fireflake/lib/models/character.dart +++ b/src/fireflake/lib/models/character.dart @@ -3,4 +3,14 @@ class Character { String storygoal; Character({required this.name, required this.storygoal}); + + factory Character.fromJson(Map json) => Character( + name: json['name'] as String, + storygoal: json['storygoal'] as String, + ); + + Map toJson() => { + 'name': name, + 'storygoal': storygoal, + }; } diff --git a/src/fireflake/lib/models/project.dart b/src/fireflake/lib/models/project.dart index 3b5ee31..6ba8722 100644 --- a/src/fireflake/lib/models/project.dart +++ b/src/fireflake/lib/models/project.dart @@ -1,11 +1,126 @@ +import 'scene.dart'; +import 'character.dart'; + class Project { + // TODO: consider making these fields final with copyWith returning new instance; current mutable for simplicity. String title; String subtitle; int expectedWordCount; + String summary; + String act1; + String act2; + String act3; + String finale; + // Step 4: Expanded paragraphs from step 2 + String expandedAct1; + String expandedAct2; + String expandedAct3; + String expandedFinale; + // Step 6: Further expanded argument + String extendedArgument; + // Step 8: List of pending scenes + List pendingScenes; + List scenes; + List characters; Project({ required this.title, this.subtitle = '', required this.expectedWordCount, + this.summary = '', + this.act1 = '', + this.act2 = '', + this.act3 = '', + this.finale = '', + this.expandedAct1 = '', + this.expandedAct2 = '', + this.expandedAct3 = '', + this.expandedFinale = '', + this.extendedArgument = '', + this.pendingScenes = const [], + this.scenes = const [], + this.characters = const [], }); + + Project copyWith({ + String? title, + String? subtitle, + int? expectedWordCount, + String? summary, + String? act1, + String? act2, + String? act3, + String? finale, + String? expandedAct1, + String? expandedAct2, + String? expandedAct3, + String? expandedFinale, + String? extendedArgument, + List? pendingScenes, + List? scenes, + List? characters, + }) { + return Project( + title: title ?? this.title, + subtitle: subtitle ?? this.subtitle, + expectedWordCount: expectedWordCount ?? this.expectedWordCount, + summary: summary ?? this.summary, + act1: act1 ?? this.act1, + act2: act2 ?? this.act2, + act3: act3 ?? this.act3, + finale: finale ?? this.finale, + expandedAct1: expandedAct1 ?? this.expandedAct1, + expandedAct2: expandedAct2 ?? this.expandedAct2, + expandedAct3: expandedAct3 ?? this.expandedAct3, + expandedFinale: expandedFinale ?? this.expandedFinale, + extendedArgument: extendedArgument ?? this.extendedArgument, + pendingScenes: pendingScenes ?? this.pendingScenes, + scenes: scenes ?? this.scenes, + characters: characters ?? this.characters, + ); + } + + factory Project.fromJson(Map json) => Project( + title: json['title'] as String, + subtitle: json['subtitle'] as String? ?? '', + expectedWordCount: json['expectedWordCount'] as int, + summary: json['summary'] as String? ?? '', + act1: json['act1'] as String? ?? '', + act2: json['act2'] as String? ?? '', + act3: json['act3'] as String? ?? '', + finale: json['finale'] as String? ?? '', + expandedAct1: json['expandedAct1'] as String? ?? '', + expandedAct2: json['expandedAct2'] as String? ?? '', + expandedAct3: json['expandedAct3'] as String? ?? '', + expandedFinale: json['expandedFinale'] as String? ?? '', + extendedArgument: json['extendedArgument'] as String? ?? '', + pendingScenes: (json['pendingScenes'] as List? ?? []) + .map((e) => e as String) + .toList(), + scenes: (json['scenes'] as List? ?? []) + .map((e) => Scene.fromJson(e as Map)) + .toList(), + characters: (json['characters'] as List? ?? []) + .map((e) => Character.fromJson(e as Map)) + .toList(), + ); + + Map toJson() => { + 'title': title, + 'subtitle': subtitle, + 'expectedWordCount': expectedWordCount, + 'summary': summary, + 'act1': act1, + 'act2': act2, + 'act3': act3, + 'finale': finale, + 'expandedAct1': expandedAct1, + 'expandedAct2': expandedAct2, + 'expandedAct3': expandedAct3, + 'expandedFinale': expandedFinale, + 'extendedArgument': extendedArgument, + 'pendingScenes': pendingScenes, + 'scenes': scenes.map((s) => s.toJson()).toList(), + 'characters': characters.map((c) => c.toJson()).toList(), + }; } diff --git a/src/fireflake/lib/models/scene.dart b/src/fireflake/lib/models/scene.dart index 21964e0..ffb416a 100644 --- a/src/fireflake/lib/models/scene.dart +++ b/src/fireflake/lib/models/scene.dart @@ -8,4 +8,16 @@ class Scene { required this.chapter, required this.summary, }); + + factory Scene.fromJson(Map json) => Scene( + id: json['id'] as String, + chapter: json['chapter'] as String, + summary: json['summary'] as String, + ); + + Map toJson() => { + 'id': id, + 'chapter': chapter, + 'summary': summary, + }; } diff --git a/src/fireflake/lib/projects_page.dart b/src/fireflake/lib/projects_page.dart index 275c7cf..4ca6514 100644 --- a/src/fireflake/lib/projects_page.dart +++ b/src/fireflake/lib/projects_page.dart @@ -2,6 +2,8 @@ import 'package:fireflake/author_info_page.dart'; import 'package:fireflake/models/project.dart'; import 'package:fireflake/projectwrapper.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'state/app_cubit.dart'; class ProjectsPage extends StatefulWidget { const ProjectsPage({super.key}); @@ -13,6 +15,13 @@ class ProjectsPage extends StatefulWidget { class _ProjectsPageState extends State { List projectsToLoad = [Project(title: 'Project 1', expectedWordCount: 1000)]; + @override + void initState() { + super.initState(); + // Load projects from disk on startup + context.read().loadProjectsFromDisk(); + } + @override Widget build(BuildContext context) { return Padding( @@ -23,10 +32,15 @@ class _ProjectsPageState extends State { children: [ Container( padding: const EdgeInsets.all(20), - width: 200, - height: 300, + width: 220, + height: 320, color: Colors.grey, - child: buildProjectList()), + child: BlocBuilder( + builder: (context, state) { + final projects = state.projects.isEmpty ? projectsToLoad : state.projects; + return buildProjectList(projects); + }, + )), Container( margin: const EdgeInsets.all(8), padding: const EdgeInsets.all(8), @@ -39,6 +53,8 @@ class _ProjectsPageState extends State { child: TextButton( style: mainButtonStyle(), onPressed: () { + final cubit = context.read(); + cubit.loadInitial(projectsToLoad); Navigator.of(context).push( MaterialPageRoute( builder: (context) => ProjectWrapper(), @@ -52,10 +68,28 @@ class _ProjectsPageState extends State { child: TextButton( style: mainButtonStyle(), onPressed: () { - print('Creating new project...'); + final cubit = context.read(); + final newProject = Project(title: 'New Project', expectedWordCount: 2000); + final updated = List.from(cubit.state.projects)..add(newProject); + cubit.loadInitial(updated); + cubit.selectProject(newProject); }, child: const Text('New')), - ) + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: TextButton( + style: mainButtonStyle(), + onPressed: () async { + await context.read().saveCurrentProjectToDisk(); + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Proyecto guardado en disco')), + ); + }, + child: const Text('Guardar'), + ), + ), ], ), ), @@ -85,14 +119,25 @@ class _ProjectsPageState extends State { foregroundColor: WidgetStateProperty.all(Colors.white)); } - ListView buildProjectList() { + ListView buildProjectList(List projects) { return ListView.builder( shrinkWrap: true, - itemCount: projectsToLoad.length, + itemCount: projects.length, itemBuilder: (context, index) { - return Text( - projectsToLoad[index].title, - style: const TextStyle(color: Colors.white), + return GestureDetector( + onTap: () async { + await context.read().loadAndSelectProject(projects[index].title); + if (!context.mounted) return; + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => ProjectWrapper(), + ), + ); + }, + child: Text( + projects[index].title, + style: const TextStyle(color: Colors.white), + ), ); }, ); diff --git a/src/fireflake/lib/projectwrapper.dart b/src/fireflake/lib/projectwrapper.dart index 2697111..c4cb22e 100644 --- a/src/fireflake/lib/projectwrapper.dart +++ b/src/fireflake/lib/projectwrapper.dart @@ -1,12 +1,17 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'info_views/projectinfo.dart'; import 'step_views/step1.dart'; import 'step_views/step2.dart'; import 'step_views/step3.dart'; import 'step_views/step4.dart'; import 'step_views/step5.dart'; +import 'step_views/step6.dart'; +import 'step_views/step7.dart'; +import 'step_views/step8.dart'; import 'step_views/step9.dart'; +import 'state/app_cubit.dart'; class ProjectWrapper extends StatefulWidget { const ProjectWrapper({super.key}); @@ -26,6 +31,9 @@ class _ProjectWrapperState extends State { StepThreePage(), StepFourPage(), StepFivePage(), + StepSixPage(), + StepSevenPage(), + StepEightPage(), StepNinePage() ]; @@ -33,15 +41,24 @@ class _ProjectWrapperState extends State { length: steps.length, child: Scaffold( appBar: AppBar( - title: const Text('Project'), + title: BlocBuilder( + builder: (context, state) { + final title = state.selectedProject?.title ?? 'Project'; + return Text(title); + }, + ), bottom: const TabBar( + isScrollable: true, tabs: [ - Tab(text: 'Project Info'), + Tab(text: 'Info'), Tab(text: 'Step 1'), Tab(text: 'Step 2'), Tab(text: 'Step 3'), Tab(text: 'Step 4'), Tab(text: 'Step 5'), + Tab(text: 'Step 6'), + Tab(text: 'Step 7'), + Tab(text: 'Step 8'), Tab(text: 'Step 9'), ], ), diff --git a/src/fireflake/lib/state/app_cubit.dart b/src/fireflake/lib/state/app_cubit.dart new file mode 100644 index 0000000..13faa8f --- /dev/null +++ b/src/fireflake/lib/state/app_cubit.dart @@ -0,0 +1,240 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../data/project_storage.dart'; +import '../models/project.dart'; +import '../models/scene.dart'; +import '../models/character.dart'; + +class AppState extends Equatable { + final List projects; + final Project? selectedProject; + final List scenes; + final List characters; + + const AppState({ + this.projects = const [], + this.selectedProject, + this.scenes = const [], + this.characters = const [], + }); + + AppState copyWith({ + List? projects, + Project? selectedProject, + List? scenes, + List? characters, + }) => AppState( + projects: projects ?? this.projects, + selectedProject: selectedProject ?? this.selectedProject, + scenes: scenes ?? this.scenes, + characters: characters ?? this.characters, + ); + + @override + List get props => [projects, selectedProject, scenes, characters]; +} + +class AppCubit extends Cubit { + AppCubit() : super(const AppState()); + + Future loadProjectsFromDisk() async { + final projects = await ProjectStorage.loadProjects(); + emit(state.copyWith(projects: projects)); + } + + void loadInitial(List projects) { + emit(state.copyWith(projects: projects)); + } + + void selectProject(Project project) { + emit(state.copyWith( + selectedProject: project, + scenes: project.scenes, + characters: project.characters, + )); + } + + Future loadAndSelectProject(String title) async { + final project = await ProjectStorage.loadProjectByTitle(title); + if (project != null) { + selectProject(project); + } + } + + void saveProject({ + required String title, + String subtitle = '', + required int expectedWordCount, + String? summary, + String? act1, + String? act2, + String? act3, + String? finale, + List? scenes, + List? characters, + }) { + final updated = List.from(state.projects); + final idx = updated.indexWhere((p) => p.title == title); + Project base = idx >= 0 + ? updated[idx] + : Project( + title: title, + subtitle: subtitle, + expectedWordCount: expectedWordCount, + ); + + final project = base.copyWith( + title: title, + subtitle: subtitle, + expectedWordCount: expectedWordCount, + summary: summary, + act1: act1, + act2: act2, + act3: act3, + finale: finale, + scenes: scenes ?? state.scenes, + characters: characters ?? state.characters, + ); + + if (idx >= 0) { + updated[idx] = project; + } else { + updated.add(project); + } + emit(state.copyWith(projects: updated, selectedProject: project)); + } + + void updateProjectSummary({ + String? summary, + String? act1, + String? act2, + String? act3, + String? finale, + List? scenes, + List? characters, + }) { + final selected = state.selectedProject; + if (selected == null) return; + final updatedProject = selected.copyWith( + summary: summary ?? selected.summary, + act1: act1 ?? selected.act1, + act2: act2 ?? selected.act2, + act3: act3 ?? selected.act3, + finale: finale ?? selected.finale, + scenes: scenes ?? state.scenes, + characters: characters ?? state.characters, + ); + final updatedProjects = state.projects + .map((p) => p.title == selected.title ? updatedProject : p) + .toList(); + emit(state.copyWith(projects: updatedProjects, selectedProject: updatedProject)); + } + + void updateExpandedParagraphs({ + String? expandedAct1, + String? expandedAct2, + String? expandedAct3, + String? expandedFinale, + }) { + final selected = state.selectedProject; + if (selected == null) return; + final updatedProject = selected.copyWith( + expandedAct1: expandedAct1 ?? selected.expandedAct1, + expandedAct2: expandedAct2 ?? selected.expandedAct2, + expandedAct3: expandedAct3 ?? selected.expandedAct3, + expandedFinale: expandedFinale ?? selected.expandedFinale, + ); + final updatedProjects = state.projects + .map((p) => p.title == selected.title ? updatedProject : p) + .toList(); + emit(state.copyWith(projects: updatedProjects, selectedProject: updatedProject)); + } + + void updateExtendedArgument(String extendedArgument) { + final selected = state.selectedProject; + if (selected == null) return; + final updatedProject = selected.copyWith(extendedArgument: extendedArgument); + final updatedProjects = state.projects + .map((p) => p.title == selected.title ? updatedProject : p) + .toList(); + emit(state.copyWith(projects: updatedProjects, selectedProject: updatedProject)); + } + + void updatePendingScenes(List pendingScenes) { + final selected = state.selectedProject; + if (selected == null) return; + final updatedProject = selected.copyWith(pendingScenes: pendingScenes); + final updatedProjects = state.projects + .map((p) => p.title == selected.title ? updatedProject : p) + .toList(); + emit(state.copyWith(projects: updatedProjects, selectedProject: updatedProject)); + } + + void addScene(Scene scene) { + final updated = List.from(state.scenes)..add(scene); + emit(state.copyWith(scenes: updated)); + } + + void updateScene(int index, Scene scene) { + final updated = List.from(state.scenes); + if (index >= 0 && index < updated.length) { + updated[index] = scene; + emit(state.copyWith(scenes: updated)); + } + } + + void removeSceneAt(int index) { + final updated = List.from(state.scenes); + if (index >= 0 && index < updated.length) { + updated.removeAt(index); + emit(state.copyWith(scenes: updated)); + } + } + + void moveSceneUp(int index) { + final updated = List.from(state.scenes); + if (index > 0 && index < updated.length) { + final scene = updated.removeAt(index); + updated.insert(index - 1, scene); + emit(state.copyWith(scenes: updated)); + } + } + + void moveSceneDown(int index) { + final updated = List.from(state.scenes); + if (index >= 0 && index < updated.length - 1) { + final scene = updated.removeAt(index); + updated.insert(index + 1, scene); + emit(state.copyWith(scenes: updated)); + } + } + + void addCharacter(Character character) { + final updated = List.from(state.characters)..add(character); + emit(state.copyWith(characters: updated)); + } + + void removeCharacter(Character character) { + final updated = List.from(state.characters)..remove(character); + emit(state.copyWith(characters: updated)); + } + + Future saveCurrentProjectToDisk() async { + final project = state.selectedProject; + if (project == null) return; + final merged = project.copyWith( + summary: project.summary, + act1: project.act1, + act2: project.act2, + act3: project.act3, + finale: project.finale, + scenes: state.scenes, + characters: state.characters, + ); + await ProjectStorage.saveProject(merged); + final projects = state.projects + .map((p) => p.title == merged.title ? merged : p) + .toList(); + emit(state.copyWith(projects: projects, selectedProject: merged)); + } +} diff --git a/src/fireflake/lib/state/author_cubit.dart b/src/fireflake/lib/state/author_cubit.dart new file mode 100644 index 0000000..0f163f6 --- /dev/null +++ b/src/fireflake/lib/state/author_cubit.dart @@ -0,0 +1,72 @@ +import 'dart:convert'; +import 'package:bloc/bloc.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import '../models/author_settings.dart'; + +class AuthorCubit extends Cubit { + static const String _storageKey = 'author_settings'; + + AuthorCubit() : super(const AuthorSettings()) { + loadSettings(); + } + + // Cargar configuración desde SharedPreferences + Future loadSettings() async { + try { + final prefs = await SharedPreferences.getInstance(); + final jsonString = prefs.getString(_storageKey); + + if (jsonString != null) { + final json = jsonDecode(jsonString) as Map; + emit(AuthorSettings.fromJson(json)); + } + } catch (e) { + // Si hay error, mantener valores por defecto + print('Error loading author settings: $e'); + } + } + + // Guardar configuración en SharedPreferences + Future saveSettings(AuthorSettings settings) async { + try { + final prefs = await SharedPreferences.getInstance(); + final jsonString = jsonEncode(settings.toJson()); + await prefs.setString(_storageKey, jsonString); + emit(settings); + } catch (e) { + print('Error saving author settings: $e'); + } + } + + // Actualizar nombre + Future updateName(String name) async { + final updated = state.copyWith(name: name); + await saveSettings(updated); + } + + // Actualizar biografía + Future updateBio(String bio) async { + final updated = state.copyWith(bio: bio); + await saveSettings(updated); + } + + // Actualizar email + Future updateEmail(String email) async { + final updated = state.copyWith(email: email); + await saveSettings(updated); + } + + // Actualizar todo a la vez + Future updateAll({ + String? name, + String? bio, + String? email, + }) async { + final updated = state.copyWith( + name: name, + bio: bio, + email: email, + ); + await saveSettings(updated); + } +} diff --git a/src/fireflake/lib/step_views/step1.dart b/src/fireflake/lib/step_views/step1.dart index 8fd1cfe..b4246b7 100644 --- a/src/fireflake/lib/step_views/step1.dart +++ b/src/fireflake/lib/step_views/step1.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../state/app_cubit.dart'; class StepOnePage extends StatefulWidget { const StepOnePage({super.key}); @@ -8,10 +10,101 @@ class StepOnePage extends StatefulWidget { } class _StepOnePageState extends State { + final _formKey = GlobalKey(); + final _summaryController = TextEditingController(); + bool _initialized = false; + + @override + void dispose() { + _summaryController.dispose(); + super.dispose(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (_initialized) return; + final project = context.read().state.selectedProject; + if (project != null) { + _summaryController.text = project.summary; + } + _initialized = true; + } + + void _save() { + if (_formKey.currentState?.validate() != true) return; + context.read().updateProjectSummary( + summary: _summaryController.text.trim(), + ); + context.read().saveCurrentProjectToDisk(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Resumen guardado')), + ); + } + @override Widget build(BuildContext context) { - return Center( - child: Text('This is Step 1'), + return Scaffold( + body: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Paso 1: Resumen en una frase', + style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + Text( + 'Escribe el resumen de tu historia en una sola frase. Este será el corazón de tu proyecto.', + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 24), + + Card( + elevation: 1, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Resumen en una frase', + style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600), + ), + const SizedBox(height: 12), + TextFormField( + controller: _summaryController, + maxLines: 3, + decoration: const InputDecoration( + labelText: 'Tu historia en una sola frase', + hintText: 'Ejemplo: Un joven mago descubre que es el elegido para salvar el mundo de la oscuridad.', + border: OutlineInputBorder(), + ), + validator: (value) => (value == null || value.trim().isEmpty) + ? 'El resumen es requerido' + : null, + ), + ], + ), + ), + ), + + const SizedBox(height: 24), + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: _save, + icon: const Icon(Icons.save), + label: const Text('Guardar'), + ), + ), + ], + ), + ), + ), ); } } diff --git a/src/fireflake/lib/step_views/step2.dart b/src/fireflake/lib/step_views/step2.dart index 9865efa..d3af4ec 100644 --- a/src/fireflake/lib/step_views/step2.dart +++ b/src/fireflake/lib/step_views/step2.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../state/app_cubit.dart'; class StepTwoPage extends StatefulWidget { const StepTwoPage({super.key}); @@ -8,10 +10,189 @@ class StepTwoPage extends StatefulWidget { } class _StepTwoPageState extends State { + final _formKey = GlobalKey(); + final _summaryController = TextEditingController(); + final _act1Controller = TextEditingController(); + final _act2Controller = TextEditingController(); + final _act3Controller = TextEditingController(); + final _finaleController = TextEditingController(); + bool _initialized = false; + + @override + void dispose() { + _summaryController.dispose(); + _act1Controller.dispose(); + _act2Controller.dispose(); + _act3Controller.dispose(); + _finaleController.dispose(); + super.dispose(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (_initialized) return; + final project = context.read().state.selectedProject; + if (project != null) { + _summaryController.text = project.summary; + _act1Controller.text = project.act1; + _act2Controller.text = project.act2; + _act3Controller.text = project.act3; + _finaleController.text = project.finale; + } + _initialized = true; + } + + void _save() { + if (_formKey.currentState?.validate() != true) return; + context.read().updateProjectSummary( + summary: _summaryController.text.trim(), + act1: _act1Controller.text.trim(), + act2: _act2Controller.text.trim(), + act3: _act3Controller.text.trim(), + finale: _finaleController.text.trim(), + ); + context.read().saveCurrentProjectToDisk(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Resumen ampliado guardado')), + ); + } + @override Widget build(BuildContext context) { - return Center( - child: Text('This is Step 2'), + return Scaffold( + body: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Ampliación del resumen', + style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + Text( + 'Desglosa el resumen en actos principales y final para estructurar la historia.', + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 24), + + _buildCard( + context, + title: 'Resumen general', + child: TextFormField( + controller: _summaryController, + maxLines: 4, + decoration: const InputDecoration( + labelText: 'Resumen general del proyecto', + border: OutlineInputBorder(), + ), + validator: (value) => (value == null || value.trim().isEmpty) + ? 'El resumen es requerido' + : null, + ), + ), + const SizedBox(height: 16), + + _buildCard( + context, + title: 'Acto I', + child: TextFormField( + controller: _act1Controller, + maxLines: 3, + decoration: const InputDecoration( + labelText: 'Planteamiento / Acto I', + border: OutlineInputBorder(), + ), + validator: (value) => (value == null || value.trim().isEmpty) + ? 'Describe el Acto I' + : null, + ), + ), + const SizedBox(height: 12), + + _buildCard( + context, + title: 'Acto II', + child: TextFormField( + controller: _act2Controller, + maxLines: 3, + decoration: const InputDecoration( + labelText: 'Nudo / Acto II', + border: OutlineInputBorder(), + ), + validator: (value) => (value == null || value.trim().isEmpty) + ? 'Describe el Acto II' + : null, + ), + ), + const SizedBox(height: 12), + + _buildCard( + context, + title: 'Acto III', + child: TextFormField( + controller: _act3Controller, + maxLines: 3, + decoration: const InputDecoration( + labelText: 'Clímax / Acto III', + border: OutlineInputBorder(), + ), + validator: (value) => (value == null || value.trim().isEmpty) + ? 'Describe el Acto III' + : null, + ), + ), + const SizedBox(height: 12), + + _buildCard( + context, + title: 'Final', + child: TextFormField( + controller: _finaleController, + maxLines: 3, + decoration: const InputDecoration( + labelText: 'Resolución / Final', + border: OutlineInputBorder(), + ), + validator: (value) => (value == null || value.trim().isEmpty) + ? 'Describe el final' + : null, + ), + ), + + const SizedBox(height: 24), + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: _save, + icon: const Icon(Icons.save), + label: const Text('Guardar'), + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildCard(BuildContext context, {required String title, required Widget child}) { + return Card( + elevation: 1, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600)), + const SizedBox(height: 12), + child, + ], + ), + ), ); } } diff --git a/src/fireflake/lib/step_views/step3.dart b/src/fireflake/lib/step_views/step3.dart index 5bf094b..d185578 100644 --- a/src/fireflake/lib/step_views/step3.dart +++ b/src/fireflake/lib/step_views/step3.dart @@ -1,5 +1,7 @@ -import 'package:fireflake/models/character.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../models/character.dart'; +import '../state/app_cubit.dart'; class StepThreePage extends StatefulWidget { const StepThreePage({super.key}); @@ -238,6 +240,12 @@ class _StepThreePageState extends State { child: ElevatedButton( onPressed: () { if (_formKey.currentState!.validate()) { + final name = _historyControllers.isNotEmpty && _historyControllers.first.text.isNotEmpty + ? _historyControllers.first.text + : 'Personaje'; + final character = Character(name: name, storygoal: _objectiveController.text); + context.read().addCharacter(character); + context.read().saveCurrentProjectToDisk(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Información del personaje guardada'), diff --git a/src/fireflake/lib/step_views/step4.dart b/src/fireflake/lib/step_views/step4.dart index ad13aed..4d356b5 100644 --- a/src/fireflake/lib/step_views/step4.dart +++ b/src/fireflake/lib/step_views/step4.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../state/app_cubit.dart'; class StepFourPage extends StatefulWidget { const StepFourPage({super.key}); @@ -8,10 +10,154 @@ class StepFourPage extends StatefulWidget { } class _StepFourPageState extends State { + final _formKey = GlobalKey(); + final Map _controllers = {}; + bool _initialized = false; + + @override + void dispose() { + for (var controller in _controllers.values) { + controller.dispose(); + } + super.dispose(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (_initialized) return; + + final project = context.read().state.selectedProject; + if (project == null) return; + + _controllers['act1'] = TextEditingController(text: project.expandedAct1.isEmpty ? project.act1 : project.expandedAct1); + _controllers['act2'] = TextEditingController(text: project.expandedAct2.isEmpty ? project.act2 : project.expandedAct2); + _controllers['act3'] = TextEditingController(text: project.expandedAct3.isEmpty ? project.act3 : project.expandedAct3); + _controllers['finale'] = TextEditingController(text: project.expandedFinale.isEmpty ? project.finale : project.expandedFinale); + + _initialized = true; + } + + void _save() { + if (_formKey.currentState?.validate() != true) return; + + final cubit = context.read(); + cubit.updateExpandedParagraphs( + expandedAct1: _controllers['act1']!.text.trim(), + expandedAct2: _controllers['act2']!.text.trim(), + expandedAct3: _controllers['act3']!.text.trim(), + expandedFinale: _controllers['finale']!.text.trim(), + ); + cubit.saveCurrentProjectToDisk(); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Párrafos expandidos guardados')), + ); + } + @override Widget build(BuildContext context) { - return Center( - child: Text('This is Step Four'), + return Scaffold( + body: BlocBuilder( + builder: (context, state) { + final project = state.selectedProject; + if (project == null) { + return const Center(child: Text('Selecciona o crea un proyecto primero')); + } + + if (project.act1.isEmpty && project.act2.isEmpty && project.act3.isEmpty && project.finale.isEmpty) { + return const Center(child: Text('Completa el Paso 2 primero (actos y final)')); + } + + if (!_initialized) { + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() {}); + }); + } + + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Paso 4: Expandir a párrafos', + style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + Text( + 'Convierte cada frase del Paso 2 en un párrafo completo.', + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 24), + + if (project.act1.isNotEmpty) ...[ + _buildExpandedSection(context, 'Acto I - Expandido', 'act1'), + const SizedBox(height: 16), + ], + + if (project.act2.isNotEmpty) ...[ + _buildExpandedSection(context, 'Acto II - Expandido', 'act2'), + const SizedBox(height: 16), + ], + + if (project.act3.isNotEmpty) ...[ + _buildExpandedSection(context, 'Acto III - Expandido', 'act3'), + const SizedBox(height: 16), + ], + + if (project.finale.isNotEmpty) ...[ + _buildExpandedSection(context, 'Final - Expandido', 'finale'), + const SizedBox(height: 16), + ], + + const SizedBox(height: 8), + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: _save, + icon: const Icon(Icons.save), + label: const Text('Guardar párrafos expandidos'), + ), + ), + ], + ), + ), + ); + }, + ), + ); + } + + Widget _buildExpandedSection(BuildContext context, String title, String key) { + return Card( + elevation: 1, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600), + ), + const SizedBox(height: 12), + TextFormField( + controller: _controllers[key], + maxLines: 6, + decoration: InputDecoration( + labelText: 'Párrafo completo para $title', + hintText: 'Expande la frase del Paso 2 en un párrafo narrativo más detallado...', + border: const OutlineInputBorder(), + alignLabelWithHint: true, + ), + validator: (value) => (value == null || value.trim().isEmpty) ? 'Este párrafo es requerido' : null, + ), + ], + ), + ), ); } } diff --git a/src/fireflake/lib/step_views/step5.dart b/src/fireflake/lib/step_views/step5.dart index 571ebb4..096e9da 100644 --- a/src/fireflake/lib/step_views/step5.dart +++ b/src/fireflake/lib/step_views/step5.dart @@ -1,4 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../models/character.dart'; +import '../state/app_cubit.dart'; class StepFivePage extends StatefulWidget { const StepFivePage({super.key}); @@ -8,10 +11,194 @@ class StepFivePage extends StatefulWidget { } class _StepFivePageState extends State { + final _formKey = GlobalKey(); + final List> _characterControllers = []; + static const int _maxCharacters = 5; + bool _initialized = false; + + @override + void initState() { + super.initState(); + _addCharacterField(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (_initialized) return; + final characters = context.read().state.characters; + if (characters.isNotEmpty) { + _characterControllers.clear(); + for (final char in characters.take(_maxCharacters)) { + _characterControllers.add({ + 'name': TextEditingController(text: char.name), + 'description': TextEditingController(text: char.storygoal), + }); + } + } + _initialized = true; + } + + @override + void dispose() { + for (var controllerMap in _characterControllers) { + controllerMap['name']?.dispose(); + controllerMap['description']?.dispose(); + } + super.dispose(); + } + + void _addCharacterField() { + if (_characterControllers.length < _maxCharacters) { + setState(() { + _characterControllers.add({ + 'name': TextEditingController(), + 'description': TextEditingController(), + }); + }); + } + } + + void _removeCharacterField(int index) { + if (_characterControllers.length > 1) { + setState(() { + _characterControllers[index]['name']?.dispose(); + _characterControllers[index]['description']?.dispose(); + _characterControllers.removeAt(index); + }); + } + } + + void _save() { + if (_formKey.currentState?.validate() != true) return; + + final cubit = context.read(); + + // Limpiar personajes actuales y agregar los nuevos + final currentCharacters = List.from(cubit.state.characters); + currentCharacters.clear(); + + for (var controllerMap in _characterControllers) { + final name = controllerMap['name']!.text.trim(); + final description = controllerMap['description']!.text.trim(); + if (name.isNotEmpty && description.isNotEmpty) { + currentCharacters.add(Character(name: name, storygoal: description)); + } + } + + // Actualizar el estado con los personajes + for (var char in currentCharacters) { + cubit.addCharacter(char); + } + + cubit.saveCurrentProjectToDisk(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Personajes principales guardados')), + ); + } + @override Widget build(BuildContext context) { - return Center( - child: Text('This is Step Five'), + return Scaffold( + body: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Paso 5: Personajes principales', + style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + Text( + 'Describe los personajes principales de tu historia (máximo $_maxCharacters).', + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 24), + + ..._characterControllers.asMap().entries.map((entry) { + final index = entry.key; + final controllers = entry.value; + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Card( + elevation: 1, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Personaje ${index + 1}', + style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600), + ), + if (_characterControllers.length > 1) + IconButton( + icon: const Icon(Icons.delete_outline, color: Colors.red), + onPressed: () => _removeCharacterField(index), + tooltip: 'Eliminar personaje', + ), + ], + ), + const SizedBox(height: 12), + TextFormField( + controller: controllers['name'], + decoration: const InputDecoration( + labelText: 'Nombre del personaje', + hintText: 'Ej: Aragorn', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.person), + ), + validator: (value) => (value == null || value.trim().isEmpty) + ? 'El nombre es requerido' + : null, + ), + const SizedBox(height: 12), + TextFormField( + controller: controllers['description'], + maxLines: 4, + decoration: const InputDecoration( + labelText: 'Descripción del personaje', + hintText: 'Rol, motivación, conflicto, objetivos...', + border: OutlineInputBorder(), + alignLabelWithHint: true, + ), + validator: (value) => (value == null || value.trim().isEmpty) + ? 'La descripción es requerida' + : null, + ), + ], + ), + ), + ), + ); + }), + + if (_characterControllers.length < _maxCharacters) + OutlinedButton.icon( + onPressed: _addCharacterField, + icon: const Icon(Icons.add), + label: const Text('Agregar personaje'), + ), + + const SizedBox(height: 24), + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: _save, + icon: const Icon(Icons.save), + label: const Text('Guardar personajes'), + ), + ), + ], + ), + ), + ), ); } } diff --git a/src/fireflake/lib/step_views/step6.dart b/src/fireflake/lib/step_views/step6.dart new file mode 100644 index 0000000..f582627 --- /dev/null +++ b/src/fireflake/lib/step_views/step6.dart @@ -0,0 +1,127 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../state/app_cubit.dart'; + +class StepSixPage extends StatefulWidget { + const StepSixPage({super.key}); + + @override + State createState() => _StepSixPageState(); +} + +class _StepSixPageState extends State { + final _formKey = GlobalKey(); + final _argumentController = TextEditingController(); + bool _initialized = false; + + @override + void dispose() { + _argumentController.dispose(); + super.dispose(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (_initialized) return; + + final project = context.read().state.selectedProject; + if (project != null) { + _argumentController.text = project.extendedArgument; + } + _initialized = true; + } + + void _save() { + if (_formKey.currentState?.validate() != true) return; + + final cubit = context.read(); + cubit.updateExtendedArgument(_argumentController.text.trim()); + cubit.saveCurrentProjectToDisk(); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Argumento ampliado guardado')), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: BlocBuilder( + builder: (context, state) { + final project = state.selectedProject; + if (project == null) { + return const Center(child: Text('Selecciona o crea un proyecto primero')); + } + + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Paso 6: Ampliar argumento', + style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + Text( + 'Vuelve al Paso 4 y amplía el argumento con más detalle narrativo. Integra conflictos, giros y desarrollo de personajes.', + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 24), + + Card( + elevation: 1, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Argumento extendido', + style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600), + ), + const SizedBox(height: 8), + Text( + 'Combina los párrafos del Paso 4 y añade más profundidad: conflictos internos, secundarios, giros de trama, desarrollo de relaciones...', + style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Colors.grey[700]), + ), + const SizedBox(height: 12), + TextFormField( + controller: _argumentController, + maxLines: 15, + decoration: const InputDecoration( + labelText: 'Argumento completo ampliado', + hintText: 'Escribe varios párrafos con el argumento completo, integrando todos los elementos narrativos...', + border: OutlineInputBorder(), + alignLabelWithHint: true, + ), + validator: (value) => (value == null || value.trim().isEmpty) + ? 'El argumento ampliado es requerido' + : null, + ), + ], + ), + ), + ), + + const SizedBox(height: 24), + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: _save, + icon: const Icon(Icons.save), + label: const Text('Guardar argumento ampliado'), + ), + ), + ], + ), + ), + ); + }, + ), + ); + } +} diff --git a/src/fireflake/lib/step_views/step7.dart b/src/fireflake/lib/step_views/step7.dart new file mode 100644 index 0000000..730db1e --- /dev/null +++ b/src/fireflake/lib/step_views/step7.dart @@ -0,0 +1,139 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../state/app_cubit.dart'; + +class StepSevenPage extends StatefulWidget { + const StepSevenPage({super.key}); + + @override + State createState() => _StepSevenPageState(); +} + +class _StepSevenPageState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + body: BlocBuilder( + builder: (context, state) { + final characters = state.characters; + + if (characters.isEmpty) { + return const Center( + child: Padding( + padding: EdgeInsets.all(24), + child: Text( + 'No hay personajes principales aún.\nCompleta el Paso 5 primero.', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 16), + ), + ), + ); + } + + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Paso 7: Tablas de personajes', + style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + Text( + 'Revisa la información detallada de los personajes principales (creados en Paso 3 y Paso 5).', + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 24), + + ...characters.asMap().entries.map((entry) { + final index = entry.key; + final character = entry.value; + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Card( + elevation: 2, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + CircleAvatar( + backgroundColor: Theme.of(context).primaryColor, + child: Text( + '${index + 1}', + style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + character.name, + style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), + ), + ), + ], + ), + const Divider(height: 24), + _buildInfoRow(context, 'Descripción / Objetivo', character.storygoal), + ], + ), + ), + ), + ); + }), + + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.blue.shade200), + ), + child: Row( + children: [ + Icon(Icons.info_outline, color: Colors.blue.shade700), + const SizedBox(width: 12), + Expanded( + child: Text( + 'Para editar personajes, vuelve al Paso 3 o Paso 5.', + style: TextStyle(color: Colors.blue.shade700), + ), + ), + ], + ), + ), + ], + ), + ); + }, + ), + ); + } + + Widget _buildInfoRow(BuildContext context, String label, String value) { + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: Theme.of(context).textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + color: Colors.grey[700], + ), + ), + const SizedBox(height: 4), + Text( + value.isEmpty ? '(No especificado)' : value, + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + ); + } +} diff --git a/src/fireflake/lib/step_views/step8.dart b/src/fireflake/lib/step_views/step8.dart new file mode 100644 index 0000000..b910978 --- /dev/null +++ b/src/fireflake/lib/step_views/step8.dart @@ -0,0 +1,209 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../state/app_cubit.dart'; + +class StepEightPage extends StatefulWidget { + const StepEightPage({super.key}); + + @override + State createState() => _StepEightPageState(); +} + +class _StepEightPageState extends State { + final _formKey = GlobalKey(); + final List _sceneControllers = []; + bool _initialized = false; + + @override + void initState() { + super.initState(); + _addSceneField(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (_initialized) return; + + final project = context.read().state.selectedProject; + if (project != null && project.pendingScenes.isNotEmpty) { + _sceneControllers.clear(); + for (final scene in project.pendingScenes) { + _sceneControllers.add(TextEditingController(text: scene)); + } + } + _initialized = true; + } + + @override + void dispose() { + for (var controller in _sceneControllers) { + controller.dispose(); + } + super.dispose(); + } + + void _addSceneField() { + setState(() { + _sceneControllers.add(TextEditingController()); + }); + } + + void _removeSceneField(int index) { + if (_sceneControllers.length > 1) { + setState(() { + _sceneControllers[index].dispose(); + _sceneControllers.removeAt(index); + }); + } + } + + void _save() { + if (_formKey.currentState?.validate() != true) return; + + final cubit = context.read(); + final scenes = _sceneControllers + .map((c) => c.text.trim()) + .where((text) => text.isNotEmpty) + .toList(); + + cubit.updatePendingScenes(scenes); + cubit.saveCurrentProjectToDisk(); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Lista de escenas guardada')), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: BlocBuilder( + builder: (context, state) { + final project = state.selectedProject; + if (project == null) { + return const Center(child: Text('Selecciona o crea un proyecto primero')); + } + + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Paso 8: Lista de escenas', + style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + Text( + 'Usando el argumento ampliado del Paso 6, escribe una lista de las escenas que faltan para completar la historia.', + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 24), + + ..._sceneControllers.asMap().entries.map((entry) { + final index = entry.key; + final controller = entry.value; + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Card( + elevation: 1, + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: Theme.of(context).primaryColor.withValues(alpha: 0.2), + shape: BoxShape.circle, + ), + child: Center( + child: Text( + '${index + 1}', + style: TextStyle( + fontWeight: FontWeight.bold, + color: Theme.of(context).primaryColor, + ), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: TextFormField( + controller: controller, + decoration: InputDecoration( + labelText: 'Escena ${index + 1}', + hintText: 'Describe brevemente la escena...', + border: const OutlineInputBorder(), + isDense: true, + ), + validator: (value) => (value == null || value.trim().isEmpty) + ? 'Requerido' + : null, + ), + ), + const SizedBox(width: 8), + if (_sceneControllers.length > 1) + IconButton( + icon: const Icon(Icons.delete_outline, color: Colors.red, size: 20), + onPressed: () => _removeSceneField(index), + tooltip: 'Eliminar', + ), + ], + ), + ), + ), + ); + }), + + const SizedBox(height: 8), + OutlinedButton.icon( + onPressed: _addSceneField, + icon: const Icon(Icons.add), + label: const Text('Agregar escena'), + ), + + const SizedBox(height: 24), + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: _save, + icon: const Icon(Icons.save), + label: const Text('Guardar lista de escenas'), + ), + ), + + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.amber.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.amber.shade200), + ), + child: Row( + children: [ + Icon(Icons.lightbulb_outline, color: Colors.amber.shade700), + const SizedBox(width: 12), + Expanded( + child: Text( + 'Después de guardar esta lista, ve al Paso 9 para escribir el resumen narrativo de cada escena.', + style: TextStyle(color: Colors.amber.shade900), + ), + ), + ], + ), + ), + ], + ), + ), + ); + }, + ), + ); + } +} diff --git a/src/fireflake/lib/step_views/step9.dart b/src/fireflake/lib/step_views/step9.dart index c7b927e..96d5e1a 100644 --- a/src/fireflake/lib/step_views/step9.dart +++ b/src/fireflake/lib/step_views/step9.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import '../models/scene.dart'; +import '../state/app_cubit.dart'; class StepNinePage extends StatefulWidget { const StepNinePage({super.key}); @@ -12,6 +14,7 @@ class StepNinePage extends StatefulWidget { class _StepNinePageState extends State with TickerProviderStateMixin { bool _isPanelOpen = false; Scene? _editingScene; + int? _editingIndex; late AnimationController _animationController; late Animation _slideAnimation; @@ -20,29 +23,6 @@ class _StepNinePageState extends State with TickerProviderStateMix final TextEditingController _summaryController = TextEditingController(); final GlobalKey _formKey = GlobalKey(); - List scenes = [ - Scene( - id: 'Escena 1', - chapter: 'Capítulo 1', - summary: 'El protagonista despierta en un mundo desconocido, rodeado de una extraña niebla que parece tener vida propia. Debe encontrar una manera de orientarse mientras descubre que sus recuerdos están fragmentados y confusos.', - ), - Scene( - id: 'Escena 2', - chapter: 'Capítulo 1', - summary: 'Durante su exploración, encuentra a un misterioso personaje encapuchado que le ofrece ayuda a cambio de un favor que no revela. La tensión aumenta cuando aparecen criaturas hostiles en la distancia.', - ), - Scene( - id: 'Escena 3', - chapter: 'Capítulo 2', - summary: 'El protagonista debe tomar una decisión crucial: confiar en el extraño o aventurarse solo. Su elección determinará no solo su supervivencia, sino también el destino de otros personajes que aún no conoce.', - ), - Scene( - id: 'Escena 4', - chapter: 'Capítulo 2', - summary: 'Una revelación inesperada cambia todo lo que el protagonista creía saber sobre su situación. Los fragmentos de sus recuerdos comienzan a formar un patrón inquietante que sugiere una conspiración más grande.', - ), - ]; - @override void initState() { super.initState(); @@ -78,7 +58,7 @@ class _StepNinePageState extends State with TickerProviderStateMix Container( padding: const EdgeInsets.all(16.0), decoration: BoxDecoration( - color: Theme.of(context).primaryColor.withOpacity(0.1), + color: Theme.of(context).primaryColor.withValues(alpha: 0.1), border: Border( bottom: BorderSide( color: Theme.of(context).dividerColor, @@ -106,116 +86,127 @@ class _StepNinePageState extends State with TickerProviderStateMix scrollDirection: Axis.horizontal, child: SizedBox( width: MediaQuery.of(context).size.width, - child: SingleChildScrollView( - child: DataTable( - columnSpacing: 20, - dataRowHeight: 80, - headingRowColor: WidgetStateProperty.all( - Theme.of(context).primaryColor.withOpacity(0.15), - ), - columns: const [ - DataColumn( - label: Expanded( - child: Text( - 'Escena', - style: TextStyle(fontWeight: FontWeight.bold), - ), - ), - ), - DataColumn( - label: Expanded( - child: Text( - 'Capítulo', - style: TextStyle(fontWeight: FontWeight.bold), - ), + child: BlocBuilder( + builder: (context, state) { + final scenes = state.scenes; + if (scenes.isEmpty) { + return const Center(child: Text('No hay escenas aún. Agrega la primera.')); + } + return SingleChildScrollView( + child: DataTable( + columnSpacing: 20, + dataRowMinHeight: 80, + dataRowMaxHeight: 80, + headingRowColor: WidgetStateProperty.all( + Theme.of(context).primaryColor.withValues(alpha: 0.15), ), - ), - DataColumn( - label: Expanded( - child: Text( - 'Resumen', - style: TextStyle(fontWeight: FontWeight.bold), - ), - ), - ), - DataColumn( - label: Expanded( - child: Text( - 'Acciones', - style: TextStyle(fontWeight: FontWeight.bold), + columns: const [ + DataColumn( + label: Expanded( + child: Text( + 'Escena', + style: TextStyle(fontWeight: FontWeight.bold), + ), + ), ), - ), - ), - ], - rows: scenes.map((scene) { - return DataRow( - cells: [ - DataCell( - Container( - padding: const EdgeInsets.symmetric(vertical: 8.0), + DataColumn( + label: Expanded( child: Text( - scene.id, - style: const TextStyle(fontWeight: FontWeight.w500), + 'Capítulo', + style: TextStyle(fontWeight: FontWeight.bold), ), ), ), - DataCell( - Container( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Text(scene.chapter), + DataColumn( + label: Expanded( + child: Text( + 'Resumen', + style: TextStyle(fontWeight: FontWeight.bold), + ), ), ), - DataCell( - Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(vertical: 8.0), + DataColumn( + label: Expanded( child: Text( - scene.summary, - softWrap: true, - maxLines: 4, - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontSize: 13), + 'Acciones', + style: TextStyle(fontWeight: FontWeight.bold), ), ), ), - DataCell( - Container( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon(Icons.keyboard_arrow_up, size: 20), - onPressed: scenes.indexOf(scene) > 0 - ? () => _moveSceneUp(scene) - : null, - tooltip: 'Mover arriba', - ), - IconButton( - icon: const Icon(Icons.keyboard_arrow_down, size: 20), - onPressed: scenes.indexOf(scene) < scenes.length - 1 - ? () => _moveSceneDown(scene) - : null, - tooltip: 'Mover abajo', + ], + rows: scenes.asMap().entries.map((entry) { + final index = entry.key; + final scene = entry.value; + return DataRow( + cells: [ + DataCell( + Container( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Text( + scene.id, + style: const TextStyle(fontWeight: FontWeight.w500), ), - IconButton( - icon: const Icon(Icons.edit, size: 20), - onPressed: () => _editScene(scene), - tooltip: 'Editar escena', + ), + ), + DataCell( + Container( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Text(scene.chapter), + ), + ), + DataCell( + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Text( + scene.summary, + softWrap: true, + maxLines: 4, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 13), ), - IconButton( - icon: const Icon(Icons.delete, size: 20), - onPressed: () => _deleteScene(scene), - tooltip: 'Eliminar escena', + ), + ), + DataCell( + Container( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.keyboard_arrow_up, size: 20), + onPressed: index > 0 + ? () => _moveSceneUp(index) + : null, + tooltip: 'Mover arriba', + ), + IconButton( + icon: const Icon(Icons.keyboard_arrow_down, size: 20), + onPressed: index < scenes.length - 1 + ? () => _moveSceneDown(index) + : null, + tooltip: 'Mover abajo', + ), + IconButton( + icon: const Icon(Icons.edit, size: 20), + onPressed: () => _editScene(scene, index), + tooltip: 'Editar escena', + ), + IconButton( + icon: const Icon(Icons.delete, size: 20), + onPressed: () => _deleteScene(index), + tooltip: 'Eliminar escena', + ), + ], ), - ], + ), ), - ), - ), - ], - ); - }).toList(), - ), + ], + ); + }).toList(), + ), + ); + }, ), ), ), @@ -226,7 +217,7 @@ class _StepNinePageState extends State with TickerProviderStateMix GestureDetector( onTap: _closeSidePanel, child: Container( - color: Colors.black.withOpacity(0.5), + color: Colors.black.withValues(alpha: 0.5), width: double.infinity, height: double.infinity, ), @@ -243,7 +234,7 @@ class _StepNinePageState extends State with TickerProviderStateMix color: Theme.of(context).scaffoldBackgroundColor, boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.3), + color: Colors.black.withValues(alpha: 0.3), spreadRadius: 0, blurRadius: 10, offset: const Offset(-5, 0), @@ -259,21 +250,22 @@ class _StepNinePageState extends State with TickerProviderStateMix ); } - void _editScene(Scene scene) { + void _editScene(Scene scene, int index) { _editingScene = scene; + _editingIndex = index; _idController.text = scene.id; _chapterController.text = scene.chapter; _summaryController.text = scene.summary; _openSidePanel(); } - void _deleteScene(Scene scene) { + void _deleteScene(int index) { showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: const Text('Confirmar eliminación'), - content: Text('¿Estás seguro de que quieres eliminar "${scene.id}"?'), + content: const Text('¿Estás seguro de que quieres eliminar esta escena?'), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), @@ -281,13 +273,8 @@ class _StepNinePageState extends State with TickerProviderStateMix ), TextButton( onPressed: () { - setState(() { - scenes.remove(scene); - }); + context.read().removeSceneAt(index); Navigator.of(context).pop(); - // ScaffoldMessenger.of(context).showSnackBar( - // SnackBar(content: Text('${scene.escena} eliminada')), - // ); }, child: const Text('Eliminar'), ), @@ -297,46 +284,20 @@ class _StepNinePageState extends State with TickerProviderStateMix ); } - void _moveSceneUp(Scene scene) { - setState(() { - int currentIndex = scenes.indexOf(scene); - if (currentIndex > 0) { - scenes.removeAt(currentIndex); - scenes.insert(currentIndex - 1, scene); - - _updateSceneNames(); - } - }); - // ScaffoldMessenger.of(context).showSnackBar( - // SnackBar(content: Text('${scene.escena} movida hacia arriba')), - // ); - } - - void _moveSceneDown(Scene scene) { - setState(() { - int currentIndex = scenes.indexOf(scene); - if (currentIndex < scenes.length - 1) { - scenes.removeAt(currentIndex); - scenes.insert(currentIndex + 1, scene); - - _updateSceneNames(); - } - }); - // ScaffoldMessenger.of(context).showSnackBar( - // SnackBar(content: Text('${scene.escena} movida hacia abajo')), - // ); + void _moveSceneUp(int index) { + context.read().moveSceneUp(index); } - void _updateSceneNames() { - for (int i = 0; i < scenes.length; i++) { - scenes[i].id = 'Escena ${i + 1}'; - } + void _moveSceneDown(int index) { + context.read().moveSceneDown(index); } void _addNewScene() { + final scenesLength = context.read().state.scenes.length; _editingScene = null; - _idController.text = 'Escena ${scenes.length + 1}'; - _chapterController.text = 'Capítulo ${(scenes.length ~/ 2) + 1}'; + _editingIndex = null; + _idController.text = 'Escena ${scenesLength + 1}'; + _chapterController.text = 'Capítulo ${(scenesLength ~/ 2) + 1}'; _summaryController.text = ''; _openSidePanel(); } @@ -353,29 +314,28 @@ class _StepNinePageState extends State with TickerProviderStateMix setState(() { _isPanelOpen = false; _editingScene = null; + _editingIndex = null; }); }); } void _saveScene() { if (_formKey.currentState!.validate()) { - setState(() { - if (_editingScene != null) { - _editingScene!.id = _idController.text; - _editingScene!.chapter = _chapterController.text; - _editingScene!.summary = _summaryController.text; - } else { - scenes.add(Scene( - id: _idController.text, - chapter: _chapterController.text, - summary: _summaryController.text, - )); - } - }); + final newScene = Scene( + id: _idController.text, + chapter: _chapterController.text, + summary: _summaryController.text, + ); + if (_editingIndex != null) { + context.read().updateScene(_editingIndex!, newScene); + } else { + context.read().addScene(newScene); + } + context.read().saveCurrentProjectToDisk(); _closeSidePanel(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(_editingScene != null ? 'Escena actualizada' : 'Nueva escena agregada'), + content: Text(_editingIndex != null ? 'Escena actualizada' : 'Nueva escena agregada'), ), ); } @@ -387,7 +347,7 @@ class _StepNinePageState extends State with TickerProviderStateMix Container( padding: const EdgeInsets.all(16.0), decoration: BoxDecoration( - color: Theme.of(context).primaryColor.withOpacity(0.1), + color: Theme.of(context).primaryColor.withValues(alpha: 0.1), border: Border( bottom: BorderSide( color: Theme.of(context).dividerColor, diff --git a/src/fireflake/pubspec.lock b/src/fireflake/pubspec.lock index f40be40..ce508cf 100644 --- a/src/fireflake/pubspec.lock +++ b/src/fireflake/pubspec.lock @@ -9,6 +9,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.13.0" + bloc: + dependency: "direct main" + description: + name: bloc + sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e" + url: "https://pub.dev" + source: hosted + version: "8.1.4" boolean_selector: dependency: transitive description: @@ -49,6 +57,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + equatable: + dependency: "direct main" + description: + name: equatable + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + url: "https://pub.dev" + source: hosted + version: "2.0.7" fake_async: dependency: transitive description: @@ -57,11 +73,35 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_bloc: + dependency: "direct main" + description: + name: flutter_bloc + sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a + url: "https://pub.dev" + source: hosted + version: "8.1.6" flutter_lints: dependency: "direct dev" description: @@ -75,6 +115,11 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" leak_tracker: dependency: transitive description: @@ -131,14 +176,150 @@ packages: url: "https://pub.dev" source: hosted version: "1.16.0" - path: + nested: dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + path: + dependency: "direct main" description: name: path sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted version: "1.9.1" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "3b4c1fc3aa55ddc9cd4aa6759984330d5c8e66aa7702a6223c61540dc6380c37" + url: "https://pub.dev" + source: hosted + version: "2.2.19" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "16eef174aacb07e09c351502740fa6254c165757638eba1e9116b0a781201bbd" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + provider: + dependency: transitive + description: + name: provider + sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" + url: "https://pub.dev" + source: hosted + version: "6.1.5+1" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" + url: "https://pub.dev" + source: hosted + version: "2.5.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: bd14436108211b0d4ee5038689a56d4ae3620fd72fd6036e113bf1345bc74d9e + url: "https://pub.dev" + source: hosted + version: "2.4.13" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" sky_engine: dependency: transitive description: flutter @@ -208,6 +389,22 @@ packages: url: "https://pub.dev" source: hosted version: "15.0.0" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" sdks: dart: ">=3.8.0 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + flutter: ">=3.29.0" diff --git a/src/fireflake/pubspec.yaml b/src/fireflake/pubspec.yaml index e7202ae..f798866 100644 --- a/src/fireflake/pubspec.yaml +++ b/src/fireflake/pubspec.yaml @@ -12,6 +12,12 @@ dependencies: sdk: flutter cupertino_icons: ^1.0.6 + flutter_bloc: ^8.1.6 + bloc: ^8.1.4 + equatable: ^2.0.5 + path_provider: ^2.1.4 + path: ^1.9.0 + shared_preferences: ^2.2.3 dev_dependencies: flutter_test: From f847acb25441ddeb48970f144f898f3c6597cf35 Mon Sep 17 00:00:00 2001 From: Elena G Blanco Date: Sat, 13 Dec 2025 16:20:47 +0100 Subject: [PATCH 2/8] Project management --- src/fireflake/lib/author_info_page.dart | 23 +- src/fireflake/lib/info_views/projectinfo.dart | 129 ++++--- src/fireflake/lib/projects_page.dart | 103 ++++-- src/fireflake/lib/state/app_cubit.dart | 31 +- src/fireflake/lib/state/author_cubit.dart | 5 +- src/fireflake/lib/step_views/step1.dart | 34 +- src/fireflake/lib/step_views/step2.dart | 48 +-- src/fireflake/lib/step_views/step3.dart | 61 ++-- src/fireflake/lib/step_views/step4.dart | 72 ++-- src/fireflake/lib/step_views/step5.dart | 80 ++--- src/fireflake/lib/step_views/step6.dart | 44 ++- src/fireflake/lib/step_views/step7.dart | 32 +- src/fireflake/lib/step_views/step8.dart | 47 +-- src/fireflake/lib/step_views/step9.dart | 337 ++++++++++-------- 14 files changed, 604 insertions(+), 442 deletions(-) diff --git a/src/fireflake/lib/author_info_page.dart b/src/fireflake/lib/author_info_page.dart index 07063e3..8691b04 100644 --- a/src/fireflake/lib/author_info_page.dart +++ b/src/fireflake/lib/author_info_page.dart @@ -40,7 +40,7 @@ class _AuthorInfoPageState extends State { email: _emailController.text, ); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Información del autor guardada')), + const SnackBar(content: Text('Author info saved')), ); } } @@ -48,7 +48,7 @@ class _AuthorInfoPageState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('Información del Autor')), + appBar: AppBar(title: const Text('Author Information')), body: BlocBuilder( builder: (context, settings) { // Actualizar controladores cuando cambie el estado @@ -69,7 +69,7 @@ class _AuthorInfoPageState extends State { child: ListView( children: [ const Text( - 'Esta configuración se guarda de forma global y será usada en todos los proyectos.', + 'This configuration is stored globally and will be used across all projects.', style: TextStyle( fontStyle: FontStyle.italic, color: Colors.grey, @@ -79,17 +79,18 @@ class _AuthorInfoPageState extends State { TextFormField( controller: _nameController, decoration: const InputDecoration( - labelText: 'Nombre', + labelText: 'Name', border: OutlineInputBorder(), ), - validator: (value) => - value == null || value.isEmpty ? 'Ingresa tu nombre' : null, + validator: (value) => value == null || value.isEmpty + ? 'Enter your name' + : null, ), const SizedBox(height: 16), TextFormField( controller: _bioController, decoration: const InputDecoration( - labelText: 'Biografía', + labelText: 'Bio', border: OutlineInputBorder(), alignLabelWithHint: true, ), @@ -105,10 +106,10 @@ class _AuthorInfoPageState extends State { keyboardType: TextInputType.emailAddress, validator: (value) { if (value == null || value.isEmpty) { - return 'Ingresa tu email'; + return 'Enter your email'; } if (!value.contains('@')) { - return 'Ingresa un email válido'; + return 'Enter a valid email'; } return null; }, @@ -120,7 +121,7 @@ class _AuthorInfoPageState extends State { padding: const EdgeInsets.all(16), ), child: const Text( - 'Guardar', + 'Save', style: TextStyle(fontSize: 16), ), ), @@ -132,4 +133,4 @@ class _AuthorInfoPageState extends State { ), ); } -} \ No newline at end of file +} diff --git a/src/fireflake/lib/info_views/projectinfo.dart b/src/fireflake/lib/info_views/projectinfo.dart index e193000..05ea15d 100644 --- a/src/fireflake/lib/info_views/projectinfo.dart +++ b/src/fireflake/lib/info_views/projectinfo.dart @@ -36,7 +36,7 @@ class _ProjectInfoPageState extends State { ); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( - content: Text('Proyecto guardado'), + content: Text('Project saved'), backgroundColor: Colors.green, ), ); @@ -78,23 +78,23 @@ class _ProjectInfoPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Información del Proyecto', - style: Theme.of(context).textTheme.headlineMedium?.copyWith( - fontWeight: FontWeight.bold, - color: Theme.of(context).primaryColor, - ), + 'Project Information', + style: + Theme.of(context).textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).primaryColor, + ), ), const SizedBox(height: 8), Text( - 'Completa la información básica de tu proyecto de escritura', + 'Fill in the basic information for your writing project', style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: Colors.grey[600], - ), + color: Colors.grey[600], + ), ), ], ), ), - Expanded( child: SingleChildScrollView( child: Column( @@ -107,26 +107,29 @@ class _ProjectInfoPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Título del Proyecto', - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, - ), + 'Project title', + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith( + fontWeight: FontWeight.w600, + ), ), const SizedBox(height: 12), TextFormField( controller: _titleController, decoration: const InputDecoration( - labelText: 'Título', - hintText: 'Ej: El Reino de las Sombras', + labelText: 'Title', + hintText: 'E.g. The Shadow Kingdom', border: OutlineInputBorder(), prefixIcon: Icon(Icons.title), ), validator: (value) { if (value == null || value.trim().isEmpty) { - return 'El título es obligatorio'; + return 'Title is required'; } if (value.trim().length < 3) { - return 'El título debe tener al menos 3 caracteres'; + return 'Title must be at least 3 characters'; } return null; }, @@ -135,9 +138,7 @@ class _ProjectInfoPageState extends State { ), ), ), - const SizedBox(height: 20), - Card( elevation: 2, child: Padding( @@ -146,31 +147,39 @@ class _ProjectInfoPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Subtítulo', - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, - ), + 'Subtitle', + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith( + fontWeight: FontWeight.w600, + ), ), const SizedBox(height: 8), Text( - 'Descripción breve o tagline del proyecto', - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Colors.grey[600], - ), + 'Short description or tagline of the project', + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith( + color: Colors.grey[600], + ), ), const SizedBox(height: 12), TextFormField( controller: _subtitleController, decoration: const InputDecoration( - labelText: 'Subtítulo', - hintText: 'Ej: Una épica aventura de fantasía', + labelText: 'Subtitle', + hintText: 'E.g. An epic fantasy adventure', border: OutlineInputBorder(), prefixIcon: Icon(Icons.short_text), ), maxLines: 2, validator: (value) { - if (value != null && value.isNotEmpty && value.trim().length < 10) { - return 'El subtítulo debe tener al menos 10 caracteres o estar vacío'; + if (value != null && + value.isNotEmpty && + value.trim().length < 10) { + return 'Subtitle must have at least 10 characters or be empty'; } return null; }, @@ -179,9 +188,7 @@ class _ProjectInfoPageState extends State { ), ), ), - const SizedBox(height: 20), - Card( elevation: 2, child: Padding( @@ -190,27 +197,33 @@ class _ProjectInfoPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Número de palabras', - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, - ), + 'Word count', + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith( + fontWeight: FontWeight.w600, + ), ), const SizedBox(height: 8), Text( - 'Número aproximado para completar el proyecto', - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Colors.grey[600], - ), + 'Approximate total to complete the project', + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith( + color: Colors.grey[600], + ), ), const SizedBox(height: 12), TextFormField( controller: _wordCountController, decoration: const InputDecoration( - labelText: 'Número de palabras', - hintText: 'Ej: 80000', + labelText: 'Word count', + hintText: 'E.g. 80000', border: OutlineInputBorder(), prefixIcon: Icon(Icons.format_list_numbered), - suffixText: 'palabras', + suffixText: 'words', ), keyboardType: TextInputType.number, inputFormatters: [ @@ -218,17 +231,17 @@ class _ProjectInfoPageState extends State { ], validator: (value) { if (value == null || value.trim().isEmpty) { - return 'El número de palabras es obligatorio'; + return 'Word count is required'; } final int? wordCount = int.tryParse(value); if (wordCount == null) { - return 'Debe ser un número válido'; + return 'Enter a valid number'; } if (wordCount < 1000) { - return 'El conteo debe ser de al menos 1,000 palabras'; + return 'Word count must be at least 1,000'; } if (wordCount > 1000000) { - return 'El conteo no puede exceder 1,000,000 palabras'; + return 'Word count cannot exceed 1,000,000'; } return null; }, @@ -237,9 +250,7 @@ class _ProjectInfoPageState extends State { ), ), ), - const SizedBox(height: 32), - Container( padding: const EdgeInsets.all(16.0), decoration: BoxDecoration( @@ -249,14 +260,15 @@ class _ProjectInfoPageState extends State { ), child: Row( children: [ - Icon(Icons.info_outline, color: Colors.blue.shade700), + Icon(Icons.info_outline, + color: Colors.blue.shade700), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Referencia de conteo de palabras:', + 'Word count reference:', style: TextStyle( fontWeight: FontWeight.w600, color: Colors.blue.shade700, @@ -264,9 +276,9 @@ class _ProjectInfoPageState extends State { ), const SizedBox(height: 4), Text( - '• Novela corta: 50,000 - 80,000 palabras\n' - '• Novela estándar: 80,000 - 100,000 palabras\n' - '• Novela épica: 100,000+ palabras', + '• Novella: 50,000 - 80,000 words\n' + '• Standard novel: 80,000 - 100,000 words\n' + '• Epic novel: 100,000+ words', style: TextStyle( fontSize: 13, color: Colors.blue.shade600, @@ -282,7 +294,6 @@ class _ProjectInfoPageState extends State { ), ), ), - Container( padding: const EdgeInsets.only(top: 24), child: Row( @@ -291,7 +302,7 @@ class _ProjectInfoPageState extends State { child: OutlinedButton.icon( onPressed: _clearForm, icon: const Icon(Icons.clear), - label: const Text('Limpiar'), + label: const Text('Clear'), ), ), const SizedBox(width: 16), @@ -300,7 +311,7 @@ class _ProjectInfoPageState extends State { child: ElevatedButton.icon( onPressed: _saveProject, icon: const Icon(Icons.save), - label: const Text('Guardar Proyecto'), + label: const Text('Save Project'), ), ), ], diff --git a/src/fireflake/lib/projects_page.dart b/src/fireflake/lib/projects_page.dart index 4ca6514..a750ec9 100644 --- a/src/fireflake/lib/projects_page.dart +++ b/src/fireflake/lib/projects_page.dart @@ -13,8 +13,6 @@ class ProjectsPage extends StatefulWidget { } class _ProjectsPageState extends State { - List projectsToLoad = [Project(title: 'Project 1', expectedWordCount: 1000)]; - @override void initState() { super.initState(); @@ -33,11 +31,20 @@ class _ProjectsPageState extends State { Container( padding: const EdgeInsets.all(20), width: 220, - height: 320, - color: Colors.grey, + height: 360, + color: Colors.grey.shade800, child: BlocBuilder( builder: (context, state) { - final projects = state.projects.isEmpty ? projectsToLoad : state.projects; + final projects = state.projects; + if (projects.isEmpty) { + return const Center( + child: Text( + 'No projects yet.\nTap "New" to create one.', + textAlign: TextAlign.center, + style: TextStyle(color: Colors.white70), + ), + ); + } return buildProjectList(projects); }, )), @@ -52,27 +59,17 @@ class _ProjectsPageState extends State { padding: const EdgeInsets.all(8.0), child: TextButton( style: mainButtonStyle(), - onPressed: () { - final cubit = context.read(); - cubit.loadInitial(projectsToLoad); - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => ProjectWrapper(), - ), - ); + onPressed: () async { + await context.read().loadProjectsFromDisk(); }, - child: const Text('Open')), + child: const Text('Refresh')), ), Padding( padding: const EdgeInsets.all(8.0), child: TextButton( style: mainButtonStyle(), onPressed: () { - final cubit = context.read(); - final newProject = Project(title: 'New Project', expectedWordCount: 2000); - final updated = List.from(cubit.state.projects)..add(newProject); - cubit.loadInitial(updated); - cubit.selectProject(newProject); + _createNewProject(context); }, child: const Text('New')), ), @@ -84,10 +81,10 @@ class _ProjectsPageState extends State { await context.read().saveCurrentProjectToDisk(); if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Proyecto guardado en disco')), + const SnackBar(content: Text('Project saved to disk')), ); }, - child: const Text('Guardar'), + child: const Text('Save'), ), ), ], @@ -126,7 +123,9 @@ class _ProjectsPageState extends State { itemBuilder: (context, index) { return GestureDetector( onTap: () async { - await context.read().loadAndSelectProject(projects[index].title); + await context + .read() + .loadAndSelectProject(projects[index].title); if (!context.mounted) return; Navigator.of(context).push( MaterialPageRoute( @@ -142,4 +141,64 @@ class _ProjectsPageState extends State { }, ); } + + Future _createNewProject(BuildContext context) async { + final titleController = TextEditingController(text: 'New project'); + final wordsController = TextEditingController(text: '50000'); + final cubit = context.read(); + final navigator = Navigator.of(context); + + final result = await showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text('Create project'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: titleController, + decoration: const InputDecoration(labelText: 'Title'), + ), + const SizedBox(height: 12), + TextField( + controller: wordsController, + keyboardType: TextInputType.number, + decoration: + const InputDecoration(labelText: 'Expected word count'), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text('Cancel'), + ), + ElevatedButton( + onPressed: () => Navigator.of(context).pop(true), + child: const Text('Create'), + ), + ], + ); + }, + ); + + if (result != true) return; + if (!mounted) return; + final title = titleController.text.trim().isEmpty + ? 'New project' + : titleController.text.trim(); + final expected = int.tryParse(wordsController.text.trim()) ?? 50000; + cubit.saveProject(title: title, expectedWordCount: expected); + final created = cubit.state.projects.firstWhere((p) => p.title == title); + cubit.selectProject(created); + await cubit.saveCurrentProjectToDisk(); + + if (!mounted) return; + navigator.push( + MaterialPageRoute( + builder: (context) => ProjectWrapper(), + ), + ); + } } diff --git a/src/fireflake/lib/state/app_cubit.dart b/src/fireflake/lib/state/app_cubit.dart index 13faa8f..cb5e2fb 100644 --- a/src/fireflake/lib/state/app_cubit.dart +++ b/src/fireflake/lib/state/app_cubit.dart @@ -23,7 +23,8 @@ class AppState extends Equatable { Project? selectedProject, List? scenes, List? characters, - }) => AppState( + }) => + AppState( projects: projects ?? this.projects, selectedProject: selectedProject ?? this.selectedProject, scenes: scenes ?? this.scenes, @@ -127,7 +128,8 @@ class AppCubit extends Cubit { final updatedProjects = state.projects .map((p) => p.title == selected.title ? updatedProject : p) .toList(); - emit(state.copyWith(projects: updatedProjects, selectedProject: updatedProject)); + emit(state.copyWith( + projects: updatedProjects, selectedProject: updatedProject)); } void updateExpandedParagraphs({ @@ -147,17 +149,20 @@ class AppCubit extends Cubit { final updatedProjects = state.projects .map((p) => p.title == selected.title ? updatedProject : p) .toList(); - emit(state.copyWith(projects: updatedProjects, selectedProject: updatedProject)); + emit(state.copyWith( + projects: updatedProjects, selectedProject: updatedProject)); } void updateExtendedArgument(String extendedArgument) { final selected = state.selectedProject; if (selected == null) return; - final updatedProject = selected.copyWith(extendedArgument: extendedArgument); + final updatedProject = + selected.copyWith(extendedArgument: extendedArgument); final updatedProjects = state.projects .map((p) => p.title == selected.title ? updatedProject : p) .toList(); - emit(state.copyWith(projects: updatedProjects, selectedProject: updatedProject)); + emit(state.copyWith( + projects: updatedProjects, selectedProject: updatedProject)); } void updatePendingScenes(List pendingScenes) { @@ -167,7 +172,8 @@ class AppCubit extends Cubit { final updatedProjects = state.projects .map((p) => p.title == selected.title ? updatedProject : p) .toList(); - emit(state.copyWith(projects: updatedProjects, selectedProject: updatedProject)); + emit(state.copyWith( + projects: updatedProjects, selectedProject: updatedProject)); } void addScene(Scene scene) { @@ -214,6 +220,10 @@ class AppCubit extends Cubit { emit(state.copyWith(characters: updated)); } + void setCharacters(List characters) { + emit(state.copyWith(characters: List.from(characters))); + } + void removeCharacter(Character character) { final updated = List.from(state.characters)..remove(character); emit(state.copyWith(characters: updated)); @@ -223,11 +233,20 @@ class AppCubit extends Cubit { final project = state.selectedProject; if (project == null) return; final merged = project.copyWith( + title: project.title, + subtitle: project.subtitle, + expectedWordCount: project.expectedWordCount, summary: project.summary, act1: project.act1, act2: project.act2, act3: project.act3, finale: project.finale, + expandedAct1: project.expandedAct1, + expandedAct2: project.expandedAct2, + expandedAct3: project.expandedAct3, + expandedFinale: project.expandedFinale, + extendedArgument: project.extendedArgument, + pendingScenes: project.pendingScenes, scenes: state.scenes, characters: state.characters, ); diff --git a/src/fireflake/lib/state/author_cubit.dart b/src/fireflake/lib/state/author_cubit.dart index 0f163f6..7c92040 100644 --- a/src/fireflake/lib/state/author_cubit.dart +++ b/src/fireflake/lib/state/author_cubit.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'package:bloc/bloc.dart'; +import 'package:flutter/foundation.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../models/author_settings.dart'; @@ -22,7 +23,7 @@ class AuthorCubit extends Cubit { } } catch (e) { // Si hay error, mantener valores por defecto - print('Error loading author settings: $e'); + debugPrint('Error loading author settings: $e'); } } @@ -34,7 +35,7 @@ class AuthorCubit extends Cubit { await prefs.setString(_storageKey, jsonString); emit(settings); } catch (e) { - print('Error saving author settings: $e'); + debugPrint('Error saving author settings: $e'); } } diff --git a/src/fireflake/lib/step_views/step1.dart b/src/fireflake/lib/step_views/step1.dart index b4246b7..b367d62 100644 --- a/src/fireflake/lib/step_views/step1.dart +++ b/src/fireflake/lib/step_views/step1.dart @@ -38,7 +38,7 @@ class _StepOnePageState extends State { ); context.read().saveCurrentProjectToDisk(); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Resumen guardado')), + const SnackBar(content: Text('Summary saved')), ); } @@ -53,16 +53,18 @@ class _StepOnePageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Paso 1: Resumen en una frase', - style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold), + 'Step 1: One-sentence summary', + style: Theme.of(context) + .textTheme + .headlineSmall + ?.copyWith(fontWeight: FontWeight.bold), ), const SizedBox(height: 8), Text( - 'Escribe el resumen de tu historia en una sola frase. Este será el corazón de tu proyecto.', + 'Write your story in a single sentence. This is the core of your project.', style: Theme.of(context).textTheme.bodyMedium, ), const SizedBox(height: 24), - Card( elevation: 1, child: Padding( @@ -71,34 +73,38 @@ class _StepOnePageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Resumen en una frase', - style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600), + 'One-sentence summary', + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith(fontWeight: FontWeight.w600), ), const SizedBox(height: 12), TextFormField( controller: _summaryController, maxLines: 3, decoration: const InputDecoration( - labelText: 'Tu historia en una sola frase', - hintText: 'Ejemplo: Un joven mago descubre que es el elegido para salvar el mundo de la oscuridad.', + labelText: 'Your story in one sentence', + hintText: + 'Example: A young wizard learns he is the chosen one to save the world from darkness.', border: OutlineInputBorder(), ), - validator: (value) => (value == null || value.trim().isEmpty) - ? 'El resumen es requerido' - : null, + validator: (value) => + (value == null || value.trim().isEmpty) + ? 'Summary is required' + : null, ), ], ), ), ), - const SizedBox(height: 24), SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: _save, icon: const Icon(Icons.save), - label: const Text('Guardar'), + label: const Text('Save'), ), ), ], diff --git a/src/fireflake/lib/step_views/step2.dart b/src/fireflake/lib/step_views/step2.dart index d3af4ec..3898c6a 100644 --- a/src/fireflake/lib/step_views/step2.dart +++ b/src/fireflake/lib/step_views/step2.dart @@ -54,7 +54,7 @@ class _StepTwoPageState extends State { ); context.read().saveCurrentProjectToDisk(); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Resumen ampliado guardado')), + const SnackBar(content: Text('Expanded summary saved')), ); } @@ -69,16 +69,18 @@ class _StepTwoPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Ampliación del resumen', - style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold), + 'Expand the summary', + style: Theme.of(context) + .textTheme + .headlineSmall + ?.copyWith(fontWeight: FontWeight.bold), ), const SizedBox(height: 8), Text( - 'Desglosa el resumen en actos principales y final para estructurar la historia.', + 'Break the summary into main acts and finale to structure the story.', style: Theme.of(context).textTheme.bodyMedium, ), const SizedBox(height: 24), - _buildCard( context, title: 'Resumen general', @@ -86,16 +88,15 @@ class _StepTwoPageState extends State { controller: _summaryController, maxLines: 4, decoration: const InputDecoration( - labelText: 'Resumen general del proyecto', + labelText: 'Overall project summary', border: OutlineInputBorder(), ), validator: (value) => (value == null || value.trim().isEmpty) - ? 'El resumen es requerido' + ? 'Summary is required' : null, ), ), const SizedBox(height: 16), - _buildCard( context, title: 'Acto I', @@ -103,16 +104,15 @@ class _StepTwoPageState extends State { controller: _act1Controller, maxLines: 3, decoration: const InputDecoration( - labelText: 'Planteamiento / Acto I', + labelText: 'Setup / Act I', border: OutlineInputBorder(), ), validator: (value) => (value == null || value.trim().isEmpty) - ? 'Describe el Acto I' + ? 'Describe Act I' : null, ), ), const SizedBox(height: 12), - _buildCard( context, title: 'Acto II', @@ -120,16 +120,15 @@ class _StepTwoPageState extends State { controller: _act2Controller, maxLines: 3, decoration: const InputDecoration( - labelText: 'Nudo / Acto II', + labelText: 'Confrontation / Act II', border: OutlineInputBorder(), ), validator: (value) => (value == null || value.trim().isEmpty) - ? 'Describe el Acto II' + ? 'Describe Act II' : null, ), ), const SizedBox(height: 12), - _buildCard( context, title: 'Acto III', @@ -137,16 +136,15 @@ class _StepTwoPageState extends State { controller: _act3Controller, maxLines: 3, decoration: const InputDecoration( - labelText: 'Clímax / Acto III', + labelText: 'Climax / Act III', border: OutlineInputBorder(), ), validator: (value) => (value == null || value.trim().isEmpty) - ? 'Describe el Acto III' + ? 'Describe Act III' : null, ), ), const SizedBox(height: 12), - _buildCard( context, title: 'Final', @@ -154,22 +152,21 @@ class _StepTwoPageState extends State { controller: _finaleController, maxLines: 3, decoration: const InputDecoration( - labelText: 'Resolución / Final', + labelText: 'Resolution / Finale', border: OutlineInputBorder(), ), validator: (value) => (value == null || value.trim().isEmpty) - ? 'Describe el final' + ? 'Describe the ending' : null, ), ), - const SizedBox(height: 24), SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: _save, icon: const Icon(Icons.save), - label: const Text('Guardar'), + label: const Text('Save'), ), ), ], @@ -179,7 +176,8 @@ class _StepTwoPageState extends State { ); } - Widget _buildCard(BuildContext context, {required String title, required Widget child}) { + Widget _buildCard(BuildContext context, + {required String title, required Widget child}) { return Card( elevation: 1, child: Padding( @@ -187,7 +185,11 @@ class _StepTwoPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(title, style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600)), + Text(title, + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith(fontWeight: FontWeight.w600)), const SizedBox(height: 12), child, ], diff --git a/src/fireflake/lib/step_views/step3.dart b/src/fireflake/lib/step_views/step3.dart index d185578..bf0cdcb 100644 --- a/src/fireflake/lib/step_views/step3.dart +++ b/src/fireflake/lib/step_views/step3.dart @@ -83,13 +83,12 @@ class _StepThreePageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( - 'Información del Personaje', + 'Character Information', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), ), const SizedBox(height: 20), - const Text( - 'Su historia (elementos con una sola frase)', + 'Backstory (single-sentence elements)', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600), ), const SizedBox(height: 10), @@ -104,12 +103,12 @@ class _StepThreePageState extends State { child: TextFormField( controller: controller, decoration: InputDecoration( - labelText: 'Elemento de historia ${index + 1}', + labelText: 'Story element ${index + 1}', border: const OutlineInputBorder(), ), validator: (value) { if (value == null || value.isEmpty) { - return 'Este campo es requerido'; + return 'This field is required'; } return null; }, @@ -126,76 +125,71 @@ class _StepThreePageState extends State { TextButton.icon( onPressed: _addHistoryField, icon: const Icon(Icons.add), - label: const Text('Agregar elemento de historia'), + label: const Text('Add story element'), ), const SizedBox(height: 20), - TextFormField( controller: _motivationsController, decoration: const InputDecoration( - labelText: 'Sus motivaciones', + labelText: 'Motivations', border: OutlineInputBorder(), ), maxLines: 3, validator: (value) { if (value == null || value.isEmpty) { - return 'Este campo es requerido'; + return 'This field is required'; } return null; }, ), const SizedBox(height: 16), - TextFormField( controller: _objectiveController, decoration: const InputDecoration( - labelText: 'Su objetivo', + labelText: 'Goal', border: OutlineInputBorder(), ), maxLines: 3, validator: (value) { if (value == null || value.isEmpty) { - return 'Este campo es requerido'; + return 'This field is required'; } return null; }, ), const SizedBox(height: 16), - TextFormField( controller: _conflictController, decoration: const InputDecoration( - labelText: 'Su conflicto', + labelText: 'Conflict', border: OutlineInputBorder(), ), maxLines: 3, validator: (value) { if (value == null || value.isEmpty) { - return 'Este campo es requerido'; + return 'This field is required'; } return null; }, ), const SizedBox(height: 16), - TextFormField( controller: _epiphanyController, decoration: const InputDecoration( - labelText: 'Epifanía', + labelText: 'Epiphany', border: OutlineInputBorder(), ), maxLines: 3, validator: (value) { if (value == null || value.isEmpty) { - return 'Este campo es requerido'; + return 'This field is required'; } return null; }, ), const SizedBox(height: 20), - const Text( - 'Su historia final (elementos con una sola frase)', + 'Final backstory (single-sentence elements)', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600), ), const SizedBox(height: 10), @@ -210,12 +204,12 @@ class _StepThreePageState extends State { child: TextFormField( controller: controller, decoration: InputDecoration( - labelText: 'Elemento de historia final ${index + 1}', + labelText: 'Final story element ${index + 1}', border: const OutlineInputBorder(), ), validator: (value) { if (value == null || value.isEmpty) { - return 'Este campo es requerido'; + return 'This field is required'; } return null; }, @@ -232,28 +226,33 @@ class _StepThreePageState extends State { TextButton.icon( onPressed: _addFinalHistoryField, icon: const Icon(Icons.add), - label: const Text('Agregar elemento de historia final'), + label: const Text('Add final story element'), ), const SizedBox(height: 30), - Center( child: ElevatedButton( onPressed: () { if (_formKey.currentState!.validate()) { - final name = _historyControllers.isNotEmpty && _historyControllers.first.text.isNotEmpty + final name = _historyControllers.isNotEmpty && + _historyControllers.first.text.isNotEmpty ? _historyControllers.first.text : 'Personaje'; - final character = Character(name: name, storygoal: _objectiveController.text); - context.read().addCharacter(character); - context.read().saveCurrentProjectToDisk(); + final character = Character( + name: name, storygoal: _objectiveController.text); + final cubit = context.read(); + final updated = + List.from(cubit.state.characters) + ..add(character); + cubit.setCharacters(updated); + cubit.saveCurrentProjectToDisk(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( - content: Text('Información del personaje guardada'), + content: Text('Character info saved'), ), ); } }, - child: const Text('Guardar Información'), + child: const Text('Save Information'), ), ), ], @@ -264,5 +263,3 @@ class _StepThreePageState extends State { ); } } - - diff --git a/src/fireflake/lib/step_views/step4.dart b/src/fireflake/lib/step_views/step4.dart index 4d356b5..3de2b71 100644 --- a/src/fireflake/lib/step_views/step4.dart +++ b/src/fireflake/lib/step_views/step4.dart @@ -30,10 +30,19 @@ class _StepFourPageState extends State { final project = context.read().state.selectedProject; if (project == null) return; - _controllers['act1'] = TextEditingController(text: project.expandedAct1.isEmpty ? project.act1 : project.expandedAct1); - _controllers['act2'] = TextEditingController(text: project.expandedAct2.isEmpty ? project.act2 : project.expandedAct2); - _controllers['act3'] = TextEditingController(text: project.expandedAct3.isEmpty ? project.act3 : project.expandedAct3); - _controllers['finale'] = TextEditingController(text: project.expandedFinale.isEmpty ? project.finale : project.expandedFinale); + _controllers['act1'] = TextEditingController( + text: + project.expandedAct1.isEmpty ? project.act1 : project.expandedAct1); + _controllers['act2'] = TextEditingController( + text: + project.expandedAct2.isEmpty ? project.act2 : project.expandedAct2); + _controllers['act3'] = TextEditingController( + text: + project.expandedAct3.isEmpty ? project.act3 : project.expandedAct3); + _controllers['finale'] = TextEditingController( + text: project.expandedFinale.isEmpty + ? project.finale + : project.expandedFinale); _initialized = true; } @@ -51,7 +60,7 @@ class _StepFourPageState extends State { cubit.saveCurrentProjectToDisk(); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Párrafos expandidos guardados')), + const SnackBar(content: Text('Expanded paragraphs saved')), ); } @@ -62,11 +71,16 @@ class _StepFourPageState extends State { builder: (context, state) { final project = state.selectedProject; if (project == null) { - return const Center(child: Text('Selecciona o crea un proyecto primero')); + return const Center( + child: Text('Select or create a project first')); } - if (project.act1.isEmpty && project.act2.isEmpty && project.act3.isEmpty && project.finale.isEmpty) { - return const Center(child: Text('Completa el Paso 2 primero (actos y final)')); + if (project.act1.isEmpty && + project.act2.isEmpty && + project.act3.isEmpty && + project.finale.isEmpty) { + return const Center( + child: Text('Complete Step 2 first (acts and finale)')); } if (!_initialized) { @@ -83,43 +97,45 @@ class _StepFourPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Paso 4: Expandir a párrafos', - style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold), + 'Step 4: Expand into paragraphs', + style: Theme.of(context) + .textTheme + .headlineSmall + ?.copyWith(fontWeight: FontWeight.bold), ), const SizedBox(height: 8), Text( - 'Convierte cada frase del Paso 2 en un párrafo completo.', + 'Turn each sentence from Step 2 into a full paragraph.', style: Theme.of(context).textTheme.bodyMedium, ), const SizedBox(height: 24), - if (project.act1.isNotEmpty) ...[ - _buildExpandedSection(context, 'Acto I - Expandido', 'act1'), + _buildExpandedSection( + context, 'Acto I - Expandido', 'act1'), const SizedBox(height: 16), ], - if (project.act2.isNotEmpty) ...[ - _buildExpandedSection(context, 'Acto II - Expandido', 'act2'), + _buildExpandedSection( + context, 'Acto II - Expandido', 'act2'), const SizedBox(height: 16), ], - if (project.act3.isNotEmpty) ...[ - _buildExpandedSection(context, 'Acto III - Expandido', 'act3'), + _buildExpandedSection( + context, 'Acto III - Expandido', 'act3'), const SizedBox(height: 16), ], - if (project.finale.isNotEmpty) ...[ - _buildExpandedSection(context, 'Final - Expandido', 'finale'), + _buildExpandedSection( + context, 'Final - Expandido', 'finale'), const SizedBox(height: 16), ], - const SizedBox(height: 8), SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: _save, icon: const Icon(Icons.save), - label: const Text('Guardar párrafos expandidos'), + label: const Text('Save expanded paragraphs'), ), ), ], @@ -141,19 +157,25 @@ class _StepFourPageState extends State { children: [ Text( title, - style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600), + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith(fontWeight: FontWeight.w600), ), const SizedBox(height: 12), TextFormField( controller: _controllers[key], maxLines: 6, decoration: InputDecoration( - labelText: 'Párrafo completo para $title', - hintText: 'Expande la frase del Paso 2 en un párrafo narrativo más detallado...', + labelText: 'Full paragraph for $title', + hintText: + 'Expand the sentence from Step 2 into a richer narrative paragraph...', border: const OutlineInputBorder(), alignLabelWithHint: true, ), - validator: (value) => (value == null || value.trim().isEmpty) ? 'Este párrafo es requerido' : null, + validator: (value) => (value == null || value.trim().isEmpty) + ? 'This paragraph is required' + : null, ), ], ), diff --git a/src/fireflake/lib/step_views/step5.dart b/src/fireflake/lib/step_views/step5.dart index 096e9da..8d101c7 100644 --- a/src/fireflake/lib/step_views/step5.dart +++ b/src/fireflake/lib/step_views/step5.dart @@ -73,27 +73,19 @@ class _StepFivePageState extends State { if (_formKey.currentState?.validate() != true) return; final cubit = context.read(); - - // Limpiar personajes actuales y agregar los nuevos - final currentCharacters = List.from(cubit.state.characters); - currentCharacters.clear(); - - for (var controllerMap in _characterControllers) { - final name = controllerMap['name']!.text.trim(); - final description = controllerMap['description']!.text.trim(); - if (name.isNotEmpty && description.isNotEmpty) { - currentCharacters.add(Character(name: name, storygoal: description)); - } - } - - // Actualizar el estado con los personajes - for (var char in currentCharacters) { - cubit.addCharacter(char); - } - + final currentCharacters = _characterControllers + .map((controllerMap) => Character( + name: controllerMap['name']!.text.trim(), + storygoal: controllerMap['description']!.text.trim(), + )) + .where((character) => + character.name.isNotEmpty && character.storygoal.isNotEmpty) + .toList(); + + cubit.setCharacters(currentCharacters); cubit.saveCurrentProjectToDisk(); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Personajes principales guardados')), + const SnackBar(content: Text('Main characters saved')), ); } @@ -108,16 +100,18 @@ class _StepFivePageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Paso 5: Personajes principales', - style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold), + 'Step 5: Main characters', + style: Theme.of(context) + .textTheme + .headlineSmall + ?.copyWith(fontWeight: FontWeight.bold), ), const SizedBox(height: 8), Text( - 'Describe los personajes principales de tu historia (máximo $_maxCharacters).', + 'Describe the main characters of your story (maximum $_maxCharacters).', style: Theme.of(context).textTheme.bodyMedium, ), const SizedBox(height: 24), - ..._characterControllers.asMap().entries.map((entry) { final index = entry.key; final controllers = entry.value; @@ -134,14 +128,18 @@ class _StepFivePageState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - 'Personaje ${index + 1}', - style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600), + 'Character ${index + 1}', + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith(fontWeight: FontWeight.w600), ), if (_characterControllers.length > 1) IconButton( - icon: const Icon(Icons.delete_outline, color: Colors.red), + icon: const Icon(Icons.delete_outline, + color: Colors.red), onPressed: () => _removeCharacterField(index), - tooltip: 'Eliminar personaje', + tooltip: 'Delete character', ), ], ), @@ -149,28 +147,30 @@ class _StepFivePageState extends State { TextFormField( controller: controllers['name'], decoration: const InputDecoration( - labelText: 'Nombre del personaje', - hintText: 'Ej: Aragorn', + labelText: 'Character name', + hintText: 'E.g. Aragorn', border: OutlineInputBorder(), prefixIcon: Icon(Icons.person), ), - validator: (value) => (value == null || value.trim().isEmpty) - ? 'El nombre es requerido' - : null, + validator: (value) => + (value == null || value.trim().isEmpty) + ? 'Name is required' + : null, ), const SizedBox(height: 12), TextFormField( controller: controllers['description'], maxLines: 4, decoration: const InputDecoration( - labelText: 'Descripción del personaje', - hintText: 'Rol, motivación, conflicto, objetivos...', + labelText: 'Character description', + hintText: 'Role, motivation, conflict, goals...', border: OutlineInputBorder(), alignLabelWithHint: true, ), - validator: (value) => (value == null || value.trim().isEmpty) - ? 'La descripción es requerida' - : null, + validator: (value) => + (value == null || value.trim().isEmpty) + ? 'Description is required' + : null, ), ], ), @@ -178,21 +178,19 @@ class _StepFivePageState extends State { ), ); }), - if (_characterControllers.length < _maxCharacters) OutlinedButton.icon( onPressed: _addCharacterField, icon: const Icon(Icons.add), - label: const Text('Agregar personaje'), + label: const Text('Add character'), ), - const SizedBox(height: 24), SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: _save, icon: const Icon(Icons.save), - label: const Text('Guardar personajes'), + label: const Text('Save characters'), ), ), ], diff --git a/src/fireflake/lib/step_views/step6.dart b/src/fireflake/lib/step_views/step6.dart index f582627..d77ec0b 100644 --- a/src/fireflake/lib/step_views/step6.dart +++ b/src/fireflake/lib/step_views/step6.dart @@ -40,7 +40,7 @@ class _StepSixPageState extends State { cubit.saveCurrentProjectToDisk(); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Argumento ampliado guardado')), + const SnackBar(content: Text('Extended argument saved')), ); } @@ -51,7 +51,8 @@ class _StepSixPageState extends State { builder: (context, state) { final project = state.selectedProject; if (project == null) { - return const Center(child: Text('Selecciona o crea un proyecto primero')); + return const Center( + child: Text('Select or create a project first')); } return SingleChildScrollView( @@ -62,16 +63,18 @@ class _StepSixPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Paso 6: Ampliar argumento', - style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold), + 'Step 6: Expand argument', + style: Theme.of(context) + .textTheme + .headlineSmall + ?.copyWith(fontWeight: FontWeight.bold), ), const SizedBox(height: 8), Text( - 'Vuelve al Paso 4 y amplía el argumento con más detalle narrativo. Integra conflictos, giros y desarrollo de personajes.', + 'Return to Step 4 and expand the argument with more narrative detail. Integrate conflicts, twists, and character development.', style: Theme.of(context).textTheme.bodyMedium, ), const SizedBox(height: 24), - Card( elevation: 1, child: Padding( @@ -80,40 +83,47 @@ class _StepSixPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Argumento extendido', - style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600), + 'Extended argument', + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith(fontWeight: FontWeight.w600), ), const SizedBox(height: 8), Text( - 'Combina los párrafos del Paso 4 y añade más profundidad: conflictos internos, secundarios, giros de trama, desarrollo de relaciones...', - style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Colors.grey[700]), + 'Combine the paragraphs from Step 4 and add more depth: internal conflicts, subplots, twists, relationship arcs...', + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith(color: Colors.grey[700]), ), const SizedBox(height: 12), TextFormField( controller: _argumentController, maxLines: 15, decoration: const InputDecoration( - labelText: 'Argumento completo ampliado', - hintText: 'Escribe varios párrafos con el argumento completo, integrando todos los elementos narrativos...', + labelText: 'Full expanded argument', + hintText: + 'Write multiple paragraphs with the full argument, integrating all narrative elements...', border: OutlineInputBorder(), alignLabelWithHint: true, ), - validator: (value) => (value == null || value.trim().isEmpty) - ? 'El argumento ampliado es requerido' - : null, + validator: (value) => + (value == null || value.trim().isEmpty) + ? 'The extended argument is required' + : null, ), ], ), ), ), - const SizedBox(height: 24), SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: _save, icon: const Icon(Icons.save), - label: const Text('Guardar argumento ampliado'), + label: const Text('Save extended argument'), ), ), ], diff --git a/src/fireflake/lib/step_views/step7.dart b/src/fireflake/lib/step_views/step7.dart index 730db1e..25b6c3e 100644 --- a/src/fireflake/lib/step_views/step7.dart +++ b/src/fireflake/lib/step_views/step7.dart @@ -22,7 +22,7 @@ class _StepSevenPageState extends State { child: Padding( padding: EdgeInsets.all(24), child: Text( - 'No hay personajes principales aún.\nCompleta el Paso 5 primero.', + 'No main characters yet.\nComplete Step 5 first.', textAlign: TextAlign.center, style: TextStyle(fontSize: 16), ), @@ -36,16 +36,18 @@ class _StepSevenPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Paso 7: Tablas de personajes', - style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold), + 'Step 7: Character tables', + style: Theme.of(context) + .textTheme + .headlineSmall + ?.copyWith(fontWeight: FontWeight.bold), ), const SizedBox(height: 8), Text( - 'Revisa la información detallada de los personajes principales (creados en Paso 3 y Paso 5).', + 'Review the detailed info of main characters (created in Step 3 and Step 5).', style: Theme.of(context).textTheme.bodyMedium, ), const SizedBox(height: 24), - ...characters.asMap().entries.map((entry) { final index = entry.key; final character = entry.value; @@ -61,30 +63,36 @@ class _StepSevenPageState extends State { Row( children: [ CircleAvatar( - backgroundColor: Theme.of(context).primaryColor, + backgroundColor: + Theme.of(context).primaryColor, child: Text( '${index + 1}', - style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold), + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold), ), ), const SizedBox(width: 12), Expanded( child: Text( character.name, - style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), + style: Theme.of(context) + .textTheme + .titleLarge + ?.copyWith(fontWeight: FontWeight.bold), ), ), ], ), const Divider(height: 24), - _buildInfoRow(context, 'Descripción / Objetivo', character.storygoal), + _buildInfoRow(context, 'Descripción / Objetivo', + character.storygoal), ], ), ), ), ); }), - const SizedBox(height: 16), Container( padding: const EdgeInsets.all(16), @@ -99,7 +107,7 @@ class _StepSevenPageState extends State { const SizedBox(width: 12), Expanded( child: Text( - 'Para editar personajes, vuelve al Paso 3 o Paso 5.', + 'To edit characters, go back to Step 3 or Step 5.', style: TextStyle(color: Colors.blue.shade700), ), ), @@ -129,7 +137,7 @@ class _StepSevenPageState extends State { ), const SizedBox(height: 4), Text( - value.isEmpty ? '(No especificado)' : value, + value.isEmpty ? '(Not specified)' : value, style: Theme.of(context).textTheme.bodyMedium, ), ], diff --git a/src/fireflake/lib/step_views/step8.dart b/src/fireflake/lib/step_views/step8.dart index b910978..c32e918 100644 --- a/src/fireflake/lib/step_views/step8.dart +++ b/src/fireflake/lib/step_views/step8.dart @@ -71,7 +71,7 @@ class _StepEightPageState extends State { cubit.saveCurrentProjectToDisk(); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Lista de escenas guardada')), + const SnackBar(content: Text('Scene list saved')), ); } @@ -82,7 +82,8 @@ class _StepEightPageState extends State { builder: (context, state) { final project = state.selectedProject; if (project == null) { - return const Center(child: Text('Selecciona o crea un proyecto primero')); + return const Center( + child: Text('Select or create a project first')); } return SingleChildScrollView( @@ -93,16 +94,18 @@ class _StepEightPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Paso 8: Lista de escenas', - style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold), + 'Step 8: Scene list', + style: Theme.of(context) + .textTheme + .headlineSmall + ?.copyWith(fontWeight: FontWeight.bold), ), const SizedBox(height: 8), Text( - 'Usando el argumento ampliado del Paso 6, escribe una lista de las escenas que faltan para completar la historia.', + 'Using the extended argument from Step 6, list the scenes needed to complete the story.', style: Theme.of(context).textTheme.bodyMedium, ), const SizedBox(height: 24), - ..._sceneControllers.asMap().entries.map((entry) { final index = entry.key; final controller = entry.value; @@ -118,7 +121,9 @@ class _StepEightPageState extends State { width: 32, height: 32, decoration: BoxDecoration( - color: Theme.of(context).primaryColor.withValues(alpha: 0.2), + color: Theme.of(context) + .primaryColor + .withValues(alpha: 0.2), shape: BoxShape.circle, ), child: Center( @@ -136,22 +141,24 @@ class _StepEightPageState extends State { child: TextFormField( controller: controller, decoration: InputDecoration( - labelText: 'Escena ${index + 1}', - hintText: 'Describe brevemente la escena...', + labelText: 'Scene ${index + 1}', + hintText: 'Briefly describe the scene...', border: const OutlineInputBorder(), isDense: true, ), - validator: (value) => (value == null || value.trim().isEmpty) - ? 'Requerido' - : null, + validator: (value) => + (value == null || value.trim().isEmpty) + ? 'Required' + : null, ), ), const SizedBox(width: 8), if (_sceneControllers.length > 1) IconButton( - icon: const Icon(Icons.delete_outline, color: Colors.red, size: 20), + icon: const Icon(Icons.delete_outline, + color: Colors.red, size: 20), onPressed: () => _removeSceneField(index), - tooltip: 'Eliminar', + tooltip: 'Delete', ), ], ), @@ -159,24 +166,21 @@ class _StepEightPageState extends State { ), ); }), - const SizedBox(height: 8), OutlinedButton.icon( onPressed: _addSceneField, icon: const Icon(Icons.add), - label: const Text('Agregar escena'), + label: const Text('Add scene'), ), - const SizedBox(height: 24), SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: _save, icon: const Icon(Icons.save), - label: const Text('Guardar lista de escenas'), + label: const Text('Save scene list'), ), ), - const SizedBox(height: 16), Container( padding: const EdgeInsets.all(16), @@ -187,11 +191,12 @@ class _StepEightPageState extends State { ), child: Row( children: [ - Icon(Icons.lightbulb_outline, color: Colors.amber.shade700), + Icon(Icons.lightbulb_outline, + color: Colors.amber.shade700), const SizedBox(width: 12), Expanded( child: Text( - 'Después de guardar esta lista, ve al Paso 9 para escribir el resumen narrativo de cada escena.', + 'After saving this list, go to Step 9 to write the narrative summary of each scene.', style: TextStyle(color: Colors.amber.shade900), ), ), diff --git a/src/fireflake/lib/step_views/step9.dart b/src/fireflake/lib/step_views/step9.dart index 96d5e1a..5629b84 100644 --- a/src/fireflake/lib/step_views/step9.dart +++ b/src/fireflake/lib/step_views/step9.dart @@ -11,13 +11,14 @@ class StepNinePage extends StatefulWidget { State createState() => _StepNinePageState(); } -class _StepNinePageState extends State with TickerProviderStateMixin { +class _StepNinePageState extends State + with TickerProviderStateMixin { bool _isPanelOpen = false; Scene? _editingScene; int? _editingIndex; late AnimationController _animationController; late Animation _slideAnimation; - + final TextEditingController _idController = TextEditingController(); final TextEditingController _chapterController = TextEditingController(); final TextEditingController _summaryController = TextEditingController(); @@ -54,165 +55,186 @@ class _StepNinePageState extends State with TickerProviderStateMix body: Stack( children: [ Column( - children: [ - Container( - padding: const EdgeInsets.all(16.0), - decoration: BoxDecoration( - color: Theme.of(context).primaryColor.withValues(alpha: 0.1), - border: Border( - bottom: BorderSide( - color: Theme.of(context).dividerColor, - width: 1, - ), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - 'Lista de Escenas', - style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + children: [ + Container( + padding: const EdgeInsets.all(16.0), + decoration: BoxDecoration( + color: Theme.of(context).primaryColor.withValues(alpha: 0.1), + border: Border( + bottom: BorderSide( + color: Theme.of(context).dividerColor, + width: 1, + ), + ), ), - ElevatedButton.icon( - onPressed: _addNewScene, - icon: const Icon(Icons.add), - label: const Text('Agregar Nueva Escena'), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Scene List', + style: + TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + ), + ElevatedButton.icon( + onPressed: _addNewScene, + icon: const Icon(Icons.add), + label: const Text('Add New Scene'), + ), + ], ), - ], - ), - ), - Expanded( - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: SizedBox( - width: MediaQuery.of(context).size.width, - child: BlocBuilder( - builder: (context, state) { - final scenes = state.scenes; - if (scenes.isEmpty) { - return const Center(child: Text('No hay escenas aún. Agrega la primera.')); - } - return SingleChildScrollView( - child: DataTable( - columnSpacing: 20, - dataRowMinHeight: 80, - dataRowMaxHeight: 80, - headingRowColor: WidgetStateProperty.all( - Theme.of(context).primaryColor.withValues(alpha: 0.15), - ), - columns: const [ - DataColumn( - label: Expanded( - child: Text( - 'Escena', - style: TextStyle(fontWeight: FontWeight.bold), - ), - ), - ), - DataColumn( - label: Expanded( - child: Text( - 'Capítulo', - style: TextStyle(fontWeight: FontWeight.bold), - ), - ), - ), - DataColumn( - label: Expanded( - child: Text( - 'Resumen', - style: TextStyle(fontWeight: FontWeight.bold), - ), + ), + Expanded( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: SizedBox( + width: MediaQuery.of(context).size.width, + child: BlocBuilder( + builder: (context, state) { + final scenes = state.scenes; + if (scenes.isEmpty) { + return const Center( + child: Text('No scenes yet. Add the first one.')); + } + return SingleChildScrollView( + child: DataTable( + columnSpacing: 20, + dataRowMinHeight: 80, + dataRowMaxHeight: 80, + headingRowColor: WidgetStateProperty.all( + Theme.of(context) + .primaryColor + .withValues(alpha: 0.15), ), - ), - DataColumn( - label: Expanded( - child: Text( - 'Acciones', - style: TextStyle(fontWeight: FontWeight.bold), + columns: const [ + DataColumn( + label: Expanded( + child: Text( + 'Scene', + style: + TextStyle(fontWeight: FontWeight.bold), + ), + ), ), - ), - ), - ], - rows: scenes.asMap().entries.map((entry) { - final index = entry.key; - final scene = entry.value; - return DataRow( - cells: [ - DataCell( - Container( - padding: const EdgeInsets.symmetric(vertical: 8.0), + DataColumn( + label: Expanded( child: Text( - scene.id, - style: const TextStyle(fontWeight: FontWeight.w500), + 'Chapter', + style: + TextStyle(fontWeight: FontWeight.bold), ), ), ), - DataCell( - Container( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Text(scene.chapter), + DataColumn( + label: Expanded( + child: Text( + 'Summary', + style: + TextStyle(fontWeight: FontWeight.bold), + ), ), ), - DataCell( - Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(vertical: 8.0), + DataColumn( + label: Expanded( child: Text( - scene.summary, - softWrap: true, - maxLines: 4, - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontSize: 13), + 'Actions', + style: + TextStyle(fontWeight: FontWeight.bold), ), ), ), - DataCell( - Container( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon(Icons.keyboard_arrow_up, size: 20), - onPressed: index > 0 - ? () => _moveSceneUp(index) - : null, - tooltip: 'Mover arriba', - ), - IconButton( - icon: const Icon(Icons.keyboard_arrow_down, size: 20), - onPressed: index < scenes.length - 1 - ? () => _moveSceneDown(index) - : null, - tooltip: 'Mover abajo', + ], + rows: scenes.asMap().entries.map((entry) { + final index = entry.key; + final scene = entry.value; + return DataRow( + cells: [ + DataCell( + Container( + padding: const EdgeInsets.symmetric( + vertical: 8.0), + child: Text( + scene.id, + style: const TextStyle( + fontWeight: FontWeight.w500), ), - IconButton( - icon: const Icon(Icons.edit, size: 20), - onPressed: () => _editScene(scene, index), - tooltip: 'Editar escena', + ), + ), + DataCell( + Container( + padding: const EdgeInsets.symmetric( + vertical: 8.0), + child: Text(scene.chapter), + ), + ), + DataCell( + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric( + vertical: 8.0), + child: Text( + scene.summary, + softWrap: true, + maxLines: 4, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 13), ), - IconButton( - icon: const Icon(Icons.delete, size: 20), - onPressed: () => _deleteScene(index), - tooltip: 'Eliminar escena', + ), + ), + DataCell( + Container( + padding: const EdgeInsets.symmetric( + vertical: 8.0), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon( + Icons.keyboard_arrow_up, + size: 20), + onPressed: index > 0 + ? () => _moveSceneUp(index) + : null, + tooltip: 'Move up', + ), + IconButton( + icon: const Icon( + Icons.keyboard_arrow_down, + size: 20), + onPressed: index < scenes.length - 1 + ? () => _moveSceneDown(index) + : null, + tooltip: 'Move down', + ), + IconButton( + icon: const Icon(Icons.edit, + size: 20), + onPressed: () => + _editScene(scene, index), + tooltip: 'Edit scene', + ), + IconButton( + icon: const Icon(Icons.delete, + size: 20), + onPressed: () => + _deleteScene(index), + tooltip: 'Delete scene', + ), + ], ), - ], + ), ), - ), - ), - ], - ); - }).toList(), - ), - ); - }, + ], + ); + }).toList(), + ), + ); + }, + ), + ), ), ), - ), + ], ), - ], - ), if (_isPanelOpen) GestureDetector( onTap: _closeSidePanel, @@ -264,19 +286,19 @@ class _StepNinePageState extends State with TickerProviderStateMix context: context, builder: (BuildContext context) { return AlertDialog( - title: const Text('Confirmar eliminación'), - content: const Text('¿Estás seguro de que quieres eliminar esta escena?'), + title: const Text('Confirm delete'), + content: const Text('Are you sure you want to delete this scene?'), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), - child: const Text('Cancelar'), + child: const Text('Cancel'), ), TextButton( onPressed: () { context.read().removeSceneAt(index); Navigator.of(context).pop(); }, - child: const Text('Eliminar'), + child: const Text('Delete'), ), ], ); @@ -296,8 +318,8 @@ class _StepNinePageState extends State with TickerProviderStateMix final scenesLength = context.read().state.scenes.length; _editingScene = null; _editingIndex = null; - _idController.text = 'Escena ${scenesLength + 1}'; - _chapterController.text = 'Capítulo ${(scenesLength ~/ 2) + 1}'; + _idController.text = 'Scene ${scenesLength + 1}'; + _chapterController.text = 'Chapter ${(scenesLength ~/ 2) + 1}'; _summaryController.text = ''; _openSidePanel(); } @@ -335,7 +357,8 @@ class _StepNinePageState extends State with TickerProviderStateMix _closeSidePanel(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(_editingIndex != null ? 'Escena actualizada' : 'Nueva escena agregada'), + content: + Text(_editingIndex != null ? 'Scene updated' : 'New scene added'), ), ); } @@ -358,7 +381,7 @@ class _StepNinePageState extends State with TickerProviderStateMix mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - _editingScene != null ? 'Editar Escena' : 'Nueva Escena', + _editingScene != null ? 'Edit Scene' : 'New Scene', style: const TextStyle( fontSize: 20, fontWeight: FontWeight.bold, @@ -381,12 +404,12 @@ class _StepNinePageState extends State with TickerProviderStateMix TextFormField( controller: _idController, decoration: const InputDecoration( - labelText: 'Nombre de la escena', + labelText: 'Scene name', border: OutlineInputBorder(), ), validator: (value) { if (value == null || value.isEmpty) { - return 'Este campo es requerido'; + return 'This field is required'; } return null; }, @@ -395,12 +418,12 @@ class _StepNinePageState extends State with TickerProviderStateMix TextFormField( controller: _chapterController, decoration: const InputDecoration( - labelText: 'Capítulo', + labelText: 'Chapter', border: OutlineInputBorder(), ), validator: (value) { if (value == null || value.isEmpty) { - return 'Este campo es requerido'; + return 'This field is required'; } return null; }, @@ -410,7 +433,7 @@ class _StepNinePageState extends State with TickerProviderStateMix child: TextFormField( controller: _summaryController, decoration: const InputDecoration( - labelText: 'Resumen de la escena', + labelText: 'Scene summary', border: OutlineInputBorder(), alignLabelWithHint: true, ), @@ -419,10 +442,10 @@ class _StepNinePageState extends State with TickerProviderStateMix textAlignVertical: TextAlignVertical.top, validator: (value) { if (value == null || value.isEmpty) { - return 'Este campo es requerido'; + return 'This field is required'; } if (value.length < 50) { - return 'El resumen debe tener al menos 50 caracteres'; + return 'Summary must have at least 50 characters'; } return null; }, @@ -434,14 +457,14 @@ class _StepNinePageState extends State with TickerProviderStateMix Expanded( child: OutlinedButton( onPressed: _closeSidePanel, - child: const Text('Cancelar'), + child: const Text('Cancel'), ), ), const SizedBox(width: 16), Expanded( child: ElevatedButton( onPressed: _saveScene, - child: Text(_editingScene != null ? 'Actualizar' : 'Agregar'), + child: Text(_editingScene != null ? 'Update' : 'Add'), ), ), ], From 1b2ba110d286b47cefd2f04117b77a05421934be Mon Sep 17 00:00:00 2001 From: Elena G Blanco Date: Sun, 14 Dec 2025 21:35:47 +0100 Subject: [PATCH 3/8] Save project info --- src/fireflake/lib/data/project_storage.dart | 5 +++-- src/fireflake/lib/info_views/projectinfo.dart | 4 +++- src/fireflake/lib/state/app_cubit.dart | 16 ++++++++++++++-- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/fireflake/lib/data/project_storage.dart b/src/fireflake/lib/data/project_storage.dart index eeb132e..5fb936e 100644 --- a/src/fireflake/lib/data/project_storage.dart +++ b/src/fireflake/lib/data/project_storage.dart @@ -49,9 +49,10 @@ class ProjectStorage { return Project.fromJson(map); } - static Future saveProject(Project project) async { + static Future saveProject(Project project, [String? filename]) async { final dir = await _baseDir(); - final file = File(p.join(dir.path, _fileName(project.title))); + final actualFilename = filename ?? _fileName(project.title); + final file = File(p.join(dir.path, actualFilename)); final jsonStr = json.encode(project.toJson()); await file.writeAsString(jsonStr); } diff --git a/src/fireflake/lib/info_views/projectinfo.dart b/src/fireflake/lib/info_views/projectinfo.dart index 05ea15d..68f737e 100644 --- a/src/fireflake/lib/info_views/projectinfo.dart +++ b/src/fireflake/lib/info_views/projectinfo.dart @@ -25,7 +25,7 @@ class _ProjectInfoPageState extends State { super.dispose(); } - void _saveProject() { + void _saveProject() async { if (_formKey.currentState!.validate()) { final cubit = context.read(); final wordCount = int.parse(_wordCountController.text); @@ -34,6 +34,8 @@ class _ProjectInfoPageState extends State { subtitle: _subtitleController.text.trim(), expectedWordCount: wordCount, ); + await cubit.saveCurrentProjectToDisk(); + if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Project saved'), diff --git a/src/fireflake/lib/state/app_cubit.dart b/src/fireflake/lib/state/app_cubit.dart index cb5e2fb..1eab293 100644 --- a/src/fireflake/lib/state/app_cubit.dart +++ b/src/fireflake/lib/state/app_cubit.dart @@ -10,12 +10,14 @@ class AppState extends Equatable { final Project? selectedProject; final List scenes; final List characters; + final String? currentProjectFilename; const AppState({ this.projects = const [], this.selectedProject, this.scenes = const [], this.characters = const [], + this.currentProjectFilename, }); AppState copyWith({ @@ -23,16 +25,18 @@ class AppState extends Equatable { Project? selectedProject, List? scenes, List? characters, + String? currentProjectFilename, }) => AppState( projects: projects ?? this.projects, selectedProject: selectedProject ?? this.selectedProject, scenes: scenes ?? this.scenes, characters: characters ?? this.characters, + currentProjectFilename: currentProjectFilename ?? this.currentProjectFilename, ); @override - List get props => [projects, selectedProject, scenes, characters]; + List get props => [projects, selectedProject, scenes, characters, currentProjectFilename]; } class AppCubit extends Cubit { @@ -48,13 +52,21 @@ class AppCubit extends Cubit { } void selectProject(Project project) { + final filename = _generateFilename(project.title); emit(state.copyWith( selectedProject: project, scenes: project.scenes, characters: project.characters, + currentProjectFilename: filename, )); } + String _generateFilename(String title) { + final slug = title.toLowerCase().replaceAll(RegExp(r'[^a-z0-9]+'), '-').replaceAll(RegExp(r'-+'), '-').trim(); + final safe = slug.isEmpty ? 'project' : slug; + return '$safe.json'; + } + Future loadAndSelectProject(String title) async { final project = await ProjectStorage.loadProjectByTitle(title); if (project != null) { @@ -250,7 +262,7 @@ class AppCubit extends Cubit { scenes: state.scenes, characters: state.characters, ); - await ProjectStorage.saveProject(merged); + await ProjectStorage.saveProject(merged, state.currentProjectFilename); final projects = state.projects .map((p) => p.title == merged.title ? merged : p) .toList(); From cae58b1a538604dc643fd13f020a9894756a4f09 Mon Sep 17 00:00:00 2001 From: Elena G Blanco Date: Thu, 18 Dec 2025 18:33:44 +0100 Subject: [PATCH 4/8] Open or delete selected project --- src/fireflake/lib/data/project_storage.dart | 8 ++ src/fireflake/lib/projects_page.dart | 133 +++++++++++++++++--- src/fireflake/lib/state/app_cubit.dart | 6 + 3 files changed, 133 insertions(+), 14 deletions(-) diff --git a/src/fireflake/lib/data/project_storage.dart b/src/fireflake/lib/data/project_storage.dart index 5fb936e..36f521b 100644 --- a/src/fireflake/lib/data/project_storage.dart +++ b/src/fireflake/lib/data/project_storage.dart @@ -56,4 +56,12 @@ class ProjectStorage { final jsonStr = json.encode(project.toJson()); await file.writeAsString(jsonStr); } + + static Future deleteProject(String title) async { + final dir = await _baseDir(); + final file = File(p.join(dir.path, _fileName(title))); + if (await file.exists()) { + await file.delete(); + } + } } diff --git a/src/fireflake/lib/projects_page.dart b/src/fireflake/lib/projects_page.dart index a750ec9..a1b73fe 100644 --- a/src/fireflake/lib/projects_page.dart +++ b/src/fireflake/lib/projects_page.dart @@ -13,10 +13,11 @@ class ProjectsPage extends StatefulWidget { } class _ProjectsPageState extends State { + int? _selectedProjectIndex; + @override void initState() { super.initState(); - // Load projects from disk on startup context.read().loadProjectsFromDisk(); } @@ -73,6 +74,38 @@ class _ProjectsPageState extends State { }, child: const Text('New')), ), + BlocBuilder( + builder: (context, state) { + final hasSelection = _selectedProjectIndex != null && + _selectedProjectIndex! < state.projects.length; + return Padding( + padding: const EdgeInsets.all(8.0), + child: TextButton( + style: mainButtonStyle(), + onPressed: hasSelection + ? () => _openSelectedProject(context, state.projects) + : null, + child: const Text('Open'), + ), + ); + }, + ), + BlocBuilder( + builder: (context, state) { + final hasSelection = _selectedProjectIndex != null && + _selectedProjectIndex! < state.projects.length; + return Padding( + padding: const EdgeInsets.all(8.0), + child: TextButton( + style: mainButtonStyle(), + onPressed: hasSelection + ? () => _deleteSelectedProject(context, state.projects) + : null, + child: const Text('Delete'), + ), + ); + }, + ), Padding( padding: const EdgeInsets.all(8.0), child: TextButton( @@ -121,27 +154,99 @@ class _ProjectsPageState extends State { shrinkWrap: true, itemCount: projects.length, itemBuilder: (context, index) { + final isSelected = _selectedProjectIndex == index; return GestureDetector( - onTap: () async { - await context - .read() - .loadAndSelectProject(projects[index].title); - if (!context.mounted) return; - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => ProjectWrapper(), - ), - ); + onTap: () { + setState(() { + _selectedProjectIndex = index; + }); }, - child: Text( - projects[index].title, - style: const TextStyle(color: Colors.white), + child: Container( + margin: const EdgeInsets.symmetric(vertical: 4), + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: isSelected ? Colors.blue.shade700 : Colors.transparent, + borderRadius: BorderRadius.circular(4), + border: Border.all( + color: isSelected ? Colors.blue.shade400 : Colors.transparent, + width: 2, + ), + ), + child: Text( + projects[index].title, + style: TextStyle( + color: isSelected ? Colors.white : Colors.white70, + fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, + ), + ), ), ); }, ); } + Future _openSelectedProject( + BuildContext context, List projects) async { + if (_selectedProjectIndex == null) return; + final project = projects[_selectedProjectIndex!]; + final cubit = context.read(); + final navigator = Navigator.of(context); + + await cubit.loadAndSelectProject(project.title); + if (!mounted) return; + navigator.push( + MaterialPageRoute( + builder: (context) => ProjectWrapper(), + ), + ); + } + + Future _deleteSelectedProject( + BuildContext context, List projects) async { + if (_selectedProjectIndex == null) return; + final project = projects[_selectedProjectIndex!]; + final projectTitle = project.title; + final cubit = context.read(); + final messenger = ScaffoldMessenger.of(context); + + final result = await showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text('Delete project'), + content: Text( + 'Are you sure you want to delete "$projectTitle"? This action cannot be undone.'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text('Cancel'), + ), + ElevatedButton( + onPressed: () => Navigator.of(context).pop(true), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + ), + child: const Text('Delete'), + ), + ], + ); + }, + ); + + if (result != true) return; + if (!mounted) return; + + await cubit.deleteProject(projectTitle); + if (!mounted) return; + + setState(() { + _selectedProjectIndex = null; + }); + messenger.showSnackBar( + SnackBar(content: Text('Project "$projectTitle" deleted')), + ); + } + Future _createNewProject(BuildContext context) async { final titleController = TextEditingController(text: 'New project'); final wordsController = TextEditingController(text: '50000'); diff --git a/src/fireflake/lib/state/app_cubit.dart b/src/fireflake/lib/state/app_cubit.dart index 1eab293..d5b319d 100644 --- a/src/fireflake/lib/state/app_cubit.dart +++ b/src/fireflake/lib/state/app_cubit.dart @@ -268,4 +268,10 @@ class AppCubit extends Cubit { .toList(); emit(state.copyWith(projects: projects, selectedProject: merged)); } + + Future deleteProject(String title) async { + await ProjectStorage.deleteProject(title); + final updated = state.projects.where((p) => p.title != title).toList(); + emit(state.copyWith(projects: updated)); + } } From 42f69f68b62b63b0f6943a07e7ef93bd41769673 Mon Sep 17 00:00:00 2001 From: Elena G Blanco Date: Sat, 20 Dec 2025 13:03:18 +0100 Subject: [PATCH 5/8] Refactor save buttons to use SaveProjectButton widget across all step views --- src/fireflake/lib/step_views/step1.dart | 12 ++--- src/fireflake/lib/step_views/step2.dart | 20 +++----- src/fireflake/lib/step_views/step3.dart | 47 +++++++++---------- src/fireflake/lib/step_views/step4.dart | 10 +--- src/fireflake/lib/step_views/step5.dart | 10 +--- src/fireflake/lib/step_views/step6.dart | 10 +--- src/fireflake/lib/step_views/step8.dart | 10 +--- src/fireflake/lib/step_views/step9.dart | 7 ++- .../lib/widgets/save_project_button.dart | 27 +++++++++++ 9 files changed, 72 insertions(+), 81 deletions(-) create mode 100644 src/fireflake/lib/widgets/save_project_button.dart diff --git a/src/fireflake/lib/step_views/step1.dart b/src/fireflake/lib/step_views/step1.dart index b367d62..162a783 100644 --- a/src/fireflake/lib/step_views/step1.dart +++ b/src/fireflake/lib/step_views/step1.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../state/app_cubit.dart'; +import '../widgets/save_project_button.dart'; class StepOnePage extends StatefulWidget { const StepOnePage({super.key}); @@ -86,7 +87,7 @@ class _StepOnePageState extends State { decoration: const InputDecoration( labelText: 'Your story in one sentence', hintText: - 'Example: A young wizard learns he is the chosen one to save the world from darkness.', + 'Example: A young witch learns they are the chosen one to save the world from darkness.', border: OutlineInputBorder(), ), validator: (value) => @@ -99,14 +100,7 @@ class _StepOnePageState extends State { ), ), const SizedBox(height: 24), - SizedBox( - width: double.infinity, - child: ElevatedButton.icon( - onPressed: _save, - icon: const Icon(Icons.save), - label: const Text('Save'), - ), - ), + SaveProjectButton(onPressed: _save), ], ), ), diff --git a/src/fireflake/lib/step_views/step2.dart b/src/fireflake/lib/step_views/step2.dart index 3898c6a..ea6001c 100644 --- a/src/fireflake/lib/step_views/step2.dart +++ b/src/fireflake/lib/step_views/step2.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../state/app_cubit.dart'; +import '../widgets/save_project_button.dart'; class StepTwoPage extends StatefulWidget { const StepTwoPage({super.key}); @@ -83,7 +84,7 @@ class _StepTwoPageState extends State { const SizedBox(height: 24), _buildCard( context, - title: 'Resumen general', + title: 'Overall summary', child: TextFormField( controller: _summaryController, maxLines: 4, @@ -99,7 +100,7 @@ class _StepTwoPageState extends State { const SizedBox(height: 16), _buildCard( context, - title: 'Acto I', + title: 'Act I', child: TextFormField( controller: _act1Controller, maxLines: 3, @@ -115,7 +116,7 @@ class _StepTwoPageState extends State { const SizedBox(height: 12), _buildCard( context, - title: 'Acto II', + title: 'Act II', child: TextFormField( controller: _act2Controller, maxLines: 3, @@ -131,7 +132,7 @@ class _StepTwoPageState extends State { const SizedBox(height: 12), _buildCard( context, - title: 'Acto III', + title: 'Act III', child: TextFormField( controller: _act3Controller, maxLines: 3, @@ -147,7 +148,7 @@ class _StepTwoPageState extends State { const SizedBox(height: 12), _buildCard( context, - title: 'Final', + title: 'Finalr', child: TextFormField( controller: _finaleController, maxLines: 3, @@ -161,14 +162,7 @@ class _StepTwoPageState extends State { ), ), const SizedBox(height: 24), - SizedBox( - width: double.infinity, - child: ElevatedButton.icon( - onPressed: _save, - icon: const Icon(Icons.save), - label: const Text('Save'), - ), - ), + SaveProjectButton(onPressed: _save), ], ), ), diff --git a/src/fireflake/lib/step_views/step3.dart b/src/fireflake/lib/step_views/step3.dart index bf0cdcb..6cd9634 100644 --- a/src/fireflake/lib/step_views/step3.dart +++ b/src/fireflake/lib/step_views/step3.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../models/character.dart'; import '../state/app_cubit.dart'; +import '../widgets/save_project_button.dart'; class StepThreePage extends StatefulWidget { const StepThreePage({super.key}); @@ -229,31 +230,27 @@ class _StepThreePageState extends State { label: const Text('Add final story element'), ), const SizedBox(height: 30), - Center( - child: ElevatedButton( - onPressed: () { - if (_formKey.currentState!.validate()) { - final name = _historyControllers.isNotEmpty && - _historyControllers.first.text.isNotEmpty - ? _historyControllers.first.text - : 'Personaje'; - final character = Character( - name: name, storygoal: _objectiveController.text); - final cubit = context.read(); - final updated = - List.from(cubit.state.characters) - ..add(character); - cubit.setCharacters(updated); - cubit.saveCurrentProjectToDisk(); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Character info saved'), - ), - ); - } - }, - child: const Text('Save Information'), - ), + SaveProjectButton( + onPressed: () { + if (_formKey.currentState!.validate()) { + final name = _historyControllers.isNotEmpty && + _historyControllers.first.text.isNotEmpty + ? _historyControllers.first.text + : 'Personaje'; + final character = + Character(name: name, storygoal: _objectiveController.text); + final cubit = context.read(); + final updated = List.from(cubit.state.characters) + ..add(character); + cubit.setCharacters(updated); + cubit.saveCurrentProjectToDisk(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Character info saved'), + ), + ); + } + }, ), ], ), diff --git a/src/fireflake/lib/step_views/step4.dart b/src/fireflake/lib/step_views/step4.dart index 3de2b71..974bf6e 100644 --- a/src/fireflake/lib/step_views/step4.dart +++ b/src/fireflake/lib/step_views/step4.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../state/app_cubit.dart'; +import '../widgets/save_project_button.dart'; class StepFourPage extends StatefulWidget { const StepFourPage({super.key}); @@ -130,14 +131,7 @@ class _StepFourPageState extends State { const SizedBox(height: 16), ], const SizedBox(height: 8), - SizedBox( - width: double.infinity, - child: ElevatedButton.icon( - onPressed: _save, - icon: const Icon(Icons.save), - label: const Text('Save expanded paragraphs'), - ), - ), + SaveProjectButton(onPressed: _save), ], ), ), diff --git a/src/fireflake/lib/step_views/step5.dart b/src/fireflake/lib/step_views/step5.dart index 8d101c7..8ec9f08 100644 --- a/src/fireflake/lib/step_views/step5.dart +++ b/src/fireflake/lib/step_views/step5.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../models/character.dart'; import '../state/app_cubit.dart'; +import '../widgets/save_project_button.dart'; class StepFivePage extends StatefulWidget { const StepFivePage({super.key}); @@ -185,14 +186,7 @@ class _StepFivePageState extends State { label: const Text('Add character'), ), const SizedBox(height: 24), - SizedBox( - width: double.infinity, - child: ElevatedButton.icon( - onPressed: _save, - icon: const Icon(Icons.save), - label: const Text('Save characters'), - ), - ), + SaveProjectButton(onPressed: _save), ], ), ), diff --git a/src/fireflake/lib/step_views/step6.dart b/src/fireflake/lib/step_views/step6.dart index d77ec0b..9ff5e3b 100644 --- a/src/fireflake/lib/step_views/step6.dart +++ b/src/fireflake/lib/step_views/step6.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../state/app_cubit.dart'; +import '../widgets/save_project_button.dart'; class StepSixPage extends StatefulWidget { const StepSixPage({super.key}); @@ -118,14 +119,7 @@ class _StepSixPageState extends State { ), ), const SizedBox(height: 24), - SizedBox( - width: double.infinity, - child: ElevatedButton.icon( - onPressed: _save, - icon: const Icon(Icons.save), - label: const Text('Save extended argument'), - ), - ), + SaveProjectButton(onPressed: _save), ], ), ), diff --git a/src/fireflake/lib/step_views/step8.dart b/src/fireflake/lib/step_views/step8.dart index c32e918..486db8f 100644 --- a/src/fireflake/lib/step_views/step8.dart +++ b/src/fireflake/lib/step_views/step8.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../state/app_cubit.dart'; +import '../widgets/save_project_button.dart'; class StepEightPage extends StatefulWidget { const StepEightPage({super.key}); @@ -173,14 +174,7 @@ class _StepEightPageState extends State { label: const Text('Add scene'), ), const SizedBox(height: 24), - SizedBox( - width: double.infinity, - child: ElevatedButton.icon( - onPressed: _save, - icon: const Icon(Icons.save), - label: const Text('Save scene list'), - ), - ), + SaveProjectButton(onPressed: _save), const SizedBox(height: 16), Container( padding: const EdgeInsets.all(16), diff --git a/src/fireflake/lib/step_views/step9.dart b/src/fireflake/lib/step_views/step9.dart index 5629b84..d12167a 100644 --- a/src/fireflake/lib/step_views/step9.dart +++ b/src/fireflake/lib/step_views/step9.dart @@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import '../models/scene.dart'; import '../state/app_cubit.dart'; +import '../widgets/save_project_button.dart'; class StepNinePage extends StatefulWidget { const StepNinePage({super.key}); @@ -462,9 +463,11 @@ class _StepNinePageState extends State ), const SizedBox(width: 16), Expanded( - child: ElevatedButton( + child: SaveProjectButton( + label: _editingScene != null + ? 'Update Scene' + : 'Save Scene', onPressed: _saveScene, - child: Text(_editingScene != null ? 'Update' : 'Add'), ), ), ], diff --git a/src/fireflake/lib/widgets/save_project_button.dart b/src/fireflake/lib/widgets/save_project_button.dart new file mode 100644 index 0000000..c9d43ee --- /dev/null +++ b/src/fireflake/lib/widgets/save_project_button.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; + +class SaveProjectButton extends StatelessWidget { + const SaveProjectButton({ + super.key, + required this.onPressed, + this.label = 'Save Project', + }); + + final VoidCallback onPressed; + final String label; + + @override + Widget build(BuildContext context) { + return SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: onPressed, + icon: const Icon(Icons.save_outlined), + label: Text(label), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 14), + ), + ), + ); + } +} From ea3af6b3e1a4bed634e77d2f978116946260f892 Mon Sep 17 00:00:00 2001 From: Elena G Blanco Date: Sun, 21 Dec 2025 17:47:39 +0100 Subject: [PATCH 6/8] Refactor UI for responsiveness and improve code readability --- src/fireflake/lib/author_info_page.dart | 122 ++--- src/fireflake/lib/data/project_storage.dart | 9 +- src/fireflake/lib/info_views/projectinfo.dart | 466 +++++++++--------- src/fireflake/lib/main.dart | 12 +- src/fireflake/lib/models/project.dart | 1 - src/fireflake/lib/projects_page.dart | 16 +- src/fireflake/lib/projectwrapper.dart | 24 +- src/fireflake/lib/state/app_cubit.dart | 12 +- src/fireflake/lib/step_views/step1.dart | 109 ++-- src/fireflake/lib/step_views/step2.dart | 196 ++++---- src/fireflake/lib/step_views/step3.dart | 20 +- src/fireflake/lib/step_views/step4.dart | 93 ++-- src/fireflake/lib/step_views/step5.dart | 183 +++---- src/fireflake/lib/step_views/step6.dart | 127 ++--- src/fireflake/lib/step_views/step7.dart | 160 +++--- src/fireflake/lib/step_views/step8.dart | 205 ++++---- src/fireflake/lib/step_views/step9.dart | 27 +- .../lib/utils/responsive_helper.dart | 64 +++ 18 files changed, 985 insertions(+), 861 deletions(-) create mode 100644 src/fireflake/lib/utils/responsive_helper.dart diff --git a/src/fireflake/lib/author_info_page.dart b/src/fireflake/lib/author_info_page.dart index 8691b04..fe554ee 100644 --- a/src/fireflake/lib/author_info_page.dart +++ b/src/fireflake/lib/author_info_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'state/author_cubit.dart'; import 'models/author_settings.dart'; +import 'utils/responsive_helper.dart'; class AuthorInfoPage extends StatefulWidget { const AuthorInfoPage({super.key}); @@ -51,7 +52,6 @@ class _AuthorInfoPageState extends State { appBar: AppBar(title: const Text('Author Information')), body: BlocBuilder( builder: (context, settings) { - // Actualizar controladores cuando cambie el estado if (_nameController.text != settings.name) { _nameController.text = settings.name; } @@ -62,70 +62,72 @@ class _AuthorInfoPageState extends State { _emailController.text = settings.email; } - return Padding( - padding: const EdgeInsets.all(16.0), - child: Form( - key: _formKey, - child: ListView( - children: [ - const Text( - 'This configuration is stored globally and will be used across all projects.', - style: TextStyle( - fontStyle: FontStyle.italic, - color: Colors.grey, + return ResponsiveWrapper( + child: Padding( + padding: ResponsiveHelper.getContentPadding(context), + child: Form( + key: _formKey, + child: ListView( + children: [ + const Text( + 'This configuration is stored globally and will be used across all projects.', + style: TextStyle( + fontStyle: FontStyle.italic, + color: Colors.grey, + ), ), - ), - const SizedBox(height: 16), - TextFormField( - controller: _nameController, - decoration: const InputDecoration( - labelText: 'Name', - border: OutlineInputBorder(), + const SizedBox(height: 16), + TextFormField( + controller: _nameController, + decoration: const InputDecoration( + labelText: 'Name', + border: OutlineInputBorder(), + ), + validator: (value) => value == null || value.isEmpty + ? 'Enter your name' + : null, ), - validator: (value) => value == null || value.isEmpty - ? 'Enter your name' - : null, - ), - const SizedBox(height: 16), - TextFormField( - controller: _bioController, - decoration: const InputDecoration( - labelText: 'Bio', - border: OutlineInputBorder(), - alignLabelWithHint: true, + const SizedBox(height: 16), + TextFormField( + controller: _bioController, + decoration: const InputDecoration( + labelText: 'Bio', + border: OutlineInputBorder(), + alignLabelWithHint: true, + ), + maxLines: 5, ), - maxLines: 5, - ), - const SizedBox(height: 16), - TextFormField( - controller: _emailController, - decoration: const InputDecoration( - labelText: 'Email', - border: OutlineInputBorder(), + const SizedBox(height: 16), + TextFormField( + controller: _emailController, + decoration: const InputDecoration( + labelText: 'Email', + border: OutlineInputBorder(), + ), + keyboardType: TextInputType.emailAddress, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Enter your email'; + } + if (!value.contains('@')) { + return 'Enter a valid email'; + } + return null; + }, ), - keyboardType: TextInputType.emailAddress, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Enter your email'; - } - if (!value.contains('@')) { - return 'Enter a valid email'; - } - return null; - }, - ), - const SizedBox(height: 24), - ElevatedButton( - onPressed: _submit, - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.all(16), + const SizedBox(height: 24), + ElevatedButton( + onPressed: _submit, + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.all(16), + ), + child: const Text( + 'Save', + style: TextStyle(fontSize: 16), + ), ), - child: const Text( - 'Save', - style: TextStyle(fontSize: 16), - ), - ), - ], + ], + ), ), ), ); diff --git a/src/fireflake/lib/data/project_storage.dart b/src/fireflake/lib/data/project_storage.dart index 36f521b..8fd2ea6 100644 --- a/src/fireflake/lib/data/project_storage.dart +++ b/src/fireflake/lib/data/project_storage.dart @@ -19,14 +19,19 @@ class ProjectStorage { } static String _fileName(String title) { - final slug = title.toLowerCase().replaceAll(RegExp(r'[^a-z0-9]+'), '-').replaceAll(RegExp(r'-+'), '-').trim(); + final slug = title + .toLowerCase() + .replaceAll(RegExp(r'[^a-z0-9]+'), '-') + .replaceAll(RegExp(r'-+'), '-') + .trim(); final safe = slug.isEmpty ? 'project' : slug; return '$safe.json'; } static Future> loadProjects() async { final dir = await _baseDir(); - final files = dir.listSync().whereType().where((f) => f.path.endsWith('.json')); + final files = + dir.listSync().whereType().where((f) => f.path.endsWith('.json')); final projects = []; for (final file in files) { try { diff --git a/src/fireflake/lib/info_views/projectinfo.dart b/src/fireflake/lib/info_views/projectinfo.dart index 68f737e..e4d5d99 100644 --- a/src/fireflake/lib/info_views/projectinfo.dart +++ b/src/fireflake/lib/info_views/projectinfo.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../state/app_cubit.dart'; +import '../utils/responsive_helper.dart'; class ProjectInfoPage extends StatefulWidget { const ProjectInfoPage({super.key}); @@ -67,259 +68,264 @@ class _ProjectInfoPageState extends State { @override Widget build(BuildContext context) { return Scaffold( - body: Padding( - padding: const EdgeInsets.all(24.0), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - margin: const EdgeInsets.only(bottom: 32.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Project Information', - style: - Theme.of(context).textTheme.headlineMedium?.copyWith( - fontWeight: FontWeight.bold, - color: Theme.of(context).primaryColor, - ), - ), - const SizedBox(height: 8), - Text( - 'Fill in the basic information for your writing project', - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: Colors.grey[600], - ), - ), - ], - ), - ), - Expanded( - child: SingleChildScrollView( + body: ResponsiveWrapper( + child: Padding( + padding: ResponsiveHelper.getContentPadding(context), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + margin: const EdgeInsets.only(bottom: 32.0), child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Card( - elevation: 2, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Project title', - style: Theme.of(context) - .textTheme - .titleMedium - ?.copyWith( - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: 12), - TextFormField( - controller: _titleController, - decoration: const InputDecoration( - labelText: 'Title', - hintText: 'E.g. The Shadow Kingdom', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.title), + Text( + 'Project Information', + style: Theme.of(context) + .textTheme + .headlineMedium + ?.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).primaryColor, + ), + ), + const SizedBox(height: 8), + Text( + 'Fill in the basic information for your writing project', + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Colors.grey[600], + ), + ), + ], + ), + ), + Expanded( + child: SingleChildScrollView( + child: Column( + children: [ + Card( + elevation: 2, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Project title', + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith( + fontWeight: FontWeight.w600, + ), ), - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'Title is required'; - } - if (value.trim().length < 3) { - return 'Title must be at least 3 characters'; - } - return null; - }, - ), - ], + const SizedBox(height: 12), + TextFormField( + controller: _titleController, + decoration: const InputDecoration( + labelText: 'Title', + hintText: 'E.g. The Shadow Kingdom', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.title), + ), + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'Title is required'; + } + if (value.trim().length < 3) { + return 'Title must be at least 3 characters'; + } + return null; + }, + ), + ], + ), ), ), - ), - const SizedBox(height: 20), - Card( - elevation: 2, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Subtitle', - style: Theme.of(context) - .textTheme - .titleMedium - ?.copyWith( - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: 8), - Text( - 'Short description or tagline of the project', - style: Theme.of(context) - .textTheme - .bodySmall - ?.copyWith( - color: Colors.grey[600], - ), - ), - const SizedBox(height: 12), - TextFormField( - controller: _subtitleController, - decoration: const InputDecoration( - labelText: 'Subtitle', - hintText: 'E.g. An epic fantasy adventure', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.short_text), + const SizedBox(height: 20), + Card( + elevation: 2, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Subtitle', + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith( + fontWeight: FontWeight.w600, + ), ), - maxLines: 2, - validator: (value) { - if (value != null && - value.isNotEmpty && - value.trim().length < 10) { - return 'Subtitle must have at least 10 characters or be empty'; - } - return null; - }, - ), - ], + const SizedBox(height: 8), + Text( + 'Short description or tagline of the project', + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith( + color: Colors.grey[600], + ), + ), + const SizedBox(height: 12), + TextFormField( + controller: _subtitleController, + decoration: const InputDecoration( + labelText: 'Subtitle', + hintText: 'E.g. An epic fantasy adventure', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.short_text), + ), + maxLines: 2, + validator: (value) { + if (value != null && + value.isNotEmpty && + value.trim().length < 10) { + return 'Subtitle must have at least 10 characters or be empty'; + } + return null; + }, + ), + ], + ), ), ), - ), - const SizedBox(height: 20), - Card( - elevation: 2, - child: Padding( + const SizedBox(height: 20), + Card( + elevation: 2, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Word count', + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 8), + Text( + 'Approximate total to complete the project', + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith( + color: Colors.grey[600], + ), + ), + const SizedBox(height: 12), + TextFormField( + controller: _wordCountController, + decoration: const InputDecoration( + labelText: 'Word count', + hintText: 'E.g. 80000', + border: OutlineInputBorder(), + prefixIcon: + Icon(Icons.format_list_numbered), + suffixText: 'words', + ), + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + ], + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'Word count is required'; + } + final int? wordCount = int.tryParse(value); + if (wordCount == null) { + return 'Enter a valid number'; + } + if (wordCount < 1000) { + return 'Word count must be at least 1,000'; + } + if (wordCount > 1000000) { + return 'Word count cannot exceed 1,000,000'; + } + return null; + }, + ), + ], + ), + ), + ), + const SizedBox(height: 32), + Container( padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.blue.shade200), + ), + child: Row( children: [ - Text( - 'Word count', - style: Theme.of(context) - .textTheme - .titleMedium - ?.copyWith( - fontWeight: FontWeight.w600, + Icon(Icons.info_outline, + color: Colors.blue.shade700), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Word count reference:', + style: TextStyle( + fontWeight: FontWeight.w600, + color: Colors.blue.shade700, + ), ), - ), - const SizedBox(height: 8), - Text( - 'Approximate total to complete the project', - style: Theme.of(context) - .textTheme - .bodySmall - ?.copyWith( - color: Colors.grey[600], + const SizedBox(height: 4), + Text( + '• Novella: 50,000 - 80,000 words\n' + '• Standard novel: 80,000 - 100,000 words\n' + '• Epic novel: 100,000+ words', + style: TextStyle( + fontSize: 13, + color: Colors.blue.shade600, + ), ), - ), - const SizedBox(height: 12), - TextFormField( - controller: _wordCountController, - decoration: const InputDecoration( - labelText: 'Word count', - hintText: 'E.g. 80000', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.format_list_numbered), - suffixText: 'words', + ], ), - keyboardType: TextInputType.number, - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly, - ], - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'Word count is required'; - } - final int? wordCount = int.tryParse(value); - if (wordCount == null) { - return 'Enter a valid number'; - } - if (wordCount < 1000) { - return 'Word count must be at least 1,000'; - } - if (wordCount > 1000000) { - return 'Word count cannot exceed 1,000,000'; - } - return null; - }, ), ], ), ), - ), - const SizedBox(height: 32), - Container( - padding: const EdgeInsets.all(16.0), - decoration: BoxDecoration( - color: Colors.blue.shade50, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.blue.shade200), + ], + ), + ), + ), + Container( + padding: const EdgeInsets.only(top: 24), + child: Row( + children: [ + Expanded( + child: OutlinedButton.icon( + onPressed: _clearForm, + icon: const Icon(Icons.clear), + label: const Text('Clear'), ), - child: Row( - children: [ - Icon(Icons.info_outline, - color: Colors.blue.shade700), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Word count reference:', - style: TextStyle( - fontWeight: FontWeight.w600, - color: Colors.blue.shade700, - ), - ), - const SizedBox(height: 4), - Text( - '• Novella: 50,000 - 80,000 words\n' - '• Standard novel: 80,000 - 100,000 words\n' - '• Epic novel: 100,000+ words', - style: TextStyle( - fontSize: 13, - color: Colors.blue.shade600, - ), - ), - ], - ), - ), - ], + ), + const SizedBox(width: 16), + Expanded( + flex: 2, + child: ElevatedButton.icon( + onPressed: _saveProject, + icon: const Icon(Icons.save), + label: const Text('Save Project'), ), ), ], ), ), - ), - Container( - padding: const EdgeInsets.only(top: 24), - child: Row( - children: [ - Expanded( - child: OutlinedButton.icon( - onPressed: _clearForm, - icon: const Icon(Icons.clear), - label: const Text('Clear'), - ), - ), - const SizedBox(width: 16), - Expanded( - flex: 2, - child: ElevatedButton.icon( - onPressed: _saveProject, - icon: const Icon(Icons.save), - label: const Text('Save Project'), - ), - ), - ], - ), - ), - ], + ], + ), ), ), ), diff --git a/src/fireflake/lib/main.dart b/src/fireflake/lib/main.dart index 6c6ee92..e160c0e 100644 --- a/src/fireflake/lib/main.dart +++ b/src/fireflake/lib/main.dart @@ -19,10 +19,10 @@ class MyApp extends StatelessWidget { BlocProvider(create: (_) => AuthorCubit()), ], child: MaterialApp( - debugShowCheckedModeBanner: false, - title: 'Fireflake', - theme: ThemeData(brightness: Brightness.light), - home: HomePage(title: 'Fireflake'), + debugShowCheckedModeBanner: false, + title: 'Fireflake', + theme: ThemeData(brightness: Brightness.light), + home: HomePage(title: 'Fireflake'), ), ); } @@ -41,10 +41,6 @@ class _HomePageState extends State { @override Widget build(BuildContext context) { return const Scaffold( - // appBar: AppBar( - // backgroundColor: Theme.of(context).colorScheme.inversePrimary, - // title: Text(widget.title), - // ), body: ProjectsPage(), ); } diff --git a/src/fireflake/lib/models/project.dart b/src/fireflake/lib/models/project.dart index 6ba8722..ec8ec1a 100644 --- a/src/fireflake/lib/models/project.dart +++ b/src/fireflake/lib/models/project.dart @@ -2,7 +2,6 @@ import 'scene.dart'; import 'character.dart'; class Project { - // TODO: consider making these fields final with copyWith returning new instance; current mutable for simplicity. String title; String subtitle; int expectedWordCount; diff --git a/src/fireflake/lib/projects_page.dart b/src/fireflake/lib/projects_page.dart index a1b73fe..be4020f 100644 --- a/src/fireflake/lib/projects_page.dart +++ b/src/fireflake/lib/projects_page.dart @@ -4,6 +4,7 @@ import 'package:fireflake/projectwrapper.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'state/app_cubit.dart'; +import 'utils/responsive_helper.dart'; class ProjectsPage extends StatefulWidget { const ProjectsPage({super.key}); @@ -24,15 +25,16 @@ class _ProjectsPageState extends State { @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.all(30), + padding: ResponsiveHelper.getContentPadding(context), child: Column( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ Container( - padding: const EdgeInsets.all(20), - width: 220, - height: 360, + padding: + EdgeInsets.all(ResponsiveHelper.isMobile(context) ? 16 : 20), + width: ResponsiveHelper.isMobile(context) ? 200 : 220, + height: ResponsiveHelper.isMobile(context) ? 320 : 360, color: Colors.grey.shade800, child: BlocBuilder( builder: (context, state) { @@ -83,7 +85,8 @@ class _ProjectsPageState extends State { child: TextButton( style: mainButtonStyle(), onPressed: hasSelection - ? () => _openSelectedProject(context, state.projects) + ? () => + _openSelectedProject(context, state.projects) : null, child: const Text('Open'), ), @@ -99,7 +102,8 @@ class _ProjectsPageState extends State { child: TextButton( style: mainButtonStyle(), onPressed: hasSelection - ? () => _deleteSelectedProject(context, state.projects) + ? () => + _deleteSelectedProject(context, state.projects) : null, child: const Text('Delete'), ), diff --git a/src/fireflake/lib/projectwrapper.dart b/src/fireflake/lib/projectwrapper.dart index c4cb22e..d2dd26e 100644 --- a/src/fireflake/lib/projectwrapper.dart +++ b/src/fireflake/lib/projectwrapper.dart @@ -1,4 +1,3 @@ - import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'info_views/projectinfo.dart'; @@ -23,19 +22,18 @@ class ProjectWrapper extends StatefulWidget { class _ProjectWrapperState extends State { @override Widget build(BuildContext context) { - const steps = [ - ProjectInfoPage(), - StepOnePage(), - StepTwoPage(), - StepThreePage(), - StepFourPage(), - StepFivePage(), - StepSixPage(), - StepSevenPage(), - StepEightPage(), - StepNinePage() - ]; + ProjectInfoPage(), + StepOnePage(), + StepTwoPage(), + StepThreePage(), + StepFourPage(), + StepFivePage(), + StepSixPage(), + StepSevenPage(), + StepEightPage(), + StepNinePage() + ]; return DefaultTabController( length: steps.length, diff --git a/src/fireflake/lib/state/app_cubit.dart b/src/fireflake/lib/state/app_cubit.dart index d5b319d..d958f82 100644 --- a/src/fireflake/lib/state/app_cubit.dart +++ b/src/fireflake/lib/state/app_cubit.dart @@ -32,11 +32,13 @@ class AppState extends Equatable { selectedProject: selectedProject ?? this.selectedProject, scenes: scenes ?? this.scenes, characters: characters ?? this.characters, - currentProjectFilename: currentProjectFilename ?? this.currentProjectFilename, + currentProjectFilename: + currentProjectFilename ?? this.currentProjectFilename, ); @override - List get props => [projects, selectedProject, scenes, characters, currentProjectFilename]; + List get props => + [projects, selectedProject, scenes, characters, currentProjectFilename]; } class AppCubit extends Cubit { @@ -62,7 +64,11 @@ class AppCubit extends Cubit { } String _generateFilename(String title) { - final slug = title.toLowerCase().replaceAll(RegExp(r'[^a-z0-9]+'), '-').replaceAll(RegExp(r'-+'), '-').trim(); + final slug = title + .toLowerCase() + .replaceAll(RegExp(r'[^a-z0-9]+'), '-') + .replaceAll(RegExp(r'-+'), '-') + .trim(); final safe = slug.isEmpty ? 'project' : slug; return '$safe.json'; } diff --git a/src/fireflake/lib/step_views/step1.dart b/src/fireflake/lib/step_views/step1.dart index 162a783..8693285 100644 --- a/src/fireflake/lib/step_views/step1.dart +++ b/src/fireflake/lib/step_views/step1.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../state/app_cubit.dart'; import '../widgets/save_project_button.dart'; +import '../utils/responsive_helper.dart'; class StepOnePage extends StatefulWidget { const StepOnePage({super.key}); @@ -46,62 +47,64 @@ class _StepOnePageState extends State { @override Widget build(BuildContext context) { return Scaffold( - body: SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Step 1: One-sentence summary', - style: Theme.of(context) - .textTheme - .headlineSmall - ?.copyWith(fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8), - Text( - 'Write your story in a single sentence. This is the core of your project.', - style: Theme.of(context).textTheme.bodyMedium, - ), - const SizedBox(height: 24), - Card( - elevation: 1, - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'One-sentence summary', - style: Theme.of(context) - .textTheme - .titleMedium - ?.copyWith(fontWeight: FontWeight.w600), - ), - const SizedBox(height: 12), - TextFormField( - controller: _summaryController, - maxLines: 3, - decoration: const InputDecoration( - labelText: 'Your story in one sentence', - hintText: - 'Example: A young witch learns they are the chosen one to save the world from darkness.', - border: OutlineInputBorder(), + body: ResponsiveWrapper( + child: SingleChildScrollView( + padding: ResponsiveHelper.getContentPadding(context), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Step 1: One-sentence summary', + style: Theme.of(context) + .textTheme + .headlineSmall + ?.copyWith(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + Text( + 'Write your story in a single sentence. This is the core of your project.', + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 24), + Card( + elevation: 1, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'One-sentence summary', + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith(fontWeight: FontWeight.w600), + ), + const SizedBox(height: 12), + TextFormField( + controller: _summaryController, + maxLines: 3, + decoration: const InputDecoration( + labelText: 'Your story in one sentence', + hintText: + 'Example: A young witch learns they are the chosen one to save the world from darkness.', + border: OutlineInputBorder(), + ), + validator: (value) => + (value == null || value.trim().isEmpty) + ? 'Summary is required' + : null, ), - validator: (value) => - (value == null || value.trim().isEmpty) - ? 'Summary is required' - : null, - ), - ], + ], + ), ), ), - ), - const SizedBox(height: 24), - SaveProjectButton(onPressed: _save), - ], + const SizedBox(height: 24), + SaveProjectButton(onPressed: _save), + ], + ), ), ), ), diff --git a/src/fireflake/lib/step_views/step2.dart b/src/fireflake/lib/step_views/step2.dart index ea6001c..c24717b 100644 --- a/src/fireflake/lib/step_views/step2.dart +++ b/src/fireflake/lib/step_views/step2.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../state/app_cubit.dart'; import '../widgets/save_project_button.dart'; +import '../utils/responsive_helper.dart'; class StepTwoPage extends StatefulWidget { const StepTwoPage({super.key}); @@ -62,108 +63,115 @@ class _StepTwoPageState extends State { @override Widget build(BuildContext context) { return Scaffold( - body: SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Expand the summary', - style: Theme.of(context) - .textTheme - .headlineSmall - ?.copyWith(fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8), - Text( - 'Break the summary into main acts and finale to structure the story.', - style: Theme.of(context).textTheme.bodyMedium, - ), - const SizedBox(height: 24), - _buildCard( - context, - title: 'Overall summary', - child: TextFormField( - controller: _summaryController, - maxLines: 4, - decoration: const InputDecoration( - labelText: 'Overall project summary', - border: OutlineInputBorder(), + body: ResponsiveWrapper( + child: SingleChildScrollView( + padding: ResponsiveHelper.getContentPadding(context), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Expand the summary', + style: Theme.of(context) + .textTheme + .headlineSmall + ?.copyWith(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + Text( + 'Break the summary into main acts and finale to structure the story.', + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 24), + _buildCard( + context, + title: 'Overall summary', + child: TextFormField( + controller: _summaryController, + maxLines: 4, + decoration: const InputDecoration( + labelText: 'Overall project summary', + border: OutlineInputBorder(), + ), + validator: (value) => + (value == null || value.trim().isEmpty) + ? 'Summary is required' + : null, ), - validator: (value) => (value == null || value.trim().isEmpty) - ? 'Summary is required' - : null, ), - ), - const SizedBox(height: 16), - _buildCard( - context, - title: 'Act I', - child: TextFormField( - controller: _act1Controller, - maxLines: 3, - decoration: const InputDecoration( - labelText: 'Setup / Act I', - border: OutlineInputBorder(), + const SizedBox(height: 16), + _buildCard( + context, + title: 'Act I', + child: TextFormField( + controller: _act1Controller, + maxLines: 3, + decoration: const InputDecoration( + labelText: 'Setup / Act I', + border: OutlineInputBorder(), + ), + validator: (value) => + (value == null || value.trim().isEmpty) + ? 'Describe Act I' + : null, ), - validator: (value) => (value == null || value.trim().isEmpty) - ? 'Describe Act I' - : null, ), - ), - const SizedBox(height: 12), - _buildCard( - context, - title: 'Act II', - child: TextFormField( - controller: _act2Controller, - maxLines: 3, - decoration: const InputDecoration( - labelText: 'Confrontation / Act II', - border: OutlineInputBorder(), + const SizedBox(height: 12), + _buildCard( + context, + title: 'Act II', + child: TextFormField( + controller: _act2Controller, + maxLines: 3, + decoration: const InputDecoration( + labelText: 'Confrontation / Act II', + border: OutlineInputBorder(), + ), + validator: (value) => + (value == null || value.trim().isEmpty) + ? 'Describe Act II' + : null, ), - validator: (value) => (value == null || value.trim().isEmpty) - ? 'Describe Act II' - : null, ), - ), - const SizedBox(height: 12), - _buildCard( - context, - title: 'Act III', - child: TextFormField( - controller: _act3Controller, - maxLines: 3, - decoration: const InputDecoration( - labelText: 'Climax / Act III', - border: OutlineInputBorder(), + const SizedBox(height: 12), + _buildCard( + context, + title: 'Act III', + child: TextFormField( + controller: _act3Controller, + maxLines: 3, + decoration: const InputDecoration( + labelText: 'Climax / Act III', + border: OutlineInputBorder(), + ), + validator: (value) => + (value == null || value.trim().isEmpty) + ? 'Describe Act III' + : null, ), - validator: (value) => (value == null || value.trim().isEmpty) - ? 'Describe Act III' - : null, ), - ), - const SizedBox(height: 12), - _buildCard( - context, - title: 'Finalr', - child: TextFormField( - controller: _finaleController, - maxLines: 3, - decoration: const InputDecoration( - labelText: 'Resolution / Finale', - border: OutlineInputBorder(), + const SizedBox(height: 12), + _buildCard( + context, + title: 'Finalr', + child: TextFormField( + controller: _finaleController, + maxLines: 3, + decoration: const InputDecoration( + labelText: 'Resolution / Finale', + border: OutlineInputBorder(), + ), + validator: (value) => + (value == null || value.trim().isEmpty) + ? 'Describe the ending' + : null, ), - validator: (value) => (value == null || value.trim().isEmpty) - ? 'Describe the ending' - : null, ), - ), - const SizedBox(height: 24), - SaveProjectButton(onPressed: _save), - ], + const SizedBox(height: 24), + SaveProjectButton(onPressed: _save), + ], + ), ), ), ), @@ -173,9 +181,9 @@ class _StepTwoPageState extends State { Widget _buildCard(BuildContext context, {required String title, required Widget child}) { return Card( - elevation: 1, + elevation: ResponsiveHelper.getCardElevation(context), child: Padding( - padding: const EdgeInsets.all(16), + padding: EdgeInsets.all(ResponsiveHelper.isMobile(context) ? 12 : 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/src/fireflake/lib/step_views/step3.dart b/src/fireflake/lib/step_views/step3.dart index 6cd9634..169fb56 100644 --- a/src/fireflake/lib/step_views/step3.dart +++ b/src/fireflake/lib/step_views/step3.dart @@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import '../models/character.dart'; import '../state/app_cubit.dart'; import '../widgets/save_project_button.dart'; +import '../utils/responsive_helper.dart'; class StepThreePage extends StatefulWidget { const StepThreePage({super.key}); @@ -75,11 +76,11 @@ class _StepThreePageState extends State { @override Widget build(BuildContext context) { return Scaffold( - body: Padding( - padding: const EdgeInsets.all(16.0), - child: Form( - key: _formKey, - child: SingleChildScrollView( + body: ResponsiveWrapper( + child: SingleChildScrollView( + padding: ResponsiveHelper.getContentPadding(context), + child: Form( + key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -237,11 +238,12 @@ class _StepThreePageState extends State { _historyControllers.first.text.isNotEmpty ? _historyControllers.first.text : 'Personaje'; - final character = - Character(name: name, storygoal: _objectiveController.text); + final character = Character( + name: name, storygoal: _objectiveController.text); final cubit = context.read(); - final updated = List.from(cubit.state.characters) - ..add(character); + final updated = + List.from(cubit.state.characters) + ..add(character); cubit.setCharacters(updated); cubit.saveCurrentProjectToDisk(); ScaffoldMessenger.of(context).showSnackBar( diff --git a/src/fireflake/lib/step_views/step4.dart b/src/fireflake/lib/step_views/step4.dart index 974bf6e..a0c6cd0 100644 --- a/src/fireflake/lib/step_views/step4.dart +++ b/src/fireflake/lib/step_views/step4.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../state/app_cubit.dart'; import '../widgets/save_project_button.dart'; +import '../utils/responsive_helper.dart'; class StepFourPage extends StatefulWidget { const StepFourPage({super.key}); @@ -90,49 +91,51 @@ class _StepFourPageState extends State { }); } - return SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Step 4: Expand into paragraphs', - style: Theme.of(context) - .textTheme - .headlineSmall - ?.copyWith(fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8), - Text( - 'Turn each sentence from Step 2 into a full paragraph.', - style: Theme.of(context).textTheme.bodyMedium, - ), - const SizedBox(height: 24), - if (project.act1.isNotEmpty) ...[ - _buildExpandedSection( - context, 'Acto I - Expandido', 'act1'), - const SizedBox(height: 16), + return ResponsiveWrapper( + child: SingleChildScrollView( + padding: ResponsiveHelper.getContentPadding(context), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Step 4: Expand into paragraphs', + style: Theme.of(context) + .textTheme + .headlineSmall + ?.copyWith(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + Text( + 'Turn each sentence from Step 2 into a full paragraph.', + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 24), + if (project.act1.isNotEmpty) ...[ + _buildExpandedSection( + context, 'Acto I - Expandido', 'act1'), + const SizedBox(height: 16), + ], + if (project.act2.isNotEmpty) ...[ + _buildExpandedSection( + context, 'Acto II - Expandido', 'act2'), + const SizedBox(height: 16), + ], + if (project.act3.isNotEmpty) ...[ + _buildExpandedSection( + context, 'Acto III - Expandido', 'act3'), + const SizedBox(height: 16), + ], + if (project.finale.isNotEmpty) ...[ + _buildExpandedSection( + context, 'Final - Expandido', 'finale'), + const SizedBox(height: 16), + ], + const SizedBox(height: 8), + SaveProjectButton(onPressed: _save), ], - if (project.act2.isNotEmpty) ...[ - _buildExpandedSection( - context, 'Acto II - Expandido', 'act2'), - const SizedBox(height: 16), - ], - if (project.act3.isNotEmpty) ...[ - _buildExpandedSection( - context, 'Acto III - Expandido', 'act3'), - const SizedBox(height: 16), - ], - if (project.finale.isNotEmpty) ...[ - _buildExpandedSection( - context, 'Final - Expandido', 'finale'), - const SizedBox(height: 16), - ], - const SizedBox(height: 8), - SaveProjectButton(onPressed: _save), - ], + ), ), ), ); @@ -143,9 +146,9 @@ class _StepFourPageState extends State { Widget _buildExpandedSection(BuildContext context, String title, String key) { return Card( - elevation: 1, + elevation: ResponsiveHelper.getCardElevation(context), child: Padding( - padding: const EdgeInsets.all(16), + padding: EdgeInsets.all(ResponsiveHelper.isMobile(context) ? 12 : 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -159,7 +162,7 @@ class _StepFourPageState extends State { const SizedBox(height: 12), TextFormField( controller: _controllers[key], - maxLines: 6, + maxLines: ResponsiveHelper.isMobile(context) ? 4 : 6, decoration: InputDecoration( labelText: 'Full paragraph for $title', hintText: diff --git a/src/fireflake/lib/step_views/step5.dart b/src/fireflake/lib/step_views/step5.dart index 8ec9f08..09975e6 100644 --- a/src/fireflake/lib/step_views/step5.dart +++ b/src/fireflake/lib/step_views/step5.dart @@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import '../models/character.dart'; import '../state/app_cubit.dart'; import '../widgets/save_project_button.dart'; +import '../utils/responsive_helper.dart'; class StepFivePage extends StatefulWidget { const StepFivePage({super.key}); @@ -93,101 +94,105 @@ class _StepFivePageState extends State { @override Widget build(BuildContext context) { return Scaffold( - body: SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Step 5: Main characters', - style: Theme.of(context) - .textTheme - .headlineSmall - ?.copyWith(fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8), - Text( - 'Describe the main characters of your story (maximum $_maxCharacters).', - style: Theme.of(context).textTheme.bodyMedium, - ), - const SizedBox(height: 24), - ..._characterControllers.asMap().entries.map((entry) { - final index = entry.key; - final controllers = entry.value; - return Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Card( - elevation: 1, - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'Character ${index + 1}', - style: Theme.of(context) - .textTheme - .titleMedium - ?.copyWith(fontWeight: FontWeight.w600), - ), - if (_characterControllers.length > 1) - IconButton( - icon: const Icon(Icons.delete_outline, - color: Colors.red), - onPressed: () => _removeCharacterField(index), - tooltip: 'Delete character', + body: ResponsiveWrapper( + child: SingleChildScrollView( + padding: ResponsiveHelper.getContentPadding(context), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Step 5: Main characters', + style: Theme.of(context) + .textTheme + .headlineSmall + ?.copyWith(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + Text( + 'Describe the main characters of your story (maximum $_maxCharacters).', + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 24), + ..._characterControllers.asMap().entries.map((entry) { + final index = entry.key; + final controllers = entry.value; + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Card( + elevation: 1, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Character ${index + 1}', + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith(fontWeight: FontWeight.w600), ), - ], - ), - const SizedBox(height: 12), - TextFormField( - controller: controllers['name'], - decoration: const InputDecoration( - labelText: 'Character name', - hintText: 'E.g. Aragorn', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.person), + if (_characterControllers.length > 1) + IconButton( + icon: const Icon(Icons.delete_outline, + color: Colors.red), + onPressed: () => + _removeCharacterField(index), + tooltip: 'Delete character', + ), + ], ), - validator: (value) => - (value == null || value.trim().isEmpty) - ? 'Name is required' - : null, - ), - const SizedBox(height: 12), - TextFormField( - controller: controllers['description'], - maxLines: 4, - decoration: const InputDecoration( - labelText: 'Character description', - hintText: 'Role, motivation, conflict, goals...', - border: OutlineInputBorder(), - alignLabelWithHint: true, + const SizedBox(height: 12), + TextFormField( + controller: controllers['name'], + decoration: const InputDecoration( + labelText: 'Character name', + hintText: 'E.g. Aragorn', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.person), + ), + validator: (value) => + (value == null || value.trim().isEmpty) + ? 'Name is required' + : null, ), - validator: (value) => - (value == null || value.trim().isEmpty) - ? 'Description is required' - : null, - ), - ], + const SizedBox(height: 12), + TextFormField( + controller: controllers['description'], + maxLines: 4, + decoration: const InputDecoration( + labelText: 'Character description', + hintText: + 'Role, motivation, conflict, goals...', + border: OutlineInputBorder(), + alignLabelWithHint: true, + ), + validator: (value) => + (value == null || value.trim().isEmpty) + ? 'Description is required' + : null, + ), + ], + ), ), ), + ); + }), + if (_characterControllers.length < _maxCharacters) + OutlinedButton.icon( + onPressed: _addCharacterField, + icon: const Icon(Icons.add), + label: const Text('Add character'), ), - ); - }), - if (_characterControllers.length < _maxCharacters) - OutlinedButton.icon( - onPressed: _addCharacterField, - icon: const Icon(Icons.add), - label: const Text('Add character'), - ), - const SizedBox(height: 24), - SaveProjectButton(onPressed: _save), - ], + const SizedBox(height: 24), + SaveProjectButton(onPressed: _save), + ], + ), ), ), ), diff --git a/src/fireflake/lib/step_views/step6.dart b/src/fireflake/lib/step_views/step6.dart index 9ff5e3b..da83cca 100644 --- a/src/fireflake/lib/step_views/step6.dart +++ b/src/fireflake/lib/step_views/step6.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../state/app_cubit.dart'; import '../widgets/save_project_button.dart'; +import '../utils/responsive_helper.dart'; class StepSixPage extends StatefulWidget { const StepSixPage({super.key}); @@ -56,71 +57,73 @@ class _StepSixPageState extends State { child: Text('Select or create a project first')); } - return SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Step 6: Expand argument', - style: Theme.of(context) - .textTheme - .headlineSmall - ?.copyWith(fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8), - Text( - 'Return to Step 4 and expand the argument with more narrative detail. Integrate conflicts, twists, and character development.', - style: Theme.of(context).textTheme.bodyMedium, - ), - const SizedBox(height: 24), - Card( - elevation: 1, - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Extended argument', - style: Theme.of(context) - .textTheme - .titleMedium - ?.copyWith(fontWeight: FontWeight.w600), - ), - const SizedBox(height: 8), - Text( - 'Combine the paragraphs from Step 4 and add more depth: internal conflicts, subplots, twists, relationship arcs...', - style: Theme.of(context) - .textTheme - .bodySmall - ?.copyWith(color: Colors.grey[700]), - ), - const SizedBox(height: 12), - TextFormField( - controller: _argumentController, - maxLines: 15, - decoration: const InputDecoration( - labelText: 'Full expanded argument', - hintText: - 'Write multiple paragraphs with the full argument, integrating all narrative elements...', - border: OutlineInputBorder(), - alignLabelWithHint: true, + return ResponsiveWrapper( + child: SingleChildScrollView( + padding: ResponsiveHelper.getContentPadding(context), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Step 6: Expand argument', + style: Theme.of(context) + .textTheme + .headlineSmall + ?.copyWith(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + Text( + 'Return to Step 4 and expand the argument with more narrative detail. Integrate conflicts, twists, and character development.', + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 24), + Card( + elevation: 1, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Extended argument', + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith(fontWeight: FontWeight.w600), + ), + const SizedBox(height: 8), + Text( + 'Combine the paragraphs from Step 4 and add more depth: internal conflicts, subplots, twists, relationship arcs...', + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith(color: Colors.grey[700]), + ), + const SizedBox(height: 12), + TextFormField( + controller: _argumentController, + maxLines: 15, + decoration: const InputDecoration( + labelText: 'Full expanded argument', + hintText: + 'Write multiple paragraphs with the full argument, integrating all narrative elements...', + border: OutlineInputBorder(), + alignLabelWithHint: true, + ), + validator: (value) => + (value == null || value.trim().isEmpty) + ? 'The extended argument is required' + : null, ), - validator: (value) => - (value == null || value.trim().isEmpty) - ? 'The extended argument is required' - : null, - ), - ], + ], + ), ), ), - ), - const SizedBox(height: 24), - SaveProjectButton(onPressed: _save), - ], + const SizedBox(height: 24), + SaveProjectButton(onPressed: _save), + ], + ), ), ), ); diff --git a/src/fireflake/lib/step_views/step7.dart b/src/fireflake/lib/step_views/step7.dart index 25b6c3e..8b3a206 100644 --- a/src/fireflake/lib/step_views/step7.dart +++ b/src/fireflake/lib/step_views/step7.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../state/app_cubit.dart'; +import '../utils/responsive_helper.dart'; class StepSevenPage extends StatefulWidget { const StepSevenPage({super.key}); @@ -30,91 +31,94 @@ class _StepSevenPageState extends State { ); } - return SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Step 7: Character tables', - style: Theme.of(context) - .textTheme - .headlineSmall - ?.copyWith(fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8), - Text( - 'Review the detailed info of main characters (created in Step 3 and Step 5).', - style: Theme.of(context).textTheme.bodyMedium, - ), - const SizedBox(height: 24), - ...characters.asMap().entries.map((entry) { - final index = entry.key; - final character = entry.value; - return Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Card( - elevation: 2, - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - CircleAvatar( - backgroundColor: - Theme.of(context).primaryColor, - child: Text( - '${index + 1}', - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold), + return ResponsiveWrapper( + child: SingleChildScrollView( + padding: ResponsiveHelper.getContentPadding(context), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Step 7: Character tables', + style: Theme.of(context) + .textTheme + .headlineSmall + ?.copyWith(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + Text( + 'Review the detailed info of main characters (created in Step 3 and Step 5).', + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 24), + ...characters.asMap().entries.map((entry) { + final index = entry.key; + final character = entry.value; + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Card( + elevation: 2, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + CircleAvatar( + backgroundColor: + Theme.of(context).primaryColor, + child: Text( + '${index + 1}', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold), + ), ), - ), - const SizedBox(width: 12), - Expanded( - child: Text( - character.name, - style: Theme.of(context) - .textTheme - .titleLarge - ?.copyWith(fontWeight: FontWeight.bold), + const SizedBox(width: 12), + Expanded( + child: Text( + character.name, + style: Theme.of(context) + .textTheme + .titleLarge + ?.copyWith( + fontWeight: FontWeight.bold), + ), ), - ), - ], - ), - const Divider(height: 24), - _buildInfoRow(context, 'Descripción / Objetivo', - character.storygoal), - ], + ], + ), + const Divider(height: 24), + _buildInfoRow(context, 'Descripción / Objetivo', + character.storygoal), + ], + ), ), ), + ); + }), + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.blue.shade200), ), - ); - }), - const SizedBox(height: 16), - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.blue.shade50, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.blue.shade200), - ), - child: Row( - children: [ - Icon(Icons.info_outline, color: Colors.blue.shade700), - const SizedBox(width: 12), - Expanded( - child: Text( - 'To edit characters, go back to Step 3 or Step 5.', - style: TextStyle(color: Colors.blue.shade700), + child: Row( + children: [ + Icon(Icons.info_outline, color: Colors.blue.shade700), + const SizedBox(width: 12), + Expanded( + child: Text( + 'To edit characters, go back to Step 3 or Step 5.', + style: TextStyle(color: Colors.blue.shade700), + ), ), - ), - ], + ], + ), ), - ), - ], + ], + ), ), ); }, diff --git a/src/fireflake/lib/step_views/step8.dart b/src/fireflake/lib/step_views/step8.dart index 486db8f..8413ed5 100644 --- a/src/fireflake/lib/step_views/step8.dart +++ b/src/fireflake/lib/step_views/step8.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../state/app_cubit.dart'; import '../widgets/save_project_button.dart'; +import '../utils/responsive_helper.dart'; class StepEightPage extends StatefulWidget { const StepEightPage({super.key}); @@ -87,117 +88,119 @@ class _StepEightPageState extends State { child: Text('Select or create a project first')); } - return SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Step 8: Scene list', - style: Theme.of(context) - .textTheme - .headlineSmall - ?.copyWith(fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8), - Text( - 'Using the extended argument from Step 6, list the scenes needed to complete the story.', - style: Theme.of(context).textTheme.bodyMedium, - ), - const SizedBox(height: 24), - ..._sceneControllers.asMap().entries.map((entry) { - final index = entry.key; - final controller = entry.value; - return Padding( - padding: const EdgeInsets.only(bottom: 12), - child: Card( - elevation: 1, - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - children: [ - Container( - width: 32, - height: 32, - decoration: BoxDecoration( - color: Theme.of(context) - .primaryColor - .withValues(alpha: 0.2), - shape: BoxShape.circle, - ), - child: Center( - child: Text( - '${index + 1}', - style: TextStyle( - fontWeight: FontWeight.bold, - color: Theme.of(context).primaryColor, + return ResponsiveWrapper( + child: SingleChildScrollView( + padding: ResponsiveHelper.getContentPadding(context), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Step 8: Scene list', + style: Theme.of(context) + .textTheme + .headlineSmall + ?.copyWith(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + Text( + 'Using the extended argument from Step 6, list the scenes needed to complete the story.', + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 24), + ..._sceneControllers.asMap().entries.map((entry) { + final index = entry.key; + final controller = entry.value; + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Card( + elevation: 1, + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: Theme.of(context) + .primaryColor + .withValues(alpha: 0.2), + shape: BoxShape.circle, + ), + child: Center( + child: Text( + '${index + 1}', + style: TextStyle( + fontWeight: FontWeight.bold, + color: Theme.of(context).primaryColor, + ), ), ), ), - ), - const SizedBox(width: 12), - Expanded( - child: TextFormField( - controller: controller, - decoration: InputDecoration( - labelText: 'Scene ${index + 1}', - hintText: 'Briefly describe the scene...', - border: const OutlineInputBorder(), - isDense: true, + const SizedBox(width: 12), + Expanded( + child: TextFormField( + controller: controller, + decoration: InputDecoration( + labelText: 'Scene ${index + 1}', + hintText: 'Briefly describe the scene...', + border: const OutlineInputBorder(), + isDense: true, + ), + validator: (value) => + (value == null || value.trim().isEmpty) + ? 'Required' + : null, ), - validator: (value) => - (value == null || value.trim().isEmpty) - ? 'Required' - : null, - ), - ), - const SizedBox(width: 8), - if (_sceneControllers.length > 1) - IconButton( - icon: const Icon(Icons.delete_outline, - color: Colors.red, size: 20), - onPressed: () => _removeSceneField(index), - tooltip: 'Delete', ), - ], + const SizedBox(width: 8), + if (_sceneControllers.length > 1) + IconButton( + icon: const Icon(Icons.delete_outline, + color: Colors.red, size: 20), + onPressed: () => _removeSceneField(index), + tooltip: 'Delete', + ), + ], + ), ), ), - ), - ); - }), - const SizedBox(height: 8), - OutlinedButton.icon( - onPressed: _addSceneField, - icon: const Icon(Icons.add), - label: const Text('Add scene'), - ), - const SizedBox(height: 24), - SaveProjectButton(onPressed: _save), - const SizedBox(height: 16), - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.amber.shade50, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.amber.shade200), + ); + }), + const SizedBox(height: 8), + OutlinedButton.icon( + onPressed: _addSceneField, + icon: const Icon(Icons.add), + label: const Text('Add scene'), ), - child: Row( - children: [ - Icon(Icons.lightbulb_outline, - color: Colors.amber.shade700), - const SizedBox(width: 12), - Expanded( - child: Text( - 'After saving this list, go to Step 9 to write the narrative summary of each scene.', - style: TextStyle(color: Colors.amber.shade900), + const SizedBox(height: 24), + SaveProjectButton(onPressed: _save), + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.amber.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.amber.shade200), + ), + child: Row( + children: [ + Icon(Icons.lightbulb_outline, + color: Colors.amber.shade700), + const SizedBox(width: 12), + Expanded( + child: Text( + 'After saving this list, go to Step 9 to write the narrative summary of each scene.', + style: TextStyle(color: Colors.amber.shade900), + ), ), - ), - ], + ], + ), ), - ), - ], + ], + ), ), ), ); diff --git a/src/fireflake/lib/step_views/step9.dart b/src/fireflake/lib/step_views/step9.dart index d12167a..0be50c3 100644 --- a/src/fireflake/lib/step_views/step9.dart +++ b/src/fireflake/lib/step_views/step9.dart @@ -4,6 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import '../models/scene.dart'; import '../state/app_cubit.dart'; import '../widgets/save_project_button.dart'; +import '../utils/responsive_helper.dart'; class StepNinePage extends StatefulWidget { const StepNinePage({super.key}); @@ -58,7 +59,7 @@ class _StepNinePageState extends State Column( children: [ Container( - padding: const EdgeInsets.all(16.0), + padding: ResponsiveHelper.getContentPadding(context), decoration: BoxDecoration( color: Theme.of(context).primaryColor.withValues(alpha: 0.1), border: Border( @@ -71,15 +72,23 @@ class _StepNinePageState extends State child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text( - 'Scene List', - style: - TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + Flexible( + child: Text( + 'Scene List', + style: Theme.of(context) + .textTheme + .headlineSmall + ?.copyWith(fontWeight: FontWeight.bold), + overflow: TextOverflow.ellipsis, + ), ), + const SizedBox(width: 8), ElevatedButton.icon( onPressed: _addNewScene, icon: const Icon(Icons.add), - label: const Text('Add New Scene'), + label: Text(ResponsiveHelper.isMobile(context) + ? 'Add' + : 'Add New Scene'), ), ], ), @@ -251,7 +260,11 @@ class _StepNinePageState extends State child: Align( alignment: Alignment.centerRight, child: Container( - width: 400, + width: ResponsiveHelper.isMobile(context) + ? MediaQuery.of(context).size.width * 0.9 + : ResponsiveHelper.isTablet(context) + ? 450 + : 500, height: double.infinity, decoration: BoxDecoration( color: Theme.of(context).scaffoldBackgroundColor, diff --git a/src/fireflake/lib/utils/responsive_helper.dart b/src/fireflake/lib/utils/responsive_helper.dart new file mode 100644 index 0000000..2151556 --- /dev/null +++ b/src/fireflake/lib/utils/responsive_helper.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; + +class ResponsiveHelper { + static bool isMobile(BuildContext context) => + MediaQuery.of(context).size.width < 600; + + static bool isTablet(BuildContext context) => + MediaQuery.of(context).size.width >= 600 && + MediaQuery.of(context).size.width < 1024; + + static bool isDesktop(BuildContext context) => + MediaQuery.of(context).size.width >= 1024; + + static double getHorizontalPadding(BuildContext context) { + if (isDesktop(context)) return 48; + if (isTablet(context)) return 32; + return 16; + } + + static double getMaxContentWidth(BuildContext context) { + if (isDesktop(context)) return 1200; + if (isTablet(context)) return 800; + return double.infinity; + } + + static EdgeInsets getContentPadding(BuildContext context) { + final horizontal = getHorizontalPadding(context); + return EdgeInsets.symmetric(horizontal: horizontal, vertical: 16); + } + + static int getGridColumns(BuildContext context) { + if (isDesktop(context)) return 2; + if (isTablet(context)) return 2; + return 1; + } + + static double getCardElevation(BuildContext context) { + return isDesktop(context) ? 2 : 1; + } + + static double getTextScale(BuildContext context) { + if (isMobile(context)) return 1.0; + if (isTablet(context)) return 1.05; + return 1.1; + } +} + +class ResponsiveWrapper extends StatelessWidget { + final Widget child; + + const ResponsiveWrapper({super.key, required this.child}); + + @override + Widget build(BuildContext context) { + return Center( + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: ResponsiveHelper.getMaxContentWidth(context), + ), + child: child, + ), + ); + } +} From 73594f6ea554d851a172153017a8f5e7a5a695f4 Mon Sep 17 00:00:00 2001 From: Elena G Blanco Date: Mon, 22 Dec 2025 10:56:51 +0100 Subject: [PATCH 7/8] Remove comments --- src/fireflake/lib/state/author_cubit.dart | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/fireflake/lib/state/author_cubit.dart b/src/fireflake/lib/state/author_cubit.dart index 7c92040..5e142fd 100644 --- a/src/fireflake/lib/state/author_cubit.dart +++ b/src/fireflake/lib/state/author_cubit.dart @@ -11,7 +11,6 @@ class AuthorCubit extends Cubit { loadSettings(); } - // Cargar configuración desde SharedPreferences Future loadSettings() async { try { final prefs = await SharedPreferences.getInstance(); @@ -22,12 +21,10 @@ class AuthorCubit extends Cubit { emit(AuthorSettings.fromJson(json)); } } catch (e) { - // Si hay error, mantener valores por defecto debugPrint('Error loading author settings: $e'); } } - // Guardar configuración en SharedPreferences Future saveSettings(AuthorSettings settings) async { try { final prefs = await SharedPreferences.getInstance(); @@ -39,25 +36,21 @@ class AuthorCubit extends Cubit { } } - // Actualizar nombre Future updateName(String name) async { final updated = state.copyWith(name: name); await saveSettings(updated); } - // Actualizar biografía Future updateBio(String bio) async { final updated = state.copyWith(bio: bio); await saveSettings(updated); } - // Actualizar email Future updateEmail(String email) async { final updated = state.copyWith(email: email); await saveSettings(updated); } - // Actualizar todo a la vez Future updateAll({ String? name, String? bio, From f101f4486547526d8bf7329dcadf727a5b0a149d Mon Sep 17 00:00:00 2001 From: Elena G Blanco Date: Mon, 22 Dec 2025 10:59:13 +0100 Subject: [PATCH 8/8] Drop deploy action --- .github/workflows/deploy.yaml | 26 -------------------------- 1 file changed, 26 deletions(-) delete mode 100644 .github/workflows/deploy.yaml diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml deleted file mode 100644 index 2437e03..0000000 --- a/.github/workflows/deploy.yaml +++ /dev/null @@ -1,26 +0,0 @@ -# name: Release app -# on: -# workflow_dispatch: -# jobs: -# build: -# runs-on: windows-latest -# steps: -# - uses: actions/checkout@v2 -# - uses: subosito/flutter-action@v1 -# - uses: actions/checkout@v2 -# - name: Install dependencies -# run: flutter pub get -# working-directory: src/fireflake -# - name: Build windows app -# run: flutter build windows -# working-directory: src/fireflake -# - uses: actions/upload-artifact@v1 -# with: -# name: Release windows exe -# path: -# - name: Upload to GitHub Release -# uses: xresloader/upload-to-github-release@v1 -# with: -# file: "src\fireflake\build\windows\x64\runner\Release\fireflake.exe" -# env: -# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file