Feat/intial nested categories (#1)

* feat: add nested category functionality

* feat(example): add nested category example app

* chore(changelog): add v0.0.1 changelog

* chore(readme): add readme

* feat(custom-header): add custom category header functionality

* feat(header_style): add default header style requirement

* feat(capitalization): add header capitalization

* fix: feedback

* fix: feedback
This commit is contained in:
Mark 2024-04-08 10:11:43 +02:00 committed by GitHub
parent fd6e7170bf
commit e5b5b5fd04
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 625 additions and 28 deletions

View file

@ -1,3 +1,5 @@
## 0.0.1
* TODO: Describe initial release.
* Initial Nested Category functionality.
* Title of categories
* Collapsible categories

View file

@ -1,39 +1,68 @@
<!--
This README describes the package. If you publish this package to pub.dev,
this README's contents appear on the landing page for your package.
# flutter_nested_categories
For information about how to write a good package README, see the guide for
[writing package pages](https://dart.dev/guides/libraries/writing-package-pages).
For general information about developing packages, see the Dart guide for
[creating packages](https://dart.dev/guides/libraries/create-library-packages)
and the Flutter guide for
[developing packages and plugins](https://flutter.dev/developing-packages).
-->
TODO: Put a short description of the package here that helps potential users
know whether this package might be useful for them.
This Flutter component allows you to easily create nested categories that are very
customizable.
## Features
TODO: List what your package can do. Maybe include images, gifs, or videos.
## Getting started
TODO: List prerequisites and provide or point to information on how to
start using the package.
* Nested Categories
* Collapsible Categories
## Usage
TODO: Include short and useful examples for package users. Add longer examples
to `/example` folder.
You can build a nested category using the `CategoryList` class, like this:
```dart
const like = 'sample';
CategoryList(
content: [
Category(
name: "Category 1",
content: [
const Text("Content 1"),
const Text("Content 2"),
],
nestedCategories: [
Category(
name: "Category 1.1",
content: [
const Text("Content 1.1"),
const Text("Content 1.2"),
],
),
],
),
],
),
```
## Additional information
You have a bunch of customization options available as well, such as:
TODO: Tell users more about the package: where to find more information, how to
contribute to the package, how to file issues, what response they can expect
from the package authors, and more.
* Setting a (text)title (and title styling/center),
* Setting a custom title,
* Collapsible categories,
* Header styling.
For a more detailed example you can see the [example](https://github.com/Iconica-Development/flutter_nested_categories/tree/main/example).
Or, you could run the example yourself:
```
git clone https://github.com/Iconica-Development/flutter_nested_categories.git
cd flutter_nested_categories
cd example
flutter run
```
## Issues
Please file any issues, bugs or feature request as an issue on our [GitHub](https://github.com/Iconica-Development/flutter_nested_categories) page. Commercial support is available if you need help with integration with your app or services. You can contact us at [support@iconica.nl](mailto:support@iconica.nl).
## Want to contribute
If you would like to contribute to the component (e.g. by improving the documentation, solving a bug or adding a cool new feature), please carefully review our [contribution guide](./CONTRIBUTING.md) and send us your [pull request](https://github.com/Iconica-Development/flutter_nested_categories/pulls).
## Author
This flutter_thermal_printer for Flutter is developed by [Iconica](https://iconica.nl). You can contact us at <support@iconica.nl>

53
example/.gitignore vendored Normal file
View file

@ -0,0 +1,53 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
.metadata
pubspec.lock
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
# Platforms
/android/
/ios/
/linux/
/macos/
/web/
/windows/

16
example/README.md Normal file
View file

@ -0,0 +1,16 @@
# flutter_nested_categories_example
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

View file

@ -0,0 +1,9 @@
include: package:flutter_iconica_analysis/analysis_options.yaml
# Possible to overwrite the rules from the package
analyzer:
exclude:
linter:
rules:

106
example/lib/main.dart Normal file
View file

@ -0,0 +1,106 @@
import "package:flutter/material.dart";
import "package:flutter_nested_categories/flutter_nested_categories.dart";
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text("Nested Categories Example"),
),
body: CategoryList(
title: "This is the title of the list",
titleCentered: true,
headerCentered: false,
isCategoryCollapsible: true,
headerStyling: CategoryHeaderStyling(
headerStyles: {
0: theme.textTheme.titleLarge?.copyWith(color: Colors.red),
2: theme.textTheme.titleLarge?.copyWith(color: Colors.blue),
},
defaultStyle:
theme.textTheme.titleLarge!.copyWith(color: Colors.green),
capitalization: CategoryHeaderCapitalization.uppercase,
),
content: [
Category(
name: "category 1",
content: [
const Text("Content 1"),
Image.network(
"https://via.placeholder.com/150?text=Content+2+Image",
),
],
nestedCategories: [
const Category(
name: "Category 1.1",
content: [
Text("Content 1.1"),
Text("Content 1.2"),
],
nestedCategories: [
Category(
name: "Category 1.1.1",
content: [
Text("Content 1.1.1"),
Text("Content 1.1.2"),
],
),
Category(
name: "Category 1.1.2",
content: [
Text("Content 1.1.2"),
Text("Content 1.1.2"),
],
),
],
),
const Category(
name: "Category 1.2",
content: [
Text("Content 1.2"),
Text("Content 1.2"),
],
),
],
),
const Category(
name: "Category 2",
content: [
Text("Content 2"),
Text("Content 2"),
],
nestedCategories: [
Category(
name: "Category 2.1",
content: [
Text("Content 2.1"),
Text("Content 2.2"),
],
nestedCategories: [
Category(
name: "Category 2.1.1",
content: [
Text("Content 2.1.1"),
Text("Content 2.1.2"),
],
),
],
),
],
),
],
),
),
);
}
}

33
example/pubspec.yaml Normal file
View file

@ -0,0 +1,33 @@
name: flutter_nested_categories_example
description: "Demonstrates how to use the flutter_nested_categories package."
publish_to: 'none'
environment:
sdk: '>=3.3.0 <4.0.0'
dependencies:
flutter:
sdk: flutter
flutter_hooks: ^0.20.0
flutter_nested_categories:
path: ../
dev_dependencies:
flutter_test:
sdk: flutter
flutter_iconica_analysis:
git:
url: https://github.com/Iconica-Development/flutter_iconica_analysis
ref: 7.0.0
flutter:
uses-material-design: true
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic

View file

@ -0,0 +1,10 @@
/// Flutter Nested Categories
///
/// A Flutter package that allows you to create nested categories that
/// are very easy to use and customize.
library flutter_nested_categories;
export "src/category.dart";
export "src/category_header_capitalization.dart";
export "src/category_header_styling.dart";
export "src/category_list.dart";

40
lib/src/category.dart Normal file
View file

@ -0,0 +1,40 @@
import "package:flutter/widgets.dart";
/// One of the categories in the list. Each category can contain content,
/// which can be an empty list, but also nested categories. The nested
/// categories are the same type as the parent category which allows for
/// infinite nesting.
class Category {
const Category({
this.name,
this.customTitle,
this.content = const <Widget>[],
this.nestedCategories = const <Category>[],
}) : assert(
name != null || customTitle != null,
"A name or a custom title must be set",
);
/// The name of the category.
final String? name;
/// Optional custom title widget for the category. This will be displayed
/// at the top of the category. If set, the text title will be ignored.
/// This will be displayed before the content of the category.
final Widget? customTitle;
/// The content of the category. This can be anything, but is usually
/// a list of widgets.
///
/// Default is an empty list.
///
/// If the category has nested categories, it will show this content
/// before the nested categories.
final List<Category> nestedCategories;
/// The nested categories of this category. This can be an empty list
/// if there are no nested categories.
///
/// Default is an empty list.
final List<Widget> content;
}

View file

@ -0,0 +1,16 @@
/// An enum that represents the capitalization of the category header.
/// This is used to determine how the header should be displayed.
/// The header is the name of the category.
enum CategoryHeaderCapitalization {
/// The first letter of the header will be capitalized.
capitalizeFirstLetter,
/// The header will be displayed in all lowercase.
lowercase,
/// The header will be displayed in all uppercase.
uppercase,
/// The header will be displayed as is.
none,
}

View file

@ -0,0 +1,24 @@
import "package:flutter/widgets.dart";
import "package:flutter_nested_categories/src/category_header_capitalization.dart";
/// This class is used to style the headers of the categories in the list.
/// The headers are the names of the categories.
class CategoryHeaderStyling {
const CategoryHeaderStyling({
required this.defaultStyle,
this.capitalization = CategoryHeaderCapitalization.none,
this.headerStyles = const {},
});
/// The styles for the headers. The key is the depth of the category.
/// The value is the text style for the header.
final Map<int, TextStyle?> headerStyles;
/// The capitalization of the headers. This is used to determine how the
/// headers should be displayed.
final CategoryHeaderCapitalization capitalization;
/// The default style for the headers. This will be used if the depth
/// is not found in the [headerStyles].
final TextStyle defaultStyle;
}

259
lib/src/category_list.dart Normal file
View file

@ -0,0 +1,259 @@
import "package:flutter/material.dart";
import "package:flutter_nested_categories/flutter_nested_categories.dart";
/// This widget displays a list of categories. Each category can contain
/// content, which can be an empty list, but also nested categories. The
/// nested categories are the same type as the parent category which allows
/// for infinite nesting.
///
/// The content of the categories is displayed in the order it is given in
/// the list. If a category has nested categories, the content of the nested
/// categories is displayed after the content of the parent category.
class CategoryList extends StatelessWidget {
const CategoryList({
required this.content,
this.headerStyling,
this.headerCentered = false,
this.customTitle,
this.title,
this.titleStyle,
this.titleCentered = false,
this.isCategoryCollapsible = true,
this.categoryDepth = 0,
super.key,
});
/// Optional title for the category list. This will be displayed at the
/// top of the list.
final String? title;
/// Optional custom title widget for the category list. This will be
/// displayed at the top of the list. If set, the text title will be
/// ignored.
final Widget? customTitle;
/// Optional title style for the title of the category list. This will
/// be applied to the title of the category list. If not set, the default
/// text style will be used.
final TextStyle? titleStyle;
/// Configure if the title should be centered.
///
/// Default is false.
final bool titleCentered;
/// Optional header styling for the categories. This will be applied to
/// the name of the categories. If not set, the default text style will
/// be used.
final CategoryHeaderStyling? headerStyling;
/// Configure if the category header should be centered.
///
/// Default is false.
final bool headerCentered;
/// Configure if the category should be collapsible.
///
/// Default is true.
final bool isCategoryCollapsible;
/// The depth of the category list. This is used to keep track of
/// the depth of the categories. This is used internally and should
/// not be set manually.
final int categoryDepth;
/// The content of the category list. This is a list of categories.
final List<Category> content;
@override
Widget build(BuildContext context) => SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (title != null || customTitle != null)
titleCentered
? Center(
child: _CategoryTitle(
title: title,
titleStyle: titleStyle,
customTitle: customTitle,
),
)
: _CategoryTitle(
title: title,
titleStyle: titleStyle,
customTitle: customTitle,
),
...content.map(
(category) => _CategoryColumn(
category: category,
headerCentered: headerCentered,
headerStyling: headerStyling,
collapsible: isCategoryCollapsible,
categoryDepth: categoryDepth,
),
),
].toList(),
),
);
}
class _CategoryTitle extends StatelessWidget {
const _CategoryTitle({
this.customTitle,
this.title,
this.titleStyle,
}) : assert(
customTitle != null || title != null,
"Either customTitle or title must be set.",
);
final String? title;
final Widget? customTitle;
final TextStyle? titleStyle;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
var styleOfTitle = titleStyle ?? theme.textTheme.titleLarge;
return customTitle ??
Text(
title!,
style: styleOfTitle,
);
}
}
class _CategoryColumn extends StatefulWidget {
const _CategoryColumn({
required this.category,
required this.categoryDepth,
required this.headerCentered,
this.headerStyling,
this.collapsible = true,
});
final Category category;
final CategoryHeaderStyling? headerStyling;
final bool headerCentered;
final int categoryDepth;
final bool collapsible;
@override
State<_CategoryColumn> createState() => _CategoryColumnState();
}
class _CategoryColumnState extends State<_CategoryColumn> {
bool _isExpanded = true;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
var styleOfCategory =
widget.headerStyling?.headerStyles[widget.categoryDepth] ??
widget.headerStyling?.defaultStyle ??
theme.textTheme.titleMedium ??
const TextStyle();
String? formatCategoryName() {
var name = widget.category.name;
if (name == null) return null;
return switch (widget.headerStyling?.capitalization) {
CategoryHeaderCapitalization.capitalizeFirstLetter =>
name[0].toUpperCase() + name.substring(1),
CategoryHeaderCapitalization.lowercase => name.toLowerCase(),
CategoryHeaderCapitalization.uppercase => name.toUpperCase(),
CategoryHeaderCapitalization.none => name,
_ => name,
};
}
Widget buildCollapsibleHeader(TextStyle styleOfCategory) => InkWell(
onTap: () {
setState(() {
_isExpanded = !_isExpanded;
});
},
child: Row(
mainAxisAlignment: widget.headerCentered
? MainAxisAlignment.center
: MainAxisAlignment.start,
children: [
ExpandIcon(
onPressed: (value) => setState(() {
_isExpanded = !_isExpanded;
}),
isExpanded: _isExpanded,
padding: EdgeInsets.zero,
),
const SizedBox(width: 8),
if (widget.category.customTitle != null)
widget.category.customTitle!
else
Text(
formatCategoryName()!,
style: styleOfCategory,
),
],
),
);
Widget buildNonCollapsibleHeader(TextStyle styleOfCategory) =>
widget.headerCentered
? Center(
child: widget.category.customTitle != null
? widget.category.customTitle!
: Text(
formatCategoryName()!,
style: styleOfCategory,
),
)
: widget.category.customTitle != null
? widget.category.customTitle!
: Text(
formatCategoryName()!,
style: styleOfCategory,
);
Widget buildNestedCategoryList() => CategoryList(
content: widget.category.nestedCategories,
headerCentered: widget.headerCentered,
headerStyling: widget.headerStyling,
isCategoryCollapsible: widget.collapsible,
categoryDepth: widget.categoryDepth + 1,
);
return SizedBox(
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.collapsible) ...[
buildCollapsibleHeader(styleOfCategory),
] else ...[
buildNonCollapsibleHeader(styleOfCategory),
],
AnimatedCrossFade(
duration: const Duration(milliseconds: 300),
firstChild: const SizedBox(width: double.infinity),
secondChild: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.category.content.isNotEmpty)
...widget.category.content,
if (widget.category.nestedCategories.isNotEmpty)
buildNestedCategoryList(),
],
),
crossFadeState: _isExpanded
? CrossFadeState.showSecond
: CrossFadeState.showFirst,
),
],
),
);
}
}