diff --git a/lib/main.dart b/lib/main.dart index 04c13572..889481ed 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:flutter/material.dart'; import 'package:maid/providers/user.dart'; import 'package:maid/ui/mobile/pages/home_page.dart'; @@ -5,17 +7,32 @@ import 'package:maid/providers/session.dart'; import 'package:maid/providers/character.dart'; import 'package:maid/static/themes.dart'; import 'package:provider/provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; -void main() { +void main() async { WidgetsFlutterBinding.ensureInitialized(); + final prefs = await SharedPreferences.getInstance(); + + String? lastUserString = prefs.getString("last_user"); + Map lastUser = json.decode(lastUserString ?? "{}"); + User user = User.fromMap(lastUser); + + String? lastCharacterString = prefs.getString("last_character"); + Map lastCharacter = json.decode(lastCharacterString ?? "{}"); + Character character = Character.fromMap(lastCharacter); + + String? lastSessionString = prefs.getString("last_session"); + Map lastSession = json.decode(lastSessionString ?? "{}"); + Session session = Session.fromMap(lastSession); + runApp( MultiProvider( providers: [ ChangeNotifierProvider(create: (context) => MainProvider()), - ChangeNotifierProvider(create: (context) => User()), - ChangeNotifierProvider(create: (context) => Character()), - ChangeNotifierProvider(create: (context) => Session()), + ChangeNotifierProvider(create: (context) => user), + ChangeNotifierProvider(create: (context) => character), + ChangeNotifierProvider(create: (context) => session), ], child: const MaidApp(), ), @@ -24,26 +41,15 @@ void main() { class MainProvider extends ChangeNotifier { ThemeMode _themeMode = ThemeMode.dark; - bool _initialised = false; ThemeMode get themeMode => _themeMode; - bool get isDarkMode => _themeMode == ThemeMode.dark; - - bool get initialised => _initialised; - - void toggleTheme() { - _themeMode = - _themeMode == ThemeMode.dark ? ThemeMode.light : ThemeMode.dark; + set themeMode(ThemeMode value) { + _themeMode = value; notifyListeners(); } - void init() { - _initialised = true; - } - void reset() { - _initialised = false; notifyListeners(); } } @@ -58,15 +64,8 @@ class MaidApp extends StatefulWidget { class MaidAppState extends State { @override Widget build(BuildContext context) { - return Consumer4( - builder: (context, mainProvider, user, character, session, child) { - if (!mainProvider.initialised) { - mainProvider.init(); - user.init(); - character.init(); - session.init(); - } - + return Consumer( + builder: (context, mainProvider, child) { return MaterialApp( debugShowCheckedModeBanner: false, title: 'Maid', diff --git a/lib/providers/character.dart b/lib/providers/character.dart index 0491b5b9..c150cd64 100644 --- a/lib/providers/character.dart +++ b/lib/providers/character.dart @@ -9,7 +9,6 @@ import 'package:maid/static/logger.dart'; import 'package:image/image.dart'; import 'package:maid/static/utilities.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:shared_preferences/shared_preferences.dart'; class Character extends ChangeNotifier { Key _key = UniqueKey(); @@ -29,39 +28,17 @@ class Character extends ChangeNotifier { Map _cachedJson = {}; Character() { - init(); + reset(); } Character.fromMap(Map inputJson) { fromMap(inputJson); } - void newCharacter() { - final key = UniqueKey().toString(); - _name = "New Character $key"; - reset(); - } - void notify() { notifyListeners(); } - void init() async { - Logger.log("Character Initialised"); - - final prefs = await SharedPreferences.getInstance(); - - Map lastCharacter = - json.decode(prefs.getString("last_character") ?? "{}"); - - if (lastCharacter.isNotEmpty) { - Logger.log(lastCharacter.toString()); - fromMap(lastCharacter); - } else { - await reset(); - } - } - Character copy() { Character newCharacter = Character(); newCharacter.from(this); @@ -92,10 +69,6 @@ class Character extends ChangeNotifier { _profile = await Utilities.fileFromAssetImage("defaultCharacter.png"); } - if (inputJson.isEmpty) { - await reset(); - } - if (inputJson["spec"] == "mcf_v1") { Logger.log("Character loaded from MCF"); fromMCFMap(inputJson); @@ -112,7 +85,6 @@ class Character extends ChangeNotifier { await reset(); } - Logger.log("Character created with name: ${inputJson["name"]}"); _useExamples = _examples.isNotEmpty; notifyListeners(); } diff --git a/lib/providers/session.dart b/lib/providers/session.dart index f2d6efa0..200e374b 100644 --- a/lib/providers/session.dart +++ b/lib/providers/session.dart @@ -1,7 +1,6 @@ import 'dart:convert'; import 'package:flutter/material.dart'; -import 'package:maid_llm/src/chat_node_tree.dart'; import 'package:maid/classes/google_gemini_model.dart'; import 'package:maid/classes/large_language_model.dart'; import 'package:maid/classes/llama_cpp_model.dart'; @@ -11,7 +10,7 @@ import 'package:maid/classes/open_ai_model.dart'; import 'package:maid/providers/character.dart'; import 'package:maid/providers/user.dart'; import 'package:maid/static/logger.dart'; -import 'package:maid_llm/src/chat_node.dart'; +import 'package:maid_llm/maid_llm.dart'; import 'package:maid/static/utilities.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -40,21 +39,6 @@ class Session extends ChangeNotifier { notifyListeners(); } - void init() async { - Logger.log("Session Initialised"); - - final prefs = await SharedPreferences.getInstance(); - - Map lastSession = - json.decode(prefs.getString("last_session") ?? "{}") ?? {}; - - if (lastSession.isNotEmpty) { - fromMap(lastSession); - } else { - newSession(); - } - } - void notify() { notifyListeners(); } @@ -91,6 +75,11 @@ class Session extends ChangeNotifier { } void fromMap(Map inputJson) { + if (inputJson.isEmpty) { + newSession(); + return; + } + chat.root = ChatNode.fromMap(inputJson['chat'] ?? {}); final type = LargeLanguageModelType.values[inputJson['llm_type'] ?? LargeLanguageModelType.llamacpp.index]; diff --git a/lib/providers/user.dart b/lib/providers/user.dart index 8e354571..fb0d7284 100644 --- a/lib/providers/user.dart +++ b/lib/providers/user.dart @@ -1,18 +1,30 @@ import 'dart:io'; -import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:maid/static/utilities.dart'; -import 'package:shared_preferences/shared_preferences.dart'; class User extends ChangeNotifier { File? _profile; String _name = "User"; + User() { + reset(); + } + + User.from(User user) { + _profile = profileFile; + _name = user.name; + } + + User.fromMap(Map inputMap) { + fromMap(inputMap); + } + Future get profile async { return _profile ?? await Utilities.fileFromAssetImage("chadUser.png"); } + File? get profileFile => _profile; String get name => _name; set profile(Future value) { @@ -27,29 +39,19 @@ class User extends ChangeNotifier { notifyListeners(); } - void init() async { - final prefs = await SharedPreferences.getInstance(); - - Map lastUser = - json.decode(prefs.getString("last_user") ?? "{}") ?? {}; - - if (lastUser.isNotEmpty) { - fromMap(lastUser); - } else { + void fromMap(Map inputMap) async { + if (inputMap.isEmpty) { reset(); + return; } - notifyListeners(); - } - - void fromMap(Map inputJson) async { - if (inputJson["profile"] != null) { - _profile = File(inputJson["profile"]); + if (inputMap["profile"] != null) { + _profile = File(inputMap["profile"]); } else { _profile ??= await Utilities.fileFromAssetImage("chadUser.png"); } - _name = inputJson["name"]; + _name = inputMap["name"]; notifyListeners(); } diff --git a/lib/ui/mobile/pages/character/character_browser_page.dart b/lib/ui/mobile/pages/character/character_browser_page.dart index d2919c66..3b0c26c3 100644 --- a/lib/ui/mobile/pages/character/character_browser_page.dart +++ b/lib/ui/mobile/pages/character/character_browser_page.dart @@ -54,71 +54,56 @@ class _CharacterBrowserPageState extends State { @override Widget build(BuildContext context) { - return Consumer( - builder: (context, character, child) { - // If characters contains a character where character.key == current, - // then insert a copy of character at index 0 - current = character.key; - - var contains = false; - - for (var element in characters) { - if (element.key == current) { - contains = true; - break; - } - } - - if (!contains) { - characters.insert(0, character.copy()); - } - - return Scaffold( - appBar: AppBar( - backgroundColor: Theme.of(context).colorScheme.background, - foregroundColor: Theme.of(context).colorScheme.onPrimary, - elevation: 0.0, - leading: IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: const Text("Character Browser"), - actions: [ - IconButton( - icon: const Icon(Icons.add), - onPressed: () { + return Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.background, + foregroundColor: Theme.of(context).colorScheme.onPrimary, + elevation: 0.0, + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: const Text("Character Browser"), + actions: [ + IconButton( + icon: const Icon(Icons.add), + onPressed: () { + setState(() { + final newCharacter = Character(); + characters.add(newCharacter); + context.read().from(newCharacter); + }); + }, + ), + ], + ), + body: SessionBusyOverlay( + child: GridView.builder( + itemCount: characters.length, + itemBuilder: (context, index) { + return Padding( + padding: const EdgeInsets.all( + 8.0), // Adjust the padding value as needed + child: CharacterBrowserTile( + character: characters[index], + onDelete: () { setState(() { - final newCharacter = Character(); - characters.add(newCharacter); - character.from(newCharacter); + characters.removeAt(index); }); }, ), - ], - ), - body: SessionBusyOverlay( - child: ListView.builder( - itemCount: characters.length, - itemBuilder: (context, index) { - return Padding( - padding: const EdgeInsets.all( - 8.0), // Adjust the padding value as needed - child: CharacterBrowserTile( - character: characters[index], - onDelete: () { - setState(() { - characters.removeAt(index); - }); - }, - ), - ); - }, - ) - ) - ); - }, + ); + }, + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 200, + crossAxisSpacing: 8.0, + mainAxisSpacing: 8.0, + childAspectRatio: 0.75 + ) + ) + ) ); } } diff --git a/lib/ui/mobile/pages/character/character_customization_page.dart b/lib/ui/mobile/pages/character/character_customization_page.dart index 228bf020..948800e9 100644 --- a/lib/ui/mobile/pages/character/character_customization_page.dart +++ b/lib/ui/mobile/pages/character/character_customization_page.dart @@ -59,6 +59,22 @@ class _CharacterCustomizationPageState extends State SharedPreferences.getInstance().then((prefs) { prefs.setString("last_character", json.encode(character.toMap())); + + final String charactersJson = prefs.getString("characters") ?? '[]'; + final List charactersList = json.decode(charactersJson); + + List characters; + characters = charactersList.map((characterMap) { + return Character.fromMap(characterMap); + }).toList(); + + characters.removeWhere((listCharacter) => character.key == listCharacter.key); + characters.insert(0, character); + + final String newCharactersJson = + json.encode(characters.map((character) => character.toMap()).toList()); + + prefs.setString("characters", newCharactersJson); }); return SessionBusyOverlay( diff --git a/lib/ui/mobile/pages/settings_page.dart b/lib/ui/mobile/pages/settings_page.dart index 18e6cdef..d3c46bcd 100644 --- a/lib/ui/mobile/pages/settings_page.dart +++ b/lib/ui/mobile/pages/settings_page.dart @@ -26,12 +26,48 @@ class _SettingsPageState extends State { return SingleChildScrollView( child: Column( children: [ - SwitchListTile( - title: const Text('Theme (Light/Dark)'), - value: mainProvider.isDarkMode, - onChanged: (value) { - mainProvider.toggleTheme(); - }, + Padding( + padding: const EdgeInsets.all(8), + child: Row( + children: [ + const Expanded( + child: Text("Theme Mode"), + ), + DropdownMenu( + hintText: "Select Theme Mode", + inputDecorationTheme: InputDecorationTheme( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(30.0), + borderSide: BorderSide.none, + ), + fillColor: Theme.of(context).colorScheme.secondary, + filled: true, + floatingLabelBehavior: FloatingLabelBehavior.never, + ), + dropdownMenuEntries: const [ + DropdownMenuEntry( + value: ThemeMode.system, + label: "System", + ), + DropdownMenuEntry( + value: ThemeMode.light, + label: "Light", + ), + DropdownMenuEntry( + value: ThemeMode.dark, + label: "Dark", + ) + ], + onSelected: (ThemeMode? value) { + if (value != null) { + mainProvider.themeMode = value; + } + }, + initialSelection: mainProvider.themeMode, + width: 200, + ) + ], + ), ), FilledButton( onPressed: () { diff --git a/lib/ui/mobile/widgets/tiles/character_browser_tile.dart b/lib/ui/mobile/widgets/tiles/character_browser_tile.dart index a3cd01d1..f87b64c1 100644 --- a/lib/ui/mobile/widgets/tiles/character_browser_tile.dart +++ b/lib/ui/mobile/widgets/tiles/character_browser_tile.dart @@ -1,9 +1,8 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:maid/providers/character.dart'; -import 'package:maid/providers/user.dart'; import 'package:maid/static/logger.dart'; -import 'package:maid/static/utilities.dart'; -import 'package:maid_ui/maid_ui.dart'; import 'package:provider/provider.dart'; class CharacterBrowserTile extends StatefulWidget { @@ -18,47 +17,68 @@ class CharacterBrowserTile extends StatefulWidget { } class _CharacterBrowserTileState extends State { - bool selected = false; - @override Widget build(BuildContext context) { - return Consumer2( - builder: (context, character, user, child) { - selected = character.key == widget.character.key; - - return GestureDetector( - child: ListTile( - tileColor: Theme.of(context).colorScheme.primary, - selectedTileColor: - Theme.of(context).colorScheme.secondary.withOpacity(0.25), - textColor: Colors.white, - selectedColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10.0), - ), - // Square image with rounded corners of the character - leading: FutureTileImage( - image: widget.character.profile, - borderRadius: BorderRadius.circular(10), - ), - minLeadingWidth: 60, - title: Column(children: [ - Text(widget.character.name), - const SizedBox(height: 10.0), - Text( - Utilities.formatPlaceholders(widget.character.description, - user.name, widget.character.name), - style: const TextStyle(fontSize: 12.0), - ), - ]), - selected: selected), - onTap: () { - character.from(widget.character); - }, - onSecondaryTapUp: (details) => _onSecondaryTapUp(details, context), - onLongPressStart: (details) => _onLongPressStart(details, context), - ); + return GestureDetector( + child: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: FutureBuilder( + future: widget.character.profile, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + if (snapshot.hasError) { + return const Icon(Icons.error); + } else { + return Stack( + fit: StackFit.expand, + children: [ + Image.file( + snapshot.data!, + fit: BoxFit.cover, + ), + Align( + alignment: Alignment.bottomCenter, + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [Colors.black.withOpacity(0.8), Colors.transparent], + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.character.name, + style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold), + ), + Text( + widget.character.description, + maxLines: 3, + overflow: TextOverflow.ellipsis, + style: const TextStyle(color: Colors.white), + ), + ], + ), + ), + ), + ], + ); + } + } else { + return const CircularProgressIndicator(); + } + } + ) + ), + onTap: () { + context.read().from(widget.character); }, + onSecondaryTapUp: (details) => _onSecondaryTapUp(details, context), + onLongPressStart: (details) => _onLongPressStart(details, context), ); }