reworked api handling, portrait behavior

This commit is contained in:
Mercurio 2023-12-12 20:04:40 +01:00
parent 3f112a857b
commit 8cad1e26df
5 changed files with 381 additions and 70 deletions

266
lib/NewShareLinkPage.dart Normal file
View file

@ -0,0 +1,266 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:intl/intl.dart';
import 'package:shared_preferences/shared_preferences.dart';
class NewShareLinkPage extends StatefulWidget {
const NewShareLinkPage({Key? key}) : super(key: key);
@override
_NewShareLinkPageState createState() => _NewShareLinkPageState();
}
class _NewShareLinkPageState extends State<NewShareLinkPage> {
late TextEditingController linkNameController;
DateTime? selectedDateTime;
bool neverExpire = false;
double durationValue = 0;
double intensityValue = 0;
String? shareLink;
@override
void initState() {
super.initState();
linkNameController = TextEditingController();
}
Future<String?> _getApiKey() async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
return prefs.getString('apiKey');
}
Future<String?> _getShockerId() async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
return prefs.getString('shockerId');
}
Future<void> saveSettings() async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
final String? apiKey = prefs.getString('apiKey');
final String? shockerId = prefs.getString('shockerId');
if (apiKey != null && shockerId != null) {
final String linkName = linkNameController.text;
final int durationInMilliseconds = (durationValue * 1000).toInt();
final createLinkResponse = await http.post(
Uri.parse('https://api.shocklink.net/1/shares/links'),
headers: {
'accept': 'application/json',
'OpenShockToken': apiKey,
},
body: jsonEncode({
'name': linkName,
'expiresOn': selectedDateTime?.toIso8601String(),
}),
);
if (createLinkResponse.statusCode == 200) {
final responseData = jsonDecode(createLinkResponse.body);
final String linkId = responseData['data'];
final addShockerResponse = await http.put(
Uri.parse('https://api.shocklink.net/1/shares/links/$linkId/$shockerId'),
headers: {
'accept': 'application/json',
'OpenShockToken': apiKey,
},
);
if (addShockerResponse.statusCode == 200) {
final setPermissionsResponse = await http.patch(
Uri.parse('https://api.shocklink.net/1/shares/links/$linkId/$shockerId'),
headers: {
'accept': 'application/json',
'OpenShockToken': apiKey,
},
body: jsonEncode({
'permissions': {
'vibrate': true,
'sound': false,
'shock': true,
},
'limits': {
'intensity': intensityValue,
'duration': durationInMilliseconds,
},
'cooldown': 30000,
}),
);
if (setPermissionsResponse.statusCode == 200) {
setState(() {
shareLink = 'https://shockl.ink/s/$linkId';
});
} else {
// Handle failure in setting permissions
}
} else {
// Handle failure in adding shocker to the link
}
} else {
// Handle failure in creating the share link
}
} else {
// Handle missing apiKey or shockerId
}
}
Future<void> _selectDate(BuildContext context) async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: selectedDateTime ?? DateTime.now(),
firstDate: DateTime.now(),
lastDate: DateTime(DateTime.now().year + 1),
);
if (picked != null) {
setState(() {
selectedDateTime = DateTime(picked.year, picked.month, picked.day,
selectedDateTime?.hour ?? 0, selectedDateTime?.minute ?? 0);
});
}
}
Future<void> _selectTime(BuildContext context) async {
final TimeOfDay? picked = await showTimePicker(
context: context,
initialTime: TimeOfDay.fromDateTime(selectedDateTime ?? DateTime.now()),
);
if (picked != null) {
setState(() {
selectedDateTime = DateTime(
selectedDateTime?.year ?? DateTime.now().year,
selectedDateTime?.month ?? DateTime.now().month,
selectedDateTime?.day ?? DateTime.now().day,
picked.hour,
picked.minute,
);
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('New Share Link'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Link Name'),
TextFormField(
controller: linkNameController,
decoration: InputDecoration(
hintText: 'Enter link name',
),
),
const SizedBox(height: 20),
Row(
children: [
Checkbox(
value: neverExpire,
onChanged: (value) {
setState(() {
neverExpire = value!;
if (neverExpire) {
selectedDateTime = null;
}
});
},
),
const Text('Never Expire'),
],
),
if (!neverExpire)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Select Date and Time'),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
TextButton(
onPressed: () => _selectDate(context),
child: const Text('Select Date'),
),
TextButton(
onPressed: () => _selectTime(context),
child: const Text('Select Time'),
),
if (selectedDateTime != null)
Text(
'Selected: ${DateFormat.yMd().add_jm().format(selectedDateTime!)}',
),
],
),
],
),
const SizedBox(height: 20),
const Text('Duration'),
Slider(
value: durationValue,
min: 0,
max: 30,
divisions: 30,
label: durationValue.round().toString(),
onChanged: (value) {
setState(() {
durationValue = value;
});
},
),
Text('Selected Duration: ${durationValue.round()}'),
const SizedBox(height: 20),
const Text('Intensity'),
Slider(
value: intensityValue,
min: 0,
max: 100,
divisions: 100,
label: intensityValue.round().toString(),
onChanged: (value) {
setState(() {
intensityValue = value;
});
},
),
Text('Selected Intensity: ${intensityValue.round()}'),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
saveSettings();
},
child: const Text('Create Share Link'),
),
if (shareLink != null)
Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Generated Share Link:',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
TextFormField(
initialValue: shareLink,
readOnly: true,
decoration: InputDecoration(
hintText: 'Generated Share Link',
border: OutlineInputBorder(),
),
),
],
),
),
],
),
),
);
}
}

