Compare commits

2 Commits
main ... glm

Author SHA1 Message Date
Yuriy Panov
2f97873095 Fix import paths and test issues
- Fixed test file import paths to point to correct Bloc file locations
- Fixed Bloc file import paths for models (../../../models/models.dart)
- Added explicit type annotations to resolve null safety warnings
- Fixed null safety issues in wishlist_bloc_test.dart
- All 70 tests now passing
2026-02-04 15:28:59 +06:00
Yuriy Panov
310463e89a refactor: create separate BLoCs for each screen with comprehensive tests
- Created 8 separate BLoCs (Home, Library, BookDetails, AddBook, Scanner, Categories, Wishlist, Settings)
- Each BLoC has its own event, state, and bloc files
- Added 70 comprehensive tests covering all BLoC functionality
- All tests passing (70/70)
- Fixed linting issues and updated deprecated APIs
- Improved code organization and maintainability
2026-02-04 14:40:00 +06:00
181 changed files with 9728 additions and 0 deletions

45
bookshelf_flutter/.gitignore vendored Normal file
View File

@@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

View File

@@ -0,0 +1,45 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "19074d12f7eaf6a8180cd4036a430c1d76de904e"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e
base_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e
- platform: android
create_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e
base_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e
- platform: ios
create_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e
base_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e
- platform: linux
create_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e
base_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e
- platform: macos
create_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e
base_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e
- platform: web
create_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e
base_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e
- platform: windows
create_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e
base_revision: 19074d12f7eaf6a8180cd4036a430c1d76de904e
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

View File

@@ -0,0 +1,16 @@
# bookshelf_flutter
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

View File

@@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

14
bookshelf_flutter/android/.gitignore vendored Normal file
View File

@@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks

View File

@@ -0,0 +1,44 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.bookshelf.bookshelf_flutter"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.bookshelf.bookshelf_flutter"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
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")
}
}
}
flutter {
source = "../.."
}

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -0,0 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="bookshelf_flutter"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View File

@@ -0,0 +1,5 @@
package com.bookshelf.bookshelf_flutter
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -0,0 +1,24 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

View File

@@ -0,0 +1,2 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true

View File

@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip

View File

@@ -0,0 +1,26 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
}
include(":app")

34
bookshelf_flutter/ios/.gitignore vendored Normal file
View File

@@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>13.0</string>
</dict>
</plist>

View File

@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"

View File

@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"

View File

@@ -0,0 +1,43 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '13.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_ios_podfile_setup
target 'Runner' do
use_frameworks!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
end

View File

@@ -0,0 +1,619 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 6HZNF574AP;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.bookshelf.bookshelfFlutter;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.bookshelf.bookshelfFlutter.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.bookshelf.bookshelfFlutter.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.bookshelf.bookshelfFlutter.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 6HZNF574AP;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.bookshelf.bookshelfFlutter;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 6HZNF574AP;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.bookshelf.bookshelfFlutter;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@@ -0,0 +1,13 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

View File

@@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View File

@@ -0,0 +1,5 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Bookshelf Flutter</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>bookshelf_flutter</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"

View File

@@ -0,0 +1,12 @@
import Flutter
import UIKit
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}

View File

@@ -0,0 +1,179 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../models/models.dart';
import 'app_event.dart';
import 'app_state.dart';
class AppBloc extends Bloc<AppEvent, AppState> {
AppBloc() : super(AppState(books: _initialBooks)) {
on<ScreenChanged>(_onScreenChanged);
on<BookClicked>(_onBookClicked);
on<AddBookClicked>(_onAddBookClicked);
on<BookSaved>(_onBookSaved);
on<BookDeleted>(_onBookDeleted);
on<BookDetected>(_onBookDetected);
on<SearchChanged>(_onSearchChanged);
}
static List<Book> get _initialBooks => [
createBook(
id: '1',
title: 'Великий Гэтсби',
author: 'Ф. Скотт Фицджеральд',
genre: 'Classic',
annotation:
'История о несбывшейся любви и трагедии американской мечты на фоне бурных двадцатых годов.',
coverUrl: 'https://picsum.photos/seed/gatsby/400/600',
pages: 208,
language: 'English',
publishedYear: 1925,
rating: 4.8,
status: BookStatus.reading,
progress: 45,
isFavorite: true,
),
createBook(
id: '2',
title: '1984',
author: 'Джордж Оруэлл',
genre: 'Dystopian',
annotation:
'Антиутопия о тоталитарном государстве, где мысли контролируются, а правда переменчива.',
coverUrl: 'https://picsum.photos/seed/1984/400/600',
pages: 328,
language: 'English',
publishedYear: 1949,
rating: 4.9,
status: BookStatus.wantToRead,
isFavorite: true,
),
createBook(
id: '3',
title: 'Дюна',
author: 'Фрэнк Герберт',
genre: 'Sci-Fi',
annotation:
'Эпическая сага о борьбе за власть над самой важной планетой во Вселенной.',
coverUrl: 'https://picsum.photos/seed/dune/400/600',
pages: 896,
language: 'English',
publishedYear: 1965,
rating: 4.7,
status: BookStatus.reading,
progress: 12,
isFavorite: false,
),
createBook(
id: '4',
title: 'Хоббит',
author: 'Дж. Р. Р. Толкин',
genre: 'Fantasy',
annotation:
'Путешествие Бильбо Бэггинса туда и обратно в поисках сокровищ гномов.',
coverUrl: 'https://picsum.photos/seed/hobbit/400/600',
pages: 310,
language: 'English',
publishedYear: 1937,
rating: 4.9,
status: BookStatus.done,
isFavorite: false,
),
];
void _onScreenChanged(ScreenChanged event, Emitter<AppState> emit) {
emit(state.copyWith(currentScreen: event.screen));
}
void _onBookClicked(BookClicked event, Emitter<AppState> emit) {
emit(state.copyWith(
selectedBook: event.book,
currentScreen: AppScreen.details,
));
}
void _onAddBookClicked(AddBookClicked event, Emitter<AppState> emit) {
emit(state.copyWith(
prefilledData: null,
selectedBook: null,
currentScreen: AppScreen.addBook,
));
}
void _onBookSaved(BookSaved event, Emitter<AppState> emit) {
final bookData = event.bookData;
if (state.selectedBook != null) {
// Edit existing book
final updatedBooks = state.books.map((book) {
if (book.id == state.selectedBook!.id) {
return book.copyWith(
title: bookData['title'] as String? ?? book.title,
author: bookData['author'] as String? ?? book.author,
genre: bookData['genre'] as String? ?? book.genre,
annotation: bookData['annotation'] as String? ?? book.annotation,
coverUrl: bookData['coverUrl'] as String?,
pages: bookData['pages'] as int?,
language: bookData['language'] as String?,
publishedYear: bookData['publishedYear'] as int?,
rating: bookData['rating'] as double?,
status: bookData['status'] as BookStatus? ?? book.status,
progress: bookData['progress'] as int?,
isFavorite: bookData['isFavorite'] as bool? ?? book.isFavorite,
);
}
return book;
}).toList();
emit(state.copyWith(
books: updatedBooks,
currentScreen: AppScreen.library,
selectedBook: null,
prefilledData: null,
));
} else {
// Add new book
final newBook = createBook(
id: DateTime.now().millisecondsSinceEpoch.toString(),
title: bookData['title'] as String? ?? 'Unknown',
author: bookData['author'] as String? ?? 'Unknown',
genre: bookData['genre'] as String? ?? 'Unknown',
annotation: bookData['annotation'] as String? ?? '',
coverUrl: bookData['coverUrl'] as String? ??
'https://picsum.photos/seed/${DateTime.now().millisecondsSinceEpoch}/400/600',
pages: bookData['pages'] as int?,
language: bookData['language'] as String?,
publishedYear: bookData['publishedYear'] as int?,
rating: bookData['rating'] as double?,
status: bookData['status'] as BookStatus? ?? BookStatus.wantToRead,
progress: bookData['progress'] as int?,
isFavorite: bookData['isFavorite'] as bool? ?? false,
);
emit(state.copyWith(
books: [...state.books, newBook],
currentScreen: AppScreen.library,
selectedBook: null,
prefilledData: null,
));
}
}
void _onBookDeleted(BookDeleted event, Emitter<AppState> emit) {
final updatedBooks = state.books.where((book) => book.id != event.id).toList();
emit(state.copyWith(
books: updatedBooks,
currentScreen: AppScreen.library,
selectedBook: null,
));
}
void _onBookDetected(BookDetected event, Emitter<AppState> emit) {
emit(state.copyWith(
prefilledData: event.bookData,
currentScreen: AppScreen.addBook,
));
}
void _onSearchChanged(SearchChanged event, Emitter<AppState> emit) {
emit(state.copyWith(searchQuery: event.query));
}
}

