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

@@ -31,12 +31,29 @@ android {
}
buildTypes {
debug {
// Fix for Samsung device crash dump error
isDebuggable = true
isJniDebuggable = false
isMinifyEnabled = false
ndk {
// Disable crash dump on Samsung devices
abiFilters.clear()
abiFilters.addAll(listOf("arm64-v8a", "armeabi-v7a"))
}
}
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
packagingOptions {
jniLibs {
useLegacyPackaging = true
}
}
}
flutter {

View File

@@ -1,8 +1,13 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Camera permission for book scanning -->
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:label="books_flutter"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
android:icon="@mipmap/ic_launcher"
android:extractNativeLibs="true">
<activity
android:name=".MainActivity"
android:exported="true"

View File

@@ -2,20 +2,26 @@ PODS:
- camera_avfoundation (0.0.1):
- Flutter
- Flutter (1.0.0)
- permission_handler_apple (9.3.0):
- Flutter
DEPENDENCIES:
- camera_avfoundation (from `.symlinks/plugins/camera_avfoundation/ios`)
- Flutter (from `Flutter`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
EXTERNAL SOURCES:
camera_avfoundation:
:path: ".symlinks/plugins/camera_avfoundation/ios"
Flutter:
:path: Flutter
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
SPEC CHECKSUMS:
camera_avfoundation: 5675ca25298b6f81fa0a325188e7df62cc217741
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e

View File

@@ -199,6 +199,7 @@
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
C307D3F810A8F8C8E009259C /* [CP] Embed Pods Frameworks */,
4F49CC92542C7E033C781E4A /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@@ -286,6 +287,23 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
4F49CC92542C7E033C781E4A /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
8BB9969949EF7BC4EB129C1A /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;

View File

@@ -45,5 +45,7 @@
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>Для сканирования обложек книг и автоматического определения информации о книге</string>
</dict>
</plist>

View File

@@ -1,76 +0,0 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../models/models.dart';
import '../constants/constants.dart';
// Events
sealed class BookEvent {}
class AddBook extends BookEvent {
final Book book;
AddBook(this.book);
}
class UpdateBook extends BookEvent {
final Book book;
UpdateBook(this.book);
}
class DeleteBook extends BookEvent {
final String id;
DeleteBook(this.id);
}
class ToggleFavorite extends BookEvent {
final String id;
ToggleFavorite(this.id);
}
// State
class BookState {
final List<Book> books;
const BookState({required this.books});
}
// Bloc
class BookBloc extends Bloc<BookEvent, BookState> {
BookBloc() : super(const BookState(books: initialBooks)) {
on<AddBook>((event, emit) {
emit(BookState(books: [...state.books, event.book]));
});
on<UpdateBook>((event, emit) {
final updated = state.books.map((b) {
return b.id == event.book.id ? event.book : b;
}).toList();
emit(BookState(books: updated));
});
on<DeleteBook>((event, emit) {
emit(
BookState(books: state.books.where((b) => b.id != event.id).toList()),
);
});
on<ToggleFavorite>((event, emit) {
final updated = state.books.map((b) {
if (b.id != event.id) return b;
return (
id: b.id,
title: b.title,
author: b.author,
genre: b.genre,
annotation: b.annotation,
coverUrl: b.coverUrl,
pages: b.pages,
language: b.language,
publishedYear: b.publishedYear,
rating: b.rating,
status: b.status,
progress: b.progress,
isFavorite: !b.isFavorite,
);
}).toList();
emit(BookState(books: updated));
});
}
}

View File

@@ -1,53 +0,0 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../models/models.dart';
// Events
sealed class NavigationEvent {}
class NavigateTo extends NavigationEvent {
final AppScreen screen;
final Book? selectedBook;
final Book? prefilledData;
NavigateTo(this.screen, {this.selectedBook, this.prefilledData});
}
// State
class NavigationState {
final AppScreen screen;
final Book? selectedBook;
final Book? prefilledData;
const NavigationState({
this.screen = AppScreen.library,
this.selectedBook,
this.prefilledData,
});
NavigationState copyWith({
AppScreen? screen,
Book? Function()? selectedBook,
Book? Function()? prefilledData,
}) {
return NavigationState(
screen: screen ?? this.screen,
selectedBook: selectedBook != null ? selectedBook() : this.selectedBook,
prefilledData: prefilledData != null
? prefilledData()
: this.prefilledData,
);
}
}
// Bloc
class NavigationBloc extends Bloc<NavigationEvent, NavigationState> {
NavigationBloc() : super(const NavigationState()) {
on<NavigateTo>((event, emit) {
emit(
NavigationState(
screen: event.screen,
selectedBook: event.selectedBook ?? state.selectedBook,
prefilledData: event.prefilledData,
),
);
});
}
}

View File

@@ -1,15 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:google_fonts/google_fonts.dart';
import 'bloc/book_bloc.dart';
import 'bloc/navigation_bloc.dart';
import 'models/models.dart';
import 'widgets/layout.dart';
import 'screens/library_screen.dart';
import 'screens/categories_screen.dart';
import 'screens/book_details_screen.dart';
import 'screens/add_book_screen.dart';
import 'screens/scanner_screen.dart';
import 'bloc/book/book_bloc.dart';
import 'widgets/bottom_nav_shell.dart';
import 'theme/app_theme.dart';
void main() async {
@@ -23,101 +16,15 @@ class BookshelfApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider(create: (_) => BookBloc()),
BlocProvider(create: (_) => NavigationBloc()),
],
return BlocProvider(
create: (_) => BookBloc(),
child: MaterialApp(
title: 'Книжная полка',
debugShowCheckedModeBanner: false,
theme: AppTheme.lightTheme(),
themeMode: ThemeMode.light,
home: const _AppShell(),
home: const BottomNavShell(),
),
);
}
}
class _AppShell extends StatelessWidget {
const _AppShell();
@override
Widget build(BuildContext context) {
return BlocBuilder<NavigationBloc, NavigationState>(
builder: (context, state) {
final showNav = switch (state.screen) {
AppScreen.library ||
AppScreen.categories ||
AppScreen.wishlist ||
AppScreen.settings => true,
_ => false,
};
final screen = switch (state.screen) {
AppScreen.library => _libraryWithFab(context),
AppScreen.categories => const CategoriesScreen(),
AppScreen.wishlist => _placeholder(
'Здесь будут книги, которые вы хотите прочитать.',
),
AppScreen.settings => _placeholder(
'Персонализируйте ваше приложение.',
),
AppScreen.details => const BookDetailsScreen(),
AppScreen.addBook => const AddBookScreen(),
AppScreen.scanner => const ScannerScreen(),
};
if (state.screen == AppScreen.scanner) {
return screen;
}
return AppLayout(showBottomNav: showNav, child: screen);
},
);
}
Widget _libraryWithFab(BuildContext context) {
return Stack(
children: [
const LibraryScreen(),
Positioned(
bottom: 16,
right: 20,
child: FloatingActionButton(
onPressed: () {
context.read<NavigationBloc>().add(
NavigateTo(
AppScreen.addBook,
selectedBook: null,
prefilledData: null,
),
);
},
child: const Icon(Icons.add),
),
),
],
);
}
Widget _placeholder(String message) {
return Builder(
builder: (context) {
final theme = Theme.of(context);
return Center(
child: Padding(
padding: const EdgeInsets.all(48),
child: Text(
message,
textAlign: TextAlign.center,
style: theme.textTheme.bodyLarge?.copyWith(
color: theme.colorScheme.onSurface.withValues(alpha: 0.5),
),
),
),
);
},
);
}
}

