mirror of
https://github.com/git-touch/git-touch
synced 2025-01-31 08:04:51 +01:00
feat: add graphql types for user screen
This commit is contained in:
parent
405cc03a30
commit
b11d6be74b
@ -27,9 +27,7 @@ targets:
|
||||
- schema: lib/github.schema.json
|
||||
output: lib/graphql/gh_user.dart
|
||||
queries_glob: lib/graphql/user.graphql
|
||||
- schema: lib/github.schema.json
|
||||
output: lib/graphql/gh_repository.dart
|
||||
queries_glob: lib/graphql/repository.graphql
|
||||
resolve_type_field: __typename
|
||||
scalar_mapping:
|
||||
- graphql_type: URI
|
||||
dart_type: String
|
||||
|
99007
lib/github.schema.json
Normal file
99007
lib/github.schema.json
Normal file
File diff suppressed because it is too large
Load Diff
1400
lib/graphql/gh_user.dart
Normal file
1400
lib/graphql/gh_user.dart
Normal file
File diff suppressed because it is too large
Load Diff
431
lib/graphql/gh_user.g.dart
Normal file
431
lib/graphql/gh_user.g.dart
Normal file
@ -0,0 +1,431 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'gh_user.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
GhUser _$GhUserFromJson(Map<String, dynamic> json) {
|
||||
return GhUser()
|
||||
..repositoryOwner = json['repositoryOwner'] == null
|
||||
? null
|
||||
: RepositoryOwner.fromJson(
|
||||
json['repositoryOwner'] as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$GhUserToJson(GhUser instance) => <String, dynamic>{
|
||||
'repositoryOwner': instance.repositoryOwner?.toJson(),
|
||||
};
|
||||
|
||||
RepositoryOwner _$RepositoryOwnerFromJson(Map<String, dynamic> json) {
|
||||
return RepositoryOwner()..resolveType = json['__typename'] as String;
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$RepositoryOwnerToJson(RepositoryOwner instance) =>
|
||||
<String, dynamic>{
|
||||
'__typename': instance.resolveType,
|
||||
};
|
||||
|
||||
User _$UserFromJson(Map<String, dynamic> json) {
|
||||
return User()
|
||||
..login = json['login'] as String
|
||||
..name = json['name'] as String
|
||||
..avatarUrl = json['avatarUrl'] as String
|
||||
..bio = json['bio'] as String
|
||||
..company = json['company'] as String
|
||||
..location = json['location'] as String
|
||||
..email = json['email'] as String
|
||||
..websiteUrl = json['websiteUrl'] as String
|
||||
..starredRepositories = json['starredRepositories'] == null
|
||||
? null
|
||||
: StarredRepositoryConnection.fromJson(
|
||||
json['starredRepositories'] as Map<String, dynamic>)
|
||||
..followers = json['followers'] == null
|
||||
? null
|
||||
: FollowerConnection.fromJson(json['followers'] as Map<String, dynamic>)
|
||||
..following = json['following'] == null
|
||||
? null
|
||||
: FollowingConnection.fromJson(
|
||||
json['following'] as Map<String, dynamic>)
|
||||
..repositories = json['repositories'] == null
|
||||
? null
|
||||
: RepositoryConnection.fromJson(
|
||||
json['repositories'] as Map<String, dynamic>)
|
||||
..pinnedItems = json['pinnedItems'] == null
|
||||
? null
|
||||
: PinnableItemConnection.fromJson(
|
||||
json['pinnedItems'] as Map<String, dynamic>)
|
||||
..viewerCanFollow = json['viewerCanFollow'] as bool
|
||||
..viewerIsFollowing = json['viewerIsFollowing'] as bool
|
||||
..url = json['url'] as String
|
||||
..resolveType = json['__typename'] as String;
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$UserToJson(User instance) => <String, dynamic>{
|
||||
'login': instance.login,
|
||||
'name': instance.name,
|
||||
'avatarUrl': instance.avatarUrl,
|
||||
'bio': instance.bio,
|
||||
'company': instance.company,
|
||||
'location': instance.location,
|
||||
'email': instance.email,
|
||||
'websiteUrl': instance.websiteUrl,
|
||||
'starredRepositories': instance.starredRepositories?.toJson(),
|
||||
'followers': instance.followers?.toJson(),
|
||||
'following': instance.following?.toJson(),
|
||||
'repositories': instance.repositories?.toJson(),
|
||||
'pinnedItems': instance.pinnedItems?.toJson(),
|
||||
'viewerCanFollow': instance.viewerCanFollow,
|
||||
'viewerIsFollowing': instance.viewerIsFollowing,
|
||||
'url': instance.url,
|
||||
'__typename': instance.resolveType,
|
||||
};
|
||||
|
||||
StarredRepositoryConnection _$StarredRepositoryConnectionFromJson(
|
||||
Map<String, dynamic> json) {
|
||||
return StarredRepositoryConnection()..totalCount = json['totalCount'] as int;
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$StarredRepositoryConnectionToJson(
|
||||
StarredRepositoryConnection instance) =>
|
||||
<String, dynamic>{
|
||||
'totalCount': instance.totalCount,
|
||||
};
|
||||
|
||||
FollowerConnection _$FollowerConnectionFromJson(Map<String, dynamic> json) {
|
||||
return FollowerConnection()..totalCount = json['totalCount'] as int;
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$FollowerConnectionToJson(FollowerConnection instance) =>
|
||||
<String, dynamic>{
|
||||
'totalCount': instance.totalCount,
|
||||
};
|
||||
|
||||
FollowingConnection _$FollowingConnectionFromJson(Map<String, dynamic> json) {
|
||||
return FollowingConnection()..totalCount = json['totalCount'] as int;
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$FollowingConnectionToJson(
|
||||
FollowingConnection instance) =>
|
||||
<String, dynamic>{
|
||||
'totalCount': instance.totalCount,
|
||||
};
|
||||
|
||||
RepositoryConnection _$RepositoryConnectionFromJson(Map<String, dynamic> json) {
|
||||
return RepositoryConnection()
|
||||
..totalCount = json['totalCount'] as int
|
||||
..nodes = (json['nodes'] as List)
|
||||
?.map((e) =>
|
||||
e == null ? null : Repository.fromJson(e as Map<String, dynamic>))
|
||||
?.toList();
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$RepositoryConnectionToJson(
|
||||
RepositoryConnection instance) =>
|
||||
<String, dynamic>{
|
||||
'totalCount': instance.totalCount,
|
||||
'nodes': instance.nodes?.map((e) => e?.toJson())?.toList(),
|
||||
};
|
||||
|
||||
Repository _$RepositoryFromJson(Map<String, dynamic> json) {
|
||||
return Repository()
|
||||
..owner = json['owner'] == null
|
||||
? null
|
||||
: RepositoryOwner.fromJson(json['owner'] as Map<String, dynamic>)
|
||||
..name = json['name'] as String
|
||||
..description = json['description'] as String
|
||||
..isPrivate = json['isPrivate'] as bool
|
||||
..isFork = json['isFork'] as bool
|
||||
..stargazers = json['stargazers'] == null
|
||||
? null
|
||||
: StargazerConnection.fromJson(
|
||||
json['stargazers'] as Map<String, dynamic>)
|
||||
..forks = json['forks'] == null
|
||||
? null
|
||||
: RepositoryConnection.fromJson(json['forks'] as Map<String, dynamic>)
|
||||
..primaryLanguage = json['primaryLanguage'] == null
|
||||
? null
|
||||
: Language.fromJson(json['primaryLanguage'] as Map<String, dynamic>)
|
||||
..resolveType = json['__typename'] as String;
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$RepositoryToJson(Repository instance) =>
|
||||
<String, dynamic>{
|
||||
'owner': instance.owner?.toJson(),
|
||||
'name': instance.name,
|
||||
'description': instance.description,
|
||||
'isPrivate': instance.isPrivate,
|
||||
'isFork': instance.isFork,
|
||||
'stargazers': instance.stargazers?.toJson(),
|
||||
'forks': instance.forks?.toJson(),
|
||||
'primaryLanguage': instance.primaryLanguage?.toJson(),
|
||||
'__typename': instance.resolveType,
|
||||
};
|
||||
|
||||
StargazerConnection _$StargazerConnectionFromJson(Map<String, dynamic> json) {
|
||||
return StargazerConnection()..totalCount = json['totalCount'] as int;
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$StargazerConnectionToJson(
|
||||
StargazerConnection instance) =>
|
||||
<String, dynamic>{
|
||||
'totalCount': instance.totalCount,
|
||||
};
|
||||
|
||||
Language _$LanguageFromJson(Map<String, dynamic> json) {
|
||||
return Language()
|
||||
..color = json['color'] as String
|
||||
..name = json['name'] as String
|
||||
..resolveType = json['__typename'] as String;
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$LanguageToJson(Language instance) => <String, dynamic>{
|
||||
'color': instance.color,
|
||||
'name': instance.name,
|
||||
'__typename': instance.resolveType,
|
||||
};
|
||||
|
||||
Node _$NodeFromJson(Map<String, dynamic> json) {
|
||||
return Node()..resolveType = json['__typename'] as String;
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$NodeToJson(Node instance) => <String, dynamic>{
|
||||
'__typename': instance.resolveType,
|
||||
};
|
||||
|
||||
PinnableItem _$PinnableItemFromJson(Map<String, dynamic> json) {
|
||||
return PinnableItem();
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$PinnableItemToJson(PinnableItem instance) =>
|
||||
<String, dynamic>{};
|
||||
|
||||
ProjectOwner _$ProjectOwnerFromJson(Map<String, dynamic> json) {
|
||||
return ProjectOwner()..resolveType = json['__typename'] as String;
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$ProjectOwnerToJson(ProjectOwner instance) =>
|
||||
<String, dynamic>{
|
||||
'__typename': instance.resolveType,
|
||||
};
|
||||
|
||||
RegistryPackageOwner _$RegistryPackageOwnerFromJson(Map<String, dynamic> json) {
|
||||
return RegistryPackageOwner()..resolveType = json['__typename'] as String;
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$RegistryPackageOwnerToJson(
|
||||
RegistryPackageOwner instance) =>
|
||||
<String, dynamic>{
|
||||
'__typename': instance.resolveType,
|
||||
};
|
||||
|
||||
RegistryPackageSearch _$RegistryPackageSearchFromJson(
|
||||
Map<String, dynamic> json) {
|
||||
return RegistryPackageSearch()..resolveType = json['__typename'] as String;
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$RegistryPackageSearchToJson(
|
||||
RegistryPackageSearch instance) =>
|
||||
<String, dynamic>{
|
||||
'__typename': instance.resolveType,
|
||||
};
|
||||
|
||||
Subscribable _$SubscribableFromJson(Map<String, dynamic> json) {
|
||||
return Subscribable()..resolveType = json['__typename'] as String;
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$SubscribableToJson(Subscribable instance) =>
|
||||
<String, dynamic>{
|
||||
'__typename': instance.resolveType,
|
||||
};
|
||||
|
||||
Starrable _$StarrableFromJson(Map<String, dynamic> json) {
|
||||
return Starrable()
|
||||
..stargazers = json['stargazers'] == null
|
||||
? null
|
||||
: StargazerConnection.fromJson(
|
||||
json['stargazers'] as Map<String, dynamic>)
|
||||
..resolveType = json['__typename'] as String;
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$StarrableToJson(Starrable instance) => <String, dynamic>{
|
||||
'stargazers': instance.stargazers?.toJson(),
|
||||
'__typename': instance.resolveType,
|
||||
};
|
||||
|
||||
UniformResourceLocatable _$UniformResourceLocatableFromJson(
|
||||
Map<String, dynamic> json) {
|
||||
return UniformResourceLocatable()..resolveType = json['__typename'] as String;
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$UniformResourceLocatableToJson(
|
||||
UniformResourceLocatable instance) =>
|
||||
<String, dynamic>{
|
||||
'__typename': instance.resolveType,
|
||||
};
|
||||
|
||||
RepositoryInfo _$RepositoryInfoFromJson(Map<String, dynamic> json) {
|
||||
return RepositoryInfo()
|
||||
..owner = json['owner'] == null
|
||||
? null
|
||||
: RepositoryOwner.fromJson(json['owner'] as Map<String, dynamic>)
|
||||
..name = json['name'] as String
|
||||
..description = json['description'] as String
|
||||
..isPrivate = json['isPrivate'] as bool
|
||||
..isFork = json['isFork'] as bool
|
||||
..resolveType = json['__typename'] as String;
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$RepositoryInfoToJson(RepositoryInfo instance) =>
|
||||
<String, dynamic>{
|
||||
'owner': instance.owner?.toJson(),
|
||||
'name': instance.name,
|
||||
'description': instance.description,
|
||||
'isPrivate': instance.isPrivate,
|
||||
'isFork': instance.isFork,
|
||||
'__typename': instance.resolveType,
|
||||
};
|
||||
|
||||
PinnableItemConnection _$PinnableItemConnectionFromJson(
|
||||
Map<String, dynamic> json) {
|
||||
return PinnableItemConnection()
|
||||
..nodes = (json['nodes'] as List)
|
||||
?.map((e) =>
|
||||
e == null ? null : PinnableItem.fromJson(e as Map<String, dynamic>))
|
||||
?.toList();
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$PinnableItemConnectionToJson(
|
||||
PinnableItemConnection instance) =>
|
||||
<String, dynamic>{
|
||||
'nodes': instance.nodes?.map((e) => e?.toJson())?.toList(),
|
||||
};
|
||||
|
||||
AuditEntryActor _$AuditEntryActorFromJson(Map<String, dynamic> json) {
|
||||
return AuditEntryActor();
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$AuditEntryActorToJson(AuditEntryActor instance) =>
|
||||
<String, dynamic>{};
|
||||
|
||||
Actor _$ActorFromJson(Map<String, dynamic> json) {
|
||||
return Actor()
|
||||
..login = json['login'] as String
|
||||
..avatarUrl = json['avatarUrl'] as String
|
||||
..url = json['url'] as String
|
||||
..resolveType = json['__typename'] as String;
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$ActorToJson(Actor instance) => <String, dynamic>{
|
||||
'login': instance.login,
|
||||
'avatarUrl': instance.avatarUrl,
|
||||
'url': instance.url,
|
||||
'__typename': instance.resolveType,
|
||||
};
|
||||
|
||||
ProfileOwner _$ProfileOwnerFromJson(Map<String, dynamic> json) {
|
||||
return ProfileOwner()
|
||||
..login = json['login'] as String
|
||||
..name = json['name'] as String
|
||||
..location = json['location'] as String
|
||||
..email = json['email'] as String
|
||||
..websiteUrl = json['websiteUrl'] as String
|
||||
..pinnedItems = json['pinnedItems'] == null
|
||||
? null
|
||||
: PinnableItemConnection.fromJson(
|
||||
json['pinnedItems'] as Map<String, dynamic>)
|
||||
..resolveType = json['__typename'] as String;
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$ProfileOwnerToJson(ProfileOwner instance) =>
|
||||
<String, dynamic>{
|
||||
'login': instance.login,
|
||||
'name': instance.name,
|
||||
'location': instance.location,
|
||||
'email': instance.email,
|
||||
'websiteUrl': instance.websiteUrl,
|
||||
'pinnedItems': instance.pinnedItems?.toJson(),
|
||||
'__typename': instance.resolveType,
|
||||
};
|
||||
|
||||
Sponsorable _$SponsorableFromJson(Map<String, dynamic> json) {
|
||||
return Sponsorable()..resolveType = json['__typename'] as String;
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$SponsorableToJson(Sponsorable instance) =>
|
||||
<String, dynamic>{
|
||||
'__typename': instance.resolveType,
|
||||
};
|
||||
|
||||
Organization _$OrganizationFromJson(Map<String, dynamic> json) {
|
||||
return Organization()
|
||||
..login = json['login'] as String
|
||||
..name = json['name'] as String
|
||||
..avatarUrl = json['avatarUrl'] as String
|
||||
..description = json['description'] as String
|
||||
..location = json['location'] as String
|
||||
..email = json['email'] as String
|
||||
..websiteUrl = json['websiteUrl'] as String
|
||||
..url = json['url'] as String
|
||||
..pinnedItems = json['pinnedItems'] == null
|
||||
? null
|
||||
: PinnableItemConnection.fromJson(
|
||||
json['pinnedItems'] as Map<String, dynamic>)
|
||||
..pinnableItems = json['pinnableItems'] == null
|
||||
? null
|
||||
: PinnableItemConnection.fromJson(
|
||||
json['pinnableItems'] as Map<String, dynamic>)
|
||||
..membersWithRole = json['membersWithRole'] == null
|
||||
? null
|
||||
: OrganizationMemberConnection.fromJson(
|
||||
json['membersWithRole'] as Map<String, dynamic>)
|
||||
..resolveType = json['__typename'] as String;
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$OrganizationToJson(Organization instance) =>
|
||||
<String, dynamic>{
|
||||
'login': instance.login,
|
||||
'name': instance.name,
|
||||
'avatarUrl': instance.avatarUrl,
|
||||
'description': instance.description,
|
||||
'location': instance.location,
|
||||
'email': instance.email,
|
||||
'websiteUrl': instance.websiteUrl,
|
||||
'url': instance.url,
|
||||
'pinnedItems': instance.pinnedItems?.toJson(),
|
||||
'pinnableItems': instance.pinnableItems?.toJson(),
|
||||
'membersWithRole': instance.membersWithRole?.toJson(),
|
||||
'__typename': instance.resolveType,
|
||||
};
|
||||
|
||||
OrganizationMemberConnection _$OrganizationMemberConnectionFromJson(
|
||||
Map<String, dynamic> json) {
|
||||
return OrganizationMemberConnection()..totalCount = json['totalCount'] as int;
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$OrganizationMemberConnectionToJson(
|
||||
OrganizationMemberConnection instance) =>
|
||||
<String, dynamic>{
|
||||
'totalCount': instance.totalCount,
|
||||
};
|
||||
|
||||
MemberStatusable _$MemberStatusableFromJson(Map<String, dynamic> json) {
|
||||
return MemberStatusable()..resolveType = json['__typename'] as String;
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$MemberStatusableToJson(MemberStatusable instance) =>
|
||||
<String, dynamic>{
|
||||
'__typename': instance.resolveType,
|
||||
};
|
||||
|
||||
GhUserArguments _$GhUserArgumentsFromJson(Map<String, dynamic> json) {
|
||||
return GhUserArguments(
|
||||
login: json['login'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$GhUserArgumentsToJson(GhUserArguments instance) =>
|
||||
<String, dynamic>{
|
||||
'login': instance.login,
|
||||
};
|
144
lib/graphql/user.graphql
Normal file
144
lib/graphql/user.graphql
Normal file
@ -0,0 +1,144 @@
|
||||
query($login: String!) {
|
||||
repositoryOwner(login: $login) {
|
||||
__typename
|
||||
... on User {
|
||||
login
|
||||
name
|
||||
avatarUrl
|
||||
bio
|
||||
company
|
||||
location
|
||||
email
|
||||
websiteUrl
|
||||
starredRepositories {
|
||||
totalCount
|
||||
}
|
||||
followers {
|
||||
totalCount
|
||||
}
|
||||
following {
|
||||
totalCount
|
||||
}
|
||||
repositories(
|
||||
first: 6
|
||||
ownerAffiliations: OWNER
|
||||
orderBy: { field: STARGAZERS, direction: DESC }
|
||||
) {
|
||||
totalCount
|
||||
nodes {
|
||||
owner {
|
||||
__typename
|
||||
login
|
||||
avatarUrl
|
||||
}
|
||||
name
|
||||
description
|
||||
isPrivate
|
||||
isFork
|
||||
stargazers {
|
||||
totalCount
|
||||
}
|
||||
forks {
|
||||
totalCount
|
||||
}
|
||||
primaryLanguage {
|
||||
color
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
pinnedItems(first: 6) {
|
||||
nodes {
|
||||
... on Repository {
|
||||
owner {
|
||||
__typename
|
||||
login
|
||||
avatarUrl
|
||||
}
|
||||
name
|
||||
description
|
||||
isPrivate
|
||||
isFork
|
||||
stargazers {
|
||||
totalCount
|
||||
}
|
||||
forks {
|
||||
totalCount
|
||||
}
|
||||
primaryLanguage {
|
||||
color
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
viewerCanFollow
|
||||
viewerIsFollowing
|
||||
url
|
||||
}
|
||||
... on Organization {
|
||||
login
|
||||
name
|
||||
avatarUrl
|
||||
description
|
||||
location
|
||||
email
|
||||
websiteUrl
|
||||
url
|
||||
pinnedItems(first: 6) {
|
||||
nodes {
|
||||
... on Repository {
|
||||
owner {
|
||||
__typename
|
||||
login
|
||||
avatarUrl
|
||||
}
|
||||
name
|
||||
description
|
||||
isPrivate
|
||||
isFork
|
||||
stargazers {
|
||||
totalCount
|
||||
}
|
||||
forks {
|
||||
totalCount
|
||||
}
|
||||
primaryLanguage {
|
||||
color
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pinnableItems(first: 6, types: [REPOSITORY]) {
|
||||
totalCount
|
||||
nodes {
|
||||
... on Repository {
|
||||
owner {
|
||||
__typename
|
||||
login
|
||||
avatarUrl
|
||||
}
|
||||
name
|
||||
description
|
||||
isPrivate
|
||||
isFork
|
||||
stargazers {
|
||||
totalCount
|
||||
}
|
||||
forks {
|
||||
totalCount
|
||||
}
|
||||
primaryLanguage {
|
||||
color
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
membersWithRole {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,18 +1,16 @@
|
||||
import 'dart:io';
|
||||
import 'dart:convert';
|
||||
import 'dart:async';
|
||||
// import 'package:artemis/client.dart';
|
||||
// import 'package:gql_http_link/gql_http_link.dart';
|
||||
import 'package:gql_http_link/gql_http_link.dart';
|
||||
import 'package:artemis/artemis.dart';
|
||||
import 'package:fimber/fimber.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:uni_links/uni_links.dart';
|
||||
import 'package:nanoid/nanoid.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
// import 'package:flutter/services.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
// import '../utils/utils.dart';
|
||||
import '../utils/constants.dart';
|
||||
import '../utils/utils.dart';
|
||||
import 'account.dart';
|
||||
@ -188,19 +186,19 @@ class AuthModel with ChangeNotifier {
|
||||
var _timeoutDuration = Duration(seconds: 10);
|
||||
// var _timeoutDuration = Duration(seconds: 1);
|
||||
|
||||
// ArtemisClient _gqlClient;
|
||||
// ArtemisClient get gqlClient {
|
||||
// if (token == null) return null;
|
||||
ArtemisClient _gqlClient;
|
||||
ArtemisClient get gqlClient {
|
||||
if (token == null) return null;
|
||||
|
||||
// if (_gqlClient == null) {
|
||||
// _gqlClient = ArtemisClient.fromLink(
|
||||
// HttpLink(_apiPrefix + '/graphql',
|
||||
// defaultHeaders: {HttpHeaders.authorizationHeader: 'token $token'}),
|
||||
// );
|
||||
// }
|
||||
if (_gqlClient == null) {
|
||||
_gqlClient = ArtemisClient.fromLink(
|
||||
HttpLink(_apiPrefix + '/graphql',
|
||||
defaultHeaders: {HttpHeaders.authorizationHeader: 'token $token'}),
|
||||
);
|
||||
}
|
||||
|
||||
// return _gqlClient;
|
||||
// }
|
||||
return _gqlClient;
|
||||
}
|
||||
|
||||
Future<dynamic> query(String query, [String _token]) async {
|
||||
if (_token == null) {
|
||||
|
0
lib/screens/gitlab/repository.dart
Normal file
0
lib/screens/gitlab/repository.dart
Normal file
@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:git_touch/graphql/gh_user.dart';
|
||||
import 'package:git_touch/models/theme.dart';
|
||||
import 'package:git_touch/scaffolds/refresh_stateful.dart';
|
||||
import 'package:git_touch/screens/settings.dart';
|
||||
@ -9,6 +10,7 @@ import 'package:git_touch/widgets/action_entry.dart';
|
||||
import 'package:git_touch/widgets/app_bar_title.dart';
|
||||
import 'package:git_touch/screens/repositories.dart';
|
||||
import 'package:git_touch/widgets/entry_item.dart';
|
||||
import 'package:git_touch/widgets/repository_item.dart';
|
||||
import 'package:git_touch/widgets/table_view.dart';
|
||||
import 'package:git_touch/widgets/text_contains_organization.dart';
|
||||
import 'package:git_touch/widgets/user_contributions.dart';
|
||||
@ -16,85 +18,19 @@ import 'package:git_touch/widgets/user_item.dart';
|
||||
import 'package:github_contributions/github_contributions.dart';
|
||||
import 'package:git_touch/models/auth.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:git_touch/widgets/repository_item.dart';
|
||||
import 'package:git_touch/widgets/action_button.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
class UserScreen extends StatelessWidget {
|
||||
final String login;
|
||||
|
||||
UserScreen(this.login);
|
||||
|
||||
Future _query(BuildContext context) async {
|
||||
var _login = login ?? Provider.of<AuthModel>(context).activeAccount.login;
|
||||
var data = await Provider.of<AuthModel>(context).query('''
|
||||
{
|
||||
repositoryOwner(login: "$_login") {
|
||||
__typename
|
||||
... on User {
|
||||
$userGqlChunk
|
||||
company
|
||||
location
|
||||
email
|
||||
websiteUrl
|
||||
starredRepositories {
|
||||
totalCount
|
||||
}
|
||||
followers {
|
||||
totalCount
|
||||
}
|
||||
following {
|
||||
totalCount
|
||||
}
|
||||
repositories(first: 6, ownerAffiliations: OWNER, orderBy: {field: STARGAZERS, direction: DESC}) {
|
||||
totalCount
|
||||
nodes {
|
||||
$repoChunk
|
||||
}
|
||||
}
|
||||
pinnedItems(first: 6) {
|
||||
nodes {
|
||||
... on Repository {
|
||||
$repoChunk
|
||||
}
|
||||
}
|
||||
}
|
||||
viewerCanFollow
|
||||
viewerIsFollowing
|
||||
url
|
||||
}
|
||||
... on Organization {
|
||||
login
|
||||
name
|
||||
avatarUrl
|
||||
description
|
||||
location
|
||||
email
|
||||
websiteUrl
|
||||
url
|
||||
pinnedItems(first: 6) {
|
||||
nodes {
|
||||
... on Repository {
|
||||
$repoChunk
|
||||
}
|
||||
}
|
||||
}
|
||||
pinnableItems(first: 6, types: [REPOSITORY]) {
|
||||
totalCount
|
||||
nodes {
|
||||
... on Repository {
|
||||
$repoChunk
|
||||
}
|
||||
}
|
||||
}
|
||||
membersWithRole {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
'''); // Use pinnableItems instead of organization here due to token permission
|
||||
|
||||
return data['repositoryOwner'];
|
||||
Future<RepositoryOwner> _query(BuildContext context) async {
|
||||
final data = await Provider.of<AuthModel>(context)
|
||||
.gqlClient
|
||||
.execute(GhUserQuery(variables: GhUserArguments(login: 'pd4d10')));
|
||||
return data.data.repositoryOwner;
|
||||
}
|
||||
|
||||
Future<List<ContributionsInfo>> _fetchContributions(
|
||||
@ -112,14 +48,209 @@ class UserScreen extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
Iterable<Widget> _buildPinnedItems(
|
||||
Iterable<Repository> pinnedItems, Iterable<Repository> repositories) {
|
||||
String title;
|
||||
Iterable<Repository> items = [];
|
||||
|
||||
if (pinnedItems.isNotEmpty) {
|
||||
title = 'pinned repositories';
|
||||
items = pinnedItems;
|
||||
} else if (repositories.isNotEmpty) {
|
||||
title = 'popular repositories';
|
||||
items = repositories;
|
||||
}
|
||||
if (items.isEmpty) return [];
|
||||
|
||||
return [
|
||||
CommonStyle.verticalGap,
|
||||
if (title != null) TableViewHeader(title),
|
||||
...join(
|
||||
CommonStyle.border,
|
||||
items.map((item) {
|
||||
return RepositoryItem.github(item);
|
||||
}).toList(),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
Widget _buildUser(
|
||||
BuildContext context, User user, List<ContributionsInfo> contributions) {
|
||||
final theme = Provider.of<ThemeModel>(context);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
UserItem(
|
||||
login: user.login,
|
||||
name: user.name,
|
||||
avatarUrl: user.avatarUrl,
|
||||
bio: user.bio,
|
||||
inUserScreen: true,
|
||||
),
|
||||
CommonStyle.border,
|
||||
Row(children: [
|
||||
EntryItem(
|
||||
count: user.repositories.totalCount,
|
||||
text: 'Repositories',
|
||||
screenBuilder: (context) => RepositoriesScreen(user.login),
|
||||
),
|
||||
EntryItem(
|
||||
count: user.starredRepositories.totalCount,
|
||||
text: 'Stars',
|
||||
screenBuilder: (context) => RepositoriesScreen.stars(user.login),
|
||||
),
|
||||
EntryItem(
|
||||
count: user.followers.totalCount,
|
||||
text: 'Followers',
|
||||
screenBuilder: (context) => UsersScreen.followers(user.login),
|
||||
),
|
||||
EntryItem(
|
||||
count: user.following.totalCount,
|
||||
text: 'Following',
|
||||
screenBuilder: (context) => UsersScreen.following(user.login),
|
||||
),
|
||||
]),
|
||||
CommonStyle.verticalGap,
|
||||
UserContributions(contributions),
|
||||
CommonStyle.verticalGap,
|
||||
TableView(
|
||||
hasIcon: true,
|
||||
items: [
|
||||
if (isNotNullOrEmpty(user.company))
|
||||
TableViewItem(
|
||||
leftIconData: Octicons.organization,
|
||||
text: TextContainsOrganization(
|
||||
user.company,
|
||||
style: TextStyle(fontSize: 16, color: theme.palette.text),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (isNotNullOrEmpty(user.location))
|
||||
TableViewItem(
|
||||
leftIconData: Octicons.location,
|
||||
text: Text(user.location),
|
||||
onTap: () {
|
||||
launchUrl('https://www.google.com/maps/place/' +
|
||||
user.location.replaceAll(RegExp(r'\s+'), ''));
|
||||
},
|
||||
),
|
||||
if (isNotNullOrEmpty(user.email))
|
||||
TableViewItem(
|
||||
leftIconData: Octicons.mail,
|
||||
text: Text(user.email),
|
||||
onTap: () {
|
||||
launchUrl('mailto:' + user.email);
|
||||
},
|
||||
),
|
||||
if (isNotNullOrEmpty(user.websiteUrl))
|
||||
TableViewItem(
|
||||
leftIconData: Octicons.link,
|
||||
text: Text(user.websiteUrl),
|
||||
onTap: () {
|
||||
var url = user.websiteUrl;
|
||||
if (!url.startsWith('http')) {
|
||||
url = 'http://$url';
|
||||
}
|
||||
launchUrl(url);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
..._buildPinnedItems(
|
||||
user.pinnedItems.nodes
|
||||
.where((n) => n is Repository)
|
||||
.cast<Repository>(),
|
||||
user.repositories.nodes),
|
||||
CommonStyle.verticalGap,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOrganization(BuildContext context, Organization payload) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
UserItem(
|
||||
login: payload.login,
|
||||
name: payload.name,
|
||||
avatarUrl: payload.avatarUrl,
|
||||
bio: payload.description,
|
||||
inUserScreen: true,
|
||||
),
|
||||
CommonStyle.border,
|
||||
Row(children: [
|
||||
EntryItem(
|
||||
// count: payload.pinnableItems.totalCount,
|
||||
count: 0, // TODO:
|
||||
text: 'Repositories',
|
||||
screenBuilder: (context) =>
|
||||
RepositoriesScreen.ofOrganization(payload.login),
|
||||
),
|
||||
EntryItem(
|
||||
count: payload.membersWithRole.totalCount,
|
||||
text: 'Members',
|
||||
screenBuilder: (context) => UsersScreen.members(payload.login),
|
||||
),
|
||||
]),
|
||||
CommonStyle.verticalGap,
|
||||
TableView(
|
||||
hasIcon: true,
|
||||
items: [
|
||||
if (isNotNullOrEmpty(payload.location))
|
||||
TableViewItem(
|
||||
leftIconData: Octicons.location,
|
||||
text: Text(payload.location),
|
||||
onTap: () {
|
||||
launchUrl('https://www.google.com/maps/place/' +
|
||||
payload.location.replaceAll(RegExp(r'\s+'), ''));
|
||||
},
|
||||
),
|
||||
if (isNotNullOrEmpty(payload.email))
|
||||
TableViewItem(
|
||||
leftIconData: Octicons.mail,
|
||||
text: Text(payload.email),
|
||||
onTap: () {
|
||||
launchUrl('mailto:' + payload.email);
|
||||
},
|
||||
),
|
||||
if (isNotNullOrEmpty(payload.websiteUrl))
|
||||
TableViewItem(
|
||||
leftIconData: Octicons.link,
|
||||
text: Text(payload.websiteUrl),
|
||||
onTap: () {
|
||||
var url = payload.websiteUrl;
|
||||
if (!url.startsWith('http')) {
|
||||
url = 'http://$url';
|
||||
}
|
||||
launchUrl(url);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
..._buildPinnedItems(
|
||||
payload.pinnedItems.nodes
|
||||
.where((n) => n is Repository)
|
||||
.cast<Repository>(),
|
||||
payload.pinnableItems.nodes
|
||||
.where((n) => n is Repository)
|
||||
.cast<Repository>(),
|
||||
),
|
||||
CommonStyle.verticalGap,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RefreshStatefulScaffold(
|
||||
fetchData: () {
|
||||
return Future.wait([
|
||||
return RefreshStatefulScaffold<
|
||||
Tuple2<RepositoryOwner, List<ContributionsInfo>>>(
|
||||
fetchData: () async {
|
||||
final vs = await Future.wait([
|
||||
_query(context),
|
||||
_fetchContributions(context),
|
||||
]);
|
||||
return Tuple2(
|
||||
vs[0] as RepositoryOwner, vs[1] as List<ContributionsInfo>);
|
||||
},
|
||||
title: AppBarTitle('User'), // TODO:
|
||||
actionBuilder: (data, _) {
|
||||
@ -129,8 +260,10 @@ class UserScreen extends StatelessWidget {
|
||||
items: [],
|
||||
);
|
||||
|
||||
switch (data[0]['__typename']) {
|
||||
final payload = data.item1;
|
||||
switch (payload.resolveType) {
|
||||
case 'User':
|
||||
final user = payload as User;
|
||||
if (login == null) {
|
||||
return ActionEntry(
|
||||
iconData: Icons.settings,
|
||||
@ -144,37 +277,37 @@ class UserScreen extends StatelessWidget {
|
||||
return ActionButton(
|
||||
title: 'User Actions',
|
||||
items: [
|
||||
if (data != null && data[0]['viewerCanFollow'])
|
||||
if (user.viewerCanFollow)
|
||||
ActionItem(
|
||||
text:
|
||||
data[0]['viewerIsFollowing'] ? 'Unfollow' : 'Follow',
|
||||
text: user.viewerIsFollowing ? 'Unfollow' : 'Follow',
|
||||
onPress: (_) async {
|
||||
if (data[0]['viewerIsFollowing']) {
|
||||
if (user.viewerIsFollowing) {
|
||||
await Provider.of<AuthModel>(context)
|
||||
.deleteWithCredentials('/user/following/$login');
|
||||
data[0]['viewerIsFollowing'] = false;
|
||||
user.viewerIsFollowing = false;
|
||||
} else {
|
||||
Provider.of<AuthModel>(context)
|
||||
.putWithCredentials('/user/following/$login');
|
||||
data[0]['viewerIsFollowing'] = true;
|
||||
user.viewerIsFollowing = true;
|
||||
}
|
||||
},
|
||||
),
|
||||
if (data != null) ...[
|
||||
ActionItem.share(data[0]['url']),
|
||||
ActionItem.launch(data[0]['url']),
|
||||
ActionItem.share(user.url),
|
||||
ActionItem.launch(user.url),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'Organization':
|
||||
final organization = payload as Organization;
|
||||
return ActionButton(
|
||||
title: 'Organization Actions',
|
||||
items: [
|
||||
if (data != null) ...[
|
||||
ActionItem.share(data[0]['url']),
|
||||
ActionItem.launch(data[0]['url']),
|
||||
ActionItem.share(organization.url),
|
||||
ActionItem.launch(organization.url),
|
||||
],
|
||||
],
|
||||
);
|
||||
@ -183,118 +316,15 @@ class UserScreen extends StatelessWidget {
|
||||
}
|
||||
},
|
||||
bodyBuilder: (data, _) {
|
||||
var user = data[0];
|
||||
var contributions = data[1];
|
||||
final isOrganization = user['__typename'] == 'Organization';
|
||||
final theme = Provider.of<ThemeModel>(context);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
UserItem(
|
||||
login: user['login'],
|
||||
name: user['name'],
|
||||
avatarUrl: user['avatarUrl'],
|
||||
bio: isOrganization ? user['description'] : user['bio'],
|
||||
inUserScreen: true,
|
||||
),
|
||||
CommonStyle.border,
|
||||
Row(children: [
|
||||
if (isOrganization) ...[
|
||||
EntryItem(
|
||||
count: user['pinnableItems']['totalCount'],
|
||||
text: 'Repositories',
|
||||
screenBuilder: (context) =>
|
||||
RepositoriesScreen.ofOrganization(user['login']),
|
||||
),
|
||||
EntryItem(
|
||||
count: user['membersWithRole']['totalCount'],
|
||||
text: 'Members',
|
||||
screenBuilder: (context) =>
|
||||
UsersScreen.members(user['login']),
|
||||
),
|
||||
] else ...[
|
||||
EntryItem(
|
||||
count: user['repositories']['totalCount'],
|
||||
text: 'Repositories',
|
||||
screenBuilder: (context) => RepositoriesScreen(user['login']),
|
||||
),
|
||||
EntryItem(
|
||||
count: user['starredRepositories']['totalCount'],
|
||||
text: 'Stars',
|
||||
screenBuilder: (context) =>
|
||||
RepositoriesScreen.stars(user['login']),
|
||||
),
|
||||
EntryItem(
|
||||
count: user['followers']['totalCount'],
|
||||
text: 'Followers',
|
||||
screenBuilder: (context) =>
|
||||
UsersScreen.followers(user['login']),
|
||||
),
|
||||
EntryItem(
|
||||
count: user['following']['totalCount'],
|
||||
text: 'Following',
|
||||
screenBuilder: (context) =>
|
||||
UsersScreen.following(user['login']),
|
||||
),
|
||||
]
|
||||
]),
|
||||
CommonStyle.verticalGap,
|
||||
if (contributions.isNotEmpty) ...[
|
||||
UserContributions(contributions),
|
||||
CommonStyle.verticalGap,
|
||||
],
|
||||
TableView(
|
||||
hasIcon: true,
|
||||
items: [
|
||||
if (!isOrganization && isNotNullOrEmpty(user['company']))
|
||||
TableViewItem(
|
||||
leftIconData: Octicons.organization,
|
||||
text: TextContainsOrganization(
|
||||
user['company'],
|
||||
style: TextStyle(fontSize: 16, color: theme.palette.text),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (isNotNullOrEmpty(user['location']))
|
||||
TableViewItem(
|
||||
leftIconData: Octicons.location,
|
||||
text: Text(user['location']),
|
||||
onTap: () {
|
||||
launchUrl('https://www.google.com/maps/place/' +
|
||||
(user['location'] as String)
|
||||
.replaceAll(RegExp(r'\s+'), ''));
|
||||
},
|
||||
),
|
||||
if (isNotNullOrEmpty(user['email']))
|
||||
TableViewItem(
|
||||
leftIconData: Octicons.mail,
|
||||
text: Text(user['email']),
|
||||
onTap: () {
|
||||
launchUrl('mailto:' + user['email']);
|
||||
},
|
||||
),
|
||||
if (isNotNullOrEmpty(user['websiteUrl']))
|
||||
TableViewItem(
|
||||
leftIconData: Octicons.link,
|
||||
text: Text(user['websiteUrl']),
|
||||
onTap: () {
|
||||
var url = user['websiteUrl'] as String;
|
||||
if (!url.startsWith('http')) {
|
||||
url = 'http://$url';
|
||||
}
|
||||
launchUrl(url);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
...buildPinnedItems(
|
||||
user['pinnedItems']['nodes'],
|
||||
user[isOrganization ? 'pinnableItems' : 'repositories']
|
||||
['nodes']),
|
||||
CommonStyle.verticalGap,
|
||||
],
|
||||
);
|
||||
final payload = data.item1;
|
||||
switch (payload.resolveType) {
|
||||
case 'User':
|
||||
return _buildUser(context, payload as User, data.item2);
|
||||
case 'Organization':
|
||||
return _buildOrganization(context, payload as Organization);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -178,32 +178,3 @@ launchUrl(String url) async {
|
||||
// TODO: fallback
|
||||
}
|
||||
}
|
||||
|
||||
Iterable<Widget> buildPinnedItems(List pinnedItems, List repositories) {
|
||||
String title;
|
||||
List items = [];
|
||||
|
||||
if (pinnedItems.isNotEmpty) {
|
||||
title = 'pinned repositories';
|
||||
items = pinnedItems;
|
||||
} else if (repositories.isNotEmpty) {
|
||||
title = 'popular repositories';
|
||||
items = repositories;
|
||||
}
|
||||
|
||||
items = items
|
||||
.where((x) => x.isNotEmpty)
|
||||
.toList(); // TODO: Pinned items may include Gist
|
||||
if (items.isEmpty) return [];
|
||||
|
||||
return [
|
||||
CommonStyle.verticalGap,
|
||||
if (title != null) TableViewHeader(title),
|
||||
...join(
|
||||
CommonStyle.border,
|
||||
items.map((item) {
|
||||
return RepositoryItem(item);
|
||||
}).toList(),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
@ -10,7 +10,12 @@ class EntryItem extends StatelessWidget {
|
||||
final WidgetBuilder screenBuilder;
|
||||
final String url;
|
||||
|
||||
EntryItem({this.count, this.text, this.screenBuilder, this.url});
|
||||
EntryItem({
|
||||
@required this.count,
|
||||
@required this.text,
|
||||
this.screenBuilder,
|
||||
this.url,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:git_touch/graphql/gh_user.dart';
|
||||
import 'package:git_touch/models/gitea.dart';
|
||||
import 'package:git_touch/models/gitlab.dart';
|
||||
import 'package:git_touch/models/theme.dart';
|
||||
@ -67,6 +68,23 @@ class RepositoryItem extends StatelessWidget {
|
||||
? []
|
||||
: payload['repositoryTopics']['nodes'];
|
||||
|
||||
RepositoryItem.github(Repository payload, {this.inRepoScreen = false})
|
||||
: this.owner = (payload.owner as User).login,
|
||||
this.avatarUrl = (payload.owner as User).avatarUrl,
|
||||
this.name = payload.name,
|
||||
this.description = payload.description,
|
||||
this.iconData = Octicons.repo, // TODO:
|
||||
this.starCount = payload.stargazers.totalCount,
|
||||
this.forkCount = payload.forks.totalCount,
|
||||
this.primaryLanguageName = payload.primaryLanguage?.name,
|
||||
this.primaryLanguageColor = payload.primaryLanguage?.color,
|
||||
this.screenBuilder = ((_) =>
|
||||
RepositoryScreen((payload.owner as User).login, payload.name)),
|
||||
this.topics = []; // TODO:
|
||||
// this.topics = payload['repositoryTopics'] == null
|
||||
// ? []
|
||||
// : payload['repositoryTopics']['nodes'];
|
||||
|
||||
RepositoryItem.gitlab(GitlabRepository payload, {this.inRepoScreen = false})
|
||||
: this.owner = payload.owner.name,
|
||||
this.avatarUrl = payload.owner.avatarUrl,
|
||||
|
@ -38,7 +38,8 @@ dependencies:
|
||||
fimber: ^0.3.2
|
||||
photo_view: ^0.7.0
|
||||
gql: ^0.11.1
|
||||
artemis: ^2.0.7
|
||||
artemis: ^2.1.2
|
||||
gql_link: ^0.2.0
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
|
Loading…
x
Reference in New Issue
Block a user