View File

@@ -0,0 +1,39 @@
import '../models/models.dart';
abstract class AppEvent {
const AppEvent();
}
class ScreenChanged extends AppEvent {
final AppScreen screen;
const ScreenChanged(this.screen);
}
class BookClicked extends AppEvent {
final Book book;
const BookClicked(this.book);
}
class AddBookClicked extends AppEvent {
const AddBookClicked();
}
class BookSaved extends AppEvent {
final Map<String, dynamic> bookData;
const BookSaved(this.bookData);
}
class BookDeleted extends AppEvent {
final String id;
const BookDeleted(this.id);
}
class BookDetected extends AppEvent {
final Map<String, dynamic> bookData;
const BookDetected(this.bookData);
}
class SearchChanged extends AppEvent {
final String query;
const SearchChanged(this.query);
}

View File

@@ -0,0 +1,53 @@
import 'package:equatable/equatable.dart';
import '../models/models.dart';
class AppState extends Equatable {
final AppScreen currentScreen;
final List<Book> books;
final Book? selectedBook;
final Map<String, dynamic>? prefilledData;
final String searchQuery;
final bool isLoading;
final String? errorMessage;
const AppState({
this.currentScreen = AppScreen.library,
this.books = const [],
this.selectedBook,
this.prefilledData,
this.searchQuery = '',
this.isLoading = false,
this.errorMessage,
});
AppState copyWith({
AppScreen? currentScreen,
List<Book>? books,
Book? selectedBook,
Map<String, dynamic>? prefilledData,
String? searchQuery,
bool? isLoading,
String? errorMessage,
}) {
return AppState(
currentScreen: currentScreen ?? this.currentScreen,
books: books ?? this.books,
selectedBook: selectedBook,
prefilledData: prefilledData ?? this.prefilledData,
searchQuery: searchQuery ?? this.searchQuery,
isLoading: isLoading ?? this.isLoading,
errorMessage: errorMessage ?? this.errorMessage,
);
}
@override
List<Object?> get props => [
currentScreen,
books,
selectedBook,
prefilledData,
searchQuery,
isLoading,
errorMessage,
];
}

View File

@@ -0,0 +1,320 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:image_picker/image_picker.dart';
import 'package:cached_network_image/cached_network_image.dart';
import '../../bloc/app_bloc.dart';
import '../../bloc/app_event.dart';
import '../../../models/models.dart';
class AddBookScreen extends StatefulWidget {
final dynamic initialData;
const AddBookScreen({super.key, this.initialData});
@override
State<AddBookScreen> createState() => _AddBookScreenState();
}
class _AddBookScreenState extends State<AddBookScreen> {
final _formKey = GlobalKey<FormState>();
late TextEditingController _titleController;
late TextEditingController _authorController;
late TextEditingController _genreController;
late TextEditingController _annotationController;
String? _coverUrl;
BookStatus _selectedStatus = BookStatus.wantToRead;
@override
void initState() {
super.initState();
_titleController = TextEditingController();
_authorController = TextEditingController();
_genreController = TextEditingController();
_annotationController = TextEditingController();
if (widget.initialData is Book) {
final book = widget.initialData as Book;
_titleController.text = book.title;
_authorController.text = book.author;
_genreController.text = book.genre;
_annotationController.text = book.annotation;
_coverUrl = book.coverUrl;
_selectedStatus = book.status;
} else if (widget.initialData is Map) {
final data = widget.initialData as Map<String, dynamic>;
_titleController.text = data['title']?.toString() ?? '';
_authorController.text = data['author']?.toString() ?? '';
_genreController.text = data['genre']?.toString() ?? '';
_annotationController.text = data['annotation']?.toString() ?? '';
_coverUrl = data['coverUrl']?.toString();
}
}
@override
void dispose() {
_titleController.dispose();
_authorController.dispose();
_genreController.dispose();
_annotationController.dispose();
super.dispose();
}
Future<void> _pickImage() async {
final picker = ImagePicker();
final XFile? image = await picker.pickImage(source: ImageSource.gallery);
if (image != null) {
setState(() {
_coverUrl = image.path;
});
}
}
void _saveBook() {
if (_formKey.currentState!.validate()) {
context.read<AppBloc>().add(
BookSaved({
'title': _titleController.text,
'author': _authorController.text,
'genre': _genreController.text,
'annotation': _annotationController.text,
'coverUrl': _coverUrl,
'status': _selectedStatus,
}),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF112116),
appBar: AppBar(
backgroundColor: const Color(0xFF112116).withValues(alpha: 0.9),
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios_new, color: Colors.white),
onPressed: () {
context.read<AppBloc>().add(
widget.initialData is Book
? const ScreenChanged(AppScreen.details)
: const ScreenChanged(AppScreen.library),
);
},
),
title: const Text('Добавить книгу'),
centerTitle: true,
),
body: Form(
key: _formKey,
child: ListView(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
children: [
Center(
child: GestureDetector(
onTap: _pickImage,
child: Container(
width: 160,
height: 240,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: const Color(0xFF346544),
width: 2,
),
color: const Color(0xFF1A3222),
),
child: _coverUrl != null
? ClipRRect(
borderRadius: BorderRadius.circular(10),
child: _coverUrl!.startsWith('http')
? CachedNetworkImage(
imageUrl: _coverUrl!,
fit: BoxFit.cover,
width: double.infinity,
height: double.infinity,
)
: Image.network(_coverUrl!, fit: BoxFit.cover),
)
: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFF17CF54).withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: const Icon(
Icons.add_a_photo,
color: Color(0xFF17CF54),
size: 28,
),
),
const SizedBox(height: 12),
const Text(
'Загрузить',
style: TextStyle(
fontSize: 12,
color: Color(0xFF93C8A5),
),
),
],
),
),
),
),
const SizedBox(height: 32),
_buildTextField(
controller: _titleController,
label: 'Название',
validator: (value) =>
value?.isEmpty ?? true ? 'Введите название' : null,
),
const SizedBox(height: 16),
_buildTextField(
controller: _authorController,
label: 'Автор',
validator: (value) =>
value?.isEmpty ?? true ? 'Введите автора' : null,
),
const SizedBox(height: 16),
_buildTextField(
controller: _genreController,
label: 'Жанр',
validator: (value) =>
value?.isEmpty ?? true ? 'Введите жанр' : null,
),
const SizedBox(height: 16),
_buildTextField(
controller: _annotationController,
label: 'Аннотация',
maxLines: 4,
validator: (value) =>
value?.isEmpty ?? true ? 'Введите аннотацию' : null,
),
const SizedBox(height: 16),
DropdownButtonFormField<BookStatus>(
initialValue: _selectedStatus,
decoration: InputDecoration(
labelText: 'Статус',
labelStyle: const TextStyle(
color: Color(0xFFE0E0E0),
fontSize: 14,
fontWeight: FontWeight.w600,
),
filled: true,
fillColor: const Color(0xFF1A3222),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Color(0xFF346544)),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Color(0xFF346544)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Color(0xFF17CF54)),
),
),
dropdownColor: const Color(0xFF1A3222),
style: const TextStyle(color: Colors.white),
items: BookStatus.values.map((status) {
return DropdownMenuItem(
value: status,
child: Text(status.displayName),
);
}).toList(),
onChanged: (value) {
if (value != null) {
setState(() {
_selectedStatus = value;
});
}
},
),
const SizedBox(height: 120),
],
),
),
bottomNavigationBar: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF112116).withValues(alpha: 0.95),
border: Border(
top: BorderSide(color: Colors.white.withValues(alpha: 0.05), width: 1),
),
),
child: Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () {
context.read<AppBloc>().add(
widget.initialData is Book
? const ScreenChanged(AppScreen.details)
: const ScreenChanged(AppScreen.library),
);
},
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
side: const BorderSide(color: Colors.transparent),
),
child: const Text('Отмена'),
),
),
const SizedBox(width: 12),
Expanded(
flex: 2,
child: ElevatedButton.icon(
onPressed: _saveBook,
icon: const Icon(Icons.save),
label: const Text('Сохранить'),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF17CF54),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
),
),
),
],
),
),
);
}
Widget _buildTextField({
required TextEditingController controller,
required String label,
int maxLines = 1,
String? Function(String?)? validator,
}) {
return TextFormField(
controller: controller,
maxLines: maxLines,
validator: validator,
style: const TextStyle(color: Colors.white, fontSize: 16),
decoration: InputDecoration(
labelText: label,
labelStyle: const TextStyle(
color: Color(0xFFE0E0E0),
fontSize: 14,
fontWeight: FontWeight.w600,
),
filled: true,
fillColor: const Color(0xFF1A3222),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Color(0xFF346544)),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Color(0xFF346544)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Color(0xFF17CF54)),
),
),
);
}
}

