GiteaClient/lib/widget/repo_overview.dart
2022-05-22 21:42:57 +02:00

499 lines
16 KiB
Dart

import 'dart:convert';
import 'package:badges/badges.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:gitea_client/cubit/file_list_load_bloc.dart';
import 'package:gitea_client/cubit/issues_bloc.dart';
import 'package:gitea_client/model/issues.dart';
import 'package:gitea_client/model/repository.dart';
import 'package:gitea_client/model/user.dart';
import 'package:gitea_client/service/gitea_service.dart';
import 'package:gitea_client/widget/code_page.dart';
import '../common/l10n.dart';
import '../model/File.dart';
class RepoPage extends StatefulWidget {
final Repository repo;
final SavedUser user;
const RepoPage({Key? key, required this.repo, required this.user})
: super(key: key);
@override
_RepoPage createState() => _RepoPage();
}
class _RepoPage extends State<RepoPage> {
int _currentIndex = 0;
late final List _screens;
@override
void initState() {
_screens = [
RepoHome(
repo: widget.repo,
user: widget.user,
),
RepoFiles(
repo: widget.repo,
user: widget.user,
),
RepoIssues(
repo: widget.repo,
user: widget.user,
),
RepoPullRequests(
repo: widget.repo,
user: widget.user,
)
];
}
void _updateIndex(int value) {
setState(() {
_currentIndex = value;
});
}
@override
Widget build(BuildContext context) {
final l10n = L10n.of(context)!;
return Scaffold(
appBar: AppBar(
title: Text(widget.repo.name),
),
body: _screens[_currentIndex],
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
currentIndex: _currentIndex,
onTap: _updateIndex,
selectedItemColor: Colors.green[700],
selectedFontSize: 13,
unselectedFontSize: 13,
iconSize: 30,
items: [
BottomNavigationBarItem(
label: l10n.home,
icon: const Icon(Icons.home),
),
BottomNavigationBarItem(
label: l10n.files,
icon: const Icon(Icons.folder),
),
if (widget.repo.hasIssues!)
BottomNavigationBarItem(
label: l10n.issues,
icon: (widget.repo.openIssuesCount! > 0)
? Badge(
badgeContent:
Text(widget.repo.openIssuesCount!.toString()),
child: const Icon(Icons.error_outline),
)
: const Icon(Icons.error_outline),
),
if (widget.repo.hasPullRequests!)
BottomNavigationBarItem(
label: l10n.pulls,
icon: (widget.repo.openPrCounter! > 0)
? Badge(
badgeContent: Text(widget.repo.openPrCounter!.toString()),
child: const Icon(Icons.mediation_rounded),
)
: const Icon(Icons.mediation_rounded),
),
],
),
);
}
}
class RepoHome extends StatefulWidget {
final Repository repo;
final SavedUser user;
const RepoHome({Key? key, required this.repo, required this.user})
: super(key: key);
@override
_RepoHome createState() => _RepoHome();
}
class _RepoHome extends State<RepoHome> {
@override
Widget build(BuildContext context) {
final media = MediaQuery.of(context).size;
final padding = MediaQuery.of(context).viewPadding;
final l10n = L10n.of(context)!;
return Column(children: [
/*Text(
widget.repo.fullName!,
style: Theme.of(context).textTheme.headline3,
),*/
FutureBuilder<RepoFile>(
future: readmeRequest,
builder: (context, snapshot) {
if (snapshot.hasError) {
return Center(
child: Column(
children: [
Text(l10n.noreadme,
style: Theme.of(context).textTheme.headline6),
],
),
);
} else if (snapshot.hasData) {
var file = snapshot.data!;
final content = utf8.decode(base64.decode(file.content!));
return Center(
child: SizedBox(
width: (media.width > 600)
? media.width * 0.6
: media.width * 0.9,
height: media.height -
padding.top -
padding.bottom -
kToolbarHeight -
kBottomNavigationBarHeight -
30,
child: Column(
children: [
Expanded(
child: Markdown(selectable: true, data: content)),
],
)),
);
} else {
return const Center(
child: CircularProgressIndicator(),
);
}
})
]);
}
Future<RepoFile>? readmeRequest;
late final GiteaService giteaService;
@override
void initState() {
giteaService = GiteaService(apiAccess: widget.user.apiAccess);
readmeRequest = giteaService.getFile(
widget.repo.owner.username!, widget.repo.name, "README.md");
}
}
class RepoFiles extends StatefulWidget {
final Repository repo;
final SavedUser user;
const RepoFiles({Key? key, required this.repo, required this.user})
: super(key: key);
@override
_RepoFiles createState() => _RepoFiles();
}
class _RepoFiles extends State<RepoFiles> {
String path = "/";
Future<List<RepoFile>>? readmeRequest;
late final GiteaService giteaService;
final _scrollController = ScrollController();
@override
void initState() {
giteaService = GiteaService(apiAccess: widget.user.apiAccess);
readmeRequest = giteaService.getFolder(
widget.repo.owner.username!, widget.repo.name, path);
}
@override
Widget build(BuildContext context) {
final l10n = L10n.of(context)!;
return BlocProvider(
create: (context) {
return FileListLoadBloc(
giteaService: GiteaService(apiAccess: widget.user.apiAccess))
..add(FileListLoadPathEvent("/", widget.repo.fullName!));
},
child: BlocBuilder<FileListLoadBloc, FileListLoadState>(
builder: (context, state) {
switch (state.status) {
case FileLoadStatus.failure:
String error_message = state.error_message!;
return Center(child: Text('failed to fetch $error_message'));
case FileLoadStatus.success:
if (state.files.isEmpty) {
return Center(
child: Text(l10n.nofiles));
}
var files = (state.path == "/")
? []
: [
RepoFile(
name: "..",
path: state.path,
sha: "",
type: "dir",
size: 0,
url: "",
htmlUrl: "",
gitUrl: "",
lLinks: Links(self: "", git: "", html: ""))
];
files.addAll(state.files);
return ListView.builder(
itemBuilder: (BuildContext context, int index) {
return FileListItem(
file: files[index],
onTap: () {
if (files[index].type == "dir") {
String prev = files[index].path.contains("/")
? files[index].path.substring(
0, files[index].path.lastIndexOf('/'))
: "/";
path = (files[index].name == "..")
? prev
: files[index].path;
context.read<FileListLoadBloc>().add(FileListLoadPathEvent(path,widget.repo.fullName!));
} else {
Navigator.of(context).pushNamed("/fileview",
arguments: CodePageData(
files[index].path,
widget.repo.name,
widget.repo.owner.username!,
widget.user));
}
},
);
},
itemCount: files.length,
controller: _scrollController,
);
default:
return const Center(child: CircularProgressIndicator());
}
},
),
);
}
}
class FileListItem extends StatelessWidget {
final RepoFile file;
final Function onTap;
const FileListItem({Key? key, required this.file, required this.onTap})
: super(key: key);
@override
Widget build(BuildContext context) {
return ListTile(
leading: (file.type == "file")
? const Icon(Icons.insert_drive_file_outlined)
: const Icon(Icons.folder_outlined),
title: Text(file.name),
onTap: () => {onTap()},
);
}
}
class RepoIssues extends StatefulWidget {
final Repository repo;
final SavedUser user;
const RepoIssues({Key? key, required this.repo, required this.user})
: super(key: key);
@override
_RepoIssues createState() => _RepoIssues();
}
class _RepoIssues extends State<RepoIssues> {
final _scrollController = ScrollController();
@override
void initState() {
super.initState();
_scrollController.addListener(_onScroll);
}
@override
Widget build(BuildContext context) {
final media = MediaQuery.of(context).size;
final l10n = L10n.of(context)!;
return DefaultTabController(
length: 2,
child: DefaultTabController(
length: 2,
child: Scaffold(
appBar: PreferredSize(
preferredSize: const Size.fromHeight(kToolbarHeight),
child: Container(
color: Colors.lightGreen,
child: SafeArea(
child: Column(
children: <Widget>[
Expanded(child: Container()),
TabBar(
tabs: [Text(l10n.open), Text(l10n.closed)],
),
],
),
),
),
),
body: TabBarView(
children: <Widget>[
Column(
children: [
Center(
child: SizedBox(
width: (media.width > 600) ? media.width * 0.5 : media.width * 0.9,
height: media.height-kToolbarHeight-kBottomNavigationBarHeight-kTextTabBarHeight-50,
child: BlocProvider(
create: (context) {
return IssuesBloc(GiteaService(apiAccess: widget.user.apiAccess),widget.repo.fullName!,"open")
..add(IssuesFetched());
},
child: BlocBuilder<IssuesBloc, IssueState>(
builder: (context, state) {
switch (state.status) {
case IssueStatus.failure:
String error_message = state.error_message!;
return Center(child: Text('failed to fetch $error_message'));
case IssueStatus.success:
if (state.issues.isEmpty) {
return Center(
child: Text(l10n.noissues));
}
return ListView.builder(
itemBuilder: (BuildContext context, int index) {
return IssuesListItem(
issue: state.issues[index],
onTap: () {},
);
},
itemCount: state.issues.length,
controller: _scrollController,
);
default:
return const Center(child: CircularProgressIndicator());
}
},
),
),
),
)
],
),
Column(
children: [
Center(
child: SizedBox(
width: (media.width > 600) ? media.width * 0.5 : media.width * 0.9,
height: media.height-kToolbarHeight-kBottomNavigationBarHeight-kTextTabBarHeight-50,
child: BlocProvider(
create: (context) {
return IssuesBloc(GiteaService(apiAccess: widget.user.apiAccess),widget.repo.fullName!,"closed")
..add(IssuesFetched());
},
child: BlocBuilder<IssuesBloc, IssueState>(
builder: (context, state) {
switch (state.status) {
case IssueStatus.failure:
String error_message = state.error_message!;
return Center(child: Text('failed to fetch $error_message'));
case IssueStatus.success:
if (state.issues.isEmpty) {
return Center(
child: Text(l10n.noissues));
}
return ListView.builder(
itemBuilder: (BuildContext context, int index) {
return IssuesListItem(
issue: state.issues[index],
onTap: () {},
);
},
itemCount: state.issues.length,
controller: _scrollController,
);
default:
return const Center(child: CircularProgressIndicator());
}
},
),
),
),
)
],
)
],
),
),
),
);
}
@override
void dispose() {
_scrollController
..removeListener(_onScroll)
..dispose();
super.dispose();
}
void _onScroll() {
if (_isBottom) context.read<IssuesBloc>().add(IssuesFetched());
}
bool get _isBottom {
if (!_scrollController.hasClients) return false;
final maxScroll = _scrollController.position.maxScrollExtent;
final currentScroll = _scrollController.offset;
return currentScroll >= (maxScroll * 0.9);
}
}
class IssuesListItem extends StatelessWidget {
final Issue issue;
final Function onTap;
const IssuesListItem({Key? key, required this.issue, required this.onTap})
: super(key: key);
@override
Widget build(BuildContext context) {
return ListTile(
leading: Text("#${issue.number!}"),
title: Text(issue.title!),
onTap: () => {onTap()},
);
}
}
class RepoPullRequests extends StatefulWidget {
final Repository repo;
final SavedUser user;
const RepoPullRequests({Key? key, required this.repo, required this.user})
: super(key: key);
@override
_RepoPullRequests createState() => _RepoPullRequests();
}
class _RepoPullRequests extends State<RepoPullRequests> {
@override
Widget build(BuildContext context) {
// TODO: implement build
throw UnimplementedError();
}
}