add troubleshooting guide

This commit is contained in:
Jonas Bark
2025-10-08 12:04:57 +02:00
parent 0ecf285a95
commit 7a8c7c963b
13 changed files with 155 additions and 278 deletions

View File

@@ -47,8 +47,7 @@ Get the latest version for free for Windows, macOS and Android here: https://git
- Note that you can't run SwiftControl and your trainer app on the same iPhone due to iOS limitations, but you can use it to remotely control MyWhoosh and similar on e.g. an iPad.
## Troubleshooting
- Your Zwift device is found but connection does not work properly? You may need to update the firmware in Zwift Companion app.
- if you have issues with the "Remote Control" functionality, try to unpair it from your phone / computer Bluetooth settings first.
Check the troubleshooting guide [here](TROUBLESHOOTING.md).
## How does it work?
The app connects to your Zwift device automatically. It does not connect to your trainer itself.

22
TROUBLESHOOTING.md Normal file
View File

@@ -0,0 +1,22 @@
## Zwift device cannot be found
You may need to update the firmware in Zwift Companion app.
## Zwift device does not send any data
You may need to update the firmware in Zwift Companion app.
## My Zwift Click v2 disconnects after a minute
Check [this](https://github.com/jonasbark/swiftcontrol/issues/68) discussion.
To make your Zwift Click V2 work best you should connect it in the Zwift app once each day.
If you don't do that SwiftControl will need to reconnect every minute.
1. Open Zwift app (not the Companion)
2. Log in (subscription not required) and open the device connection screen
3. Connect your Trainer, then connect the Zwift Click V2
4. Close the Zwift app again and connect again in SwiftControl
## Remote control is not working - nothing happens
- Try to unpair it from your phone / computer Bluetooth settings, then re-pair it.
- Try restarting the pairing process in SwiftControl
- try restarting Bluetooth on your phone and on the device you want to control
- If your other device is an iOS device, go to Settings > Accessibility > Touch > AssistiveTouch > Pointer Devices > Devices and pair your device. Make sure AssistiveTouch is enabled.

View File

@@ -1,98 +0,0 @@
import 'package:flutter/material.dart';
import 'package:swift_control/utils/changelog.dart';
class ChangelogPage extends StatefulWidget {
const ChangelogPage({super.key});
@override
State<ChangelogPage> createState() => _ChangelogPageState();
}
class _ChangelogPageState extends State<ChangelogPage> {
List<ChangelogEntry>? _entries;
String? _error;
@override
void initState() {
super.initState();
_loadChangelog();
}
Future<void> _loadChangelog() async {
try {
final entries = await ChangelogParser.parse();
setState(() {
_entries = entries;
});
} catch (e) {
setState(() {
_error = 'Failed to load changelog: $e';
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Changelog'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: _error != null
? Center(child: Text(_error!))
: _entries == null
? Center(child: CircularProgressIndicator())
: ListView.builder(
padding: EdgeInsets.all(16),
itemCount: _entries!.length,
itemBuilder: (context, index) {
final entry = _entries![index];
return Card(
margin: EdgeInsets.only(bottom: 16),
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Version ${entry.version}',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
Text(
entry.date,
style: Theme.of(context).textTheme.bodySmall,
),
],
),
SizedBox(height: 12),
...entry.changes.map(
(change) => Padding(
padding: EdgeInsets.only(bottom: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('', style: TextStyle(fontSize: 16)),
Expanded(
child: Text(
change,
style: Theme.of(context).textTheme.bodyMedium,
),
),
],
),
),
),
],
),
),
);
},
),
);
}
}

84
lib/pages/markdown.dart Normal file
View File

