mirror of
https://github.com/rainloreley/shlink-manager.git
synced 2024-11-24 02:33:01 +01:00
a lot of work
This commit is contained in:
parent
68130104da
commit
622496174c
|
@ -0,0 +1,10 @@
|
|||
import 'package:shlink_app/API/Classes/ShlinkStats/ShlinkStats_Visits.dart';
|
||||
|
||||
class ShlinkStats {
|
||||
ShlinkStats_Visits nonOrphanVisits;
|
||||
ShlinkStats_Visits orphanVisits;
|
||||
int shortUrlsCount;
|
||||
int tagsCount;
|
||||
|
||||
ShlinkStats(this.nonOrphanVisits, this.orphanVisits, this.shortUrlsCount, this.tagsCount);
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
class ShlinkStats_Visits {
|
||||
int total;
|
||||
int nonBots;
|
||||
int bots;
|
||||
|
||||
ShlinkStats_Visits(this.total, this.nonBots, this.bots);
|
||||
|
||||
ShlinkStats_Visits.fromJson(Map<String, dynamic> json)
|
||||
: total = json["total"],
|
||||
nonBots = json["nonBots"],
|
||||
bots = json["bots"];
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:shlink_app/API/Classes/ShortURL_DeviceLongUrls.dart';
|
||||
import 'package:shlink_app/API/Classes/ShortURL_Meta.dart';
|
||||
import 'package:shlink_app/API/Classes/ShortURL_VisitsSummary.dart';
|
||||
import 'package:shlink_app/API/Classes/ShortURL/ShortURL_DeviceLongUrls.dart';
|
||||
import 'package:shlink_app/API/Classes/ShortURL/ShortURL_Meta.dart';
|
||||
import 'package:shlink_app/API/Classes/ShortURL/ShortURL_VisitsSummary.dart';
|
||||
|
||||
class ShortURL {
|
||||
String shortCode;
|
|
@ -1,5 +1,3 @@
|
|||
import 'dart:convert';
|
||||
|
||||
class ShortURL_DeviceLongUrls {
|
||||
final String? android;
|
||||
final String? ios;
|
||||
|
@ -11,4 +9,10 @@ class ShortURL_DeviceLongUrls {
|
|||
: android = json["android"],
|
||||
ios = json["ios"],
|
||||
desktop = json["desktop"];
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"android": android,
|
||||
"ios": ios,
|
||||
"desktop": desktop
|
||||
};
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
import '../ShortURL/ShortURL_DeviceLongUrls.dart';
|
||||
|
||||
class ShortURLSubmission {
|
||||
String longUrl;
|
||||
ShortURL_DeviceLongUrls? deviceLongUrls;
|
||||
String? validSince;
|
||||
String? validUntil;
|
||||
int? maxVisits;
|
||||
List<String> tags;
|
||||
String? title;
|
||||
bool crawlable;
|
||||
bool forwardQuery;
|
||||
String? customSlug;
|
||||
bool findIfExists;
|
||||
String? domain;
|
||||
int? shortCodeLength;
|
||||
|
||||
ShortURLSubmission({required this.longUrl, required this.deviceLongUrls, this.validSince, this.validUntil, this.maxVisits, required this.tags, this.title, required this.crawlable, required this.forwardQuery, this.customSlug, required this.findIfExists, this.domain, this.shortCodeLength});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
"longUrl": longUrl,
|
||||
"deviceLongUrls": deviceLongUrls?.toJson(),
|
||||
"validSince": validSince,
|
||||
"validUntil": validUntil,
|
||||
"maxVisits": maxVisits,
|
||||
"tags": tags,
|
||||
"title": title,
|
||||
"crawlable": crawlable,
|
||||
"forwardQuery": forwardQuery,
|
||||
"customSlug": customSlug,
|
||||
"findIfExists": findIfExists,
|
||||
"domain": domain,
|
||||
"shortCodeLength": shortCodeLength
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../ServerManager.dart';
|
||||
|
||||
FutureOr<Either<String, Failure>> API_connect(String? api_key, String? server_url, String apiVersion) async {
|
||||
try {
|
||||
final response = await http.get(Uri.parse("${server_url}/rest/v${apiVersion}/short-urls"), headers: {
|
||||
"X-Api-Key": api_key ?? "",
|
||||
});
|
||||
if (response.statusCode == 200) {
|
||||
return left("");
|
||||
}
|
||||
else {
|
||||
try {
|
||||
var jsonBody = jsonDecode(response.body);
|
||||
return right(ApiFailure(type: jsonBody["type"], detail: jsonBody["detail"], title: jsonBody["title"], status: jsonBody["status"]));
|
||||
}
|
||||
catch(resErr) {
|
||||
return right(RequestFailure(response.statusCode, resErr.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(reqErr) {
|
||||
return right(RequestFailure(0, reqErr.toString()));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../ServerManager.dart';
|
||||
|
||||
FutureOr<Either<String, Failure>> API_deleteShortUrl(String shortCode, String? api_key, String? server_url, String apiVersion) async {
|
||||
try {
|
||||
final response = await http.delete(Uri.parse("${server_url}/rest/v${apiVersion}/short-urls/${shortCode}"), headers: {
|
||||
"X-Api-Key": api_key ?? "",
|
||||
});
|
||||
if (response.statusCode == 204) {
|
||||
// get returned short url
|
||||
return left("");
|
||||
}
|
||||
else {
|
||||
try {
|
||||
var jsonBody = jsonDecode(response.body);
|
||||
return right(ApiFailure(type: jsonBody["type"], detail: jsonBody["detail"], title: jsonBody["title"], status: jsonBody["status"]));
|
||||
}
|
||||
catch(resErr) {
|
||||
return right(RequestFailure(response.statusCode, resErr.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(reqErr) {
|
||||
return right(RequestFailure(0, reqErr.toString()));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,149 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:shlink_app/API/Classes/ShlinkStats/ShlinkStats_Visits.dart';
|
||||
import '../Classes/ShlinkStats/ShlinkStats.dart';
|
||||
import '../ServerManager.dart';
|
||||
|
||||
FutureOr<Either<ShlinkStats, Failure>> API_getShlinkStats(String? api_key, String? server_url, String apiVersion) async {
|
||||
|
||||
var nonOrphanVisits;
|
||||
var orphanVisits;
|
||||
var shortUrlsCount;
|
||||
var tagsCount;
|
||||
var failure;
|
||||
|
||||
var visitStatsResponse = await _getVisitStats(api_key, server_url, apiVersion);
|
||||
visitStatsResponse.fold((l) {
|
||||
nonOrphanVisits = l.nonOrphanVisits;
|
||||
orphanVisits = l.orphanVisits;
|
||||
}, (r) {
|
||||
failure = r;
|
||||
return right(r);
|
||||
});
|
||||
|
||||
var shortUrlsCountResponse = await _getShortUrlsCount(api_key, server_url, apiVersion);
|
||||
shortUrlsCountResponse.fold((l) {
|
||||
shortUrlsCount = l;
|
||||
}, (r) {
|
||||
failure = r;
|
||||
return right(r);
|
||||
});
|
||||
|
||||
var tagsCountResponse = await _getTagsCount(api_key, server_url, apiVersion);
|
||||
tagsCountResponse.fold((l) {
|
||||
tagsCount = l;
|
||||
}, (r) {
|
||||
failure = r;
|
||||
return right(r);
|
||||
});
|
||||
|
||||
while(failure == null && (nonOrphanVisits == null || orphanVisits == null || shortUrlsCount == null || tagsCount == null)) {
|
||||
await Future.delayed(Duration(milliseconds: 100));
|
||||
}
|
||||
|
||||
if (failure != null) {
|
||||
return right(failure);
|
||||
}
|
||||
return left(ShlinkStats(nonOrphanVisits, orphanVisits, shortUrlsCount, tagsCount));
|
||||
}
|
||||
|
||||
/*Future<tuple.Tuple3<FutureOr<Either<_ShlinkVisitStats, Failure>>, FutureOr<Either<int, Failure>>, FutureOr<Either<int, Failure>>>> waiterFunction(String? api_key, String? server_url, String apiVersion) async {
|
||||
late FutureOr<Either<_ShlinkVisitStats, Failure>> visits;
|
||||
late FutureOr<Either<int, Failure>> shortUrlsCount;
|
||||
late FutureOr<Either<int, Failure>> tagsCount;
|
||||
|
||||
await Future.wait([
|
||||
_getVisitStats(api_key, server_url, apiVersion).then((value) => visits = value),
|
||||
_getShortUrlsCount(api_key, server_url, apiVersion).then((value) => shortUrlsCount = value),
|
||||
_getTagsCount(api_key, server_url, apiVersion).then((value) => tagsCount = value),
|
||||
]);
|
||||
|
||||
return Future.value(tuple.Tuple3(visits, shortUrlsCount, tagsCount));
|
||||
|
||||
}*/
|
||||
|
||||
class _ShlinkVisitStats {
|
||||
ShlinkStats_Visits nonOrphanVisits;
|
||||
ShlinkStats_Visits orphanVisits;
|
||||
|
||||
_ShlinkVisitStats(this.nonOrphanVisits, this.orphanVisits);
|
||||
}
|
||||
|
||||
FutureOr<Either<_ShlinkVisitStats, Failure>> _getVisitStats(String? api_key, String? server_url, String apiVersion) async {
|
||||
try {
|
||||
final response = await http.get(Uri.parse("${server_url}/rest/v${apiVersion}/visits"), headers: {
|
||||
"X-Api-Key": api_key ?? "",
|
||||
});
|
||||
if (response.statusCode == 200) {
|
||||
var jsonResponse = jsonDecode(response.body);
|
||||
var nonOrphanVisits = ShlinkStats_Visits.fromJson(jsonResponse["visits"]["nonOrphanVisits"]);
|
||||
var orphanVisits = ShlinkStats_Visits.fromJson(jsonResponse["visits"]["orphanVisits"]);
|
||||
return left(_ShlinkVisitStats(nonOrphanVisits, orphanVisits));
|
||||
|
||||
}
|
||||
else {
|
||||
try {
|
||||
var jsonBody = jsonDecode(response.body);
|
||||
return right(ApiFailure(type: jsonBody["type"], detail: jsonBody["detail"], title: jsonBody["title"], status: jsonBody["status"]));
|
||||
}
|
||||
catch(resErr) {
|
||||
return right(RequestFailure(response.statusCode, resErr.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(reqErr) {
|
||||
return right(RequestFailure(0, reqErr.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
// get short urls count
|
||||
FutureOr<Either<int, Failure>> _getShortUrlsCount(String? api_key, String? server_url, String apiVersion) async {
|
||||
try {
|
||||
final response = await http.get(Uri.parse("${server_url}/rest/v${apiVersion}/short-urls"), headers: {
|
||||
"X-Api-Key": api_key ?? "",
|
||||
});
|
||||
if (response.statusCode == 200) {
|
||||
var jsonResponse = jsonDecode(response.body);
|
||||
return left(jsonResponse["shortUrls"]["pagination"]["totalItems"]);
|
||||
}
|
||||
else {
|
||||
try {
|
||||
var jsonBody = jsonDecode(response.body);
|
||||
return right(ApiFailure(type: jsonBody["type"], detail: jsonBody["detail"], title: jsonBody["title"], status: jsonBody["status"]));
|
||||
}
|
||||
catch(resErr) {
|
||||
return right(RequestFailure(response.statusCode, resErr.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(reqErr) {
|
||||
return right(RequestFailure(0, reqErr.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
// get tags count
|
||||
FutureOr<Either<int, Failure>> _getTagsCount(String? api_key, String? server_url, String apiVersion) async {
|
||||
try {
|
||||
final response = await http.get(Uri.parse("${server_url}/rest/v${apiVersion}/tags"), headers: {
|
||||
"X-Api-Key": api_key ?? "",
|
||||
});
|
||||
if (response.statusCode == 200) {
|
||||
var jsonResponse = jsonDecode(response.body);
|
||||
return left(jsonResponse["tags"]["pagination"]["totalItems"]);
|
||||
}
|
||||
else {
|
||||
try {
|
||||
var jsonBody = jsonDecode(response.body);
|
||||
return right(ApiFailure(type: jsonBody["type"], detail: jsonBody["detail"], title: jsonBody["title"], status: jsonBody["status"]));
|
||||
}
|
||||
catch(resErr) {
|
||||
return right(RequestFailure(response.statusCode, resErr.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(reqErr) {
|
||||
return right(RequestFailure(0, reqErr.toString()));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
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/ShortURL.dart';
|
||||
import '../ServerManager.dart';
|
||||
|
||||
FutureOr<Either<List<ShortURL>, Failure>> API_getShortUrls(String? api_key, String? server_url, String apiVersion) async {
|
||||
var _currentPage = 1;
|
||||
var _maxPages = 2;
|
||||
List<ShortURL> _allUrls = [];
|
||||
|
||||
Failure? error;
|
||||
|
||||
while (_currentPage <= _maxPages) {
|
||||
final response = await _getShortUrlPage(_currentPage, api_key, server_url, apiVersion);
|
||||
response.fold((l) {
|
||||
_allUrls.addAll(l.urls);
|
||||
_maxPages = l.totalPages;
|
||||
_currentPage++;
|
||||
}, (r) {
|
||||
_maxPages = 0;
|
||||
error = r;
|
||||
});
|
||||
}
|
||||
if (error == null) {
|
||||
return left(_allUrls);
|
||||
}
|
||||
else {
|
||||
return right(error!);
|
||||
}
|
||||
}
|
||||
|
||||
FutureOr<Either<ShortURLPageResponse, Failure>> _getShortUrlPage(int page, String? api_key, String? server_url, String apiVersion) async {
|
||||
try {
|
||||
final response = await http.get(Uri.parse("${server_url}/rest/v${apiVersion}/short-urls?page=${page}"), headers: {
|
||||
"X-Api-Key": api_key ?? "",
|
||||
});
|
||||
if (response.statusCode == 200) {
|
||||
var jsonResponse = jsonDecode(response.body);
|
||||
var pagesCount = jsonResponse["shortUrls"]["pagination"]["pagesCount"] as int;
|
||||
List<ShortURL> shortURLs = (jsonResponse["shortUrls"]["data"] as List<dynamic>).map((e) {
|
||||
return ShortURL.fromJson(e);
|
||||
}).toList();
|
||||
return left(ShortURLPageResponse(shortURLs, pagesCount));
|
||||
}
|
||||
else {
|
||||
try {
|
||||
var jsonBody = jsonDecode(response.body);
|
||||
return right(ApiFailure(type: jsonBody["type"], detail: jsonBody["detail"], title: jsonBody["title"], status: jsonBody["status"]));
|
||||
}
|
||||
catch(resErr) {
|
||||
return right(RequestFailure(response.statusCode, resErr.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(reqErr) {
|
||||
return right(RequestFailure(0, reqErr.toString()));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:shlink_app/API/Classes/ShortURLSubmission/ShortURLSubmission.dart';
|
||||
import '../ServerManager.dart';
|
||||
|
||||
FutureOr<Either<String, Failure>> API_submitShortUrl(ShortURLSubmission shortUrl, String? api_key, String? server_url, String apiVersion) async {
|
||||
try {
|
||||
final response = await http.post(Uri.parse("${server_url}/rest/v${apiVersion}/short-urls"), headers: {
|
||||
"X-Api-Key": api_key ?? "",
|
||||
}, body: jsonEncode(shortUrl.toJson()));
|
||||
if (response.statusCode == 200) {
|
||||
// get returned short url
|
||||
var jsonBody = jsonDecode(response.body);
|
||||
return left(jsonBody["shortUrl"]);
|
||||
}
|
||||
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"] ?? null));
|
||||
}
|
||||
catch(resErr) {
|
||||
return right(RequestFailure(response.statusCode, resErr.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(reqErr) {
|
||||
return right(RequestFailure(0, reqErr.toString()));
|
||||
}
|
||||
}
|
|
@ -1,9 +1,15 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:shlink_app/API/Classes/ShortURL.dart';
|
||||
import 'package:shlink_app/API/Classes/ShlinkStats/ShlinkStats.dart';
|
||||
import 'package:shlink_app/API/Classes/ShortURL/ShortURL.dart';
|
||||
import 'package:shlink_app/API/Classes/ShortURLSubmission/ShortURLSubmission.dart';
|
||||
import 'package:shlink_app/API/Methods/connect.dart';
|
||||
import 'package:shlink_app/API/Methods/getShlinkStats.dart';
|
||||
import 'package:shlink_app/API/Methods/getShortUrls.dart';
|
||||
|
||||
import 'Methods/deleteShortUrl.dart';
|
||||
import 'Methods/submitShortUrl.dart';
|
||||
|
||||
class ServerManager {
|
||||
|
||||
|
@ -53,80 +59,23 @@ class ServerManager {
|
|||
|
||||
FutureOr<Either<String, Failure>> connect() async {
|
||||
_loadCredentials();
|
||||
try {
|
||||
final response = await http.get(Uri.parse("${_server_url}/rest/v${apiVersion}/short-urls"), headers: {
|
||||
"X-Api-Key": _api_key ?? "",
|
||||
});
|
||||
if (response.statusCode == 200) {
|
||||
return left("");
|
||||
}
|
||||
else {
|
||||
try {
|
||||
var jsonBody = jsonDecode(response.body);
|
||||
return right(ApiFailure(jsonBody["type"], jsonBody["detail"], jsonBody["title"], jsonBody["status"]));
|
||||
}
|
||||
catch(resErr) {
|
||||
return right(RequestFailure(response.statusCode, resErr.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(reqErr) {
|
||||
return right(RequestFailure(0, reqErr.toString()));
|
||||
}
|
||||
return API_connect(_api_key, _server_url, apiVersion);
|
||||
}
|
||||
|
||||
FutureOr<Either<List<ShortURL>, Failure>> getShortUrls() async {
|
||||
var _currentPage = 1;
|
||||
var _maxPages = 2;
|
||||
List<ShortURL> _allUrls = [];
|
||||
|
||||
Failure? error;
|
||||
|
||||
while (_currentPage <= _maxPages) {
|
||||
final response = await _getShortUrlPage(_currentPage);
|
||||
response.fold((l) {
|
||||
_allUrls.addAll(l.urls);
|
||||
_maxPages = l.totalPages;
|
||||
_currentPage++;
|
||||
}, (r) {
|
||||
_maxPages = 0;
|
||||
error = r;
|
||||
});
|
||||
}
|
||||
if (error == null) {
|
||||
return left(_allUrls);
|
||||
}
|
||||
else {
|
||||
return right(error!);
|
||||
}
|
||||
return API_getShortUrls(_api_key, _server_url, apiVersion);
|
||||
}
|
||||
|
||||
FutureOr<Either<ShortURLPageResponse, Failure>> _getShortUrlPage(int page) async {
|
||||
try {
|
||||
final response = await http.get(Uri.parse("${_server_url}/rest/v${apiVersion}/short-urls?page=${page}"), headers: {
|
||||
"X-Api-Key": _api_key ?? "",
|
||||
});
|
||||
if (response.statusCode == 200) {
|
||||
var jsonResponse = jsonDecode(response.body);
|
||||
var pagesCount = jsonResponse["shortUrls"]["pagination"]["pagesCount"] as int;
|
||||
List<ShortURL> shortURLs = (jsonResponse["shortUrls"]["data"] as List<dynamic>).map((e) {
|
||||
return ShortURL.fromJson(e);
|
||||
}).toList();
|
||||
return left(ShortURLPageResponse(shortURLs, pagesCount));
|
||||
FutureOr<Either<ShlinkStats, Failure>> getShlinkStats() async {
|
||||
return API_getShlinkStats(_api_key, _server_url, apiVersion);
|
||||
}
|
||||
else {
|
||||
try {
|
||||
var jsonBody = jsonDecode(response.body);
|
||||
return right(ApiFailure(jsonBody["type"], jsonBody["detail"], jsonBody["title"], jsonBody["status"]));
|
||||
}
|
||||
catch(resErr) {
|
||||
return right(RequestFailure(response.statusCode, resErr.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(reqErr) {
|
||||
return right(RequestFailure(0, reqErr.toString()));
|
||||
|
||||
FutureOr<Either<String, Failure>> submitShortUrl(ShortURLSubmission shortUrl) async {
|
||||
return API_submitShortUrl(shortUrl, _api_key, _server_url, apiVersion);
|
||||
}
|
||||
|
||||
FutureOr<Either<String, Failure>> deleteShortUrl(String shortCode) async {
|
||||
return API_deleteShortUrl(shortCode, _api_key, _server_url, apiVersion);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -151,6 +100,7 @@ class ApiFailure extends Failure {
|
|||
String detail;
|
||||
String title;
|
||||
int status;
|
||||
List<dynamic>? invalidElements;
|
||||
|
||||
ApiFailure(this.type, this.detail, this.title, this.status);
|
||||
ApiFailure({required this.type, required this.detail, required this.title, required this.status, this.invalidElements});
|
||||
}
|
|
@ -1,4 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:shlink_app/API/Classes/ShlinkStats/ShlinkStats.dart';
|
||||
import 'package:shlink_app/API/ServerManager.dart';
|
||||
import 'package:shlink_app/ShortURLEditView.dart';
|
||||
import 'globals.dart' as globals;
|
||||
|
||||
class HomeView extends StatefulWidget {
|
||||
|
@ -10,10 +13,34 @@ class HomeView extends StatefulWidget {
|
|||
|
||||
class _HomeViewState extends State<HomeView> {
|
||||
|
||||
ShlinkStats? shlinkStats;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
// TODO: implement initState
|
||||
super.initState();
|
||||
WidgetsBinding.instance
|
||||
.addPostFrameCallback((_) => loadShlinkStats());
|
||||
}
|
||||
|
||||
void loadShlinkStats() async {
|
||||
final response = await globals.serverManager.getShlinkStats();
|
||||
response.fold((l) {
|
||||
setState(() {
|
||||
shlinkStats = l;
|
||||
});
|
||||
}, (r) {
|
||||
var text = "";
|
||||
if (r is RequestFailure) {
|
||||
text = r.description;
|
||||
}
|
||||
else {
|
||||
text = (r as ApiFailure).detail;
|
||||
}
|
||||
|
||||
final snackBar = SnackBar(content: Text(text), backgroundColor: Colors.red[400], behavior: SnackBarBehavior.floating);
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -30,9 +57,66 @@ class _HomeViewState extends State<HomeView> {
|
|||
Text(globals.serverManager.getServerUrl(), style: TextStyle(fontSize: 16, color: Colors.grey[600]))
|
||||
],
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.spaceEvenly,
|
||||
children: [
|
||||
_ShlinkStatsCardWidget(icon: Icons.link, text: "${shlinkStats?.shortUrlsCount.toString() ?? "0"} Short URLs", borderColor: Colors.blue),
|
||||
_ShlinkStatsCardWidget(icon: Icons.remove_red_eye, text: "${shlinkStats?.nonOrphanVisits.total ?? "0"} Visits", borderColor: Colors.green),
|
||||
_ShlinkStatsCardWidget(icon: Icons.warning, text: "${shlinkStats?.orphanVisits.total ?? "0"} Orphan Visits", borderColor: Colors.red),
|
||||
_ShlinkStatsCardWidget(icon: Icons.sell, text: "${shlinkStats?.tagsCount.toString() ?? "0"} Tags", borderColor: Colors.purple),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(MaterialPageRoute(builder: (context) => ShortURLEditView()));
|
||||
},
|
||||
child: Icon(Icons.add),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// stats card widget
|
||||
class _ShlinkStatsCardWidget extends StatefulWidget {
|
||||
const _ShlinkStatsCardWidget({this.text, this.icon, this.borderColor});
|
||||
|
||||
final icon;
|
||||
final borderColor;
|
||||
final text;
|
||||
|
||||
@override
|
||||
State<_ShlinkStatsCardWidget> createState() => _ShlinkStatsCardWidgetState();
|
||||
}
|
||||
|
||||
class _ShlinkStatsCardWidgetState extends State<_ShlinkStatsCardWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var randomColor = ([...Colors.primaries]..shuffle()).first;
|
||||
return Padding(
|
||||
padding: EdgeInsets.all(4),
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: widget.borderColor ?? randomColor),
|
||||
borderRadius: BorderRadius.circular(8)
|
||||
),
|
||||
child: SizedBox(
|
||||
child: Wrap(
|
||||
children: [
|
||||
Icon(widget.icon),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 4),
|
||||
child: Text(widget.text, style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,9 +17,7 @@ class _NavigationBarViewState extends State<NavigationBarView> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
child: views.elementAt(_selectedView),
|
||||
),
|
||||
body: views.elementAt(_selectedView),
|
||||
bottomNavigationBar: NavigationBar(
|
||||
destinations: [
|
||||
NavigationDestination(icon: Icon(Icons.home), label: "Home"),
|
||||
|
|
|
@ -0,0 +1,300 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:shlink_app/API/Classes/ShortURLSubmission/ShortURLSubmission.dart';
|
||||
import 'package:shlink_app/API/ServerManager.dart';
|
||||
import 'globals.dart' as globals;
|
||||
|
||||
class ShortURLEditView extends StatefulWidget {
|
||||
const ShortURLEditView({super.key});
|
||||
|
||||
@override
|
||||
State<ShortURLEditView> createState() => _ShortURLEditViewState();
|
||||
}
|
||||
|
||||
class _ShortURLEditViewState extends State<ShortURLEditView> with SingleTickerProviderStateMixin {
|
||||
|
||||
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;
|
||||
|
||||
String longUrlError = "";
|
||||
String randomSlugLengthError = "";
|
||||
|
||||
bool isSaving = false;
|
||||
|
||||
late AnimationController _customSlugDiceAnimationController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_customSlugDiceAnimationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: Duration(milliseconds: 500),
|
||||
);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
longUrlController.dispose();
|
||||
customSlugController.dispose();
|
||||
titleController.dispose();
|
||||
randomSlugLengthController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _submitShortUrl() async {
|
||||
var newSubmission = ShortURLSubmission(
|
||||
longUrl: longUrlController.text,
|
||||
deviceLongUrls: null, tags: [],
|
||||
crawlable: isCrawlable,
|
||||
forwardQuery: forwardQuery,
|
||||
findIfExists: true,
|
||||
title: titleController.text != "" ? titleController.text : null,
|
||||
customSlug: customSlugController.text != "" && !randomSlug ? customSlugController.text : null,
|
||||
shortCodeLength: randomSlug ? int.parse(randomSlugLengthController.text) : null);
|
||||
var response = await globals.serverManager.submitShortUrl(newSubmission);
|
||||
|
||||
response.fold((l) async {
|
||||
setState(() {
|
||||
isSaving = false;
|
||||
});
|
||||
|
||||
if (copyToClipboard) {
|
||||
await Clipboard.setData(ClipboardData(text: l));
|
||||
final snackBar = SnackBar(content: Text("Copied to clipboard!"), backgroundColor: Colors.green[400], behavior: SnackBarBehavior.floating);
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
}
|
||||
else {
|
||||
final snackBar = SnackBar(content: Text("Short URL created!"), backgroundColor: Colors.green[400], behavior: SnackBarBehavior.floating);
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
}
|
||||
Navigator.pop(context);
|
||||
|
||||
return true;
|
||||
}, (r) {
|
||||
setState(() {
|
||||
isSaving = false;
|
||||
});
|
||||
|
||||
var text = "";
|
||||
|
||||
if (r is RequestFailure) {
|
||||
text = r.description;
|
||||
}
|
||||
else {
|
||||
text = (r as ApiFailure).detail;
|
||||
if ((r as ApiFailure).invalidElements != null) {
|
||||
text = text + ": " + (r as ApiFailure).invalidElements.toString();
|
||||
}
|
||||
}
|
||||
|
||||
final snackBar = SnackBar(content: Text(text), backgroundColor: Colors.red[400], behavior: SnackBarBehavior.floating);
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
SliverAppBar.medium(
|
||||
title: Text("New Short URL", style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(left: 16, right: 16, top: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
TextField(
|
||||
controller: longUrlController,
|
||||
decoration: InputDecoration(
|
||||
errorText: longUrlError != "" ? longUrlError : null,
|
||||
border: OutlineInputBorder(),
|
||||
label: Row(
|
||||
children: [
|
||||
Icon(Icons.public),
|
||||
SizedBox(width: 8),
|
||||
Text("Long URL")
|
||||
],
|
||||
)
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: customSlugController,
|
||||
style: TextStyle(color: randomSlug ? Colors.grey : Theme.of(context).brightness == Brightness.light ? Colors.black : Colors.white),
|
||||
onChanged: (_) {
|
||||
if (randomSlug) setState(() {
|
||||
randomSlug = false;
|
||||
});
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
label: Row(
|
||||
children: [
|
||||
Icon(Icons.link),
|
||||
SizedBox(width: 8),
|
||||
Text("${randomSlug ? "Random" : "Custom"} slug", style: TextStyle(fontStyle: randomSlug ? FontStyle.italic : FontStyle.normal),)
|
||||
],
|
||||
)
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
RotationTransition(
|
||||
turns: Tween(begin: 0.0, end: 3.0).animate(CurvedAnimation(parent: _customSlugDiceAnimationController, curve: Curves.easeInOutExpo)),
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
if (randomSlug) {
|
||||
_customSlugDiceAnimationController.reverse(from: 1);
|
||||
}
|
||||
else {
|
||||
_customSlugDiceAnimationController.forward(from: 0);
|
||||
}
|
||||
setState(() {
|
||||
randomSlug = !randomSlug;
|
||||
});
|
||||
|
||||
},
|
||||
icon: Icon(randomSlug ? Icons.casino : Icons.casino_outlined, color: randomSlug ? Colors.green : Theme.of(context).primaryColor,)
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
if (randomSlug)
|
||||
SizedBox(height: 16),
|
||||
|
||||
if (randomSlug)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text("Random slug length"),
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: TextField(
|
||||
controller: randomSlugLengthController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
errorText: randomSlugLengthError != "" ? "" : null,
|
||||
border: OutlineInputBorder(),
|
||||
label: Row(
|
||||
children: [
|
||||
Icon(Icons.tag),
|
||||
SizedBox(width: 8),
|
||||
Text("Length")
|
||||
],
|
||||
)
|
||||
),
|
||||
))
|
||||
],
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: titleController,
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
label: Row(
|
||||
children: [
|
||||
Icon(Icons.badge),
|
||||
SizedBox(width: 8),
|
||||
Text("Title")
|
||||
],
|
||||
)
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text("Crawlable"),
|
||||
Switch(
|
||||
value: isCrawlable,
|
||||
onChanged: (_) {
|
||||
setState(() {
|
||||
isCrawlable = !isCrawlable;
|
||||
});
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text("Forward query params"),
|
||||
Switch(
|
||||
value: forwardQuery,
|
||||
onChanged: (_) {
|
||||
setState(() {
|
||||
forwardQuery = !forwardQuery;
|
||||
});
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text("Copy to clipboard"),
|
||||
Switch(
|
||||
value: copyToClipboard,
|
||||
onChanged: (_) {
|
||||
setState(() {
|
||||
copyToClipboard = !copyToClipboard;
|
||||
});
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
)
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () {
|
||||
if (!isSaving) {
|
||||
setState(() {
|
||||
isSaving = true;
|
||||
longUrlError = "";
|
||||
randomSlugLengthError = "";
|
||||
});
|
||||
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();
|
||||
}
|
||||
}
|
||||
},
|
||||
child: isSaving ? Padding(padding: EdgeInsets.all(16), child: CircularProgressIndicator(strokeWidth: 3)) : Icon(Icons.save)
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,193 @@
|
|||
import 'package:dynamic_color/dynamic_color.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shlink_app/API/Classes/ShortURL/ShortURL.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:shlink_app/API/ServerManager.dart';
|
||||
import 'globals.dart' as globals;
|
||||
|
||||
class URLDetailView extends StatefulWidget {
|
||||
const URLDetailView({super.key, required this.shortURL});
|
||||
|
||||
final ShortURL shortURL;
|
||||
|
||||
@override
|
||||
State<URLDetailView> createState() => _URLDetailViewState();
|
||||
}
|
||||
|
||||
class _URLDetailViewState extends State<URLDetailView> {
|
||||
|
||||
Future showDeletionConfirmation() {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text("Delete Short URL"),
|
||||
content: SingleChildScrollView(
|
||||
child: ListBody(
|
||||
children: [
|
||||
const Text("You're about to delete"),
|
||||
SizedBox(height: 4),
|
||||
Text("${widget.shortURL.title ?? widget.shortURL.shortCode}", style: TextStyle(fontStyle: FontStyle.italic),),
|
||||
SizedBox(height: 4),
|
||||
const Text("It'll be gone forever! (a very long time)")
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => { Navigator.of(context).pop() }, child: const Text("Cancel")),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
var response = await globals.serverManager.deleteShortUrl(widget.shortURL.shortCode);
|
||||
|
||||
response.fold((l) {
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context, "reload");
|
||||
|
||||
final snackBar = SnackBar(content: Text("Short URL deleted!"), backgroundColor: Colors.green[400], behavior: SnackBarBehavior.floating);
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
return true;
|
||||
}, (r) {
|
||||
var text = "";
|
||||
if (r is RequestFailure) {
|
||||
text = r.description;
|
||||
}
|
||||
else {
|
||||
text = (r as ApiFailure).detail;
|
||||
}
|
||||
|
||||
final snackBar = SnackBar(content: Text(text), backgroundColor: Colors.red[400], behavior: SnackBarBehavior.floating);
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
return false;
|
||||
});
|
||||
},
|
||||
child: Text("Delete", style: TextStyle(color: Colors.red)),
|
||||
)
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
SliverAppBar.medium(
|
||||
title: Text(widget.shortURL.title ?? widget.shortURL.shortCode, style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
actions: [
|
||||
IconButton(onPressed: () {
|
||||
showDeletionConfirmation();
|
||||
}, icon: Icon(Icons.delete, color: Colors.red,))
|
||||
],
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 16.0, right: 16.0),
|
||||
child: Wrap(
|
||||
children: widget.shortURL.tags.map((tag) {
|
||||
var randomColor = ([...Colors.primaries]..shuffle()).first.harmonizeWith(Theme.of(context).colorScheme.primary);
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(right: 4, top: 4),
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(top: 4, bottom: 4, left: 12, right: 12),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
color: randomColor,
|
||||
),
|
||||
child: Text(tag, style: TextStyle(color: randomColor.computeLuminance() < 0.5 ? Colors.white : Colors.black),),
|
||||
),
|
||||
);
|
||||
}).toList()
|
||||
),
|
||||
),
|
||||
),
|
||||
_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: "iOS", content: widget.shortURL.deviceLongUrls.ios, sub: true),
|
||||
_ListCell(title: "Android", content: widget.shortURL.deviceLongUrls.android, sub: true),
|
||||
_ListCell(title: "Desktop", content: widget.shortURL.deviceLongUrls.desktop, sub: true),
|
||||
_ListCell(title: "Creation Date", content: widget.shortURL.dateCreated),
|
||||
_ListCell(title: "Visits", content: ""),
|
||||
_ListCell(title: "Total", content: widget.shortURL.visitsSummary.total, sub: true),
|
||||
_ListCell(title: "Non-Bots", content: widget.shortURL.visitsSummary.nonBots, sub: true),
|
||||
_ListCell(title: "Bots", content: widget.shortURL.visitsSummary.bots, sub: true),
|
||||
_ListCell(title: "Meta", content: ""),
|
||||
_ListCell(title: "Valid Since", content: widget.shortURL.meta.validSince, sub: true),
|
||||
_ListCell(title: "Valid Until", content: widget.shortURL.meta.validUntil, sub: true),
|
||||
_ListCell(title: "Max Visits", content: widget.shortURL.meta.maxVisits, sub: true),
|
||||
_ListCell(title: "Domain", content: widget.shortURL.domain),
|
||||
_ListCell(title: "Crawlable", content: widget.shortURL.crawlable, last: true)
|
||||
|
||||
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ListCell extends StatefulWidget {
|
||||
const _ListCell({required this.title, required this.content, this.sub = false, this.last = false});
|
||||
|
||||
final String title;
|
||||
final dynamic content;
|
||||
final bool sub;
|
||||
final bool last;
|
||||
|
||||
@override
|
||||
State<_ListCell> createState() => _ListCellState();
|
||||
}
|
||||
|
||||
class _ListCellState extends State<_ListCell> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: 16, bottom: widget.last ? 30 : 0),
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(top: 16, left: 8, right: 8),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(top: BorderSide(width: 1, color: MediaQuery.of(context).platformBrightness == Brightness.dark ? Colors.grey[800]! : Colors.grey[300]!)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
if (widget.sub)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(right: 4),
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
height: 8,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Theme.of(context).brightness == Brightness.dark ? Colors.grey[700] : Colors.grey[300],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(widget.title, style: TextStyle(fontWeight: FontWeight.bold),)],
|
||||
),
|
||||
if (widget.content is bool)
|
||||
Icon(widget.content ? Icons.check : Icons.close, color: widget.content ? Colors.green : Colors.red)
|
||||
else if (widget.content is int)
|
||||
Text(widget.content.toString())
|
||||
else if (widget.content is String)
|
||||
Expanded(
|
||||
child: Text(widget.content, textAlign: TextAlign.end, overflow: TextOverflow.ellipsis, maxLines: 1,),
|
||||
)
|
||||
else if (widget.content is DateTime)
|
||||
Text(DateFormat('yyyy-MM-dd - HH:mm').format(widget.content))
|
||||
else
|
||||
Text("N/A")
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,11 @@
|
|||
import 'package:dynamic_color/dynamic_color.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
|
||||
import 'package:shlink_app/API/Classes/ShortURL.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:shlink_app/API/Classes/ShortURL/ShortURL.dart';
|
||||
import 'package:shlink_app/API/ServerManager.dart';
|
||||
import 'package:shlink_app/URLDetailView.dart';
|
||||
import 'globals.dart' as globals;
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class URLListView extends StatefulWidget {
|
||||
const URLListView({Key? key}) : super(key: key);
|
||||
|
@ -14,6 +17,8 @@ class URLListView extends StatefulWidget {
|
|||
class _URLListViewState extends State<URLListView> {
|
||||
|
||||
List<ShortURL> shortUrls = [];
|
||||
bool _qrCodeShown = false;
|
||||
String _qrUrl = "";
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
@ -23,14 +28,13 @@ class _URLListViewState extends State<URLListView> {
|
|||
.addPostFrameCallback((_) => loadAllShortUrls());
|
||||
}
|
||||
|
||||
|
||||
|
||||
void loadAllShortUrls() async {
|
||||
Future<void> loadAllShortUrls() async {
|
||||
final response = await globals.serverManager.getShortUrls();
|
||||
response.fold((l) {
|
||||
setState(() {
|
||||
shortUrls = l;
|
||||
});
|
||||
return true;
|
||||
}, (r) {
|
||||
var text = "";
|
||||
if (r is RequestFailure) {
|
||||
|
@ -40,24 +44,42 @@ class _URLListViewState extends State<URLListView> {
|
|||
text = (r as ApiFailure).detail;
|
||||
}
|
||||
|
||||
final snackBar = SnackBar(content: Text(text), behavior: SnackBarBehavior.floating);
|
||||
final snackBar = SnackBar(content: Text(text), backgroundColor: Colors.red[400], behavior: SnackBarBehavior.floating);
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: CustomScrollView(
|
||||
body: Stack(
|
||||
children: [
|
||||
ColorFiltered(
|
||||
colorFilter: ColorFilter.mode(Colors.black.withOpacity(_qrCodeShown ? 0.4 : 0), BlendMode.srcOver),
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
//loadAllShortUrls();
|
||||
return loadAllShortUrls();
|
||||
//Future.value(true);
|
||||
},
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
SliverAppBar.medium(
|
||||
title: Text("All short URLs", style: TextStyle(fontWeight: FontWeight.bold))
|
||||
title: Text("Short URLs", style: TextStyle(fontWeight: FontWeight.bold))
|
||||
),
|
||||
SliverList(delegate: SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) {
|
||||
(BuildContext _context, int index) {
|
||||
final shortURL = shortUrls[index];
|
||||
return Padding(
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
final result = await Navigator.of(context).push(MaterialPageRoute(builder: (context) => URLDetailView(shortURL: shortURL)));
|
||||
|
||||
if (result == "reload") {
|
||||
loadAllShortUrls();
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(top: 8, bottom: 8),
|
||||
|
@ -75,23 +97,90 @@ class _URLListViewState extends State<URLListView> {
|
|||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Text("${shortURL.title ?? shortURL.shortCode}", textScaleFactor: 1.4, style: TextStyle(fontWeight: FontWeight.bold),),
|
||||
Text("${shortURL.longUrl}",maxLines: 1, overflow: TextOverflow.ellipsis, textScaleFactor: 0.9, style: TextStyle(color: Colors.grey[600]),)
|
||||
Text("${shortURL.longUrl}",maxLines: 1, overflow: TextOverflow.ellipsis, textScaleFactor: 0.9, style: TextStyle(color: Colors.grey[600]),),
|
||||
// List tags in a row
|
||||
Wrap(
|
||||
children: shortURL.tags.map((tag) {
|
||||
var randomColor = ([...Colors.primaries]..shuffle()).first.harmonizeWith(Theme.of(context).colorScheme.primary);
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(right: 4, top: 4),
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(top: 4, bottom: 4, left: 12, right: 12),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
color: randomColor,
|
||||
),
|
||||
child: Text(tag, style: TextStyle(color: randomColor.computeLuminance() < 0.5 ? Colors.white : Colors.black),),
|
||||
),
|
||||
);
|
||||
}).toList()
|
||||
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(onPressed: () async {
|
||||
await Clipboard.setData(ClipboardData(text: shortURL.shortUrl));
|
||||
final snackBar = SnackBar(content: Text("Copied to clipboard!"), behavior: SnackBarBehavior.floating, backgroundColor: Colors.green[400]);
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
}, icon: Icon(Icons.copy)),
|
||||
IconButton(onPressed: () {
|
||||
|
||||
setState(() {
|
||||
_qrUrl = shortURL.shortUrl;
|
||||
_qrCodeShown = true;
|
||||
});
|
||||
}, icon: Icon(Icons.qr_code))
|
||||
],
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
},
|
||||
childCount: shortUrls.length
|
||||
))
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_qrCodeShown)
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_qrCodeShown = false;
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
color: Colors.black.withOpacity(0),
|
||||
),
|
||||
),
|
||||
if (_qrCodeShown)
|
||||
Center(
|
||||
child: SizedBox(
|
||||
width: MediaQuery.of(context).size.width / 1.7,
|
||||
height: MediaQuery.of(context).size.width / 1.7,
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: QrImageView(
|
||||
data: _qrUrl,
|
||||
version: QrVersions.auto,
|
||||
size: 200.0,
|
||||
eyeStyle: QrEyeStyle(
|
||||
eyeShape: QrEyeShape.square,
|
||||
color: MediaQuery.of(context).platformBrightness == Brightness.dark ? Colors.white : Colors.black,
|
||||
),
|
||||
dataModuleStyle: QrDataModuleStyle(
|
||||
dataModuleShape: QrDataModuleShape.square,
|
||||
color: MediaQuery.of(context).platformBrightness == Brightness.dark ? Colors.white : Colors.black,
|
||||
),
|
||||
)
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:shlink_app/LoginView.dart';
|
||||
import 'package:shlink_app/NavigationBarView.dart';
|
||||
import 'globals.dart' as globals;
|
||||
import 'package:dynamic_color/dynamic_color.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const MyApp());
|
||||
|
@ -11,18 +11,44 @@ void main() {
|
|||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
static final _defaultLightColorScheme =
|
||||
ColorScheme.fromSwatch(primarySwatch: Colors.blue);
|
||||
|
||||
static final _defaultDarkColorScheme = ColorScheme.fromSwatch(
|
||||
primarySwatch: Colors.blue, brightness: Brightness.dark);
|
||||
|
||||
// This widget is the root of your application.
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DynamicColorBuilder(builder: (lightColorScheme, darkColorScheme) {
|
||||
return MaterialApp(
|
||||
title: 'Flutter Demo',
|
||||
title: 'Shlink',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: ThemeData(
|
||||
primarySwatch: Colors.blue,
|
||||
brightness: Brightness.light,
|
||||
appBarTheme: AppBarTheme(
|
||||
backgroundColor: Color(0xfffafafa),
|
||||
),
|
||||
colorScheme: lightColorScheme ?? _defaultLightColorScheme,
|
||||
useMaterial3: true
|
||||
),
|
||||
darkTheme: ThemeData(
|
||||
appBarTheme: AppBarTheme(
|
||||
backgroundColor: Color(0xff0d0d0d),
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
),
|
||||
colorScheme: darkColorScheme?.copyWith(background: Colors.black, primary: Colors.blue) ?? _defaultDarkColorScheme,
|
||||
useMaterial3: true,
|
||||
),
|
||||
/*theme: ThemeData(
|
||||
primarySwatch: Colors.blue,
|
||||
brightness: Brightness.light,
|
||||
useMaterial3: true,
|
||||
colorScheme: ColorScheme.fromSwatch().copyWith(
|
||||
secondary: Colors.orange
|
||||
)
|
||||
),*/
|
||||
/*darkTheme: ThemeData(
|
||||
primarySwatch: Colors.blue,
|
||||
brightness: Brightness.dark,
|
||||
useMaterial3: true,
|
||||
|
@ -31,10 +57,11 @@ class MyApp extends StatelessWidget {
|
|||
surface: Color(0xff0d0d0d),
|
||||
secondaryContainer: Colors.grey[300]
|
||||
)
|
||||
),
|
||||
),*/
|
||||
themeMode: ThemeMode.system,
|
||||
home: const InitialPage(),
|
||||
home: InitialPage()
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
48
pubspec.lock
48
pubspec.lock
|
@ -57,6 +57,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.10.1"
|
||||
dynamic_color:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dynamic_color
|
||||
sha256: de4798a7069121aee12d5895315680258415de9b00e717723a1bd73d58f0126d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.6.6"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -160,6 +168,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.2"
|
||||
intl:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: intl
|
||||
sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.18.1"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -200,14 +216,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.8.0"
|
||||
modal_bottom_sheet:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: modal_bottom_sheet
|
||||
sha256: "3bba63c62d35c931bce7f8ae23a47f9a05836d8cb3c11122ada64e0b2f3d718f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0-pre"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -224,6 +232,22 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
qr:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: qr
|
||||
sha256: "64957a3930367bf97cc211a5af99551d630f2f4625e38af10edd6b19131b64b3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
qr_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: qr_flutter
|
||||
sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.0"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
|
@ -277,6 +301,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.16"
|
||||
tuple:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: tuple
|
||||
sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
@ -19,8 +19,8 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||
version: 1.0.0+1
|
||||
|
||||
environment:
|
||||
sdk: '>=2.19.6 <3.0.0'
|
||||
|
||||
#sdk: '>=2.19.6 <3.0.0'
|
||||
sdk: ^2.19.0
|
||||
# Dependencies specify other packages that your package needs in order to work.
|
||||
# To automatically upgrade your package dependencies to the latest versions
|
||||
# consider running `flutter pub upgrade --major-versions`. Alternatively,
|
||||
|
@ -39,7 +39,10 @@ dependencies:
|
|||
flutter_process_text: ^1.1.2
|
||||
flutter_secure_storage: ^8.0.0
|
||||
dartz: ^0.10.1
|
||||
modal_bottom_sheet: ^3.0.0-pre
|
||||
qr_flutter: ^4.1.0
|
||||
tuple: ^2.0.2
|
||||
intl: ^0.18.1
|
||||
dynamic_color: ^1.6.6
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
Loading…
Reference in New Issue
Block a user