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 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..fe554ee 100644 --- a/src/fireflake/lib/author_info_page.dart +++ b/src/fireflake/lib/author_info_page.dart @@ -1,7 +1,11 @@ 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}); + const AuthorInfoPage({super.key}); @override State createState() => _AuthorInfoPageState(); @@ -9,15 +13,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('Author info saved')), ); } } @@ -25,40 +49,90 @@ 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('Author Information')), + body: BlocBuilder( + builder: (context, settings) { + 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 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(), + ), + 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, + ), + 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 '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), + ), + child: const Text( + 'Save', + style: TextStyle(fontSize: 16), + ), + ), + ], + ), ), - ], - ), - ), + ), + ); + }, ), ); } -} \ No newline at end of file +} diff --git a/src/fireflake/lib/data/project_storage.dart b/src/fireflake/lib/data/project_storage.dart new file mode 100644 index 0000000..8fd2ea6 --- /dev/null +++ b/src/fireflake/lib/data/project_storage.dart @@ -0,0 +1,72 @@ +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, [String? filename]) async { + final dir = await _baseDir(); + final actualFilename = filename ?? _fileName(project.title); + final file = File(p.join(dir.path, actualFilename)); + 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/info_views/projectinfo.dart b/src/fireflake/lib/info_views/projectinfo.dart index 90599a0..e4d5d99 100644 --- a/src/fireflake/lib/info_views/projectinfo.dart +++ b/src/fireflake/lib/info_views/projectinfo.dart @@ -1,5 +1,8 @@ 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}); @@ -13,6 +16,7 @@ class _ProjectInfoPageState extends State { final TextEditingController _titleController = TextEditingController(); final TextEditingController _subtitleController = TextEditingController(); final TextEditingController _wordCountController = TextEditingController(); + bool _initializedFromState = false; @override void dispose() { @@ -22,14 +26,23 @@ class _ProjectInfoPageState extends State { super.dispose(); } - void _saveProject() { + void _saveProject() async { 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, + ); + await cubit.saveCurrentProjectToDisk(); + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Project saved'), + backgroundColor: Colors.green, + ), + ); } } @@ -39,251 +52,280 @@ 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( - 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( - 'Información del Proyecto', - style: Theme.of(context).textTheme.headlineMedium?.copyWith( - fontWeight: FontWeight.bold, - color: Theme.of(context).primaryColor, + 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: [ + Text( + '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', - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: Colors.grey[600], + 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( - 'Título del Proyecto', - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, + 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, + ), ), - ), - const SizedBox(height: 12), - TextFormField( - controller: _titleController, - decoration: const InputDecoration( - labelText: 'Título', - hintText: 'Ej: El Reino de las Sombras', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.title), + 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; + }, ), - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'El título es obligatorio'; - } - if (value.trim().length < 3) { - return 'El título debe tener al menos 3 caracteres'; - } - return null; - }, - ), - ], + ], + ), ), ), - ), - - const SizedBox(height: 20), - - Card( - elevation: 2, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Subtítulo', - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, + 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( - 'Descripción breve o tagline del proyecto', - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Colors.grey[600], + 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: 'Subtítulo', - hintText: 'Ej: Una épica aventura de fantasía', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.short_text), + 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; + }, ), - 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'; - } - return null; - }, - ), - ], + ], + ), ), ), - ), - - const SizedBox(height: 20), - - Card( - elevation: 2, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Número de palabras', - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, + 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( - 'Número aproximado para completar el proyecto', - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Colors.grey[600], + 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: 'Número de palabras', - hintText: 'Ej: 80000', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.format_list_numbered), - suffixText: 'palabras', + 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; + }, ), - keyboardType: TextInputType.number, - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly, - ], - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'El número de palabras es obligatorio'; - } - final int? wordCount = int.tryParse(value); - if (wordCount == null) { - return 'Debe ser un número válido'; - } - if (wordCount < 1000) { - return 'El conteo debe ser de al menos 1,000 palabras'; - } - if (wordCount > 1000000) { - return 'El conteo no puede exceder 1,000,000 palabras'; - } - 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), - ), - child: Row( - children: [ - 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:', - style: TextStyle( - fontWeight: FontWeight.w600, - color: Colors.blue.shade700, + 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), + ), + 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( - '• Novela corta: 50,000 - 80,000 palabras\n' - '• Novela estándar: 80,000 - 100,000 palabras\n' - '• Novela épica: 100,000+ palabras', - style: TextStyle( - fontSize: 13, - color: Colors.blue.shade600, + 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, + ), ), - ), - ], + ], + ), ), - ), - ], + ], + ), ), - ), - ], + ], + ), ), ), - ), - - Container( - padding: const EdgeInsets.only(top: 24), - child: Row( - children: [ - Expanded( - child: OutlinedButton.icon( - onPressed: _clearForm, - icon: const Icon(Icons.clear), - label: const Text('Limpiar'), + 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('Guardar Proyecto'), + 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 efde0bd..e160c0e 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( - debugShowCheckedModeBanner: false, - title: 'Fireflake', - theme: ThemeData(brightness: Brightness.light), - home: HomePage(title: 'Fireflake'), + return MultiBlocProvider( + providers: [ + BlocProvider(create: (_) => AppCubit()), + BlocProvider(create: (_) => AuthorCubit()), + ], + child: MaterialApp( + debugShowCheckedModeBanner: false, + title: 'Fireflake', + theme: ThemeData(brightness: Brightness.light), + home: HomePage(title: 'Fireflake'), + ), ); } } @@ -32,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/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..ec8ec1a 100644 --- a/src/fireflake/lib/models/project.dart +++ b/src/fireflake/lib/models/project.dart @@ -1,11 +1,125 @@ +import 'scene.dart'; +import 'character.dart'; + class Project { 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..be4020f 100644 --- a/src/fireflake/lib/projects_page.dart +++ b/src/fireflake/lib/projects_page.dart @@ -2,6 +2,9 @@ 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'; +import 'utils/responsive_helper.dart'; class ProjectsPage extends StatefulWidget { const ProjectsPage({super.key}); @@ -11,22 +14,43 @@ class ProjectsPage extends StatefulWidget { } class _ProjectsPageState extends State { - List projectsToLoad = [Project(title: 'Project 1', expectedWordCount: 1000)]; + int? _selectedProjectIndex; + + @override + void initState() { + super.initState(); + context.read().loadProjectsFromDisk(); + } @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: 200, - height: 300, - color: Colors.grey, - child: buildProjectList()), + 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) { + 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); + }, + )), Container( margin: const EdgeInsets.all(8), padding: const EdgeInsets.all(8), @@ -38,24 +62,68 @@ class _ProjectsPageState extends State { padding: const EdgeInsets.all(8.0), child: TextButton( style: mainButtonStyle(), - onPressed: () { - 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: () { - print('Creating new project...'); + _createNewProject(context); }, 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( + style: mainButtonStyle(), + onPressed: () async { + await context.read().saveCurrentProjectToDisk(); + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Project saved to disk')), + ); + }, + child: const Text('Save'), + ), + ), ], ), ), @@ -85,16 +153,161 @@ 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), + final isSelected = _selectedProjectIndex == index; + return GestureDetector( + onTap: () { + setState(() { + _selectedProjectIndex = index; + }); + }, + 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'); + 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/projectwrapper.dart b/src/fireflake/lib/projectwrapper.dart index 2697111..d2dd26e 100644 --- a/src/fireflake/lib/projectwrapper.dart +++ b/src/fireflake/lib/projectwrapper.dart @@ -1,12 +1,16 @@ - 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}); @@ -18,30 +22,41 @@ class ProjectWrapper extends StatefulWidget { class _ProjectWrapperState extends State { @override Widget build(BuildContext context) { - const steps = [ - ProjectInfoPage(), - StepOnePage(), - StepTwoPage(), - StepThreePage(), - StepFourPage(), - StepFivePage(), - StepNinePage() - ]; + ProjectInfoPage(), + StepOnePage(), + StepTwoPage(), + StepThreePage(), + StepFourPage(), + StepFivePage(), + StepSixPage(), + StepSevenPage(), + StepEightPage(), + StepNinePage() + ]; return DefaultTabController( 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..d958f82 --- /dev/null +++ b/src/fireflake/lib/state/app_cubit.dart @@ -0,0 +1,283 @@ +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; + final String? currentProjectFilename; + + const AppState({ + this.projects = const [], + this.selectedProject, + this.scenes = const [], + this.characters = const [], + this.currentProjectFilename, + }); + + AppState copyWith({ + List? projects, + 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, currentProjectFilename]; +} + +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) { + 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) { + 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 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)); + } + + Future saveCurrentProjectToDisk() async { + 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, + ); + await ProjectStorage.saveProject(merged, state.currentProjectFilename); + final projects = state.projects + .map((p) => p.title == merged.title ? merged : p) + .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)); + } +} diff --git a/src/fireflake/lib/state/author_cubit.dart b/src/fireflake/lib/state/author_cubit.dart new file mode 100644 index 0000000..5e142fd --- /dev/null +++ b/src/fireflake/lib/state/author_cubit.dart @@ -0,0 +1,66 @@ +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'; + +class AuthorCubit extends Cubit { + static const String _storageKey = 'author_settings'; + + AuthorCubit() : super(const AuthorSettings()) { + loadSettings(); + } + + 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) { + debugPrint('Error loading author settings: $e'); + } + } + + 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) { + debugPrint('Error saving author settings: $e'); + } + } + + Future updateName(String name) async { + final updated = state.copyWith(name: name); + await saveSettings(updated); + } + + Future updateBio(String bio) async { + final updated = state.copyWith(bio: bio); + await saveSettings(updated); + } + + Future updateEmail(String email) async { + final updated = state.copyWith(email: email); + await saveSettings(updated); + } + + 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..8693285 100644 --- a/src/fireflake/lib/step_views/step1.dart +++ b/src/fireflake/lib/step_views/step1.dart @@ -1,4 +1,8 @@ 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}); @@ -8,10 +12,102 @@ 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('Summary saved')), + ); + } + @override Widget build(BuildContext context) { - return Center( - child: Text('This is Step 1'), + return Scaffold( + 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, + ), + ], + ), + ), + ), + 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 9865efa..c24717b 100644 --- a/src/fireflake/lib/step_views/step2.dart +++ b/src/fireflake/lib/step_views/step2.dart @@ -1,4 +1,8 @@ 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}); @@ -8,10 +12,191 @@ 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('Expanded summary saved')), + ); + } + @override Widget build(BuildContext context) { - return Center( - child: Text('This is Step 2'), + return Scaffold( + 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, + ), + ), + 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, + ), + ), + 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, + ), + ), + 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, + ), + ), + 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, + ), + ), + const SizedBox(height: 24), + SaveProjectButton(onPressed: _save), + ], + ), + ), + ), + ), + ); + } + + Widget _buildCard(BuildContext context, + {required String title, required Widget child}) { + return Card( + elevation: ResponsiveHelper.getCardElevation(context), + child: Padding( + padding: EdgeInsets.all(ResponsiveHelper.isMobile(context) ? 12 : 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..169fb56 100644 --- a/src/fireflake/lib/step_views/step3.dart +++ b/src/fireflake/lib/step_views/step3.dart @@ -1,5 +1,9 @@ -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'; +import '../widgets/save_project_button.dart'; +import '../utils/responsive_helper.dart'; class StepThreePage extends StatefulWidget { const StepThreePage({super.key}); @@ -72,22 +76,21 @@ 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: [ 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), @@ -102,12 +105,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; }, @@ -124,76 +127,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), @@ -208,12 +206,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; }, @@ -230,23 +228,31 @@ 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()) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Información del personaje guardada'), - ), - ); - } - }, - child: const Text('Guardar Información'), - ), + 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'), + ), + ); + } + }, ), ], ), @@ -256,5 +262,3 @@ class _StepThreePageState extends State { ); } } - - diff --git a/src/fireflake/lib/step_views/step4.dart b/src/fireflake/lib/step_views/step4.dart index ad13aed..a0c6cd0 100644 --- a/src/fireflake/lib/step_views/step4.dart +++ b/src/fireflake/lib/step_views/step4.dart @@ -1,4 +1,8 @@ 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}); @@ -8,10 +12,171 @@ 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('Expanded paragraphs saved')), + ); + } + @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('Select or create a project first')); + } + + 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) { + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() {}); + }); + } + + 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), + ], + ), + ), + ), + ); + }, + ), + ); + } + + Widget _buildExpandedSection(BuildContext context, String title, String key) { + return Card( + elevation: ResponsiveHelper.getCardElevation(context), + child: Padding( + padding: EdgeInsets.all(ResponsiveHelper.isMobile(context) ? 12 : 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: ResponsiveHelper.isMobile(context) ? 4 : 6, + decoration: InputDecoration( + 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) + ? 'This paragraph is required' + : null, + ), + ], + ), + ), ); } } diff --git a/src/fireflake/lib/step_views/step5.dart b/src/fireflake/lib/step_views/step5.dart index 571ebb4..09975e6 100644 --- a/src/fireflake/lib/step_views/step5.dart +++ b/src/fireflake/lib/step_views/step5.dart @@ -1,4 +1,9 @@ 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'; +import '../utils/responsive_helper.dart'; class StepFivePage extends StatefulWidget { const StepFivePage({super.key}); @@ -8,10 +13,189 @@ 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(); + 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('Main characters saved')), + ); + } + @override Widget build(BuildContext context) { - return Center( - child: Text('This is Step Five'), + return Scaffold( + 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), + ), + if (_characterControllers.length > 1) + IconButton( + icon: const Icon(Icons.delete_outline, + color: Colors.red), + onPressed: () => + _removeCharacterField(index), + tooltip: 'Delete character', + ), + ], + ), + 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, + ), + 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'), + ), + 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 new file mode 100644 index 0000000..da83cca --- /dev/null +++ b/src/fireflake/lib/step_views/step6.dart @@ -0,0 +1,134 @@ +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}); + + @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('Extended argument saved')), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: BlocBuilder( + builder: (context, state) { + final project = state.selectedProject; + if (project == null) { + return const Center( + child: Text('Select or create a project first')); + } + + 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, + ), + ], + ), + ), + ), + 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 new file mode 100644 index 0000000..8b3a206 --- /dev/null +++ b/src/fireflake/lib/step_views/step7.dart @@ -0,0 +1,151 @@ +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}); + + @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 main characters yet.\nComplete Step 5 first.', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 16), + ), + ), + ); + } + + 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 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( + 'To edit characters, go back to Step 3 or Step 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 ? '(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 new file mode 100644 index 0000000..8413ed5 --- /dev/null +++ b/src/fireflake/lib/step_views/step8.dart @@ -0,0 +1,211 @@ +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}); + + @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('Scene list saved')), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: BlocBuilder( + builder: (context, state) { + final project = state.selectedProject; + if (project == null) { + return const Center( + child: Text('Select or create a project first')); + } + + 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, + ), + 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(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), + ), + 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 c7b927e..0be50c3 100644 --- a/src/fireflake/lib/step_views/step9.dart +++ b/src/fireflake/lib/step_views/step9.dart @@ -1,6 +1,10 @@ import 'package:flutter/material.dart'; +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}); @@ -9,40 +13,19 @@ 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(); 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(); @@ -74,159 +57,199 @@ 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.withOpacity(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: ResponsiveHelper.getContentPadding(context), + 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: [ + 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: Text(ResponsiveHelper.isMobile(context) + ? 'Add' + : 'Add New Scene'), + ), + ], ), - ], - ), - ), - Expanded( - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: SizedBox( - width: MediaQuery.of(context).size.width, + ), + Expanded( 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), - ), - ), - ), - DataColumn( - label: Expanded( - child: Text( - 'Resumen', - style: TextStyle(fontWeight: FontWeight.bold), - ), - ), - ), - DataColumn( - label: Expanded( - child: Text( - 'Acciones', - style: TextStyle(fontWeight: FontWeight.bold), - ), - ), - ), - ], - rows: scenes.map((scene) { - return DataRow( - cells: [ - DataCell( - Container( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Text( - scene.id, - style: const TextStyle(fontWeight: FontWeight.w500), - ), - ), - ), - DataCell( - Container( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Text(scene.chapter), + 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), ), - ), - 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), + columns: const [ + DataColumn( + label: Expanded( + child: Text( + 'Scene', + 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', + DataColumn( + label: Expanded( + child: Text( + 'Chapter', + style: + TextStyle(fontWeight: FontWeight.bold), + ), + ), + ), + DataColumn( + label: Expanded( + child: Text( + 'Summary', + style: + TextStyle(fontWeight: FontWeight.bold), + ), + ), + ), + DataColumn( + label: Expanded( + child: Text( + 'Actions', + 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), + child: Text( + scene.id, + style: const TextStyle( + fontWeight: FontWeight.w500), + ), + ), ), - IconButton( - icon: const Icon(Icons.keyboard_arrow_down, size: 20), - onPressed: scenes.indexOf(scene) < scenes.length - 1 - ? () => _moveSceneDown(scene) - : null, - tooltip: 'Mover abajo', + DataCell( + Container( + padding: const EdgeInsets.symmetric( + vertical: 8.0), + child: Text(scene.chapter), + ), ), - IconButton( - icon: const Icon(Icons.edit, size: 20), - onPressed: () => _editScene(scene), - tooltip: 'Editar escena', + 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: '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, child: Container( - color: Colors.black.withOpacity(0.5), + color: Colors.black.withValues(alpha: 0.5), width: double.infinity, height: double.infinity, ), @@ -237,13 +260,17 @@ class _StepNinePageState extends State with TickerProviderStateMix 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, 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,37 +286,33 @@ 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}"?'), + 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: () { - 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'), + child: const Text('Delete'), ), ], ); @@ -297,46 +320,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 = 'Scene ${scenesLength + 1}'; + _chapterController.text = 'Chapter ${(scenesLength ~/ 2) + 1}'; _summaryController.text = ''; _openSidePanel(); } @@ -353,29 +350,29 @@ 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 ? 'Scene updated' : 'New scene added'), ), ); } @@ -387,7 +384,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, @@ -398,7 +395,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, @@ -421,12 +418,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; }, @@ -435,12 +432,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; }, @@ -450,7 +447,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, ), @@ -459,10 +456,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; }, @@ -474,14 +471,16 @@ 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( + child: SaveProjectButton( + label: _editingScene != null + ? 'Update Scene' + : 'Save Scene', onPressed: _saveScene, - child: Text(_editingScene != null ? 'Actualizar' : 'Agregar'), ), ), ], 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, + ), + ); + } +} 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), + ), + ), + ); + } +} 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: