diff --git a/lib/models/github.dart b/lib/models/github.dart index 445853c..c321b60 100644 --- a/lib/models/github.dart +++ b/lib/models/github.dart @@ -326,13 +326,47 @@ class GithubUserOrganizationItem { _$GithubUserOrganizationItemFromJson(json); } +@JsonSerializable(fieldRename: FieldRename.snake) +class GistFiles { + GistFiles({ + this.filename, + this.size, + this.rawUrl, + this.type, + this.language, + this.truncated, + this.content, + }); + String filename; + int size; + String rawUrl; + String type; + String language; + bool truncated; + String content; + + factory GistFiles.fromJson(Map json) => + _$GistFilesFromJson(json); +} + @JsonSerializable(fieldRename: FieldRename.snake) class GithubGistsItem { - int id; - bool isFork; - bool isPublic; - String name; + String id; + String description; + bool public; + Map files; + GithubEventUser owner; + List get fileNames { + List filenames = []; + files.forEach((String key, GistFiles value) { + filenames.add(value); + }); + return filenames; + } + + DateTime createdAt; DateTime updatedAt; + GithubGistsItem(); factory GithubGistsItem.fromJson(Map json) => _$GithubGistsItemFromJson(json); diff --git a/lib/models/github.g.dart b/lib/models/github.g.dart index f6ad7e6..62b21a7 100644 --- a/lib/models/github.g.dart +++ b/lib/models/github.g.dart @@ -461,12 +461,43 @@ Map _$GithubUserOrganizationItemToJson( 'url': instance.url, }; +GistFiles _$GistFilesFromJson(Map json) { + return GistFiles( + filename: json['filename'] as String, + size: json['size'] as int, + rawUrl: json['raw_url'] as String, + type: json['type'] as String, + language: json['language'] as String, + truncated: json['truncated'] as bool, + content: json['content'] as String, + ); +} + +Map _$GistFilesToJson(GistFiles instance) => { + 'filename': instance.filename, + 'size': instance.size, + 'raw_url': instance.rawUrl, + 'type': instance.type, + 'language': instance.language, + 'truncated': instance.truncated, + 'content': instance.content, + }; + GithubGistsItem _$GithubGistsItemFromJson(Map json) { return GithubGistsItem() - ..id = json['id'] as int - ..isFork = json['is_fork'] as bool - ..isPublic = json['is_public'] as bool - ..name = json['name'] as String + ..id = json['id'] as String + ..description = json['description'] as String + ..public = json['public'] as bool + ..files = (json['files'] as Map)?.map( + (k, e) => MapEntry( + k, e == null ? null : GistFiles.fromJson(e as Map)), + ) + ..owner = json['owner'] == null + ? null + : GithubEventUser.fromJson(json['owner'] as Map) + ..createdAt = json['created_at'] == null + ? null + : DateTime.parse(json['created_at'] as String) ..updatedAt = json['updated_at'] == null ? null : DateTime.parse(json['updated_at'] as String); @@ -475,9 +506,11 @@ GithubGistsItem _$GithubGistsItemFromJson(Map json) { Map _$GithubGistsItemToJson(GithubGistsItem instance) => { 'id': instance.id, - 'is_fork': instance.isFork, - 'is_public': instance.isPublic, - 'name': instance.name, + 'description': instance.description, + 'public': instance.public, + 'files': instance.files, + 'owner': instance.owner, + 'created_at': instance.createdAt?.toIso8601String(), 'updated_at': instance.updatedAt?.toIso8601String(), }; diff --git a/lib/router.dart b/lib/router.dart index 3bfc2d9..65431ec 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -7,6 +7,7 @@ import 'package:git_touch/screens/code_theme.dart'; import 'package:git_touch/screens/gh_commits.dart'; import 'package:git_touch/screens/gh_contributors.dart'; import 'package:git_touch/screens/gh_files.dart'; +import 'package:git_touch/screens/gh_gists_files.dart'; import 'package:git_touch/screens/gh_org_repos.dart'; import 'package:git_touch/screens/gl_commit.dart'; import 'package:git_touch/screens/gl_starrers.dart'; @@ -38,8 +39,9 @@ import 'package:git_touch/screens/settings.dart'; import 'package:git_touch/screens/gh_user.dart'; import 'package:git_touch/screens/gh_users.dart'; import 'package:git_touch/screens/gh_user_organization.dart'; +import 'package:git_touch/screens/gh_gists.dart'; +import 'package:git_touch/screens/gh_gist_object.dart'; import 'package:git_touch/screens/gh_compare.dart'; -// import 'package:git_touch/screens/gh_gists.dart'; class RouterScreen { String path; @@ -78,6 +80,8 @@ class GithubRouter { GithubRouter.watchers, GithubRouter.contributors, GithubRouter.files, + GithubRouter.gistFiles, + GithubRouter.gistObject, GithubRouter.compare, ]; static final user = RouterScreen('/:login', (_, p) { @@ -99,7 +103,7 @@ class GithubRouter { case 'organizations': return GhUserOrganizationScreen(login); case 'gists': - // return GhGistsScreen(login); + return GhGistsScreen(login); default: return GhUserScreen(login); } @@ -112,6 +116,18 @@ class GithubRouter { branch: p['ref'].first); } }); + static final gistObject = RouterScreen('/:login/gists/:id/:file', (_, p) { + return GistObjectScreen( + p['login'].first, + p['id'].first, + p['file'].first, + raw: p['raw']?.first, + content: p['content'].first, + ); + }); + static final gistFiles = RouterScreen('/:login/gists/:id', (_, p) { + return GhGistsFilesScreen(p['login'].first, p['id'].first); + }); static final issueAdd = RouterScreen('/:owner/:name/issues/new', (_, p) { return GhIssueFormScreen(p['owner'].first, p['name'].first); }); diff --git a/lib/screens/gh_gist_object.dart b/lib/screens/gh_gist_object.dart new file mode 100644 index 0000000..757073a --- /dev/null +++ b/lib/screens/gh_gist_object.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:git_touch/scaffolds/common.dart'; +import 'package:git_touch/widgets/app_bar_title.dart'; +import 'package:git_touch/widgets/blob_view.dart'; +import 'package:git_touch/widgets/action_entry.dart'; + +class GistObjectScreen extends StatelessWidget { + final String login; + final String id; + final String file; + final String raw; + final String content; + + GistObjectScreen(this.login, this.id, this.file, {this.raw, this.content}); + + @override + Widget build(BuildContext context) { + return CommonScaffold( + title: AppBarTitle(file), + action: ActionEntry( + iconData: Icons.settings, + url: '/choose-code-theme', + ), + body: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: BlobView( + file, + text: content, + ))); + } +} diff --git a/lib/screens/gh_gists.dart b/lib/screens/gh_gists.dart new file mode 100644 index 0000000..533197c --- /dev/null +++ b/lib/screens/gh_gists.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:git_touch/models/github.dart'; +import 'package:git_touch/scaffolds/list_stateful.dart'; +import 'package:git_touch/widgets/app_bar_title.dart'; +import 'package:git_touch/widgets/gists_item.dart'; +import 'package:provider/provider.dart'; +import 'package:git_touch/models/auth.dart'; + +class GhGistsScreen extends StatelessWidget { + final String login; + GhGistsScreen(this.login); + + Future> _query(BuildContext context, + [int page = 1]) async { + final auth = Provider.of(context); + final res = await auth.ghClient.getJSON>( + '/users/$login/gists?page=$page', + convert: (vs) => [for (var v in vs) GithubGistsItem.fromJson(v)], + ); + return ListPayload( + cursor: page + 1, + items: res, + hasMore: res.isNotEmpty, + ); + } + + @override + Widget build(BuildContext context) { + return ListStatefulScaffold( + title: AppBarTitle('Gists'), + onRefresh: () => _query(context), + onLoadMore: (cursor) => _query(context, cursor), + itemBuilder: (v) { + return GistsItem( + description: v.description, + login: login, + files: v.files, + filenames: v.fileNames, + language: v.fileNames[0].language, + avatarUrl: v.owner.avatarUrl, + updatedAt: v.updatedAt, + id: v.id, + ); + }, + ); + } +} diff --git a/lib/screens/gh_gists_files.dart b/lib/screens/gh_gists_files.dart new file mode 100644 index 0000000..8c0d3ed --- /dev/null +++ b/lib/screens/gh_gists_files.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:git_touch/models/github.dart'; +import 'package:git_touch/scaffolds/refresh_stateful.dart'; +import 'package:git_touch/widgets/app_bar_title.dart'; +import 'package:git_touch/widgets/object_tree.dart'; +import 'package:provider/provider.dart'; +import 'package:git_touch/models/auth.dart'; + +class GhGistsFilesScreen extends StatelessWidget { + final String id; + final String login; + GhGistsFilesScreen(this.login, this.id); + + @override + Widget build(BuildContext context) { + final auth = Provider.of(context); + return RefreshStatefulScaffold( + title: AppBarTitle('Files'), + fetchData: () async { + final data = await auth.ghClient.getJSON( + '/gists/$id', + convert: (vs) => GithubGistsItem.fromJson(vs), + ); + return data; + }, + bodyBuilder: (payload, _) { + return ObjectTree( + items: payload.fileNames.map((v) { + final uri = Uri( + path: '/github/$login/gists/$id/${v.filename}', + queryParameters: { + 'content': v.content, + ...(v.rawUrl == null ? {} : {'raw': v.rawUrl}), + }, + ).toString(); + return ObjectTreeItem( + url: uri, + type: 'file', + name: v.filename, + downloadUrl: v.rawUrl, + size: v.size, + ); + }), + ); + }, + ); + } +} diff --git a/lib/screens/gh_user.dart b/lib/screens/gh_user.dart index b038a49..27e21d3 100644 --- a/lib/screens/gh_user.dart +++ b/lib/screens/gh_user.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:git_touch/graphql/gh.dart'; +import 'package:git_touch/models/github.dart'; import 'package:git_touch/models/theme.dart'; import 'package:git_touch/scaffolds/refresh_stateful.dart'; import 'package:git_touch/utils/utils.dart'; @@ -152,6 +153,10 @@ class GhUserScreen extends StatelessWidget { TableView( hasIcon: true, items: [ + TableViewItem( + leftIconData: Octicons.book, + text: Text('Gists'), + url: '/github/$login?tab=gists'), TableViewItem( leftIconData: Octicons.home, text: Text('Organizations'), diff --git a/lib/widgets/gists_item.dart b/lib/widgets/gists_item.dart new file mode 100644 index 0000000..f390a83 --- /dev/null +++ b/lib/widgets/gists_item.dart @@ -0,0 +1,130 @@ +import 'package:flutter/material.dart'; +import 'package:git_touch/models/github.dart'; +import 'package:git_touch/models/theme.dart'; +import 'package:git_touch/utils/utils.dart'; +import 'package:git_touch/widgets/avatar.dart'; +import 'package:git_touch/widgets/link.dart'; +import 'package:provider/provider.dart'; +import 'package:github/src/const/language_color.dart'; +import 'package:timeago/timeago.dart' as timeago; + +class GistsItem extends StatelessWidget { + final String description; + final String login; + final Map files; + final List filenames; + final String language; + final String avatarUrl; + final DateTime updatedAt; + final String id; + + GistsItem({ + @required this.description, + @required this.login, + @required this.files, + @required this.filenames, + @required this.language, + @required this.avatarUrl, + @required this.updatedAt, + @required this.id, + }); + + @override + Widget build(BuildContext context) { + final theme = Provider.of(context); + return Link( + url: '/github/$login/gists/$id', + child: Container( + padding: CommonStyle.padding, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Avatar( + url: avatarUrl, + size: AvatarSize.small, + linkUrl: '/github/$login', + ), + SizedBox(width: 8), + Expanded( + child: Text.rich( + TextSpan(children: [ + TextSpan( + text: '$login / ', + style: TextStyle( + fontSize: 18, + color: theme.palette.primary, + ), + ), + TextSpan( + text: filenames[0].filename, + style: TextStyle( + fontSize: 18, + color: theme.palette.primary, + fontWeight: FontWeight.w600, + ), + ), + ]), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + SizedBox(height: 8), + if (description != null && description.isNotEmpty) ...[ + Text( + description, + style: TextStyle( + color: theme.palette.secondaryText, + fontSize: 16, + ), + ), + SizedBox(height: 10), + ], + if (updatedAt != null) ...[ + Text( + 'Updated ${timeago.format(updatedAt)}', + style: TextStyle( + fontSize: 14, + color: theme.palette.tertiaryText, + ), + ), + SizedBox(height: 10), + ], + DefaultTextStyle( + style: TextStyle(color: theme.palette.text, fontSize: 14), + child: Row( + children: [ + if (language != null) + Container( + width: 12, + height: 12, + decoration: BoxDecoration( + color: convertColor(languagesColor[language]), + shape: BoxShape.circle, + ), + ), + SizedBox(width: 4), + if (language != null) + Text( + language, + overflow: TextOverflow.ellipsis, + ), + SizedBox(width: 24), + ], + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +}