mirror of
https://github.com/Iconica-Development/flutter_timeline.git
synced 2025-05-18 18:13:46 +02:00
feat: add timeline creation screen
This commit is contained in:
parent
c3e251e318
commit
56d92d69f6
9 changed files with 406 additions and 4 deletions
|
@ -2,4 +2,5 @@
|
|||
//
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
///
|
||||
library flutter_timeline_firebase;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
);
|
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue