<|start_filename|>lib/widget/no_more_view.dart<|end_filename|>
import 'package:flutter/material.dart';

class NoMoreView extends StatelessWidget {
  final Color textColor;

  NoMoreView({this.textColor});

  @override
  Widget build(BuildContext context) {
    return new Center(
      child: new Padding(
        padding: const EdgeInsets.symmetric(vertical: 4.0),
        child: new Text(
          '- 真的没有了 -',
          style: new TextStyle(
            color: textColor ?? Colors.black54,
            fontSize: 14.0,
            decoration: TextDecoration.none,
            fontWeight: FontWeight.w200,
          ),
        ),
      ),
    );
  }
}


<|start_filename|>lib/module/personal_page.dart<|end_filename|>
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:pxnyan/model/illustration.dart';
import 'package:pxnyan/model/member_illust.dart';
import 'package:pxnyan/model/user.dart';
import 'package:flutter_advanced_networkimage/flutter_advanced_networkimage.dart';
import 'package:pxnyan/http/api.dart' as Api;
import 'package:pxnyan/module/illust_details_page.dart';
import 'package:pxnyan/utils/utils.dart' as Utils;
import 'package:pxnyan/widget/empty_view.dart';
import 'package:pxnyan/widget/error_view.dart';
import 'package:pxnyan/widget/loading_view.dart';
import 'package:pxnyan/widget/no_more_view.dart';

class PersonalPage extends StatelessWidget {
  final User _user;

  PersonalPage(this._user);

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      body: new NestedScrollView(
        headerSliverBuilder: (context, innerBoxIsScrolled) {
          return <Widget>[
            new SliverAppBar(
              pinned: true,
              forceElevated: innerBoxIsScrolled,
              flexibleSpace: new FlexibleSpaceBar(
                title: new Text(_user.name),
                background: new Container(
                  child: new Image(
                    fit: BoxFit.cover,
                    image: new AdvancedNetworkImage(
                      _user.profileImageUrls.px170x170,
                      header: Api.HEADER,
                    ),
                  ),
                  foregroundDecoration: new BoxDecoration(
                    gradient: new LinearGradient(
                      begin: Alignment.bottomCenter,
                      end: Alignment.topCenter,
                      colors: <Color>[Colors.black26, Colors.transparent],
                    ),
                  ),
                ),
              ),
              expandedHeight: 192.0,
            ),
          ];
        },
        body: new PersonalContent(_user.id),
      ),
    );
  }
}

class PersonalContent extends StatefulWidget {
  final int _id;

  PersonalContent(this._id);

  @override
  State<StatefulWidget> createState() => new PersonalContentState();
}

class PersonalContentState extends State<PersonalContent> {
  final List<Illustration> _data = <Illustration>[];
  int _illustCount = -1;

  int _page;
  bool _isLoading = false;
  bool _isNoMore = false;
  bool _isError = false;

  Future loadData(bool clear) async {
    if (!_isNoMore && !_isLoading) {
      setState(() => _isLoading = true);
      if (clear) _page = 1;
      Future<MemberIllust> rankFuture = Api.fetchMemberIllust(widget._id, _page);
      return rankFuture.then(
        (MemberIllust result) {
          if (result.status == Api.SUCCESS) {
            if (_illustCount == -1) {
              _illustCount = result.pagination.total;
            }
            if (result.response.isNotEmpty) {
              if (clear) {
                _data.clear();
              }
              _data.addAll(result.response);
            }
            if (result.pagination.next == null) {
              _isNoMore = true;
            } else {
              _page = result.pagination.next;
            }
          }
        },
        onError: (error) {
          _isError = true;
          Scaffold.of(context).showSnackBar(new SnackBar(content: new Text('没届到')));
        },
      ).whenComplete(() => setState(() => _isLoading = false));
    }
  }

  @override
  Widget build(BuildContext context) {
    if (_data.isEmpty) {
      if (_isNoMore) {
        return new EmptyView();
      } else if (_isError) {
        return new ErrorView(_handleErrorClick);
      } else {
        loadData(true);
        return new LoadingView(_isLoading);
      }
    } else {
      return _buildIllustList();
    }
  }

  Widget _buildIllustList() {
    return new RefreshIndicator(
      child: new NotificationListener<ScrollEndNotification>(
        onNotification: _handleScrollNotification,
        child: new ListView.builder(
          padding: const EdgeInsets.only(top: 5.0),
          itemCount: _data.length + 2,
          itemBuilder: (context, index) {
            if (index == 0) {
              return new Padding(
                padding: const EdgeInsets.symmetric(horizontal: 4.0),
                child: new Text(
                  '作品（$_illustCount）',
                  style: const TextStyle(
                    decoration: TextDecoration.none,
                    fontWeight: FontWeight.w200,
                    fontSize: 15.0,
                    color: Colors.black54,
                  ),
                ),
              );
            } else if (index == _data.length + 1) {
              return _isNoMore
                  ? new NoMoreView()
                  : new Padding(
                      padding: const EdgeInsets.symmetric(vertical: 4.0),
                      child: new LoadingView(_isLoading),
                    );
            } else {
              return _buildIllustItem(_data[index - 1]);
            }
          },
        ),
      ),
      onRefresh: _handleRefresh,
    );
  }

