add flutter_whatsnew

This commit is contained in:
2026-01-14 01:05:24 -08:00
parent ea97794e19
commit 42350734bb
43 changed files with 60270 additions and 0 deletions
@@ -0,0 +1,5 @@
library flutter_whatsnew;
export 'src/base.dart';
export 'src/changelog.dart';
export 'src/scheduled.dart';
+238
View File
@@ -0,0 +1,238 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'changelog.dart';
class WhatsNewPage extends StatelessWidget {
final Widget title;
final Widget buttonText;
final List<ListTile>? items;
final VoidCallback? onButtonPressed;
final bool changelog;
final String? changes;
final Color? backgroundColor;
final Color? buttonColor;
final String? path;
final MarkdownTapLinkCallback? onTapLink;
final bool adaptive;
const WhatsNewPage({
required this.items,
required this.title,
required this.buttonText,
this.onButtonPressed,
this.backgroundColor,
this.buttonColor,
this.onTapLink,
this.adaptive = true,
}) : changelog = false,
changes = null,
path = null;
const WhatsNewPage.changelog({
this.title = const Text(
'Changelog',
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 22.0,
fontWeight: FontWeight.bold,
),
),
this.buttonText = const Text(
'Continue',
style: const TextStyle(color: Colors.white),
),
this.onButtonPressed,
this.changes,
this.backgroundColor,
this.buttonColor,
this.path,
this.onTapLink,
this.adaptive = true,
}) : changelog = true,
items = null;
static void showDetailPopUp(
BuildContext context,
String title,
String detail,
) async {
void showDemoDialog<T>({
required BuildContext context,
required Widget child,
}) {
showDialog<T>(
context: context,
barrierDismissible: false,
builder: (BuildContext context) => child,
);
}
return showDemoDialog<Null>(
context: context,
child: AlertDialog(
title: Text(title),
content: Text(detail),
actions: <Widget>[
TextButton(
child: Text('OK'),
onPressed: () {
Navigator.pop(context);
},
),
],
),
);
}
@override
Widget build(BuildContext context) {
assert(items != null || changelog);
if (adaptive &&
!kIsWeb &&
(defaultTargetPlatform == TargetPlatform.iOS ||
defaultTargetPlatform == TargetPlatform.macOS)) {
return _buildIOS(context);
}
return _buildAndroid(context);
}
Widget _buildAndroid(BuildContext context) {
final buttonStyle = ElevatedButton.styleFrom(
backgroundColor: buttonColor ?? Theme.of(context).colorScheme.primary,
foregroundColor: buttonColor != null
? (buttonColor!.computeLuminance() > 0.5
? Colors.black
: Colors.white)
: Theme.of(context).colorScheme.onPrimary,
);
if (changelog) {
return Scaffold(
backgroundColor:
backgroundColor ?? Theme.of(context).scaffoldBackgroundColor,
body: SafeArea(
child: Stack(
fit: StackFit.loose,
children: <Widget>[
Positioned(
top: 10.0,
left: 0.0,
right: 0.0,
child: title,
),
Positioned(
left: 0.0,
right: 0.0,
top: 50.0,
bottom: 80.0,
child: ChangeLogView(
changes: changes,
path: path,
onTapLink: onTapLink,
adaptive: adaptive,
),
),
Positioned(
bottom: 5.0,
right: 10.0,
left: 10.0,
child: ListTile(
title: ElevatedButton(
child: buttonText,
style: buttonStyle,
onPressed: onButtonPressed != null
? onButtonPressed
: () {
Navigator.pop(context);
},
),
),
),
],
),
),
);
} else if (items != null) {
return Scaffold(
backgroundColor:
backgroundColor ?? Theme.of(context).scaffoldBackgroundColor,
body: SafeArea(
child: Stack(
fit: StackFit.loose,
children: <Widget>[
Positioned(
top: 10.0,
left: 0.0,
right: 0.0,
child: title,
),
Positioned(
left: 0.0,
right: 0.0,
top: 50.0,
bottom: 80.0,
child: ListView(
children: items!
.map(
(ListTile item) => ListTile(
title: item.title,
subtitle: item.subtitle,
leading: item.leading,
trailing: item.trailing,
onTap: item.onTap,
onLongPress: item.onLongPress,
),
)
.toList(),
),
),
Positioned(
bottom: 5.0,
right: 10.0,
left: 10.0,
child: ListTile(
title: ElevatedButton(
child: buttonText,
style: buttonStyle,
onPressed: onButtonPressed != null
? onButtonPressed
: () => Navigator.pop(context),
),
),
),
],
),
),
);
}
return Container();
}
Widget _buildIOS(BuildContext context) {
Widget? child;
if (changelog) {
child = ChangeLogView(
changes: changes,
path: path,
onTapLink: onTapLink,
adaptive: adaptive,
);
} else if (items != null) {
child = Material(
child: ListView(
children: items!,
),
);
}
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: title,
),
child: SafeArea(
child: child ?? Container(),
),
);
}
}
@@ -0,0 +1,57 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
class ChangeLogView extends StatefulWidget {
const ChangeLogView({
this.onTapLink,
this.path,
this.changes,
required this.adaptive,
});
final String? changes;
final String? path;
final bool adaptive;
final MarkdownTapLinkCallback? onTapLink;
@override
_ChangeLogViewState createState() => _ChangeLogViewState();
}
class _ChangeLogViewState extends State<ChangeLogView> {
String? _changelog;
@override
void initState() {
if (widget.changes == null) {
rootBundle.loadString(widget.path ?? "CHANGELOG.md").then((data) {
setState(() {
_changelog = data;
});
});
} else {
setState(() {
_changelog = widget.changes;
});
}
super.initState();
}
@override
Widget build(BuildContext context) {
if (_changelog == null) {
if (widget.adaptive &&
(defaultTargetPlatform == TargetPlatform.iOS ||
defaultTargetPlatform == TargetPlatform.macOS)) {
return Center(child: CupertinoActivityIndicator());
} else {
return Center(child: CircularProgressIndicator());
}
}
return Markdown(
data: _changelog!,
onTapLink: widget.onTapLink,
);
}
}
@@ -0,0 +1,68 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'base.dart';
class ScheduledWhatsNewPage extends StatefulWidget {
const ScheduledWhatsNewPage({
Key? key,
required this.details,
this.delay,
this.appVersion,
required this.child,
}) : super(key: key);
final WhatsNewPage details;
final Widget child;
/// Wait a set duration and show the dialog
final Duration? delay;
/// Check the last saved version and show the dialog if different (or fresh)
final String? appVersion;
@override
State<ScheduledWhatsNewPage> createState() => _ScheduledWhatsNewPageState();
}
class _ScheduledWhatsNewPageState extends State<ScheduledWhatsNewPage> {
late SharedPreferences prefs;
@override
void initState() {
SharedPreferences.getInstance().then((value) {
prefs = value;
check();
});
super.initState();
}
void check() async {
if (widget.appVersion != null) {
final settingsKey = 'last-app-version';
final lastVersion = prefs.getString(settingsKey);
if (lastVersion == widget.appVersion) {
return;
}
await prefs.setString(settingsKey, widget.appVersion!);
}
if (widget.delay != null) {
await Future<void>.delayed(widget.delay!);
}
show(context);
}
void show(BuildContext context) {
if (!mounted) return;
final nav = Navigator.of(context, rootNavigator: true);
nav.push(MaterialPageRoute(
builder: (context) => widget.details,
fullscreenDialog: true,
));
}
@override
Widget build(BuildContext context) {
return widget.child;
}
}