mirror of
https://github.com/rainloreley/shlink-manager.git
synced 2024-11-21 17:33:03 +01:00
edit existing short urls
This commit is contained in:
parent
d00ba4b4e9
commit
4172c968c4
|
@ -22,13 +22,14 @@ Shlink Manager is an app for Android to see and manage all shortened URLs create
|
|||
✅ Display tags<br/>
|
||||
✅ Display QR code<br/>
|
||||
✅ Dark mode support<br/>
|
||||
✅ Edit existing short URLs<br/>
|
||||
|
||||
## 🔨 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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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<Either<String, Failure>> apiSubmitShortUrl(ShortURLSubmission shortUrl,
|
||||
FutureOr<Either<ShortURL, Failure>> apiSubmitShortUrl(ShortURLSubmission shortUrl,
|
||||
String? apiKey, String? serverUrl, String apiVersion) async {
|
||||
try {
|
||||
final response =
|
||||
|
@ -18,7 +19,7 @@ FutureOr<Either<String, Failure>> 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);
|
||||
|
|
|
@ -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<Either<ShortURL, Failure>> apiUpdateShortUrl(ShortURLSubmission shortUrl, String? apiKey, String? serverUrl, String apiVersion) async {
|
||||
String shortCode = shortUrl.customSlug ?? "";
|
||||
if (shortCode == "") {
|
||||
return right(RequestFailure(0, "Missing short code"));
|
||||
}
|
||||
Map<String, dynamic> 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()));
|
||||
}
|
||||
}
|
|
@ -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<Either<String, Failure>> submitShortUrl(
|
||||
FutureOr<Either<ShortURL, Failure>> submitShortUrl(
|
||||
ShortURLSubmission shortUrl) async {
|
||||
return apiSubmitShortUrl(shortUrl, apiKey, serverUrl, apiVersion);
|
||||
}
|
||||
|
||||
FutureOr<Either<ShortURL, Failure>> updateShortUrl(
|
||||
ShortURLSubmission shortUrl) async {
|
||||
return apiUpdateShortUrl(shortUrl, apiKey, serverUrl, apiVersion);
|
||||
}
|
||||
|
||||
/// Deletes a short URL from the server, identified by its slug
|
||||
FutureOr<Either<String, Failure>> deleteShortUrl(String shortCode) async {
|
||||
return apiDeleteShortUrl(shortCode, apiKey, serverUrl, apiVersion);
|
||||
|
|
|
@ -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<ShortURLEditView> createState() => _ShortURLEditViewState();
|
||||
|
@ -23,6 +27,8 @@ class _ShortURLEditViewState extends State<ShortURLEditView>
|
|||
bool forwardQuery = true;
|
||||
bool copyToClipboard = true;
|
||||
|
||||
bool disableSlugEditor = false;
|
||||
|
||||
String longUrlError = "";
|
||||
String randomSlugLengthError = "";
|
||||
|
||||
|
@ -36,6 +42,7 @@ class _ShortURLEditViewState extends State<ShortURLEditView>
|
|||
vsync: this,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
);
|
||||
loadExistingUrl();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
|
@ -48,6 +55,19 @@ class _ShortURLEditViewState extends State<ShortURLEditView>
|
|||
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<ShortURLEditView>
|
|||
: null,
|
||||
shortCodeLength:
|
||||
randomSlug ? int.parse(randomSlugLengthController.text) : null);
|
||||
var response = await globals.serverManager.submitShortUrl(newSubmission);
|
||||
dartz.Either<ShortURL, Failure> 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<ShortURLEditView>
|
|||
});
|
||||
|
||||
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<ShortURLEditView>
|
|||
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<ShortURLEditView>
|
|||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
enabled: !disableSlugEditor,
|
||||
controller: customSlugController,
|
||||
style: TextStyle(
|
||||
color: randomSlug
|
||||
|
@ -182,7 +208,7 @@ class _ShortURLEditViewState extends State<ShortURLEditView>
|
|||
parent: _customSlugDiceAnimationController,
|
||||
curve: Curves.easeInOutExpo)),
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
onPressed: disableSlugEditor ? null : () {
|
||||
if (randomSlug) {
|
||||
_customSlugDiceAnimationController.reverse(
|
||||
from: 1);
|
||||
|
|
|
@ -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<URLDetailView> {
|
||||
|
||||
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<URLDetailView> {
|
|||
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<URLDetailView> {
|
|||
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<URLDetailView> {
|
|||
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<URLDetailView> {
|
|||
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)
|
||||
],
|
||||
),
|
||||
|
|
|
@ -194,12 +194,11 @@ class _ShortURLCellState extends State<ShortURLCell> {
|
|||
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(
|
||||
|
|
Loading…
Reference in New Issue
Block a user