Kenneth Bruen
2 years ago
16 changed files with 636 additions and 21 deletions
@ -0,0 +1,16 @@
|
||||
import 'dart:convert'; |
||||
|
||||
import 'package:http/http.dart' as http; |
||||
import 'package:info_tren/models/changelog_entry.dart'; |
||||
import 'package:info_tren/utils/iterable_extensions.dart'; |
||||
|
||||
Future<List<ChangelogEntry>> getRemoteReleases() async { |
||||
final Uri uri = Uri.parse('https://gitea.dcdev.ro/api/v1/repos/kbruen/info_tren/releases'); |
||||
final response = await http.get(uri); |
||||
final json = jsonDecode(response.body) as List<dynamic>; |
||||
return json.map((e) => ChangelogEntry( |
||||
version: ChangelogVersion.parse(e['tag_name']), |
||||
description: e['body'], |
||||
apkLink: (e['assets'] as List<dynamic>).where((e) => (e['name'] as String).contains('.apk')).map((e) => Uri.parse(e['browser_download_url'] as String)).firstOrNull, |
||||
)).toList(); |
||||
} |
@ -0,0 +1,75 @@
|
||||
import 'package:quiver/core.dart'; |
||||
|
||||
class ChangelogEntry { |
||||
final ChangelogVersion version; |
||||
final String description; |
||||
final Uri? apkLink; |
||||
|
||||
ChangelogEntry({required this.version, required this.description, this.apkLink}); |
||||
|
||||
factory ChangelogEntry.fromTextBlock(String text) { |
||||
final lines = text.split(RegExp(r'(\r?\n)+')); |
||||
return ChangelogEntry( |
||||
version: ChangelogVersion.parse(lines.first), |
||||
description: lines.skip(1).join('\n'), |
||||
); |
||||
} |
||||
|
||||
static List<ChangelogEntry> fromTextFile(String text) { |
||||
final blocks = text.split(RegExp(r'(\r?\n){2}')); |
||||
return blocks.map(ChangelogEntry.fromTextBlock).toList(); |
||||
} |
||||
} |
||||
|
||||
class ChangelogVersion implements Comparable<ChangelogVersion> { |
||||
final int major; |
||||
final int minor; |
||||
final int patch; |
||||
final String? prerelease; |
||||
|
||||
ChangelogVersion(this.major, this.minor, this.patch, [this.prerelease]); |
||||
|
||||
factory ChangelogVersion.parse(String version) { |
||||
if (version.startsWith('v')) { |
||||
version = version.substring(1); |
||||
} |
||||
String? prerelease; |
||||
if (version.contains('-')) { |
||||
final index = version.indexOf('-'); |
||||
prerelease = version.substring(index + 1); |
||||
version = version.substring(0, index); |
||||
} |
||||
final splitted = version.split('.').map(int.parse).toList(); |
||||
return ChangelogVersion(splitted[0], splitted[1], splitted[2], prerelease); |
||||
} |
||||
|
||||
@override |
||||
String toString() { |
||||
final vString = 'v$major.$minor.$patch'; |
||||
return prerelease == null ? vString : '$vString-$prerelease'; |
||||
} |
||||
|
||||
@override |
||||
bool operator==(dynamic other) { |
||||
if (other is! ChangelogVersion) { |
||||
return false; |
||||
} |
||||
return major == other.major && minor == other.minor && patch == other.patch && prerelease == other.prerelease; |
||||
} |
||||
|
||||
@override |
||||
int get hashCode { |
||||
return hash3(major.hashCode, minor.hashCode, patch.hashCode); |
||||
} |
||||
|
||||
@override |
||||
int compareTo(ChangelogVersion other) { |
||||
if (major != other.major) { |
||||
return major.compareTo(other.major); |
||||
} |
||||
if (minor != other.minor) { |
||||
return minor.compareTo(other.minor); |
||||
} |
||||
return patch.compareTo(other.patch); |
||||
} |
||||
} |
@ -0,0 +1,75 @@
|
||||
import 'package:flutter/widgets.dart'; |
||||
import 'package:info_tren/api/releases.dart'; |
||||
import 'package:info_tren/models/changelog_entry.dart'; |
||||
import 'package:info_tren/models/ui_design.dart'; |
||||
import 'package:info_tren/pages/about/about_page_cupertino.dart'; |
||||
import 'package:info_tren/pages/about/about_page_material.dart'; |
||||
import 'package:info_tren/utils/default_ui_design.dart'; |
||||
import 'package:package_info_plus/package_info_plus.dart'; |
||||
|
||||
class AboutPage extends StatefulWidget { |
||||
final UiDesign? uiDesign; |
||||
|
||||
const AboutPage({Key? key, this.uiDesign}) : super(key: key); |
||||
|
||||
static String routeName = '/about'; |
||||
|
||||
@override |
||||
State<StatefulWidget> createState() { |
||||
final uiDesign = this.uiDesign ?? defaultUiDesign; |
||||
|
||||
switch (uiDesign) { |
||||
case UiDesign.MATERIAL: |
||||
return AboutPageStateMaterial(); |
||||
case UiDesign.CUPERTINO: |
||||
return AboutPageStateCupertino(); |
||||
default: |
||||
throw UnmatchedUiDesignException(uiDesign); |
||||
} |
||||
} |
||||
} |
||||
|
||||
abstract class AboutPageState extends State<AboutPage> { |
||||
static const String DOWNLOAD = String.fromEnvironment('DOWNLOAD'); |
||||
|
||||
final String pageTitle = 'Despre aplicație'; |
||||
final String versionTitleText = 'Versiunea aplicației'; |
||||
final String latestVersionText = 'Cea mai recentă versiune'; |
||||
final String currentVersionText = 'Versiunea curentă'; |
||||
|
||||
List<ChangelogEntry> localChangelog = []; |
||||
List<ChangelogEntry> remoteChangelog = []; |
||||
List<ChangelogEntry> get mergedChangelogs { |
||||
final logs = remoteChangelog.toList(); |
||||
final versions = logs.map((log) => log.version).toSet(); |
||||
for (final log in localChangelog) { |
||||
if (!versions.contains(log.version)) { |
||||
logs.add(log); |
||||
versions.add(log.version); |
||||
} |
||||
} |
||||
logs.sort((l1, l2) => l2.version.compareTo(l1.version)); |
||||
return logs; |
||||
} |
||||
|
||||
PackageInfo? packageInfo; |
||||
|
||||
@override |
||||
void didChangeDependencies() { |
||||
PackageInfo.fromPlatform().then((value) => setState(() { |
||||
packageInfo = value; |
||||
})); |
||||
|
||||
DefaultAssetBundle.of(context).loadString('CHANGELOG.txt').then((value) { |
||||
setState(() { |
||||
localChangelog = ChangelogEntry.fromTextFile(value); |
||||
}); |
||||
}); |
||||
|
||||
getRemoteReleases().then((value) => setState(() { |
||||
remoteChangelog = value; |
||||
})); |
||||
|
||||
super.didChangeDependencies(); |
||||
} |
||||
} |
@ -0,0 +1,127 @@
|
||||
import 'package:flutter/cupertino.dart'; |
||||
import 'package:info_tren/components/cupertino_divider.dart'; |
||||
import 'package:info_tren/pages/about/about_page.dart'; |
||||
import 'package:url_launcher/url_launcher.dart'; |
||||
|
||||
class AboutPageStateCupertino extends AboutPageState { |
||||
@override |
||||
Widget build(BuildContext context) { |
||||
return CupertinoPageScaffold( |
||||
navigationBar: CupertinoNavigationBar( |
||||
middle: Text(pageTitle), |
||||
), |
||||
child: Builder( |
||||
builder: (context) { |
||||
final topPadding = MediaQuery.of(context).padding.top; |
||||
return SingleChildScrollView( |
||||
child: Column( |
||||
mainAxisSize: MainAxisSize.min, |
||||
crossAxisAlignment: CrossAxisAlignment.stretch, |
||||
children: [ |
||||
SizedBox( |
||||
height: topPadding, |
||||
), |
||||
Center( |
||||
child: Text( |
||||
'Info Tren', |
||||
style: CupertinoTheme.of(context).textTheme.navLargeTitleTextStyle, |
||||
), |
||||
), |
||||
if (packageInfo != null) |
||||
Center( |
||||
child: Text( |
||||
packageInfo!.packageName, |
||||
style: TextStyle( |
||||
inherit: true, |
||||
fontSize: 14, |
||||
), |
||||
), |
||||
), |
||||
Padding( |
||||
padding: const EdgeInsets.symmetric(vertical: 8.0), |
||||
child: CupertinoDivider(), |
||||
), |
||||
for (final log in mergedChangelogs) ...[ |
||||
Padding( |
||||
padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), |
||||
child: Row( |
||||
crossAxisAlignment: CrossAxisAlignment.center, |
||||
children: [ |
||||
Expanded( |
||||
child: Text( |
||||
log.version.toString(), |
||||
style: TextStyle( |
||||
inherit: true, |
||||
fontSize: 24, |
||||
), |
||||
), |
||||
), |
||||
if (localChangelog.isNotEmpty && log.version == localChangelog.first.version) |
||||
Container( |
||||
decoration: BoxDecoration( |
||||
border: Border.all( |
||||
color: CupertinoTheme.of(context).textTheme.textStyle.color ?? CupertinoColors.inactiveGray, |
||||
width: 1, |
||||
), |
||||
borderRadius: BorderRadius.circular(20), |
||||
), |
||||
child: Padding( |
||||
padding: const EdgeInsets.all(4), |
||||
child: Text( |
||||
currentVersionText, |
||||
style: TextStyle( |
||||
inherit: true, |
||||
), |
||||
), |
||||
), |
||||
), |
||||
if (remoteChangelog.isNotEmpty && log.version == remoteChangelog.first.version && (localChangelog.isEmpty || localChangelog.first.version != log.version)) |
||||
Container( |
||||
decoration: BoxDecoration( |
||||
border: Border.all( |
||||
color: CupertinoColors.activeGreen, |
||||
width: 1, |
||||
), |
||||
borderRadius: BorderRadius.circular(20), |
||||
), |
||||
child: Padding( |
||||
padding: const EdgeInsets.all(4), |
||||
child: Text( |
||||
latestVersionText, |
||||
style: TextStyle( |
||||
inherit: true, |
||||
color: CupertinoColors.activeGreen, |
||||
), |
||||
), |
||||
), |
||||
), |
||||
if (AboutPageState.DOWNLOAD == 'apk' && log.apkLink != null) |
||||
CupertinoButton( |
||||
padding: EdgeInsets.all(4), |
||||
minSize: 0, |
||||
onPressed: () { |
||||
launchUrl(log.apkLink!); |
||||
}, |
||||
child: Icon(CupertinoIcons.arrow_down_circle), |
||||
), |
||||
], |
||||
), |
||||
), |
||||
Padding( |
||||
padding: const EdgeInsets.all(8.0), |
||||
child: RichText( |
||||
text: TextSpan( |
||||
text: log.description, |
||||
), |
||||
), |
||||
), |
||||
CupertinoDivider(), |
||||
], |
||||
], |
||||
), |
||||
); |
||||
} |
||||
), |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,112 @@
|
||||
import 'package:flutter/material.dart'; |
||||
import 'package:info_tren/pages/about/about_page.dart'; |
||||
import 'package:url_launcher/url_launcher.dart'; |
||||
|
||||
class AboutPageStateMaterial extends AboutPageState { |
||||
@override |
||||
Widget build(BuildContext context) { |
||||
return Scaffold( |
||||
appBar: AppBar( |
||||
title: Text(pageTitle), |
||||
centerTitle: true, |
||||
), |
||||
body: SingleChildScrollView( |
||||
child: Column( |
||||
mainAxisSize: MainAxisSize.min, |
||||
crossAxisAlignment: CrossAxisAlignment.stretch, |
||||
children: [ |
||||
Center( |
||||
child: Text( |
||||
'Info Tren', |
||||
style: Theme.of(context).textTheme.displayMedium, |
||||
), |
||||
), |
||||
if (packageInfo != null) |
||||
Center( |
||||
child: Text( |
||||
packageInfo!.packageName, |
||||
style: Theme.of(context).textTheme.caption, |
||||
), |
||||
), |
||||
// ListTile( |
||||
// title: Text(versionTitleText), |
||||
// subtitle: localChangelog.isEmpty ? null : Text(localChangelog.first.title), |
||||
// ), |
||||
Divider(), |
||||
for (final log in mergedChangelogs) ...[ |
||||
Padding( |
||||
padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), |
||||
child: Row( |
||||
crossAxisAlignment: CrossAxisAlignment.center, |
||||
children: [ |
||||
Expanded( |
||||
child: Text( |
||||
log.version.toString(), |
||||
style: Theme.of(context).textTheme.headline4, |
||||
), |
||||
), |
||||
if (localChangelog.isNotEmpty && log.version == localChangelog.first.version) |
||||
Container( |
||||
decoration: BoxDecoration( |
||||
border: Border.all( |
||||
color: Theme.of(context).colorScheme.onBackground, |
||||
width: 1, |
||||
), |
||||
borderRadius: BorderRadius.circular(20), |
||||
), |
||||
child: Padding( |
||||
padding: const EdgeInsets.all(4), |
||||
child: Text( |
||||
currentVersionText, |
||||
style: TextStyle( |
||||
inherit: true, |
||||
), |
||||
), |
||||
), |
||||
), |
||||
if (remoteChangelog.isNotEmpty && log.version == remoteChangelog.first.version && (localChangelog.isEmpty || localChangelog.first.version != log.version)) |
||||
Container( |
||||
decoration: BoxDecoration( |
||||
border: Border.all( |
||||
color: Colors.green, |
||||
width: 1, |
||||
), |
||||
borderRadius: BorderRadius.circular(20), |
||||
), |
||||
child: Padding( |
||||
padding: const EdgeInsets.all(4), |
||||
child: Text( |
||||
latestVersionText, |
||||
style: TextStyle( |
||||
inherit: true, |
||||
color: Colors.green, |
||||
), |
||||
), |
||||
), |
||||
), |
||||
if (AboutPageState.DOWNLOAD == 'apk' && log.apkLink != null) |
||||
IconButton( |
||||
onPressed: () { |
||||
launchUrl(log.apkLink!); |
||||
}, |
||||
icon: Icon(Icons.download), |
||||
tooltip: 'Download APK', |
||||
), |
||||
], |
||||
), |
||||
), |
||||
Padding( |
||||
padding: const EdgeInsets.all(8.0), |
||||
child: RichText( |
||||
text: TextSpan( |
||||
text: log.description, |
||||
), |
||||
), |
||||
), |
||||
], |
||||
], |
||||
), |
||||
), |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,6 @@
|
||||
extension ITerableExtensions<T> on Iterable<T> { |
||||
T? get firstOrNull { |
||||
final lst = take(1).toList(); |
||||
return lst.isEmpty ? null : lst.first; |
||||
} |
||||
} |
Loading…
Reference in new issue