View File

@@ -1,36 +1,3 @@
import 'package:flutter/material.dart';
typedef Book = ({
String id,
String title,
String author,
String genre,
String annotation,
String? coverUrl,
int? pages,
String? language,
int? publishedYear,
double? rating,
String status,
double? progress,
bool isFavorite,
});
typedef Category = ({
String id,
String name,
int count,
IconData icon,
Color iconColor,
Color backgroundColor,
});
enum AppScreen {
library,
categories,
wishlist,
settings,
details,
addBook,
scanner,
}
// Barrel file - exports all models
export 'book.dart';
export 'category.dart';

View File

@@ -1,42 +1,44 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../bloc/book_bloc.dart';
import '../bloc/navigation_bloc.dart';
import '../bloc/book/book_bloc.dart';
import '../bloc/book/book_event.dart';
import '../bloc/add_book/add_book_bloc.dart';
import '../bloc/add_book/add_book_event.dart';
import '../bloc/add_book/add_book_state.dart';
import '../config/api_config.dart';
import '../models/models.dart';
import '../theme/app_spacing.dart';
import 'scanner_screen.dart';
class AddBookScreen extends StatefulWidget {
const AddBookScreen({super.key});
class AddBookScreen extends StatelessWidget {
final Book? editBook;
final Book? prefilledData;
const AddBookScreen({super.key, this.editBook, this.prefilledData});
@override
State<AddBookScreen> createState() => _AddBookScreenState();
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => AddBookBloc(
onAddBook: (book) => context.read<BookBloc>().add(AddBook(book)),
onUpdateBook: (book) => context.read<BookBloc>().add(UpdateBook(book)),
)..add(InitializeForm(editBook: editBook, prefilledData: prefilledData)),
child: const _AddBookScreenContent(),
);
}
}
class _AddBookScreenState extends State<AddBookScreen> {
class _AddBookScreenContent extends StatefulWidget {
const _AddBookScreenContent();
@override
State<_AddBookScreenContent> createState() => _AddBookScreenContentState();
}
class _AddBookScreenContentState extends State<_AddBookScreenContent> {
final _titleController = TextEditingController();
final _authorController = TextEditingController();
final _annotationController = TextEditingController();
String _genre = 'fiction';
bool _initialized = false;
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_initialized) {
_initialized = true;
final navState = context.read<NavigationBloc>().state;
final book = navState.selectedBook;
final prefilled = navState.prefilledData;
final source = book ?? prefilled;
if (source != null) {
_titleController.text = source.title;
_authorController.text = source.author;
_annotationController.text = source.annotation;
_genre = source.genre.isNotEmpty ? source.genre : 'fiction';
}
}
}
@override
void dispose() {
@@ -50,12 +52,31 @@ class _AddBookScreenState extends State<AddBookScreen> {
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
final navState = context.read<NavigationBloc>().state;
final isEditing =
navState.selectedBook != null && navState.prefilledData == null;
final title = isEditing ? 'Редактировать' : 'Добавить книгу';
return SafeArea(
return BlocListener<AddBookBloc, AddBookState>(
listener: (context, state) {
// Update controllers when state changes (e.g., from scanned book)
if (_titleController.text != state.title) {
_titleController.text = state.title;
}
if (_authorController.text != state.author) {
_authorController.text = state.author;
}
if (_annotationController.text != state.annotation) {
_annotationController.text = state.annotation;
}
// Navigate back when saved
if (state.isSaved) {
Navigator.pop(context);
}
},
child: BlocBuilder<AddBookBloc, AddBookState>(
builder: (context, state) {
final title = state.isEditing ? 'Редактировать' : 'Добавить книгу';
return Material(
child: SafeArea(
child: Column(
children: [
// Header
@@ -70,11 +91,7 @@ class _AddBookScreenState extends State<AddBookScreen> {
children: [
IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => context.read<NavigationBloc>().add(
isEditing
? NavigateTo(AppScreen.details)
: NavigateTo(AppScreen.library),
),
onPressed: () => Navigator.pop(context),
),
Text(title, style: textTheme.headlineMedium),
],
@@ -86,9 +103,7 @@ class _AddBookScreenState extends State<AddBookScreen> {
children: [
// Cover placeholder / scanner trigger
GestureDetector(
onTap: () => context.read<NavigationBloc>().add(
NavigateTo(AppScreen.scanner),
),
onTap: () => _openScanner(context),
child: Container(
height: 160,
decoration: BoxDecoration(
@@ -122,15 +137,30 @@ class _AddBookScreenState extends State<AddBookScreen> {
),
),
const SizedBox(height: AppSpacing.lg),
_field('Название', _titleController, textTheme),
_field(
context,
'Название',
_titleController,
textTheme,
(value) =>
context.read<AddBookBloc>().add(UpdateTitle(value)),
),
const SizedBox(height: AppSpacing.md),
_field('Автор', _authorController, textTheme),
_field(
context,
'Автор',
_authorController,
textTheme,
(value) => context.read<AddBookBloc>().add(
UpdateAuthor(value),
),
),
const SizedBox(height: AppSpacing.md),
// Genre dropdown
Text('Жанр', style: textTheme.labelMedium),
const SizedBox(height: AppSpacing.xs),
DropdownButtonFormField<String>(
initialValue: _genre,
value: state.genre,
dropdownColor: colorScheme.surface,
decoration: const InputDecoration(),
items: const [
@@ -138,8 +168,14 @@ class _AddBookScreenState extends State<AddBookScreen> {
value: 'fiction',
child: Text('Фантастика'),
),
DropdownMenuItem(value: 'fantasy', child: Text('Фэнтези')),
DropdownMenuItem(value: 'science', child: Text('Научпоп')),
DropdownMenuItem(
value: 'fantasy',
child: Text('Фэнтези'),
),
DropdownMenuItem(
value: 'science',
child: Text('Научпоп'),
),
DropdownMenuItem(
value: 'biography',
child: Text('Биография'),
@@ -148,14 +184,27 @@ class _AddBookScreenState extends State<AddBookScreen> {
value: 'detective',
child: Text('Детектив'),
),
DropdownMenuItem(value: 'other', child: Text('Другое')),
DropdownMenuItem(
value: 'other',
child: Text('Другое'),
),
],
onChanged: (v) => setState(() => _genre = v ?? _genre),
onChanged: (v) {
if (v != null) {
context.read<AddBookBloc>().add(UpdateGenre(v));
}
},
),
const SizedBox(height: AppSpacing.md),
Text('Аннотация', style: textTheme.labelMedium),
const SizedBox(height: AppSpacing.xs),
TextField(controller: _annotationController, maxLines: 4),
TextField(
controller: _annotationController,
maxLines: 4,
onChanged: (value) => context.read<AddBookBloc>().add(
UpdateAnnotation(value),
),
),
const SizedBox(height: 100),
],
),
@@ -183,11 +232,7 @@ class _AddBookScreenState extends State<AddBookScreen> {
children: [
Expanded(
child: OutlinedButton(
onPressed: () => context.read<NavigationBloc>().add(
isEditing
? NavigateTo(AppScreen.details)
: NavigateTo(AppScreen.library),
),
onPressed: () => Navigator.pop(context),
child: const Text('Отмена'),
),
),
@@ -195,7 +240,8 @@ class _AddBookScreenState extends State<AddBookScreen> {
Expanded(
flex: 2,
child: ElevatedButton(
onPressed: _save,
onPressed: () =>
context.read<AddBookBloc>().add(SaveBook()),
child: const Text('Сохранить'),
),
),
@@ -204,52 +250,46 @@ class _AddBookScreenState extends State<AddBookScreen> {
),
],
),
),
);
},
),
);
}
Widget _field(
BuildContext context,
String label,
TextEditingController controller,
TextTheme textTheme,
void Function(String) onChanged,
) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: textTheme.labelMedium),
const SizedBox(height: AppSpacing.xs),
TextField(controller: controller),
TextField(controller: controller, onChanged: onChanged),
],
);
}
void _save() {
final navState = context.read<NavigationBloc>().state;
final existing = navState.selectedBook;
final isEditing = existing != null && navState.prefilledData == null;
Future<void> _openScanner(BuildContext context) async {
if (!context.mounted) return;
final Book book = (
id: isEditing ? existing.id : '${Random().nextInt(100000)}',
title: _titleController.text,
author: _authorController.text,
genre: _genre,
annotation: _annotationController.text,
coverUrl: isEditing
? existing.coverUrl
: 'https://picsum.photos/seed/newbook/400/600',
pages: isEditing ? existing.pages : 0,
language: isEditing ? existing.language : 'Russian',
publishedYear: isEditing ? existing.publishedYear : DateTime.now().year,
rating: isEditing ? existing.rating : 5.0,
status: isEditing ? existing.status : 'want_to_read',
progress: isEditing ? existing.progress : null,
isFavorite: isEditing ? existing.isFavorite : false,
final scannedBook = await Navigator.of(context, rootNavigator: true)
.push<Book>(
MaterialPageRoute(
builder: (_) => ScannerScreen(
geminiApiKey: ApiConfig.geminiApiKey,
openaiApiKey: ApiConfig.openaiApiKey,
openaiBaseUrl: ApiConfig.openaiBaseUrl,
),
),
);
if (isEditing) {
context.read<BookBloc>().add(UpdateBook(book));
} else {
context.read<BookBloc>().add(AddBook(book));
if (scannedBook != null && context.mounted) {
context.read<AddBookBloc>().add(ApplyScannedBook(scannedBook));
}
context.read<NavigationBloc>().add(NavigateTo(AppScreen.library));
}
}

View File

@@ -1,24 +1,22 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../bloc/book_bloc.dart';
import '../bloc/navigation_bloc.dart';
import '../bloc/book/book_bloc.dart';
import '../bloc/book/book_event.dart';
import '../models/models.dart';
import '../theme/app_theme.dart';
import '../theme/app_spacing.dart';
import 'add_book_screen.dart';
class BookDetailsScreen extends StatelessWidget {
const BookDetailsScreen({super.key});
final Book book;
const BookDetailsScreen({super.key, required this.book});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
return BlocBuilder<NavigationBloc, NavigationState>(
builder: (context, navState) {
final book = navState.selectedBook;
if (book == null) return const SizedBox.shrink();
final statusLabel = switch (book.status) {
'reading' => 'Читаю',
'done' => 'Прочитано',
@@ -27,6 +25,7 @@ class BookDetailsScreen extends StatelessWidget {
};
return SingleChildScrollView(
child: Material(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -60,9 +59,7 @@ class BookDetailsScreen extends StatelessWidget {
children: [
IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => context.read<NavigationBloc>().add(
NavigateTo(AppScreen.library),
),
onPressed: () => Navigator.pop(context),
),
IconButton(
icon: const Icon(Icons.more_vert),
@@ -92,10 +89,7 @@ class BookDetailsScreen extends StatelessWidget {
AppSpacing.radiusLarge,
),
child: book.coverUrl != null
? Image.network(
book.coverUrl!,
fit: BoxFit.cover,
)
? Image.network(book.coverUrl!, fit: BoxFit.cover)
: Container(
color: colorScheme.surfaceContainerHighest,
child: Center(
@@ -161,9 +155,7 @@ class BookDetailsScreen extends StatelessWidget {
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest,
border: Border.all(color: colorScheme.outline),
borderRadius: BorderRadius.circular(
AppSpacing.radiusSmall,
),
borderRadius: BorderRadius.circular(AppSpacing.radiusSmall),
),
child: Text(book.genre, style: textTheme.labelMedium),
),
@@ -174,10 +166,9 @@ class BookDetailsScreen extends StatelessWidget {
Expanded(
child: ElevatedButton.icon(
onPressed: () {
context.read<NavigationBloc>().add(
NavigateTo(
AppScreen.addBook,
selectedBook: book,
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => AddBookScreen(editBook: book),
),
);
},
@@ -190,9 +181,7 @@ class BookDetailsScreen extends StatelessWidget {
child: OutlinedButton.icon(
onPressed: () {
context.read<BookBloc>().add(DeleteBook(book.id));
context.read<NavigationBloc>().add(
NavigateTo(AppScreen.library),
);
Navigator.pop(context);
},
icon: const Icon(Icons.delete_outline, size: 18),
label: const Text('Удалить'),
@@ -255,8 +244,7 @@ class BookDetailsScreen extends StatelessWidget {
),
],
),
);
},
),
);
}

View File

@@ -1,36 +1,48 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../bloc/book_bloc.dart';
import '../bloc/navigation_bloc.dart';
import '../models/models.dart';
import '../bloc/book/book_bloc.dart';
import '../bloc/book/book_state.dart';
import '../bloc/library/library_bloc.dart';
import '../bloc/library/library_event.dart';
import '../bloc/library/library_state.dart';
import '../widgets/book_card.dart';
import '../theme/app_spacing.dart';
import 'book_details_screen.dart';
import 'add_book_screen.dart';
class LibraryScreen extends StatefulWidget {
class LibraryScreen extends StatelessWidget {
const LibraryScreen({super.key});
@override
State<LibraryScreen> createState() => _LibraryScreenState();
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => LibraryBloc(),
child: const _LibraryScreenContent(),
);
}
}
class _LibraryScreenState extends State<LibraryScreen> {
String _search = '';
int _tabIndex = 0;
class _LibraryScreenContent extends StatelessWidget {
const _LibraryScreenContent();
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
return BlocBuilder<LibraryBloc, LibraryState>(
builder: (context, libraryState) {
return BlocBuilder<BookBloc, BookState>(
builder: (context, state) {
final filtered = state.books.where((b) {
final q = _search.toLowerCase();
builder: (context, bookState) {
final filtered = bookState.books.where((b) {
final q = libraryState.searchQuery.toLowerCase();
return b.title.toLowerCase().contains(q) ||
b.author.toLowerCase().contains(q);
}).toList();
return SafeArea(
return Stack(
children: [
SafeArea(
child: Column(
children: [
// Header
@@ -44,7 +56,10 @@ class _LibraryScreenState extends State<LibraryScreen> {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Книжная полка', style: textTheme.displayMedium),
Text(
'Книжная полка',
style: textTheme.displayMedium,
),
IconButton(
icon: const Icon(Icons.notifications_outlined),
onPressed: () {},
@@ -61,10 +76,17 @@ class _LibraryScreenState extends State<LibraryScreen> {
0,
),
child: TextField(
onChanged: (v) => setState(() => _search = v),
onChanged: (v) {
context.read<LibraryBloc>().add(
UpdateSearchQuery(v),
);
},
decoration: InputDecoration(
hintText: 'Поиск книг...',
prefixIcon: Icon(Icons.search, color: colorScheme.primary),
prefixIcon: Icon(
Icons.search,
color: colorScheme.primary,
),
),
),
),
@@ -78,16 +100,26 @@ class _LibraryScreenState extends State<LibraryScreen> {
),
child: Row(
children: [
_tab('Все книги', 0),
_tab(
context,
'Все книги',
0,
libraryState.tabIndex,
),
const SizedBox(width: AppSpacing.sm),
_tab('Категории', 1),
_tab(
context,
'Категории',
1,
libraryState.tabIndex,
),
],
),
),
const SizedBox(height: AppSpacing.md),
// Grid
Expanded(
child: _tabIndex == 0
child: libraryState.tabIndex == 0
? GridView.builder(
padding: const EdgeInsets.fromLTRB(
AppSpacing.lg,
@@ -106,10 +138,11 @@ class _LibraryScreenState extends State<LibraryScreen> {
itemBuilder: (context, i) => BookCard(
book: filtered[i],
onTap: () {
context.read<NavigationBloc>().add(
NavigateTo(
AppScreen.details,
selectedBook: filtered[i],
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => BookDetailsScreen(
book: filtered[i],
),
),
);
},
@@ -131,7 +164,9 @@ class _LibraryScreenState extends State<LibraryScreen> {
),
decoration: BoxDecoration(
color: colorScheme.surface,
border: Border.all(color: colorScheme.outline),
border: Border.all(
color: colorScheme.outline,
),
borderRadius: BorderRadius.circular(
AppSpacing.radiusMedium,
),
@@ -144,9 +179,8 @@ class _LibraryScreenState extends State<LibraryScreen> {
trailing: Text(
'${filtered.where((b) => b.genre == genre).length}',
style: textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurface.withValues(
alpha: 0.6,
),
color: colorScheme.onSurface
.withValues(alpha: 0.6),
),
),
),
@@ -156,19 +190,37 @@ class _LibraryScreenState extends State<LibraryScreen> {
),
],
),
),
Positioned(
bottom: 16,
right: 20,
child: FloatingActionButton(
onPressed: () {
Navigator.of(context, rootNavigator: true).push(
MaterialPageRoute(
builder: (_) => const AddBookScreen(),
),
);
},
child: const Icon(Icons.add),
),
),
],
);
},
);
},
);
}
Widget _tab(String label, int index) {
final selected = _tabIndex == index;
Widget _tab(BuildContext context, String label, int index, int currentIndex) {
final selected = currentIndex == index;
final colorScheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
final disableAnimations = MediaQuery.of(context).disableAnimations;
return GestureDetector(
onTap: () => setState(() => _tabIndex = index),
onTap: () => context.read<LibraryBloc>().add(ChangeTab(index)),
child: AnimatedContainer(
duration: disableAnimations
? Duration.zero

View File

@@ -1,21 +1,175 @@
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 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 placeholder
Container(color: Colors.black87),
// Scan frame
Center(
// 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(
@@ -25,19 +179,79 @@ class ScannerScreen extends StatelessWidget {
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),
// 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),
),
),
// Header
SafeArea(
),
),
// 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(
@@ -45,15 +259,10 @@ class ScannerScreen extends StatelessWidget {
children: [
IconButton(
icon: const Icon(Icons.close, color: Colors.white),
onPressed: () => context.read<NavigationBloc>().add(
NavigateTo(AppScreen.addBook),
),
onPressed: () => Navigator.pop(context),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: const Color(0xFF17CF54),
borderRadius: BorderRadius.circular(8),
@@ -63,6 +272,7 @@ class ScannerScreen extends StatelessWidget {
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
color: Colors.white,
),
),
),
@@ -70,30 +280,69 @@ class ScannerScreen extends StatelessWidget {
],
),
),
);
}
Widget _buildProcessingOverlay() {
return Container(
color: Colors.black87,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF17CF54)),
),
// Instructions
Positioned(
bottom: 140,
left: 0,
right: 0,
child: Text(
'Поместите обложку в рамку',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey.shade400),
const SizedBox(height: 24),
const Text(
'Анализ обложки...',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
// Capture button
Positioned(
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: Center(
child: GestureDetector(
onTap: () {
// Placeholder - no actual camera capture
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Камера не подключена (заглушка)'),
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,
),
);
},
@@ -108,18 +357,55 @@ class ScannerScreen extends StatelessWidget {
child: Container(
width: 64,
height: 64,
decoration: const BoxDecoration(
decoration: 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),
),
],
),
);
}
}

View File

@@ -1,12 +1,115 @@
import 'dart:io';
import 'dart:convert';
import 'package:google_generative_ai/google_generative_ai.dart';
import '../models/models.dart';
class GeminiService {
GeminiService({required String apiKey});
final String apiKey;
late final GenerativeModel _model;
Future<Book?> analyzeBookCover(String base64Image) async {
// Placeholder - Gemini API integration would go here
// Would use google_generative_ai package to send the image
// and extract book metadata via structured JSON output
GeminiService({required this.apiKey}) {
_model = GenerativeModel(model: 'gemini-1.5-flash', apiKey: apiKey);
}
Future<Book?> analyzeBookCover(String imagePath) async {
try {
// Read the image file
final imageFile = File(imagePath);
final imageBytes = await imageFile.readAsBytes();
final base64Image = base64Encode(imageBytes);
// Create the prompt for book analysis
const prompt = '''
Analyze this book cover image and extract the following information in JSON format:
{
"title": "book title (required)",
"author": "author name (required)",
"genre": "fiction/fantasy/science/detective/biography/other",
"annotation": "brief description or summary if visible, otherwise generate a generic one"
}
Rules:
- Extract exact text from the cover
- If genre is unclear, choose the most appropriate one
- If annotation is not visible, create a brief generic description
- Return ONLY valid JSON, no additional text
- Ensure all required fields are present
''';
// Create the image part for the model
final imagePart = Content.data('image/jpeg', imageBytes);
// Generate content with both text and image
final response = await _model.generateContent([
Content.text(prompt),
imagePart,
]);
final responseText = response.text?.trim();
if (responseText == null || responseText.isEmpty) {
print('Empty response from Gemini');
return null;
}
// Extract JSON from response (handle potential markdown formatting)
String jsonString = responseText;
if (jsonString.contains('```json')) {
jsonString = jsonString.split('```json')[1].split('```')[0].trim();
} else if (jsonString.contains('```')) {
jsonString = jsonString.split('```')[1].split('```')[0].trim();
}
// Parse JSON response
final Map<String, dynamic> jsonData = json.decode(jsonString);
// Create Book object with extracted data
final Book book = (
id: DateTime.now().millisecondsSinceEpoch.toString(),
title: jsonData['title']?.toString() ?? 'Неизвестная книга',
author: jsonData['author']?.toString() ?? 'Неизвестный автор',
genre: _normalizeGenre(jsonData['genre']?.toString()),
annotation: jsonData['annotation']?.toString() ?? 'Нет описания',
coverUrl: null, // Will be set by the caller
pages: null,
language: 'Russian',
publishedYear: DateTime.now().year,
rating: 5.0,
status: 'want_to_read',
progress: null,
isFavorite: false,
);
return book;
} catch (e) {
print('Error analyzing book cover: $e');
return null;
}
}
String _normalizeGenre(String? genre) {
if (genre == null || genre.isEmpty) return 'other';
final normalized = genre.toLowerCase().trim();
// Map various genre names to our standard genres
final genreMap = {
'фантастика': 'fiction',
'fantasy': 'fantasy',
'фэнтези': 'fantasy',
'science': 'science',
'научпоп': 'science',
'научная': 'science',
'biography': 'biography',
'биография': 'biography',
'detective': 'detective',
'детектив': 'detective',
'роман': 'other',
'novel': 'other',
'poetry': 'other',
'поэзия': 'other',
};
return genreMap[normalized] ?? normalized;
}
}

View File

@@ -62,8 +62,9 @@ class _BookCardState extends State<BookCard> {
height: double.infinity,
loadingBuilder:
(context, child, loadingProgress) {
if (loadingProgress == null)
if (loadingProgress == null) {
return child;
}
return ShimmerLoading(
borderRadius: AppSpacing.radiusMedium,
height: double.infinity,

View File

@@ -1,27 +1,15 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../models/models.dart';
import '../bloc/navigation_bloc.dart';
class BottomNav extends StatelessWidget {
const BottomNav({super.key});
final int currentIndex;
final ValueChanged<int> onTap;
const BottomNav({super.key, required this.currentIndex, required this.onTap});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return BlocBuilder<NavigationBloc, NavigationState>(
builder: (context, state) {
final currentIndex = switch (state.screen) {
AppScreen.library ||
AppScreen.details ||
AppScreen.addBook ||
AppScreen.scanner => 0,
AppScreen.categories => 1,
AppScreen.wishlist => 2,
AppScreen.settings => 3,
};
return Container(
decoration: BoxDecoration(
color: colorScheme.surface,
@@ -38,19 +26,11 @@ class BottomNav extends StatelessWidget {
),
child: BottomNavigationBar(
currentIndex: currentIndex,
onTap: onTap,
backgroundColor: Colors.transparent,
elevation: 0,
selectedItemColor: colorScheme.primary,
unselectedItemColor: colorScheme.onSurface.withValues(alpha: 0.6),
onTap: (index) {
final screen = [
AppScreen.library,
AppScreen.categories,
AppScreen.wishlist,
AppScreen.settings,
][index];
context.read<NavigationBloc>().add(NavigateTo(screen));
},
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.local_library),
@@ -71,7 +51,5 @@ class BottomNav extends StatelessWidget {
],
),
);
},
);
}
}

View File

@@ -1,17 +0,0 @@
import 'package:flutter/material.dart';
import 'bottom_nav.dart';
class AppLayout extends StatelessWidget {
final Widget child;
final bool showBottomNav;
const AppLayout({super.key, required this.child, this.showBottomNav = true});
@override
Widget build(BuildContext context) {
return Scaffold(
body: child,
bottomNavigationBar: showBottomNav ? const BottomNav() : null,
);
}
}

View File

@@ -1,6 +1,22 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
archive:
dependency: transitive
description:
name: archive
sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd"
url: "https://pub.dev"
source: hosted
version: "4.0.7"
args:
dependency: transitive
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.dev"
source: hosted
version: "2.7.0"
async:
dependency: transitive
description:
@@ -73,6 +89,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.0"
checked_yaml:
dependency: transitive
description:
name: checked_yaml
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
url: "https://pub.dev"
source: hosted
version: "2.0.4"
cli_util:
dependency: transitive
description:
name: cli_util
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
url: "https://pub.dev"
source: hosted
version: "0.4.2"
clock:
dependency: transitive
description:
@@ -158,6 +190,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "9.1.1"
flutter_launcher_icons:
dependency: "direct dev"
description:
name: flutter_launcher_icons
sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7"
url: "https://pub.dev"
source: hosted
version: "0.14.4"
flutter_lints:
dependency: "direct dev"
description:
@@ -217,7 +257,7 @@ packages:
source: hosted
version: "1.0.0"
http:
dependency: transitive
dependency: "direct main"
description:
name: http
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
@@ -232,6 +272,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.2"
image:
dependency: transitive
description:
name: image
sha256: "492bd52f6c4fbb6ee41f781ff27765ce5f627910e1e0cbecfa3d9add5562604c"
url: "https://pub.dev"
source: hosted
version: "4.7.2"
json_annotation:
dependency: transitive
description:
name: json_annotation
sha256: "805fa86df56383000f640384b282ce0cb8431f1a7a2396de92fb66186d8c57df"
url: "https://pub.dev"
source: hosted
version: "4.10.0"
leak_tracker:
dependency: transitive
description:
@@ -376,6 +432,62 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.0"
permission_handler:
dependency: "direct main"
description:
name: permission_handler
sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849"
url: "https://pub.dev"
source: hosted
version: "11.4.0"
permission_handler_android:
dependency: transitive
description:
name: permission_handler_android
sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc
url: "https://pub.dev"
source: hosted
version: "12.1.0"
permission_handler_apple:
dependency: transitive
description:
name: permission_handler_apple
sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023
url: "https://pub.dev"
source: hosted
version: "9.4.7"
permission_handler_html:
dependency: transitive
description:
name: permission_handler_html
sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24"
url: "https://pub.dev"
source: hosted
version: "0.1.3+5"
permission_handler_platform_interface:
dependency: transitive
description:
name: permission_handler_platform_interface
sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878
url: "https://pub.dev"
source: hosted
version: "4.3.0"
permission_handler_windows:
dependency: transitive
description:
name: permission_handler_windows
sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
url: "https://pub.dev"
source: hosted
version: "0.2.1"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1"
url: "https://pub.dev"
source: hosted
version: "7.0.1"
platform:
dependency: transitive
description:
@@ -392,6 +504,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.8"
posix:
dependency: transitive
description:
name: posix
sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61"
url: "https://pub.dev"
source: hosted
version: "6.0.3"
provider:
dependency: transitive
description:
@@ -509,6 +629,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.0"
xml:
dependency: transitive
description:
name: xml
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
url: "https://pub.dev"
source: hosted
version: "6.6.1"
yaml:
dependency: transitive
description:

View File

@@ -36,6 +36,8 @@ dependencies:
camera: ^0.11.1
google_generative_ai: ^0.4.6
google_fonts: ^6.2.1
permission_handler: ^11.0.0
http: ^1.2.0
dev_dependencies:
flutter_test:
@@ -47,6 +49,7 @@ dev_dependencies:
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^6.0.0
flutter_launcher_icons: ^0.14.1
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
@@ -89,3 +92,12 @@ flutter:
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/to/font-from-package
# Flutter Launcher Icons configuration
flutter_launcher_icons:
android: true
ios: true
image_path: "assets/icon/app_icon.png"
adaptive_icon_background: "#0891B2" # Your app's cyan theme color
adaptive_icon_foreground: "assets/icon/app_icon_foreground.png"
remove_alpha_ios: true