Compare commits

...

10 Commits

9 changed files with 579 additions and 175 deletions

12
.contacts_dataist_ir.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 895 KiB

View File

@ -0,0 +1,94 @@
import '../models/contact.dart';
import 'package:path/path.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
class DatabaseHelper {
static final DatabaseHelper _instance = DatabaseHelper._internal();
static Database? _database;
factory DatabaseHelper() {
return _instance;
}
DatabaseHelper._internal();
Future<Database> get database async {
if (_database != null) {
return _database!;
}
_database = await _initDatabase();
return _database!;
}
Future<Database> _initDatabase() async {
sqfliteFfiInit();
var databaseFactory = databaseFactoryFfi;
final databasePath = await getDatabasesPath();
final path = join(databasePath, 'contacts.db');
return await databaseFactory.openDatabase(
path,
options: OpenDatabaseOptions(
version: 1,
onCreate: (db, version) async {
await db.execute('''
CREATE TABLE contacts(
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
phone TEXT,
email TEXT,
address TEXT,
company TEXT,
notes TEXT
)
''');
},
),
);
}
Future<int> insertContact(Contact contact) async {
final db = await database;
return await db.insert(
'contacts',
contact.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
Future<List<Contact>> getContacts() async {
final db = await database;
final List<Map<String, dynamic>> maps = await db.query(
'contacts',
orderBy: 'name ASC',
); // Order by name for display
return List.generate(maps.length, (i) {
return Contact.fromMap(maps[i]);
});
}
Future<int> updateContact(Contact contact) async {
final db = await database;
return await db.update(
'contacts',
contact.toMap(),
where: 'id = ?',
whereArgs: [contact.id],
);
}
Future<int> deleteContact(int id) async {
final db = await database;
return await db.delete('contacts', where: 'id = ?', whereArgs: [id]);
}
Future<void> closeDatabase() async {
final db = await _database;
if (db != null && db.isOpen) {
await db.close();
_database = null; // Clear the instance
}
}
}

View File

@ -1,122 +1,26 @@
import 'package:flutter/material.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'package:contacts_dataist_ir/screens/contact_list_screen.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
sqfliteFfiInit();
databaseFactory = databaseFactoryFfi;
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
title: 'Contacts Dataist IR',
theme: ThemeData(
// This is the theme of your application.
//
// TRY THIS: Try running your application with "flutter run". You'll see
// the application has a purple toolbar. Then, without quitting the app,
// try changing the seedColor in the colorScheme below to Colors.green
// and then invoke "hot reload" (save your changes or press the "hot
// reload" button in a Flutter-supported IDE, or press "r" if you used
// the command line to start the app).
//
// Notice that the counter didn't reset back to zero; the application
// state is not lost during the reload. To reset the state, use hot
// restart instead.
//
// This works for code too, not just values: Most code changes can be
// tested with just a hot reload.
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
// This call to setState tells the Flutter framework that something has
// changed in this State, which causes it to rerun the build method below
// so that the display can reflect the updated values. If we changed
// _counter without calling setState(), then the build method would not be
// called again, and so nothing would appear to happen.
_counter++;
});
}
@override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// TRY THIS: Try changing the color here to a specific color (to
// Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
// change color while the other colors stay the same.
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Column(
// Column is also a layout widget. It takes a list of children and
// arranges them vertically. By default, it sizes itself to fit its
// children horizontally, and tries to be as tall as its parent.
//
// Column has various properties to control how it sizes itself and
// how it positions its children. Here we use mainAxisAlignment to
// center the children vertically; the main axis here is the vertical
// axis because Columns are vertical (the cross axis would be
// horizontal).
//
// TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint"
// action in the IDE, or press "p" in the console), to see the
// wireframe for each widget.
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('You have pushed the button this many times:'),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
home: const ContactListScreen(),
);
}
}

48
lib/models/contact.dart Normal file
View File

@ -0,0 +1,48 @@
class Contact {
int? id;
String name;
String? phone;
String? email;
String? address;
String? company;
String? notes;
Contact({
this.id,
required this.name,
this.phone,
this.email,
this.address,
this.company,
this.notes,
});
Map<String, dynamic> toMap() {
return {
'id': id,
'name': name,
'phone': phone,
'email': email,
'address': address,
'company': company,
'notes': notes,
};
}
factory Contact.fromMap(Map<String, dynamic> map) {
return Contact(
id: map['id'],
name: map['name'],
phone: map['phone'],
email: map['email'],
address: map['address'],
company: map['company'],
notes: map['notes'],
);
}
@override
String toString() {
return 'Contact{id: $id, name: $name, phone: $phone, email: $email, address: $address, company: $company, notes: $notes}';
}
}

