openai service
This commit is contained in:
@@ -1,123 +1,409 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../bloc/navigation_bloc.dart';
|
||||
import '../models/models.dart';
|
||||
import 'package:camera/camera.dart';
|
||||
import '../bloc/scanner/scanner_bloc.dart';
|
||||
import '../bloc/scanner/scanner_event.dart';
|
||||
import '../bloc/scanner/scanner_state.dart';
|
||||
import '../services/camera_service.dart';
|
||||
|
||||
class ScannerScreen extends StatelessWidget {
|
||||
const ScannerScreen({super.key});
|
||||
final String? geminiApiKey;
|
||||
final String? openaiApiKey;
|
||||
final String openaiBaseUrl;
|
||||
|
||||
const ScannerScreen({
|
||||
super.key,
|
||||
this.geminiApiKey,
|
||||
this.openaiApiKey,
|
||||
this.openaiBaseUrl = 'http://localhost:8317',
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: Stack(
|
||||
children: [
|
||||
// Camera placeholder
|
||||
Container(color: Colors.black87),
|
||||
// Scan frame
|
||||
Center(
|
||||
child: FractionallySizedBox(
|
||||
widthFactor: 0.75,
|
||||
child: AspectRatio(
|
||||
aspectRatio: 2 / 3,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.white30, width: 2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Center(
|
||||
child: Text(
|
||||
'Камера недоступна\n(заглушка)',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: Colors.white38),
|
||||
),
|
||||
),
|
||||
),
|
||||
return BlocProvider(
|
||||
create: (context) =>
|
||||
ScannerBloc(cameraService: CameraService())..add(InitializeCamera()),
|
||||
child: _ScannerScreenContent(
|
||||
geminiApiKey: geminiApiKey,
|
||||
openaiApiKey: openaiApiKey,
|
||||
openaiBaseUrl: openaiBaseUrl,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ScannerScreenContent extends StatelessWidget {
|
||||
final String? geminiApiKey;
|
||||
final String? openaiApiKey;
|
||||
final String openaiBaseUrl;
|
||||
|
||||
const _ScannerScreenContent({
|
||||
this.geminiApiKey,
|
||||
this.openaiApiKey,
|
||||
this.openaiBaseUrl = 'http://localhost:8317',
|
||||
});
|
||||
|
||||
void _showErrorDialog(BuildContext context, String errorMessage) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: const Text('Ошибка'),
|
||||
content: Text(errorMessage),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(dialogContext).pop();
|
||||
context.read<ScannerBloc>().add(DismissError());
|
||||
},
|
||||
child: const Text('OK'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocListener<ScannerBloc, ScannerState>(
|
||||
listener: (context, state) {
|
||||
// Show error dialog when error message is present
|
||||
if (state.errorMessage != null) {
|
||||
_showErrorDialog(context, state.errorMessage!);
|
||||
}
|
||||
|
||||
// Navigate back with analyzed book
|
||||
if (state.analyzedBook != null) {
|
||||
Navigator.pop(context, state.analyzedBook);
|
||||
}
|
||||
},
|
||||
child: BlocBuilder<ScannerBloc, ScannerState>(
|
||||
builder: (context, state) {
|
||||
final cameraService = context.read<ScannerBloc>().cameraService;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: Stack(
|
||||
children: [
|
||||
// Camera preview
|
||||
if (state.isInitialized && cameraService.controller != null)
|
||||
Center(child: CameraPreview(cameraService.controller!))
|
||||
else if (state.hasPermissionError)
|
||||
_buildPermissionError(context)
|
||||
else
|
||||
_buildLoading(),
|
||||
|
||||
// Scan frame overlay
|
||||
if (state.isInitialized && !state.isAnalyzing)
|
||||
_buildScanFrame(),
|
||||
|
||||
// Header
|
||||
_buildHeader(context),
|
||||
|
||||
// Processing overlay
|
||||
if (state.isAnalyzing) _buildProcessingOverlay(),
|
||||
|
||||
// Controls
|
||||
if (state.isInitialized && !state.isAnalyzing)
|
||||
_buildControls(context, state.isCapturing),
|
||||
|
||||
// Instructions
|
||||
if (state.isInitialized && !state.isAnalyzing)
|
||||
_buildInstructions(),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoading() {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPermissionError(BuildContext context) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.camera_alt_outlined,
|
||||
size: 64,
|
||||
color: Colors.white38,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Нет доступа к камере',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Header
|
||||
SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, color: Colors.white),
|
||||
onPressed: () => context.read<NavigationBloc>().add(
|
||||
NavigateTo(AppScreen.addBook),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF17CF54),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Text(
|
||||
'СКАНЕР',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'Разрешите доступ к камере для сканирования обложек книг',
|
||||
style: TextStyle(color: Colors.white70, fontSize: 14),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
context.read<ScannerBloc>().add(InitializeCamera());
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF17CF54),
|
||||
),
|
||||
child: const Text('Повторить'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildScanFrame() {
|
||||
return Center(
|
||||
child: FractionallySizedBox(
|
||||
widthFactor: 0.75,
|
||||
child: AspectRatio(
|
||||
aspectRatio: 2 / 3,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.white30, width: 2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
// Corner accents
|
||||
child: Stack(
|
||||
children: [
|
||||
// Top left corner
|
||||
Positioned(
|
||||
top: -2,
|
||||
left: -2,
|
||||
child: Container(
|
||||
width: 20,
|
||||
height: 20,
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
left: BorderSide(color: Color(0xFF17CF54), width: 4),
|
||||
top: BorderSide(color: Color(0xFF17CF54), width: 4),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 48),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Top right corner
|
||||
Positioned(
|
||||
top: -2,
|
||||
right: -2,
|
||||
child: Container(
|
||||
width: 20,
|
||||
height: 20,
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
right: BorderSide(color: Color(0xFF17CF54), width: 4),
|
||||
top: BorderSide(color: Color(0xFF17CF54), width: 4),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Bottom left corner
|
||||
Positioned(
|
||||
bottom: -2,
|
||||
left: -2,
|
||||
child: Container(
|
||||
width: 20,
|
||||
height: 20,
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
left: BorderSide(color: Color(0xFF17CF54), width: 4),
|
||||
bottom: BorderSide(color: Color(0xFF17CF54), width: 4),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Bottom right corner
|
||||
Positioned(
|
||||
bottom: -2,
|
||||
right: -2,
|
||||
child: Container(
|
||||
width: 20,
|
||||
height: 20,
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
right: BorderSide(color: Color(0xFF17CF54), width: 4),
|
||||
bottom: BorderSide(color: Color(0xFF17CF54), width: 4),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Instructions
|
||||
Positioned(
|
||||
bottom: 140,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Text(
|
||||
'Поместите обложку в рамку',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: Colors.grey.shade400),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(BuildContext context) {
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, color: Colors.white),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF17CF54),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Text(
|
||||
'СКАНЕР',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 48),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProcessingOverlay() {
|
||||
return Container(
|
||||
color: Colors.black87,
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF17CF54)),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const Text(
|
||||
'Анализ обложки...',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Это может занять несколько секунд',
|
||||
style: TextStyle(color: Colors.grey.shade400, fontSize: 14),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildControls(BuildContext context, bool isCapturing) {
|
||||
return Positioned(
|
||||
bottom: 50,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Flash button (placeholder)
|
||||
Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
margin: const EdgeInsets.only(right: 20),
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.flash_off, color: Colors.white),
|
||||
onPressed: () {
|
||||
// Flash functionality can be added later
|
||||
},
|
||||
),
|
||||
),
|
||||
// Capture button
|
||||
Positioned(
|
||||
bottom: 50,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Center(
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
// Placeholder - no actual camera capture
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Камера не подключена (заглушка)'),
|
||||
),
|
||||
);
|
||||
},
|
||||
GestureDetector(
|
||||
onTap: isCapturing
|
||||
? null
|
||||
: () {
|
||||
context.read<ScannerBloc>().add(
|
||||
CaptureAndAnalyze(
|
||||
openaiApiKey: openaiApiKey,
|
||||
openaiBaseUrl: openaiBaseUrl,
|
||||
geminiApiKey: geminiApiKey,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 4),
|
||||
),
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
width: 64,
|
||||
height: 64,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 4),
|
||||
),
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: 64,
|
||||
height: 64,
|
||||
decoration: const BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
color: isCapturing ? Colors.white38 : Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Camera switch button
|
||||
Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
margin: const EdgeInsets.only(left: 20),
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.flip_camera_ios, color: Colors.white),
|
||||
onPressed: () {
|
||||
context.read<ScannerBloc>().add(SwitchCamera());
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInstructions() {
|
||||
return Positioned(
|
||||
bottom: 140,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'Поместите обложку в рамку',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.grey.shade400,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Убедитесь, что текст читается четко',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: Colors.grey.shade500, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user