openai service

This commit is contained in:
2026-02-08 12:04:45 +06:00
parent 5c7b65a0d3
commit d7722ad81d
19 changed files with 1372 additions and 1008 deletions

View File

@@ -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),
),
],
),
);