RELEASE: moved from beta to First stable release.

Check CHANGELOG.md for full changelog
This commit is contained in:
Ujwal
2026-02-27 04:14:40 +05:45
parent eecb823e62
commit 7992d65bc8
64 changed files with 6208 additions and 2752 deletions
@@ -0,0 +1,130 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:url_launcher/url_launcher.dart';
import 'update_checker_service.dart';
class UpdateBanner extends StatefulWidget {
final UpdateInfo updateInfo;
final VoidCallback onDismiss;
const UpdateBanner({
super.key,
required this.updateInfo,
required this.onDismiss,
});
@override
State<UpdateBanner> createState() => _UpdateBannerState();
}
class _UpdateBannerState extends State<UpdateBanner> {
bool _isExpanded = false;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
width: double.infinity,
decoration: BoxDecoration(
color: colorScheme.secondaryContainer,
border: Border(
bottom: BorderSide(
color: colorScheme.outlineVariant,
width: 0.5,
),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Row(
children: [
const Text('🎉', style: TextStyle(fontSize: 16)),
const SizedBox(width: 8),
Expanded(
child: Text(
'FocusGram ${widget.updateInfo.latestVersion} available',
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
),
IconButton(
icon: Icon(
_isExpanded ? Icons.expand_less : Icons.expand_more,
size: 18,
),
onPressed: () {
HapticFeedback.lightImpact();
setState(() => _isExpanded = !_isExpanded);
},
),
IconButton(
icon: const Icon(Icons.close, size: 18),
onPressed: () {
HapticFeedback.lightImpact();
widget.onDismiss();
},
),
],
),
),
if (_isExpanded) ...[
const Divider(height: 1),
Padding(
padding: const EdgeInsets.fromLTRB(12, 8, 12, 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"What's new",
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
_formatReleaseNotes(widget.updateInfo.whatsNew),
style: theme.textTheme.bodySmall,
),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: OutlinedButton(
onPressed: () async {
final uri = Uri.parse(widget.updateInfo.releaseUrl);
await launchUrl(
uri,
mode: LaunchMode.externalApplication,
);
},
child: const Text('Download on GitHub'),
),
),
],
),
),
],
],
),
);
}
String _formatReleaseNotes(String raw) {
var text = raw;
text = text.replaceAll(RegExp(r'#{1,6}\s'), '');
text = text.replaceAll(RegExp(r'\*\*(.*?)\*\*'), r'\1');
text = text.replaceAll(RegExp(r'\*(.*?)\*'), r'\1');
text =
text.replaceAll(RegExp(r'\[([^\]]+)\]\([^)]+\)'), r'\1'); // links -> text
text = text.replaceAll(RegExp(r'`([^`]+)`'), r'\1');
return text.trim();
}
}
@@ -0,0 +1,105 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:package_info_plus/package_info_plus.dart';
import 'package:shared_preferences/shared_preferences.dart';
class UpdateInfo {
final String latestVersion; // e.g. "1.0.0"
final String releaseUrl; // html_url
final String whatsNew; // trimmed body
final bool isUpdateAvailable;
const UpdateInfo({
required this.latestVersion,
required this.releaseUrl,
required this.whatsNew,
required this.isUpdateAvailable,
});
}
class UpdateCheckerService extends ChangeNotifier {
static const String _lastDismissedKey = 'last_dismissed_update_version';
static const String _githubUrl =
'https://api.github.com/repos/Ujwal223/FocusGram/releases/latest';
UpdateInfo? _updateInfo;
bool _isDismissed = false;
bool get hasUpdate => _updateInfo != null && !_isDismissed;
UpdateInfo? get updateInfo => hasUpdate ? _updateInfo : null;
Future<void> checkForUpdates() async {
try {
final response = await http
.get(Uri.parse(_githubUrl))
.timeout(const Duration(seconds: 5));
if (response.statusCode != 200) return;
final data = json.decode(response.body);
final String gitVersionTag =
data['tag_name'] ?? ''; // e.g. "v0.9.8-beta.2"
final String htmlUrl = data['html_url'] ?? '';
final String body = (data['body'] as String?) ?? '';
if (gitVersionTag.isEmpty || htmlUrl.isEmpty) return;
final packageInfo = await PackageInfo.fromPlatform();
final currentVersion = packageInfo.version; // e.g. "0.9.8-beta.2"
if (!_isNewerVersion(gitVersionTag, currentVersion)) return;
final prefs = await SharedPreferences.getInstance();
final dismissedVersion = prefs.getString(_lastDismissedKey);
if (dismissedVersion == gitVersionTag) {
_isDismissed = true;
return;
}
final cleanVersion =
gitVersionTag.startsWith('v') ? gitVersionTag.substring(1) : gitVersionTag;
var trimmed = body.trim();
if (trimmed.length > 1500) {
trimmed = trimmed.substring(0, 1500).trim();
}
_updateInfo = UpdateInfo(
latestVersion: cleanVersion,
releaseUrl: htmlUrl,
whatsNew: trimmed,
isUpdateAvailable: true,
);
_isDismissed = false;
notifyListeners();
} catch (e) {
debugPrint('Update check failed: $e');
}
}
Future<void> dismissUpdate() async {
if (_updateInfo == null) return;
_isDismissed = true;
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_lastDismissedKey, _updateInfo!.latestVersion);
notifyListeners();
}
bool _isNewerVersion(String gitTag, String current) {
// Clean versions: strip 'v' and everything after '-' (beta/rc)
String cleanGit = gitTag.startsWith('v') ? gitTag.substring(1) : gitTag;
String cleanCurrent = current;
List<String> gitParts = cleanGit.split('-')[0].split('.');
List<String> currentParts = cleanCurrent.split('-')[0].split('.');
for (int i = 0; i < gitParts.length && i < currentParts.length; i++) {
int gitNum = int.tryParse(gitParts[i]) ?? 0;
int curNum = int.tryParse(currentParts[i]) ?? 0;
if (gitNum > curNum) return true;
if (gitNum < curNum) return false;
}
return false;
}
}