feat: add timeline creation screen

This commit is contained in:
Freek van de Ven 2023-11-18 13:21:42 +01:00
parent c3e251e318
commit 56d92d69f6
9 changed files with 406 additions and 4 deletions

View file

@ -2,4 +2,5 @@
//
// SPDX-License-Identifier: BSD-3-Clause
///
library flutter_timeline_firebase;

View file

@ -4,6 +4,8 @@
///
library flutter_timeline_view;
export 'src/config/timeline_options.dart';
export 'src/config/timeline_translations.dart';
export 'src/screens/timeline_post_creation_screen.dart';
export 'src/screens/timeline_post_screen.dart';
export 'src/screens/timeline_screen.dart';

View file

@ -0,0 +1,50 @@
// SPDX-FileCopyrightText: 2023 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/material.dart';
import 'package:flutter_image_picker/flutter_image_picker.dart';
import 'package:flutter_timeline_view/src/config/timeline_translations.dart';
import 'package:intl/intl.dart';
@immutable
class TimelineOptions {
const TimelineOptions({
this.translations = const TimelineTranslations(),
this.imagePickerConfig = const ImagePickerConfig(),
this.imagePickerTheme = const ImagePickerTheme(),
this.dateformat,
this.buttonBuilder,
this.textInputBuilder,
});
/// The format to display the post time in
final DateFormat? dateformat;
final TimelineTranslations translations;
final ButtonBuilder? buttonBuilder;
final TextInputBuilder? textInputBuilder;
/// ImagePickerTheme can be used to change the UI of the
/// Image Picker Widget to change the text/icons to your liking.
final ImagePickerTheme imagePickerTheme;
/// ImagePickerConfig can be used to define the
/// size and quality for the uploaded image.
final ImagePickerConfig imagePickerConfig;
}
typedef ButtonBuilder = Widget Function(
BuildContext context,
VoidCallback onPressed,
String text, {
bool enabled,
});
typedef TextInputBuilder = Widget Function(
TextEditingController controller,
Widget? suffixIcon,
String hintText,
);

View file

@ -0,0 +1,40 @@
// SPDX-FileCopyrightText: 2023 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/material.dart';
@immutable
class TimelineTranslations {
const TimelineTranslations({
this.title = 'Title',
this.content = 'Content',
this.contentDescription = 'What do you want to share?',
this.uploadImage = 'Upload image',
this.uploadImageDescription = 'Upload an image to your message (optional)',
this.allowComments = 'Are people allowed to comment?',
this.allowCommentsDescription =
'Indicate whether people are allowed to respond',
this.checkPost = 'Check post overview',
this.deletePost = 'Delete post',
this.viewPost = 'View post',
this.likesTitle = 'Likes',
this.commentsTitle = 'Comments',
this.writeComment = 'Write your comment here...',
});
final String title;
final String content;
final String contentDescription;
final String uploadImage;
final String uploadImageDescription;
final String allowComments;
final String allowCommentsDescription;
final String checkPost;
final String deletePost;
final String viewPost;
final String likesTitle;
final String commentsTitle;
final String writeComment;
}

View file