View File

@@ -0,0 +1,110 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../models/models.dart';
import 'add_book_event.dart';
import 'add_book_state.dart';
class AddBookBloc extends Bloc<AddBookEvent, AddBookState> {
AddBookBloc() : super(AddBookState.initial()) {
on<InitializeForm>(_onInitializeForm);
on<UpdateFormField>(_onUpdateFormField);
on<ToggleFavorite>(_onToggleFavorite);
on<ClearForm>(_onClearForm);
on<SubmitBook>(_onSubmitBook);
}
void _onInitializeForm(InitializeForm event, Emitter<AddBookState> emit) {
emit(state.copyWith(
prefilledData: event.prefilledData,
isLoading: false,
));
if (event.prefilledData != null) {
final data = event.prefilledData!;
emit(state.copyWith(
title: data['title'] as String? ?? '',
author: data['author'] as String? ?? '',
genre: data['genre'] as String? ?? '',
annotation: data['annotation'] as String? ?? '',
coverUrl: data['coverUrl'] as String? ?? '',
pages: data['pages'] as int?,
language: data['language'] as String? ?? '',
publishedYear: data['publishedYear'] as int?,
rating: data['rating'] as double? ?? 0.0,
status: data['status'] as BookStatus? ?? BookStatus.wantToRead,
progress: data['progress'] as int? ?? 0,
isFavorite: data['isFavorite'] as bool? ?? false,
));
}
}
void _onUpdateFormField(UpdateFormField event, Emitter<AddBookState> emit) {
switch (event.field) {
case 'title':
emit(state.copyWith(title: event.value as String));
break;
case 'author':
emit(state.copyWith(author: event.value as String));
break;
case 'genre':
emit(state.copyWith(genre: event.value as String));
break;
case 'annotation':
emit(state.copyWith(annotation: event.value as String));
break;
case 'coverUrl':
emit(state.copyWith(coverUrl: event.value as String));
break;
case 'pages':
emit(state.copyWith(pages: event.value as int?));
break;
case 'language':
emit(state.copyWith(language: event.value as String));
break;
case 'publishedYear':
emit(state.copyWith(publishedYear: event.value as int?));
break;
case 'rating':
emit(state.copyWith(rating: event.value as double?));
break;
case 'status':
emit(state.copyWith(status: event.value as BookStatus));
break;
case 'progress':
emit(state.copyWith(progress: event.value as int?));
break;
}
}
void _onToggleFavorite(ToggleFavorite event, Emitter<AddBookState> emit) {
emit(state.copyWith(isFavorite: !state.isFavorite));
}
void _onClearForm(ClearForm event, Emitter<AddBookState> emit) {
emit(AddBookState.initial());
}
void _onSubmitBook(SubmitBook event, Emitter<AddBookState> emit) {
final book = createBook(
id: DateTime.now().millisecondsSinceEpoch.toString(),
title: state.title.isNotEmpty ? state.title : 'Unknown',
author: state.author.isNotEmpty ? state.author : 'Unknown',
genre: state.genre.isNotEmpty ? state.genre : 'Unknown',
annotation: state.annotation,
coverUrl: state.coverUrl.isNotEmpty
? state.coverUrl
: 'https://picsum.photos/seed/${DateTime.now().millisecondsSinceEpoch}/400/600',
pages: state.pages,
language: state.language,
publishedYear: state.publishedYear,
rating: state.rating,
status: state.status,
progress: state.progress,
isFavorite: state.isFavorite,
);
emit(state.copyWith(
submittedBook: book,
isSubmitted: true,
));
}
}

View File

@@ -0,0 +1,27 @@
abstract class AddBookEvent {
const AddBookEvent();
}
class InitializeForm extends AddBookEvent {
final Map<String, dynamic>? prefilledData;
const InitializeForm(this.prefilledData);
}
class UpdateFormField extends AddBookEvent {
final String field;
final dynamic value;
const UpdateFormField(this.field, this.value);
}
class ToggleFavorite extends AddBookEvent {
const ToggleFavorite();
}
class ClearForm extends AddBookEvent {
const ClearForm();
}
class SubmitBook extends AddBookEvent {
const SubmitBook();
}

View File

@@ -0,0 +1,109 @@
import 'package:equatable/equatable.dart';
import '../../../models/models.dart';
class AddBookState extends Equatable {
final String title;
final String author;
final String genre;
final String annotation;
final String coverUrl;
final int? pages;
final String language;
final int? publishedYear;
final double? rating;
final BookStatus status;
final int? progress;
final bool isFavorite;
final Map<String, dynamic>? prefilledData;
final Book? submittedBook;
final bool isSubmitted;
final bool isLoading;
final String? errorMessage;
const AddBookState({
this.title = '',
this.author = '',
this.genre = '',
this.annotation = '',
this.coverUrl = '',
this.pages,
this.language = '',
this.publishedYear,
this.rating,
this.status = BookStatus.wantToRead,
this.progress,
this.isFavorite = false,
this.prefilledData,
this.submittedBook,
this.isSubmitted = false,
this.isLoading = false,
this.errorMessage,
});
factory AddBookState.initial() {
return const AddBookState(
isLoading: false,
);
}
AddBookState copyWith({
String? title,
String? author,
String? genre,
String? annotation,
String? coverUrl,
int? pages,
String? language,
int? publishedYear,
double? rating,
BookStatus? status,
int? progress,
bool? isFavorite,
Map<String, dynamic>? prefilledData,
Book? submittedBook,
bool? isSubmitted,
bool? isLoading,
String? errorMessage,
}) {
return AddBookState(
title: title ?? this.title,
author: author ?? this.author,
genre: genre ?? this.genre,
annotation: annotation ?? this.annotation,
coverUrl: coverUrl ?? this.coverUrl,
pages: pages ?? this.pages,
language: language ?? this.language,
publishedYear: publishedYear ?? this.publishedYear,
rating: rating ?? this.rating,
status: status ?? this.status,
progress: progress ?? this.progress,
isFavorite: isFavorite ?? this.isFavorite,
prefilledData: prefilledData ?? this.prefilledData,
submittedBook: submittedBook ?? this.submittedBook,
isSubmitted: isSubmitted ?? this.isSubmitted,
isLoading: isLoading ?? this.isLoading,
errorMessage: errorMessage ?? this.errorMessage,
);
}
@override
List<Object?> get props => [
title,
author,
genre,
annotation,
coverUrl,
pages,
language,
publishedYear,
rating,
status,
progress,
isFavorite,
prefilledData,
submittedBook,
isSubmitted,
isLoading,
errorMessage,
];
}

View File

@@ -0,0 +1,64 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../models/models.dart';
import 'book_details_event.dart';
import 'book_details_state.dart';
class BookDetailsBloc extends Bloc<BookDetailsEvent, BookDetailsState> {
BookDetailsBloc() : super(BookDetailsState.initial()) {
on<LoadBookDetails>(_onLoadBookDetails);
on<ToggleFavorite>(_onToggleFavorite);
on<UpdateProgress>(_onUpdateProgress);
on<UpdateStatus>(_onUpdateStatus);
on<DeleteBook>(_onDeleteBook);
}
void _onLoadBookDetails(LoadBookDetails event, Emitter<BookDetailsState> emit) {
emit(state.copyWith(
book: event.book,
isLoading: false,
));
}
void _onToggleFavorite(ToggleFavorite event, Emitter<BookDetailsState> emit) {
if (state.book == null) return;
final updatedBook = state.book!.copyWith(
isFavorite: !state.book!.isFavorite,
);
emit(state.copyWith(
book: updatedBook,
));
}
void _onUpdateProgress(UpdateProgress event, Emitter<BookDetailsState> emit) {
if (state.book == null) return;
final updatedBook = state.book!.copyWith(
progress: event.progress,
status: event.progress >= 100 ? BookStatus.done : BookStatus.reading,
);
emit(state.copyWith(
book: updatedBook,
));
}
void _onUpdateStatus(UpdateStatus event, Emitter<BookDetailsState> emit) {
if (state.book == null) return;
final updatedBook = state.book!.copyWith(
status: event.status,
);
emit(state.copyWith(
book: updatedBook,
));
}
void _onDeleteBook(DeleteBook event, Emitter<BookDetailsState> emit) {
emit(state.copyWith(
isDeleted: true,
));
}
}

View File

@@ -0,0 +1,28 @@
import '../../../models/models.dart';
abstract class BookDetailsEvent {
const BookDetailsEvent();
}
class LoadBookDetails extends BookDetailsEvent {
final Book book;
const LoadBookDetails(this.book);
}
class ToggleFavorite extends BookDetailsEvent {
const ToggleFavorite();
}
class UpdateProgress extends BookDetailsEvent {
final int progress;
const UpdateProgress(this.progress);
}
class UpdateStatus extends BookDetailsEvent {
final BookStatus status;
const UpdateStatus(this.status);
}
class DeleteBook extends BookDetailsEvent {
const DeleteBook();
}

View File

@@ -0,0 +1,44 @@
import 'package:equatable/equatable.dart';
import '../../../models/models.dart';
class BookDetailsState extends Equatable {
final Book? book;
final bool isLoading;
final bool isDeleted;
final String? errorMessage;
const BookDetailsState({
this.book,
this.isLoading = false,
this.isDeleted = false,
this.errorMessage,
});
factory BookDetailsState.initial() {
return const BookDetailsState(
isLoading: true,
);
}
BookDetailsState copyWith({
Book? book,
bool? isLoading,
bool? isDeleted,
String? errorMessage,
}) {
return BookDetailsState(
book: book ?? this.book,
isLoading: isLoading ?? this.isLoading,
isDeleted: isDeleted ?? this.isDeleted,
errorMessage: errorMessage ?? this.errorMessage,
);
}
@override
List<Object?> get props => [
book,
isLoading,
isDeleted,
errorMessage,
];
}

View File

@@ -0,0 +1,288 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:cached_network_image/cached_network_image.dart';
import '../../bloc/app_bloc.dart';
import '../../bloc/app_event.dart';
import '../../../models/models.dart';
class BookDetailsScreen extends StatelessWidget {
final Book book;
const BookDetailsScreen({super.key, required this.book});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF112116),
body: CustomScrollView(
slivers: [
SliverAppBar(
expandedHeight: 460,
pinned: true,
backgroundColor: Colors.transparent,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios_new, color: Colors.white),
onPressed: () {
context.read<AppBloc>().add(const ScreenChanged(AppScreen.library));
},
),
actions: [
IconButton(
icon: const Icon(Icons.more_horiz, color: Colors.white),
onPressed: () {},
),
],
flexibleSpace: FlexibleSpaceBar(
background: Stack(
fit: StackFit.expand,
children: [
CachedNetworkImage(
imageUrl: book.coverUrl ??
'https://picsum.photos/seed/placeholder/400/600',
fit: BoxFit.cover,
width: double.infinity,
),
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
const Color(0xFF112116),
],
),
),
),
],
),
),
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.4),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: Colors.white.withValues(alpha: 0.1),
),
),
child: Text(
book.status.displayName,
style: const TextStyle(
color: Color(0xFF17CF54),
fontSize: 12,
fontWeight: FontWeight.bold,
letterSpacing: 1,
),
),
),
const SizedBox(height: 16),
Text(
book.title,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 8),
Text(
book.author,
style: const TextStyle(
fontSize: 18,
color: Color(0xFF93C8A5),
),
),
const SizedBox(height: 24),
Wrap(
spacing: 8,
runSpacing: 8,
children: book.genre.split(',').map((genre) {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
decoration: BoxDecoration(
color: const Color(0xFF244730),
borderRadius: BorderRadius.circular(8),
),
child: Text(
genre.trim(),
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
);
}).toList(),
),
const SizedBox(height: 24),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: () {
context.read<AppBloc>().add(const ScreenChanged(AppScreen.addBook));
},
icon: const Icon(Icons.edit_square),
label: const Text('Edit Details'),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF17CF54),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
onPressed: () {
context.read<AppBloc>().add(BookDeleted(book.id));
},
icon: const Icon(Icons.delete),
label: const Text('Delete'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white.withValues(alpha: 0.05),
foregroundColor: Colors.white,
side: BorderSide(
color: Colors.white.withValues(alpha: 0.1),
),
padding: const EdgeInsets.symmetric(vertical: 16),
),
),
),
],
),
const SizedBox(height: 32),
const Align(
alignment: Alignment.centerLeft,
child: Text(
'About',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: Colors.white.withValues(alpha: 0.05),
),
),
child: Text(
book.annotation,
style: const TextStyle(
fontSize: 15,
color: Color(0xFF93C8A5),
height: 1.5,
),
),
),
const SizedBox(height: 24),
GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 2,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 1.2,
children: [
_buildInfoCard(
Icons.menu_book,
'Pages',
book.pages?.toString() ?? 'N/A',
Colors.blue,
),
_buildInfoCard(
Icons.language,
'Language',
book.language ?? 'Russian',
Colors.purple,
),
_buildInfoCard(
Icons.calendar_month,
'Published',
book.publishedYear?.toString() ?? 'N/A',
Colors.orange,
),
_buildInfoCard(
Icons.star,
'Rating',
'${book.rating ?? 0}/5',
Colors.amber,
),
],
),
],
),
),
),
],
),
);
}
Widget _buildInfoCard(IconData icon, String label, String value, Color color) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Colors.white.withValues(alpha: 0.05),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: color, size: 18),
),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
color: Colors.grey.shade500,
letterSpacing: 0.5,
),
),
const SizedBox(height: 2),
Text(
value,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
],
),
);
}
}

View File

@@ -0,0 +1,99 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../models/models.dart';
import 'categories_event.dart';
import 'categories_state.dart';
class CategoriesBloc extends Bloc<CategoriesEvent, CategoriesState> {
CategoriesBloc() : super(CategoriesState.initial()) {
on<LoadCategories>(_onLoadCategories);
on<SelectCategory>(_onSelectCategory);
on<SearchCategories>(_onSearchCategories);
}
void _onLoadCategories(LoadCategories event, Emitter<CategoriesState> emit) {
final categories = _getCategories();
emit(state.copyWith(
categories: categories,
filteredCategories: categories,
isLoading: false,
));
}
void _onSelectCategory(SelectCategory event, Emitter<CategoriesState> emit) {
emit(state.copyWith(selectedCategory: event.category));
}
void _onSearchCategories(SearchCategories event, Emitter<CategoriesState> emit) {
final query = event.query.toLowerCase();
final filtered = state.categories.where((category) {
return category.name.toLowerCase().contains(query);
}).toList();
emit(state.copyWith(
searchQuery: event.query,
filteredCategories: filtered,
));
}
List<Category> _getCategories() {
return [
createCategory(
id: '1',
name: 'Classic',
count: 15,
icon: Icons.category,
colorClass: 'category-classic',
),
createCategory(
id: '2',
name: 'Sci-Fi',
count: 8,
icon: Icons.rocket_launch,
colorClass: 'category-scifi',
),
createCategory(
id: '3',
name: 'Fantasy',
count: 12,
icon: Icons.auto_awesome,
colorClass: 'category-fantasy',
),
createCategory(
id: '4',
name: 'Mystery',
count: 6,
icon: Icons.search,
colorClass: 'category-mystery',
),
createCategory(
id: '5',
name: 'Romance',
count: 10,
icon: Icons.favorite,
colorClass: 'category-romance',
),
createCategory(
id: '6',
name: 'Non-fiction',
count: 9,
icon: Icons.book,
colorClass: 'category-nonfiction',
),
createCategory(
id: '7',
name: 'Dystopian',
count: 4,
icon: Icons.nightlife,
colorClass: 'category-dystopian',
),
createCategory(
id: '8',
name: 'Adventure',
count: 7,
icon: Icons.explore,
colorClass: 'category-adventure',
),
];
}
}

View File

@@ -0,0 +1,19 @@
import '../../../models/models.dart';
abstract class CategoriesEvent {
const CategoriesEvent();
}
class LoadCategories extends CategoriesEvent {
const LoadCategories();
}
class SelectCategory extends CategoriesEvent {
final Category category;
const SelectCategory(this.category);
}
class SearchCategories extends CategoriesEvent {
final String query;
const SearchCategories(this.query);
}

View File

@@ -0,0 +1,54 @@
import 'package:equatable/equatable.dart';
import '../../../models/models.dart';
class CategoriesState extends Equatable {
final List<Category> categories;
final List<Category> filteredCategories;
final Category? selectedCategory;
final String searchQuery;
final bool isLoading;
final String? errorMessage;
const CategoriesState({
this.categories = const [],
this.filteredCategories = const [],
this.selectedCategory,
this.searchQuery = '',
this.isLoading = false,
this.errorMessage,
});
factory CategoriesState.initial() {
return const CategoriesState(
isLoading: true,
);
}
CategoriesState copyWith({
List<Category>? categories,
List<Category>? filteredCategories,
Category? selectedCategory,
String? searchQuery,
bool? isLoading,
String? errorMessage,
}) {
return CategoriesState(
categories: categories ?? this.categories,
filteredCategories: filteredCategories ?? this.filteredCategories,
selectedCategory: selectedCategory,
searchQuery: searchQuery ?? this.searchQuery,
isLoading: isLoading ?? this.isLoading,
errorMessage: errorMessage ?? this.errorMessage,
);
}
@override
List<Object?> get props => [
categories,
filteredCategories,
selectedCategory,
searchQuery,
isLoading,
errorMessage,
];
}

View File

@@ -0,0 +1,224 @@
import 'package:flutter/material.dart';
import '../../../models/models.dart';
class CategoriesScreen extends StatelessWidget {
const CategoriesScreen({super.key});
static const List<Category> categories = [
(
id: 'fiction',
name: 'Фантастика',
count: 24,
icon: Icons.rocket_launch,
colorClass: 'bg-indigo-500/20 text-indigo-300',
),
(
id: 'fantasy',
name: 'Фэнтези',
count: 18,
icon: Icons.auto_fix_high,
colorClass: 'bg-purple-500/20 text-purple-300',
),
(
id: 'nonfiction',
name: 'Научпоп',
count: 7,
icon: Icons.psychology,
colorClass: 'bg-teal-500/20 text-teal-300',
),
(
id: 'business',
name: 'Бизнес',
count: 3,
icon: Icons.business_center,
colorClass: 'bg-blue-500/20 text-blue-300',
),
(
id: 'education',
name: 'Учебная',
count: 0,
icon: Icons.school,
colorClass: 'bg-orange-500/20 text-orange-300',
),
(
id: 'classics',
name: 'Классика',
count: 15,
icon: Icons.history_edu,
colorClass: 'bg-amber-500/20 text-amber-300',
),
];
Color _getCategoryColor(String colorClass) {
if (colorClass.contains('indigo')) return Colors.indigo.shade400;
if (colorClass.contains('purple')) return Colors.purple.shade400;
if (colorClass.contains('teal')) return Colors.teal.shade400;
if (colorClass.contains('blue')) return Colors.blue.shade400;
if (colorClass.contains('orange')) return Colors.orange.shade400;
if (colorClass.contains('amber')) return Colors.amber.shade400;
return Colors.grey.shade400;
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF112116),
body: Column(
children: [
_buildHeader(),
Expanded(
child: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: categories.length,
itemBuilder: (context, index) {
final category = categories[index];
return _buildCategoryCard(category);
},
),
),
],
),
);
}
Widget _buildHeader() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: const Color(0xFF112116).withValues(alpha: 0.95),
border: Border(
bottom: BorderSide(color: Colors.white.withValues(alpha: 0.05), width: 1),
),
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Изменить',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.6),
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: const Color(0xFF17CF54).withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: const Icon(
Icons.add,
color: Color(0xFF17CF54),
size: 24,
),
),
],
),
const SizedBox(height: 8),
const Text(
'Категории',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 8),
Container(
height: 40,
decoration: BoxDecoration(
color: const Color(0xFF1A3023),
borderRadius: BorderRadius.circular(8),
),
child: TextField(
style: const TextStyle(color: Colors.white, fontSize: 16),
decoration: InputDecoration(
hintText: 'Поиск жанра...',
hintStyle: TextStyle(
color: Colors.white.withValues(alpha: 0.4),
),
prefixIcon: Icon(
Icons.search,
color: Colors.white.withValues(alpha: 0.4),
size: 20,
),
border: InputBorder.none,
contentPadding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
),
),
),
],
),
);
}
Widget _buildCategoryCard(Category category) {
return GestureDetector(
onTap: () {
// Navigate to category details
},
child: Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF1A3023),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white.withValues(alpha: 0.05)),
),
child: Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: _getCategoryColor(category.colorClass).withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
category.icon,
color: _getCategoryColor(category.colorClass),
size: 24,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
category.name,
style: const TextStyle(
fontSize: 17,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
Text(
category.count > 0
? '${category.count} книги'
: 'Нет книг',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: Colors.white.withValues(alpha: 0.5),
),
),
],
),
),
Icon(
Icons.chevron_right,
color: Colors.white.withValues(alpha: 0.2),
size: 24,
),
],
),
),
);
}
}

View File

@@ -0,0 +1,22 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../models/models.dart';
import 'home_event.dart';
import 'home_state.dart';
class HomeBloc extends Bloc<HomeEvent, HomeState> {
HomeBloc() : super(HomeState.initial()) {
on<LoadHomeData>(_onLoadHomeData);
on<NavigateToScreen>(_onNavigateToScreen);
}
void _onLoadHomeData(LoadHomeData event, Emitter<HomeState> emit) {
emit(state.copyWith(
currentScreen: AppScreen.library,
isLoading: false,
));
}
void _onNavigateToScreen(NavigateToScreen event, Emitter<HomeState> emit) {
emit(state.copyWith(currentScreen: event.screen));
}
}

View File

@@ -0,0 +1,14 @@
import '../../../models/models.dart';
abstract class HomeEvent {
const HomeEvent();
}
class LoadHomeData extends HomeEvent {
const LoadHomeData();
}
class NavigateToScreen extends HomeEvent {
final AppScreen screen;
const NavigateToScreen(this.screen);
}

View File

@@ -0,0 +1,39 @@
import 'package:equatable/equatable.dart';
import '../../../models/models.dart';
class HomeState extends Equatable {
final AppScreen currentScreen;
final bool isLoading;
final String? errorMessage;
const HomeState({
this.currentScreen = AppScreen.library,
this.isLoading = false,
this.errorMessage,
});
factory HomeState.initial() {
return const HomeState(
isLoading: true,
);
}
HomeState copyWith({
AppScreen? currentScreen,
bool? isLoading,
String? errorMessage,
}) {
return HomeState(
currentScreen: currentScreen ?? this.currentScreen,
isLoading: isLoading ?? this.isLoading,
errorMessage: errorMessage ?? this.errorMessage,
);
}
@override
List<Object?> get props => [
currentScreen,
isLoading,
errorMessage,
];
}

View File

@@ -0,0 +1,68 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../bloc/app_bloc.dart';
import '../../bloc/app_state.dart';
import '../../../models/models.dart';
import '../library/library_screen.dart';
import '../categories/categories_screen.dart';
import '../book_details/book_details_screen.dart';
import '../add_book/add_book_screen.dart';
import '../scanner/scanner_screen.dart';
import '../wishlist/wishlist_screen.dart';
import '../settings/settings_screen.dart';
import '../../widgets/bottom_nav.dart';
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: BlocBuilder<AppBloc, AppState>(
builder: (context, state) {
final currentScreen = state.currentScreen;
final hideNav = [
AppScreen.scanner,
AppScreen.details,
AppScreen.addBook,
].contains(currentScreen);
return Stack(
children: [
_buildScreen(context, state),
if (!hideNav)
Positioned(
left: 0,
right: 0,
bottom: 0,
child: BottomNav(currentScreen: currentScreen),
),
],
);
},
),
);
}
Widget _buildScreen(BuildContext context, AppState state) {
switch (state.currentScreen) {
case AppScreen.library:
return const LibraryScreen();
case AppScreen.categories:
return const CategoriesScreen();
case AppScreen.wishlist:
return const WishlistScreen();
case AppScreen.settings:
return const SettingsScreen();
case AppScreen.details:
if (state.selectedBook == null) return const SizedBox();
return BookDetailsScreen(book: state.selectedBook!);
case AppScreen.addBook:
return AddBookScreen(
initialData: state.selectedBook ?? state.prefilledData,
);
case AppScreen.scanner:
return const ScannerScreen();
}
}
}

View File