  Widget _buildIllustItem(Illustration illust) {
    return new GestureDetector(
      onTap: () {
        Navigator.push(
          context,
          new MaterialPageRoute(
            builder: (context) =>
            new IllustDetailsPage(illust.id, illust.title, illust.imageUrls.px480mw),
          ),
        );
      },
      child: new Card(
        elevation: 3.0,
        child: new SizedBox(
          height: 100.0,
          child: new Row(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              new SizedBox(
                width: 120.0,
                child: new Image(
                  fit: BoxFit.cover,
                  image: new AdvancedNetworkImage(
                    illust.imageUrls.px128x128,
                    header: Api.HEADER,
                  ),
                ),
              ),
              new Expanded(
                child: new Padding(
                  padding: const EdgeInsets.symmetric(
                    vertical: 4.0,
                    horizontal: 8.0,
                  ),
                  child: new Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: <Widget>[
                      new Text(
                        illust.title,
                        maxLines: 2,
                        overflow: TextOverflow.ellipsis,
                        style: const TextStyle(fontSize: 16.0),
                      ),
                      new Expanded(
                        child: new Row(
                          crossAxisAlignment: CrossAxisAlignment.end,
                          mainAxisAlignment: MainAxisAlignment.end,
                          children: <Widget>[
                            new Icon(Icons.remove_red_eye, color: Colors.blue, size: 20.0),
                            new Container(
                              margin: const EdgeInsets.only(left: 3.0),
                              child: new Text(
                                '${Utils.getFormatNumber(illust.stats.viewsCount)}',
                                style: const TextStyle(fontSize: 14.0),
                              ),
                            ),
                            new Container(
                              margin: const EdgeInsets.only(left: 8.0),
                              child: new Icon(Icons.favorite, color: Colors.blue, size: 20.0),
                            ),
                            new Container(
                              margin: const EdgeInsets.only(left: 3.0),
                              child: new Text(
                                '${Utils.getFormatNumber(illust.stats.favoritedCount.public +
                                    illust.stats.favoritedCount.private)}',
                                style: const TextStyle(fontSize: 14.0),
                              ),
                            ),
                          ],
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

  Future<Null> _handleRefresh() async {
    await loadData(true);
    return null;
  }

  bool _handleScrollNotification(ScrollEndNotification notification) {
    if (notification.metrics.extentAfter == 0.0) {
      loadData(false);
    }
    return false;
  }

  void _handleErrorClick() {
    _isError = false;
    loadData(true);
  }
}


<|start_filename|>lib/module/home_page.dart<|end_filename|>
import 'package:flutter/material.dart';
import 'package:pxnyan/module/rank_page.dart';

class HomePage extends StatefulWidget {
  @override
  State createState() => new _HomePageState();
}

class _HomePageState extends State<HomePage> {
  final List<_Mode> _modes = const <_Mode>[
    const _Mode('daily', '日榜'),
    const _Mode('weekly', '周榜'),
    const _Mode('monthly', '月榜'),
    const _Mode('rookie', '新人'),
    const _Mode('original', '原创'),
    const _Mode('male', '男性向'),
    const _Mode('female', '女性向'),
  ];

  @override
  Widget build(BuildContext context) {
    return new DefaultTabController(
      length: _modes.length,
      child: new Scaffold(
        body: new NestedScrollView(
          headerSliverBuilder: (context, innerBoxIsScrolled) {
            return <Widget>[
              new SliverAppBar(
                floating: true,
                pinned: true,
                forceElevated: innerBoxIsScrolled,
                title: new Text('PxNyan'),
                bottom: new TabBar(
                  isScrollable: true,
                  tabs: _buildTabs(),
                ),
              ),
            ];
          },
          body: new TabBarView(
            children: _buildRankPages(),
          ),
        ),
      ),
    );
  }

  List<Tab> _buildTabs() {
    final tabs = <Tab>[];
    _modes.forEach((mode) => tabs.add(new Tab(text: mode.cn)));
    return tabs;
  }

  List<Widget> _buildRankPages() {
    final pages = <Widget>[];
    _modes.forEach((mode) => pages.add(new RankPage(mode.en)));
    return pages;
  }
}

class _Mode extends Object {
  final String en;
  final String cn;

  const _Mode(this.en, this.cn);
}


<|start_filename|>lib/model/image_urls.dart<|end_filename|>
import 'package:json_annotation/json_annotation.dart';

part 'image_urls.g.dart';

@JsonSerializable()
class ImageUrls extends Object with _$ImageUrlsSerializerMixin {
  @JsonKey(name: 'px_128x128') String px128x128;
  @JsonKey(name: 'px_480mw') String px480mw;
  String large;

  ImageUrls(this.px128x128, this.px480mw, this.large);

  factory ImageUrls.fromJson(Map<String, dynamic> json) => _$ImageUrlsFromJson(json);
}


<|start_filename|>lib/model/member_illust.dart<|end_filename|>
import 'package:json_annotation/json_annotation.dart';
import 'package:pxnyan/model/illustration.dart';
import 'package:pxnyan/model/pagination.dart';

part 'member_illust.g.dart';

@JsonSerializable()
class MemberIllust extends Object with _$MemberIllustSerializerMixin {
  String status;
  List<Illustration> response;
  int count;
  Pagination pagination;

  MemberIllust(this.status, this.response, this.count, this.pagination);

  factory MemberIllust.fromJson(Map<String, dynamic> json) => _$MemberIllustFromJson(json);
}


<|start_filename|>lib/model/illust_details.g.dart<|end_filename|>
// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'illust_details.dart';

// **************************************************************************
// Generator: JsonSerializableGenerator
// **************************************************************************

IllustDetails _$IllustDetailsFromJson(Map<String, dynamic> json) =>
    new IllustDetails(
        json['status'] as String,
        (json['response'] as List)
            ?.map((e) => e == null
                ? null
                : new Illustration.fromJson(e as Map<String, dynamic>))
            ?.toList(),
        json['count'] as int);

abstract class _$IllustDetailsSerializerMixin {
  String get status;
  List<Illustration> get response;
  int get count;
  Map<String, dynamic> toJson() =>
      <String, dynamic>{'status': status, 'response': response, 'count': count};
}


<|start_filename|>lib/main.dart<|end_filename|>
import 'package:flutter/material.dart';
import 'package:pxnyan/module/home_page.dart';

void main() => runApp(new PxNyanApp());

class PxNyanApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'PxNyan',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new HomePage(),
    );
  }
}


<|start_filename|>lib/module/illust_details_page.dart<|end_filename|>
import 'dart:async';
import 'dart:ui' show ImageFilter;

import 'package:flutter_advanced_networkimage/flutter_advanced_networkimage.dart';
import 'package:flutter/material.dart';
import 'package:pxnyan/http/api.dart' as Api;
import 'package:pxnyan/model/illust_details.dart';
import 'package:pxnyan/model/illustration.dart';
import 'package:pxnyan/module/gallery_page.dart';
import 'package:pxnyan/utils/utils.dart' as Utils;
import 'package:pxnyan/widget/corner_label.dart';
import 'package:pxnyan/widget/error_view.dart';
import 'package:pxnyan/widget/loading_view.dart';

class IllustDetailsPage extends StatefulWidget {
  final int _id;
  final String _illustTitle;
  final String _bgImgUrl;

  IllustDetailsPage(this._id, this._illustTitle, this._bgImgUrl);

  @override
  State<StatefulWidget> createState() => new _IllustDetailsPageState();
}

class _IllustDetailsPageState extends State<IllustDetailsPage> {
  IllustDetails _illustDetails;
  List<String> _imageUrls = <String>[];

  Future<IllustDetails> getIllustDetails() {
    return _illustDetails == null
        ? Api.fetchIllustDetails(widget._id)
        : new Future.value(_illustDetails);
  }

  @override
  Widget build(BuildContext context) {
    return new Container(
      decoration: new BoxDecoration(
        image: new DecorationImage(
          fit: BoxFit.cover,
          image: new AdvancedNetworkImage(
            widget._bgImgUrl,
            header: Api.HEADER,
          ),
        ),
      ),
      child: new BackdropFilter(
        filter: new ImageFilter.blur(sigmaX: 6.0, sigmaY: 6.0),
        child: new Scaffold(
          appBar: new AppBar(
            title: new Text(widget._illustTitle),
            backgroundColor: Colors.transparent,
            elevation: 0.0,
          ),
          backgroundColor: Colors.black12,
          body: new FutureBuilder<IllustDetails>(
            future: getIllustDetails(),
            builder: (context, snapshot) {
              switch (snapshot.connectionState) {
                case ConnectionState.waiting:
                  return new LoadingView(true);
                default:
                  if (snapshot.hasData) {
                    if (snapshot.data.status == Api.SUCCESS) {
                      if (_illustDetails == null) {
                        _illustDetails = snapshot.data;
                      }
                      Illustration illustration = snapshot.data.response[0];
                      _imageUrls.clear();
                      if (illustration.isManga) {
                        _imageUrls
                            .addAll(illustration.metadata.pages.map((page) => page.imageUrls.large));
                      } else {
                        _imageUrls.add(illustration.imageUrls.large);
                      }
                      return _buildDetails(illustration);
                    } else {
                      return new ErrorView(_handleErrorClick, textColor: Colors.white);
                    }
                  } else if (snapshot.hasError) {
                    return new ErrorView(_handleErrorClick, textColor: Colors.white);
                  }
              }
            },
          ),
        ),
      ),
    );
  }

  Widget _buildDetails(Illustration illustration) {
    return new ListView(
      padding: const EdgeInsets.symmetric(horizontal: 16.0),
      children: <Widget>[
        new GestureDetector(
          onTap: _handleImageClick,
          child: new Card(
            elevation: 3.0,
            child: new Stack(
              children: <Widget>[
                new Image(
                  image: new AdvancedNetworkImage(
                    illustration.imageUrls.px480mw,
                    header: Api.HEADER,
                  ),
                ),
                new Align(
                  alignment: AlignmentDirectional.topEnd,
                  child: new CornerLabel('${illustration.pageCount}'),
                ),
              ],
            ),
          ),
        ),
        new Card(
            margin: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 4.0),
            elevation: 3.0,
            child: new Padding(
              padding: const EdgeInsets.all(8.0),
              child: new Column(
                children: <Widget>[
                  new Row(
                    children: <Widget>[
                      new Icon(Icons.person, color: Colors.blue, size: 20.0),
                      new Expanded(
                        child: new Container(
                          margin: const EdgeInsets.only(left: 3.0),
                          child: new Text(
                            illustration.user.name,
                            maxLines: 1,
                            overflow: TextOverflow.ellipsis,
                            style: const TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold),
                          ),
                        ),
                      ),
                      new Icon(Icons.remove_red_eye, color: Colors.blue, size: 20.0),
                      new Container(
                        margin: const EdgeInsets.only(left: 3.0),
                        child: new Text(
                          '${Utils.getFormatNumber(illustration.stats.viewsCount)}',
                          style: const TextStyle(fontSize: 14.0),
                        ),
                      ),
                      new Container(
                        margin: const EdgeInsets.only(left: 8.0),
                        child: new Icon(Icons.favorite, color: Colors.blue, size: 20.0),
                      ),
                      new Container(
                        margin: const EdgeInsets.only(left: 3.0),
                        child: new Text(
                          '${Utils.getFormatNumber(illustration.stats.favoritedCount.public +
                              illustration.stats.favoritedCount.private)}',
                          style: const TextStyle(fontSize: 14.0),
                        ),
                      ),
                    ],
                  ),
                  new Container(
                    alignment: AlignmentDirectional.topStart,
                    margin: const EdgeInsets.only(top: 8.0),
                    child: new Wrap(
                      spacing: 5.0,
                      runSpacing: 5.0,
                      children: _buildTags(illustration.tags),
                    ),
                  ),
                  new Container(
                    alignment: AlignmentDirectional.topStart,
                    margin: const EdgeInsets.only(top: 8.0),
                    child: new Text(illustration.caption ?? ''),
                  ),
                ],
              ),
            )),
      ],
    );
  }

  List<Widget> _buildTags(List<String> tags) {
    return tags
        .map((tag) => new Chip(
              labelStyle: const TextStyle(color: Colors.white, fontSize: 12.0),
              backgroundColor: Colors.blue,
              label: new Text(tag),
            ))
        .toList();
  }

  void _handleErrorClick() => setState(() {});

  void _handleImageClick() {
    Navigator.push(
      context,
      new MaterialPageRoute(
        builder: (context) => new GalleryPage(_imageUrls),
      ),
    );
  }
}


<|start_filename|>lib/widget/error_view.dart<|end_filename|>
import 'package:flutter/material.dart';

class ErrorView extends StatelessWidget {
  final Color textColor;
  final GestureTapCallback onTap;

  ErrorView(this.onTap, {this.textColor});

  @override
  Widget build(BuildContext context) {
    return new GestureDetector(
      onTap: onTap,
      child: new Center(
        child: new Text(
          '出错了，请点击重试',
          style: new TextStyle(
            color: textColor ?? Colors.black54,
            fontSize: 14.0,
            decoration: TextDecoration.none,
            fontWeight: FontWeight.w200,
          ),
        ),
      ),
    );
  }
}


<|start_filename|>lib/model/rank.g.dart<|end_filename|>
// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'rank.dart';

// **************************************************************************
// Generator: JsonSerializableGenerator
// **************************************************************************

Rank _$RankFromJson(Map<String, dynamic> json) => new Rank(
    json['status'] as String,
    (json['response'] as List)
        ?.map((e) =>
            e == null ? null : new Response.fromJson(e as Map<String, dynamic>))
        ?.toList(),
    json['count'] as int,
    json['pagination'] == null
        ? null
        : new Pagination.fromJson(json['pagination'] as Map<String, dynamic>));

abstract class _$RankSerializerMixin {
  String get status;
  List<Response> get response;
  int get count;
  Pagination get pagination;
  Map<String, dynamic> toJson() => <String, dynamic>{
        'status': status,
        'response': response,
        'count': count,
        'pagination': pagination
      };
}

Response _$ResponseFromJson(Map<String, dynamic> json) => new Response(
    json['content'] as String,
    json['mode'] as String,
    json['date'] as String,
    (json['works'] as List)
        ?.map((e) =>
            e == null ? null : new Work.fromJson(e as Map<String, dynamic>))
        ?.toList());

abstract class _$ResponseSerializerMixin {
  String get content;
  String get mode;
  String get date;
  List<Work> get works;
  Map<String, dynamic> toJson() => <String, dynamic>{
        'content': content,
        'mode': mode,
        'date': date,
        'works': works
      };
}

Work _$WorkFromJson(Map<String, dynamic> json) => new Work(
    json['rank'] as int,
    json['work'] == null
        ? null
        : new WorkBean.fromJson(json['work'] as Map<String, dynamic>),
    json['previous_rank'] as int);

abstract class _$WorkSerializerMixin {
  int get rank;
  WorkBean get work;
  int get previousRank;
  Map<String, dynamic> toJson() => <String, dynamic>{
        'rank': rank,
        'work': work,
        'previous_rank': previousRank
      };
}

WorkBean _$WorkBeanFromJson(Map<String, dynamic> json) => new WorkBean(
    json['id'] as int,
    json['title'] as String,
    (json['tags'] as List)?.map((e) => e as String)?.toList(),
    json['width'] as int,
    json['height'] as int,
    json['user'] == null
        ? null
        : new User.fromJson(json['user'] as Map<String, dynamic>),
    json['created_time'] as String,
    json['image_urls'] == null
        ? null
        : new ImageUrls.fromJson(json['image_urls'] as Map<String, dynamic>));

abstract class _$WorkBeanSerializerMixin {
  int get id;
  String get title;
  List<String> get tags;
  int get width;
  int get height;
  User get user;
  String get createdTime;
  ImageUrls get imageUrls;
  Map<String, dynamic> toJson() => <String, dynamic>{
        'id': id,
        'title': title,
        'tags': tags,
        'width': width,
        'height': height,
        'user': user,
        'created_time': createdTime,
        'image_urls': imageUrls
      };
}


<|start_filename|>lib/model/member_illust.g.dart<|end_filename|>
// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'member_illust.dart';

// **************************************************************************
// Generator: JsonSerializableGenerator
// **************************************************************************

MemberIllust
    _$MemberIllustFromJson(Map<String, dynamic> json) =>
        new MemberIllust(
            json['status'] as String,
            (json['response'] as List)
                ?.map((e) => e == null
                    ? null
                    : new Illustration.fromJson(e as Map<String, dynamic>))
                ?.toList(),
            json['count'] as int,
            json['pagination'] == null
                ? null
                : new Pagination.fromJson(
                    json['pagination'] as Map<String, dynamic>));

abstract class _$MemberIllustSerializerMixin {
  String get status;
  List<Illustration> get response;
  int get count;
  Pagination get pagination;
  Map<String, dynamic> toJson() => <String, dynamic>{
        'status': status,
        'response': response,
        'count': count,
        'pagination': pagination
      };
}


<|start_filename|>lib/model/pagination.g.dart<|end_filename|>
// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'pagination.dart';

// **************************************************************************
// Generator: JsonSerializableGenerator
// **************************************************************************

Pagination _$PaginationFromJson(Map<String, dynamic> json) => new Pagination(
    json['previous'] as int,
    json['next'] as int,
    json['current'] as int,
    json['per_page'] as int,
    json['total'] as int,
    json['pages'] as int);

abstract class _$PaginationSerializerMixin {
  int get previous;
  int get next;
  int get current;
  int get total;
  int get pages;
  int get perPage;
  Map<String, dynamic> toJson() => <String, dynamic>{
        'previous': previous,
        'next': next,
        'current': current,
        'total': total,
        'pages': pages,
        'per_page': perPage
      };
}


<|start_filename|>lib/model/image_urls.g.dart<|end_filename|>
// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'image_urls.dart';

// **************************************************************************
// Generator: JsonSerializableGenerator
// **************************************************************************

ImageUrls _$ImageUrlsFromJson(Map<String, dynamic> json) => new ImageUrls(
    json['px_128x128'] as String,
    json['px_480mw'] as String,
    json['large'] as String);

abstract class _$ImageUrlsSerializerMixin {
  String get px128x128;
  String get px480mw;
  String get large;
  Map<String, dynamic> toJson() => <String, dynamic>{
        'px_128x128': px128x128,
        'px_480mw': px480mw,
        'large': large
      };
}


<|start_filename|>lib/model/illustration.dart<|end_filename|>
import 'package:json_annotation/json_annotation.dart';
import 'package:pxnyan/model/image_urls.dart';
import 'package:pxnyan/model/user.dart';

part 'illustration.g.dart';


@JsonSerializable()
class Illustration extends Object with _$IllustrationSerializerMixin {
  int id;
  String title;
  String caption;
  List<String> tags;
  @JsonKey(name: 'image_urls') ImageUrls imageUrls;
  int width;
  int height;
  Stats stats;
  @JsonKey(name: 'age_limit') String ageLimit;
  @JsonKey(name: 'created_time') String createdTime;
  User user;
  @JsonKey(name: 'is_manga') bool isManga;
  @JsonKey(name: 'page_count') int pageCount;
  Metadata metadata;

