Browse Source

Add about page, in-app changelog, Android download

master v2.7.5
Kenneth Bruen 2 years ago
parent
commit
bf654ea837
Signed by: kbruen
GPG Key ID: C1980A470C3EE5B1
  1. 6
      .vscode/launch.json
  2. 5
      CHANGELOG.txt
  3. 16
      lib/api/releases.dart
  4. 26
      lib/main.dart
  5. 75
      lib/models/changelog_entry.dart
  6. 75
      lib/pages/about/about_page.dart
  7. 127
      lib/pages/about/about_page_cupertino.dart
  8. 112
      lib/pages/about/about_page_material.dart
  9. 25
      lib/pages/main/main_page.dart
  10. 26
      lib/pages/main/main_page_cupertino.dart
  11. 13
      lib/pages/main/main_page_material.dart
  12. 6
      lib/utils/iterable_extensions.dart
  13. 4
      linux/flutter/generated_plugin_registrant.cc
  14. 1
      linux/flutter/generated_plugins.cmake
  15. 129
      pubspec.lock
  16. 11
      pubspec.yaml

6
.vscode/launch.json vendored

@ -7,7 +7,11 @@
{
"name": "Current Device",
"request": "launch",
"type": "dart"
"type": "dart",
"args": [
"--dart-define",
"DOWNLOAD=apk"
]
},
{
"name": "info_tren (profile mode)",

5
CHANGELOG.TXT → CHANGELOG.txt

@ -1,3 +1,7 @@
v2.7.5
Added about page and in-app changelog.
On Android, added download buttons.
v2.7.4
Addressed Android 12 component exporting rule.
See: https://developer.android.com/about/versions/12/behavior-changes-12#exported
@ -51,7 +55,6 @@ Tweaks
v2.0.6
Brought feature parity with iOS _(except for v2.0.2, which is iOS specific)_.
v2.0.5
- increased font weight on iOS
- added support for system bolt font request on iOS

16
lib/api/releases.dart

@ -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();
}

26
lib/main.dart

@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
// import 'package:flutter_redux/flutter_redux.dart';
import 'package:info_tren/models/ui_design.dart';
import 'package:info_tren/pages/about/about_page.dart';
import 'package:info_tren/pages/main/main_page.dart';
import 'package:info_tren/pages/station_arrdep_page/select_station/select_station.dart';
import 'package:info_tren/pages/station_arrdep_page/view_station/view_station.dart';
@ -28,10 +29,19 @@ void main() {
Map<String, WidgetBuilder> routesByUiDesign(UiDesign uiDesign) => {
Navigator.defaultRouteName: (context) {
return MainPage(uiDesign: uiDesign,);
return MainPage(
uiDesign: uiDesign,
);
},
AboutPage.routeName: (context) {
return AboutPage(
uiDesign: uiDesign,
);
},
SelectTrainPage.routeName: (context) {
return SelectTrainPage(uiDesign: uiDesign);
return SelectTrainPage(
uiDesign: uiDesign,
);
},
TrainInfo.routeName: (context) {
return TrainInfo(
@ -40,7 +50,9 @@ Map<String, WidgetBuilder> routesByUiDesign(UiDesign uiDesign) => {
);
},
SelectStationPage.routeName: (context) {
return SelectStationPage(uiDesign: uiDesign,);
return SelectStationPage(
uiDesign: uiDesign,
);
},
ViewStationPage.routeName: (context) {
return ViewStationPage(
@ -80,9 +92,11 @@ class StartPoint extends StatelessWidget {
title: appTitle,
theme: ThemeData(
primarySwatch: Colors.blue,
brightness: Brightness.dark,
primaryColor: Colors.blue.shade600,
accentColor: Colors.blue.shade700,
colorScheme: ColorScheme.fromSwatch(
brightness: Brightness.dark,
primarySwatch: Colors.blue,
accentColor: Colors.blue.shade700,
),
// fontFamily: 'Atkinson Hyperlegible',
),
routes: routesByUiDesign(UiDesign.MATERIAL),

75
lib/models/changelog_entry.dart

@ -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);
}
}

75
lib/pages/about/about_page.dart

@ -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();
}
}

127
lib/pages/about/about_page_cupertino.dart

@ -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(),
],
],
),
);
}
),
);
}
}

112
lib/pages/about/about_page_material.dart

@ -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,
),
),
),
],
],
),
),
);
}
}

25
lib/pages/main/main_page.dart

@ -1,5 +1,6 @@
import 'package:flutter/widgets.dart';
import 'package:info_tren/models/ui_design.dart';
import 'package:info_tren/pages/about/about_page.dart';
import 'package:info_tren/pages/main/main_page_cupertino.dart';
import 'package:info_tren/pages/main/main_page_material.dart';
import 'package:info_tren/pages/station_arrdep_page/select_station/select_station.dart';
@ -28,21 +29,31 @@ class MainPage extends StatelessWidget {
abstract class MainPageShared extends StatelessWidget {
final String pageTitle = 'Info Tren';
final String moreOptionsText = 'Mai multe opțiuni';
List<MainPageOption> get options => [
MainPageOption(
List<MainPageAction> get popupMenu => [
MainPageAction(
name: 'Despre aplicație',
action: (context) {
Navigator.of(context).pushNamed(AboutPage.routeName);
},
),
];
List<MainPageAction> get options => [
MainPageAction(
name: 'Informații despre tren',
action: (BuildContext context) {
action: (context) {
onTrainInfoPageInvoke(context);
},
),
MainPageOption(
MainPageAction(
name: 'Tabelă plecari/sosiri',
action: (context) {
onStationBoardPageInvoke(context);
},
),
MainPageOption(
MainPageAction(
name: 'Planificare rută',
// TODO: Implement route planning
action: null,
@ -62,9 +73,9 @@ abstract class MainPageShared extends StatelessWidget {
}
}
class MainPageOption {
class MainPageAction {
final String name;
final void Function(BuildContext context)? action;
MainPageOption({required this.name, this.action});
MainPageAction({required this.name, this.action});
}

26
lib/pages/main/main_page_cupertino.dart

@ -7,6 +7,32 @@ class MainPageCupertino extends MainPageShared {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text(pageTitle),
trailing: CupertinoButton(
child: Icon(CupertinoIcons.ellipsis_circle),
padding: EdgeInsets.zero,
onPressed: () {
showCupertinoModalPopup(
context: context,
builder: (context) {
return CupertinoActionSheet(
actions: popupMenu.map((m) => CupertinoActionSheetAction(
onPressed: () {
Navigator.of(context).pop();
m.action?.call(context);
},
child: Text(m.name),
)).toList(),
cancelButton: CupertinoActionSheetAction(
onPressed: () {
Navigator.of(context).pop();
},
child: Text('Anulare'),
),
);
},
);
},
),
),
child: SafeArea(
child: Center(

13
lib/pages/main/main_page_material.dart

@ -8,6 +8,19 @@ class MainPageMaterial extends MainPageShared {
appBar: AppBar(
title: Text(pageTitle),
centerTitle: true,
actions: [
PopupMenuButton<int>(
icon: Icon(Icons.more_vert),
tooltip: moreOptionsText,
itemBuilder: (_) => popupMenu.asMap().entries.map((e) => PopupMenuItem(
child: Text(e.value.name),
value: e.key,
)).toList(),
onSelected: (index) {
popupMenu[index].action?.call(context);
},
),
],
),
body: SafeArea(
child: Center(

6
lib/utils/iterable_extensions.dart

@ -0,0 +1,6 @@
extension ITerableExtensions<T> on Iterable<T> {
T? get firstOrNull {
final lst = take(1).toList();
return lst.isEmpty ? null : lst.first;
}
}

4
linux/flutter/generated_plugin_registrant.cc

@ -6,6 +6,10 @@
#include "generated_plugin_registrant.h"
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
}

1
linux/flutter/generated_plugins.cmake

@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
url_launcher_linux
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST

129
pubspec.lock

@ -140,7 +140,7 @@ packages:
name: cupertino_icons
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.3"
version: "1.0.5"
dart_style:
dependency: transitive
description:
@ -148,6 +148,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.1"
ffi:
dependency: transitive
description:
name: ffi
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
file:
dependency: transitive
description:
@ -174,6 +181,11 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.8.2"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
frontend_server_client:
dependency: transitive
description:
@ -286,6 +298,48 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
package_info_plus:
dependency: "direct main"
description:
name: package_info_plus
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.3"
package_info_plus_linux:
dependency: transitive
description:
name: package_info_plus_linux
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.5"
package_info_plus_macos:
dependency: transitive
description:
name: package_info_plus_macos
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0"
package_info_plus_platform_interface:
dependency: transitive
description:
name: package_info_plus_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
package_info_plus_web:
dependency: transitive
description:
name: package_info_plus_web
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.5"
package_info_plus_windows:
dependency: transitive
description:
name: package_info_plus_windows
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
path:
dependency: transitive
description:
@ -293,6 +347,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.2"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.2"
pool:
dependency: transitive
description:
@ -315,7 +376,7 @@ packages:
source: hosted
version: "1.1.0"
quiver:
dependency: transitive
dependency: "direct main"
description:
name: quiver
url: "https://pub.dartlang.org"
@ -438,6 +499,62 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.1"
url_launcher:
dependency: "direct main"
description:
name: url_launcher
url: "https://pub.dartlang.org"
source: hosted
version: "6.1.5"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.17"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.17"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.12"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
vector_math:
dependency: transitive
description:
@ -459,6 +576,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.0"
win32:
dependency: transitive
description:
name: win32
url: "https://pub.dartlang.org"
source: hosted
version: "2.7.0"
yaml:
dependency: transitive
description:
@ -468,3 +592,4 @@ packages:
version: "3.1.1"
sdks:
dart: ">=2.17.0 <3.0.0"
flutter: ">=2.10.0"

11
pubspec.yaml

@ -14,7 +14,7 @@ description: O aplicație de vizualizare a datelor puse la dispoziție de Inform
version: 2.7.4
environment:
sdk: ">=2.12.0 <3.0.0"
sdk: ">=2.15.0 <3.0.0"
dependencies:
flutter:
@ -22,13 +22,15 @@ dependencies:
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
# cupertino_icons: ^0.1.2
cupertino_icons: ^1.0.5
rxdart: ^0.22.0
http: ^0.13.0
cupertino_icons: ^0.1.2
tuple: ^2.0.0
sprintf: ^6.0.0
flutter_redux: ^0.8.2
package_info_plus: ^1.4.3
quiver: ^3.1.0
url_launcher: ^6.1.5
dev_dependencies:
# flutter_test:
@ -49,7 +51,8 @@ flutter:
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
assets:
- CHANGELOG.txt
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg

Loading…
Cancel
Save