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