import 'dart:developer'; import 'package:easy_debounce/easy_debounce.dart'; import 'package:flutter/material.dart'; import 'package:hub/components/templates_components/details_component/details_component_widget.dart'; import 'package:hub/features/backend/index.dart'; import 'package:hub/flutter_flow/index.dart'; import 'package:hub/shared/extensions.dart'; import 'package:hub/shared/utils.dart'; import 'package:hub/shared/widgets.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_rx_bloc/flutter_rx_bloc.dart'; import 'package:rx_bloc/rx_bloc.dart'; import 'package:rxdart/rxdart.dart' as rx; part 'documents.rxb.g.dart'; /// ----------------------------------------------- /// [TypeDefs] ----------------------------------- /// ----------------------------------------------- typedef DocumentKey = GlobalKey; /// ----------------------------------------------- /// [Extensions] --------------------------------- /// ----------------------------------------------- extension ExplicitRxdartStartWithExtension on Stream { Stream rxdartStartWith(T? value) => rx.StartWithExtension(this).startWith(value); } /// ----------------------------------------------- /// [Pages] --------------------------------------- /// ----------------------------------------------- class DocumentPage extends StatefulPage { const DocumentPage({super.key}); @override State createState() => DocumentPageState(); } class DocumentPageState extends PageState { @override void initState() { super.initState(); } @override Widget build(BuildContext context) { return RxBlocMultiBuilder2( state1: (bloc) => bloc.states.isDocumentSelected, state2: (bloc) => bloc.states.currentDocument, bloc: context.read(), builder: (context, isSelect, current, bloc) { if (isSelect.hasData && isSelect.data!) { return _buildDocumentViewScreen(current, bloc); } else { return _buildDocumentManagerScreen(); } }, ); } Widget _buildDocumentManagerScreen() { final model = context.read().model; return DocumentManagerScreen( model: model, state: this, ); } Widget _buildDocumentViewScreen( AsyncSnapshot<(Document, Uri)?> snapshot, DocumentPageBlocType bloc) { if (snapshot.hasData) { return DocumentViewerScreen( doc: snapshot.data!, bloc: bloc, ); } else { return const Center(child: CircularProgressIndicator()); } } } /// ----------------------------------------------- /// [Screens] ------------------------------------ /// ----------------------------------------------- class DocumentManagerScreen extends StatelessScreen { final DocumentModel model; final DocumentPageState state; const DocumentManagerScreen({ super.key, required this.model, required this.state, }); @override Widget build(BuildContext context) { final String title = FFLocalizations.of(context).getVariableText( enText: 'Documents', ptText: 'Documentos', ); final theme = FlutterFlowTheme.of(context); action() => Navigator.pop(context); return Scaffold( backgroundColor: theme.primaryBackground, appBar: buildAppBar(title, context, action), body: buildBody(context), ); } Widget buildBody(BuildContext context) { const SizedBox space = SizedBox(height: 10); final controller = EnhancedListViewController( headerBuilder: model.itemHeaderBuilder, bodyBuilder: model.itemBodyBuilder, footerBuilder: model.itemFooterBuilder, ); final repository = EnhancedListViewRepository( fetchHeader: model.generateHeaderItems, fetchBody: model.generateBodyItems, fetchFooter: model.generateFooterItems, ); return Column( children: [ Expanded( child: EnhancedListView( key: model.vehicleScreenManager, controller: controller, repository: repository, ), ), ] // .addToStart(space) .addToEnd(space), ); } } class DocumentViewerScreen extends StatefulScreen { const DocumentViewerScreen({ super.key, required this.doc, required this.bloc, }); final (Document, Uri) doc; final DocumentPageBlocType bloc; @override ScreenState createState() => _DocumentViewerScreenState(); } class _DocumentViewerScreenState extends ScreenState { @override void initState() { super.initState(); } @override Widget build(BuildContext context) { final String title = widget.doc.$1.description; final theme = FlutterFlowTheme.of(context); final locale = FFLocalizations.of(context); final Color color = widget.doc.$1.category.color; ; backAction() => widget.bloc.events.unselectDocument(); infoAction() => DetailsComponentWidget( buttons: [], statusHashMap: [ Map.from({ widget.doc.$1.description: widget.doc.$1.category.color, }) ], labelsHashMap: Map.from({ locale.getVariableText( enText: 'Description', ptText: 'Descrição', ): widget.doc.$1.description, locale.getVariableText( enText: 'Category', ptText: 'Categoria', ): widget.doc.$1.category.title, if (widget.doc.$1.person.isNotEmpty) locale.getVariableText( enText: 'Person', ptText: 'Pessoa', ): widget.doc.$1.person, if (widget.doc.$1.property.isNotEmpty) locale.getVariableText( enText: 'Property', ptText: 'Propriedade', ): widget.doc.$1.property, locale.getVariableText( enText: 'Created At', ptText: 'Criado em', ): ValidatorUtil.toLocalDateTime( 'yyyy-MM-dd', widget.doc.$1.createdAt), locale.getVariableText( enText: 'Updated At', ptText: 'Atualizado em', ): ValidatorUtil.toLocalDateTime( 'yyyy-MM-dd', widget.doc.$1.updatedAt), }), ); return PopScope( canPop: false, onPopInvokedWithResult: (didPop, result) => backAction(), child: Scaffold( backgroundColor: theme.primaryBackground, appBar: buildAppBar(title, context, backAction, infoAction), body: buildBody(context), ), ); } Widget buildBody(BuildContext context) { return ReadView( title: widget.doc.$1.description, url: widget.doc.$2.toString(), ); } } /// ----------------------------------------------- /// [Models] -------------------------------------- /// ----------------------------------------------- class DocumentModel extends FlutterFlowModel { final DocumentPageBlocType bloc; DocumentModel(this.bloc); late EnhancedListViewKey vehicleScreenManager; late DocumentKey vehicleScreenViewer; late PagingController _pagingController; late bool categoryIsSelected; /// ------------ @override void initState(BuildContext context) { vehicleScreenManager = EnhancedListViewKey(); vehicleScreenViewer = DocumentKey(); _pagingController = PagingController(firstPageKey: 1); categoryIsSelected = false; } @override void dispose() { _pagingController.dispose(); vehicleScreenManager.currentState?.dispose(); vehicleScreenViewer.currentState?.dispose(); } /// ------------ /// [Body] void onView(Document document, BuildContext context) async { bloc.events.selectDocument(document); } Widget itemBodyBuilder( BuildContext context, T item, int index) { log('ItemBuilder -> $index'); return DocumentComponent( document: item, onPressed: onView, ); } Future> generateBodyItems( int pageKey, int pageSize, Q query) async { final List error = [null]; log('Query: ${query is Document}'); final GetDocuments getDocuments = FreAccessWSGlobal.getDocuments; final ApiCallResponse newItems = await getDocuments.call(pageKey, query); if (newItems.jsonBody == null) return error; if (newItems.jsonBody['error'] == true) return error; final List list = newItems.jsonBody['value']['list']; late final List docs = []; for (var item in list) { final String description = item['description']; final String type = item['type']; final String category = item['category']['description']; final String color = item['category']['color']; final String person = item['person'] ?? ''; final String property = item['property'] ?? ''; final String createdAt = item['createdAt']; final String updatedAt = item['updatedAt']; final int categoryId = item['category']['id']; final int documentId = item['id']; final doc = Document( id: documentId, description: description, type: type, category: Category( id: categoryId, color: color.toColor(), title: category, ), person: person, property: property, createdAt: createdAt, updatedAt: updatedAt, ); docs.add(doc); } return docs as List; } /// [Footer] Widget itemFooterBuilder( Future> Function() fetchData) => Builder(builder: (context) { CategoryComponent categoryItemBuilder(T? item) { return CategoryComponent(category: item! as Category); } return EnhancedCarouselView( dataProvider: fetchData, itemBuilder: categoryItemBuilder, filter: filterByCategory, ); }); Future> generateFooterItems() async { final List error = [null]; final GetCategories getCategories = FreAccessWSGlobal.getCategories; final ApiCallResponse newItems = await getCategories.call(); if (newItems.jsonBody['error'] == true) return error; if (newItems.jsonBody == null) return error; final list = newItems.jsonBody['value'] as List; late final List cats = []; for (var item in list) { final String color = item['color']; final String title = item['description']; final int id = item['id']; final cat = Category( id: id, color: color.toColor(), title: title, ); cats.add(cat); } return cats as List; } /// [Header] Widget itemHeaderBuilder( Future> Function() generateHeaderItems) { return Builder(builder: (context) { final theme = FlutterFlowTheme.of(context); final locale = FFLocalizations.of(context); TextEditingController editingController = TextEditingController(); return TextFormField( controller: editingController, onChanged: (value) => EasyDebounce.debounce( '_model.keyTextFieldTextController', const Duration(milliseconds: 500), () => filterBySearchBar(Document.from(value), context), ), cursorColor: theme.primaryText, showCursor: false, cursorWidth: 2.0, cursorRadius: Radius.circular(100), style: TextStyle( color: theme.primaryText, fontSize: 16.0, decorationColor: Colors.amber, ), keyboardType: TextInputType.text, textInputAction: TextInputAction.search, autocorrect: true, textCapitalization: TextCapitalization.sentences, decoration: InputDecoration( prefixIcon: Icon(Icons.search, color: theme.primary), labelText: locale.getVariableText( ptText: 'Pesquisar', enText: 'Search', ), labelStyle: TextStyle( color: theme.primaryText, fontSize: 16.0, ), hintText: locale.getVariableText( ptText: 'Digite sua pesquisa', enText: 'Enter your search', ), hintStyle: TextStyle( color: theme.accent2, fontSize: 14.0, ), filled: true, fillColor: Colors.transparent, helperStyle: TextStyle( color: theme.primaryText, decorationColor: theme.primaryText, ), focusColor: theme.primaryText, contentPadding: EdgeInsets.symmetric(vertical: 10.0, horizontal: 15.0), enabledBorder: UnderlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(15.0)), borderSide: BorderSide(color: theme.primaryText), ), focusedBorder: UnderlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(15.0)), borderSide: BorderSide(color: theme.primaryText), ), errorBorder: UnderlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(15.0)), borderSide: BorderSide(color: theme.primaryText), ), focusedErrorBorder: UnderlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(15.0)), borderSide: BorderSide(color: theme.primaryText, width: 2.0), ), ), ); }); } Future> generateHeaderItems() async { final SearchField item = SearchField(); return [item] as List; } /// [Filter] void filterBySearchBar(T query, BuildContext context) { final key = vehicleScreenManager.currentState; return key?.filterBodyItems(query); } void filterByCategory(T query, BuildContext context) { final key = vehicleScreenManager.currentState; categoryIsSelected ? key?.filterBodyItems(null) : key?.filterBodyItems(query); categoryIsSelected = !categoryIsSelected; } /// [Exception] void onFetchError(Object e, StackTrace s) { DialogUtil.errorDefault(vehicleScreenViewer.currentContext!); LogUtil.requestAPIFailed( "proccessRequest.php", "", "Consulta de Veículo", e, s); } } /// ----------------------------------------------- /// [BLoCs] --------------------------------------- /// ----------------------------------------------- abstract class DocumentPageBlocEvents { void selectDocument(Document document); void unselectDocument(); } abstract class DocumentPageBlocStates { Stream get isDocumentSelected; Stream<(Document, Uri)?> get currentDocument; } @RxBloc() class DocumentPageBloc extends $DocumentPageBloc { late final DocumentModel model; DocumentPageBloc(BuildContext context) { model = DocumentModel(this); model.initState(context); } @override Stream<(Document, Uri)?> _mapToCurrentDocumentState() => _$selectDocumentEvent .switchMap((event) async* { final uri = await GetPDF().call(event.id); yield (event, uri); }) .rxdartStartWith(null) .mergeWith([_$unselectDocumentEvent.map((_) => null)]); @override Stream _mapToIsDocumentSelectedState() => _mapToCurrentDocumentState() .map((document) => document != null) .rxdartStartWith(false) .map((isSelected) => isSelected ?? false); } /// ----------------------------------------------- /// [Interfaces] --------------------------------- /// ----------------------------------------------- abstract interface class Archive extends Entity {} interface class SearchField extends Archive { SearchField(); } interface class Document extends Archive { final int id; final String description; final String type; final Category category; final String person; final String property; String createdAt; String updatedAt; Document({ required this.id, required this.description, required this.type, required this.category, required this.person, required this.property, required this.createdAt, required this.updatedAt, }); factory Document.from(String desc) => Document( id: 0, description: desc, type: '', category: Category.fromDesc(''), person: '', property: '', createdAt: '', updatedAt: '', ); } interface class Category extends Archive { final int id; final Color color; final String title; Category({ required this.id, required this.color, required this.title, }); factory Category.fromDesc(String desc) { return Category( id: 0, color: Colors.transparent, title: desc, ); } static Color isSelected() => Colors.black; } /// ----------------------------------------------- /// [Components] ------------------------------------- /// ----------------------------------------------- // ignore: must_be_immutable class DocumentComponent extends StatelessComponent { final Document document; void Function(Document, BuildContext) onPressed; DocumentComponent({ super.key, required this.document, required this.onPressed, }); Tooltip _buildTooltip(String text, Color color, BuildContext context, BoxConstraints constraints) { final Color textColor = FlutterFlowTheme.of(context).info; final area = (MediaQuery.of(context).size.height + MediaQuery.of(context).size.width) / 2; final double boxHeight = area * 0.033; final double boxWidth = area * 0.19; return Tooltip( message: text, child: Container( width: boxWidth, height: boxHeight, decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(10), ), child: Center( child: AutoText( text, overflow: TextOverflow.ellipsis, style: TextStyle( color: textColor, fontWeight: FontWeight.bold, ), ), ), ), ); } @override Widget build(BuildContext context) { final Color primaryText = FlutterFlowTheme.of(context).primaryText; final Color primaryColor = FlutterFlowTheme.of(context).primary; final TextStyle textStyleMajor = TextStyle( color: primaryText, fontWeight: FontWeight.bold, ); final TextStyle textStyleMinor = TextStyle( color: primaryText, fontWeight: FontWeight.normal, fontStyle: FontStyle.italic, ); return Padding( padding: const EdgeInsets.all(8), child: LayoutBuilder( builder: (context, constraints) { final double boxHeight = constraints.maxHeight > 350 ? MediaQuery.of(context).size.height * 0.07 : MediaQuery.of(context).size.height * 2; final color = document.category.color; // final color = FlutterFlowTheme.of(context).primary; final icon = Icons.description; const space = SizedBox(width: 10); final description = document.description; final title = document.category.title; const double size = 20; return InkWell( onTap: () => onPressed(document, context), enableFeedback: true, overlayColor: WidgetStateProperty.all(primaryColor), borderRadius: BorderRadius.circular(10), child: SizedBox( height: boxHeight, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, spacing: size, children: [ // const SizedBox(width: 10), Icon(icon, color: color), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ Tooltip( message: description, child: AutoText( description, style: textStyleMajor, overflow: TextOverflow.ellipsis, ), ), AutoText( ValidatorUtil.toLocalDateTime( 'yyyy-MM-dd', document.updatedAt, ), style: textStyleMinor, overflow: TextOverflow.ellipsis, ), ], ), ), Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.end, children: [ _buildTooltip(title, color, context, constraints), ], ), ), // const SizedBox(width: 10), Center( child: Icon( Icons.arrow_right, color: primaryText, ), ), ] // .addToStart(space) .addToEnd(space), ), ), ); }, ), ); } DocumentComponent copyWith({ Document? document, }) { return DocumentComponent( document: document ?? this.document, onPressed: onPressed, ); } } class CategoryComponent extends StatelessComponent { final Category category; const CategoryComponent({ super.key, required this.category, }); @override Widget build(BuildContext context) { final backgroundTheme = FlutterFlowTheme.of(context).primaryBackground; return ColoredBox( color: backgroundTheme, child: Padding( padding: const EdgeInsets.all(8.0), child: Column( children: [ Container( padding: const EdgeInsets.all(8.0), decoration: BoxDecoration( color: category.color, shape: BoxShape.circle, ), child: Icon( Icons.folder, color: Colors.white, size: 40, ), ), const SizedBox(height: 8), Text( category.title, style: TextStyle( color: category.color, fontWeight: FontWeight.bold, ), overflow: TextOverflow.ellipsis, ), ], ), ), ); } }