From 81316684498f24418e276a8d19a436e18aa16339 Mon Sep 17 00:00:00 2001 From: Rivers Cuomo Date: Mon, 22 Jul 2024 08:31:19 -0700 Subject: [PATCH] ingredient form submit --- lib/screens/update_screen.dart | 3 +- lib/services/spotify_service.dart | 64 ++++--- lib/widgets/ingredient_management_widget.dart | 172 +++++++++++------- lib/widgets/job_form.dart | 4 +- 4 files changed, 152 insertions(+), 91 deletions(-) diff --git a/lib/screens/update_screen.dart b/lib/screens/update_screen.dart index e4d1194..0842dd3 100644 --- a/lib/screens/update_screen.dart +++ b/lib/screens/update_screen.dart @@ -217,7 +217,8 @@ class _UpdateScreenState extends State { onChanged: (value) => updateJob(index, job.copyWith(playlistId: value)), ), - IngredientManagementWidget( + const SizedBox(height: 20), + IngredientForm( initialIngredients: job.recipe, onIngredientsChanged: (updatedIngredients) { setState(() { diff --git a/lib/services/spotify_service.dart b/lib/services/spotify_service.dart index b011c5b..3840c4b 100644 --- a/lib/services/spotify_service.dart +++ b/lib/services/spotify_service.dart @@ -1,12 +1,13 @@ -import 'dart:convert'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:http/http.dart' as http; -import 'package:spotify/spotify.dart'; +import 'dart:convert'; import 'dart:html' as html; +import 'package:spotify/spotify.dart'; class SpotifyService { static const FlutterSecureStorage _secureStorage = FlutterSecureStorage(); static const String _accessTokenKey = 'accessToken'; + static const String _refreshTokenKey = 'refreshToken'; late SpotifyApi _spotify; final String clientId; @@ -45,10 +46,11 @@ class SpotifyService { if (response.statusCode == 200) { final tokenData = json.decode(response.body); final accessToken = tokenData['access_token']; + final refreshToken = tokenData['refresh_token']; await _secureStorage.write(key: _accessTokenKey, value: accessToken); + await _secureStorage.write(key: _refreshTokenKey, value: refreshToken); print('Access Token: $accessToken'); - _spotify = SpotifyApi(SpotifyApiCredentials(clientId, clientSecret, - accessToken: accessToken)); + _spotify = SpotifyApi(SpotifyApiCredentials(clientId, clientSecret, accessToken: accessToken)); } else { print('Failed to exchange code for token: ${response.body}'); throw Exception('Failed to authenticate with Spotify'); @@ -71,9 +73,8 @@ class SpotifyService { return await _secureStorage.read(key: _accessTokenKey); } - // refresh token Future refreshAccessToken() async { - final refreshToken = await _secureStorage.read(key: 'refreshToken'); + final refreshToken = await _secureStorage.read(key: _refreshTokenKey); final tokenEndpoint = Uri.parse('https://accounts.spotify.com/api/token'); final response = await http.post( tokenEndpoint, @@ -85,46 +86,63 @@ class SpotifyService { 'client_secret': clientSecret, }, ); - print(response); if (response.statusCode == 200) { final tokenData = json.decode(response.body); final accessToken = tokenData['access_token']; await _secureStorage.write(key: _accessTokenKey, value: accessToken); - print('Access Token: $accessToken'); - _spotify = SpotifyApi(SpotifyApiCredentials(clientId, clientSecret, - accessToken: accessToken)); + print('Access Token refreshed: $accessToken'); + _spotify = SpotifyApi(SpotifyApiCredentials(clientId, clientSecret, accessToken: accessToken)); } else { print('Failed to refresh token: ${response.body}'); throw Exception('Failed to refresh token'); } } - Future> getUserPlaylists(String userId) async { + Future _ensureAuthenticated() async { final accessToken = await getAccessToken(); if (accessToken == null) { throw Exception('Not authenticated'); } - _spotify = SpotifyApi(SpotifyApiCredentials(clientId, clientSecret, - accessToken: accessToken)); + _spotify = SpotifyApi(SpotifyApiCredentials(clientId, clientSecret, accessToken: accessToken)); + } + + Future getPlaylist(String playlistId) async { + await _ensureAuthenticated(); + try { + return await _spotify.playlists.get(playlistId); + } catch (e) { + print('Error fetching playlist: $e'); + rethrow; + } + } + + Future> getUserPlaylists(String userId) async { + await _ensureAuthenticated(); final playlists = await _spotify.playlists.getUsersPlaylists(userId).all(); return playlists.toList(); } - Future updatePlaylist(String playlistId, - {String? name, String? description}) async { - final accessToken = await getAccessToken(); - if (accessToken == null) { - throw Exception('Not authenticated'); - } - // _spotify = SpotifyApi(SpotifyApiCredentials(clientId, clientSecret, accessToken: accessToken)); - // await _spotify.playlists.updatePlaylist(playlistId, description: description); + Future updatePlaylist(String playlistId, {String? name, String? description}) async { + await _ensureAuthenticated(); + // await _spotify.playlists.changePlaylistDetails(playlistId, name: name, description: description); + } + + Future getArtist(String artistId) async { + await _ensureAuthenticated(); + return await _spotify.artists.get(artistId); + } + + Future> getPlaylistTracks(String playlistId) async { + await _ensureAuthenticated(); + return await _spotify.playlists.getTracksByPlaylistId(playlistId).all(); } Future logout() async { await _secureStorage.delete(key: _accessTokenKey); - _initializeSpotify(); // Reset to unauthenticated state + await _secureStorage.delete(key: _refreshTokenKey); + _initializeSpotify(); } // Add more methods as needed... -} +} \ No newline at end of file diff --git a/lib/widgets/ingredient_management_widget.dart b/lib/widgets/ingredient_management_widget.dart index 6aabea4..f4e2fd3 100644 --- a/lib/widgets/ingredient_management_widget.dart +++ b/lib/widgets/ingredient_management_widget.dart @@ -1,108 +1,150 @@ import 'package:flutter/material.dart'; import 'package:spotkin_flutter/app_core.dart'; +import 'package:flutter/material.dart'; -class IngredientManagementWidget extends StatefulWidget { +class IngredientForm extends StatefulWidget { final List initialIngredients; final Function(List) onIngredientsChanged; - const IngredientManagementWidget({ + const IngredientForm({ Key? key, required this.initialIngredients, required this.onIngredientsChanged, }) : super(key: key); @override - _IngredientManagementWidgetState createState() => _IngredientManagementWidgetState(); + _IngredientFormState createState() => _IngredientFormState(); } -class _IngredientManagementWidgetState extends State { - late List _ingredients; +class _IngredientFormState extends State { + final _formKey = GlobalKey(); + late List _ingredientRows; @override void initState() { super.initState(); - _ingredients = List.from(widget.initialIngredients); + _initIngredientRows(); + } + + void _initIngredientRows() { + _ingredientRows = widget.initialIngredients.map((ingredient) => + IngredientFormRow( + playlistController: TextEditingController(text: ingredient.sourcePlaylistId), + quantityController: TextEditingController(text: ingredient.quantity.toString()), + ) + ).toList(); + if (_ingredientRows.isEmpty) { + _addNewRow(); + } } @override - void didUpdateWidget(IngredientManagementWidget oldWidget) { + void didUpdateWidget(IngredientForm oldWidget) { super.didUpdateWidget(oldWidget); if (widget.initialIngredients != oldWidget.initialIngredients) { - setState(() { - _ingredients = List.from(widget.initialIngredients); - }); + _initIngredientRows(); } } - void _updateIngredient(int index, Ingredient updatedIngredient) { + void _addNewRow() { setState(() { - _ingredients[index] = updatedIngredient; - widget.onIngredientsChanged(_ingredients); + _ingredientRows.add(IngredientFormRow( + playlistController: TextEditingController(), + quantityController: TextEditingController(), + )); }); } void _removeIngredient(int index) { setState(() { - _ingredients.removeAt(index); - widget.onIngredientsChanged(_ingredients); + _ingredientRows.removeAt(index); }); } + void _submitAndAddIngredient() { + if (_formKey.currentState!.validate()) { + List updatedIngredients = _ingredientRows.map((row) => + Ingredient( + sourcePlaylistName: '', // You might want to update this if needed + sourcePlaylistId: row.playlistController.text, + quantity: int.tryParse(row.quantityController.text) ?? 0, + ) + ).toList(); + + widget.onIngredientsChanged(updatedIngredients); + _addNewRow(); + } + } + @override Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Ingredients', style: Theme.of(context).textTheme.titleMedium), - ..._ingredients.asMap().entries.map((entry) { - int idx = entry.key; - Ingredient ingredient = entry.value; - return Row( - children: [ - Expanded( - child: TextFormField( - key: ValueKey('sourcePlaylistId_$idx'), - initialValue: ingredient.sourcePlaylistId, - decoration: const InputDecoration( - labelText: 'Source playlist link', - hintText: 'Enter Spotify playlist link or ID', + return Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Ingredients', style: Theme.of(context).textTheme.titleMedium), + ..._ingredientRows.asMap().entries.map((entry) { + int idx = entry.key; + IngredientFormRow row = entry.value; + return Row( + children: [ + Expanded( + child: TextFormField( + controller: row.playlistController, + decoration: const InputDecoration( + labelText: 'Source playlist link', + hintText: 'Enter Spotify playlist link or ID', + ), + validator: Utils.validateSpotifyPlaylistInput, ), - validator: Utils.validateSpotifyPlaylistInput, - onChanged: (value) { - _updateIngredient(idx, ingredient.copyWith(sourcePlaylistId: value)); - }, ), - ), - const SizedBox(width: 10), - Expanded( - child: TextFormField( - key: ValueKey('quantity_$idx'), - initialValue: ingredient.quantity.toString(), - decoration: const InputDecoration(labelText: 'Quantity'), - keyboardType: TextInputType.number, - onChanged: (value) { - _updateIngredient(idx, ingredient.copyWith(quantity: int.tryParse(value) ?? 0)); - }, + const SizedBox(width: 10), + Expanded( + child: TextFormField( + controller: row.quantityController, + decoration: const InputDecoration(labelText: 'Quantity'), + keyboardType: TextInputType.number, + validator: (value) { + if (value == null || value.isEmpty || int.tryParse(value) == null) { + return 'Please enter a valid number'; + } + return null; + }, + ), ), - ), - IconButton( - icon: const Icon(Icons.delete), - onPressed: () => _removeIngredient(idx), - ), - - ], - ); - }), - ElevatedButton( - onPressed: () { - setState(() { - _ingredients.add(Ingredient(sourcePlaylistName: '', sourcePlaylistId: '', quantity: 0)); - widget.onIngredientsChanged(_ingredients); - }); - }, - child: const Text('Add Ingredient'), - ), - ], + IconButton( + icon: const Icon(Icons.delete), + onPressed: () => _removeIngredient(idx), + ), + ], + ); + }), + ElevatedButton( + onPressed: _submitAndAddIngredient, + child: const Text('Add Ingredient'), + ), + ], + ), ); } + + @override + void dispose() { + for (var row in _ingredientRows) { + row.playlistController.dispose(); + row.quantityController.dispose(); + } + super.dispose(); + } +} + +class IngredientFormRow { + final TextEditingController playlistController; + final TextEditingController quantityController; + + IngredientFormRow({ + required this.playlistController, + required this.quantityController, + }); } \ No newline at end of file diff --git a/lib/widgets/job_form.dart b/lib/widgets/job_form.dart index 70a5207..5163152 100644 --- a/lib/widgets/job_form.dart +++ b/lib/widgets/job_form.dart @@ -29,9 +29,9 @@ class _JobFormState extends State { ), TextFormField( controller: _playlistIdController, - decoration: const InputDecoration(labelText: 'Target playlist link'), + decoration: const InputDecoration(labelText: 'Target playlist'), validator: (value) => - value?.isEmpty ?? true ? 'Please enter a playlist ID' : null, + value?.isEmpty ?? true ? 'Please enter a Spotify playlist link' : null, ), const SizedBox(height: 10),