View file

@ -49,7 +49,7 @@ Future<void> saveSettings(String apiKey, String shockerId) async {
await prefs.setString('shockerId', shockerId); await prefs.setString('shockerId', shockerId);
} }
Future<void> sendApiRequest(int intensity, int time, int type) async { Future<bool> sendApiRequest(int intensity, int time, int type) async {
// Fetch saved information from SharedPreferences // Fetch saved information from SharedPreferences
final SharedPreferences prefs = await SharedPreferences.getInstance(); final SharedPreferences prefs = await SharedPreferences.getInstance();
@ -82,13 +82,8 @@ Future<void> sendApiRequest(int intensity, int time, int type) async {
); );
if (response.statusCode == 200) { if (response.statusCode == 200) {
// Request successful, handle the response if needed return true;
print('API request successful');
print(response.body);
} else { } else {
// Request failed, handle the error return false;
print('API request failed');
print('Status code: ${response.statusCode}');
print('Response body: ${response.body}');
} }
} }

View file

@ -60,7 +60,6 @@ class _LogsPageState extends State<logs_page> {
} }
Icon getIconForType(String type) { Icon getIconForType(String type) {
print('Type: $type');
switch (type.toLowerCase()) { switch (type.toLowerCase()) {
case 'vibrate': case 'vibrate':
return const Icon(Icons.vibration); return const Icon(Icons.vibration);
@ -82,62 +81,85 @@ class _LogsPageState extends State<logs_page> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final appState = Provider.of<AppState>(context, listen: false); final appState = Provider.of<AppState>(context, listen: false);
appState.currentIndex = 2; appState.currentIndex = 2;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Logs'), title: const Text('Logs'),
), ),
body: RefreshIndicator( body: LayoutBuilder(
onRefresh: _handleRefresh, builder: (context, constraints) {
child: SingleChildScrollView( final isLandscape =
scrollDirection: Axis.horizontal, MediaQuery.of(context).orientation == Orientation.landscape;
child: SingleChildScrollView(
child: DataTable( return RefreshIndicator(
columnSpacing: 10, onRefresh: _handleRefresh,
dataRowMaxHeight: 50, child: SingleChildScrollView(
columns: const [ scrollDirection: Axis.horizontal,
DataColumn(label: Text('Name')), child: SingleChildScrollView(
DataColumn(label: Text('Intensity')), child: DataTable(
DataColumn(label: Text('Duration')), columnSpacing: 10,
DataColumn(label: Text('Type')), columns: [
DataColumn(label: Text('Time')), DataColumn(label: SizedBox(
], width: isLandscape ? constraints.maxWidth * 0.25 : null,
rows: logs.map((log) { child: Text('Name'),
final controlledBy = )),
DataColumn(label: SizedBox(
width: isLandscape ? constraints.maxWidth * 0.15 : null,
child: Text('Intensity'),
)),
DataColumn(label: SizedBox(
width: isLandscape ? constraints.maxWidth * 0.15
: null,
child: Text('Duration'),
)),
DataColumn(label: SizedBox(
width: isLandscape ? constraints.maxWidth * 0.15 : null,
child: Text('Type'),
)),
DataColumn(label: SizedBox(
width: isLandscape ? constraints.maxWidth * 0.2 : null,
child: Text('Time'),
)),
],
rows: logs.map<DataRow>((log) {
final controlledBy =
log['controlledBy'] as Map<String, dynamic>?; log['controlledBy'] as Map<String, dynamic>?;
if (controlledBy != null) { if (controlledBy != null) {
final name = getDisplayName(controlledBy); final name = getDisplayName(controlledBy);
final intensity = log['intensity'] as int?; final intensity = log['intensity'] as int?;
final duration = (log['duration'] as int?)! / 1000; final duration = (log['duration'] as int?)! / 1000;
final type = log['type'] as String?; final type = log['type'] as String?;
final createdAt = log['createdOn'] as String?; final createdAt = log['createdOn'] as String?;
if (intensity != null && type != null && createdAt != null) { if (intensity != null &&
final userTimezone = type != null &&
DateTime.now().timeZoneOffset; // Get user's timezone createdAt != null) {
final userTimezone = DateTime.now().timeZoneOffset;
final utcDateTime = DateTime.parse(createdAt);
final localDateTime =
utcDateTime.add(userTimezone);
final utcDateTime = DateTime.parse(createdAt); final formattedCreatedAt = DateFormat('dd/MM/yy - HH:mm')
final localDateTime = utcDateTime .format(localDateTime);
.add(userTimezone); // Convert to local timezone
final formattedCreatedAt = return DataRow(
DateFormat('dd/MM/yy - HH:mm').format(localDateTime); cells: [
DataCell(Text(name)),
return DataRow( DataCell(Text(intensity.toString())),
cells: [ DataCell(Text(duration.toString())),
DataCell(Text(name)), DataCell(getIconForType(type)),
DataCell(Text(intensity.toString())), DataCell(Text(formattedCreatedAt)),
DataCell(Text(duration.toString())), ],
DataCell(getIconForType(type)), );
DataCell(Text(formattedCreatedAt)), }
], }
); return DataRow(cells: []);
} }).toList(),
} ),
return const DataRow(cells: []); ),
}).toList(),
), ),
), );
), },
), ),
bottomNavigationBar: BottomBar( bottomNavigationBar: BottomBar(
currentIndex: appState.currentIndex, currentIndex: appState.currentIndex,

View file

@ -111,26 +111,35 @@ class _SliderPageState extends State<SliderPage> {
ElevatedButton.icon( ElevatedButton.icon(
icon: const Icon(Icons.flash_on), icon: const Icon(Icons.flash_on),
label: const Text('Shock'), label: const Text('Shock'),
onPressed: () { onPressed: () async {
if (intensityValue < 1 || timeValue < 1) { if (intensityValue < 1 || timeValue < 1) {
// this whole thing was written by a silly little cat :3 // this whole thing was written by a silly little cat :3
} else { } else {
HapticFeedback.vibrate(); HapticFeedback.vibrate();
sendApiRequest(intensityValue, timeValue, 1); bool success = await sendApiRequest(
showToast('API request sent'); intensityValue, timeValue, 1);
if (success) {
showToast('Shock API request successful');
} else {
showToast('Failed to send Shock API request');
}
} }
}, }
), ),
const SizedBox(width: 8.0), const SizedBox(width: 8.0),
ElevatedButton.icon( ElevatedButton.icon(
icon: const Icon(Icons.vibration), icon: const Icon(Icons.vibration),
label: const Text('Vibrate'), label: const Text('Vibrate'),
onPressed: () { onPressed: () async {
if (intensityValue < 1 || timeValue < 1) { if (intensityValue < 1 || timeValue < 1) {
} else { } else {
HapticFeedback.vibrate(); HapticFeedback.vibrate();
sendApiRequest(intensityValue, timeValue, 2); bool success = await sendApiRequest(intensityValue, timeValue, 2);
showToast('API request sent'); if (success) {
showToast('Vibrate API request successful');
} else {
showToast('Failed to send Vibrate API request');
}
} }
}, },
), ),

View file

@ -7,6 +7,7 @@ import 'logs_page.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'NewShareLinkPage.dart';
class settings_page extends StatefulWidget { class settings_page extends StatefulWidget {
const settings_page({Key? key}); const settings_page({Key? key});
@ -39,8 +40,6 @@ class _SettingsPageState extends State<settings_page> {
setState(() { setState(() {
apiKeyController.text = prefs.getString('apiKey') ?? ''; apiKeyController.text = prefs.getString('apiKey') ?? '';
shockerIdController.text = prefs.getString('shockerId') ?? ''; shockerIdController.text = prefs.getString('shockerId') ?? '';
intensityLimitController.text = prefs.getString('intensityLimit') ?? '';
durationLimitController.text = prefs.getString('durationLimit') ?? '';
numberOfLogs = prefs.getDouble(logsSharedPreferenceKey) ?? 30; numberOfLogs = prefs.getDouble(logsSharedPreferenceKey) ?? 30;
}); });
} }
@ -50,8 +49,6 @@ class _SettingsPageState extends State<settings_page> {
prefs.setString('apiKey', apiKeyController.text); prefs.setString('apiKey', apiKeyController.text);
prefs.setString('shockerId', shockerIdController.text); prefs.setString('shockerId', shockerIdController.text);
await runChecks(); await runChecks();
// Save the selected number of logs to shared preferences
prefs.setDouble(logsSharedPreferenceKey, numberOfLogs); prefs.setDouble(logsSharedPreferenceKey, numberOfLogs);
Navigator.pop(context); Navigator.pop(context);
@ -135,7 +132,8 @@ class _SettingsPageState extends State<settings_page> {
appBar: AppBar( appBar: AppBar(
title: const Text('Settings'), title: const Text('Settings'),
), ),
body: Padding( body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -199,6 +197,26 @@ class _SettingsPageState extends State<settings_page> {
child: const Text('Save'), child: const Text('Save'),
), ),
const SizedBox(height: 15), const SizedBox(height: 15),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const NewShareLinkPage()),
);
},
child: const Text('New Share Link'),
),
ElevatedButton(
onPressed: () {
},
child: const Text('My Share Links'),
),
],
),
FutureBuilder<String>( FutureBuilder<String>(
future: fetchCommitData(), future: fetchCommitData(),
builder: (context, snapshot) { builder: (context, snapshot) {
@ -208,7 +226,7 @@ class _SettingsPageState extends State<settings_page> {
return Text('Error: ${snapshot.error}'); return Text('Error: ${snapshot.error}');
} else { } else {
return Text( return Text(
'App Version: 0.3-rc0[hf] - Build Date: Dec. 11, 2023\n' 'App Version: 0.3-rc3 - Build Date: Dec. 11, 2023\n'
'(C) Mercury, 2023\n' '(C) Mercury, 2023\n'
'Connected to api.shocklink.org, version ${snapshot.data}', 'Connected to api.shocklink.org, version ${snapshot.data}',
textAlign: TextAlign.left, textAlign: TextAlign.left,
@ -220,6 +238,7 @@ class _SettingsPageState extends State<settings_page> {
], ],
), ),
), ),
),
bottomNavigationBar: BottomBar( bottomNavigationBar: BottomBar(
currentIndex: appState.currentIndex, currentIndex: appState.currentIndex,
onTap: (index) { onTap: (index) {