  Illustration(this.id, this.title, this.caption, this.tags, this.imageUrls, this.width,
      this.height, this.stats, this.ageLimit, this.createdTime, this.user, this.isManga,
      this.pageCount, this.metadata);

  factory Illustration.fromJson(Map<String, dynamic> json) => _$IllustrationFromJson(json);
}

@JsonSerializable()
class Stats extends Object with _$StatsSerializerMixin {
  @JsonKey(name: 'scored_count') int scoredCount;
  int score;
  @JsonKey(name: 'views_count') int viewsCount;
  @JsonKey(name: 'favorited_count') FavoritedCount favoritedCount;
  @JsonKey(name: 'commented_count') int commentedCount;

  Stats(this.scoredCount, this.score, this.viewsCount, this.favoritedCount, this.commentedCount);

  factory Stats.fromJson(Map<String, dynamic> json) => _$StatsFromJson(json);
}

@JsonSerializable()
class FavoritedCount extends Object with _$FavoritedCountSerializerMixin {
  int public;
  int private;

  FavoritedCount(this.public, this.private);

  factory FavoritedCount.fromJson(Map<String, dynamic> json) => _$FavoritedCountFromJson(json);
}

@JsonSerializable()
class Metadata extends Object with _$MetadataSerializerMixin {
  List<Page> pages;