@@ -0,0 +1,124 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../models/models.dart';
import 'library_event.dart';
import 'library_state.dart';
class LibraryBloc extends Bloc<LibraryEvent, LibraryState> {
LibraryBloc() : super(LibraryState.initial()) {
on<LoadBooks>(_onLoadBooks);
on<SearchBooks>(_onSearchBooks);
on<BookSelected>(_onBookSelected);
on<FilterByStatus>(_onFilterByStatus);
on<ClearFilters>(_onClearFilters);
}
static List<Book> get _initialBooks => [
createBook(
id: '1',
title: 'Великий Гэтсби',
author: 'Ф. Скотт Фицджеральд',
genre: 'Classic',
annotation:
'История о несбывшейся любви и трагедии американской мечты на фоне бурных двадцатых годов.',
coverUrl: 'https://picsum.photos/seed/gatsby/400/600',
pages: 208,
language: 'English',
publishedYear: 1925,
rating: 4.8,
status: BookStatus.reading,
progress: 45,
isFavorite: true,
),
createBook(
id: '2',
title: '1984',
author: 'Джордж Оруэлл',
genre: 'Dystopian',
annotation:
'Антиутопия о тоталитарном государстве, где мысли контролируются, а правда переменчива.',
coverUrl: 'https://picsum.photos/seed/1984/400/600',
pages: 328,
language: 'English',
publishedYear: 1949,
rating: 4.9,
status: BookStatus.wantToRead,
isFavorite: true,
),
createBook(
id: '3',
title: 'Дюна',
author: 'Фрэнк Герберт',
genre: 'Sci-Fi',
annotation:
'Эпическая сага о борьбе за власть над самой важной планетой во Вселенной.',
coverUrl: 'https://picsum.photos/seed/dune/400/600',
pages: 896,
language: 'English',
publishedYear: 1965,
rating: 4.7,
status: BookStatus.reading,
progress: 12,
isFavorite: false,
),
createBook(
id: '4',
title: 'Хоббит',
author: 'Дж. Р. Р. Толкин',
genre: 'Fantasy',
annotation:
'Путешествие Бильбо Бэггинса туда и обратно в поисках сокровищ гномов.',
coverUrl: 'https://picsum.photos/seed/hobbit/400/600',
pages: 310,
language: 'English',
publishedYear: 1937,
rating: 4.9,
status: BookStatus.done,
isFavorite: false,
),
];
void _onLoadBooks(LoadBooks event, Emitter<LibraryState> emit) {
emit(state.copyWith(
books: _initialBooks,
filteredBooks: _initialBooks,
isLoading: false,
));
}
void _onSearchBooks(SearchBooks event, Emitter<LibraryState> emit) {
final query = event.query.toLowerCase();
final filtered = state.books.where((Book book) {
return book.title.toLowerCase().contains(query) ||
book.author.toLowerCase().contains(query) ||
book.genre.toLowerCase().contains(query);
}).toList();
emit(state.copyWith(
searchQuery: event.query,
filteredBooks: filtered,
));
}
void _onBookSelected(BookSelected event, Emitter<LibraryState> emit) {
emit(state.copyWith(selectedBook: event.book));
}
void _onFilterByStatus(FilterByStatus event, Emitter<LibraryState> emit) {
final filtered = state.books.where((Book book) {
return book.status == event.status;
}).toList();
emit(state.copyWith(
statusFilter: event.status,
filteredBooks: filtered,
));
}
void _onClearFilters(ClearFilters event, Emitter<LibraryState> emit) {
emit(state.copyWith(
searchQuery: '',
statusFilter: null,
filteredBooks: state.books,
));
}
}

View File

@@ -0,0 +1,28 @@
import '../../../models/models.dart';
abstract class LibraryEvent {
const LibraryEvent();
}
class LoadBooks extends LibraryEvent {
const LoadBooks();
}
class SearchBooks extends LibraryEvent {
final String query;
const SearchBooks(this.query);
}
class BookSelected extends LibraryEvent {
final Book book;
const BookSelected(this.book);
}
class FilterByStatus extends LibraryEvent {
final BookStatus status;
const FilterByStatus(this.status);
}
class ClearFilters extends LibraryEvent {
const ClearFilters();
}

View File

@@ -0,0 +1,59 @@
import 'package:equatable/equatable.dart';
import '../../../models/models.dart';
class LibraryState extends Equatable {
final List<Book> books;
final List<Book> filteredBooks;
final Book? selectedBook;
final String searchQuery;
final BookStatus? statusFilter;
final bool isLoading;
final String? errorMessage;
const LibraryState({
this.books = const [],
this.filteredBooks = const [],
this.selectedBook,
this.searchQuery = '',
this.statusFilter,
this.isLoading = false,
this.errorMessage,
});
factory LibraryState.initial() {
return const LibraryState(
isLoading: true,
);
}
LibraryState copyWith({
List<Book>? books,
List<Book>? filteredBooks,
Book? selectedBook,
String? searchQuery,
BookStatus? statusFilter,
bool? isLoading,
String? errorMessage,
}) {
return LibraryState(
books: books ?? this.books,
filteredBooks: filteredBooks ?? this.filteredBooks,
selectedBook: selectedBook,
searchQuery: searchQuery ?? this.searchQuery,
statusFilter: statusFilter ?? this.statusFilter,
isLoading: isLoading ?? this.isLoading,
errorMessage: errorMessage ?? this.errorMessage,
);
}
@override
List<Object?> get props => [
books,
filteredBooks,
selectedBook,
searchQuery,
statusFilter,
isLoading,
errorMessage,
];
}

View File

@@ -0,0 +1,288 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:cached_network_image/cached_network_image.dart';
import '../../bloc/app_bloc.dart';
import '../../bloc/app_event.dart';
import '../../bloc/app_state.dart';
import '../../../models/models.dart';
class LibraryScreen extends StatefulWidget {
const LibraryScreen({super.key});
@override
State<LibraryScreen> createState() => _LibraryScreenState();
}
class _LibraryScreenState extends State<LibraryScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF112116),
body: Column(
children: [
_buildHeader(context),
Expanded(child: _buildBookGrid(context)),
],
),
floatingActionButton: Padding(
padding: const EdgeInsets.only(bottom: 80),
child: FloatingActionButton(
onPressed: () {
context.read<AppBloc>().add(const AddBookClicked());
},
backgroundColor: const Color(0xFF17CF54),
elevation: 8,
child: const Icon(Icons.add, size: 28),
),
),
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
);
}
Widget _buildHeader(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: const Color(0xFF112116).withValues(alpha: 0.95),
border: Border(
bottom: BorderSide(color: Colors.white.withValues(alpha: 0.05), width: 1),
),
),
child: Column(
children: [
Row(
children: [
const SizedBox(width: 40),
Expanded(
child: Text(
'Книжная полка',
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
const SizedBox(
width: 40,
child: Icon(
Icons.notifications_outlined,
color: Colors.white,
size: 24,
),
),
],
),
const SizedBox(height: 8),
BlocBuilder<AppBloc, AppState>(
buildWhen: (previous, current) =>
previous.searchQuery != current.searchQuery,
builder: (context, state) {
return Container(
height: 48,
decoration: BoxDecoration(
color: const Color(0xFF244730),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white.withValues(alpha: 0.05)),
),
child: TextField(
onChanged: (value) {
context.read<AppBloc>().add(SearchChanged(value));
},
style: const TextStyle(color: Colors.white, fontSize: 16),
decoration: InputDecoration(
hintText: 'Поиск по названию или автору...',
hintStyle: const TextStyle(
color: Color(0xFF93C8A5),
),
prefixIcon: const Icon(
Icons.search,
color: Color(0xFF93C8A5),
size: 20,
),
border: InputBorder.none,
contentPadding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
),
),
);
},
),
],
),
);
}
Widget _buildBookGrid(BuildContext context) {
return BlocBuilder<AppBloc, AppState>(
builder: (context, state) {
final filteredBooks = state.books.where((book) {
final query = state.searchQuery.toLowerCase();
return book.title.toLowerCase().contains(query) ||
book.author.toLowerCase().contains(query);
}).toList();
if (filteredBooks.isEmpty) {
return Center(
child: Text(
'Книги не найдены',
style: TextStyle(
color: const Color(0xFF93C8A5),
fontSize: 16,
),
),
);
}
return GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
childAspectRatio: 0.67,
),
itemCount: filteredBooks.length,
itemBuilder: (context, index) {
final book = filteredBooks[index];
return _buildBookCard(context, book);
},
);
},
);
}
Widget _buildBookCard(BuildContext context, Book book) {
return GestureDetector(
onTap: () {
context.read<AppBloc>().add(BookClicked(book));
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Stack(
children: [
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: const Color(0xFF1A3222),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.3),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: CachedNetworkImage(
imageUrl: book.coverUrl ??
'https://picsum.photos/seed/placeholder/400/600',
fit: BoxFit.cover,
width: double.infinity,
placeholder: (context, url) => const Center(
child: CircularProgressIndicator(
color: Color(0xFF17CF54),
),
),
errorWidget: (context, url, error) => Container(
color: const Color(0xFF1A3222),
child: const Icon(
Icons.book_outlined,
size: 48,
color: Color(0xFF93C8A5),
),
),
),
),
),
if (book.status == BookStatus.reading && book.progress != null)
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Container(
height: 4,
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.5),
),
child: FractionallySizedBox(
widthFactor: book.progress! / 100,
alignment: Alignment.centerLeft,
child: Container(
color: const Color(0xFF17CF54),
),
),
),
),
if (book.status == BookStatus.done)
Positioned(
top: 8,
left: 8,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
color: const Color(0xFF17CF54),
borderRadius: BorderRadius.circular(12),
),
child: const Text(
'DONE',
style: TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
),
if (book.isFavorite)
Positioned(
top: 8,
right: 8,
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.6),
shape: BoxShape.circle,
),
child: const Icon(
Icons.favorite,
size: 14,
color: Colors.white,
),
),
),
],
),
),
const SizedBox(height: 8),
Text(
book.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
Text(
book.author,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Color(0xFF93C8A5),
fontSize: 14,
),
),
],
),
);
}
}