@@ -0,0 +1,84 @@
import 'package:flex_color_scheme/flex_color_scheme.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_md/flutter_md.dart';
import 'package:http/http.dart' as http;
import 'package:url_launcher/url_launcher_string.dart';
class MarkdownPage extends StatefulWidget {
final String assetPath;
const MarkdownPage({super.key, required this.assetPath});
@override
State<MarkdownPage> createState() => _ChangelogPageState();
}
class _ChangelogPageState extends State<MarkdownPage> {
Markdown? _markdown;
String? _error;
@override
void initState() {
super.initState();
_loadChangelog();
}
Future<void> _loadChangelog() async {
try {
final md = await rootBundle.loadString(widget.assetPath);
setState(() {
_markdown = Markdown.fromString(md);
});
// load latest version
final response = await http.get(
Uri.parse('https://raw.githubusercontent.com/jonasbark/swiftcontrol/refs/heads/ios/${widget.assetPath}'),
);
if (response.statusCode == 200) {
final latestMd = response.body;
if (latestMd != md) {
setState(() {
_markdown = Markdown.fromString(md);
});
}
}
} catch (e) {
setState(() {
_error = 'Failed to load changelog: $e';
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.assetPath.replaceAll('.md', '').toLowerCase().capitalize),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body:
_error != null
? Center(child: Text(_error!))
: _markdown == null
? Center(child: CircularProgressIndicator())
: SingleChildScrollView(
padding: EdgeInsets.all(16),
child: Row(
children: [
Expanded(
child: MarkdownWidget(
markdown: _markdown!,
theme: MarkdownThemeData(
textStyle: TextStyle(fontSize: 14.0, color: Colors.black87),
onLinkTap: (title, url) {
launchUrlString(url);
},
),
),
),
],
),
),
);
}
}

View File

@@ -158,7 +158,7 @@ class _RequirementsPageState extends State<RequirementsPage> with WidgetsBinding
(index, req) => Step(
title: Text(req.name),
content: Container(
padding: const EdgeInsets.symmetric(vertical: 16.0),
padding: const EdgeInsets.only(top: 16.0),
alignment: Alignment.centerLeft,
child:
(index == _currentStep

View File

@@ -1,79 +0,0 @@
import 'package:flutter/services.dart';
class ChangelogEntry {
final String version;
final String date;
final List<String> changes;
ChangelogEntry({
required this.version,
required this.date,
required this.changes,
});
@override
String toString() {
return '### $version ($date)\n${changes.map((c) => '- $c').join('\n')}';
}
}
class ChangelogParser {
static Future<List<ChangelogEntry>> parse() async {
final content = await rootBundle.loadString('CHANGELOG.md');
return parseContent(content);
}
static List<ChangelogEntry> parseContent(String content) {
final entries = <ChangelogEntry>[];
final lines = content.split('\n');
ChangelogEntry? currentEntry;
for (var line in lines) {
// Check if this is a version header (e.g., "### 2.6.0 (2025-09-28)")
if (line.startsWith('### ')) {
// Save previous entry if exists
if (currentEntry != null) {
entries.add(currentEntry);
}
// Parse new entry
final header = line.substring(4).trim();
final match = RegExp(r'^(\S+)\s+\(([^)]+)\)').firstMatch(header);
if (match != null) {
currentEntry = ChangelogEntry(
version: match.group(1)!,
date: match.group(2)!,
changes: [],
);
}
} else if (line.startsWith('- ') && currentEntry != null) {
// Add change to current entry
currentEntry.changes.add(line.substring(2).trim());
} else if (line.startsWith(' - ') && currentEntry != null) {
// Sub-bullet point
currentEntry.changes.add(line.substring(4).trim());
}
}
// Add the last entry
if (currentEntry != null) {
entries.add(currentEntry);
}
return entries;
}
static Future<ChangelogEntry?> getLatestEntry() async {
final entries = await parse();
return entries.isNotEmpty ? entries.first : null;
}
static Future<String?> getLatestEntryForPlayStore() async {
final entry = await getLatestEntry();
if (entry == null) return null;
// Format for Play Store: just the changes, no version header
return entry.changes.join('\n');
}
}

View File

@@ -93,7 +93,8 @@ class BluetoothConnectRequirement extends PlatformRequirement {
}
class NotificationRequirement extends PlatformRequirement {
NotificationRequirement() : super('Allow adding persistent Notification (keeps app alive)');
NotificationRequirement()
: super('Allow persistent Notification', description: 'This keeps the app alive in background');
@override
Future<void> call(BuildContext context, VoidCallback onUpdate) async {

View File

@@ -8,6 +8,8 @@ import 'package:swift_control/main.dart';
import 'package:swift_control/utils/actions/remote.dart';
import 'package:swift_control/utils/requirements/platform.dart';
import '../../pages/markdown.dart';
final peripheralManager = PeripheralManager();
bool _isAdvertising = false;
bool _isSubscribedToEvents = false;
@@ -271,10 +273,20 @@ class RemoteRequirement extends PlatformRequirement {
),
],
),
if (_isAdvertising)
if (_isAdvertising) ...[
Text(
'If your other device is an iOS device, go to Settings > Accessibility > Touch > AssistiveTouch > Pointer Devices > Devices and pair your device. Make sure to AssistiveTouch is enabled.',
'If your other device is an iOS device, go to Settings > Accessibility > Touch > AssistiveTouch > Pointer Devices > Devices and pair your device. Make sure AssistiveTouch is enabled.',
),
TextButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (c) => MarkdownPage(assetPath: 'TROUBLESHOOTING.md')),
);
},
child: Text('Check the troubleshooting guide'),
),
],
],
),
);

View File

@@ -1,13 +1,15 @@
import 'package:flutter/material.dart';
import 'package:swift_control/utils/changelog.dart';
import 'package:flutter/services.dart';
import 'package:flutter_md/flutter_md.dart';
class ChangelogDialog extends StatelessWidget {
final ChangelogEntry entry;
final Markdown entry;
const ChangelogDialog({super.key, required this.entry});
@override
Widget build(BuildContext context) {
final latestVersion = Markdown(blocks: entry.blocks.skip(1).take(2).toList(), markdown: entry.markdown);
return AlertDialog(
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -16,29 +18,12 @@ class ChangelogDialog extends StatelessWidget {
Text('What\'s New'),
SizedBox(height: 4),
Text(
'Version ${entry.version}',
'Version ${entry.blocks.first.text}',
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.normal),
),
],
),
content: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children:
entry.changes
.map(
(change) => Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('', style: TextStyle(fontSize: 16)),
Expanded(child: Text(change, style: Theme.of(context).textTheme.bodyMedium)),
],
),
)
.toList(),
),
),
content: Container(constraints: BoxConstraints(minWidth: 460), child: MarkdownWidget(markdown: latestVersion)),
actions: [TextButton(onPressed: () => Navigator.of(context).pop(), child: Text('Got it!'))],
);
}
@@ -47,9 +32,10 @@ class ChangelogDialog extends StatelessWidget {
// Show dialog if this is a new version
if (lastSeenVersion != currentVersion) {
try {
final entry = await ChangelogParser.getLatestEntry();
if (entry != null && context.mounted) {
showDialog(context: context, builder: (context) => ChangelogDialog(entry: entry));
final entry = await rootBundle.loadString('CHANGELOG.md');
if (context.mounted) {
final markdown = Markdown.fromString(entry);
showDialog(context: context, builder: (context) => ChangelogDialog(entry: markdown));
}
} catch (e) {
print('Failed to load changelog for dialog: $e');

View File

@@ -5,11 +5,11 @@ import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:swift_control/bluetooth/messages/ride_notification.dart';
import 'package:swift_control/main.dart';
import 'package:swift_control/pages/markdown.dart';
import 'package:swift_control/utils/keymap/buttons.dart';
import 'package:swift_control/widgets/title.dart';
import 'package:url_launcher/url_launcher_string.dart';
import '../pages/changelog.dart';
import '../pages/device.dart';
List<Widget> buildMenuButtons() {
@@ -100,7 +100,16 @@ class MenuButton extends StatelessWidget {
PopupMenuItem(
child: Text('Changelog'),
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (c) => ChangelogPage()));
Navigator.push(context, MaterialPageRoute(builder: (c) => MarkdownPage(assetPath: 'CHANGELOG.md')));
},
),
PopupMenuItem(
child: Text('Troubleshooting Guide'),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (c) => MarkdownPage(assetPath: 'TROUBLESHOOTING.md')),
);
},
),
PopupMenuItem(

View File

@@ -325,6 +325,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.3"
flutter_md:
dependency: "direct main"
description:
name: flutter_md
sha256: b5a67ae49135f7a76a0cc6f938ee3e8754e71d8448b97cf99c11512877f1d055
url: "https://pub.dev"
source: hosted
version: "0.0.7"
flutter_plugin_android_lifecycle:
dependency: transitive
description:

View File

@@ -17,6 +17,7 @@ dependencies:
version: ^3.0.0
bluetooth_low_energy: ^6.1.0
protobuf: ^4.2.0
flutter_md: ^0.0.7
permission_handler: ^12.0.1
dartx: any
image_picker: ^1.1.2
@@ -44,3 +45,4 @@ flutter:
uses-material-design: true
assets:
- CHANGELOG.md
- TROUBLESHOOTING.md

View File

@@ -1,69 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:swift_control/utils/changelog.dart';
void main() {
group('ChangelogParser', () {
test('parses changelog entries correctly', () {
const testContent = '''
### 2.6.0 (2025-09-28)
- Fix crashes on some Android devices
- refactor touch placements: show touches on screen
- show firmware version of connected device
### 2.5.0 (2025-09-25)
- Improve usability
- SwiftControl is now available via the Play Store
- SwiftControl will continue to be available to download for free on GitHub
''';
final entries = ChangelogParser.parseContent(testContent);
expect(entries.length, 2);
expect(entries[0].version, '2.6.0');
expect(entries[0].date, '2025-09-28');
expect(entries[0].changes.length, 3);
expect(entries[0].changes[0], 'Fix crashes on some Android devices');
expect(entries[1].version, '2.5.0');
expect(entries[1].date, '2025-09-25');
expect(entries[1].changes.length, 3);
expect(entries[1].changes[0], 'Improve usability');
expect(entries[1].changes[1], 'SwiftControl is now available via the Play Store');
expect(entries[1].changes[2], 'SwiftControl will continue to be available to download for free on GitHub');
});
test('handles empty content', () {
const testContent = '';
final entries = ChangelogParser.parseContent(testContent);
expect(entries.length, 0);
});
test('handles single entry', () {
const testContent = '''
### 1.0.0 (2025-01-01)
- Initial release
''';
final entries = ChangelogParser.parseContent(testContent);
expect(entries.length, 1);
expect(entries[0].version, '1.0.0');
expect(entries[0].changes.length, 1);
expect(entries[0].changes[0], 'Initial release');
});
test('ChangelogEntry toString formats correctly', () {
final entry = ChangelogEntry(
version: '1.0.0',
date: '2025-01-01',
changes: ['Change 1', 'Change 2'],
);
final result = entry.toString();
expect(result, contains('### 1.0.0 (2025-01-01)'));
expect(result, contains('- Change 1'));
expect(result, contains('- Change 2'));
});
});
}