View File

@ -0,0 +1,133 @@
import 'package:flutter/material.dart';
import 'package:contacts_dataist_ir/models/contact.dart';
import 'package:contacts_dataist_ir/database/database_helper.dart';
class AddEditContactScreen extends StatefulWidget {
final Contact? contact; // Optional: if editing an existing contact
const AddEditContactScreen({super.key, this.contact});
@override
State<AddEditContactScreen> createState() => _AddEditContactScreenState();
}
class _AddEditContactScreenState extends State<AddEditContactScreen> {
final _formKey = GlobalKey<FormState>(); // Key for form validation
final DatabaseHelper _dbHelper = DatabaseHelper();
late TextEditingController _nameController;
late TextEditingController _phoneController;
late TextEditingController _emailController;
late TextEditingController _addressController;
late TextEditingController _companyController;
late TextEditingController _notesController;
@override
void initState() {
super.initState();
_nameController = TextEditingController(text: widget.contact?.name ?? '');
_phoneController = TextEditingController(text: widget.contact?.phone ?? '');
_emailController = TextEditingController(text: widget.contact?.email ?? '');
_addressController = TextEditingController(text: widget.contact?.address ?? '');
_companyController = TextEditingController(text: widget.contact?.company ?? '');
_notesController = TextEditingController(text: widget.contact?.notes ?? '');
}
@override
void dispose() {
_nameController.dispose();
_phoneController.dispose();
_emailController.dispose();
_addressController.dispose();
_companyController.dispose();
_notesController.dispose();
super.dispose();
}
Future<void> _saveContact() async {
if (_formKey.currentState!.validate()) {
final isEditing = widget.contact != null;
final Contact contact = Contact(
id: isEditing ? widget.contact!.id : null,
name: _nameController.text,
phone: _phoneController.text.isEmpty ? null : _phoneController.text,
email: _emailController.text.isEmpty ? null : _emailController.text,
address: _addressController.text.isEmpty ? null : _addressController.text,
company: _companyController.text.isEmpty ? null : _companyController.text,
notes: _notesController.text.isEmpty ? null : _notesController.text,
);
try {
if (isEditing) {
await _dbHelper.updateContact(contact);
} else {
await _dbHelper.insertContact(contact);
}
if (mounted) { // Check if the widget is still mounted before navigating
Navigator.pop(context, true); // Pop with 'true' indicating success
}
} catch (e) {
print('Error saving contact: $e');
// TODO: Show an error message to the user
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.contact == null ? 'Add New Contact' : 'Edit Contact'),
backgroundColor: Theme.of(context).colorScheme.primary,
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: ListView(
children: <Widget>[
TextFormField(
controller: _nameController,
decoration: const InputDecoration(labelText: 'Name *'),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter a name';
}
return null;
},
),
TextFormField(
controller: _phoneController,
decoration: const InputDecoration(labelText: 'Phone'),
keyboardType: TextInputType.phone,
),
TextFormField(
controller: _emailController,
decoration: const InputDecoration(labelText: 'Email'),
keyboardType: TextInputType.emailAddress,
),
TextFormField(
controller: _addressController,
decoration: const InputDecoration(labelText: 'Address'),
),
TextFormField(
controller: _companyController,
decoration: const InputDecoration(labelText: 'Company'),
),
TextFormField(
controller: _notesController,
decoration: const InputDecoration(labelText: 'Notes'),
maxLines: 3,
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _saveContact,
child: Text(widget.contact == null ? 'Save Contact' : 'Update Contact'),
),
],
),
),
),
);
}
}

View File

@ -0,0 +1,136 @@
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:contacts_dataist_ir/models/contact.dart';
import 'package:contacts_dataist_ir/database/database_helper.dart';
import 'package:contacts_dataist_ir/screens/add_edit_contact_screen.dart'; // Ensure this import is here
class ContactListScreen extends StatefulWidget {
const ContactListScreen({super.key});
@override
State<ContactListScreen> createState() => _ContactListScreenState();
}
class _ContactListScreenState extends State<ContactListScreen> {
final DatabaseHelper _dbHelper = DatabaseHelper();
List<Contact> _contacts = [];
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadContacts();
}
Future<void> _loadContacts() async {
setState(() {
_isLoading = true;
});
try {
final contacts = await _dbHelper.getContacts();
setState(() {
_contacts = contacts;
});
} catch (e) {
if (kDebugMode) {
print('Error loading contacts: $e');
}
} finally {
setState(() {
_isLoading = false;
});
}
}
void _navigateToAddEditContact({Contact? contact}) async {
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => AddEditContactScreen(contact: contact),
),
);
if (result == true) {
_loadContacts();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Contacts Dataist IR'),
backgroundColor: Theme.of(context).colorScheme.primary,
),
body:
_isLoading
? const Center(child: CircularProgressIndicator())
: _contacts.isEmpty
? const Center(
child: Text(
'No contacts found. Add a new one!',
style: TextStyle(fontSize: 18),
),
)
: ListView.builder(
itemCount: _contacts.length,
itemBuilder: (context, index) {
final contact = _contacts[index];
return Card(
margin: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 5,
),
child: ListTile(
title: Text(contact.name),
subtitle: Text(
contact.phone ?? contact.email ?? 'No phone or email',
),
onTap: () {
_navigateToAddEditContact(contact: contact);
},
trailing: IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () async {
final confirm = await showDialog<bool>(
context: context,
builder:
(context) => AlertDialog(
title: const Text('Delete Contact'),
content: Text(
'Are you sure you want to delete ${contact.name}?',
),
actions: [
TextButton(
onPressed:
() => Navigator.pop(context, false),
child: const Text('Cancel'),
),
TextButton(
onPressed:
() => Navigator.pop(context, true),
child: const Text('Delete'),
),
],
),
);
if (confirm == true) {
await _dbHelper.deleteContact(contact.id!);
_loadContacts();
}
},
),
),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
_navigateToAddEditContact();
},
child: const Icon(Icons.add),
),
);
}
}

View File

@ -5,6 +5,8 @@
import FlutterMacOS
import Foundation
import path_provider_foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
}

View File

@ -41,14 +41,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.19.1"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
url: "https://pub.dev"
source: hosted
version: "1.0.8"
fake_async:
dependency: transitive
description:
@ -57,6 +49,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.2"
ffi:
dependency: transitive
description:
name: ffi
sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
flutter:
dependency: "direct main"
description: flutter
@ -139,6 +139,70 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.9.1"
path_provider:
dependency: "direct main"
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9
url: "https://pub.dev"
source: hosted
version: "2.2.17"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.3.0"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
sky_engine:
dependency: transitive
description: flutter
@ -152,6 +216,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.10.1"
sqflite_common:
dependency: transitive
description:
name: sqflite_common
sha256: "84731e8bfd8303a3389903e01fb2141b6e59b5973cacbb0929021df08dddbe8b"
url: "https://pub.dev"
source: hosted
version: "2.5.5"
sqflite_common_ffi:
dependency: "direct main"
description:
name: sqflite_common_ffi
sha256: "1f3ef3888d3bfbb47785cc1dda0dc7dd7ebd8c1955d32a9e8e9dae1e38d1c4c1"
url: "https://pub.dev"
source: hosted
version: "2.3.5"
sqlite3:
dependency: transitive
description:
name: sqlite3
sha256: c0503c69b44d5714e6abbf4c1f51a3c3cc42b75ce785f44404765e4635481d38
url: "https://pub.dev"
source: hosted
version: "2.7.6"
stack_trace:
dependency: transitive
description:
@ -176,6 +264,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.1"
synchronized:
dependency: transitive
description:
name: synchronized
sha256: "0669c70faae6270521ee4f05bffd2919892d42d1276e6c495be80174b6bc0ef6"
url: "https://pub.dev"
source: hosted
version: "3.3.1"
term_glyph:
dependency: transitive
description:
@ -192,6 +288,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.7.4"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
vector_math:
dependency: transitive
description:
@ -208,6 +312,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "14.3.1"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
sdks:
dart: ">=3.7.2 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54"
flutter: ">=3.27.0"

View File

@ -1,82 +1,40 @@
# General Section (Metadata)
name: contacts_dataist_ir
description: "A new Flutter project."
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
description: ~/dataist_ir/data_collection/contacts_dataist_ir
homepage: contacts_dataist_ir
documentation: https://wiki.dataist.ir/
version: 1.0.0+1
# Environment
environment:
sdk: ^3.7.2
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
# Dependencies
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
# Provider
path_provider: ^2.1.5
# Database
sqflite_common_ffi: ^2.3.5
dev_dependencies:
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^5.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
# Flutter specific configurations
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# Assets & Fonts:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/to/asset-from-package
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# - assets/images/
# fonts:
# - family: Schyler
# - family: Vazir
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: assets/fonts/vazir.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
@ -84,6 +42,3 @@ flutter:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/to/font-from-package