1- import 'dart:convert' ;
21import 'dart:io' ;
2+
33import 'package:flutter/material.dart' ;
4+ import 'package:get/get.dart' ;
45import 'package:image_picker/image_picker.dart' ;
5- import 'package:http/http.dart' as http;
6- import 'package:path/path.dart' as path;
7- import 'package:flutter_dotenv/flutter_dotenv.dart' ;
8- import 'utils/appdata.dart' ;
6+
7+ import 'services/feedback_service.dart' ;
98
109class IssueForm extends StatefulWidget {
1110 const IssueForm ({super .key});
1211
1312 @override
14- IssueFormState createState () => IssueFormState ();
13+ State < IssueForm > createState () => _IssueFormState ();
1514}
1615
17- class IssueFormState extends State <IssueForm > {
16+ class _IssueFormState extends State <IssueForm > {
1817 final TextEditingController _nameController = TextEditingController ();
1918 final TextEditingController _titleController = TextEditingController ();
2019 final TextEditingController _bodyController = TextEditingController ();
21- File ? _imageFile;
2220 final ImagePicker _picker = ImagePicker ();
23- String option = 'Issue' ;
24- bool isLoading = false ;
2521
26- Future <void > createIssue (
27- String title, String body, String ? label, File ? imageFile) async {
28- List <String > labels = label == 'Issue'
29- ? ['bug' ]
30- : label == 'Suggestion'
31- ? ['ui' ]
32- : ['bug' ];
22+ File ? _imageFile;
23+ bool isLoading = false ;
24+ FeedbackCategory category = FeedbackCategory .issue;
3325
34- String imageUrl = '' ;
35- if (imageFile != null ) {
36- // Upload image to GitHub
37- imageUrl = await uploadImageToGitHub (imageFile);
26+ FeedbackService get feedbackService {
27+ if (! Get .isRegistered <FeedbackService >()) {
28+ Get .put (FeedbackService (), permanent: true );
3829 }
30+ return FeedbackService .to;
31+ }
3932
40- final issueBody = body +
41- (imageUrl.isNotEmpty ? '\n\n <img src="$imageUrl " width=250/>' : '' ) +
42- (_nameController.text.isNotEmpty
43- ? '\n\n $option raised by ${_nameController .text .trim ()}.'
44- : '' );
45-
46- final response = await http.post (
47- Uri .parse ('$githubApi /issues' ),
48- headers: {
49- 'Authorization' : 'token ${dotenv .env ['githubToken' ]}' ,
50- 'Accept' : 'application/vnd.github.v3+json' ,
51- 'Content-Type' : 'application/json' ,
52- },
53- body: jsonEncode ({
54- 'title' : title,
55- 'body' : issueBody,
56- 'labels' : labels, // Optional: Labels for the issue
57- }),
58- );
59-
60- if (response.statusCode == 201 ) {
61- // Success
62- String issueUrl = jsonDecode (response.body)['html_url' ];
33+ Future <void > pickImage () async {
34+ final picked = await _picker.pickImage (source: ImageSource .gallery);
35+ if (! mounted) return ;
36+ setState (() {
37+ _imageFile = picked != null ? File (picked.path) : null ;
38+ });
39+ }
6340
64- // Clear inputs
41+ Future <void > _submit () async {
42+ final title = _titleController.text.trim ();
43+ final body = _bodyController.text.trim ();
44+ if (title.isEmpty || body.isEmpty) return ;
45+
46+ setState (() => isLoading = true );
47+ try {
48+ final issueUrl = await feedbackService.createIssue (
49+ title: title,
50+ body: body,
51+ category: category,
52+ reporter: _nameController.text.trim (),
53+ screenshot: _imageFile,
54+ );
55+ if (! mounted) return ;
6556 _nameController.clear ();
6657 _titleController.clear ();
6758 _bodyController.clear ();
68- setState (() {
69- _imageFile = null ;
70- });
71-
72- // Show success dialog
59+ setState (() => _imageFile = null );
7360 showDialog (
7461 context: context,
75- builder: (BuildContext context) {
76- return AlertDialog (
77- title: const Text ('Success' ),
78- content: Text ('Issue created: $issueUrl ' ),
79- actions: [
80- TextButton (
81- onPressed: () {
82- Navigator .of (context).pop ();
83- },
84- child: const Text ('OK' ),
85- ),
86- ],
87- );
88- },
62+ builder: (_) => AlertDialog (
63+ title: const Text ('Success' ),
64+ content: Text ('Issue created: ${issueUrl ?? 'N/A' }' ),
65+ actions: [
66+ TextButton (
67+ onPressed: () => Navigator .of (context).pop (),
68+ child: const Text ('OK' ),
69+ ),
70+ ],
71+ ),
8972 );
90- } else {
91- // Error handling
92- print ('Failed to create issue: ${response .statusCode }' );
93- print (response.body);
94- }
95- }
96-
97- Future <String > uploadImageToGitHub (File imageFile) async {
98- final String base64Image = base64Encode (imageFile.readAsBytesSync ());
99- final String fileName = path.basename (imageFile.path);
100-
101- final response = await http.put (
102- Uri .parse ('$githubApi /contents/$fileName ' ),
103- headers: {
104- 'Authorization' : 'token ${dotenv .env ['githubToken' ]}' ,
105- 'Accept' : 'application/vnd.github.v3+json' ,
106- 'Content-Type' : 'application/json' ,
107- },
108- body: jsonEncode ({
109- 'message' : 'Uploading screenshot' ,
110- 'content' : base64Image,
111- 'branch' : 'assets' ,
112- }),
113- );
114-
115- if (response.statusCode == 201 ) {
116- return jsonDecode (response.body)['content' ]['download_url' ];
117- } else {
118- print ('Failed to upload image: ${response .statusCode }' );
119- return '' ;
120- }
121- }
122-
123- Future <void > pickImage () async {
124- final pickedFile = await _picker.pickImage (source: ImageSource .gallery);
125-
126- setState (() {
127- if (pickedFile != null ) {
128- _imageFile = File (pickedFile.path);
129- } else {
130- print ('No image selected.' );
73+ } catch (e) {
74+ if (! mounted) return ;
75+ ScaffoldMessenger .of (context).showSnackBar (
76+ SnackBar (content: Text ('Failed to submit feedback: $e ' )),
77+ );
78+ } finally {
79+ if (mounted) {
80+ setState (() => isLoading = false );
13181 }
132- });
82+ }
13383 }
13484
13585 @override
@@ -143,41 +93,37 @@ class IssueFormState extends State<IssueForm> {
14393 @override
14494 Widget build (BuildContext context) {
14595 return Scaffold (
146- appBar: AppBar (
147- title: const Text ('Feedback' ),
148- ),
96+ appBar: AppBar (title: const Text ('Feedback' )),
14997 body: SingleChildScrollView (
150- padding: const EdgeInsets .all (20.0 ),
98+ padding: const EdgeInsets .all (20 ),
15199 child: Column (
152100 crossAxisAlignment: CrossAxisAlignment .start,
153101 children: [
154102 const Text (
155- 'Choose the any option' ,
156- style: TextStyle (
157- fontSize: 18 ,
158- fontWeight: FontWeight .bold,
159- ),
103+ 'Choose an option' ,
104+ style: TextStyle (fontSize: 18 , fontWeight: FontWeight .bold),
160105 ),
161- DropdownButton <String >(
162- value: option, // Set the initial value
163- items: < String > ['Issue' , 'Suggestion' ].map ((String value) {
164- return DropdownMenuItem <String >(
165- value: value,
166- child: Text (value),
167- );
168- }).toList (),
106+ DropdownButton <FeedbackCategory >(
107+ value: category,
108+ items: const [
109+ DropdownMenuItem (
110+ value: FeedbackCategory .issue,
111+ child: Text ('Issue' ),
112+ ),
113+ DropdownMenuItem (
114+ value: FeedbackCategory .suggestion,
115+ child: Text ('Suggestion' ),
116+ ),
117+ ],
169118 onChanged: (newValue) {
170- setState (() {
171- option = newValue! ;
172- });
119+ if (newValue == null ) return ;
120+ setState (() => category = newValue);
173121 },
174122 ),
123+ const SizedBox (height: 10 ),
175124 const Text (
176125 'Your Name' ,
177- style: TextStyle (
178- fontSize: 18 ,
179- fontWeight: FontWeight .bold,
180- ),
126+ style: TextStyle (fontSize: 18 , fontWeight: FontWeight .bold),
181127 ),
182128 TextField (
183129 controller: _nameController,
@@ -186,33 +132,28 @@ class IssueFormState extends State<IssueForm> {
186132 border: OutlineInputBorder (),
187133 ),
188134 ),
135+ const SizedBox (height: 20 ),
189136 Text (
190- '$option Title' ,
191- style: const TextStyle (
192- fontSize: 18 ,
193- fontWeight: FontWeight .bold,
194- ),
137+ '${category == FeedbackCategory .issue ? 'Issue' : 'Suggestion' } Title' ,
138+ style: const TextStyle (fontSize: 18 , fontWeight: FontWeight .bold),
195139 ),
196140 TextField (
197141 controller: _titleController,
198- decoration: InputDecoration (
199- hintText: 'Enter ${ option . toLowerCase ()} title' ,
200- border: const OutlineInputBorder (),
142+ decoration: const InputDecoration (
143+ hintText: 'Enter title' ,
144+ border: OutlineInputBorder (),
201145 ),
202146 ),
203147 const SizedBox (height: 20 ),
204148 Text (
205- '$option Body' ,
206- style: const TextStyle (
207- fontSize: 18 ,
208- fontWeight: FontWeight .bold,
209- ),
149+ '${category == FeedbackCategory .issue ? 'Issue' : 'Suggestion' } Body' ,
150+ style: const TextStyle (fontSize: 18 , fontWeight: FontWeight .bold),
210151 ),
211152 TextField (
212153 controller: _bodyController,
213- decoration: InputDecoration (
214- hintText: 'Enter ${ option . toLowerCase ()} body ' ,
215- border: const OutlineInputBorder (),
154+ decoration: const InputDecoration (
155+ hintText: 'Enter details ' ,
156+ border: OutlineInputBorder (),
216157 ),
217158 maxLines: null ,
218159 ),
@@ -227,34 +168,22 @@ class IssueFormState extends State<IssueForm> {
227168 if (_imageFile != null )
228169 ClipRRect (
229170 borderRadius: BorderRadius .circular (5 ),
230- child: Image .file (
231- _imageFile! ,
232- height: 100 ,
233- fit: BoxFit .cover,
234- ),
171+ child: Image .file (_imageFile! , height: 100 , fit: BoxFit .cover),
235172 ),
236173 ],
237174 ),
238175 const SizedBox (height: 20 ),
239176 ElevatedButton (
240- onPressed: () async {
241- setState (() {
242- isLoading = true ;
243- });
244- final title = _titleController.text;
245- final body = _bodyController.text;
246- if (title.isNotEmpty && body.isNotEmpty) {
247- await createIssue (title, body, option, _imageFile);
248- }
249- setState (() {
250- isLoading = false ;
251- });
252- },
177+ onPressed: isLoading ? null : _submit,
253178 child: isLoading
254- ? const CircularProgressIndicator (
255- color: Colors .white,
179+ ? const SizedBox (
180+ width: 20 ,
181+ height: 20 ,
182+ child: CircularProgressIndicator (strokeWidth: 2 , color: Colors .white),
256183 )
257- : Text ('Send $option ' ),
184+ : Text (
185+ 'Send ${category == FeedbackCategory .issue ? 'Issue' : 'Suggestion' }' ,
186+ ),
258187 ),
259188 ],
260189 ),
0 commit comments