WIP
This commit is contained in:
parent
07f55bbceb
commit
866e438c87
|
@ -5,8 +5,7 @@ import 'dart:convert';
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:hub/features/documents/index.dart' as doc;
|
||||
import 'package:hub/features/documents/documents.dart' as doc;
|
||||
import 'package:hub/features/notification/index.dart';
|
||||
import 'package:hub/features/storage/index.dart';
|
||||
|
||||
|
|
|
@ -1,3 +1 @@
|
|||
part of 'index.dart';
|
||||
|
||||
abstract interface class Archive extends Entity {}
|
||||
|
|
|
@ -1,68 +1 @@
|
|||
part of 'index.dart';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
class CategoryItem extends StatelessComponent {
|
||||
final Category category;
|
||||
|
||||
const CategoryItem({
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,177 +1 @@
|
|||
part of 'index.dart';
|
||||
|
||||
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.fromDesc(String desc) => Document(
|
||||
id: 0,
|
||||
description: desc,
|
||||
type: '',
|
||||
category: Category.fromDesc(''),
|
||||
person: '',
|
||||
property: '',
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
);
|
||||
}
|
||||
|
||||
// ignore: must_be_immutable
|
||||
class DocumentItem extends StatelessComponent {
|
||||
final Document document;
|
||||
void Function(Document, BuildContext) onPressed;
|
||||
|
||||
DocumentItem({
|
||||
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;
|
||||
|
||||
return InkWell(
|
||||
onTap: () => onPressed(document, context),
|
||||
enableFeedback: true,
|
||||
overlayColor: WidgetStateProperty.all<Color>(primaryColor),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: SizedBox(
|
||||
height: boxHeight,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// const SizedBox(width: 10),
|
||||
Icon(Icons.description, color: document.category.color),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Tooltip(
|
||||
message: document.description,
|
||||
child: AutoText(
|
||||
document.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(
|
||||
document.category.title,
|
||||
document.category.color,
|
||||
context,
|
||||
constraints,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// const SizedBox(width: 10),
|
||||
Center(
|
||||
child: Icon(
|
||||
Icons.arrow_right,
|
||||
color: primaryText,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
DocumentItem copyWith({
|
||||
Document? document,
|
||||
}) {
|
||||
return DocumentItem(
|
||||
document: document ?? this.document,
|
||||
onPressed: onPressed,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,148 +1 @@
|
|||
part of 'index.dart';
|
||||
|
||||
/// -----------------------------------------------
|
||||
/// [DocumentPageBloc]
|
||||
/// -----------------------------------------------
|
||||
|
||||
class DocumentPageBloc extends Bloc<DocumentPageEvent, DocumentPageState> {
|
||||
final DocumentPageModel model;
|
||||
static DocumentPageBloc? _singleton;
|
||||
|
||||
factory DocumentPageBloc(DocumentPageModel model) {
|
||||
_singleton ??= DocumentPageBloc._internal(model);
|
||||
return _singleton!;
|
||||
}
|
||||
|
||||
DocumentPageBloc._internal(this.model) : super(DocumentPageState()) {
|
||||
on<SelectDocumentEvent>(_selectDocument);
|
||||
on<UnselectDocumentEvent>(_unselectDocument);
|
||||
on<FilterCategoryEvent>(_filterCategoryEvent);
|
||||
}
|
||||
|
||||
Future<void> _filterCategoryEvent(
|
||||
FilterCategoryEvent event, Emitter<DocumentPageState> emit) async {
|
||||
_selectCategory(event, emit);
|
||||
state.isCategorySelected
|
||||
? _unselectCategory(event, emit)
|
||||
: _selectCategory(event, emit);
|
||||
}
|
||||
|
||||
Future<void> _selectCategory(
|
||||
FilterCategoryEvent event, Emitter<DocumentPageState> emit) async {
|
||||
log('filterItems A: ${event.query}');
|
||||
emit(state.copyWith(
|
||||
isCategorySelected: true,
|
||||
));
|
||||
|
||||
final listViewState = model.vihicleScreenManager.currentState!;
|
||||
listViewState.widget.bodyItems = (await model.generateBodyItems(
|
||||
1, 10, event.query)) as BodyItemsBuilder<Document>;
|
||||
}
|
||||
|
||||
Future<void> _unselectCategory(
|
||||
FilterCategoryEvent event, Emitter<DocumentPageState> emit) async {
|
||||
emit(state.copyWith(
|
||||
isCategorySelected: false,
|
||||
));
|
||||
|
||||
final listViewState = model.vihicleScreenManager.currentState!;
|
||||
listViewState.widget.bodyItems = (await model.generateBodyItems(
|
||||
1, 10, null)) as BodyItemsBuilder<Document>;
|
||||
}
|
||||
|
||||
Future<void> _selectDocument(
|
||||
SelectDocumentEvent event, Emitter<DocumentPageState> emit) async {
|
||||
print('-> select');
|
||||
emit(
|
||||
state.copyWith(
|
||||
uri: await GetPDF().call(
|
||||
event.document.id,
|
||||
),
|
||||
currentDocument: event.document,
|
||||
isDocumentSelected: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _unselectDocument(
|
||||
UnselectDocumentEvent event, Emitter<DocumentPageState> emit) async {
|
||||
emit(
|
||||
state.copyWith(
|
||||
currentDocument: null,
|
||||
isDocumentSelected: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// -----------------------------------------------
|
||||
/// [DocumentPageEvent]
|
||||
/// -----------------------------------------------
|
||||
|
||||
abstract class DocumentPageEvent {}
|
||||
|
||||
class SelectDocumentEvent extends DocumentPageEvent {
|
||||
final Document document;
|
||||
SelectDocumentEvent(
|
||||
this.document,
|
||||
);
|
||||
}
|
||||
|
||||
class UnselectDocumentEvent extends DocumentPageEvent {}
|
||||
|
||||
class FilterCategoryEvent extends DocumentPageEvent {
|
||||
final Query query;
|
||||
FilterCategoryEvent(this.query);
|
||||
}
|
||||
|
||||
/// -----------------------------------------------
|
||||
/// [DocumentPageState]
|
||||
/// -----------------------------------------------
|
||||
|
||||
class DocumentPageState {
|
||||
final bool isCategorySelected;
|
||||
final bool isDocumentSelected;
|
||||
final Document? currentDocument;
|
||||
final Category? currentCategory;
|
||||
final Uri? uri;
|
||||
final int? count;
|
||||
final dynamic page;
|
||||
final Query? query;
|
||||
|
||||
const DocumentPageState({
|
||||
this.query,
|
||||
this.count,
|
||||
this.page,
|
||||
this.uri,
|
||||
this.currentDocument,
|
||||
this.isCategorySelected = false,
|
||||
this.currentCategory,
|
||||
this.isDocumentSelected = false,
|
||||
});
|
||||
|
||||
DocumentPageState copyWith({
|
||||
Uri? uri,
|
||||
Query? query,
|
||||
int? count,
|
||||
dynamic page,
|
||||
List<Document?>? documents,
|
||||
Document? currentDocument,
|
||||
bool? isDocumentSelected,
|
||||
List<Category?>? categories,
|
||||
Category? currentCategory,
|
||||
bool? isCategorySelected,
|
||||
}) {
|
||||
return DocumentPageState(
|
||||
uri: uri ?? this.uri,
|
||||
query: query ?? this.query,
|
||||
count: count ?? this.count,
|
||||
page: page ?? this.page,
|
||||
//
|
||||
currentDocument: currentDocument ?? this.currentDocument,
|
||||
isDocumentSelected: isDocumentSelected ?? this.isDocumentSelected,
|
||||
//
|
||||
currentCategory: currentCategory ?? this.currentCategory,
|
||||
isCategorySelected: isCategorySelected ?? this.isCategorySelected,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,175 +1 @@
|
|||
part of 'index.dart';
|
||||
|
||||
class DocumentPageModel extends FlutterFlowModel<DocumentPage> {
|
||||
DocumentPageModel._privateConstructor();
|
||||
|
||||
static final DocumentPageModel _instance =
|
||||
DocumentPageModel._privateConstructor();
|
||||
|
||||
factory DocumentPageModel() {
|
||||
return _instance;
|
||||
}
|
||||
|
||||
late EnhancedListViewKey<Document, Category, Null> vihicleScreenManager;
|
||||
late DocumentKey vehicleScreenViewer;
|
||||
late PagingController<int, Document> _pagingController;
|
||||
|
||||
/// ------------
|
||||
|
||||
@override
|
||||
void initState(BuildContext context) {
|
||||
vihicleScreenManager = EnhancedListViewKey<Document, Category, Null>();
|
||||
vehicleScreenViewer = DocumentKey();
|
||||
|
||||
_pagingController = PagingController<int, Document>(firstPageKey: 1);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pagingController.dispose();
|
||||
vihicleScreenManager.currentState?.dispose();
|
||||
vehicleScreenViewer.currentState?.dispose();
|
||||
}
|
||||
|
||||
/// ------------
|
||||
|
||||
/// [onView]
|
||||
void onView(Document document, BuildContext context) async {
|
||||
context.read<DocumentPageBloc>().add(SelectDocumentEvent(document));
|
||||
}
|
||||
|
||||
/// [itemBodyBuilder]
|
||||
DocumentItem itemBodyBuilder<T extends Document>(
|
||||
BuildContext context, T item, int index) {
|
||||
print('ItemBuilder -> $index');
|
||||
return DocumentItem(
|
||||
document: item,
|
||||
onPressed: onView,
|
||||
);
|
||||
}
|
||||
|
||||
CategoryItem categoryItemBuilder<T>(T? item) {
|
||||
return CategoryItem(category: item! as Category);
|
||||
}
|
||||
|
||||
/// [itemHeaderBuilder]
|
||||
Widget itemHeaderBuilder<T>(Future<List<T?>> Function() gen) =>
|
||||
Builder(builder: (context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(15, 0, 50, 0),
|
||||
child: Text(
|
||||
FFLocalizations.of(context).getVariableText(
|
||||
enText: 'Recent Documents', ptText: 'Últimos Documentos'),
|
||||
style: TextStyle(
|
||||
color: FlutterFlowTheme.of(context).primaryText,
|
||||
fontSize: LimitedFontSizeUtil.getHeaderFontSize(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
EnhancedCarouselView<T>(
|
||||
generateItems: gen,
|
||||
itemBuilder: categoryItemBuilder,
|
||||
filter: filter<T>,
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
/// [generateBodyItems]
|
||||
Future<List<T?>> generateBodyItems<T>(
|
||||
int pageKey, int pageSize, dynamic query) async {
|
||||
log('generateDocuments: $query');
|
||||
|
||||
final List<T?> error = [null];
|
||||
print('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<dynamic> list = newItems.jsonBody['value']['list'];
|
||||
|
||||
late final List<Document> docs = [];
|
||||
|
||||
for (var item in list) {
|
||||
log('-> generateDocuments: $item');
|
||||
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<T?>;
|
||||
}
|
||||
|
||||
/// [generateHeaderItems]
|
||||
Future<List<T?>> generateHeaderItems<T>() async {
|
||||
log('generateCategories: ');
|
||||
final List<T?> 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<dynamic>;
|
||||
late final List<Category> 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);
|
||||
}
|
||||
log('cats: $cats');
|
||||
return cats as List<T?>;
|
||||
}
|
||||
|
||||
/// [filter]
|
||||
void filter<T>(T query, BuildContext context) {
|
||||
context
|
||||
.read<DocumentPageBloc>()
|
||||
.add(FilterCategoryEvent(query as Archive?));
|
||||
}
|
||||
|
||||
/// [onFetchError]
|
||||
void onFetchError(Object e, StackTrace s) {
|
||||
DialogUtil.errorDefault(vehicleScreenViewer.currentContext!);
|
||||
LogUtil.requestAPIFailed(
|
||||
"proccessRequest.php", "", "Consulta de Veículo", e, s);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,44 +1 @@
|
|||
part of 'index.dart';
|
||||
|
||||
typedef DocumentKey = GlobalKey<FREDocumentPageState>;
|
||||
|
||||
class DocumentPage extends StatefulPage {
|
||||
const DocumentPage({super.key});
|
||||
|
||||
@override
|
||||
State<DocumentPage> createState() => FREDocumentPageState();
|
||||
}
|
||||
|
||||
class FREDocumentPageState<T extends DocumentPage>
|
||||
extends PageState<DocumentPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) => buildBody(context);
|
||||
DocumentPageModel model = DocumentPageModel();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
model.initState(context);
|
||||
}
|
||||
|
||||
Widget buildBody(BuildContext context) {
|
||||
return BlocProvider<DocumentPageBloc>(
|
||||
create: (context) => DocumentPageBloc(model),
|
||||
child: BlocBuilder<DocumentPageBloc, DocumentPageState>(
|
||||
builder: (context, state) {
|
||||
print('Bloc -> ${state.isCategorySelected}');
|
||||
|
||||
if (state.isDocumentSelected)
|
||||
return DocumentViewScreen(
|
||||
doc: state.currentDocument!,
|
||||
uri: state.uri!,
|
||||
);
|
||||
else
|
||||
return DocumentManagerScreen(
|
||||
model: model,
|
||||
state: state,
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,49 +1 @@
|
|||
part of 'index.dart';
|
||||
|
||||
class DocumentManagerScreen extends StatelessScreen {
|
||||
final DocumentPageModel 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) {
|
||||
final SizedBox space = SizedBox(height: 30);
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: EnhancedListView<Document, Category, Null>(
|
||||
key: model.vihicleScreenManager,
|
||||
headerBuilder: model.itemHeaderBuilder<Category>,
|
||||
headerItems: model.generateHeaderItems<Category>,
|
||||
bodyBuilder: model.itemBodyBuilder<Document>,
|
||||
bodyItems: model.generateBodyItems<Document>,
|
||||
footerBuilder: null,
|
||||
footerItems: null,
|
||||
),
|
||||
),
|
||||
] //
|
||||
.addToStart(space)
|
||||
.addToEnd(space),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,46 +1 @@
|
|||
part of 'index.dart';
|
||||
|
||||
class DocumentViewScreen extends StatefulScreen {
|
||||
const DocumentViewScreen({
|
||||
super.key,
|
||||
required this.doc,
|
||||
required this.uri,
|
||||
});
|
||||
|
||||
final Document doc;
|
||||
final Uri uri;
|
||||
|
||||
@override
|
||||
ScreenState<DocumentViewScreen> createState() => _DocumentViewScreenState();
|
||||
}
|
||||
|
||||
class _DocumentViewScreenState extends ScreenState<DocumentViewScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
action() {
|
||||
context.read<DocumentPageBloc>().add(UnselectDocumentEvent());
|
||||
}
|
||||
|
||||
final String title = widget.doc.description;
|
||||
final theme = FlutterFlowTheme.of(context);
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, result) => action(),
|
||||
child: Scaffold(
|
||||
backgroundColor: theme.primaryBackground,
|
||||
appBar: buildAppBar(title, context, action),
|
||||
body: buildBody(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildBody(BuildContext context) {
|
||||
// final PDFViewerKey _viewerKey = PDFViewerKey();
|
||||
|
||||
return ReadView(
|
||||
// search: _viewerKey,
|
||||
title: widget.doc.description,
|
||||
url: widget.uri.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,745 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:hub/features/backend/index.dart';
|
||||
import 'package:hub/flutter_flow/index.dart';
|
||||
import 'package:hub/shared/extensions/index.dart';
|
||||
import 'package:hub/shared/utils/index.dart';
|
||||
import 'package:hub/shared/widgets/widgets.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
|
||||
/// -----------------------------------------------
|
||||
/// [TypeDefs]
|
||||
/// -----------------------------------------------
|
||||
|
||||
typedef DocumentKey = GlobalKey<FREDocumentPageState>;
|
||||
|
||||
/// -----------------------------------------------
|
||||
|
||||
/// [Page]
|
||||
/// -----------------------------------------------
|
||||
|
||||
class DocumentPage extends StatefulPage {
|
||||
const DocumentPage({super.key});
|
||||
|
||||
@override
|
||||
State<DocumentPage> createState() => FREDocumentPageState();
|
||||
}
|
||||
|
||||
class FREDocumentPageState<T extends DocumentPage>
|
||||
extends PageState<DocumentPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) => buildBody(context);
|
||||
DocumentPageModel model = DocumentPageModel();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
model.initState(context);
|
||||
}
|
||||
|
||||
Widget buildBody(BuildContext context) {
|
||||
log('Build -> DocumentPage');
|
||||
return BlocProvider<DocumentPageBloc>(
|
||||
create: (context) => DocumentPageBloc(model),
|
||||
child: BlocBuilder<DocumentPageBloc, DocumentPageState>(
|
||||
builder: (context, state) {
|
||||
log('Build -> DocumentPageBloc');
|
||||
print('Bloc -> ${state.isCategorySelected}');
|
||||
|
||||
if (state.isDocumentSelected)
|
||||
return DocumentViewScreen(
|
||||
doc: state.currentDocument!,
|
||||
uri: state.uri!,
|
||||
);
|
||||
else
|
||||
return DocumentManagerScreen(
|
||||
model: model,
|
||||
state: state,
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// -----------------------------------------------
|
||||
|
||||
/// [Model]
|
||||
/// -----------------------------------------------
|
||||
|
||||
class DocumentPageModel extends FlutterFlowModel<DocumentPage> {
|
||||
DocumentPageModel._privateConstructor();
|
||||
|
||||
static final DocumentPageModel _instance =
|
||||
DocumentPageModel._privateConstructor();
|
||||
|
||||
factory DocumentPageModel() {
|
||||
return _instance;
|
||||
}
|
||||
|
||||
late EnhancedListViewKey<Document, Category, Null> vehicleScreenManager;
|
||||
late DocumentKey vehicleScreenViewer;
|
||||
late PagingController<int, Document> _pagingController;
|
||||
|
||||
/// ------------
|
||||
|
||||
@override
|
||||
void initState(BuildContext context) {
|
||||
vehicleScreenManager = EnhancedListViewKey<Document, Category, Null>();
|
||||
vehicleScreenViewer = DocumentKey();
|
||||
|
||||
_pagingController = PagingController<int, Document>(firstPageKey: 1);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pagingController.dispose();
|
||||
vehicleScreenManager.currentState?.dispose();
|
||||
vehicleScreenViewer.currentState?.dispose();
|
||||
}
|
||||
|
||||
/// ------------
|
||||
|
||||
/// [onView]
|
||||
void onView(Document document, BuildContext context) async {
|
||||
vehicleScreenManager.currentContext!
|
||||
.read<DocumentPageBloc>()
|
||||
.add(SelectDocumentEvent(document));
|
||||
}
|
||||
|
||||
/// [itemBodyBuilder]
|
||||
DocumentItem itemBodyBuilder<T extends Document>(
|
||||
BuildContext context, T item, int index) {
|
||||
print('ItemBuilder -> $index');
|
||||
return DocumentItem(
|
||||
document: item,
|
||||
onPressed: onView,
|
||||
);
|
||||
}
|
||||
|
||||
CategoryItem categoryItemBuilder<T>(T? item) {
|
||||
return CategoryItem(category: item! as Category);
|
||||
}
|
||||
|
||||
/// [itemHeaderBuilder]
|
||||
Widget itemHeaderBuilder<T>(Future<List<T?>> Function() gen) =>
|
||||
Builder(builder: (context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(15, 0, 50, 0),
|
||||
child: Text(
|
||||
FFLocalizations.of(context).getVariableText(
|
||||
enText: 'Recent Documents', ptText: 'Últimos Documentos'),
|
||||
style: TextStyle(
|
||||
color: FlutterFlowTheme.of(context).primaryText,
|
||||
fontSize: LimitedFontSizeUtil.getHeaderFontSize(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
EnhancedCarouselView<T>(
|
||||
generateItems: gen,
|
||||
itemBuilder: categoryItemBuilder,
|
||||
filter: filter<T>,
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
/// [generateBodyItems]
|
||||
Future<List<T?>> generateBodyItems<T>(
|
||||
int pageKey, int pageSize, dynamic query) async {
|
||||
log('generateDocuments: $query');
|
||||
|
||||
final List<T?> error = [null];
|
||||
print('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<dynamic> list = newItems.jsonBody['value']['list'];
|
||||
|
||||
late final List<Document> docs = [];
|
||||
|
||||
for (var item in list) {
|
||||
log('-> generateDocuments: $item');
|
||||
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<T?>;
|
||||
}
|
||||
|
||||
/// [generateHeaderItems]
|
||||
Future<List<T?>> generateHeaderItems<T>() async {
|
||||
log('generateCategories: ');
|
||||
final List<T?> 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<dynamic>;
|
||||
late final List<Category> 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);
|
||||
}
|
||||
log('cats: $cats');
|
||||
return cats as List<T?>;
|
||||
}
|
||||
|
||||
/// [filter]
|
||||
void filter<T>(T query, BuildContext context) {
|
||||
context
|
||||
.read<DocumentPageBloc>()
|
||||
.add(FilterCategoryEvent(query as Archive?));
|
||||
}
|
||||
|
||||
/// [onFetchError]
|
||||
void onFetchError(Object e, StackTrace s) {
|
||||
DialogUtil.errorDefault(vehicleScreenViewer.currentContext!);
|
||||
LogUtil.requestAPIFailed(
|
||||
"proccessRequest.php", "", "Consulta de Veículo", e, s);
|
||||
}
|
||||
}
|
||||
|
||||
/// -----------------------------------------------
|
||||
/// [BLoC]
|
||||
/// -----------------------------------------------
|
||||
|
||||
/// [DocumentPageBloc]
|
||||
|
||||
class DocumentPageBloc extends Bloc<DocumentPageEvent, DocumentPageState> {
|
||||
final DocumentPageModel model;
|
||||
static DocumentPageBloc? _singleton;
|
||||
|
||||
factory DocumentPageBloc(DocumentPageModel model) {
|
||||
_singleton ??= DocumentPageBloc._internal(model);
|
||||
return _singleton!;
|
||||
}
|
||||
|
||||
DocumentPageBloc._internal(this.model) : super(DocumentPageState()) {
|
||||
on<SelectDocumentEvent>(_selectDocument);
|
||||
on<UnselectDocumentEvent>(_unselectDocument);
|
||||
on<FilterCategoryEvent>(_filterCategoryEvent);
|
||||
}
|
||||
|
||||
Future<void> _filterCategoryEvent(
|
||||
FilterCategoryEvent event, Emitter<DocumentPageState> emit) async {
|
||||
_selectCategory(event, emit);
|
||||
state.isCategorySelected
|
||||
? _unselectCategory(event, emit)
|
||||
: _selectCategory(event, emit);
|
||||
}
|
||||
|
||||
Future<void> _selectCategory(
|
||||
FilterCategoryEvent event, Emitter<DocumentPageState> emit) async {
|
||||
log('filterItems A: ${event.query}');
|
||||
emit(state.copyWith(
|
||||
isCategorySelected: true,
|
||||
));
|
||||
|
||||
final listViewState = model.vehicleScreenManager.currentState!;
|
||||
listViewState.widget.bodyItems = (await model.generateBodyItems(
|
||||
1, 10, event.query)) as BodyItemsBuilder<Document>;
|
||||
}
|
||||
|
||||
Future<void> _unselectCategory(
|
||||
FilterCategoryEvent event, Emitter<DocumentPageState> emit) async {
|
||||
emit(state.copyWith(
|
||||
isCategorySelected: false,
|
||||
));
|
||||
|
||||
final listViewState = model.vehicleScreenManager.currentState!;
|
||||
listViewState.widget.bodyItems = (await model.generateBodyItems(
|
||||
1, 10, null)) as BodyItemsBuilder<Document>;
|
||||
}
|
||||
|
||||
Future<void> _selectDocument(
|
||||
SelectDocumentEvent event, Emitter<DocumentPageState> emit) async {
|
||||
print('-> select');
|
||||
emit(
|
||||
state.copyWith(
|
||||
uri: await GetPDF().call(
|
||||
event.document.id,
|
||||
),
|
||||
currentDocument: event.document,
|
||||
isDocumentSelected: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _unselectDocument(
|
||||
UnselectDocumentEvent event, Emitter<DocumentPageState> emit) async {
|
||||
emit(
|
||||
state.copyWith(
|
||||
currentDocument: null,
|
||||
isDocumentSelected: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// [DocumentPageEvent]
|
||||
|
||||
abstract class DocumentPageEvent {}
|
||||
|
||||
class SelectDocumentEvent extends DocumentPageEvent {
|
||||
final Document document;
|
||||
SelectDocumentEvent(
|
||||
this.document,
|
||||
);
|
||||
}
|
||||
|
||||
class UnselectDocumentEvent extends DocumentPageEvent {}
|
||||
|
||||
class FilterCategoryEvent extends DocumentPageEvent {
|
||||
final Query query;
|
||||
FilterCategoryEvent(this.query);
|
||||
}
|
||||
|
||||
/// [DocumentPageState]
|
||||
|
||||
class DocumentPageState {
|
||||
final bool isCategorySelected;
|
||||
final bool isDocumentSelected;
|
||||
final Document? currentDocument;
|
||||
final Category? currentCategory;
|
||||
final Uri? uri;
|
||||
final int? count;
|
||||
final dynamic page;
|
||||
final Query? query;
|
||||
|
||||
const DocumentPageState({
|
||||
this.query,
|
||||
this.count,
|
||||
this.page,
|
||||
this.uri,
|
||||
this.currentDocument,
|
||||
this.isCategorySelected = false,
|
||||
this.currentCategory,
|
||||
this.isDocumentSelected = false,
|
||||
});
|
||||
|
||||
DocumentPageState copyWith({
|
||||
Uri? uri,
|
||||
Query? query,
|
||||
int? count,
|
||||
dynamic page,
|
||||
List<Document?>? documents,
|
||||
Document? currentDocument,
|
||||
bool? isDocumentSelected,
|
||||
List<Category?>? categories,
|
||||
Category? currentCategory,
|
||||
bool? isCategorySelected,
|
||||
}) {
|
||||
return DocumentPageState(
|
||||
uri: uri ?? this.uri,
|
||||
query: query ?? this.query,
|
||||
count: count ?? this.count,
|
||||
page: page ?? this.page,
|
||||
//
|
||||
currentDocument: currentDocument ?? this.currentDocument,
|
||||
isDocumentSelected: isDocumentSelected ?? this.isDocumentSelected,
|
||||
//
|
||||
currentCategory: currentCategory ?? this.currentCategory,
|
||||
isCategorySelected: isCategorySelected ?? this.isCategorySelected,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// -----------------------------------------------
|
||||
/// [Screens]
|
||||
/// -----------------------------------------------
|
||||
|
||||
/// [DocumentManagerScreen]
|
||||
|
||||
class DocumentManagerScreen extends StatelessScreen {
|
||||
final DocumentPageModel 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) {
|
||||
log('Build -> DocumentManagerScreen');
|
||||
final SizedBox space = SizedBox(height: 30);
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: EnhancedListView<Document, Category, Null>(
|
||||
key: model.vehicleScreenManager,
|
||||
headerBuilder: model.itemHeaderBuilder<Category>,
|
||||
headerItems: model.generateHeaderItems<Category>,
|
||||
bodyBuilder: model.itemBodyBuilder<Document>,
|
||||
bodyItems: model.generateBodyItems<Document>,
|
||||
footerBuilder: null,
|
||||
footerItems: null,
|
||||
),
|
||||
),
|
||||
] //
|
||||
.addToStart(space)
|
||||
.addToEnd(space),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// [DocumentViewScreen]
|
||||
|
||||
class DocumentViewScreen extends StatefulScreen {
|
||||
const DocumentViewScreen({
|
||||
super.key,
|
||||
required this.doc,
|
||||
required this.uri,
|
||||
});
|
||||
|
||||
final Document doc;
|
||||
final Uri uri;
|
||||
|
||||
@override
|
||||
ScreenState<DocumentViewScreen> createState() => _DocumentViewScreenState();
|
||||
}
|
||||
|
||||
class _DocumentViewScreenState extends ScreenState<DocumentViewScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
action() {
|
||||
context.read<DocumentPageBloc>().add(UnselectDocumentEvent());
|
||||
}
|
||||
|
||||
final String title = widget.doc.description;
|
||||
final theme = FlutterFlowTheme.of(context);
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, result) => action(),
|
||||
child: Scaffold(
|
||||
backgroundColor: theme.primaryBackground,
|
||||
appBar: buildAppBar(title, context, action),
|
||||
body: buildBody(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildBody(BuildContext context) {
|
||||
// final PDFViewerKey _viewerKey = PDFViewerKey();
|
||||
|
||||
return ReadView(
|
||||
// search: _viewerKey,
|
||||
title: widget.doc.description,
|
||||
url: widget.uri.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// -----------------------------------------------
|
||||
/// [Interfaces]
|
||||
/// -----------------------------------------------
|
||||
|
||||
abstract interface class Archive extends Entity {}
|
||||
|
||||
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.fromDesc(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;
|
||||
}
|
||||
|
||||
/// -----------------------------------------------
|
||||
/// [Widgets]
|
||||
/// -----------------------------------------------
|
||||
|
||||
// ignore: must_be_immutable
|
||||
class DocumentItem extends StatelessComponent {
|
||||
final Document document;
|
||||
void Function(Document, BuildContext) onPressed;
|
||||
|
||||
DocumentItem({
|
||||
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;
|
||||
|
||||
return InkWell(
|
||||
onTap: () => onPressed(document, context),
|
||||
enableFeedback: true,
|
||||
overlayColor: WidgetStateProperty.all<Color>(primaryColor),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: SizedBox(
|
||||
height: boxHeight,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// const SizedBox(width: 10),
|
||||
Icon(Icons.description, color: document.category.color),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Tooltip(
|
||||
message: document.description,
|
||||
child: AutoText(
|
||||
document.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(
|
||||
document.category.title,
|
||||
document.category.color,
|
||||
context,
|
||||
constraints,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// const SizedBox(width: 10),
|
||||
Center(
|
||||
child: Icon(
|
||||
Icons.arrow_right,
|
||||
color: primaryText,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
DocumentItem copyWith({
|
||||
Document? document,
|
||||
}) {
|
||||
return DocumentItem(
|
||||
document: document ?? this.document,
|
||||
onPressed: onPressed,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CategoryItem extends StatelessComponent {
|
||||
final Category category;
|
||||
|
||||
const CategoryItem({
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:hub/features/backend/index.dart';
|
||||
import 'package:hub/flutter_flow/index.dart';
|
||||
import 'package:hub/shared/extensions/index.dart';
|
||||
import 'package:hub/shared/mixins/pegeable_mixin.dart';
|
||||
import 'package:hub/shared/utils/index.dart';
|
||||
import 'package:hub/shared/widgets/widgets.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
part 'document_page_widget.dart';
|
||||
part 'document_screen_manager.dart';
|
||||
part 'document_screen_viewer.dart';
|
||||
part 'document_page_model.dart';
|
||||
part 'document_item_component.dart';
|
||||
part 'document_page_bloc.dart';
|
||||
part 'category_item_component.dart';
|
||||
part 'archive_item_component.dart';
|
|
@ -4,7 +4,7 @@ import 'dart:io';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:hub/features/backend/index.dart';
|
||||
import 'package:hub/features/documents/index.dart';
|
||||
import 'package:hub/features/documents/documents.dart';
|
||||
import 'package:hub/features/history/index.dart';
|
||||
import 'package:hub/features/home/index.dart';
|
||||
import 'package:hub/features/local/index.dart';
|
||||
|
|
|
@ -179,117 +179,50 @@ class EnhancedListView<BodyType, HeaderType, FooterType>
|
|||
|
||||
class EnhancedListViewState<ItemType, HeaderType, FooterType>
|
||||
extends State<EnhancedListView<ItemType, HeaderType, FooterType>> {
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
bool _isLoadingMore = false;
|
||||
List<ItemType?> items = [];
|
||||
List<HeaderType?> headerItems = [];
|
||||
List<FooterType?> footerItems = [];
|
||||
int currentPage = 1;
|
||||
Query<ItemType> query;
|
||||
late EnhancedListViewBloc<ItemType, HeaderType, FooterType> bloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_scrollController.addListener(_onScroll);
|
||||
_loadBodyItems();
|
||||
_loadHeaderItems();
|
||||
_loadFooterItems();
|
||||
}
|
||||
|
||||
void _onScroll() {
|
||||
if (_scrollController.position.pixels ==
|
||||
_scrollController.position.maxScrollExtent &&
|
||||
!_isLoadingMore) {
|
||||
_loadMoreBodyItems();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadBodyItems() async {
|
||||
log('loadInitialItems');
|
||||
final newItems = await widget.bodyItems(1, 10, query);
|
||||
setState(() {
|
||||
items = newItems;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _loadMoreBodyItems() async {
|
||||
log('loadMoreItems');
|
||||
setState(() {
|
||||
_isLoadingMore = true;
|
||||
});
|
||||
final newItems = await widget.bodyItems(currentPage + 1, 10, query);
|
||||
setState(() {
|
||||
_isLoadingMore = false;
|
||||
if (newItems.isNotEmpty) {
|
||||
items.addAll(newItems);
|
||||
currentPage++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _loadHeaderItems() async {
|
||||
if (widget.headerItems != null) {
|
||||
log('loadHeaderItems');
|
||||
final newHeaderItems = await widget.headerItems!();
|
||||
setState(() {
|
||||
headerItems = newHeaderItems;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadFooterItems() async {
|
||||
if (widget.footerItems != null) {
|
||||
log('loadFooterItems');
|
||||
final newFooterItems = await widget.footerItems!();
|
||||
setState(() {
|
||||
footerItems = newFooterItems;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> filterBodyItems(Query<ItemType> newQuery) async {
|
||||
log('filterItems B: ${newQuery.toString()}');
|
||||
setState(() {
|
||||
query = newQuery;
|
||||
items = [];
|
||||
currentPage = 1;
|
||||
});
|
||||
await _loadBodyItems();
|
||||
bloc = EnhancedListViewBloc<ItemType, HeaderType, FooterType>(
|
||||
bodyItemsBuilder: widget.bodyItems,
|
||||
headerItemsBuilder: widget.headerItems ?? () async => [],
|
||||
footerItemsBuilder: widget.footerItems ?? () async => [],
|
||||
);
|
||||
bloc.events.loadBodyItems();
|
||||
bloc.events.loadHeaderItems();
|
||||
bloc.events.loadFooterItems();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
log('key: ${widget.key}');
|
||||
return ListView.builder(
|
||||
controller: _scrollController,
|
||||
itemCount: items.length +
|
||||
(headerItems.isNotEmpty ? 1 : 0) +
|
||||
(footerItems.isNotEmpty ? 1 : 0) +
|
||||
(_isLoadingMore ? 1 : 0),
|
||||
itemBuilder: (context, index) {
|
||||
if (headerItems.isNotEmpty && index == 0) {
|
||||
return widget.headerBuilder!(() => Future.value(headerItems));
|
||||
}
|
||||
if (footerItems.isNotEmpty &&
|
||||
index == items.length + (headerItems.isNotEmpty ? 1 : 0)) {
|
||||
return widget.footerBuilder!(() => Future.value(footerItems));
|
||||
}
|
||||
if (_isLoadingMore &&
|
||||
index ==
|
||||
items.length +
|
||||
(headerItems.isNotEmpty ? 1 : 0) +
|
||||
(footerItems.isNotEmpty ? 1 : 0)) {
|
||||
return const EnhancedProgressIndicator();
|
||||
}
|
||||
final item = items[index - (headerItems.isNotEmpty ? 1 : 0)];
|
||||
return widget.bodyBuilder(context, item as ItemType, index);
|
||||
return StreamBuilder<List<ItemType>>(
|
||||
stream: bloc.states.bodyItems.cast<List<ItemType>>(),
|
||||
builder: (context, bodySnapshot) {
|
||||
return StreamBuilder<List<HeaderType>>(
|
||||
stream: bloc.states.headerItems.cast<List<HeaderType>>(),
|
||||
builder: (context, headerSnapshot) {
|
||||
return StreamBuilder<List<FooterType>>(
|
||||
stream: bloc.states.footerItems.cast<List<FooterType>>(),
|
||||
builder: (context, footerSnapshot) {
|
||||
return ListView.builder(
|
||||
itemCount: bodySnapshot.data?.length ?? 0,
|
||||
itemBuilder: (context, index) {
|
||||
return widget.bodyBuilder(
|
||||
context, bodySnapshot.data![index], index);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.dispose();
|
||||
bloc.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
@ -358,7 +291,7 @@ class AuthorizationError implements Exception {
|
|||
AuthorizationError(this.message);
|
||||
}
|
||||
|
||||
/// [State Managment]
|
||||
/// [State Management]
|
||||
Stream<bool> get loadingState => Stream<bool>.empty();
|
||||
Stream<Exception> get errorState => Stream<Exception>.empty();
|
||||
|
||||
|
@ -367,111 +300,108 @@ abstract class EnhancedListViewRepository<T> {
|
|||
int page, int pageSize, PaginatedListViewBodyBuilder<T> builder);
|
||||
}
|
||||
|
||||
abstract class EnhancedListViewBlocStates<T> {
|
||||
abstract class EnhancedListViewEvents<T, H, F> {
|
||||
void loadBodyItems({bool reset = false});
|
||||
void loadHeaderItems();
|
||||
void loadFooterItems();
|
||||
}
|
||||
|
||||
abstract class EnhancedListViewStates<T, H, F> {
|
||||
Stream<List<T>> get bodyItems;
|
||||
Stream<List<H>> get headerItems;
|
||||
Stream<List<F>> get footerItems;
|
||||
Stream<bool> get isLoading;
|
||||
Stream<String> get errors;
|
||||
Stream<EnhancedPaginatedList<T>> get paginatedList;
|
||||
|
||||
@RxBlocIgnoreState()
|
||||
Future<void> get refreshDone;
|
||||
}
|
||||
|
||||
abstract class EnhancedListViewEvents<T> {
|
||||
void loadPage({bool reset = false});
|
||||
}
|
||||
|
||||
abstract class EnhancedListViewBlocType<T> extends RxBlocTypeBase {
|
||||
EnhancedListViewEvents<T> get events;
|
||||
EnhancedListViewBlocStates<T> get states;
|
||||
}
|
||||
|
||||
abstract class $EnhancedListViewBloc<T> extends RxBlocBase
|
||||
implements
|
||||
EnhancedListViewEvents<T>,
|
||||
EnhancedListViewBlocStates<T>,
|
||||
EnhancedListViewBlocType<T> {
|
||||
final _compositeSubscription = CompositeSubscription();
|
||||
|
||||
final _$loadPageEvent = PublishSubject<bool>();
|
||||
|
||||
late final Stream<bool> _isLoadingState = _mapToIsLoadingState();
|
||||
late final Stream<String> _errorsState = _mapToErrorsState();
|
||||
late final Stream<EnhancedPaginatedList<T>> _paginatedListState =
|
||||
_mapToPaginatedListState();
|
||||
|
||||
@override
|
||||
void loadPage({bool reset = false}) => _$loadPageEvent.add(reset);
|
||||
|
||||
@override
|
||||
Stream<bool> get isLoading => _isLoadingState;
|
||||
@override
|
||||
Stream<String> get errors => _errorsState;
|
||||
@override
|
||||
Stream<EnhancedPaginatedList<T>> get paginatedList => _paginatedListState;
|
||||
|
||||
Stream<bool> _mapToIsLoadingState();
|
||||
Stream<String> _mapToErrorsState();
|
||||
Stream<EnhancedPaginatedList<T>> _mapToPaginatedListState();
|
||||
|
||||
@override
|
||||
EnhancedListViewEvents<T> get events => this;
|
||||
@override
|
||||
EnhancedListViewBlocStates<T> get states => this;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_$loadPageEvent.close();
|
||||
_compositeSubscription.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class EnhancedListViewBloc<T> extends $EnhancedListViewBloc<T> {
|
||||
@RxBloc()
|
||||
class EnhancedListViewBloc<T, H, F> extends $EnhancedListViewBloc<T, H, F> {
|
||||
EnhancedListViewBloc({
|
||||
required EnhancedListViewRepository<T> repository,
|
||||
required PaginatedListViewBodyBuilder<T> builder,
|
||||
required T item,
|
||||
int initialPageSize = 50,
|
||||
required this.bodyItemsBuilder,
|
||||
required this.headerItemsBuilder,
|
||||
required this.footerItemsBuilder,
|
||||
}) {
|
||||
_$loadPageEvent
|
||||
_$loadBodyItemsEvent
|
||||
.startWith(true)
|
||||
.fetchData(
|
||||
repository,
|
||||
(context, item, index) => builder(context, item, index),
|
||||
_paginatedList,
|
||||
)
|
||||
.setResultStateHandler(this)
|
||||
.mergeWithPaginatedList(_paginatedList)
|
||||
.bind(_paginatedList)
|
||||
.switchMap((reset) => _fetchBodyItems(reset))
|
||||
.bind(_bodyItems)
|
||||
.addTo(_compositeSubscription);
|
||||
|
||||
_$loadHeaderItemsEvent
|
||||
.switchMap((_) => _fetchHeaderItems())
|
||||
.bind(_headerItems)
|
||||
.addTo(_compositeSubscription);
|
||||
|
||||
_$loadFooterItemsEvent
|
||||
.switchMap((_) => _fetchFooterItems())
|
||||
.bind(_footerItems)
|
||||
.addTo(_compositeSubscription);
|
||||
}
|
||||
|
||||
final _paginatedList = BehaviorSubject<EnhancedPaginatedList<T>>.seeded(
|
||||
EnhancedPaginatedList<T>(
|
||||
items: [],
|
||||
pageSize: 1,
|
||||
currentPage: 1,
|
||||
error: Exception(),
|
||||
isInitialized: true,
|
||||
isLoading: false,
|
||||
totalCount: 0,
|
||||
),
|
||||
);
|
||||
final BodyItemsBuilder<T> bodyItemsBuilder;
|
||||
final HeaderItemsBuilder<H> headerItemsBuilder;
|
||||
final FooterItemsBuilder<F> footerItemsBuilder;
|
||||
|
||||
@override
|
||||
Future<void> get refreshDone async => _paginatedList.value.awaitLoad();
|
||||
final _bodyItems = BehaviorSubject<List<T>>.seeded([]);
|
||||
final _headerItems = BehaviorSubject<List<H>>.seeded([]);
|
||||
final _footerItems = BehaviorSubject<List<F>>.seeded([]);
|
||||
final _isLoading = BehaviorSubject<bool>.seeded(false);
|
||||
final _errors = BehaviorSubject<String>();
|
||||
|
||||
@override
|
||||
Stream<EnhancedPaginatedList<T>> _mapToPaginatedListState() => _paginatedList;
|
||||
@override
|
||||
Stream<String> _mapToErrorsState() =>
|
||||
errorState.map((error) => error.toString());
|
||||
@override
|
||||
Stream<bool> _mapToIsLoadingState() => loadingState;
|
||||
Stream<List<T>> _fetchBodyItems(bool reset) async* {
|
||||
try {
|
||||
_isLoading.add(true);
|
||||
final items = await bodyItemsBuilder(1, 10, null);
|
||||
yield items.whereType<T>().toList();
|
||||
} catch (e) {
|
||||
_errors.add(e.toString());
|
||||
} finally {
|
||||
_isLoading.add(false);
|
||||
}
|
||||
}
|
||||
|
||||
Stream<List<H>> _fetchHeaderItems() async* {
|
||||
try {
|
||||
_isLoading.add(true);
|
||||
final items = await headerItemsBuilder();
|
||||
yield items.whereType<H>().toList();
|
||||
} catch (e) {
|
||||
_errors.add(e.toString());
|
||||
} finally {
|
||||
_isLoading.add(false);
|
||||
}
|
||||
}
|
||||
|
||||
Stream<List<F>> _fetchFooterItems() async* {
|
||||
try {
|
||||
_isLoading.add(true);
|
||||
final items = await footerItemsBuilder();
|
||||
yield items.whereType<F>().toList();
|
||||
} catch (e) {
|
||||
_errors.add(e.toString());
|
||||
} finally {
|
||||
_isLoading.add(false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_paginatedList.close();
|
||||
_bodyItems.close();
|
||||
_headerItems.close();
|
||||
_footerItems.close();
|
||||
_isLoading.close();
|
||||
_errors.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<List<T>> _mapToBodyItemsState() => _bodyItems.stream;
|
||||
@override
|
||||
Stream<String> _mapToErrorsState() => _errors.stream;
|
||||
@override
|
||||
Stream<List<F>> _mapToFooterItemsState() => _footerItems.stream;
|
||||
@override
|
||||
Stream<List<H>> _mapToHeaderItemsState() => _headerItems.stream;
|
||||
@override
|
||||
Stream<bool> _mapToIsLoadingState() => _isLoading.stream;
|
||||
}
|
||||
|
|
|
@ -2,14 +2,10 @@ import 'dart:developer';
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
import 'package:easy_debounce/easy_debounce.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:hub/features/documents/index.dart';
|
||||
import 'package:hub/flutter_flow/index.dart';
|
||||
import 'package:hub/shared/mixins/pegeable_mixin.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:pdfx/pdfx.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
@ -17,6 +13,8 @@ import 'package:rx_bloc_list/rx_bloc_list.dart';
|
|||
import 'package:rxdart/rxdart.dart';
|
||||
import 'package:rx_bloc/rx_bloc.dart';
|
||||
|
||||
part 'widgets.rxb.g.dart';
|
||||
|
||||
/// [Base]
|
||||
part 'page.dart';
|
||||
part 'component.dart';
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
// dart format width=80
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
// **************************************************************************
|
||||
// Generator: RxBlocGeneratorForAnnotation
|
||||
// **************************************************************************
|
||||
|
||||
part of 'widgets.dart';
|
||||
|
||||
/// Used as a contractor for the bloc, events and states classes
|
||||
/// @nodoc
|
||||
abstract class EnhancedListViewBlocType extends RxBlocTypeBase {
|
||||
EnhancedListViewEvents get events;
|
||||
EnhancedListViewStates get states;
|
||||
}
|
||||
|
||||
/// [$EnhancedListViewBloc<T, H, F>] extended by the [EnhancedListViewBloc<T, H, F>]
|
||||
/// @nodoc
|
||||
abstract class $EnhancedListViewBloc<T, H, F> extends RxBlocBase
|
||||
implements
|
||||
EnhancedListViewEvents,
|
||||
EnhancedListViewStates,
|
||||
EnhancedListViewBlocType {
|
||||
final _compositeSubscription = CompositeSubscription();
|
||||
|
||||
/// Тhe [Subject] where events sink to by calling [loadBodyItems]
|
||||
final _$loadBodyItemsEvent = PublishSubject<bool>();
|
||||
|
||||
/// Тhe [Subject] where events sink to by calling [loadHeaderItems]
|
||||
final _$loadHeaderItemsEvent = PublishSubject<void>();
|
||||
|
||||
/// Тhe [Subject] where events sink to by calling [loadFooterItems]
|
||||
final _$loadFooterItemsEvent = PublishSubject<void>();
|
||||
|
||||
/// The state of [bodyItems] implemented in [_mapToBodyItemsState]
|
||||
late final Stream<List<T>> _bodyItemsState = _mapToBodyItemsState();
|
||||
|
||||
/// The state of [headerItems] implemented in [_mapToHeaderItemsState]
|
||||
late final Stream<List<H>> _headerItemsState = _mapToHeaderItemsState();
|
||||
|
||||
/// The state of [footerItems] implemented in [_mapToFooterItemsState]
|
||||
late final Stream<List<F>> _footerItemsState = _mapToFooterItemsState();
|
||||
|
||||
/// The state of [isLoading] implemented in [_mapToIsLoadingState]
|
||||
late final Stream<bool> _isLoadingState = _mapToIsLoadingState();
|
||||
|
||||
/// The state of [errors] implemented in [_mapToErrorsState]
|
||||
late final Stream<String> _errorsState = _mapToErrorsState();
|
||||
|
||||
@override
|
||||
void loadBodyItems({bool reset = false}) => _$loadBodyItemsEvent.add(reset);
|
||||
|
||||
@override
|
||||
void loadHeaderItems() => _$loadHeaderItemsEvent.add(null);
|
||||
|
||||
@override
|
||||
void loadFooterItems() => _$loadFooterItemsEvent.add(null);
|
||||
|
||||
@override
|
||||
Stream<List<T>> get bodyItems => _bodyItemsState;
|
||||
|
||||
@override
|
||||
Stream<List<H>> get headerItems => _headerItemsState;
|
||||
|
||||
@override
|
||||
Stream<List<F>> get footerItems => _footerItemsState;
|
||||
|
||||
@override
|
||||
Stream<bool> get isLoading => _isLoadingState;
|
||||
|
||||
@override
|
||||
Stream<String> get errors => _errorsState;
|
||||
|
||||
Stream<List<T>> _mapToBodyItemsState();
|
||||
|
||||
Stream<List<H>> _mapToHeaderItemsState();
|
||||
|
||||
Stream<List<F>> _mapToFooterItemsState();
|
||||
|
||||
Stream<bool> _mapToIsLoadingState();
|
||||
|
||||
Stream<String> _mapToErrorsState();
|
||||
|
||||
@override
|
||||
EnhancedListViewEvents get events => this;
|
||||
|
||||
@override
|
||||
EnhancedListViewStates get states => this;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_$loadBodyItemsEvent.close();
|
||||
_$loadHeaderItemsEvent.close();
|
||||
_$loadFooterItemsEvent.close();
|
||||
_compositeSubscription.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue