mirror of
https://github.com/Iconica-Development/flutter_timeline.git
synced 2025-05-19 02:23: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
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
///
|
||||||
library flutter_timeline_firebase;
|
library flutter_timeline_firebase;
|
||||||
|
|
|
@ -4,6 +4,8 @@
|
||||||
///
|
///
|
||||||
library flutter_timeline_view;
|
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_creation_screen.dart';
|
||||||
export 'src/screens/timeline_post_screen.dart';
|
export 'src/screens/timeline_post_screen.dart';
|
||||||
export 'src/screens/timeline_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 {
|
import 'dart:typed_data';
|
||||||
const TimelinePostCreationScreen({super.key});
|
|
||||||
|
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
|
@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/material.dart';
|
||||||
import 'package:flutter_timeline_interface/flutter_timeline_interface.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/material.dart';
|
||||||
import 'package:flutter_timeline_interface/flutter_timeline_interface.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:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
intl: any
|
||||||
|
cached_network_image: ^3.2.2
|
||||||
|
|
||||||
flutter_timeline_interface:
|
flutter_timeline_interface:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/Iconica-Development/flutter_timeline.git
|
url: https://github.com/Iconica-Development/flutter_timeline.git
|
||||||
path: packages/flutter_timeline_interface
|
path: packages/flutter_timeline_interface
|
||||||
ref: 0.0.1
|
ref: 0.0.1
|
||||||
|
flutter_image_picker:
|
||||||
|
git:
|
||||||
|
url: https://github.com/Iconica-Development/flutter_image_picker
|
||||||
|
ref: 1.0.4
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_lints: ^2.0.0
|
flutter_lints: ^2.0.0
|
||||||
|
|
Loading…
Reference in a new issue