gemini/sample-apps/photo-discovery/app/lib/ui/screens/quick_id.dart (394 lines of code) (raw):
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:firebase_vertexai/firebase_vertexai.dart';
import 'package:image_picker/image_picker.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:go_router/go_router.dart';
import 'package:image_gallery_saver/image_gallery_saver.dart';
import 'package:permission_handler/permission_handler.dart';
import '../components/adaptive_helper_widgets.dart';
import '../../models/metadata.dart';
import '../components/core_components.dart';
import '../../functionality/state.dart';
import '../../functionality/adaptive/policies.dart';
import '../screens/chat.dart';
import '../utilities.dart';
import '../../config.dart';
class GenerateMetadataScreen extends StatefulWidget {
const GenerateMetadataScreen({super.key});
@override
State<GenerateMetadataScreen> createState() => _GenerateMetadataScreenState();
}
class _GenerateMetadataScreenState extends State<GenerateMetadataScreen> {
late final GenerativeModel model;
bool _loading = false;
Uint8List? _image;
double chatWidth = 0;
@override
void initState() {
super.initState();
model = FirebaseVertexAI.instance.generativeModel(
model: geminiModel,
generationConfig: GenerationConfig(
temperature: 0,
responseMimeType: 'application/json',
),
);
}
@override
void didChangeDependencies() {
if (Policy.shouldTakePicture) {
requestCameraPermissions();
}
super.didChangeDependencies();
}
void requestCameraPermissions() async {
// Request multiple permissions at once.
await [Permission.camera, Permission.photos].request();
}
void pickImage(ImageSource source) async {
try {
final pickedImage = await ImagePicker().pickImage(source: source);
if (pickedImage == null) {
return;
}
var fileBytes = await pickedImage.readAsBytes();
if (source == ImageSource.camera &&
await Permission.mediaLibrary.request().isGranted) {
ImageGallerySaver.saveImage(
fileBytes,
quality: 100,
name: pickedImage.name,
);
}
setState(() {
_image = fileBytes;
});
_sendVertexMessage();
} catch (e) {
_showError(e.toString());
}
}
void removeImage(BuildContext context) {
setState(() {
_image = null;
context.read<AppState>().clearMetadata();
});
}
@override
Widget build(BuildContext context) {
Metadata? metadata = context.watch<AppState>().metadata;
final isExpanded = MediaQuery.sizeOf(context).width >= Breakpoints.expanded;
// Ask for a photo from the user if they haven't provided one.
if (_image == null) {
return PhotoSelectionScreen(
onTakePicture: () => pickImage(ImageSource.camera),
onSelectPicture: () => pickImage(ImageSource.gallery),
);
}
// Build main content screen with image, metadata, etc.
return (isExpanded)
// Build horizontal row layout for wide devices
? ExpandedScreen(
image: _image!,
loading: _loading,
metadata: metadata,
onRemoveImage: () => removeImage(context),
)
// Build vertical column layout for small devices
: CompactScreen(
image: _image!,
loading: _loading,
metadata: metadata,
onRemoveImage: () => removeImage(context),
);
}
Future<void> _sendVertexMessage() async {
if (_loading == true || _image == null) {
return;
}
setState(() {
_loading = true;
});
try {
final messageContents = Content.multi(
[
TextPart(
'What is the subject in this photo? Provide the name of the photo subject, and description as specific as possible, and 3 suggested questions that I can ask for more information about this object. Answer in JSON format with the keys "name", "description" and "suggestedQuestions".'),
DataPart('image/jpeg', _image!),
],
);
var response = await model.generateContent([messageContents]);
var text = response.text;
if (text == null) {
_showError('No response from API.');
return;
} else {
var jsonMap = json.decode(text);
if (mounted) {
context.read<AppState>().updateMetadata(Metadata.fromJson(jsonMap));
}
}
} catch (e) {
_showError(e.toString());
} finally {
setState(() {
_loading = false;
});
}
}
void _showError(String message) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Something went wrong'),
content: SingleChildScrollView(
child: Text(message),
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('OK'),
)
],
);
},
);
}
}
class CompactScreen extends StatelessWidget {
const CompactScreen(
{required this.image,
required this.loading,
required this.metadata,
required this.onRemoveImage,
super.key});
final Uint8List image;
final bool loading;
final Metadata? metadata;
final VoidCallback onRemoveImage;
void goToChat(BuildContext context) {
if (loading) return;
context.go('/chat');
}
@override
Widget build(BuildContext context) {
Widget content = LayoutBuilder(
builder: (context, constraints) {
return ListView(
children: [
Image.memory(image),
const SizedBox.square(dimension: 16),
Padding(
padding: const EdgeInsets.all(8),
child: MetadataCard(
loading: loading,
metadata: metadata,
),
),
const SizedBox.square(dimension: 8),
Row(mainAxisAlignment: MainAxisAlignment.center, children: [
RemoveImageButton(
onPressed: onRemoveImage,
),
const SizedBox.square(
dimension: 8,
),
TellMeMoreButton(
onPressed: () => goToChat(context),
)
]),
const SizedBox.square(dimension: 24),
],
);
},
);
if (Policy.shouldHaveKeyboardShortcuts) {
content = ShortcutHelper(
bindings: <ShortcutActivator, VoidCallback>{
const SingleActivator(control: true, LogicalKeyboardKey.keyT): () {
goToChat(context);
},
},
child: content,
);
}
return content;
}
}
class ExpandedScreen extends StatelessWidget {
ExpandedScreen(
{required this.image,
required this.loading,
required this.metadata,
required this.onRemoveImage,
super.key});
final Uint8List image;
final bool loading;
final Metadata? metadata;
final VoidCallback onRemoveImage;
final OverlayPortalController _aiChatController = OverlayPortalController();
void showChat() {
if (loading) return;
_aiChatController.toggle();
}
@override
Widget build(BuildContext context) {
Widget content = LayoutBuilder(builder: (context, constraints) {
return Padding(
padding: const EdgeInsets.all(4.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ConstrainedBox(
constraints: BoxConstraints(maxWidth: constraints.maxWidth * .55),
child: Image.memory(image),
),
SizedBox.square(
dimension: constraints.maxWidth * .010,
),
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
ConstrainedBox(
constraints:
BoxConstraints(maxWidth: constraints.maxWidth * .4),
child: MetadataCard(
loading: loading,
metadata: metadata,
),
),
const SizedBox.square(dimension: 24),
Row(mainAxisAlignment: MainAxisAlignment.center, children: [
RemoveImageButton(
onPressed: onRemoveImage,
),
const SizedBox.square(
dimension: 8,
),
TellMeMoreButton(
onPressed: () => showChat(),
),
ChatPopUp(
opController: _aiChatController,
onToggleChat: () => showChat(),
),
]),
]),
],
),
);
});
if (Policy.shouldHaveKeyboardShortcuts) {
content = ShortcutHelper(
bindings: <ShortcutActivator, VoidCallback>{
const SingleActivator(control: true, LogicalKeyboardKey.keyT): () {
showChat();
},
},
child: content,
);
}
return content;
}
}
class RemoveImageButton extends StatelessWidget {
const RemoveImageButton({required this.onPressed, super.key});
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return TextButton.icon(
icon: Icon(
FontAwesomeIcons.trashCan,
color: Theme.of(context).colorScheme.error,
),
onPressed: onPressed,
label: Text(
'Remove image',
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
);
}
}
class TellMeMoreButton extends StatelessWidget {
const TellMeMoreButton({required this.onPressed, super.key});
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return ElevatedButton.icon(
style: const ButtonStyle(
padding: WidgetStatePropertyAll(
EdgeInsets.symmetric(horizontal: 36, vertical: 16),
),
),
onPressed: onPressed,
icon: const Icon(FontAwesomeIcons.solidMessage),
label: const Text(
'Tell me more',
style: TextStyle(
fontSize: 18,
),
),
);
}
}
class PhotoSelectionScreen extends StatelessWidget {
const PhotoSelectionScreen(
{required this.onTakePicture, required this.onSelectPicture, super.key});
final VoidCallback onTakePicture;
final VoidCallback onSelectPicture;
@override
Widget build(BuildContext context) {
return Column(mainAxisAlignment: MainAxisAlignment.center, children: [
if (Policy.shouldTakePicture)
ElevatedButton.icon(
onPressed: onTakePicture,
icon: const Icon(FontAwesomeIcons.camera),
label: const Text('Take Photo'),
),
const SizedBox.square(
dimension: 16,
),
ElevatedButton.icon(
onPressed: onSelectPicture,
icon: const Icon(FontAwesomeIcons.image),
label: const Text('Choose from Library'),
),
]);
}
}
class ChatPopUp extends StatelessWidget {
const ChatPopUp(
{required this.opController, required this.onToggleChat, super.key});
final OverlayPortalController opController;
final VoidCallback onToggleChat;
@override
Widget build(BuildContext context) {
return OverlayPortal(
controller: opController,
overlayChildBuilder: (BuildContext context) {
var width = MediaQuery.sizeOf(context).width;
var height = MediaQuery.sizeOf(context).height;
return Positioned(
right: width * .05,
bottom: 0,
child: Container(
decoration: BoxDecoration(boxShadow: [
BoxShadow(
color: Theme.of(context).colorScheme.surfaceDim,
blurRadius: 36,
)
]),
width: width * .28,
height: height * .5,
child: ChatPage(
onExit: onToggleChat,
),
),
);
},
);
}
}