397 lines
14 KiB
Dart
397 lines
14 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:gamer_updater/game.dart';
|
|
import 'package:image_picker/image_picker.dart';
|
|
|
|
class GameCard extends StatefulWidget {
|
|
final Game game;
|
|
final Function(Game) onGameUpdated;
|
|
final Function()? onDelete;
|
|
final bool isNameEditable;
|
|
|
|
const GameCard({
|
|
super.key,
|
|
required this.game,
|
|
required this.onGameUpdated,
|
|
this.onDelete,
|
|
this.isNameEditable = false,
|
|
});
|
|
|
|
@override
|
|
State<GameCard> createState() => _GameCardState();
|
|
}
|
|
|
|
class _GameCardState extends State<GameCard>
|
|
with SingleTickerProviderStateMixin {
|
|
late final AnimationController _controller;
|
|
bool _isLoading = false;
|
|
int _deleteClickCount = 0;
|
|
late TextEditingController _nameController;
|
|
late TextEditingController _versionRegexController;
|
|
late TextEditingController _rssFeedUrlController;
|
|
late TextEditingController _lastPlayedController;
|
|
late FocusNode _nameFocus;
|
|
late FocusNode _versionRegexFocus;
|
|
late FocusNode _rssFeedUrlFocus;
|
|
late FocusNode _lastPlayedFocus;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_controller = AnimationController(
|
|
duration: const Duration(seconds: 1),
|
|
vsync: this,
|
|
);
|
|
_nameController = TextEditingController(text: widget.game.name);
|
|
_versionRegexController = TextEditingController(
|
|
text: widget.game.versionRegex,
|
|
);
|
|
_rssFeedUrlController = TextEditingController(text: widget.game.rssFeedUrl);
|
|
_lastPlayedController = TextEditingController(text: widget.game.lastPlayed);
|
|
|
|
_nameFocus = FocusNode();
|
|
_versionRegexFocus = FocusNode();
|
|
_rssFeedUrlFocus = FocusNode();
|
|
_lastPlayedFocus = FocusNode();
|
|
|
|
_setupFocusListeners();
|
|
}
|
|
|
|
void _setupFocusListeners() {
|
|
void updateGame() {
|
|
try {
|
|
var name =
|
|
widget.isNameEditable ? _nameController.text : widget.game.name;
|
|
if (name.isNotEmpty) {
|
|
widget.onGameUpdated(
|
|
Game(
|
|
name: name,
|
|
versionRegex: _versionRegexController.text,
|
|
lastPlayed: _lastPlayedController.text,
|
|
rssFeedUrl: _rssFeedUrlController.text,
|
|
actualVersion: widget.game.actualVersion,
|
|
lastUpdated: widget.game.lastUpdated,
|
|
imageData: widget.game.imageData,
|
|
),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(
|
|
context,
|
|
).showSnackBar(SnackBar(content: Text(e.toString())));
|
|
}
|
|
}
|
|
}
|
|
|
|
_nameFocus.addListener(() {
|
|
if (!_nameFocus.hasFocus) updateGame();
|
|
});
|
|
_versionRegexFocus.addListener(() {
|
|
if (!_versionRegexFocus.hasFocus) updateGame();
|
|
});
|
|
_rssFeedUrlFocus.addListener(() {
|
|
if (!_rssFeedUrlFocus.hasFocus) updateGame();
|
|
});
|
|
_lastPlayedFocus.addListener(() {
|
|
if (!_lastPlayedFocus.hasFocus) updateGame();
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
_nameController.dispose();
|
|
_versionRegexController.dispose();
|
|
_rssFeedUrlController.dispose();
|
|
_lastPlayedController.dispose();
|
|
_nameFocus.dispose();
|
|
_versionRegexFocus.dispose();
|
|
_rssFeedUrlFocus.dispose();
|
|
_lastPlayedFocus.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _refreshVersion() async {
|
|
setState(() => _isLoading = true);
|
|
_controller.repeat();
|
|
|
|
final updatedGame = widget.game;
|
|
try {
|
|
await updatedGame.updateActualVersion();
|
|
} catch (e) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(
|
|
context,
|
|
).showSnackBar(SnackBar(content: Text(e.toString())));
|
|
}
|
|
}
|
|
widget.onGameUpdated(updatedGame);
|
|
|
|
_controller.stop();
|
|
_controller.reset();
|
|
setState(() => _isLoading = false);
|
|
}
|
|
|
|
void _handleDeleteClick() {
|
|
setState(() {
|
|
_deleteClickCount++;
|
|
if (_deleteClickCount >= 2) {
|
|
widget.onDelete?.call();
|
|
_deleteClickCount = 0;
|
|
}
|
|
});
|
|
|
|
// Reset click count after a delay
|
|
Future.delayed(const Duration(seconds: 1), () {
|
|
if (mounted) {
|
|
setState(() => _deleteClickCount = 0);
|
|
}
|
|
});
|
|
}
|
|
|
|
Future<void> _pickImage() async {
|
|
final picker = ImagePicker();
|
|
final image = await picker.pickImage(source: ImageSource.gallery);
|
|
if (image != null) {
|
|
final imageBytes = await image.readAsBytes();
|
|
final updatedGame = await GameRepository.updateImage(
|
|
Game(
|
|
name: widget.game.name,
|
|
versionRegex: _versionRegexController.text,
|
|
lastPlayed: _lastPlayedController.text,
|
|
rssFeedUrl: _rssFeedUrlController.text,
|
|
actualVersion: widget.game.actualVersion,
|
|
lastUpdated: widget.game.lastUpdated,
|
|
imageData: imageBytes,
|
|
),
|
|
);
|
|
widget.onGameUpdated(updatedGame);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final isUpToDate = widget.game.actualVersion == widget.game.lastPlayed;
|
|
final hasImage = widget.game.imageData != null;
|
|
|
|
return Card(
|
|
child: Stack(
|
|
children: [
|
|
if (hasImage)
|
|
Positioned.fill(
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(12),
|
|
child: Opacity(
|
|
opacity: 0.4,
|
|
child: Image.memory(widget.game.imageData!, fit: BoxFit.cover),
|
|
),
|
|
),
|
|
),
|
|
if (hasImage)
|
|
Positioned.fill(
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(12),
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
colors: [
|
|
Colors.black.withAlpha(110),
|
|
Colors.black.withAlpha(90),
|
|
Colors.black.withAlpha(70),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 24.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
SizedBox(
|
|
height: 40,
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Expanded(
|
|
child: TextField(
|
|
controller: _nameController,
|
|
focusNode: _nameFocus,
|
|
style: Theme.of(context).textTheme.titleLarge
|
|
?.copyWith(color: hasImage ? Colors.white : null),
|
|
enabled: widget.isNameEditable,
|
|
decoration: const InputDecoration.collapsed(
|
|
hintText: 'New Game',
|
|
),
|
|
onSubmitted: (_) => _nameFocus.unfocus(),
|
|
),
|
|
),
|
|
Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
if (!widget.isNameEditable)
|
|
IconButton(
|
|
icon: const Icon(Icons.image),
|
|
color: hasImage ? Colors.white : null,
|
|
onPressed: _pickImage,
|
|
),
|
|
if (widget.onDelete != null)
|
|
IconButton(
|
|
icon: Icon(
|
|
Icons.delete,
|
|
color:
|
|
_deleteClickCount > 0
|
|
? Colors.red
|
|
: (hasImage ? Colors.white : null),
|
|
),
|
|
onPressed: _handleDeleteClick,
|
|
),
|
|
if (!widget.isNameEditable)
|
|
RotationTransition(
|
|
turns: _controller,
|
|
child: IconButton(
|
|
icon: Icon(
|
|
Icons.refresh,
|
|
color: hasImage ? Colors.white : null,
|
|
),
|
|
onPressed: _isLoading ? null : _refreshVersion,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Row(
|
|
children: [
|
|
SizedBox(
|
|
width: 200,
|
|
child: Text(
|
|
'Version:',
|
|
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
|
color: hasImage ? Colors.white : null,
|
|
),
|
|
),
|
|
),
|
|
Text(
|
|
widget.game.actualVersion.isEmpty
|
|
? 'Unknown'
|
|
: widget.game.actualVersion,
|
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
|
color: isUpToDate ? Colors.green : Colors.red,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
children: [
|
|
SizedBox(
|
|
width: 200,
|
|
child: Text(
|
|
'Last Updated:',
|
|
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
|
color: hasImage ? Colors.white : null,
|
|
),
|
|
),
|
|
),
|
|
Text(
|
|
widget.game.lastUpdated.isEmpty
|
|
? 'Never'
|
|
: DateTime.parse(widget.game.lastUpdated).toString(),
|
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
|
color: hasImage ? Colors.white : null,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
children: [
|
|
SizedBox(
|
|
width: 200,
|
|
child: Text(
|
|
'Last Played:',
|
|
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
|
color: hasImage ? Colors.white : null,
|
|
),
|
|
),
|
|
),
|
|
Text(
|
|
widget.game.lastPlayed,
|
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
|
color: isUpToDate ? Colors.green : Colors.red,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
const Divider(),
|
|
const SizedBox(height: 16),
|
|
TextField(
|
|
controller: _versionRegexController,
|
|
focusNode: _versionRegexFocus,
|
|
style: TextStyle(color: hasImage ? Colors.white : null),
|
|
decoration: InputDecoration(
|
|
labelText: 'Version Regex',
|
|
labelStyle: TextStyle(
|
|
color: hasImage ? Colors.white70 : null,
|
|
),
|
|
enabledBorder:
|
|
hasImage
|
|
? const UnderlineInputBorder(
|
|
borderSide: BorderSide(color: Colors.white70),
|
|
)
|
|
: null,
|
|
),
|
|
onSubmitted: (_) => _versionRegexFocus.unfocus(),
|
|
),
|
|
TextField(
|
|
controller: _rssFeedUrlController,
|
|
focusNode: _rssFeedUrlFocus,
|
|
style: TextStyle(color: hasImage ? Colors.white : null),
|
|
decoration: InputDecoration(
|
|
labelText: 'RSS Feed URL',
|
|
labelStyle: TextStyle(
|
|
color: hasImage ? Colors.white70 : null,
|
|
),
|
|
enabledBorder:
|
|
hasImage
|
|
? const UnderlineInputBorder(
|
|
borderSide: BorderSide(color: Colors.white70),
|
|
)
|
|
: null,
|
|
),
|
|
onSubmitted: (_) => _rssFeedUrlFocus.unfocus(),
|
|
),
|
|
TextField(
|
|
controller: _lastPlayedController,
|
|
focusNode: _lastPlayedFocus,
|
|
style: TextStyle(color: hasImage ? Colors.white : null),
|
|
decoration: InputDecoration(
|
|
labelText: 'Last Played',
|
|
labelStyle: TextStyle(
|
|
color: hasImage ? Colors.white70 : null,
|
|
),
|
|
enabledBorder:
|
|
hasImage
|
|
? const UnderlineInputBorder(
|
|
borderSide: BorderSide(color: Colors.white70),
|
|
)
|
|
: null,
|
|
),
|
|
onSubmitted: (_) => _lastPlayedFocus.unfocus(),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|