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 ## 0.0.1
* TODO: Describe initial release. * Initial Nested Category functionality.
* Title of categories
* Collapsible categories

View file

@ -1,39 +1,68 @@
<!-- # flutter_nested_categories
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.
For information about how to write a good package README, see the guide for This Flutter component allows you to easily create nested categories that are very
[writing package pages](https://dart.dev/guides/libraries/writing-package-pages). customizable.
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.
## Features ## Features
TODO: List what your package can do. Maybe include images, gifs, or videos. * Nested Categories
* Collapsible Categories
## Getting started
TODO: List prerequisites and provide or point to information on how to
start using the package.
## Usage ## Usage
TODO: Include short and useful examples for package users. Add longer examples You can build a nested category using the `CategoryList` class, like this:
to `/example` folder.
```dart ```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 * Setting a (text)title (and title styling/center),
contribute to the package, how to file issues, what response they can expect * Setting a custom title,
from the package authors, and more. * 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,
),
],
),
);
}
}