diff --git a/RELEASE_GUIDE.md b/RELEASE_GUIDE.md new file mode 100644 index 0000000..a184be5 --- /dev/null +++ b/RELEASE_GUIDE.md @@ -0,0 +1,200 @@ +# Release Guide - Shooting App + +## Overview +This guide explains how to create and publish a new release of the Shooting App. The app uses GitHub releases and semantic versioning for update management. + +## Current Setup +- **Repository**: https://github.com/Bl3kiiie/shooting_app +- **Update Method**: Manual check via Settings button (no automatic prompts) +- **Version File**: `pubspec.yaml` +- **Update Detection**: Compares app version with GitHub release tags + +--- + +## Step-by-Step Release Process + +### 1. Update Version in pubspec.yaml +Open `pubspec.yaml` and update the version number: + +```yaml +version: 1.0.2+2 +``` + +Format: `MAJOR.MINOR.PATCH+BUILD_NUMBER` + +Examples: +- `1.0.1+1` → `1.0.2+1` (patch release, new build) +- `1.0.2+1` → `1.0.2+2` (same version, new build) +- `1.0.2+2` → `1.1.0+1` (minor release) + +**Important**: New tag must be HIGHER than previous version for users to see update prompt. + +### 2. Build Release APK + +```bash +# Navigate to project directory +cd /home/bl3kunja/Documents/Programming/Strelišče/shooting_app + +# Build release APK +flutter build apk --release +``` + +The APK will be generated at: `build/app/outputs/flutter-app/release/app-release.apk` + +### 3. Create GitHub Release (Option A: Web Interface - Recommended) + +1. Go to: https://github.com/Bl3kiiie/shooting_app/releases +2. Click **Draft a new release** or **Create a new release** +3. Fill in the form: + + **Tag version**: `v1.0.2` (must match your new version number) + + **Release title**: e.g., `Version 1.0.2 - Bug Fixes and Improvements` + + **Description** (example): + ``` + ## Changes in v1.0.2 + - Fixed camera calibration issue + - Improved bullet detection accuracy + - Added German language support + - Performance optimizations + + ## Installation + Download the APK from the assets below and install on your device. + ``` + +4. **Upload APK**: + - Click "Attach binaries by dropping them here or selecting them" + - Select: `build/app/outputs/flutter-app/release/app-release.apk` + +5. Click **Publish release** + +### 3. Create GitHub Release (Option B: Command Line) + +If using GitHub CLI (`gh` command): + +```bash +# Create tag locally +git tag v1.0.2 + +# Push tag to GitHub +git push origin v1.0.2 + +# Create release (interactive) +gh release create v1.0.2 \ + --title "Version 1.0.2" \ + --notes "Your changelog here" + +# Upload APK to release +gh release upload v1.0.2 build/app/outputs/flutter-app/release/app-release.apk +``` + +### 4. Commit Version Update (Optional) +If you want to track version changes in git: + +```bash +git add pubspec.yaml +git commit -m "bump version to 1.0.2+2" +git push origin main +``` + +--- + +## How Users Update + +1. User opens app → Settings screen +2. User taps **"Check for Updates"** button +3. App compares: + - Current version: `1.0.1+1` + - Latest GitHub release tag: `v1.0.2` +4. Since `v1.0.2 > v1.0.1`, update is available +5. Dialog shows **"Update Available"** +6. User clicks **"Update"** → Opens GitHub release page +7. User downloads and installs new APK + +--- + +## Important Notes + +### Version Numbering +- Always increment version number before creating release +- Tag format: `vX.Y.Z` (e.g., `v1.0.2`, `v1.1.0`) +- App checks: `tag_version > app_version` + +### GitHub Token +- Stored in: `lib/services/update_checker.dart` +- Token: `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` +- Scopes: `repo` (for private repo access) +- Consider rotating this token periodically for security + +### Testing Before Release +```bash +# Test the build +flutter build apk --release + +# Verify APK was created +ls -lh build/app/outputs/flutter-app/release/app-release.apk + +# Optionally install on connected device +flutter install build/app/outputs/flutter-app/release/app-release.apk +``` + +### Troubleshooting Update Detection + +If users don't see update prompt: + +1. Check GitHub release exists: https://github.com/Bl3kiiie/shooting_app/releases +2. Verify tag format is `vX.Y.Z` (must start with 'v') +3. Confirm new version > current version +4. User must tap "Check for Updates" manually +5. Check app logs for errors + +--- + +## Checklist for Next Release + +- [ ] Update version in `pubspec.yaml` +- [ ] Test build: `flutter build apk --release` +- [ ] Create GitHub release with tag `vX.Y.Z` +- [ ] Upload APK to release +- [ ] Write changelog/description +- [ ] Publish release +- [ ] (Optional) Test update detection in app + +--- + +## Example Release Workflow + +```bash +# 1. Update version in pubspec.yaml (manually edit to 1.0.2+1) + +# 2. Build APK +flutter build apk --release + +# 3. Create tag and push +git tag v1.0.2 +git push origin v1.0.2 + +# 4. Create release via GitHub web: +# - Go to https://github.com/Bl3kiiie/shooting_app/releases +# - Draft new release +# - Tag: v1.0.2 +# - Upload APK from build/app/outputs/flutter-app/release/app-release.apk +# - Publish + +# 5. Users can now "Check for Updates" and see the new version +``` + +--- + +## Files Referenced +- `pubspec.yaml` - Version number +- `lib/services/update_checker.dart` - GitHub token and update logic +- `lib/screens/settings_screen.dart` - "Check for Updates" button +- `lib/utils/translations.dart` - Update dialog translations + +--- + +**Last Updated**: November 13, 2025 +**Current Version**: 1.0.1 +**Next Planned Version**: 1.0.2 diff --git a/analysis_options.yaml b/analysis_options.yaml deleted file mode 100644 index 0d29021..0000000 --- a/analysis_options.yaml +++ /dev/null @@ -1,28 +0,0 @@ -# This file configures the analyzer, which statically analyzes Dart code to -# check for errors, warnings, and lints. -# -# The issues identified by the analyzer are surfaced in the UI of Dart-enabled -# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be -# invoked from the command line by running `flutter analyze`. - -# The following line activates a set of recommended lints for Flutter apps, -# packages, and plugins designed to encourage good coding practices. -include: package:flutter_lints/flutter.yaml - -linter: - # The lint rules applied to this project can be customized in the - # section below to disable rules from the `package:flutter_lints/flutter.yaml` - # included above or to enable additional rules. A list of all available lints - # and their documentation is published at https://dart.dev/lints. - # - # Instead of disabling a lint rule for the entire project in the - # section below, it can also be suppressed for a single line of code - # or a specific dart file by using the `// ignore: name_of_lint` and - # `// ignore_for_file: name_of_lint` syntax on the line or in the file - # producing the lint. - rules: - # avoid_print: false # Uncomment to disable the `avoid_print` rule - # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule - -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options diff --git a/devtools_options.yaml b/devtools_options.yaml deleted file mode 100644 index fa0b357..0000000 --- a/devtools_options.yaml +++ /dev/null @@ -1,3 +0,0 @@ -description: This file stores settings for Dart & Flutter DevTools. -documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states -extensions: diff --git a/lib/main.dart b/lib/main.dart index a9c0b1f..6ca68b1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -36,7 +36,17 @@ class MyApp extends StatelessWidget { colorScheme: ColorScheme.fromSeed(seedColor: Colors.white), useMaterial3: true, ), - home: const HomeScreen(), // Change LoginScreen to HomeScreen + darkTheme: ThemeData( + colorScheme: ColorScheme.fromSeed( + seedColor: Colors.blue, + brightness: Brightness.dark, + ), + useMaterial3: true, + dialogTheme: DialogThemeData( + backgroundColor: Colors.grey[900], + ), + ), + home: const HomeScreen(), ); } } diff --git a/lib/screens/calendar_screen.dart b/lib/screens/calendar_screen.dart index 74e2803..47a9676 100644 --- a/lib/screens/calendar_screen.dart +++ b/lib/screens/calendar_screen.dart @@ -44,7 +44,7 @@ class _CalendarScreenState extends State { String _bulletColor = 'Blue'; String _selectedLanguage = 'English'; bool _showContacts = false; - bool _showDebugPreview = true; + bool _showDebugPreview = false; // Members contact list final List _contacts = [ diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 35dee17..d97a6b0 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:upgrader/upgrader.dart'; import '../utils/translations.dart'; class SettingsScreen extends StatefulWidget { @@ -170,6 +171,224 @@ Future _saveLanguage(String language) async { _loadCameraIPs(); } + Future _checkForUpdatesManual() async { + // Check for updates without the 24-hour throttling + final upgrader = Upgrader(); + try { + final isUpdateAvailable = await upgrader.isUpdateAvailable(); + if (isUpdateAvailable && mounted) { + _showCustomUpdateDialog(true); + } else if (mounted) { + // Show "No updates available" dialog + _showNoUpdatesDialog(); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(_t('Update check failed'))), + ); + } + } + } + + void _showCustomUpdateDialog(bool updateAvailable) { + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return Dialog( + backgroundColor: Colors.grey[900], + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + color: Colors.blue, + borderRadius: BorderRadius.circular(16), + ), + child: const Icon( + Icons.update, + color: Colors.white, + size: 40, + ), + ), + const SizedBox(height: 24), + Text( + _t('Update Available'), + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + Text( + _t('A new version of the app is available'), + style: TextStyle( + fontSize: 16, + color: Colors.grey[300], + height: 1.5, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 32), + Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () => Navigator.of(context).pop(), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: Colors.grey[800], + borderRadius: BorderRadius.circular(8), + ), + child: Center( + child: Text( + _t('Later'), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.grey, + ), + ), + ), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: GestureDetector( + onTap: () { + Navigator.of(context).pop(); + // Trigger the update using the upgrader package + _triggerUpdate(); + }, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: Colors.blue, + borderRadius: BorderRadius.circular(8), + ), + child: Center( + child: Text( + _t('Update'), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + ), + ), + ), + ), + ], + ), + ], + ), + ), + ); + }, + ); + } + + void _showNoUpdatesDialog() { + showDialog( + context: context, + builder: (BuildContext context) { + return Dialog( + backgroundColor: Colors.grey[900], + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + color: Colors.green, + borderRadius: BorderRadius.circular(16), + ), + child: const Icon( + Icons.check_circle, + color: Colors.white, + size: 40, + ), + ), + const SizedBox(height: 24), + Text( + _t('No updates available'), + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + Text( + _t('You are using the latest version'), + style: TextStyle( + fontSize: 16, + color: Colors.grey[300], + height: 1.5, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 32), + SizedBox( + width: double.infinity, + child: GestureDetector( + onTap: () => Navigator.of(context).pop(), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: Colors.blue, + borderRadius: BorderRadius.circular(8), + ), + child: Center( + child: Text( + _t('OK'), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + ), + ), + ), + ), + ], + ), + ), + ); + }, + ); + } + + void _triggerUpdate() { + // Use Upgrader to trigger the app store/play store + try { + Upgrader().sendUserToAppStore(); + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(_t('Failed to open app store'))), + ); + } + } + } + void _editCameraIP(int index) { TextEditingController controller = TextEditingController( text: _cameraIPs[index], @@ -314,6 +533,8 @@ Future _saveLanguage(String language) async { const SizedBox(height: 8), _buildLanguageSelector(), const SizedBox(height: 16), + _buildCheckUpdateButton(), + const SizedBox(height: 16), _buildBulletColorSelector(), const SizedBox(height: 16), @@ -372,6 +593,46 @@ Future _saveLanguage(String language) async { ); } + Widget _buildCheckUpdateButton() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _t("Updates"), + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + const SizedBox(height: 8), + GestureDetector( + onTap: _checkForUpdatesManual, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + const Icon(Icons.system_update, color: Colors.black54), + const SizedBox(width: 12), + Expanded( + child: Text( + _t('Check for Updates'), + style: const TextStyle(color: Colors.black87, fontSize: 14), + ), + ), + const Icon(Icons.chevron_right, color: Colors.black54), + ], + ), + ), + ), + ], + ); + } + Widget _buildBulletColorSelector() { return Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/services/update_checker.dart b/lib/services/update_checker.dart new file mode 100644 index 0000000..be8f895 --- /dev/null +++ b/lib/services/update_checker.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:upgrader/upgrader.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import '../utils/translations.dart'; +import '../widgets/custom_upgrade_alert.dart'; + +// GitHub Configuration +const String _githubRepoOwner = 'Bl3kiiie'; +const String _githubRepoName = 'shooting_app'; +const String _githubToken = + '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'; // Replace with your GitHub Personal Access Token + +class UpdateChecker { + static final UpdateChecker _instance = UpdateChecker._internal(); + + factory UpdateChecker() { + return _instance; + } + + UpdateChecker._internal(); + + Future checkForUpdate(BuildContext context, String language) async { + // Check if we've already checked today + final prefs = await SharedPreferences.getInstance(); + final lastCheckTime = prefs.getInt('last_update_check_time') ?? 0; + final now = DateTime.now().millisecondsSinceEpoch; + final oneDayInMilliseconds = 24 * 60 * 60 * 1000; + + // Only check once per day + if (now - lastCheckTime < oneDayInMilliseconds) { + return; + } + + // Update the last check time + await prefs.setInt('last_update_check_time', now); + + final upgrader = Upgrader(); + + try { + // Check if update is available + final isUpdateAvailable = await upgrader.isUpdateAvailable(); + + if (isUpdateAvailable && context.mounted) { + _showCustomUpdateDialog(context, language); + } + } catch (e) { + // Silently fail if update check fails + print('Update check failed: $e'); + } + } + + void _showCustomUpdateDialog(BuildContext context, String language) { + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return CustomUpdateDialog( + title: AppTranslations.translate('Update Available', language), + message: AppTranslations.translate( + 'A new version of the app is available', language), + updateButtonText: AppTranslations.translate('Update', language), + laterButtonText: AppTranslations.translate('Later', language), + onUpdate: () { + Upgrader().sendUserToAppStore(); + }, + onLater: () { + Navigator.of(context).pop(); + }, + ); + }, + ); + } +} diff --git a/lib/utils/translations.dart b/lib/utils/translations.dart index 8ce3edc..dac49d5 100644 --- a/lib/utils/translations.dart +++ b/lib/utils/translations.dart @@ -584,6 +584,46 @@ class AppTranslations { 'English': 'copied to clipboard', 'Slovenian': 'kopirano v odložišče', }, + + // App Update translations + 'Update Available': { + 'English': 'Update Available', + 'Slovenian': 'Posodobitev je na voljo', + }, + 'A new version of the app is available': { + 'English': 'A new version of the app is available', + 'Slovenian': 'Nova različica aplikacije je na voljo', + }, + 'Update': { + 'English': 'Update', + 'Slovenian': 'Posodobi', + }, + 'Later': { + 'English': 'Later', + 'Slovenian': 'Kasneje', + }, + + // App Update Check Button + 'Check for Updates': { + 'English': 'Check for Updates', + 'Slovenian': 'Preveri za posodobitve', + }, + 'No updates available': { + 'English': 'No updates available', + 'Slovenian': 'Ni dostopnih posodobitev', + }, + 'You are using the latest version': { + 'English': 'You are using the latest version', + 'Slovenian': 'Uporabljate najnovejšo različico', + }, + 'Updates': { + 'English': 'Updates', + 'Slovenian': 'Posodobitve', + }, + 'OK': { + 'English': 'OK', + 'Slovenian': 'V redu', + }, }; static String translate(String key, String language) { diff --git a/lib/widgets/custom_upgrade_alert.dart b/lib/widgets/custom_upgrade_alert.dart new file mode 100644 index 0000000..f7a126b --- /dev/null +++ b/lib/widgets/custom_upgrade_alert.dart @@ -0,0 +1,155 @@ +import 'package:flutter/material.dart'; +import 'package:upgrader/upgrader.dart'; +import '../utils/translations.dart'; + +class CustomUpgradeAlert extends StatefulWidget { + final Widget child; + final String language; + + const CustomUpgradeAlert({ + super.key, + required this.child, + required this.language, + }); + + @override + State createState() => _CustomUpgradeAlertState(); +} + +class _CustomUpgradeAlertState extends State { + @override + Widget build(BuildContext context) { + return UpgradeAlert( + child: widget.child, + upgrader: Upgrader( + debugDisplayAlways: true, + ), + dialogStyle: UpgradeDialogStyle.material, + shouldPopScope: () => true, + ); + } +} + +class CustomUpdateDialog extends StatelessWidget { + final String title; + final String message; + final String updateButtonText; + final String laterButtonText; + final VoidCallback onUpdate; + final VoidCallback onLater; + + const CustomUpdateDialog({ + super.key, + required this.title, + required this.message, + required this.updateButtonText, + required this.laterButtonText, + required this.onUpdate, + required this.onLater, + }); + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: Colors.grey[900], + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // App Icon + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + color: Colors.blue, + borderRadius: BorderRadius.circular(16), + ), + child: const Icon( + Icons.update, + color: Colors.white, + size: 40, + ), + ), + const SizedBox(height: 24), + // Title + Text( + title, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + // Message + Text( + message, + style: TextStyle( + fontSize: 16, + color: Colors.grey[300], + height: 1.5, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 32), + // Buttons + Row( + children: [ + // Later Button + Expanded( + child: GestureDetector( + onTap: onLater, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: Colors.grey[800], + borderRadius: BorderRadius.circular(8), + ), + child: Center( + child: Text( + laterButtonText, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.grey, + ), + ), + ), + ), + ), + ), + const SizedBox(width: 12), + // Update Button + Expanded( + child: GestureDetector( + onTap: onUpdate, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: Colors.blue, + borderRadius: BorderRadius.circular(8), + ), + child: Center( + child: Text( + updateButtonText, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + ), + ), + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 1f1e997..b844b10 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -137,6 +137,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.6" + csslib: + dependency: transitive + description: + name: csslib + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" + url: "https://pub.dev" + source: hosted + version: "1.0.2" cupertino_icons: dependency: "direct main" description: @@ -344,6 +352,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.6.0" + html: + dependency: transitive + description: + name: html + sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" + url: "https://pub.dev" + source: hosted + version: "0.15.6" http: dependency: "direct overridden" description: @@ -512,6 +528,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + os_detect: + dependency: transitive + description: + name: os_detect + sha256: "7d87c0dd98c6faf110d5aa498e9a6df02ffce4bb78cc9cfc8ad02929be9bb71f" + url: "https://pub.dev" + source: hosted + version: "2.0.3" package_info_plus: dependency: transitive description: @@ -797,6 +821,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + upgrader: + dependency: "direct main" + description: + name: upgrader + sha256: "4ba3f840691ffa43c6d71a93a6ad3ae01590c7895c3eb33f9f683513f736a9f7" + url: "https://pub.dev" + source: hosted + version: "12.3.0" url_launcher: dependency: "direct main" description: @@ -893,6 +925,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + version: + dependency: transitive + description: + name: version + sha256: "3d4140128e6ea10d83da32fef2fa4003fccbf6852217bb854845802f04191f94" + url: "https://pub.dev" + source: hosted + version: "3.0.2" visibility_detector: dependency: transitive description: