This commit is contained in:
2025-11-13 18:12:19 +01:00
parent f882460ff3
commit 7113b3a7fc
10 changed files with 781 additions and 33 deletions
+200
View File
@@ -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
-28
View File
@@ -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
-3
View File
@@ -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:
+11 -1
View File
@@ -36,7 +36,17 @@ class MyApp extends StatelessWidget {
colorScheme: ColorScheme.fromSeed(seedColor: Colors.white), colorScheme: ColorScheme.fromSeed(seedColor: Colors.white),
useMaterial3: true, 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(),
); );
} }
} }
+1 -1
View File
@@ -44,7 +44,7 @@ class _CalendarScreenState extends State<CalendarScreen> {
String _bulletColor = 'Blue'; String _bulletColor = 'Blue';
String _selectedLanguage = 'English'; String _selectedLanguage = 'English';
bool _showContacts = false; bool _showContacts = false;
bool _showDebugPreview = true; bool _showDebugPreview = false;
// Members contact list // Members contact list
final List<Contact> _contacts = [ final List<Contact> _contacts = [
+261
View File
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:upgrader/upgrader.dart';
import '../utils/translations.dart'; import '../utils/translations.dart';
class SettingsScreen extends StatefulWidget { class SettingsScreen extends StatefulWidget {
@@ -170,6 +171,224 @@ Future<void> _saveLanguage(String language) async {
_loadCameraIPs(); _loadCameraIPs();
} }
Future<void> _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) { void _editCameraIP(int index) {
TextEditingController controller = TextEditingController( TextEditingController controller = TextEditingController(
text: _cameraIPs[index], text: _cameraIPs[index],
@@ -314,6 +533,8 @@ Future<void> _saveLanguage(String language) async {
const SizedBox(height: 8), const SizedBox(height: 8),
_buildLanguageSelector(), _buildLanguageSelector(),
const SizedBox(height: 16), const SizedBox(height: 16),
_buildCheckUpdateButton(),
const SizedBox(height: 16),
_buildBulletColorSelector(), _buildBulletColorSelector(),
const SizedBox(height: 16), const SizedBox(height: 16),
@@ -372,6 +593,46 @@ Future<void> _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() { Widget _buildBulletColorSelector() {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
+73
View File
@@ -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<void> 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();
},
);
},
);
}
}
+40
View File
@@ -584,6 +584,46 @@ class AppTranslations {
'English': 'copied to clipboard', 'English': 'copied to clipboard',
'Slovenian': 'kopirano v odložišče', '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) { static String translate(String key, String language) {
+155
View File
@@ -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<CustomUpgradeAlert> createState() => _CustomUpgradeAlertState();
}
class _CustomUpgradeAlertState extends State<CustomUpgradeAlert> {
@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,
),
),
),
),
),
),
],
),
],
),
),
);
}
}
+40
View File
@@ -137,6 +137,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.6" version: "3.0.6"
csslib:
dependency: transitive
description:
name: csslib
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
cupertino_icons: cupertino_icons:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -344,6 +352,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.6.0" version: "1.6.0"
html:
dependency: transitive
description:
name: html
sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602"
url: "https://pub.dev"
source: hosted
version: "0.15.6"
http: http:
dependency: "direct overridden" dependency: "direct overridden"
description: description:
@@ -512,6 +528,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.0" 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: package_info_plus:
dependency: transitive dependency: transitive
description: description:
@@ -797,6 +821,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" 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: url_launcher:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -893,6 +925,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.4" version: "2.1.4"
version:
dependency: transitive
description:
name: version
sha256: "3d4140128e6ea10d83da32fef2fa4003fccbf6852217bb854845802f04191f94"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
visibility_detector: visibility_detector:
dependency: transitive dependency: transitive
description: description: