Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ access.
- Display version information in a simple text widget
- Optionally also display the release date
- Automatic date extraction from CHANGELOG.md files
- Clickable link to view the full CHANGELOG
- Customizable styling
- Clickable to view the full CHANGELOG in an in-app dialogue with markdown rendering
- Customisable styling
- Fallback date support
- Custom tooltip messages
- Visual indicator for outdated version
Expand Down Expand Up @@ -58,15 +58,12 @@ With CHANGELOG support:
```dart
VersionWidget(
version: '1.0.5',
changelogUrl: 'https://raw.githubusercontent.com/anusii/version_wdiget/main/CHANGELOG.md',
changelogUrl: 'https://raw.githubusercontent.com/anusii/version_widget/main/CHANGELOG.md',
showDate: true,
defaultDate: '20240101',
)
```

Note that you should specify the raw domain which may avoid a missing
CORS header issue from github.

With custom tooltip messages:

```dart
Expand Down
221 changes: 198 additions & 23 deletions lib/src/widgets/version_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@
///
/// Authors: Kevin Wang.

import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';

import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:http/http.dart' as http;
import 'package:markdown_tooltip/markdown_tooltip.dart';
import 'package:url_launcher/url_launcher.dart';
Expand Down Expand Up @@ -155,6 +157,10 @@ class _VersionWidgetState extends State<VersionWidget> {
bool _isChecking = true;
bool _hasInternet = true;

/// The full CHANGELOG content for display in the dialogue.

String _changelogContent = '';

@override
void initState() {
super.initState();
Expand All @@ -166,6 +172,23 @@ class _VersionWidgetState extends State<VersionWidget> {
}
}

/// Converts GitHub blob URLs to raw content URLs.
/// This is necessary for CORS compatibility in web environments.
///
/// Converts:
/// - https://github.com/user/repo/blob/branch/file.md
/// to:
/// - https://raw.githubusercontent.com/user/repo/branch/file.md

String _convertToRawUrl(String url) {
if (url.contains('github.com') && url.contains('/blob/')) {
return url
.replaceFirst('github.com', 'raw.githubusercontent.com')
.replaceFirst('/blob/', '/');
}
return url;
}

String _formatDate(String dateStr) {
try {
final year = dateStr.substring(0, 4);
Expand Down Expand Up @@ -197,11 +220,139 @@ class _VersionWidgetState extends State<VersionWidget> {
}
}

/// Displays the CHANGELOG content in a dialogue with markdown rendering.
/// This method is called when the user taps on the version text.

void _showChangelogDialog(BuildContext context) {
if (_changelogContent.isEmpty) {
// Show a message if CHANGELOG content is not available.

showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Changelog'),
content: const Text('Changelog content is not available.'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Close'),
),
],
);
},
);
return;
}

showDialog(
context: context,
builder: (BuildContext context) {
return Dialog(
child: Container(
constraints: BoxConstraints(
maxWidth: 800,
maxHeight: MediaQuery.of(context).size.height * 0.8,
),
child: Column(
children: [
// Title bar with close button.

Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(4),
topRight: Radius.circular(4),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Changelog',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Colors.white,
),
),
IconButton(
icon: const Icon(Icons.close, color: Colors.white),
onPressed: () => Navigator.of(context).pop(),
tooltip: 'Close',
),
],
),
),

// Markdown content.

Expanded(
child: Markdown(
data: _changelogContent,
selectable: true,
onTapLink: (text, href, title) async {
if (href != null) {
final Uri url = Uri.parse(href);
if (await canLaunchUrl(url)) {
await launchUrl(url);
}
}
},
),
),

// Bottom action bar.

Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
border: Border(
top: BorderSide(
color: Theme.of(context).dividerColor,
width: 1,
),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (widget.changelogUrl != null)
TextButton.icon(
icon: const Icon(Icons.open_in_new),
label: const Text('View on GitHub'),
onPressed: () async {
final Uri url = Uri.parse(widget.changelogUrl!);
if (await canLaunchUrl(url)) {
await launchUrl(url);
}
},
),
const SizedBox(width: 8),
FilledButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Close'),
),
],
),
),
],
),
),
);
},
);
}

/// Fetches and parses the changelog file to extract version and date information.
/// The method handles several scenarios:
/// 1. No changelog URL provided: Uses default values
/// 2. Changelog fetch successful: Extracts version and date
/// 3. Changelog fetch failed: Falls back to default values
///
/// For web environments, this method automatically converts GitHub blob URLs
/// to raw.githubusercontent.com URLs to avoid CORS issues.

Future<void> _fetchChangelog() async {
if (widget.changelogUrl == null) {
Expand All @@ -215,18 +366,41 @@ class _VersionWidgetState extends State<VersionWidget> {
}

try {
final response = await http.get(Uri.parse(widget.changelogUrl!));
// Convert GitHub blob URLs to raw URLs for CORS compatibility.

final url = _convertToRawUrl(widget.changelogUrl!);

if (kIsWeb && url != widget.changelogUrl) {
debugPrint(
'Web platform detected: Converting URL from ${widget.changelogUrl} '
'to $url');
}

final response = await http.get(Uri.parse(url));

if (response.statusCode != 200) {
throw Exception('Failed to load changelog: '
'HTTP ${response.statusCode}');
}

final content = response.body;

// Extract all version and date pairs from CHANGELOG.md
// Store the full CHANGELOG content for display in dialogue.

_changelogContent = content;

// Extract all version and date pairs from CHANGELOG.md.

final matches = RegExp(r'\[([\d.]+) (\d{8})').allMatches(content);

if (matches.isNotEmpty) {
// First match is the latest version
// First match is the latest version.

final latestMatch = matches.first;
_latestVersion = latestMatch.group(1)!;

// Find the date for the current version
// Find the date for the current version.

String? currentVersionDate;
for (final match in matches) {
if (match.group(1) == _currentVersion) {
Expand All @@ -253,7 +427,15 @@ class _VersionWidgetState extends State<VersionWidget> {
});
}
} catch (e) {
debugPrint('Error fetching changelog: $e');
if (kIsWeb) {
debugPrint('Error fetching changelog on web platform: $e');
debugPrint('Make sure the CHANGELOG URL uses '
'raw.githubusercontent.com for GitHub files');
debugPrint('Original URL: ${widget.changelogUrl}');
debugPrint('Converted URL: ${_convertToRawUrl(widget.changelogUrl!)}');
} else {
debugPrint('Error fetching changelog: $e');
}
setState(() {
_currentDate = '';
_latestVersion = _currentVersion;
Expand Down Expand Up @@ -282,27 +464,20 @@ class _VersionWidgetState extends State<VersionWidget> {

**Version:** $_currentVersion. According to the CHANGELOG from the app
repository ${_isLatest ? widget.isLatestTooltip ?? defaultLatestTooltip : widget.notLatestTooltip ?? defaultNotLatestTooltip} **Tap** on the
**Version** string to visit the app's CHANGELOG file in your browser.
**Version** string to view the app's CHANGELOG.

''';

return MarkdownTooltip(
message: tooltipMessage,
child: GestureDetector(
onTap: widget.changelogUrl == null
? null
: () async {
final Uri url = Uri.parse(widget.changelogUrl!);
if (await canLaunchUrl(url)) {
await launchUrl(url);
} else {
debugPrint('Could not launch ${widget.changelogUrl}');
}
},
child: MouseRegion(
cursor: widget.changelogUrl == null
? SystemMouseCursors.basic
: SystemMouseCursors.click,
return GestureDetector(
onTap: widget.changelogUrl == null
? null
: () => _showChangelogDialog(context),
child: MouseRegion(
cursor: widget.changelogUrl == null
? SystemMouseCursors.basic
: SystemMouseCursors.click,
child: MarkdownTooltip(
message: tooltipMessage,
child: Text(
displayText,
style: (widget.userTextStyle != null)
Expand Down
3 changes: 2 additions & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: version_widget
description: A Flutter widget that displays version information with optional changelog date and link.
version: 1.0.5
version: 1.1.0
repository: https://github.com/anusii/version_widget
homepage: https://github.com/anusii/version_widget

Expand All @@ -14,6 +14,7 @@ dependencies:
http: ^1.1.0
markdown_tooltip: ^0.0.7
url_launcher: ^6.1.14
flutter_markdown: ^0.7.7+1

dev_dependencies:
flutter_lints: ^2.0.0
Expand Down