diff --git a/README.md b/README.md
index 5a7f0f6..9edc190 100644
--- a/README.md
+++ b/README.md
@@ -22,13 +22,14 @@ Shlink Manager is an app for Android to see and manage all shortened URLs create
✅ Display tags
✅ Display QR code
✅ Dark mode support
+✅ Edit existing short URLs
## 🔨 To Do
-- [ ] Edit existing short URLs
- [ ] Add support for iOS (maybe in the future)
- [ ] add tags
- [ ] specify individual long URLs per device
- [ ] improve app icon
+- [ ] Refactor code
- [ ] ...and more
## 💻 Development
diff --git a/lib/API/Classes/ShortURL/short_url.dart b/lib/API/Classes/ShortURL/short_url.dart
index 82d0f01..2e26f0d 100644
--- a/lib/API/Classes/ShortURL/short_url.dart
+++ b/lib/API/Classes/ShortURL/short_url.dart
@@ -63,4 +63,16 @@ class ShortURL {
domain = json["domain"],
title = json["title"],
crawlable = json["crawlable"];
+ ShortURL.empty()
+ : shortCode = "",
+ shortUrl = "",
+ longUrl = "",
+ deviceLongUrls = DeviceLongUrls("", "", ""),
+ dateCreated = DateTime.now(),
+ visitsSummary = VisitsSummary(0, 0, 0),
+ tags = [],
+ meta = ShortURLMeta(DateTime.now(), DateTime.now(), 0),
+ domain = "",
+ title = "",
+ crawlable = false;
}
diff --git a/lib/API/Methods/submit_short_url.dart b/lib/API/Methods/submit_short_url.dart
index 9d3f580..dbdeb43 100644
--- a/lib/API/Methods/submit_short_url.dart
+++ b/lib/API/Methods/submit_short_url.dart
@@ -2,11 +2,12 @@ import 'dart:async';
import 'dart:convert';
import 'package:dartz/dartz.dart';
import 'package:http/http.dart' as http;
+import 'package:shlink_app/API/Classes/ShortURL/short_url.dart';
import 'package:shlink_app/API/Classes/ShortURLSubmission/short_url_submission.dart';
import '../server_manager.dart';
/// Submits a short URL to a server for it to be added
-FutureOr> apiSubmitShortUrl(ShortURLSubmission shortUrl,
+FutureOr> apiSubmitShortUrl(ShortURLSubmission shortUrl,
String? apiKey, String? serverUrl, String apiVersion) async {
try {
final response =
@@ -18,7 +19,7 @@ FutureOr> apiSubmitShortUrl(ShortURLSubmission shortUrl,
if (response.statusCode == 200) {
// get returned short url
var jsonBody = jsonDecode(response.body);
- return left(jsonBody["shortUrl"]);
+ return left(ShortURL.fromJson(jsonBody));
} else {
try {
var jsonBody = jsonDecode(response.body);
diff --git a/lib/API/Methods/update_short_url.dart b/lib/API/Methods/update_short_url.dart
new file mode 100644
index 0000000..904d22c
--- /dev/null
+++ b/lib/API/Methods/update_short_url.dart
@@ -0,0 +1,45 @@
+import 'dart:async';
+import 'dart:convert';
+import 'package:dartz/dartz.dart';
+import 'package:http/http.dart' as http;
+import 'package:shlink_app/API/Classes/ShortURL/short_url.dart';
+import 'package:shlink_app/API/Classes/ShortURLSubmission/short_url_submission.dart';
+import '../server_manager.dart';
+
+/// Updates an existing short URL
+FutureOr> apiUpdateShortUrl(ShortURLSubmission shortUrl, String? apiKey, String? serverUrl, String apiVersion) async {
+ String shortCode = shortUrl.customSlug ?? "";
+ if (shortCode == "") {
+ return right(RequestFailure(0, "Missing short code"));
+ }
+ Map shortUrlData = shortUrl.toJson();
+ shortUrlData.remove("shortCode");
+ shortUrlData.remove("shortUrl");
+ try {
+ final response = await http.patch(Uri.parse("$serverUrl/rest/v$apiVersion/short-urls/$shortCode"),
+ headers: {
+ "X-Api-Key": apiKey ?? "",
+ },
+ body: jsonEncode(shortUrlData));
+
+ if (response.statusCode == 200) {
+ // get returned short url
+ var jsonBody = jsonDecode(response.body);
+ return left(ShortURL.fromJson(jsonBody));
+ } else {
+ try {
+ var jsonBody = jsonDecode(response.body);
+ return right(ApiFailure(
+ type: jsonBody["type"],
+ detail: jsonBody["detail"],
+ title: jsonBody["title"],
+ status: jsonBody["status"],
+ invalidElements: jsonBody["invalidElements"]));
+ } catch (resErr) {
+ return right(RequestFailure(response.statusCode, resErr.toString()));
+ }
+ }
+ } catch (reqErr) {
+ return right(RequestFailure(0, reqErr.toString()));
+ }
+}
\ No newline at end of file
diff --git a/lib/API/server_manager.dart b/lib/API/server_manager.dart
index 9dd39a0..624b2d2 100644
--- a/lib/API/server_manager.dart
+++ b/lib/API/server_manager.dart
@@ -10,6 +10,7 @@ import 'package:shlink_app/API/Methods/get_recent_short_urls.dart';
import 'package:shlink_app/API/Methods/get_server_health.dart';
import 'package:shlink_app/API/Methods/get_shlink_stats.dart';
import 'package:shlink_app/API/Methods/get_short_urls.dart';
+import 'package:shlink_app/API/Methods/update_short_url.dart';
import 'Methods/delete_short_url.dart';
import 'Methods/submit_short_url.dart';
@@ -100,11 +101,16 @@ class ServerManager {
}
/// Saves a new short URL to the server
- FutureOr> submitShortUrl(
+ FutureOr> submitShortUrl(
ShortURLSubmission shortUrl) async {
return apiSubmitShortUrl(shortUrl, apiKey, serverUrl, apiVersion);
}
+ FutureOr> updateShortUrl(
+ ShortURLSubmission shortUrl) async {
+ return apiUpdateShortUrl(shortUrl, apiKey, serverUrl, apiVersion);
+ }
+
/// Deletes a short URL from the server, identified by its slug
FutureOr> deleteShortUrl(String shortCode) async {
return apiDeleteShortUrl(shortCode, apiKey, serverUrl, apiVersion);
diff --git a/lib/views/short_url_edit_view.dart b/lib/views/short_url_edit_view.dart
index cd1a14b..dcc3e58 100644
--- a/lib/views/short_url_edit_view.dart
+++ b/lib/views/short_url_edit_view.dart
@@ -1,11 +1,15 @@
+import 'package:dartz/dartz.dart' as dartz;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
+import 'package:shlink_app/API/Classes/ShortURL/short_url.dart';
import 'package:shlink_app/API/Classes/ShortURLSubmission/short_url_submission.dart';
import 'package:shlink_app/API/server_manager.dart';
import '../globals.dart' as globals;
class ShortURLEditView extends StatefulWidget {
- const ShortURLEditView({super.key});
+ const ShortURLEditView({super.key, this.shortUrl});
+
+ final ShortURL? shortUrl;
@override
State createState() => _ShortURLEditViewState();
@@ -23,6 +27,8 @@ class _ShortURLEditViewState extends State
bool forwardQuery = true;
bool copyToClipboard = true;
+ bool disableSlugEditor = false;
+
String longUrlError = "";
String randomSlugLengthError = "";
@@ -36,6 +42,7 @@ class _ShortURLEditViewState extends State
vsync: this,
duration: const Duration(milliseconds: 500),
);
+ loadExistingUrl();
super.initState();
}
@@ -48,6 +55,19 @@ class _ShortURLEditViewState extends State
super.dispose();
}
+ 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;
+ }
+ }
+
void _submitShortUrl() async {
var newSubmission = ShortURLSubmission(
longUrl: longUrlController.text,
@@ -62,7 +82,12 @@ class _ShortURLEditViewState extends State
: null,
shortCodeLength:
randomSlug ? int.parse(randomSlugLengthController.text) : null);
- var response = await globals.serverManager.submitShortUrl(newSubmission);
+ dartz.Either response;
+ if (widget.shortUrl != null) {
+ response = await globals.serverManager.updateShortUrl(newSubmission);
+ } else {
+ response = await globals.serverManager.submitShortUrl(newSubmission);
+ }
response.fold((l) async {
setState(() {
@@ -70,7 +95,7 @@ class _ShortURLEditViewState extends State
});
if (copyToClipboard) {
- await Clipboard.setData(ClipboardData(text: l));
+ await Clipboard.setData(ClipboardData(text: l.shortUrl));
final snackBar = SnackBar(
content: const Text("Copied to clipboard!"),
backgroundColor: Colors.green[400],
@@ -83,7 +108,7 @@ class _ShortURLEditViewState extends State
behavior: SnackBarBehavior.floating);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
- Navigator.pop(context);
+ Navigator.pop(context, l);
return true;
}, (r) {
@@ -143,6 +168,7 @@ class _ShortURLEditViewState extends State
children: [
Expanded(
child: TextField(
+ enabled: !disableSlugEditor,
controller: customSlugController,
style: TextStyle(
color: randomSlug
@@ -182,7 +208,7 @@ class _ShortURLEditViewState extends State
parent: _customSlugDiceAnimationController,
curve: Curves.easeInOutExpo)),
child: IconButton(
- onPressed: () {
+ onPressed: disableSlugEditor ? null : () {
if (randomSlug) {
_customSlugDiceAnimationController.reverse(
from: 1);
diff --git a/lib/views/url_detail_view.dart b/lib/views/url_detail_view.dart
index 7efcc4d..b3d7082 100644
--- a/lib/views/url_detail_view.dart
+++ b/lib/views/url_detail_view.dart
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:shlink_app/API/Classes/ShortURL/short_url.dart';
import 'package:intl/intl.dart';
import 'package:shlink_app/API/server_manager.dart';
+import 'package:shlink_app/views/short_url_edit_view.dart';
import 'package:shlink_app/widgets/url_tags_list_widget.dart';
import '../globals.dart' as globals;
@@ -15,6 +16,16 @@ class URLDetailView extends StatefulWidget {
}
class _URLDetailViewState extends State {
+
+ ShortURL shortURL = ShortURL.empty();
+ @override
+ void initState() {
+ super.initState();
+ setState(() {
+ shortURL = widget.shortURL;
+ });
+ }
+
Future showDeletionConfirmation() {
return showDialog(
context: context,
@@ -27,7 +38,7 @@ class _URLDetailViewState extends State {
const Text("You're about to delete"),
const SizedBox(height: 4),
Text(
- widget.shortURL.title ?? widget.shortURL.shortCode,
+ shortURL.title ?? shortURL.shortCode,
style: const TextStyle(fontStyle: FontStyle.italic),
),
const SizedBox(height: 4),
@@ -42,11 +53,11 @@ class _URLDetailViewState extends State {
TextButton(
onPressed: () async {
var response = await globals.serverManager
- .deleteShortUrl(widget.shortURL.shortCode);
+ .deleteShortUrl(shortURL.shortCode);
response.fold((l) {
Navigator.pop(context);
- Navigator.pop(context, "reload");
+ Navigator.pop(context);
final snackBar = SnackBar(
content: const Text("Short URL deleted!"),
@@ -84,9 +95,21 @@ class _URLDetailViewState extends State {
body: CustomScrollView(
slivers: [
SliverAppBar.medium(
- title: Text(widget.shortURL.title ?? widget.shortURL.shortCode,
+ title: Text(shortURL.title ?? shortURL.shortCode,
style: const TextStyle(fontWeight: FontWeight.bold)),
actions: [
+ IconButton(
+ onPressed: () async {
+ ShortURL updatedUrl = await Navigator.of(context).push(MaterialPageRoute(
+ builder: (context) => ShortURLEditView(shortUrl: shortURL)));
+ setState(() {
+ shortURL = updatedUrl;
+ });
+ },
+ icon: const Icon(
+ Icons.edit
+ )
+ ),
IconButton(
onPressed: () {
showDeletionConfirmation();
@@ -100,56 +123,56 @@ class _URLDetailViewState extends State {
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(left: 16.0, right: 16.0),
- child: UrlTagsListWidget(tags: widget.shortURL.tags)
+ child: UrlTagsListWidget(tags: shortURL.tags)
),
),
- _ListCell(title: "Short Code", content: widget.shortURL.shortCode),
- _ListCell(title: "Short URL", content: widget.shortURL.shortUrl),
- _ListCell(title: "Long URL", content: widget.shortURL.longUrl),
+ _ListCell(title: "Short Code", content: shortURL.shortCode),
+ _ListCell(title: "Short URL", content: shortURL.shortUrl),
+ _ListCell(title: "Long URL", content: shortURL.longUrl),
_ListCell(
title: "iOS",
- content: widget.shortURL.deviceLongUrls.ios,
+ content: shortURL.deviceLongUrls.ios,
sub: true),
_ListCell(
title: "Android",
- content: widget.shortURL.deviceLongUrls.android,
+ content: shortURL.deviceLongUrls.android,
sub: true),
_ListCell(
title: "Desktop",
- content: widget.shortURL.deviceLongUrls.desktop,
+ content: shortURL.deviceLongUrls.desktop,
sub: true),
_ListCell(
- title: "Creation Date", content: widget.shortURL.dateCreated),
+ title: "Creation Date", content: shortURL.dateCreated),
const _ListCell(title: "Visits", content: ""),
_ListCell(
title: "Total",
- content: widget.shortURL.visitsSummary.total,
+ content: shortURL.visitsSummary.total,
sub: true),
_ListCell(
title: "Non-Bots",
- content: widget.shortURL.visitsSummary.nonBots,
+ content: shortURL.visitsSummary.nonBots,
sub: true),
_ListCell(
title: "Bots",
- content: widget.shortURL.visitsSummary.bots,
+ content: shortURL.visitsSummary.bots,
sub: true),
const _ListCell(title: "Meta", content: ""),
_ListCell(
title: "Valid Since",
- content: widget.shortURL.meta.validSince,
+ content: shortURL.meta.validSince,
sub: true),
_ListCell(
title: "Valid Until",
- content: widget.shortURL.meta.validUntil,
+ content: shortURL.meta.validUntil,
sub: true),
_ListCell(
title: "Max Visits",
- content: widget.shortURL.meta.maxVisits,
+ content: shortURL.meta.maxVisits,
sub: true),
- _ListCell(title: "Domain", content: widget.shortURL.domain),
+ _ListCell(title: "Domain", content: shortURL.domain),
_ListCell(
title: "Crawlable",
- content: widget.shortURL.crawlable,
+ content: shortURL.crawlable,
last: true)
],
),
diff --git a/lib/views/url_list_view.dart b/lib/views/url_list_view.dart
index 5ddd597..aee1da5 100644
--- a/lib/views/url_list_view.dart
+++ b/lib/views/url_list_view.dart
@@ -194,12 +194,11 @@ class _ShortURLCellState extends State {
Widget build(BuildContext context) {
return GestureDetector(
onTap: () async {
- final result = await Navigator.of(context).push(MaterialPageRoute(
- builder: (context) => URLDetailView(shortURL: widget.shortURL)));
-
- if (result == "reload") {
- widget.reload();
- }
+ await Navigator.of(context).push(MaterialPageRoute(
+ builder: (context) => URLDetailView(shortURL: widget.shortURL)))
+ .then((a) => {
+ widget.reload()
+ });
},
child: Padding(
padding: EdgeInsets.only(