shlink-manager/lib/views/short_url_edit_view.dart

358 lines
13 KiB
Dart
Raw Normal View History

2024-01-28 22:55:28 +01:00
import 'package:dartz/dartz.dart' as dartz;
2023-07-09 23:00:00 +02:00
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
2024-01-28 22:55:28 +01:00
import 'package:shlink_app/API/Classes/ShortURL/short_url.dart';
2024-01-27 23:07:06 +01:00
import 'package:shlink_app/API/Classes/ShortURLSubmission/short_url_submission.dart';
import 'package:shlink_app/API/server_manager.dart';
import '../globals.dart' as globals;
2023-07-09 23:00:00 +02:00
class ShortURLEditView extends StatefulWidget {
2024-03-31 21:07:30 +02:00
const ShortURLEditView({super.key, this.shortUrl, this.longUrl});
2024-01-28 22:55:28 +01:00
final ShortURL? shortUrl;
2024-03-31 21:07:30 +02:00
final String? longUrl;
2023-07-09 23:00:00 +02:00
@override
State<ShortURLEditView> createState() => _ShortURLEditViewState();
}
2024-01-28 00:32:09 +01:00
class _ShortURLEditViewState extends State<ShortURLEditView>
with SingleTickerProviderStateMixin {
2023-07-09 23:00:00 +02:00
final longUrlController = TextEditingController();
final customSlugController = TextEditingController();
final titleController = TextEditingController();
final randomSlugLengthController = TextEditingController(text: "5");
bool randomSlug = true;
bool isCrawlable = true;
bool forwardQuery = true;
bool copyToClipboard = true;
2024-01-28 22:55:28 +01:00
bool disableSlugEditor = false;
2023-07-09 23:00:00 +02:00
String longUrlError = "";
String randomSlugLengthError = "";
bool isSaving = false;
late AnimationController _customSlugDiceAnimationController;
@override
void initState() {
_customSlugDiceAnimationController = AnimationController(
vsync: this,
2024-01-27 23:07:06 +01:00
duration: const Duration(milliseconds: 500),
2023-07-09 23:00:00 +02:00
);
2024-01-28 22:55:28 +01:00
loadExistingUrl();
2024-03-31 21:07:30 +02:00
if (widget.longUrl != null) {
longUrlController.text = widget.longUrl!;
}
2023-07-09 23:00:00 +02:00
super.initState();
}
@override
void dispose() {
longUrlController.dispose();
customSlugController.dispose();
titleController.dispose();
randomSlugLengthController.dispose();
super.dispose();
}
2024-01-28 22:55:28 +01:00
void loadExistingUrl() {
if (widget.shortUrl != null) {
longUrlController.text = widget.shortUrl!.longUrl;
isCrawlable = widget.shortUrl!.crawlable;
// for some reason this attribute is not returned by the api
forwardQuery = true;
titleController.text = widget.shortUrl!.title ?? "";
customSlugController.text = widget.shortUrl!.shortCode;
disableSlugEditor = true;
randomSlug = false;
}
}
2023-07-09 23:00:00 +02:00
void _submitShortUrl() async {
var newSubmission = ShortURLSubmission(
longUrl: longUrlController.text,
2024-01-28 00:32:09 +01:00
tags: [],
2023-07-09 23:00:00 +02:00
crawlable: isCrawlable,
forwardQuery: forwardQuery,
findIfExists: true,
title: titleController.text != "" ? titleController.text : null,
2024-01-28 00:32:09 +01:00
customSlug: customSlugController.text != "" && !randomSlug
? customSlugController.text
: null,
shortCodeLength:
randomSlug ? int.parse(randomSlugLengthController.text) : null);
2024-01-28 22:55:28 +01:00
dartz.Either<ShortURL, Failure> response;
if (widget.shortUrl != null) {
response = await globals.serverManager.updateShortUrl(newSubmission);
} else {
response = await globals.serverManager.submitShortUrl(newSubmission);
}
2023-07-09 23:00:00 +02:00
response.fold((l) async {
setState(() {
isSaving = false;
});
if (copyToClipboard) {
2024-01-28 22:55:28 +01:00
await Clipboard.setData(ClipboardData(text: l.shortUrl));
2024-01-28 00:32:09 +01:00
final snackBar = SnackBar(
content: const Text("Copied to clipboard!"),
backgroundColor: Colors.green[400],
behavior: SnackBarBehavior.floating);
2023-07-09 23:00:00 +02:00
ScaffoldMessenger.of(context).showSnackBar(snackBar);
2024-01-28 00:32:09 +01:00
} else {
final snackBar = SnackBar(
content: const Text("Short URL created!"),
backgroundColor: Colors.green[400],
behavior: SnackBarBehavior.floating);
2023-07-09 23:00:00 +02:00
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
2024-01-28 22:55:28 +01:00
Navigator.pop(context, l);
2023-07-09 23:00:00 +02:00
return true;
}, (r) {
setState(() {
isSaving = false;
});
var text = "";
if (r is RequestFailure) {
text = r.description;
2024-01-28 00:32:09 +01:00
} else {
2023-07-09 23:00:00 +02:00
text = (r as ApiFailure).detail;
2024-01-27 23:07:06 +01:00
if ((r).invalidElements != null) {
text = "$text: ${(r).invalidElements}";
2023-07-09 23:00:00 +02:00
}
}
2024-01-28 00:32:09 +01:00
final snackBar = SnackBar(
content: Text(text),
backgroundColor: Colors.red[400],
behavior: SnackBarBehavior.floating);
2023-07-09 23:00:00 +02:00
ScaffoldMessenger.of(context).showSnackBar(snackBar);
return false;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: [
SliverAppBar.medium(
title: Text("${disableSlugEditor ? "Edit" : "New"} Short URL",
style: const TextStyle(fontWeight: FontWeight.bold)),
2023-07-09 23:00:00 +02:00
),
SliverToBoxAdapter(
2024-01-28 00:32:09 +01:00
child: Padding(
padding: const EdgeInsets.only(left: 16, right: 16, top: 16),
child: Column(
children: [
TextField(
controller: longUrlController,
decoration: InputDecoration(
2023-07-09 23:00:00 +02:00
errorText: longUrlError != "" ? longUrlError : null,
2024-01-27 23:07:06 +01:00
border: const OutlineInputBorder(),
label: const Row(
2023-07-09 23:00:00 +02:00
children: [
Icon(Icons.public),
SizedBox(width: 8),
Text("Long URL")
],
2024-01-28 00:32:09 +01:00
)),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: TextField(
2024-01-28 22:55:28 +01:00
enabled: !disableSlugEditor,
2024-01-28 00:32:09 +01:00
controller: customSlugController,
style: TextStyle(
color: randomSlug
? Colors.grey
: Theme.of(context).brightness ==
Brightness.light
? Colors.black
: Colors.white),
onChanged: (_) {
if (randomSlug) {
setState(() {
2023-07-09 23:00:00 +02:00
randomSlug = false;
});
2024-01-28 00:32:09 +01:00
}
},
decoration: InputDecoration(
2024-01-27 23:07:06 +01:00
border: const OutlineInputBorder(),
2023-07-09 23:00:00 +02:00
label: Row(
children: [
2024-01-27 23:07:06 +01:00
const Icon(Icons.link),
const SizedBox(width: 8),
2024-01-28 00:32:09 +01:00
Text(
"${randomSlug ? "Random" : "Custom"} slug",
style: TextStyle(
fontStyle: randomSlug
? FontStyle.italic
: FontStyle.normal),
)
2023-07-09 23:00:00 +02:00
],
2024-01-28 00:32:09 +01:00
)),
2023-07-09 23:00:00 +02:00
),
2024-01-28 00:32:09 +01:00
),
const SizedBox(width: 8),
RotationTransition(
turns: Tween(begin: 0.0, end: 3.0).animate(
CurvedAnimation(
parent: _customSlugDiceAnimationController,
curve: Curves.easeInOutExpo)),
child: IconButton(
2024-03-31 21:58:31 +02:00
onPressed: disableSlugEditor
? null
: () {
if (randomSlug) {
_customSlugDiceAnimationController.reverse(
from: 1);
} else {
_customSlugDiceAnimationController.forward(
from: 0);
}
setState(() {
randomSlug = !randomSlug;
});
},
2024-01-28 00:32:09 +01:00
icon: Icon(
randomSlug ? Icons.casino : Icons.casino_outlined,
color: randomSlug ? Colors.green : Colors.grey)),
)
],
),
if (randomSlug) const SizedBox(height: 16),
if (randomSlug)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text("Random slug length"),
SizedBox(
width: 100,
child: TextField(
controller: randomSlugLengthController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
errorText:
randomSlugLengthError != "" ? "" : null,
border: const OutlineInputBorder(),
label: const Row(
children: [
Icon(Icons.tag),
SizedBox(width: 8),
Text("Length")
],
)),
))
2023-07-09 23:00:00 +02:00
],
),
2024-01-28 00:32:09 +01:00
const SizedBox(height: 16),
TextField(
controller: titleController,
decoration: const InputDecoration(
2023-07-09 23:00:00 +02:00
border: OutlineInputBorder(),
label: Row(
children: [
Icon(Icons.badge),
SizedBox(width: 8),
Text("Title")
],
2024-01-28 00:32:09 +01:00
)),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text("Crawlable"),
Switch(
value: isCrawlable,
onChanged: (_) {
setState(() {
isCrawlable = !isCrawlable;
});
},
)
],
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text("Forward query params"),
Switch(
value: forwardQuery,
onChanged: (_) {
setState(() {
forwardQuery = !forwardQuery;
});
},
)
],
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text("Copy to clipboard"),
Switch(
value: copyToClipboard,
onChanged: (_) {
setState(() {
copyToClipboard = !copyToClipboard;
});
},
)
],
),
],
),
))
2023-07-09 23:00:00 +02:00
],
),
floatingActionButton: FloatingActionButton(
2024-01-28 00:32:09 +01:00
onPressed: () {
if (!isSaving) {
2023-07-09 23:00:00 +02:00
setState(() {
2024-01-28 00:32:09 +01:00
isSaving = true;
longUrlError = "";
randomSlugLengthError = "";
2023-07-09 23:00:00 +02:00
});
2024-01-28 00:32:09 +01:00
if (longUrlController.text == "") {
setState(() {
longUrlError = "URL cannot be empty";
isSaving = false;
});
return;
} else if (int.tryParse(randomSlugLengthController.text) ==
null ||
int.tryParse(randomSlugLengthController.text)! < 1 ||
int.tryParse(randomSlugLengthController.text)! > 50) {
setState(() {
randomSlugLengthError = "invalid number";
isSaving = false;
});
return;
} else {
_submitShortUrl();
}
2023-07-09 23:00:00 +02:00
}
2024-01-28 00:32:09 +01:00
},
child: isSaving
? const Padding(
padding: EdgeInsets.all(16),
child: CircularProgressIndicator(strokeWidth: 3))
: const Icon(Icons.save)),
2023-07-09 23:00:00 +02:00
);
}
}