View File

@@ -0,0 +1,40 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'scanner_event.dart';
import 'scanner_state.dart';
class ScannerBloc extends Bloc<ScannerEvent, ScannerState> {
ScannerBloc() : super(ScannerState.initial()) {
on<StartScanning>(_onStartScanning);
on<StopScanning>(_onStopScanning);
on<BookDetected>(_onBookDetected);
on<ClearDetectedBook>(_onClearDetectedBook);
}
void _onStartScanning(StartScanning event, Emitter<ScannerState> emit) {
emit(state.copyWith(
isScanning: true,
isProcessing: false,
errorMessage: null,
));
}
void _onStopScanning(StopScanning event, Emitter<ScannerState> emit) {
emit(state.copyWith(isScanning: false));
}
void _onBookDetected(BookDetected event, Emitter<ScannerState> emit) {
emit(state.copyWith(
detectedBookData: event.bookData,
isProcessing: true,
isScanning: false,
));
}
void _onClearDetectedBook(ClearDetectedBook event, Emitter<ScannerState> emit) {
emit(state.copyWith(
isScanning: false,
clearDetectedBookData: true,
isProcessing: false,
));
}
}

View File

@@ -0,0 +1,20 @@
abstract class ScannerEvent {
const ScannerEvent();
}
class StartScanning extends ScannerEvent {
const StartScanning();
}
class StopScanning extends ScannerEvent {
const StopScanning();
}
class BookDetected extends ScannerEvent {
final Map<String, dynamic> bookData;
const BookDetected(this.bookData);
}
class ClearDetectedBook extends ScannerEvent {
const ClearDetectedBook();
}

View File

@@ -0,0 +1,42 @@
import 'package:equatable/equatable.dart';
class ScannerState extends Equatable {
final bool isScanning;
final bool isProcessing;
final Map<String, dynamic>? detectedBookData;
final String? errorMessage;
const ScannerState({
this.isScanning = false,
this.isProcessing = false,
this.detectedBookData,
this.errorMessage,
});
factory ScannerState.initial() {
return const ScannerState();
}
ScannerState copyWith({
bool? isScanning,
bool? isProcessing,
Map<String, dynamic>? detectedBookData,
String? errorMessage,
bool clearDetectedBookData = false,
}) {
return ScannerState(
isScanning: isScanning ?? this.isScanning,
isProcessing: isProcessing ?? this.isProcessing,
detectedBookData: clearDetectedBookData ? null : (detectedBookData ?? this.detectedBookData),
errorMessage: errorMessage ?? this.errorMessage,
);
}
@override
List<Object?> get props => [
isScanning,
isProcessing,
detectedBookData,
errorMessage,
];
}

View File

