Files
bookshelf/books_flutter/lib/screens/scanner_screen.dart
2026-02-08 12:04:45 +06:00

412 lines
12 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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 {
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 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,
),
),
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),
),
),
),
),
// 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),
),
),
),
),
],
),
),
),
),
);
}
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
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: 64,
height: 64,
decoration: BoxDecoration(
shape: BoxShape.circle,
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),
),
],
),
);
}
}