diff --git a/CHANGELOG.md b/CHANGELOG.md index 41cc7d8..f9ef8a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ ## 0.0.1 -* TODO: Describe initial release. +* Initial Nested Category functionality. +* Title of categories +* Collapsible categories diff --git a/README.md b/README.md index 02fe8ec..19bc37c 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,68 @@ - - -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 diff --git a/analysis_options.yaml b/analysis_options.yaml index 31b4b51..0736605 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,4 +1,4 @@ -include: package:flutter_iconica_analysis/analysis_options.yaml +include: package:flutter_iconica_analysis/components_options.yaml # Possible to overwrite the rules from the package diff --git a/example/.gitignore b/example/.gitignore new file mode 100644 index 0000000..8760a85 --- /dev/null +++ b/example/.gitignore @@ -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/ diff --git a/example/README.md b/example/README.md new file mode 100644 index 0000000..05a6344 --- /dev/null +++ b/example/README.md @@ -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. diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml new file mode 100644 index 0000000..31b4b51 --- /dev/null +++ b/example/analysis_options.yaml @@ -0,0 +1,9 @@ +include: package:flutter_iconica_analysis/analysis_options.yaml + +# Possible to overwrite the rules from the package + +analyzer: + exclude: + +linter: + rules: diff --git a/example/lib/main.dart b/example/lib/main.dart new file mode 100644 index 0000000..d4ba714 --- /dev/null +++ b/example/lib/main.dart @@ -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"), + ], + ), + ], + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/example/pubspec.yaml b/example/pubspec.yaml new file mode 100644 index 0000000..23a6e36 --- /dev/null +++ b/example/pubspec.yaml @@ -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 \ No newline at end of file diff --git a/lib/flutter_nested_categories.dart b/lib/flutter_nested_categories.dart new file mode 100644 index 0000000..72fb340 --- /dev/null +++ b/lib/flutter_nested_categories.dart @@ -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"; diff --git a/lib/src/category.dart b/lib/src/category.dart new file mode 100644 index 0000000..a536415 --- /dev/null +++ b/lib/src/category.dart @@ -0,0 +1,49 @@ +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 { + /// Creates a category. + /// + /// The [name] is the name of the category. This will be displayed at the + /// top of the category. If the [customTitle] is set, this will be ignored. + /// Inside of the [content] you can put any widget you want. This will be + /// displayed after the title of the category. If the category has nested + /// categories, the content will be displayed before the nested categories. + /// The [nestedCategories] are the nested categories of this category. This + /// can be an empty list if there are no nested categories. + const Category({ + this.name, + this.customTitle, + this.content = const [], + this.nestedCategories = const [], + }) : 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 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 content; +} diff --git a/lib/src/category_header_capitalization.dart b/lib/src/category_header_capitalization.dart new file mode 100644 index 0000000..a4d03b6 --- /dev/null +++ b/lib/src/category_header_capitalization.dart @@ -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, +} diff --git a/lib/src/category_header_styling.dart b/lib/src/category_header_styling.dart new file mode 100644 index 0000000..aad4b73 --- /dev/null +++ b/lib/src/category_header_styling.dart @@ -0,0 +1,32 @@ +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 { + /// Creates a category header styling. + /// + /// The [defaultStyle] is the default style for the headers. This will be + /// used if the depth is not found in the [headerStyles]. + /// + /// The [capitalization] is used to determine how the headers should be + /// displayed. This is useful if your data comes out of a database for + /// example and you want to display it in a certain way. + 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 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; +} diff --git a/lib/src/category_list.dart b/lib/src/category_list.dart new file mode 100644 index 0000000..bd7d6ff --- /dev/null +++ b/lib/src/category_list.dart @@ -0,0 +1,265 @@ +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 { + /// Creates a category list. + /// + /// This Widget allows you to create nested 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. + 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 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, + ), + ], + ), + ); + } +}