feat: update checks
This commit is contained in:
@@ -58,7 +58,7 @@ jobs:
|
||||
EOF
|
||||
|
||||
- name: Build APK
|
||||
run: flutter build apk --release
|
||||
run: flutter build apk --release --dart-define=ENABLE_UPDATE_CHECK=true
|
||||
|
||||
- name: Clean up keystore
|
||||
if: always()
|
||||
@@ -118,7 +118,7 @@ jobs:
|
||||
run: flutter pub get
|
||||
|
||||
- name: Build iOS (no codesign)
|
||||
run: flutter build ios --release --no-codesign
|
||||
run: flutter build ios --release --no-codesign --dart-define=ENABLE_UPDATE_CHECK=true
|
||||
|
||||
- name: Create IPA
|
||||
run: |
|
||||
@@ -176,7 +176,7 @@ jobs:
|
||||
run: flutter pub get
|
||||
|
||||
- name: Build macOS
|
||||
run: flutter build macos --release
|
||||
run: flutter build macos --release --dart-define=ENABLE_UPDATE_CHECK=true
|
||||
|
||||
- name: Import Code Signing Certificate
|
||||
env:
|
||||
@@ -298,7 +298,7 @@ jobs:
|
||||
run: flutter pub get
|
||||
|
||||
- name: Build Windows
|
||||
run: flutter build windows --release
|
||||
run: flutter build windows --release --dart-define=ENABLE_UPDATE_CHECK=true
|
||||
|
||||
- name: Build Windows Installer
|
||||
run: .\windows\build-installer.ps1
|
||||
@@ -361,7 +361,7 @@ jobs:
|
||||
run: flutter pub get
|
||||
|
||||
- name: Build Linux
|
||||
run: flutter build linux --release
|
||||
run: flutter build linux --release --dart-define=ENABLE_UPDATE_CHECK=true
|
||||
|
||||
- name: Create Archive
|
||||
run: |
|
||||
|
||||
+79
-1
@@ -3,6 +3,7 @@ import 'dart:io' show Platform;
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'screens/main_screen.dart';
|
||||
import 'screens/auth_screen.dart';
|
||||
import 'services/storage_service.dart';
|
||||
@@ -10,6 +11,7 @@ import 'services/plex_auth_service.dart';
|
||||
import 'services/server_connection_service.dart';
|
||||
import 'services/macos_titlebar_service.dart';
|
||||
import 'services/fullscreen_state_manager.dart';
|
||||
import 'services/update_service.dart';
|
||||
import 'providers/user_profile_provider.dart';
|
||||
import 'providers/plex_client_provider.dart';
|
||||
import 'providers/theme_provider.dart';
|
||||
@@ -126,6 +128,78 @@ class _SetupScreenState extends State<SetupScreen> {
|
||||
_loadSavedCredentials();
|
||||
}
|
||||
|
||||
void _checkForUpdatesOnStartup() async {
|
||||
// Delay slightly to allow UI to settle
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
try {
|
||||
final updateInfo = await UpdateService.checkForUpdatesOnStartup();
|
||||
|
||||
if (updateInfo != null && updateInfo['hasUpdate'] == true && mounted) {
|
||||
_showUpdateDialog(updateInfo);
|
||||
}
|
||||
} catch (e) {
|
||||
appLogger.e('Error checking for updates', error: e);
|
||||
}
|
||||
}
|
||||
|
||||
void _showUpdateDialog(Map<String, dynamic> updateInfo) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext dialogContext) {
|
||||
return AlertDialog(
|
||||
title: const Text('Update Available'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Version ${updateInfo['latestVersion']} is available',
|
||||
style: Theme.of(dialogContext).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Current: ${updateInfo['currentVersion']}',
|
||||
style: Theme.of(dialogContext).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(dialogContext);
|
||||
},
|
||||
child: const Text('Later'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await UpdateService.skipVersion(updateInfo['latestVersion']);
|
||||
if (dialogContext.mounted) {
|
||||
Navigator.pop(dialogContext);
|
||||
}
|
||||
},
|
||||
child: const Text('Skip This Version'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () async {
|
||||
final url = Uri.parse(updateInfo['releaseUrl']);
|
||||
if (await canLaunchUrl(url)) {
|
||||
await launchUrl(url, mode: LaunchMode.externalApplication);
|
||||
}
|
||||
if (dialogContext.mounted) {
|
||||
Navigator.pop(dialogContext);
|
||||
}
|
||||
},
|
||||
child: const Text('View Release'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _loadSavedCredentials() async {
|
||||
final storage = await StorageService.getInstance();
|
||||
|
||||
@@ -173,10 +247,14 @@ class _SetupScreenState extends State<SetupScreen> {
|
||||
|
||||
// Handle result
|
||||
if (result.isSuccess) {
|
||||
// Success! Set client in provider and navigate to main screen
|
||||
// Success! Set client in provider
|
||||
if (mounted) {
|
||||
context.plexClient.setClient(result.client!);
|
||||
|
||||
// Check for updates BEFORE navigation to keep context valid
|
||||
_checkForUpdatesOnStartup();
|
||||
|
||||
// Navigate to main screen after update check is initiated
|
||||
if (mounted) {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import '../providers/theme_provider.dart';
|
||||
import '../providers/settings_provider.dart';
|
||||
import '../services/settings_service.dart' as settings;
|
||||
import '../services/keyboard_shortcuts_service.dart';
|
||||
import '../services/update_service.dart';
|
||||
import '../widgets/desktop_app_bar.dart';
|
||||
import '../widgets/hotkey_recorder_widget.dart';
|
||||
import 'about_screen.dart';
|
||||
@@ -27,6 +29,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
int _seekTimeSmall = 10;
|
||||
int _seekTimeLarge = 30;
|
||||
|
||||
// Update checking state
|
||||
bool _isCheckingForUpdate = false;
|
||||
Map<String, dynamic>? _updateInfo;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -70,6 +76,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
const SizedBox(height: 24),
|
||||
_buildAdvancedSection(),
|
||||
const SizedBox(height: 24),
|
||||
if (UpdateService.isUpdateCheckEnabled) ...[
|
||||
_buildUpdateSection(),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
_buildAboutSection(),
|
||||
const SizedBox(height: 24),
|
||||
]),
|
||||
@@ -259,6 +269,56 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUpdateSection() {
|
||||
final hasUpdate = _updateInfo != null && _updateInfo!['hasUpdate'] == true;
|
||||
|
||||
return Card(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Text(
|
||||
'Updates',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: Icon(
|
||||
hasUpdate ? Icons.system_update : Icons.check_circle,
|
||||
color: hasUpdate ? Colors.orange : null,
|
||||
),
|
||||
title: Text(
|
||||
hasUpdate ? 'Update Available' : 'Check for Updates',
|
||||
),
|
||||
subtitle: hasUpdate
|
||||
? Text('Version ${_updateInfo!['latestVersion']} is available')
|
||||
: const Text('Check for the latest version on GitHub'),
|
||||
trailing: _isCheckingForUpdate
|
||||
? const SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.chevron_right),
|
||||
onTap: _isCheckingForUpdate
|
||||
? null
|
||||
: () {
|
||||
if (hasUpdate) {
|
||||
_showUpdateDialog();
|
||||
} else {
|
||||
_checkForUpdates();
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAboutSection() {
|
||||
return Card(
|
||||
child: ListTile(
|
||||
@@ -585,6 +645,90 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _checkForUpdates() async {
|
||||
setState(() {
|
||||
_isCheckingForUpdate = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final updateInfo = await UpdateService.checkForUpdates();
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_updateInfo = updateInfo;
|
||||
_isCheckingForUpdate = false;
|
||||
});
|
||||
|
||||
if (updateInfo == null || updateInfo['hasUpdate'] != true) {
|
||||
// Show "no updates" message
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('You are on the latest version'),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isCheckingForUpdate = false;
|
||||
});
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Failed to check for updates'),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _showUpdateDialog() {
|
||||
if (_updateInfo == null) return;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Update Available'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Version ${_updateInfo!['latestVersion']} is available',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Current: ${_updateInfo!['currentVersion']}',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Close'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () async {
|
||||
final url = Uri.parse(_updateInfo!['releaseUrl']);
|
||||
if (await canLaunchUrl(url)) {
|
||||
await launchUrl(url, mode: LaunchMode.externalApplication);
|
||||
}
|
||||
if (context.mounted) Navigator.pop(context);
|
||||
},
|
||||
child: const Text('View Release'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showLibraryDensityDialog() {
|
||||
final settingsProvider = context.read<SettingsProvider>();
|
||||
showDialog(
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// Service to check for new versions on GitHub
|
||||
/// Only enabled when ENABLE_UPDATE_CHECK build flag is set
|
||||
class UpdateService {
|
||||
static final Logger _logger = Logger();
|
||||
static const String _githubRepo = 'edde746/plezy';
|
||||
|
||||
// SharedPreferences keys
|
||||
static const String _keySkippedVersion = 'update_skipped_version';
|
||||
static const String _keyLastCheckTime = 'update_last_check_time';
|
||||
|
||||
// Check cooldown: 6 hours
|
||||
static const Duration _checkCooldown = Duration(hours: 6);
|
||||
|
||||
/// Check if update checking is enabled via build flag
|
||||
static bool get isUpdateCheckEnabled {
|
||||
const enabled = bool.fromEnvironment('ENABLE_UPDATE_CHECK', defaultValue: false);
|
||||
return enabled;
|
||||
}
|
||||
|
||||
/// Skip a specific version
|
||||
static Future<void> skipVersion(String version) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_keySkippedVersion, version);
|
||||
}
|
||||
|
||||
/// Get the skipped version
|
||||
static Future<String?> getSkippedVersion() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getString(_keySkippedVersion);
|
||||
}
|
||||
|
||||
/// Clear skipped version
|
||||
static Future<void> clearSkippedVersion() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_keySkippedVersion);
|
||||
}
|
||||
|
||||
/// Check if cooldown period has passed since last check
|
||||
static Future<bool> shouldCheckForUpdates() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final lastCheckString = prefs.getString(_keyLastCheckTime);
|
||||
|
||||
if (lastCheckString == null) return true;
|
||||
|
||||
final lastCheck = DateTime.parse(lastCheckString);
|
||||
final now = DateTime.now();
|
||||
final timeSinceLastCheck = now.difference(lastCheck);
|
||||
|
||||
return timeSinceLastCheck >= _checkCooldown;
|
||||
}
|
||||
|
||||
/// Update the last check timestamp
|
||||
static Future<void> _updateLastCheckTime() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_keyLastCheckTime, DateTime.now().toIso8601String());
|
||||
}
|
||||
|
||||
/// Check for updates on GitHub (manual check, ignores cooldown)
|
||||
/// Returns a map with update info, or null if no update or error
|
||||
static Future<Map<String, dynamic>?> checkForUpdates({bool silent = false}) async {
|
||||
if (!isUpdateCheckEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
final currentVersion = packageInfo.version;
|
||||
|
||||
final dio = Dio();
|
||||
final response = await dio.get(
|
||||
'https://api.github.com/repos/$_githubRepo/releases/latest',
|
||||
options: Options(
|
||||
headers: {
|
||||
'Accept': 'application/vnd.github+json',
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = response.data;
|
||||
final latestVersion = data['tag_name'] as String;
|
||||
|
||||
// Remove 'v' prefix if present
|
||||
final cleanVersion = latestVersion.startsWith('v')
|
||||
? latestVersion.substring(1)
|
||||
: latestVersion;
|
||||
|
||||
final hasUpdate = _isNewerVersion(cleanVersion, currentVersion);
|
||||
|
||||
if (hasUpdate) {
|
||||
// Check if this version was skipped (always check, regardless of silent mode)
|
||||
final skippedVersion = await getSkippedVersion();
|
||||
if (skippedVersion == cleanVersion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
'hasUpdate': true,
|
||||
'currentVersion': currentVersion,
|
||||
'latestVersion': cleanVersion,
|
||||
'releaseUrl': data['html_url'] as String,
|
||||
'releaseName': data['name'] as String? ?? 'Version $cleanVersion',
|
||||
'releaseNotes': data['body'] as String? ?? '',
|
||||
'publishedAt': data['published_at'] as String,
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
_logger.e('Failed to check for updates: $e');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Check for updates on startup (respects cooldown and skipped versions)
|
||||
/// Returns update info if available, null otherwise
|
||||
static Future<Map<String, dynamic>?> checkForUpdatesOnStartup() async {
|
||||
if (!isUpdateCheckEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check cooldown
|
||||
if (!await shouldCheckForUpdates()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Perform the check
|
||||
final updateInfo = await checkForUpdates(silent: true);
|
||||
|
||||
// Update last check time
|
||||
await _updateLastCheckTime();
|
||||
|
||||
return updateInfo;
|
||||
}
|
||||
|
||||
/// Compare two version strings
|
||||
/// Returns true if newVersion is newer than currentVersion
|
||||
static bool _isNewerVersion(String newVersion, String currentVersion) {
|
||||
try {
|
||||
// Split by '.' and parse as integers
|
||||
final newParts = newVersion.split('.').map((p) {
|
||||
// Handle versions like "1.2.3+4" by taking only the numeric part
|
||||
final numPart = p.split('+').first.split('-').first;
|
||||
return int.tryParse(numPart) ?? 0;
|
||||
}).toList();
|
||||
|
||||
final currentParts = currentVersion.split('.').map((p) {
|
||||
final numPart = p.split('+').first.split('-').first;
|
||||
return int.tryParse(numPart) ?? 0;
|
||||
}).toList();
|
||||
|
||||
// Compare each part
|
||||
final maxLength = newParts.length > currentParts.length
|
||||
? newParts.length
|
||||
: currentParts.length;
|
||||
|
||||
for (int i = 0; i < maxLength; i++) {
|
||||
final newPart = i < newParts.length ? newParts[i] : 0;
|
||||
final currentPart = i < currentParts.length ? currentParts[i] : 0;
|
||||
|
||||
if (newPart > currentPart) return true;
|
||||
if (newPart < currentPart) return false;
|
||||
}
|
||||
|
||||
return false; // Versions are equal
|
||||
} catch (e) {
|
||||
_logger.e('Error comparing versions: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user