Skip to content

Commit ef54c3e

Browse files
authored
Feature/company logo (#15)
* feat(logo): allow users to put logo * fix(store): enhance store info CRUDs
1 parent cb2a66e commit ef54c3e

12 files changed

+646
-124
lines changed

lib/src/pages/auth_page.dart

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,9 @@ import '../isar_collection/isar_collections.dart' show Store, Recipient;
44
import '../isar_service.dart';
55
import '../shared/center_circular.dart';
66
import 'edit_currency_page.dart' show EditCurrencyArgs;
7+
import 'edit_store_state.dart' show kNote;
78
import 'invoice_page.dart' show InvoiceArgs;
89

9-
const note =
10-
'Thank you for your business! Please complete the remaining balance by the due date to avoid late fees. If you have any questions, feel free to contact us.';
11-
1210
class AuthPage extends StatefulWidget {
1311
static const String routeName = '/';
1412

@@ -45,7 +43,7 @@ class _AuthPageState extends State<AuthPage> {
4543
..accountHolderName = ''
4644
..swiftCode = ''
4745
..tax = 0
48-
..thankNote = note
46+
..thankNote = kNote
4947
..locale = ''
5048
..symbol = '';
5149
await _db.saveStore(store);

lib/src/pages/edit_store_page.dart

Lines changed: 222 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
1+
import 'dart:typed_data';
2+
13
import 'package:flutter/material.dart';
24

35
import '../constants.dart';
46
import '../isar_collection/isar_collections.dart' show Store;
57
import '../isar_service.dart';
68
import '../shared/bottom_sheet_scroll_header.dart';
9+
import '../shared/dashed_border_container.dart';
10+
import '../shared/padded_text.dart';
711
import 'edit_currency_state.dart';
12+
import 'edit_store_state.dart';
13+
import 'preview_state.dart' show getTextLogo;
814

915
class EditStorePage extends StatefulWidget {
1016
static const routeName = '/edit-store';
@@ -23,41 +29,94 @@ class _EditStorePageState extends State<EditStorePage> {
2329
final _formKey = GlobalKey<FormState>();
2430
final _email = TextEditingController();
2531
final _name = TextEditingController();
32+
final _note = TextEditingController();
2633
late Currency _curr;
34+
var _action = LogoAction.update;
35+
36+
Uint8List? _imageBytes;
2737

2838
@override
2939
void initState() {
3040
super.initState();
41+
WidgetsBinding.instance.addPostFrameCallback((_) {
42+
loadImage((v) => setState(() => _imageBytes = v));
43+
});
3144
_editedStore = widget.store;
3245
_email.text = widget.store.email!;
3346
_name.text = widget.store.name!;
47+
_note.text = widget.store.thankNote!;
3448
_curr = getCurrency(widget.store.locale!);
3549
}
3650

3751
@override
3852
void dispose() {
3953
_email.dispose();
4054
_name.dispose();
55+
_note.dispose();
4156
super.dispose();
4257
}
4358

4459
@override
4560
Widget build(BuildContext context) {
4661
final colors = Theme.of(context).colorScheme;
4762
final disabledColor = Theme.of(context).disabledColor;
48-
63+
final textTheme = Theme.of(context).textTheme;
64+
final alertStyle =
65+
textTheme.bodySmall!.copyWith(color: colors.onTertiaryContainer);
66+
final textBold = TextStyle(fontWeight: FontWeight.bold);
4967
return Scaffold(
5068
appBar: AppBar(
5169
title: Text('Edit store info'),
5270
),
5371
body: Form(
5472
key: _formKey,
55-
child: Column(
56-
spacing: 16.0,
57-
crossAxisAlignment: CrossAxisAlignment.stretch,
58-
mainAxisSize: MainAxisSize.min,
73+
child: ListView(
5974
children: [
60-
SizedBox(),
75+
_DividerText(text: 'Logo'),
76+
Padding(
77+
padding: kPx,
78+
child: _CompanyLogoButton(
79+
bytes: _imageBytes,
80+
name: widget.store.name!,
81+
action: _imageBytes != null
82+
? PopupMenuButton<LogoAction>(
83+
initialValue: _action,
84+
onSelected: (v) => _onSelected(v),
85+
iconColor: colors.primary,
86+
itemBuilder: (context) => <PopupMenuEntry<LogoAction>>[
87+
const PopupMenuItem<LogoAction>(
88+
value: LogoAction.update,
89+
child: Text('Update'),
90+
),
91+
const PopupMenuItem<LogoAction>(
92+
value: LogoAction.delete,
93+
child: Text('Remove'),
94+
),
95+
],
96+
)
97+
: IconButton(
98+
onPressed: () => _onPickImage(),
99+
icon: Icon(Icons.edit, color: colors.primary),
100+
),
101+
),
102+
),
103+
_AlertTextBox(
104+
margin: EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 0.0),
105+
backgroundColor: colors.tertiaryContainer,
106+
iconColor: colors.onTertiaryContainer,
107+
icon: Icons.info_outline,
108+
child: Text.rich(
109+
style: alertStyle,
110+
TextSpan(
111+
text: kLogoHelp1,
112+
children: [
113+
TextSpan(text: kLogoHelp2, style: textBold),
114+
TextSpan(text: kLogoHelp3),
115+
],
116+
),
117+
),
118+
),
119+
_DividerText(text: 'Main'),
61120
Padding(
62121
padding: kPx,
63122
child: TextFormField(
@@ -70,6 +129,7 @@ class _EditStorePageState extends State<EditStorePage> {
70129
onChanged: (v) => setState(() {}),
71130
),
72131
),
132+
const SizedBox(height: 16.0),
73133
Padding(
74134
padding: kPx,
75135
child: TextFormField(
@@ -82,10 +142,27 @@ class _EditStorePageState extends State<EditStorePage> {
82142
onChanged: (v) => setState(() {}),
83143
),
84144
),
145+
const SizedBox(height: 16.0),
85146
_CurrencyButton(
86147
title: getName(_curr),
87148
onPressed: () => _onEditCurrency(),
88149
),
150+
_DividerText(text: 'Tank note'),
151+
Padding(
152+
padding: kPx,
153+
child: TextFormField(
154+
controller: _note,
155+
decoration: InputDecoration(
156+
hintText: 'Thank you for your business!',
157+
label: Text('Thank note'),
158+
),
159+
keyboardType: TextInputType.text,
160+
maxLines: 8,
161+
minLines: 2,
162+
onChanged: (v) => setState(() {}),
163+
),
164+
),
165+
SizedBox(height: kToolbarHeight * 2),
89166
],
90167
),
91168
),
@@ -110,6 +187,7 @@ class _EditStorePageState extends State<EditStorePage> {
110187
_editedStore.email = _email.text.trim();
111188
_editedStore.locale = getLocale(_curr);
112189
_editedStore.symbol = getSymbol(_curr);
190+
_editedStore.thankNote = _note.text.isEmpty ? kNote : _note.text.trim();
113191
await _db.updateStore(_editedStore);
114192
nav.pushNamedAndRemoveUntil('/', (_) => false);
115193
}
@@ -144,6 +222,25 @@ class _EditStorePageState extends State<EditStorePage> {
144222
setState(() => _curr = currency);
145223
nav.pop();
146224
}
225+
226+
Future<void> _onPickImage() async {
227+
await pickImage((v) => setState(() => _imageBytes = v));
228+
}
229+
230+
Future<void> _onDelete() async {
231+
await removeImage();
232+
setState(() => _imageBytes = null);
233+
}
234+
235+
Future<void> _onSelected(LogoAction action) async {
236+
switch (action) {
237+
case LogoAction.update:
238+
await _onPickImage().then((_) => setState(() => _action = action));
239+
break;
240+
default:
241+
await _onDelete().then((_) => setState(() => _action = action));
242+
}
243+
}
147244
}
148245

149246
class EditStoreArgs {
@@ -180,11 +277,129 @@ class _CurrencyButton extends StatelessWidget {
180277
),
181278
Padding(
182279
padding: const EdgeInsets.all(12.0),
183-
child: Icon(Icons.arrow_drop_down_outlined),
280+
child: Icon(
281+
Icons.arrow_drop_down_outlined,
282+
color: colors.primary,
283+
),
184284
),
185285
],
186286
),
187287
),
188288
);
189289
}
190290
}
291+
292+
class _CompanyLogoButton extends StatelessWidget {
293+
const _CompanyLogoButton({
294+
required this.action,
295+
this.bytes,
296+
required this.name,
297+
});
298+
299+
final String name;
300+
final Uint8List? bytes;
301+
final Widget action;
302+
303+
@override
304+
Widget build(BuildContext context) {
305+
final textTheme = Theme.of(context).textTheme;
306+
final colors = Theme.of(context).colorScheme;
307+
308+
return Card(
309+
child: Row(
310+
crossAxisAlignment: CrossAxisAlignment.start,
311+
mainAxisAlignment: MainAxisAlignment.spaceBetween,
312+
children: [
313+
Padding(
314+
padding: const EdgeInsets.all(16.0),
315+
child: DashedBorderContainer(
316+
color: colors.outline,
317+
strokeWidth: 1,
318+
dashSpace: bytes != null ? 0.0 : 3.0,
319+
dashWidth: 5.0,
320+
borderRadius: 6.0,
321+
child: Container(
322+
alignment: Alignment.center,
323+
width: 128.0,
324+
height: 128.0,
325+
clipBehavior: Clip.antiAlias,
326+
decoration: BoxDecoration(
327+
borderRadius: BorderRadius.all(Radius.circular(6.0)),
328+
image: bytes != null
329+
? DecorationImage(image: MemoryImage(bytes!))
330+
: null,
331+
),
332+
child: bytes != null
333+
? null
334+
: Text(
335+
getTextLogo(name),
336+
style: textTheme.displaySmall,
337+
),
338+
),
339+
),
340+
),
341+
Padding(
342+
padding: const EdgeInsets.all(8.0),
343+
child: action,
344+
),
345+
],
346+
),
347+
);
348+
}
349+
}
350+
351+
class _DividerText extends StatelessWidget {
352+
const _DividerText({required this.text});
353+
354+
final String text;
355+
356+
@override
357+
Widget build(BuildContext context) {
358+
final textTheme = Theme.of(context).textTheme;
359+
360+
return PaddedText(
361+
text: text,
362+
style: textTheme.titleMedium,
363+
left: 16,
364+
top: 20,
365+
right: 16,
366+
bottom: 12,
367+
);
368+
}
369+
}
370+
371+
class _AlertTextBox extends StatelessWidget {
372+
const _AlertTextBox({
373+
required this.backgroundColor,
374+
required this.icon,
375+
required this.child,
376+
required this.iconColor,
377+
this.margin,
378+
});
379+
380+
final Color backgroundColor;
381+
final Widget child;
382+
final IconData icon;
383+
final Color iconColor;
384+
final EdgeInsets? margin;
385+
386+
@override
387+
Widget build(BuildContext context) {
388+
return Container(
389+
margin: margin,
390+
padding: EdgeInsets.all(12.0),
391+
decoration: BoxDecoration(
392+
color: backgroundColor,
393+
borderRadius: BorderRadius.all(Radius.circular(12.0)),
394+
),
395+
child: Row(
396+
spacing: 12.0,
397+
crossAxisAlignment: CrossAxisAlignment.start,
398+
children: [
399+
Icon(icon, color: iconColor),
400+
Expanded(child: child),
401+
],
402+
),
403+
);
404+
}
405+
}

lib/src/pages/edit_store_state.dart

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import 'dart:io';
2+
import 'dart:typed_data';
3+
4+
import 'package:image_picker/image_picker.dart';
5+
import 'package:path_provider/path_provider.dart';
6+
7+
enum LogoAction { update, delete }
8+
9+
const kLogoHelp1 = 'For the best results, please choose a ';
10+
const kLogoHelp2 = 'square (1:1 ratio) ';
11+
const kLogoHelp3 = 'image.';
12+
13+
const kNote =
14+
'Thank you for your business! Please complete the remaining balance by the due date to avoid late fees. If you have any questions, feel free to contact us.';
15+
16+
const kLogoPng = 'logo.png';
17+
18+
Future<File> saveImage(File image) async {
19+
final directory = await getApplicationDocumentsDirectory();
20+
final path = '${directory.path}/$kLogoPng';
21+
return image.copy(path);
22+
}
23+
24+
Future<void> pickImage(Function(Uint8List)? onBytes) async {
25+
final pickedFile = await ImagePicker().pickImage(source: ImageSource.gallery);
26+
if (pickedFile != null) {
27+
final savedImage = await saveImage(File(pickedFile.path));
28+
final bytes = await savedImage.readAsBytes();
29+
onBytes!(bytes);
30+
}
31+
}
32+
33+
Future<void> loadImage(Function(Uint8List)? onBytes) async {
34+
final directory = await getApplicationDocumentsDirectory();
35+
final imagePath = '${directory.path}/$kLogoPng';
36+
if (File(imagePath).existsSync()) {
37+
final bytes = await File(imagePath).readAsBytes();
38+
onBytes!(bytes);
39+
}
40+
}
41+
42+
Future<void> removeImage() async {
43+
final directory = await getApplicationDocumentsDirectory();
44+
final imagePath = '${directory.path}/$kLogoPng';
45+
final file = File(imagePath);
46+
47+
if (file.existsSync()) {
48+
await file.delete();
49+
}
50+
}

0 commit comments

Comments
 (0)