@ -1,8 +1,237 @@
import 'package:flutter/material.dart';
// SPDX-FileCopyrightText: 2023 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
class TimelinePostCreationScreen extends StatelessWidget {
const TimelinePostCreationScreen({super.key});
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_image_picker/flutter_image_picker.dart';
import 'package:flutter_timeline_view/src/config/timeline_options.dart';
import 'package:flutter_timeline_view/src/widgets/dotted_container.dart';
class TimelinePostCreationScreen extends StatefulWidget {
const TimelinePostCreationScreen({
required this.options,
this.padding = const EdgeInsets.symmetric(vertical: 24, horizontal: 16),
super.key,
});
/// The options for the timeline
final TimelineOptions options;
/// The padding around the screen
final EdgeInsets padding;
@override
Widget build(BuildContext context) => const Placeholder();
State<TimelinePostCreationScreen> createState() =>
_TimelinePostCreationScreenState();
}
class _TimelinePostCreationScreenState
extends State<TimelinePostCreationScreen> {
TextEditingController titleController = TextEditingController();
TextEditingController contentController = TextEditingController();
Uint8List? image;
bool editingDone = false;
bool allowComments = false;
@override
void initState() {
super.initState();
titleController.addListener(checkIfEditingDone);
contentController.addListener(checkIfEditingDone);
}
@override
void dispose() {
titleController.dispose();
contentController.dispose();
super.dispose();
}
void checkIfEditingDone() {
setState(() {
editingDone = titleController.text.isNotEmpty &&
contentController.text.isNotEmpty &&
image != null;
});
}
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return Padding(
padding: widget.padding,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.options.translations.title,
style: theme.textTheme.displaySmall,
),
widget.options.textInputBuilder?.call(
titleController,
null,
'',
) ??
TextField(
controller: titleController,
),
const SizedBox(height: 16),
Text(
widget.options.translations.content,
style: theme.textTheme.displaySmall,
),
const SizedBox(height: 4),
Text(
widget.options.translations.contentDescription,
style: theme.textTheme.bodyMedium,
),
// input field for the content
SizedBox(
height: 100,
child: TextField(
controller: contentController,
expands: true,
maxLines: null,
minLines: null,
),
),
const SizedBox(
height: 16,
),
// input field for the content
Text(
widget.options.translations.uploadImage,
style: theme.textTheme.displaySmall,
),
Text(
widget.options.translations.uploadImageDescription,
style: theme.textTheme.bodyMedium,
),
// image picker field
const SizedBox(
height: 8,
),
Stack(
children: [
GestureDetector(
onTap: () async {
// open a dialog to choose between camera and gallery
var result = await showModalBottomSheet<Uint8List?>(
context: context,
builder: (context) => Container(
padding: const EdgeInsets.all(8.0),
color: Colors.black,
child: ImagePicker(
imagePickerConfig: widget.options.imagePickerConfig,
imagePickerTheme: widget.options.imagePickerTheme,
),
),
);
if (result != null) {
setState(() {
image = result;
});
}
checkIfEditingDone();
},
child: image != null
? ClipRRect(
borderRadius: BorderRadius.circular(8.0),
child: Image.memory(
image!,
width: double.infinity,
height: 150.0,
fit: BoxFit.cover,
// give it a rounded border
),
)
: CustomPaint(
painter: DashedBorderPainter(
color: theme.textTheme.displayMedium?.color ??
Colors.white,
dashLength: 4.0,
dashWidth: 1.5,
space: 4,
),
child: const SizedBox(
width: double.infinity,
height: 150.0,
child: Icon(
Icons.image,
size: 32,
),
),
),
),
// if an image is selected, show a delete button
if (image != null) ...[
Positioned(
top: 8,
right: 8,
child: GestureDetector(
onTap: () {
setState(() {
image = null;
});
checkIfEditingDone();
},
child: Container(
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.5),
borderRadius: BorderRadius.circular(8.0),
),
child: const Icon(
Icons.delete,
color: Colors.white,
),
),
),
),
],
],
),
const SizedBox(height: 16),
Text(
widget.options.translations.commentsTitle,
style: theme.textTheme.displaySmall,
),
Text(
widget.options.translations.allowCommentsDescription,
style: theme.textTheme.bodyMedium,
),
// radio buttons for yes or no
Switch(
value: allowComments,
onChanged: (newValue) {
setState(() {
allowComments = newValue;
});
},
),
const Spacer(),
Align(
alignment: Alignment.bottomCenter,
child: (widget.options.buttonBuilder != null)
? widget.options.buttonBuilder!(
context,
() {},
widget.options.translations.checkPost,
enabled: editingDone,
)
: ElevatedButton(
onPressed: editingDone ? () {} : null,
child: Text(
widget.options.translations.checkPost,
style: theme.textTheme.bodyMedium,
),
),
),
],
),
);
}
}

View file

@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: 2023 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/material.dart';
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';

View file

@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: 2023 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/material.dart';
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';

View file

@ -0,0 +1,65 @@
// SPDX-FileCopyrightText: 2023 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/material.dart';
class DashedBorderPainter extends CustomPainter {
DashedBorderPainter({
this.color = Colors.black,
this.dashWidth = 2.0,
this.dashLength = 6.0,
this.space = 3.0,
});
final Color color;
final double dashWidth;
final double dashLength;
final double space;
@override
void paint(Canvas canvas, Size size) {
var paint = Paint()
..color = color
..strokeWidth = dashWidth;
var x = 0.0;
var y = 0.0;
// Top border
while (x < size.width) {
canvas.drawLine(Offset(x, 0), Offset(x + dashLength, 0), paint);
x += dashLength + space;
}
// Right border
while (y < size.height) {
canvas.drawLine(
Offset(size.width, y),
Offset(size.width, y + dashLength),
paint,
);
y += dashLength + space;
}
x = size.width;
// Bottom border
while (x > 0) {
canvas.drawLine(
Offset(x, size.height),
Offset(x - dashLength, size.height),
paint,
);
x -= dashLength + space;
}
y = size.height;
// Left border
while (y > 0) {
canvas.drawLine(Offset(0, y), Offset(0, y - dashLength), paint);
y -= dashLength + space;
}
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}

View file

@ -14,11 +14,18 @@ environment:
dependencies:
flutter:
sdk: flutter
intl: any
cached_network_image: ^3.2.2
flutter_timeline_interface:
git:
url: https://github.com/Iconica-Development/flutter_timeline.git
path: packages/flutter_timeline_interface
ref: 0.0.1
flutter_image_picker:
git:
url: https://github.com/Iconica-Development/flutter_image_picker
ref: 1.0.4
dev_dependencies:
flutter_lints: ^2.0.0