mirrored_repositories/git-touch/lib
import 'package:antd_mobile/antd_mobile.dart';
import 'package:flutter/cupertino.dart';
import 'package:git_touch/utils/utils.dart';
import 'package:git_touch/widgets/error_reload.dart';
import 'package:git_touch/widgets/link.dart';
import 'package:git_touch/widgets/loading.dart';

class LongListPayload<T, K> {
  LongListPayload({
    required this.header,
    required this.totalCount,
    required this.cursor,
    required this.leadingItems,
    this.trailingItems,
  });
  T header;
  int totalCount;
  String? cursor;
  List<K> leadingItems;
  List<K>? trailingItems;
}

// This is a scaffold for issue and pull request
// Since the list could be very long, and some users may only want to to check trailing items
// We should load leading and trailing items at first fetching, and do load more in the middle
// e.g. https://github.com/reactjs/rfcs/pull/68
class LongListStatefulScaffold<T, K> extends StatefulWidget {
  const LongListStatefulScaffold({
    required this.title,
    this.trailingBuilder,
    required this.headerBuilder,
    required this.itemBuilder,
    required this.onRefresh,
    required this.onLoadMore,
  });
  final Widget title;
  final Widget Function(T t)? trailingBuilder;
  final Widget Function(T t) headerBuilder;
  final Widget Function(K k) itemBuilder;
  final Future<LongListPayload<T, K>> Function() onRefresh;
  final Future<LongListPayload<T, K>> Function(String? cursor) onLoadMore;

  @override
  State<LongListStatefulScaffold<T, K>> createState() =>
      _LongListStatefulScaffoldState();
}

class _LongListStatefulScaffoldState<T, K>
    extends State<LongListStatefulScaffold<T, K>> {
  late bool loading;
  bool loadingMore = false;
  String error = '';

  LongListPayload<T, K>? payload;

  @override
  void initState() {
    super.initState();
    _refresh();
  }

  Future<void> _refresh() async {
    // Fimber.d('long list scaffold refresh');
    setState(() {
      error = '';
      loading = true;
    });
    try {
      payload = await widget.onRefresh();
    } catch (err) {
      error = err.toString();
      rethrow;
    } finally {
      if (mounted) {
        setState(() {
          loading = false;
        });
      }
    }
  }

  Future<void> _loadMore() async {
    // Fimber.d('long list scaffold load more');
    setState(() {
      loadingMore = true;
    });
    try {
      final LongListPayload<T?, K> p = await widget.onLoadMore(payload!.cursor);
      payload!.totalCount = p.totalCount;
      payload!.cursor = p.cursor;
      payload!.leadingItems.addAll(p.leadingItems);
    } finally {
      if (mounted) {
        setState(() {
          loadingMore = false;
        });
      }
    }
  }

  Widget _buildItem(BuildContext context, int index) {
    if (index % 2 == 1) {
      return CommonStyle.border;
    }

    final realIndex = index ~/ 2;

    if (realIndex < payload!.leadingItems.length) {
      return widget.itemBuilder(payload!.leadingItems[realIndex]);
    } else if (realIndex == payload!.leadingItems.length) {
      final count = payload!.totalCount -
          payload!.leadingItems.length +
          payload!.trailingItems!.length;
      return Container(
        padding: CommonStyle.padding,
        child: Center(
          child: LinkWidget(
            onTap: _loadMore,
            child: Container(
              padding: CommonStyle.padding,
              decoration: BoxDecoration(
                border: Border.all(color: AntTheme.of(context).colorText),
              ),
              child: Column(
                children: <Widget>[
                  Text('$count hidden items',
                      style: TextStyle(
                          color: AntTheme.of(context).colorText, fontSize: 15)),
                  const Padding(padding: EdgeInsets.only(top: 4)),
                  loadingMore
                      ? const CupertinoActivityIndicator()
                      : Text(
                          'Load more...',
                          style: TextStyle(
                              color: AntTheme.of(context).colorPrimary,
                              fontSize: 16),
                        ),
                ],
              ),
            ),
          ),
        ),
      );
    } else {
      return widget.itemBuilder(payload!
          .trailingItems![realIndex - payload!.leadingItems.length - 1]);
    }
  }

  int get _itemCount {
    var count = payload!.leadingItems.length + payload!.trailingItems!.length;
    if (payload!.totalCount > count) {
      count++;
    }
    return 2 * count; // including bottom border
  }

  Widget _buildSliver() {
    if (error.isNotEmpty) {
      return SliverToBoxAdapter(
          child: ErrorReload(text: error, onTap: _refresh));
    } else if (loading) {
      // TODO:
      return const SliverToBoxAdapter(child: Loading(more: false));
    } else {
      return SliverList(
        delegate:
            SliverChildBuilderDelegate(_buildItem, childCount: _itemCount),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    final slivers = <Widget>[
      CupertinoSliverRefreshControl(onRefresh: _refresh)
    ];
    if (payload != null) {
      slivers.add(
        SliverToBoxAdapter(child: widget.headerBuilder(payload!.header)),
      );
    }
    slivers.add(_buildSliver());

    return CupertinoPageScaffold(
      navigationBar: CupertinoNavigationBar(
        middle: widget.title,
        trailing:
            payload == null ? null : widget.trailingBuilder!(payload!.header),
      ),
      child: SafeArea(
        child: CupertinoScrollbar(
          child: CustomScrollView(slivers: slivers),
        ),
      ),
    );
  }
}