  Metadata(this.pages);

  factory Metadata.fromJson(Map<String, dynamic> json) => _$MetadataFromJson(json);
}

@JsonSerializable()
class Page extends Object with _$PageSerializerMixin {
  @JsonKey(name: 'image_urls') ImageUrls imageUrls;

  Page(this.imageUrls);

  factory Page.fromJson(Map<String, dynamic> json) => _$PageFromJson(json);
}

<|start_filename|>lib/widget/loading_view.dart<|end_filename|>
import 'package:flutter/material.dart';

class LoadingView extends StatelessWidget {
  final bool show;

  LoadingView(this.show);

  @override
  Widget build(BuildContext context) {
    return new Opacity(
      opacity: show ? 1.0 : 0.0,
      child: new Center(
        child: new Container(
          width: 25.0,
          height: 25.0,
          child: const CircularProgressIndicator(
            strokeWidth: 2.0,
          ),
        ),
      ),
    );
  }
}

<|start_filename|>lib/model/user.dart<|end_filename|>
import 'package:json_annotation/json_annotation.dart';

part 'user.g.dart';

@JsonSerializable()
class User extends Object with _$UserSerializerMixin {
  int id;
  String account;
  String name;
  @JsonKey(name: 'profile_image_urls') Avatar profileImageUrls;

  User(this.id, this.account, this.name, this.profileImageUrls);

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}

@JsonSerializable()
class Avatar extends Object with _$AvatarSerializerMixin {
  @JsonKey(name: 'px_170x170') String px170x170;
  @JsonKey(name: 'px_50x50') String px50x50;

  Avatar(this.px170x170, this.px50x50);

  factory Avatar.fromJson(Map<String, dynamic> json) => _$AvatarFromJson(json);
}


<|start_filename|>lib/http/api.dart<|end_filename|>
import 'dart:async';
import 'dart:convert';
import 'package:pxnyan/model/illust_details.dart';
import 'package:pxnyan/model/member_illust.dart';
import 'package:pxnyan/model/rank.dart';
import 'package:http/http.dart' as http;

const String _URL_BASE = 'https://api.imjad.cn/pixiv/v1/';
const int _PAGE_SIZE = 20;
const Duration _TIMEOUT = const Duration(seconds: 20);
const Map<String, String> HEADER = {'Referer': 'https://www.pixiv.net/'};

Future<Rank> fetchRank(String mode, String date, int page) async {
  final response = await http
      .get('$_URL_BASE?type=rank&per_page=$_PAGE_SIZE&mode=$mode&date=$date&page=$page')
      .timeout(_TIMEOUT);
  final responseJson = json.decode(response.body);
  return new Rank.fromJson(responseJson);
}

Future<IllustDetails> fetchIllustDetails(int id) async {
  final response = await http.get('$_URL_BASE?type=illust&id=$id').timeout(_TIMEOUT);
  final responseJson = json.decode(response.body);
  return new IllustDetails.fromJson(responseJson);
}

Future<MemberIllust> fetchMemberIllust(int id, int page) async {
  print('$_URL_BASE?type=member_illust&per_page=$_PAGE_SIZE&page=$page&id=$id');
  final response = await http
      .get('$_URL_BASE?type=member_illust&per_page=$_PAGE_SIZE&page=$page&id=$id')
      .timeout(_TIMEOUT);
  final responseJson = json.decode(response.body);
  return new MemberIllust.fromJson(responseJson);
}

const String SUCCESS = 'success';


<|start_filename|>lib/model/rank.dart<|end_filename|>
import 'package:json_annotation/json_annotation.dart';
import 'package:pxnyan/model/image_urls.dart';
import 'package:pxnyan/model/pagination.dart';
import 'package:pxnyan/model/user.dart';

part 'rank.g.dart';

@JsonSerializable()
class Rank extends Object with _$RankSerializerMixin {
  String status;
  List<Response> response;
  int count;
  Pagination pagination;

  Rank(this.status, this.response, this.count, this.pagination);

  factory Rank.fromJson(Map<String, dynamic> json) => _$RankFromJson(json);
}

@JsonSerializable()
class Response extends Object with _$ResponseSerializerMixin {
  String content;
  String mode;
  String date;
  List<Work> works;

  Response(this.content, this.mode, this.date, this.works);

  factory Response.fromJson(Map<String, dynamic> json) => _$ResponseFromJson(json);
}

@JsonSerializable()
class Work extends Object with _$WorkSerializerMixin {
  int rank;
  WorkBean work;
  @JsonKey(name: 'previous_rank') int previousRank;

  Work(this.rank, this.work, this.previousRank);

  factory Work.fromJson(Map<String, dynamic> json) => _$WorkFromJson(json);
}

@JsonSerializable()
class WorkBean extends Object with _$WorkBeanSerializerMixin {
  int id;
  String title;
  List<String> tags;
  int width;
  int height;
  User user;
  @JsonKey(name: 'created_time') String createdTime;
  @JsonKey(name: 'image_urls') ImageUrls imageUrls;

  WorkBean(this.id, this.title, this.tags, this.width, this.height, this.user, this.createdTime, this.imageUrls);

  factory WorkBean.fromJson(Map<String, dynamic> json) => _$WorkBeanFromJson(json);
}


<|start_filename|>lib/model/illust_details.dart<|end_filename|>
import 'package:json_annotation/json_annotation.dart';
import 'package:pxnyan/model/image_urls.dart';
import 'package:pxnyan/model/user.dart';
import 'package:pxnyan/model/illustration.dart';

part 'illust_details.g.dart';

@JsonSerializable()
class IllustDetails extends Object with _$IllustDetailsSerializerMixin {
  String status;
  List<Illustration> response;
  int count;

  IllustDetails(this.status, this.response, this.count);

  factory IllustDetails.fromJson(Map<String, dynamic> json) => _$IllustDetailsFromJson(json);
}

<|start_filename|>lib/utils/utils.dart<|end_filename|>
String getYesterdayString() {
  final DateTime now = new DateTime.now();
  final DateTime date = now.subtract(new Duration(days: now.hour < 12 ? 2 : 1));
  return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
}

String getFormatNumber(int number) {
  return number < 1000 ? '$number' : '${(number / 1000.0).toStringAsFixed(1)}K';
}

<|start_filename|>lib/widget/corner_label.dart<|end_filename|>
import 'package:flutter/material.dart';
import 'dart:math' as math;

class CornerLabel extends StatelessWidget {
  final String _text;

  CornerLabel(this._text);

