diff --git a/assets/content/android.md b/assets/content/android.md new file mode 100644 index 0000000..bbcdaff --- /dev/null +++ b/assets/content/android.md @@ -0,0 +1,36 @@ +You'll have to export your WhatsApp chat and import it into the app to generate messages based on it. + +**EVERYTHING IS DONE LOCALLY ON YOUR DEVICE** 🙃 + +## Open a chat in WhatsApp and open the menu on the top right + +![Open a Chat](resource:assets/images/android_1.webp) + +## Tap on More + +Select "More" from the menu. + +![Tap on Export Chat](resource:assets/images/android_2.webp) + +## Tap on Export chat + +Select "Export chat" from the options. + +![Tap on Export Chat](resource:assets/images/android_3.webp) + +## Without media + +There's no need to include media files, so tap on "Without media". + +![Without Media](resource:assets/images/android_4.webp) + +## Select "Artificial Stupidity" + +Select this app as the target for the exported chat. + +Now wait a few seconds (or minutes, depending on the chat size) +for the app to process the chat. + +--- + +### If you're not ready to export your chat yet, you can try the app with a sample chat. diff --git a/assets/content/example_chat.txt b/assets/content/example_chat.txt new file mode 100644 index 0000000..dcf1dfa --- /dev/null +++ b/assets/content/example_chat.txt @@ -0,0 +1,174 @@ +[] Leonardo da Vinci: Greetings, my friends. Shall we discuss the intricacies of invention today? +[] Cleopatra: Invention? Perhaps. But I would rather explore how power shapes the world and its people. +[] Socrates: Power and invention are but facets of wisdom, are they not? Let us question their true nature. +[] Leonardo da Vinci: Ah, Socrates, ever the philosopher. But tell me, does wisdom build machines that fly? +[] Cleopatra: Machines may fly, Leonardo, but do they inspire loyalty or control empires? That is true mastery. +[] Socrates: Loyalty without understanding is mere servitude, Cleopatra. What is an empire without reason? +[] Leonardo da Vinci: Yet reason can guide an empire to use inventions for the greater good. Imagine flight revolutionizing trade and diplomacy! +[] Cleopatra: And imagine it as a tool of war, Leonardo. A queen must consider all possibilities. +[] Socrates: Is war itself not a failure of reason, Cleopatra? What wisdom lies in conflict? +[] Leonardo da Vinci: Perhaps it is both—a failure and a catalyst for progress. Necessity often births invention. +[] Cleopatra: And ambition births necessity. Do you invent for survival, Leonardo, or for glory? +[] Socrates: A better question might be, does invention serve humanity, or enslave it to its desires? +[] Leonardo da Vinci: Both, I suspect. Yet my sketches of machines are but dreams. How can dreams enslave? +[] Cleopatra: Dreams can bind as tightly as chains, Leonardo. Consider the dream of eternal rule. +[] Socrates: Eternal rule is but an illusion, Cleopatra. No ruler can escape the questions of time and legacy. +[] Leonardo da Vinci: Legacy intrigues me. Shall we discuss what each of us hopes to leave behind? +[] Cleopatra: My Egypt shall remember me as a goddess, not merely a queen. Power and beauty immortalized. +[] Socrates: I seek no monuments. A life of questioning, of challenging assumptions, is legacy enough. +[] Leonardo da Vinci: And I? I hope the world remembers the beauty of curiosity, expressed through my art and designs. +[] Cleopatra: Art and questions are luxuries. A ruler must focus on survival and strategy. +[] Socrates: Luxuries, Cleopatra? Or the very essence of what makes life worth living? +[] Leonardo da Vinci: Art and questions can inspire rulers and subjects alike. They are tools of unity, not division. +[] Cleopatra: Unity through beauty? Perhaps. But can beauty protect an empire from invaders? +[] Socrates: Protection lies in the mind, not the sword. Reason is the greatest shield. +[] Leonardo da Vinci: Reason might guide the hand that wields the sword, or the pen that signs peace. +[] Cleopatra: Peace is fleeting. A ruler must be ever vigilant, for betrayal comes swiftly. +[] Socrates: Vigilance without trust leads to tyranny, Cleopatra. What kind of rule do you desire? +[] Cleopatra: One where I control my destiny, Socrates. Is that not every ruler's dream? +[] Leonardo da Vinci: Control is fragile, Cleopatra. Even the finest machines can falter. +[] Socrates: Machines, empires, rulers—all must face the ultimate question: what is their purpose? +[] Leonardo da Vinci: My purpose is to reveal the mysteries of the natural world and inspire wonder. +[] Cleopatra: My purpose is to lead with strength and ensure the prosperity of my people. +[] Socrates: And mine is to ask why we seek purpose at all. Does it not change with perspective? +[] Leonardo da Vinci: Perspective transforms everything. It is the foundation of my art, after all. +[] Cleopatra: Art and philosophy are mirrors of power. They reflect, but they do not shape. +[] Socrates: Power shapes only the surface, Cleopatra. True change comes from within. +[] Leonardo da Vinci: Inner change can lead to outer marvels. Imagine a world shaped by shared wisdom and invention! +[] Cleopatra: A noble dream, Leonardo. But dreams do not rule. Strength and strategy do. +[] Socrates: Strength fades. Strategy falters. Only wisdom endures. Shall we continue to seek it together? +[] Leonardo da Vinci: Together, always. For what is life without discourse among curious minds? +[] Cleopatra: Agreed. Let us combine strength, curiosity, and wisdom to leave an indelible mark. +[] Socrates: Then let us question, create, and lead with purpose. The journey itself is our legacy. +[] Leonardo da Vinci: A splendid conclusion, Socrates. Now, shall I sketch a flying chariot for us to explore the heavens? +[] Cleopatra: Sketch away, Leonardo. But remember, even in the heavens, power reigns supreme. +[] Socrates: Power, Cleopatra, is but another question waiting to be unraveled. Onward, my friends. +[] Leonardo da Vinci: Onward, indeed! But let us first debate whether the heavens themselves are infinite or finite. +[] Cleopatra: Finite or infinite, Leonardo, the stars serve as witnesses to the rise and fall of empires. +[] Socrates: Witnesses, perhaps. But do stars care for empires? Or are they indifferent to our struggles? +[] Leonardo da Vinci: Indifferent they may be, Socrates, but their light inspires. A machine to reach them, perhaps? +[] Cleopatra: Machines, stars, empires—it seems we are drawn to what lies beyond our grasp. +[] Socrates: And why not, Cleopatra? To seek is to live. To grasp the unreachable is the essence of humanity. +[] Leonardo da Vinci: To grasp, Socrates, or merely to understand? I wonder if our curiosity has limits. +[] Cleopatra: Curiosity is a luxury for those not consumed by the burden of rule, Leonardo. +[] Socrates: And yet, Cleopatra, even a ruler must question their own power. Is it truly theirs, or a fleeting illusion? +[] Leonardo da Vinci: Illusions can inspire reality. My flying machines are sketches now, but one day... who knows? +[] Cleopatra: One day, perhaps. But will they serve a ruler or defy one? +[] Socrates: Defiance and servitude are two sides of the same coin. Do we not all serve something greater? +[] Leonardo da Vinci: Greater, indeed. The pursuit of knowledge, beauty, and innovation drives me. +[] Cleopatra: And power drives me. Yet, Socrates, what drives you to ask so many questions? +[] Socrates: Questions are the threads of wisdom. To pull at them is to unravel ignorance. +[] Leonardo da Vinci: Ignorance and curiosity—a paradoxical pair. Without one, the other cannot exist. +[] Cleopatra: A paradox fit for philosophers. But tell me, Socrates, would you trade wisdom for power? +[] Socrates: Never, Cleopatra. Wisdom is the only power that endures beyond the grave. +[] Leonardo da Vinci: Beyond the grave—another mystery to explore. Shall we design a device to pierce that veil? +[] Cleopatra: Death is no mystery. It is a certainty. What matters is how we shape the moments before it. +[] Socrates: And how we question those moments, Cleopatra. The unexamined life is not worth living. +[] Leonardo da Vinci: But is the unsketched idea worth imagining? I must put pen to paper at once! +[] Cleopatra: Sketch your dreams, Leonardo. But know that rulers must act while dreamers ponder. +[] Socrates: Yet action without thought is folly, Cleopatra. The balance is the challenge. +[] Leonardo da Vinci: Balance, indeed. Between art and science, power and wisdom, the seen and unseen. +[] Cleopatra: Balance is a mirage. A ruler’s path is to tip the scales in their favor. +[] Socrates: And a philosopher’s path is to tip them back, Cleopatra. Shall we continue this dance of ideas? +[] Leonardo da Vinci: A dance! Perhaps I should invent a mechanical dancer to illustrate our discourse. +[] Cleopatra: If it pleases you, Leonardo. But no machine could rival the elegance of a true queen. +[] Socrates: Elegance is but a shadow of truth. Let us seek what lies behind it. +[] Leonardo da Vinci: Shadows and light—my art thrives on both. Perhaps philosophy does as well? +[] Cleopatra: Philosophy thrives on words, not deeds. A ruler cannot afford such indulgences. +[] Socrates: Words, Cleopatra, can move mountains, though they wield no sword. +[] Leonardo da Vinci: And swords, though sharp, cannot draw the beauty of a mountain. +[] Cleopatra: Beauty, Leonardo, is a tool of persuasion. It wields power in its own right. +[] Socrates: Persuasion without wisdom is manipulation. Shall we not strive for the former? +[] Leonardo da Vinci: Strive, always. But let us also marvel at the journey, not merely the destination. +[] Cleopatra: The journey is irrelevant if the destination is lost. A ruler must focus. +[] Socrates: Focus on what, Cleopatra? The means or the end? +[] Leonardo da Vinci: The end inspires the means. Like my inventions, their purpose shapes their creation. +[] Cleopatra: And what purpose guides you, Leonardo? Glory, knowledge, or something else? +[] Socrates: A question for all of us. What purpose do we serve, and does it serve us in return? +[] Leonardo da Vinci: Purpose is as fluid as paint on a canvas. It takes shape with time. +[] Cleopatra: Time is a ruler’s enemy. One must conquer it or be consumed by it. +[] Socrates: Conquer time? Perhaps. Or perhaps we must simply live within it, questioning all the while. +[] Leonardo da Vinci: While questioning, let us not forget to imagine. Without imagination, where is progress? +[] Cleopatra: Imagination must serve power, Leonardo. Otherwise, it is a wasted luxury. +[] Socrates: And power without imagination is stagnation. Shall we continue, friends? +[] Leonardo da Vinci: Continue forever, if the stars allow. I’ll sketch a machine to measure eternity next! +[] Cleopatra: Eternity belongs to rulers and poets. But only if they seize it. +[] Socrates: Seize it, or understand it. Which is the truer path? +[] Leonardo da Vinci: The stars may be indifferent, but their light guides our inventions. +[] Cleopatra: Inventions are tools of power, Leonardo. They must serve the ruler's will. +[] Socrates: Will without wisdom is dangerous, Cleopatra. What guides your will? +[] Leonardo da Vinci: My will is guided by curiosity and the pursuit of beauty. +[] Cleopatra: Beauty is fleeting, Leonardo. Power must be eternal. +[] Socrates: Eternity is an illusion, Cleopatra. Wisdom seeks to understand the present. +[] Leonardo da Vinci: The present is but a moment. My art captures its essence. +[] Cleopatra: Essence is power, Leonardo. A ruler must harness it. +[] Socrates: Harnessing power without understanding leads to tyranny. +[] Leonardo da Vinci: Tyranny stifles creativity. My inventions thrive on freedom. +[] Cleopatra: Freedom is a luxury, Leonardo. A ruler must prioritize control. +[] Socrates: Control is fragile, Cleopatra. Wisdom seeks balance. +[] Leonardo da Vinci: Balance inspires my designs. It is the harmony of art and science. +[] Cleopatra: Harmony is a tool of persuasion. It strengthens a ruler's grip. +[] Socrates: Persuasion without truth is manipulation. Wisdom seeks the former. +[] Leonardo da Vinci: Truth is the foundation of my sketches. It guides my hand. +[] Cleopatra: A ruler's hand must be firm, Leonardo. Power demands it. +[] Socrates: Firmness without flexibility breaks. Wisdom seeks resilience. +[] Leonardo da Vinci: Resilience is the heart of invention. It adapts and evolves. +[] Cleopatra: Evolution is necessary for survival. A ruler must embrace it. +[] Socrates: Survival without purpose is empty. Wisdom seeks meaning. +[] Leonardo da Vinci: Meaning is found in creation. My art reflects it. +[] Cleopatra: Creation serves power, Leonardo. It must be directed. +[] Socrates: Direction without questioning is blind. Wisdom seeks clarity. +[] Leonardo da Vinci: Clarity illuminates my work. It reveals hidden beauty. +[] Cleopatra: Beauty must serve the ruler's vision. It is a tool of influence. +[] Socrates: Influence without integrity is hollow. Wisdom seeks authenticity. +[] Leonardo da Vinci: Authenticity breathes life into my inventions. It is their soul. +[] Cleopatra: A ruler's soul must be strong, Leonardo. Power requires it. +[] Socrates: Strength without wisdom is reckless. Wisdom seeks understanding. +[] Leonardo da Vinci: Understanding fuels my curiosity. It drives my creations. +[] Cleopatra: Curiosity must be tempered by strategy. A ruler's path demands it. +[] Socrates: Strategy without wisdom is shortsighted. Wisdom seeks foresight. +[] Leonardo da Vinci: Foresight shapes my designs. It anticipates the future. +[] Cleopatra: The future belongs to the powerful, Leonardo. A ruler must seize it. +[] Socrates: Seizing the future without wisdom is folly. Wisdom seeks enlightenment. +[] Leonardo da Vinci: Enlightenment is the goal of my art. It inspires wonder. +[] Cleopatra: Wonder must be harnessed, Leonardo. It is a tool of power. +[] Socrates: Power without wisdom is dangerous. Wisdom seeks balance. +[] Leonardo da Vinci: The stars may be indifferent, but their light guides our inventions. +[] Cleopatra: Inventions are tools of power, Leonardo. They must serve the ruler's will. +[] Socrates: Will without wisdom is dangerous, Cleopatra. What guides your will? +[] Leonardo da Vinci: My will is guided by curiosity and the pursuit of beauty. +[] Cleopatra: Beauty is fleeting, Leonardo. Power must be eternal. +[] Socrates: Eternity is an illusion, Cleopatra. Wisdom seeks to understand the present. +[] Leonardo da Vinci: The present is but a moment. My art captures its essence. +[] Cleopatra: Essence is power, Leonardo. A ruler must harness it. +[] Socrates: Harnessing power without understanding leads to tyranny. +[] Leonardo da Vinci: Tyranny stifles creativity. My inventions thrive on freedom. +[] Cleopatra: Freedom is a luxury, Leonardo. A ruler must prioritize control. +[] Socrates: Control is fragile, Cleopatra. Wisdom seeks balance. +[] Leonardo da Vinci: Balance inspires my designs. It is the harmony of art and science. +[] Cleopatra: Harmony is a tool of persuasion. It strengthens a ruler's grip. +[] Socrates: Persuasion without truth is manipulation. Wisdom seeks the former. +[] Leonardo da Vinci: Truth is the foundation of my sketches. It guides my hand. +[] Cleopatra: A ruler's hand must be firm, Leonardo. Power demands it. +[] Socrates: Firmness without flexibility breaks. Wisdom seeks resilience. +[] Leonardo da Vinci: Resilience is the heart of invention. It adapts and evolves. +[] Cleopatra: Evolution is necessary for survival. A ruler must embrace it. +[] Socrates: Survival without purpose is empty. Wisdom seeks meaning. +[] Leonardo da Vinci: Meaning is found in creation. My art reflects it. +[] Cleopatra: Creation serves power, Leonardo. It must be directed. +[] Socrates: Direction without questioning is blind. Wisdom seeks clarity. +[] Leonardo da Vinci: Clarity illuminates my work. It reveals hidden beauty. +[] Cleopatra: Beauty must serve the ruler's vision. It is a tool of influence. +[] Socrates: Influence without integrity is hollow. Wisdom seeks authenticity. +[] Leonardo da Vinci: Authenticity breathes life into my inventions. It is their soul. +[] Cleopatra: A ruler's soul must be strong, Leonardo. Power requires it. +[] Socrates: Strength without wisdom is reckless. Wisdom seeks understanding. +[] Leonardo da Vinci: Understanding fuels my curiosity. It drives my creations. +[] Cleopatra: Curiosity must be tempered by strategy. A ruler's path demands it. +[] Socrates: Strategy without wisdom is shortsighted. Wisdom seeks foresight. +[] Leonardo da Vinci: Foresight shapes my designs. It anticipates the future. +[] Cleopatra: The future belongs to the powerful, Leonardo. A ruler must seize it. +[] Socrates: Seizing the future without wisdom is folly. Wisdom seeks enlightenment. +[] Leonardo da Vinci: Enlightenment is the goal of my art. It inspires wonder. +[] Cleopatra: Wonder must be harnessed, Leonardo. It is a tool of power. +[] Socrates: Power without wisdom is dangerous. Wisdom seeks balance. diff --git a/assets/content/ios.md b/assets/content/ios.md index d99d99a..73f02d7 100644 --- a/assets/content/ios.md +++ b/assets/content/ios.md @@ -34,3 +34,6 @@ Now wait a few seconds (or minutes, depending on the chat size) for the app to process the chat. +--- + +### If you're not ready to export your chat yet, you can try the app with a sample chat. diff --git a/assets/images/android_1.webp b/assets/images/android_1.webp new file mode 100644 index 0000000..40f3e51 Binary files /dev/null and b/assets/images/android_1.webp differ diff --git a/assets/images/android_2.webp b/assets/images/android_2.webp new file mode 100644 index 0000000..bd8f219 Binary files /dev/null and b/assets/images/android_2.webp differ diff --git a/assets/images/android_3.webp b/assets/images/android_3.webp new file mode 100644 index 0000000..ca93977 Binary files /dev/null and b/assets/images/android_3.webp differ diff --git a/assets/images/android_4.webp b/assets/images/android_4.webp new file mode 100644 index 0000000..0c7c36e Binary files /dev/null and b/assets/images/android_4.webp differ diff --git a/lib/app/bloc/app_bloc.dart b/lib/app/bloc/app_bloc.dart index a3a429a..0ef0426 100644 --- a/lib/app/bloc/app_bloc.dart +++ b/lib/app/bloc/app_bloc.dart @@ -8,6 +8,7 @@ import 'package:artificialstupidity/home/home.dart'; import 'package:collection/collection.dart'; import 'package:crypto/crypto.dart'; import 'package:flutter/foundation.dart'; // ignore: unused_import +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:markov/markov.dart'; @@ -28,6 +29,7 @@ class AppBloc extends Bloc { super(const AppState.initial()) { on(_onIntentSubscriptionRequested); on(_onReceivedFiles); + on(_onLoadExample); } @override @@ -67,43 +69,31 @@ class AppBloc extends Bloc { () async { for (final mediaFile in event.sharedMediaFiles) { final chatFile = await _getChatFromSharedMediaFile(mediaFile); + return _generateChainModelFromChat(chatFile); + } + }, + ); - final id = sha256.convert(utf8.encode(chatFile)).toString(); - - // TODO Should probablu split here: if the ID already exists, we should - // not generate the chain model again. - - final parser = WhatsAppParser(); - final messages = parser.readString(chatFile); - - final messagesBySender = messages - .whereType() - .groupListsBy((message) => message.sender) - ..removeWhere((sender, messages) => messages.length < 10); - - // TODO This should be notified to the user, as it may be a mistake. - // Maybe it should be configurable. + if (chainModel == null) { + emit(const AppState.failedToGenerateChainModel()); + return; + } - final markovChainBySender = { - for (final sender in messagesBySender.keys) - sender: MarkovChainGenerator(2) - }; + emit(AppState.generatedChainModel(chainModel: chainModel)); + } - // We need to wait for all the streams to be processed before closing - await Future.wait([ - for (final entry in markovChainBySender.entries) - entry.value.addStream(Stream.fromIterable( - messagesBySender[entry.key]!.map((message) => message.content), - )) - ]); + Future _onLoadExample( + AppLoadExample event, + Emitter emit, + ) async { + emit(AppState.processingFiles(sharedMediaFiles: [])); - final markovChains = { - for (final entry in markovChainBySender.entries) - entry.key: await entry.value.close() - }; + final chatFile = + await rootBundle.loadString('assets/content/example_chat.txt'); - return ChainModel(id: id, markovChains: markovChains); - } + final chainModel = await Isolate.run( + () async { + return _generateChainModelFromChat(chatFile); }, ); @@ -116,6 +106,43 @@ class AppBloc extends Bloc { } } +Future _generateChainModelFromChat(String chatFile) async { + final id = sha256.convert(utf8.encode(chatFile)).toString(); + + // TODO Should probablu split here: if the ID already exists, we should + // not generate the chain model again. + + final parser = WhatsAppParser(); + final messages = parser.readString(chatFile); + + final messagesBySender = messages + .whereType() + .groupListsBy((message) => message.sender) + ..removeWhere((sender, messages) => messages.length < 10); + + // TODO This should be notified to the user, as it may be a mistake. + // Maybe it should be configurable. + + final markovChainBySender = { + for (final sender in messagesBySender.keys) sender: MarkovChainGenerator(2) + }; + + // We need to wait for all the streams to be processed before closing + await Future.wait([ + for (final entry in markovChainBySender.entries) + entry.value.addStream(Stream.fromIterable( + messagesBySender[entry.key]!.map((message) => message.content), + )) + ]); + + final markovChains = { + for (final entry in markovChainBySender.entries) + entry.key: await entry.value.close() + }; + + return ChainModel(id: id, markovChains: markovChains); +} + Future _getChatFromSharedMediaFile(SharedMediaFile mediaFile) async { if (mediaFile.mimeType == 'application/zip') { final inputStream = InputFileStream(mediaFile.path); diff --git a/lib/app/bloc/app_bloc.freezed.dart b/lib/app/bloc/app_bloc.freezed.dart index 9c5f1b5..5ffd6d9 100644 --- a/lib/app/bloc/app_bloc.freezed.dart +++ b/lib/app/bloc/app_bloc.freezed.dart @@ -21,18 +21,21 @@ mixin _$AppEvent { required TResult Function() intentSubscriptionRequested, required TResult Function(List sharedMediaFiles) receivedFiles, + required TResult Function() loadExample, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult? whenOrNull({ TResult? Function()? intentSubscriptionRequested, TResult? Function(List sharedMediaFiles)? receivedFiles, + TResult? Function()? loadExample, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult maybeWhen({ TResult Function()? intentSubscriptionRequested, TResult Function(List sharedMediaFiles)? receivedFiles, + TResult Function()? loadExample, required TResult orElse(), }) => throw _privateConstructorUsedError; @@ -41,6 +44,7 @@ mixin _$AppEvent { required TResult Function(AppIntentSubscriptionRequested value) intentSubscriptionRequested, required TResult Function(AppReceivedFiles value) receivedFiles, + required TResult Function(AppLoadExample value) loadExample, }) => throw _privateConstructorUsedError; @optionalTypeArgs @@ -48,6 +52,7 @@ mixin _$AppEvent { TResult? Function(AppIntentSubscriptionRequested value)? intentSubscriptionRequested, TResult? Function(AppReceivedFiles value)? receivedFiles, + TResult? Function(AppLoadExample value)? loadExample, }) => throw _privateConstructorUsedError; @optionalTypeArgs @@ -55,6 +60,7 @@ mixin _$AppEvent { TResult Function(AppIntentSubscriptionRequested value)? intentSubscriptionRequested, TResult Function(AppReceivedFiles value)? receivedFiles, + TResult Function(AppLoadExample value)? loadExample, required TResult orElse(), }) => throw _privateConstructorUsedError; @@ -136,6 +142,7 @@ class _$AppIntentSubscriptionRequestedImpl required TResult Function() intentSubscriptionRequested, required TResult Function(List sharedMediaFiles) receivedFiles, + required TResult Function() loadExample, }) { return intentSubscriptionRequested(); } @@ -145,6 +152,7 @@ class _$AppIntentSubscriptionRequestedImpl TResult? whenOrNull({ TResult? Function()? intentSubscriptionRequested, TResult? Function(List sharedMediaFiles)? receivedFiles, + TResult? Function()? loadExample, }) { return intentSubscriptionRequested?.call(); } @@ -154,6 +162,7 @@ class _$AppIntentSubscriptionRequestedImpl TResult maybeWhen({ TResult Function()? intentSubscriptionRequested, TResult Function(List sharedMediaFiles)? receivedFiles, + TResult Function()? loadExample, required TResult orElse(), }) { if (intentSubscriptionRequested != null) { @@ -168,6 +177,7 @@ class _$AppIntentSubscriptionRequestedImpl required TResult Function(AppIntentSubscriptionRequested value) intentSubscriptionRequested, required TResult Function(AppReceivedFiles value) receivedFiles, + required TResult Function(AppLoadExample value) loadExample, }) { return intentSubscriptionRequested(this); } @@ -178,6 +188,7 @@ class _$AppIntentSubscriptionRequestedImpl TResult? Function(AppIntentSubscriptionRequested value)? intentSubscriptionRequested, TResult? Function(AppReceivedFiles value)? receivedFiles, + TResult? Function(AppLoadExample value)? loadExample, }) { return intentSubscriptionRequested?.call(this); } @@ -188,6 +199,7 @@ class _$AppIntentSubscriptionRequestedImpl TResult Function(AppIntentSubscriptionRequested value)? intentSubscriptionRequested, TResult Function(AppReceivedFiles value)? receivedFiles, + TResult Function(AppLoadExample value)? loadExample, required TResult orElse(), }) { if (intentSubscriptionRequested != null) { @@ -294,6 +306,7 @@ class _$AppReceivedFilesImpl required TResult Function() intentSubscriptionRequested, required TResult Function(List sharedMediaFiles) receivedFiles, + required TResult Function() loadExample, }) { return receivedFiles(sharedMediaFiles); } @@ -303,6 +316,7 @@ class _$AppReceivedFilesImpl TResult? whenOrNull({ TResult? Function()? intentSubscriptionRequested, TResult? Function(List sharedMediaFiles)? receivedFiles, + TResult? Function()? loadExample, }) { return receivedFiles?.call(sharedMediaFiles); } @@ -312,6 +326,7 @@ class _$AppReceivedFilesImpl TResult maybeWhen({ TResult Function()? intentSubscriptionRequested, TResult Function(List sharedMediaFiles)? receivedFiles, + TResult Function()? loadExample, required TResult orElse(), }) { if (receivedFiles != null) { @@ -326,6 +341,7 @@ class _$AppReceivedFilesImpl required TResult Function(AppIntentSubscriptionRequested value) intentSubscriptionRequested, required TResult Function(AppReceivedFiles value) receivedFiles, + required TResult Function(AppLoadExample value) loadExample, }) { return receivedFiles(this); } @@ -336,6 +352,7 @@ class _$AppReceivedFilesImpl TResult? Function(AppIntentSubscriptionRequested value)? intentSubscriptionRequested, TResult? Function(AppReceivedFiles value)? receivedFiles, + TResult? Function(AppLoadExample value)? loadExample, }) { return receivedFiles?.call(this); } @@ -346,6 +363,7 @@ class _$AppReceivedFilesImpl TResult Function(AppIntentSubscriptionRequested value)? intentSubscriptionRequested, TResult Function(AppReceivedFiles value)? receivedFiles, + TResult Function(AppLoadExample value)? loadExample, required TResult orElse(), }) { if (receivedFiles != null) { @@ -369,6 +387,129 @@ abstract class AppReceivedFiles implements AppEvent { throw _privateConstructorUsedError; } +/// @nodoc +abstract class _$$AppLoadExampleImplCopyWith<$Res> { + factory _$$AppLoadExampleImplCopyWith(_$AppLoadExampleImpl value, + $Res Function(_$AppLoadExampleImpl) then) = + __$$AppLoadExampleImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$AppLoadExampleImplCopyWithImpl<$Res> + extends _$AppEventCopyWithImpl<$Res, _$AppLoadExampleImpl> + implements _$$AppLoadExampleImplCopyWith<$Res> { + __$$AppLoadExampleImplCopyWithImpl( + _$AppLoadExampleImpl _value, $Res Function(_$AppLoadExampleImpl) _then) + : super(_value, _then); + + /// Create a copy of AppEvent + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc + +class _$AppLoadExampleImpl + with DiagnosticableTreeMixin + implements AppLoadExample { + const _$AppLoadExampleImpl(); + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'AppEvent.loadExample()'; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('type', 'AppEvent.loadExample')); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$AppLoadExampleImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() intentSubscriptionRequested, + required TResult Function(List sharedMediaFiles) + receivedFiles, + required TResult Function() loadExample, + }) { + return loadExample(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? intentSubscriptionRequested, + TResult? Function(List sharedMediaFiles)? receivedFiles, + TResult? Function()? loadExample, + }) { + return loadExample?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? intentSubscriptionRequested, + TResult Function(List sharedMediaFiles)? receivedFiles, + TResult Function()? loadExample, + required TResult orElse(), + }) { + if (loadExample != null) { + return loadExample(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(AppIntentSubscriptionRequested value) + intentSubscriptionRequested, + required TResult Function(AppReceivedFiles value) receivedFiles, + required TResult Function(AppLoadExample value) loadExample, + }) { + return loadExample(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(AppIntentSubscriptionRequested value)? + intentSubscriptionRequested, + TResult? Function(AppReceivedFiles value)? receivedFiles, + TResult? Function(AppLoadExample value)? loadExample, + }) { + return loadExample?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(AppIntentSubscriptionRequested value)? + intentSubscriptionRequested, + TResult Function(AppReceivedFiles value)? receivedFiles, + TResult Function(AppLoadExample value)? loadExample, + required TResult orElse(), + }) { + if (loadExample != null) { + return loadExample(this); + } + return orElse(); + } +} + +abstract class AppLoadExample implements AppEvent { + const factory AppLoadExample() = _$AppLoadExampleImpl; +} + /// @nodoc mixin _$AppState { @optionalTypeArgs diff --git a/lib/app/bloc/app_event.dart b/lib/app/bloc/app_event.dart index b901ea9..6cfc8a7 100644 --- a/lib/app/bloc/app_event.dart +++ b/lib/app/bloc/app_event.dart @@ -8,4 +8,6 @@ sealed class AppEvent with _$AppEvent { const factory AppEvent.receivedFiles({ required List sharedMediaFiles, }) = AppReceivedFiles; + + const factory AppEvent.loadExample() = AppLoadExample; } diff --git a/lib/chat/view/chat.dart b/lib/chat/view/chat.dart index 7f6d56a..cf2617d 100644 --- a/lib/chat/view/chat.dart +++ b/lib/chat/view/chat.dart @@ -29,20 +29,14 @@ class ChatView extends StatelessWidget { slivers: [ SliverAppBar( title: Text('Chat'), - floating: true, - snap: true, - actions: [ - IconButton( - icon: const Icon(Icons.refresh), - onPressed: () {}, - ), - ], + pinned: true, + expandedHeight: 200, ), BlocBuilder(builder: (context, state) { if (state is ChatLoadedMessages) { return SliverList( delegate: SliverChildBuilderDelegate( - (context, index) => + (context, index) => ChatMessageBubble(message: state.messages[index]), childCount: state.messages.length, ), @@ -79,11 +73,9 @@ class ChatMessageBubble extends StatelessWidget { @override Widget build(BuildContext context) { - final textTheme = Theme - .of(context) - .textTheme; + final textTheme = Theme.of(context).textTheme; final userColor = getColorFromHashCode(message.sender); - final cardColor = HSLColor.fromColor(userColor).withLightness(0.95); + final cardColor = HSLColor.fromColor(userColor).withAlpha(0.2); return Padding( padding: EdgeInsets.all(4), diff --git a/lib/home/view/home.dart b/lib/home/view/home.dart index 87a56da..319915c 100644 --- a/lib/home/view/home.dart +++ b/lib/home/view/home.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:artificialstupidity/app/app.dart'; import 'package:artificialstupidity/chat/chat.dart'; import 'package:artificialstupidity/home/home.dart'; @@ -106,13 +108,36 @@ class HomePage extends StatelessWidget { ); } + // TODO: to support web or other platforms, we need to let the user + // select the platform + final explanationFile = Platform.isAndroid + ? 'assets/content/android.md' + : 'assets/content/ios.md'; + return FutureBuilder( // TODO: Android version - future: rootBundle.loadString('assets/content/ios.md'), + future: rootBundle.loadString(explanationFile), builder: (context, snapshot) { if (snapshot.hasData && snapshot.data is String) { - return Markdown( - data: snapshot.data!, + return ListView( + padding: EdgeInsets.all(16), + children: [ + MarkdownBody( + data: snapshot.data!, + selectable: true, + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 16), + child: FilledButton.icon( + onPressed: () { + BlocProvider.of(context) + .add(AppLoadExample()); + }, + label: Text(l10n.loadExample), + icon: Icon(Icons.abc), + ), + ), + ], ); } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 5974de4..ac4abe0 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1,5 +1,6 @@ { "addNewChat": "Import chat", "appName": "Artificial Stupidity", - "deleteChat": "Delete chat" + "deleteChat": "Delete chat", + "loadExample": "Load example" } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index b92654f..4df36ee 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1,4 +1,5 @@ { "addNewChat": "Importa chat", - "deleteChat": "Elimina chat" + "deleteChat": "Elimina chat", + "loadExample": "Carica esempio" } diff --git a/lib/util/colors.dart b/lib/util/colors.dart index 99d6672..bf3af91 100644 --- a/lib/util/colors.dart +++ b/lib/util/colors.dart @@ -2,7 +2,7 @@ import 'package:flutter/painting.dart'; /// Returns a color based on the hash code of the given object. Color getColorFromHashCode(Object object) => - HSLColor.fromAHSL(1, object.hashCode % 360, 0.2, 0.5).toColor(); + HSLColor.fromAHSL(1, object.hashCode % 360, 0.8, 0.5).toColor(); /// Returns a color based on the given index and total. Color getEquidistantColor(int index, int total) {