@@ -0,0 +1,270 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../bloc/app_bloc.dart';
import '../../bloc/app_event.dart';
import '../../../models/models.dart';
class ScannerScreen extends StatelessWidget {
const ScannerScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Stack(
children: [
// Camera placeholder
Container(
color: Colors.black87,
width: double.infinity,
height: double.infinity,
),
// Header
Positioned(
top: 48,
left: 0,
right: 0,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
onPressed: () {
context
.read<AppBloc>()
.add(const ScreenChanged(AppScreen.addBook));
},
icon: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.4),
shape: BoxShape.circle,
),
child: const Icon(Icons.close, color: Colors.white, size: 24),
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 6,
),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.4),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: Colors.white.withValues(alpha: 0.1),
),
),
child: const Text(
'СКАНЕР',
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w500,
letterSpacing: 1,
),
),
),
const SizedBox(width: 40),
],
),
),
),
// Scanner frame
Center(
child: Container(
width: MediaQuery.of(context).size.width * 0.75,
height: MediaQuery.of(context).size.width * 0.75 * 1.5,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: Colors.white.withValues(alpha: 0.2),
width: 2,
),
),
child: Stack(
children: [
// Corner accents - top left
Positioned(
top: -2,
left: -2,
child: ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(8),
),
child: Container(
width: 32,
height: 32,
decoration: BoxDecoration(
border: Border(
left: const BorderSide(color: Color(0xFF17CF54), width: 4),
top: const BorderSide(color: Color(0xFF17CF54), width: 4),
right: const BorderSide(color: Colors.transparent, width: 4),
bottom: const BorderSide(color: Colors.transparent, width: 4),
),
),
),
),
),
// Corner accents - top right
Positioned(
top: -2,
right: -2,
child: ClipRRect(
borderRadius: const BorderRadius.only(
topRight: Radius.circular(8),
),
child: Container(
width: 32,
height: 32,
decoration: BoxDecoration(
border: Border(
right: const BorderSide(color: Color(0xFF17CF54), width: 4),
top: const BorderSide(color: Color(0xFF17CF54), width: 4),
left: const BorderSide(color: Colors.transparent, width: 4),
bottom: const BorderSide(color: Colors.transparent, width: 4),
),
),
),
),
),
// Corner accents - bottom left
Positioned(
bottom: -2,
left: -2,
child: ClipRRect(
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(8),
),
child: Container(
width: 32,
height: 32,
decoration: BoxDecoration(
border: Border(
left: const BorderSide(color: Color(0xFF17CF54), width: 4),
bottom: const BorderSide(color: Color(0xFF17CF54), width: 4),
right: const BorderSide(color: Colors.transparent, width: 4),
top: const BorderSide(color: Colors.transparent, width: 4),
),
),
),
),
),
// Corner accents - bottom right
Positioned(
bottom: -2,
right: -2,
child: ClipRRect(
borderRadius: const BorderRadius.only(
bottomRight: Radius.circular(8),
),
child: Container(
width: 32,
height: 32,
decoration: BoxDecoration(
border: Border(
right: const BorderSide(color: Color(0xFF17CF54), width: 4),
bottom: const BorderSide(color: Color(0xFF17CF54), width: 4),
left: const BorderSide(color: Colors.transparent, width: 4),
top: const BorderSide(color: Colors.transparent, width: 4),
),
),
),
),
),
// Scan line animation
Center(
child: Container(
height: 2,
width: double.infinity,
margin: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: const Color(0xFF17CF54),
boxShadow: [
BoxShadow(
color: const Color(0xFF17CF54).withValues(alpha: 0.8),
blurRadius: 15,
spreadRadius: 2,
),
],
),
),
),
],
),
),
),
// Instructions
Positioned(
bottom: 120,
left: 0,
right: 0,
child: Column(
children: [
const Text(
'Поместите обложку в рамку',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 16),
const Text(
'Камера будет добавлена в будущих обновлениях',
style: TextStyle(
color: Color(0xFF17CF54),
fontSize: 14,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
],
),
),
// Capture button
Positioned(
bottom: 40,
left: 0,
right: 0,
child: Center(
child: GestureDetector(
onTap: () {
// Simulate detection
context.read<AppBloc>().add(
BookDetected({
'title': 'Новая книга',
'author': 'Неизвестный автор',
'genre': 'Неизвестный жанр',
'annotation': 'Добавьте описание книги',
}),
);
},
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
border: Border.all(
color: Colors.white.withValues(alpha: 0.3),
width: 4,
),
boxShadow: [
BoxShadow(
color: Colors.white.withValues(alpha: 0.3),
blurRadius: 20,
spreadRadius: 2,
),
],
),
),
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,39 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'settings_event.dart';
import 'settings_state.dart';
class SettingsBloc extends Bloc<SettingsEvent, SettingsState> {
SettingsBloc() : super(SettingsState.initial()) {
on<LoadSettings>(_onLoadSettings);
on<UpdateTheme>(_onUpdateTheme);
on<UpdateLanguage>(_onUpdateLanguage);
on<ToggleNotifications>(_onToggleNotifications);
on<ClearData>(_onClearData);
}
void _onLoadSettings(LoadSettings event, Emitter<SettingsState> emit) {
emit(state.copyWith(
isDarkMode: false,
language: 'English',
notificationsEnabled: true,
isLoading: false,
));
}
void _onUpdateTheme(UpdateTheme event, Emitter<SettingsState> emit) {
emit(state.copyWith(isDarkMode: event.isDarkMode));
}
void _onUpdateLanguage(UpdateLanguage event, Emitter<SettingsState> emit) {
emit(state.copyWith(language: event.language));
}
void _onToggleNotifications(
ToggleNotifications event, Emitter<SettingsState> emit) {
emit(state.copyWith(notificationsEnabled: !state.notificationsEnabled));
}
void _onClearData(ClearData event, Emitter<SettingsState> emit) {
emit(state.copyWith(dataCleared: true));
}
}

View File

@@ -0,0 +1,25 @@
abstract class SettingsEvent {
const SettingsEvent();
}
class LoadSettings extends SettingsEvent {
const LoadSettings();
}
class UpdateTheme extends SettingsEvent {
final bool isDarkMode;
const UpdateTheme(this.isDarkMode);
}
class UpdateLanguage extends SettingsEvent {
final String language;
const UpdateLanguage(this.language);
}
class ToggleNotifications extends SettingsEvent {
const ToggleNotifications();
}
class ClearData extends SettingsEvent {
const ClearData();
}

View File

@@ -0,0 +1,53 @@
import 'package:equatable/equatable.dart';
class SettingsState extends Equatable {
final bool isDarkMode;
final String language;
final bool notificationsEnabled;
final bool dataCleared;
final bool isLoading;
final String? errorMessage;
const SettingsState({
this.isDarkMode = false,
this.language = 'English',
this.notificationsEnabled = true,
this.dataCleared = false,
this.isLoading = false,
this.errorMessage,
});
factory SettingsState.initial() {
return const SettingsState(
isLoading: true,
);
}
SettingsState copyWith({
bool? isDarkMode,
String? language,
bool? notificationsEnabled,
bool? dataCleared,
bool? isLoading,
String? errorMessage,
}) {
return SettingsState(
isDarkMode: isDarkMode ?? this.isDarkMode,
language: language ?? this.language,
notificationsEnabled: notificationsEnabled ?? this.notificationsEnabled,
dataCleared: dataCleared ?? this.dataCleared,
isLoading: isLoading ?? this.isLoading,
errorMessage: errorMessage ?? this.errorMessage,
);
}
@override
List<Object?> get props => [
isDarkMode,
language,
notificationsEnabled,
dataCleared,
isLoading,
errorMessage,
];
}

View File

@@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
class SettingsScreen extends StatelessWidget {
const SettingsScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF112116),
body: Center(
child: Padding(
padding: const EdgeInsets.all(40),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Настройки',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 8),
Text(
'Персонализируйте ваше приложение.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
color: const Color(0xFF93C8A5),
),
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,93 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../models/models.dart';
import 'wishlist_event.dart';
import 'wishlist_state.dart';
class WishlistBloc extends Bloc<WishlistEvent, WishlistState> {
WishlistBloc() : super(WishlistState.initial()) {
on<LoadWishlist>(_onLoadWishlist);
on<RemoveFromWishlist>(_onRemoveFromWishlist);
on<SearchWishlist>(_onSearchWishlist);
on<MoveToLibrary>(_onMoveToLibrary);
}
static List<Book> get _initialBooks => [
createBook(
id: '2',
title: '1984',
author: 'Джордж Оруэлл',
genre: 'Dystopian',
annotation:
'Антиутопия о тоталитарном государстве, где мысли контролируются, а правда переменчива.',
coverUrl: 'https://picsum.photos/seed/1984/400/600',
pages: 328,
language: 'English',
publishedYear: 1949,
rating: 4.9,
status: BookStatus.wantToRead,
isFavorite: true,
),
];
void _onLoadWishlist(LoadWishlist event, Emitter<WishlistState> emit) {
final wishlistBooks = _initialBooks.where((book) {
return book.status == BookStatus.wantToRead || book.isFavorite;
}).toList();
emit(state.copyWith(
books: wishlistBooks,
filteredBooks: wishlistBooks,
isLoading: false,
));
}
void _onRemoveFromWishlist(RemoveFromWishlist event, Emitter<WishlistState> emit) {
final updatedBooks = state.books.where((book) => book.id != event.bookId).toList();
final updatedFiltered = state.filteredBooks.where((book) => book.id != event.bookId).toList();
emit(state.copyWith(
books: updatedBooks,
filteredBooks: updatedFiltered,
));
}
void _onSearchWishlist(SearchWishlist event, Emitter<WishlistState> emit) {
final query = event.query.toLowerCase();
final filtered = state.books.where((book) {
return book.title.toLowerCase().contains(query) ||
book.author.toLowerCase().contains(query) ||
book.genre.toLowerCase().contains(query);
}).toList();
emit(state.copyWith(
searchQuery: event.query,
filteredBooks: filtered,
));
}
void _onMoveToLibrary(MoveToLibrary event, Emitter<WishlistState> emit) {
final updatedBooks = state.books.map((book) {
if (book.id == event.bookId) {
return book.copyWith(
status: BookStatus.reading,
);
}
return book;
}).toList();
final updatedFiltered = state.filteredBooks.map((book) {
if (book.id == event.bookId) {
return book.copyWith(
status: BookStatus.reading,
);
}
return book;
}).toList();
emit(state.copyWith(
books: updatedBooks,
filteredBooks: updatedFiltered,
movedBookId: event.bookId,
));
}
}

View File

@@ -0,0 +1,22 @@
abstract class WishlistEvent {
const WishlistEvent();
}
class LoadWishlist extends WishlistEvent {
const LoadWishlist();
}
class RemoveFromWishlist extends WishlistEvent {
final String bookId;
const RemoveFromWishlist(this.bookId);
}
class SearchWishlist extends WishlistEvent {
final String query;
const SearchWishlist(this.query);
}
class MoveToLibrary extends WishlistEvent {
final String bookId;
const MoveToLibrary(this.bookId);
}

View File

@@ -0,0 +1,54 @@
import 'package:equatable/equatable.dart';
import '../../../models/models.dart';
class WishlistState extends Equatable {
final List<Book> books;
final List<Book> filteredBooks;
final String searchQuery;
final String? movedBookId;
final bool isLoading;
final String? errorMessage;
const WishlistState({
this.books = const [],
this.filteredBooks = const [],
this.searchQuery = '',
this.movedBookId,
this.isLoading = false,
this.errorMessage,
});
factory WishlistState.initial() {
return const WishlistState(
isLoading: true,
);
}
WishlistState copyWith({
List<Book>? books,
List<Book>? filteredBooks,
String? searchQuery,
String? movedBookId,
bool? isLoading,
String? errorMessage,
}) {
return WishlistState(
books: books ?? this.books,
filteredBooks: filteredBooks ?? this.filteredBooks,
searchQuery: searchQuery ?? this.searchQuery,
movedBookId: movedBookId,
isLoading: isLoading ?? this.isLoading,
errorMessage: errorMessage ?? this.errorMessage,
);
}
@override
List<Object?> get props => [
books,
filteredBooks,
searchQuery,
movedBookId,
isLoading,
errorMessage,
];
}

View File

@@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
class WishlistScreen extends StatelessWidget {
const WishlistScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF112116),
body: Center(
child: Padding(
padding: const EdgeInsets.all(40),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Вишлист',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 8),
Text(
'Здесь будут книги, которые вы хотите прочитать.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
color: const Color(0xFF93C8A5),
),
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,43 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'bloc/app_bloc.dart';
import 'features/home/home_screen.dart';
void main() {
runApp(const BookshelfApp());
}
class BookshelfApp extends StatelessWidget {
const BookshelfApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Книжная полка',
debugShowCheckedModeBanner: false,
theme: ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
scaffoldBackgroundColor: const Color(0xFF112116),
colorScheme: ColorScheme.dark(
primary: const Color(0xFF17CF54),
surface: const Color(0xFF1A3222),
surfaceContainer: const Color(0xFF244730),
onSurface: Colors.white,
onSurfaceVariant: const Color(0xFF93C8A5),
),
appBarTheme: const AppBarTheme(
backgroundColor: Color(0xFF112116),
elevation: 0,
centerTitle: true,
),
cardColor: const Color(0xFF1A3023),
fontFamily: 'Inter',
),
home: BlocProvider(
create: (context) => AppBloc(),
child: const HomeScreen(),
),
);
}
}

View File

@@ -0,0 +1,10 @@
// App screen enum
enum AppScreen {
library,
categories,
wishlist,
settings,
details,
addBook,
scanner,
}

Some files were not shown because too many files have changed in this diff Show More