  @override
  Widget build(BuildContext context) {
    return new ClipPath(
      clipper: new _TriangleClip(), // 用 Path 切个三角形
      child: new Container(
        width: 30.0,
        height: 30.0,
        color: Colors.blue,
        child: new Transform.rotate(
          angle: math.pi / 4, // 45°
          child: new Transform.translate(
            offset: new Offset(0.0, -8.0),
            child: new Container(
              alignment: AlignmentDirectional.center,
              width: 30.0,
              height: 30.0,
              child: new Text(
                _text,
                style: const TextStyle(fontSize: 12.0, color: Colors.white),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

class _TriangleClip extends CustomClipper<Path> {
  @override
  Path getClip(Size size) => new Path()
    ..lineTo(size.width, 0.0)
    ..lineTo(size.width, size.height)
    ..close();

  @override
  bool shouldReclip(CustomClipper<Path> oldClipper) => true;
}


<|start_filename|>lib/module/rank_page.dart<|end_filename|>
import 'dart:async';
import 'dart:ui' show ImageFilter;
import 'package:flutter/material.dart';
import 'package:pxnyan/model/rank.dart';
import 'package:pxnyan/http/api.dart' as Api;
import 'package:pxnyan/module/illust_details_page.dart';
import 'package:pxnyan/module/personal_page.dart';
import 'package:pxnyan/utils/utils.dart' as Utils;
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:flutter_advanced_networkimage/flutter_advanced_networkimage.dart';
import 'package:pxnyan/widget/empty_view.dart';
import 'package:pxnyan/widget/error_view.dart';
import 'package:pxnyan/widget/loading_view.dart';
import 'package:pxnyan/widget/no_more_view.dart';

class RankPage extends StatefulWidget {
  final String _mode;

  RankPage(this._mode);

  @override
  State createState() => new _RankPageState();
}

class _RankPageState extends State<RankPage> {
  String _pageIdentifier; // 用于存储 _page 的标识符
  String _rankDataIdentifier; // 用于存储 data 的标识符

  final List<Work> data = <Work>[];

  int _page;
  bool _isLoading = false;
  bool _isNoMore = false;
  bool _isError = false;

  ScrollController _scrollController;

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

    _pageIdentifier = '${widget._mode}_pageIdentifier';
    _rankDataIdentifier = '${widget._mode}_rankDataIdentifier';

    // TabBarView 目前没有像 ViewPager 一样的缓存或预加载功能, 每次子页面都会重新加载,
    // 所以在这里获取一下存储的页面数据, 避免重新获取数据
    _page = PageStorage.of(context).readState(context, identifier: _pageIdentifier) ?? 1;
    data.addAll(
      PageStorage.of(context).readState(context, identifier: _rankDataIdentifier) ?? <Work>[],
    );
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    // 获取到 NestedScrollView 为子 ScrollView 添加的特殊 ScrollController
    // 如果自己为 ScrollView 添加一个新的 ScrollController 会导致
    // NestedScrollView 和 SliverAppBar 带来的自动隐藏 AppBar 失效
    _scrollController = PrimaryScrollController.of(context);
    _scrollController.addListener(_handleScroll);
  }

  @override
  void dispose() {
    _scrollController.removeListener(_handleScroll);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    if (data.isEmpty) {
      if (_isNoMore) {
        return new EmptyView();
      } else if (_isError) {
        return new ErrorView(_handleErrorClick);
      } else {
        loadData(true);
        return new LoadingView(_isLoading);
      }
    } else {
      return _buildRanks();
    }
  }

  Future loadData(bool clear) async {
    if (!_isNoMore && !_isLoading) {
      setState(() => _isLoading = true);
      if (clear) _page = 1;
      Future<Rank> rankFuture = Api.fetchRank(widget._mode, Utils.getYesterdayString(), _page);
      return rankFuture.then(
        (Rank rank) {
          if (rank.status == Api.SUCCESS) {
            if (rank.response[0].works.isNotEmpty) {
              if (clear) {
                data.clear();
              }
              data.addAll(rank.response[0].works);

              // 存储页面数据用于恢复
              PageStorage.of(context).writeState(context, data, identifier: _rankDataIdentifier);
            }
            if (rank.pagination.next == null) {
              _isNoMore = true;
            } else {
              _page = rank.pagination.next;
              PageStorage.of(context).writeState(context, _page, identifier: _pageIdentifier);
            }
          }
        },
        onError: (error) {
          _isError = true;
          Scaffold.of(context).showSnackBar(new SnackBar(content: new Text('没届到')));
        },
      ).whenComplete(() => setState(() => _isLoading = false));
    }
  }

  Future<Null> _handleRefresh() async {
    await loadData(true);
    return null;
  }

  void _handleScroll() {
    if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) {
      loadData(false);
    }
  }

  void _handleErrorClick() {
    _isError = false;
    loadData(true);
  }

  void _scrollToTop() {
    _scrollController.animateTo(
      0.0,
      curve: Curves.easeOut,
      duration: const Duration(milliseconds: 300),
    );
  }

  Widget _buildRanks() {
    return new Stack(
      children: <Widget>[
        new RefreshIndicator(
          child: new StaggeredGridView.countBuilder(
            key: new PageStorageKey<String>(widget._mode),
            // 使用了 NestedScrollView 和 SliverAppBar 后, 子 ScrollView 会被添加一个系统状态栏高度的
            // topPadding, 手动干掉它
            padding: new EdgeInsets.only(top: 0.0),
            crossAxisCount: 2,
            itemCount: data.length + 1,
            staggeredTileBuilder: (int index) {
              if (index == data.length) {
                return new StaggeredTile.count(2, 0.3);
              } else {
                final WorkBean work = data[index].work;
                return new StaggeredTile.count(1, work.height / work.width);
              }
            },
            itemBuilder: (context, index) {
              if (index == data.length) {
                return _isNoMore ? new NoMoreView() : new LoadingView(_isLoading);
              } else {
                return _buildRankItem(data[index].work);
              }
            },
          ),
          onRefresh: _handleRefresh,
        ),
        new Container(
          alignment: AlignmentDirectional.bottomEnd,
          padding: const EdgeInsets.only(
            right: 16.0,
            bottom: 16.0,
          ),
          child: new FloatingActionButton(
            onPressed: _scrollToTop,
            child: new Icon(Icons.arrow_upward),
            backgroundColor: Colors.orangeAccent,
          ),
        )
      ],
    );
  }

  Widget _buildRankItem(WorkBean work) {
    return new GestureDetector(
      onTap: () {
        Navigator.push(
          context,
          new MaterialPageRoute(
            builder: (context) =>
                new IllustDetailsPage(work.id, work.title, work.imageUrls.px480mw),
          ),
        );
      },
      child: new Card(
        elevation: 3.0,
        child: new Container(
          alignment: AlignmentDirectional.bottomCenter,
          decoration: new BoxDecoration(
            image: new DecorationImage(
              fit: BoxFit.cover,
              image: new AdvancedNetworkImage(
                work.imageUrls.px480mw,
                header: Api.HEADER,
              ),
            ),
          ),
          child: new BackdropFilter(
            filter: new ImageFilter.blur(sigmaX: 3.0, sigmaY: 3.0),
            child: new Container(
              padding: const EdgeInsets.symmetric(
                vertical: 4.0,
                horizontal: 8.0,
              ),
              decoration: new BoxDecoration(
                color: Colors.black12,
              ),
              child: new Row(
                children: <Widget>[
                  new Container(
                    width: 30.0,
                    height: 30.0,
                    child: new GestureDetector(
                      onTap: () {
                        Navigator.push(
                          context,
                          new MaterialPageRoute(
                            builder: (context) =>
                            new PersonalPage(work.user),
                          ),
                        );
                      },
                      child: new CircleAvatar(
                        backgroundImage: new AdvancedNetworkImage(
                          work.user.profileImageUrls.px50x50,
                          header: Api.HEADER,
                        ),
                      ),
                    ),
                  ),
                  // 用 Expanded 包裹一下，不然会有一个警告
                  // 具体查看官方文档 https://flutter.io/tutorials/layout/
                  new Expanded(
                    child: new Container(
                      margin: const EdgeInsets.only(left: 8.0),
                      child: new Text(
                        work.user.name,
                        overflow: TextOverflow.ellipsis,
                        maxLines: 1,
                        style: const TextStyle(
                          color: Colors.white,
                        ),
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}


<|start_filename|>lib/model/illustration.g.dart<|end_filename|>
// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'illustration.dart';

// **************************************************************************
// Generator: JsonSerializableGenerator
// **************************************************************************

Illustration _$IllustrationFromJson(Map<String, dynamic> json) =>
    new Illustration(
        json['id'] as int,
        json['title'] as String,
        json['caption'] as String,
        (json['tags'] as List)?.map((e) => e as String)?.toList(),
        json['image_urls'] == null
            ? null
            : new ImageUrls.fromJson(
                json['image_urls'] as Map<String, dynamic>),
        json['width'] as int,
        json['height'] as int,
        json['stats'] == null
            ? null
            : new Stats.fromJson(json['stats'] as Map<String, dynamic>),
        json['age_limit'] as String,
        json['created_time'] as String,
        json['user'] == null
            ? null
            : new User.fromJson(json['user'] as Map<String, dynamic>),
        json['is_manga'] as bool,
        json['page_count'] as int,
        json['metadata'] == null
            ? null
            : new Metadata.fromJson(json['metadata'] as Map<String, dynamic>));

abstract class _$IllustrationSerializerMixin {
  int get id;
  String get title;
  String get caption;
  List<String> get tags;
  ImageUrls get imageUrls;
  int get width;
  int get height;
  Stats get stats;
  String get ageLimit;
  String get createdTime;
  User get user;
  bool get isManga;
  int get pageCount;
  Metadata get metadata;
  Map<String, dynamic> toJson() => <String, dynamic>{
        'id': id,
        'title': title,
        'caption': caption,
        'tags': tags,
        'image_urls': imageUrls,
        'width': width,
        'height': height,
        'stats': stats,
        'age_limit': ageLimit,
        'created_time': createdTime,
        'user': user,
        'is_manga': isManga,
        'page_count': pageCount,
        'metadata': metadata
      };
}

Stats _$StatsFromJson(Map<String, dynamic> json) => new Stats(
    json['scored_count'] as int,
    json['score'] as int,
    json['views_count'] as int,
    json['favorited_count'] == null
        ? null
        : new FavoritedCount.fromJson(
            json['favorited_count'] as Map<String, dynamic>),
    json['commented_count'] as int);

abstract class _$StatsSerializerMixin {
  int get scoredCount;
  int get score;
  int get viewsCount;
  FavoritedCount get favoritedCount;
  int get commentedCount;
  Map<String, dynamic> toJson() => <String, dynamic>{
        'scored_count': scoredCount,
        'score': score,
        'views_count': viewsCount,
        'favorited_count': favoritedCount,
        'commented_count': commentedCount
      };
}

FavoritedCount _$FavoritedCountFromJson(Map<String, dynamic> json) =>
    new FavoritedCount(json['public'] as int, json['private'] as int);

abstract class _$FavoritedCountSerializerMixin {
  int get public;
  int get private;
  Map<String, dynamic> toJson() =>
      <String, dynamic>{'public': public, 'private': private};
}

Metadata _$MetadataFromJson(Map<String, dynamic> json) =>
    new Metadata((json['pages'] as List)
        ?.map((e) =>
            e == null ? null : new Page.fromJson(e as Map<String, dynamic>))
        ?.toList());

abstract class _$MetadataSerializerMixin {
  List<Page> get pages;
  Map<String, dynamic> toJson() => <String, dynamic>{'pages': pages};
}

Page _$PageFromJson(Map<String, dynamic> json) =>
    new Page(json['image_urls'] == null
        ? null
        : new ImageUrls.fromJson(json['image_urls'] as Map<String, dynamic>));

abstract class _$PageSerializerMixin {
  ImageUrls get imageUrls;
  Map<String, dynamic> toJson() => <String, dynamic>{'image_urls': imageUrls};
}


<|start_filename|>lib/model/pagination.dart<|end_filename|>
import 'package:json_annotation/json_annotation.dart';

part 'pagination.g.dart';

@JsonSerializable()
class Pagination extends Object with _$PaginationSerializerMixin {
  int previous;
  int next;
  int current;
  int total;
  int pages;
  @JsonKey(name: 'per_page') int perPage;

  Pagination(this.previous, this.next, this.current, this.perPage, this.total, this.pages);

  factory Pagination.fromJson(Map<String, dynamic> json) => _$PaginationFromJson(json);
}

<|start_filename|>lib/model/user.g.dart<|end_filename|>
// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'user.dart';

// **************************************************************************
// Generator: JsonSerializableGenerator
// **************************************************************************

User _$UserFromJson(Map<String, dynamic> json) => new User(
    json['id'] as int,
    json['account'] as String,
    json['name'] as String,
    json['profile_image_urls'] == null
        ? null
        : new Avatar.fromJson(
            json['profile_image_urls'] as Map<String, dynamic>));

abstract class _$UserSerializerMixin {
  int get id;
  String get account;
  String get name;
  Avatar get profileImageUrls;
  Map<String, dynamic> toJson() => <String, dynamic>{
        'id': id,
        'account': account,
        'name': name,
        'profile_image_urls': profileImageUrls
      };
}

Avatar _$AvatarFromJson(Map<String, dynamic> json) =>
    new Avatar(json['px_170x170'] as String, json['px_50x50'] as String);

abstract class _$AvatarSerializerMixin {
  String get px170x170;
  String get px50x50;
  Map<String, dynamic> toJson() =>
      <String, dynamic>{'px_170x170': px170x170, 'px_50x50': px50x50};
}


<|start_filename|>lib/module/gallery_page.dart<|end_filename|>
import 'package:flutter/material.dart';
import 'package:flutter_advanced_networkimage/zoomable_widget.dart';
import 'package:flutter_advanced_networkimage/transition_to_image.dart';
import 'package:flutter_advanced_networkimage/flutter_advanced_networkimage.dart';
import 'package:pxnyan/http/api.dart' as Api;

class GalleryPage extends StatefulWidget {
  final List<String> _imageUrls;

  GalleryPage(this._imageUrls);

  @override
  State<StatefulWidget> createState() => new GalleryPageState();
}

class GalleryPageState extends State<GalleryPage> {
  int position = 0;

  @override
  Widget build(BuildContext context) {
    return new Container(
      color: Colors.grey[800],
      child: new Stack(
        children: <Widget>[
          new PageView.builder(
            itemCount: widget._imageUrls.length,
            onPageChanged: (position) {
              setState(() {
                this.position = position;
              });
            },
            itemBuilder: (context, index) {
              TransitionToImage imageWidget = new TransitionToImage(
                new AdvancedNetworkImage(widget._imageUrls[index], header: Api.HEADER),
                useReload: true,
                reloadWidget: new Icon(
                  Icons.replay,
                  color: Colors.white,
                ),
              );

              return new ZoomableWidget(
                maxScale: 3.0,
                child: imageWidget,
                tapCallback: imageWidget.reloadImage,
              );
            },
          ),
          new Container(
            margin: const EdgeInsets.only(bottom: 16.0),
            alignment: AlignmentDirectional.bottomCenter,
            child: new Text(
              '${position + 1}/${widget._imageUrls.length}',
              style: new TextStyle(
                fontSize: 16.0,
                color: Colors.white,
                decoration: TextDecoration.none,
              ),
            ),
          ),
          new Container(
            margin: const EdgeInsets.only(top: 40.0, left: 16.0),
            child: new GestureDetector(
              onTap: () => Navigator.pop(context),
              child: new Icon(
                Icons.close,
                color: Colors.white,
              ),
            ),
          )
        ],
      ),
    );
  }
}
