diff --git a/assets/fonts/MyIcons.ttf b/assets/fonts/MyIcons.ttf new file mode 100644 index 0000000..92c458a Binary files /dev/null and b/assets/fonts/MyIcons.ttf differ diff --git a/lib/src/database/database.dart b/lib/src/database/database.dart index 8418c12..e57d882 100644 --- a/lib/src/database/database.dart +++ b/lib/src/database/database.dart @@ -1,10 +1,8 @@ import 'dart:io'; -import 'dart:ffi'; import 'package:convert/convert.dart'; import 'package:drift/drift.dart'; import 'package:drift/native.dart'; -import 'package:flutter/foundation.dart'; import 'package:kdbx/kdbx.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; diff --git a/lib/src/my_icons_icons.dart b/lib/src/my_icons_icons.dart new file mode 100644 index 0000000..c75253b --- /dev/null +++ b/lib/src/my_icons_icons.dart @@ -0,0 +1,30 @@ +/// Flutter icons MyIcons +/// Copyright (C) 2025 by original authors @ fluttericon.com, fontello.com +/// This font was generated by FlutterIcon.com, which is derived from Fontello. +/// +/// To use this font, place it in your fonts/ directory and include the +/// following in your pubspec.yaml +/// +/// flutter: +/// fonts: +/// - family: MyIcons +/// fonts: +/// - asset: fonts/MyIcons.ttf +/// +/// +/// * Linearicons Free, Copyright (C) Linearicons.com +/// Author: Perxis +/// License: CC BY-SA 4.0 (https://creativecommons.org/licenses/by-sa/4.0/) +/// Homepage: https://linearicons.com +/// +import 'package:flutter/widgets.dart'; + +class MyIcons { + MyIcons._(); + + static const _kFontFam = 'MyIcons'; + static const String? _kFontPkg = null; + + static const IconData chevron_down = IconData(0xe874, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData chevron_right = IconData(0xe876, fontFamily: _kFontFam, fontPackage: _kFontPkg); +} diff --git a/lib/src/providers.dart b/lib/src/providers.dart index 25598cc..7b627ea 100644 --- a/lib/src/providers.dart +++ b/lib/src/providers.dart @@ -1,13 +1,12 @@ import 'package:cryptowl/main.dart'; -import 'package:flutter/foundation.dart'; +import 'package:cryptowl/src/service/password_repository.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:kdbx/kdbx.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'config/meta.dart'; import 'database/database.dart'; import 'domain/user.dart'; import 'service/app_service.dart'; +import 'service/category_repository.dart'; import 'service/kdbx_service.dart'; import 'service/password_service.dart'; @@ -28,6 +27,18 @@ PasswordService passwordService(Ref ref) { return PasswordService(); } +@riverpod +PasswordRepository passwordRepository(Ref ref) { + final db = ref.watch(userDatabaseProvider); + return PasswordRepository(db); +} + +@riverpod +CategoryRepository categoryRepository(Ref ref) { + final db = ref.watch(userDatabaseProvider); + return CategoryRepository(db); +} + @Riverpod(keepAlive: true) class CurrentUser extends _$CurrentUser { @override @@ -35,19 +46,23 @@ class CurrentUser extends _$CurrentUser { return null; } - void setUser(User user) => state = user; + void setUser(User? user) => state = user; } -@Riverpod(keepAlive: true) +@Riverpod(keepAlive: false) AppDb userDatabase(Ref ref) { logger.fine("opening user db..."); final currentUser = ref.watch(currentUserProvider); if (currentUser == null) { + logger.severe("Current user not logged in!"); throw Exception("User not login"); } final meta = currentUser.meta; final db = AppDb.open("${meta.dbInstance}.enc", meta.dbEncryptionKey); - ref.onDispose(() => db.close()); + ref.onDispose(() { + logger.fine("Disposing db..."); + db.close(); + }); return db; } diff --git a/lib/src/providers.g.dart b/lib/src/providers.g.dart index e1414e3..0b0546c 100644 --- a/lib/src/providers.g.dart +++ b/lib/src/providers.g.dart @@ -55,11 +55,49 @@ final passwordServiceProvider = AutoDisposeProvider.internal( @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element typedef PasswordServiceRef = AutoDisposeProviderRef; -String _$userDatabaseHash() => r'c3e22e7508cdec36a028e0bc972ac27577e83029'; +String _$passwordRepositoryHash() => + r'06e68b09b6f4bf17088b4e698c7d2190ba1c0609'; + +/// See also [passwordRepository]. +@ProviderFor(passwordRepository) +final passwordRepositoryProvider = + AutoDisposeProvider.internal( + passwordRepository, + name: r'passwordRepositoryProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$passwordRepositoryHash, + dependencies: null, + allTransitiveDependencies: null, +); + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +typedef PasswordRepositoryRef = AutoDisposeProviderRef; +String _$categoryRepositoryHash() => + r'65cc4f183255dbbc8075ee8d138a9ac9e641153d'; + +/// See also [categoryRepository]. +@ProviderFor(categoryRepository) +final categoryRepositoryProvider = + AutoDisposeProvider.internal( + categoryRepository, + name: r'categoryRepositoryProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$categoryRepositoryHash, + dependencies: null, + allTransitiveDependencies: null, +); + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +typedef CategoryRepositoryRef = AutoDisposeProviderRef; +String _$userDatabaseHash() => r'9a7742a0ffcdf1396db931610eade835706ea0e8'; /// See also [userDatabase]. @ProviderFor(userDatabase) -final userDatabaseProvider = Provider.internal( +final userDatabaseProvider = AutoDisposeProvider.internal( userDatabase, name: r'userDatabaseProvider', debugGetCreateSourceHash: @@ -70,8 +108,8 @@ final userDatabaseProvider = Provider.internal( @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element -typedef UserDatabaseRef = ProviderRef; -String _$currentUserHash() => r'eb4dca39bc198684cf9e0096e64c53dff1878306'; +typedef UserDatabaseRef = AutoDisposeProviderRef; +String _$currentUserHash() => r'914d37ced3f0f492908c1995f8e44b88aa4fff0c'; /// See also [CurrentUser]. @ProviderFor(CurrentUser) diff --git a/lib/src/screens/components/app_drawer.dart b/lib/src/screens/components/app_drawer.dart new file mode 100644 index 0000000..41f1831 --- /dev/null +++ b/lib/src/screens/components/app_drawer.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../login.dart'; + +class AppDrawer extends ConsumerWidget { + const AppDrawer({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final loginNotifier = ref.watch(loginStateProvider.notifier); + + return Drawer( + child: ListView( + // Important: Remove any padding from the ListView. + padding: EdgeInsets.zero, + children: [ + const DrawerHeader( + child: Text('Cryptowl 0.1'), + ), + ListTile( + title: const Text('Backup'), + onTap: () { + // Update the state of the app. + // ... + }, + ), + ListTile( + title: const Text('Logout'), + onTap: () async { + await loginNotifier.logout(); + }, + ), + ], + ), + ); + } +} diff --git a/lib/src/screens/components/category_group.dart b/lib/src/screens/components/category_group.dart new file mode 100644 index 0000000..5213e37 --- /dev/null +++ b/lib/src/screens/components/category_group.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; + +import '../../my_icons_icons.dart'; + +class CategoryGroup extends StatelessWidget { + final String name; + + const CategoryGroup({super.key, required this.name}); + + @override + Widget build(BuildContext context) { + return InkWell( + child: Row( + children: [ + Icon( + MyIcons.chevron_right, + size: 12, + ), + Padding( + padding: EdgeInsets.only(left: 10), + child: Text( + name, + style: TextStyle( + color: Colors.grey, + ), + ), + ), + ], + ), + onTap: () {}, + ); + } +} diff --git a/lib/src/screens/components/category_item.dart b/lib/src/screens/components/category_item.dart index 5db106c..9c6368b 100644 --- a/lib/src/screens/components/category_item.dart +++ b/lib/src/screens/components/category_item.dart @@ -1,9 +1,23 @@ import 'package:flutter/material.dart'; class CategoryItem extends StatelessWidget { + final String name; + final IconData icon; + final int count; + + const CategoryItem( + {super.key, required this.name, required this.icon, required this.count}); + @override Widget build(BuildContext context) { - // TODO: implement build - throw UnimplementedError(); + return ListTile( + dense: true, + contentPadding: EdgeInsets.only(right: 10), + visualDensity: VisualDensity(horizontal: 0, vertical: -4), + leading: Icon(icon), + title: Text(name), + trailing: Text("$count"), + onTap: () {}, + ); } } diff --git a/lib/src/screens/components/password_categories.dart b/lib/src/screens/components/password_categories.dart new file mode 100644 index 0000000..88d372c --- /dev/null +++ b/lib/src/screens/components/password_categories.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +import '../../domain/category.dart'; +import '../../providers.dart'; +import '../passwords.dart'; +import 'category_group.dart'; +import 'category_item.dart'; + +part 'password_categories.g.dart'; + +@riverpod +Future> categories(Ref ref) async { + return ref.watch(categoryRepositoryProvider).list(); +} + +class PasswordCategories extends ConsumerWidget { + const PasswordCategories({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final categories = ref.watch(categoriesProvider); + + return SingleChildScrollView( + child: Column( + children: [ + CategoryItem( + name: "All items", + icon: Icons.dashboard, + count: 10, + ), + CategoryItem( + name: "Favourite", + icon: Icons.favorite, + count: 10, + ), + CategoryItem( + name: "Trash", + icon: Icons.recycling, + count: 10, + ), + SizedBox(height: 15), + CategoryGroup(name: "TYPES"), + CategoryItem( + name: "Login", + icon: Icons.recycling, + count: 10, + ), + CategoryItem( + name: "Card", + icon: Icons.recycling, + count: 10, + ), + CategoryItem( + name: "SSH Key", + icon: Icons.recycling, + count: 10, + ), + SizedBox(height: 15), + CategoryGroup(name: "CATEGORIES"), + categories.when( + data: (list) { + return Column( + children: [ + ...list.map((c) { + return CategoryItem( + name: c.name, + icon: Icons.folder_outlined, + count: 10, + ); + }) + ], + ); + }, + loading: () => const Center( + child: CircularProgressIndicator(), + ), + error: (e, _) => ErrorWidget(e), + ), + ], + ), + ); + } +} diff --git a/lib/src/screens/components/password_categories.g.dart b/lib/src/screens/components/password_categories.g.dart new file mode 100644 index 0000000..15e4f64 --- /dev/null +++ b/lib/src/screens/components/password_categories.g.dart @@ -0,0 +1,26 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'password_categories.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$categoriesHash() => r'f62895ef7ece27a9d7c5d33bc71a9a22dd483c35'; + +/// See also [categories]. +@ProviderFor(categories) +final categoriesProvider = AutoDisposeFutureProvider>.internal( + categories, + name: r'categoriesProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$categoriesHash, + dependencies: null, + allTransitiveDependencies: null, +); + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +typedef CategoriesRef = AutoDisposeFutureProviderRef>; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/src/screens/home.dart b/lib/src/screens/home.dart index 478ddcc..2f0b96a 100644 --- a/lib/src/screens/home.dart +++ b/lib/src/screens/home.dart @@ -3,6 +3,8 @@ import 'dart:io'; import 'package:cryptowl/main.dart'; import 'package:flutter/material.dart'; +import 'components/app_drawer.dart'; +import 'components/password_categories.dart'; import 'passwords.dart'; class HomeScreen extends StatelessWidget { @@ -148,8 +150,42 @@ class _DesktopHomeScreenState extends State { ), body: Padding( padding: EdgeInsets.all(8), - child: PasswordListScreen(), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 240, + child: PasswordCategories(), + ), + Container( + decoration: BoxDecoration( + border: Border( + left: BorderSide( + color: const Color.fromARGB(255, 222, 222, 222), + width: 1, + ), + right: BorderSide( + color: const Color.fromARGB(255, 222, 222, 222), + width: 1, + ), + ), + ), + padding: EdgeInsets.only(left: 10, right: 10), + child: SizedBox( + width: 350, + child: PasswordListScreen(), + ), + ), + Container( + constraints: const BoxConstraints(maxWidth: 100), + child: Center( + child: Text("Details"), + ), + ), + ], + ), ), + drawer: AppDrawer(), ); } } diff --git a/lib/src/screens/login.dart b/lib/src/screens/login.dart index 31dfb7a..d0c2a0e 100644 --- a/lib/src/screens/login.dart +++ b/lib/src/screens/login.dart @@ -1,3 +1,4 @@ +import 'package:cryptowl/main.dart'; import 'package:cryptowl/src/common/exceptions.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -28,6 +29,15 @@ class LoginState extends _$LoginState { return true; }); } + + Future logout() async { + logger.fine("Logging out..."); + state = const AsyncLoading(); + state = await AsyncValue.guard(() async { + ref.read(currentUserProvider.notifier).setUser(null); + return false; + }); + } } class LoginScreen extends ConsumerStatefulWidget { diff --git a/lib/src/screens/login.g.dart b/lib/src/screens/login.g.dart index acbcdb4..513f088 100644 --- a/lib/src/screens/login.g.dart +++ b/lib/src/screens/login.g.dart @@ -6,7 +6,7 @@ part of 'login.dart'; // RiverpodGenerator // ************************************************************************** -String _$loginStateHash() => r'084551f8c6719e82e40a5dd965d9161837bd4559'; +String _$loginStateHash() => r'2c94049ed8ecf2178e7882f2c4c866ab7b07d279'; /// See also [LoginState]. @ProviderFor(LoginState) diff --git a/lib/src/screens/passwords.dart b/lib/src/screens/passwords.dart index 5216e6d..1b273c1 100644 --- a/lib/src/screens/passwords.dart +++ b/lib/src/screens/passwords.dart @@ -28,11 +28,14 @@ class PasswordListScreen extends ConsumerWidget { itemCount: items.length, itemBuilder: (_, index) { return ListTile( + dense: true, + contentPadding: EdgeInsets.only(right: 10), leading: const Icon(Icons.admin_panel_settings), title: Text(items[index].title), - trailing: Text("5"), shape: Border( - bottom: BorderSide(), + bottom: BorderSide( + color: const Color.fromARGB(255, 233, 231, 231), + ), ), ); }), diff --git a/lib/src/service/category_repository.dart b/lib/src/service/category_repository.dart new file mode 100644 index 0000000..b4f9362 --- /dev/null +++ b/lib/src/service/category_repository.dart @@ -0,0 +1,14 @@ +import 'package:drift/drift.dart'; +import '../database/database.dart'; +import '../domain/category.dart'; + +class CategoryRepository { + final AppDb db; + + CategoryRepository(this.db); + + Future> list() async { + final items = await db.categories.select().get(); + return items.map((item) => Category.fromEntity(item)).toList(); + } +} diff --git a/lib/src/service/password_repository.dart b/lib/src/service/password_repository.dart new file mode 100644 index 0000000..81ad7cb --- /dev/null +++ b/lib/src/service/password_repository.dart @@ -0,0 +1,44 @@ +import 'package:drift/drift.dart'; +import 'package:kdbx/kdbx.dart'; +import '../database/database.dart'; +import '../domain/password.dart'; + +class PasswordRepository { + final AppDb db; + + PasswordRepository(this.db); + + Future> list() async { + final items = await db.passwords.select().get(); + return items.map((item) => Password.fromEntity(item)).toList(); + } + + Future findById(String id) async { + final item = await (db.passwords.select() + ..where((tbl) => tbl.id.equals(id))) + .getSingle(); + + return Password.fromEntity(item); + } + + Future insert(Password item) async { + assert(item.id == null); + await db.into(db.passwords).insert(item.toCompanion()); + return item; + } + + Future create(String title, ProtectedValue value, + {String? url, String? username, String? remark}) async { + final now = DateTime.now(); + final item = Password( + type: 1, + categoryId: 1, + title: title, + value: value, + remark: remark, + createTime: now, + lastUpdateTime: now, + ); + return insert(item); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 903ecbf..7f98515 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -57,5 +57,8 @@ flutter: generate: true assets: - # Add assets from the images directory to the application. - assets/images/ + fonts: + - family: MyIcons + fonts: + - asset: assets/fonts/MyIcons.ttf \ No newline at end of file