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/ShortURL_DeviceLongUrls.dart';
|
||||||
import 'package:shlink_app/API/Classes/ShortURL_Meta.dart';
|
import 'package:shlink_app/API/Classes/ShortURL/ShortURL_Meta.dart';
|
||||||
import 'package:shlink_app/API/Classes/ShortURL_VisitsSummary.dart';
|
import 'package:shlink_app/API/Classes/ShortURL/ShortURL_VisitsSummary.dart';
|
||||||
|
|
||||||
class ShortURL {
|
class ShortURL {
|
||||||
String shortCode;
|
String shortCode;
|
|
@ -1,5 +1,3 @@
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
class ShortURL_DeviceLongUrls {
|
class ShortURL_DeviceLongUrls {
|
||||||
final String? android;
|
final String? android;
|
||||||
final String? ios;
|
final String? ios;
|
||||||
|
@ -11,4 +9,10 @@ class ShortURL_DeviceLongUrls {
|
||||||
: android = json["android"],
|
: android = json["android"],
|
||||||
ios = json["ios"],
|
ios = json["ios"],
|
||||||
desktop = json["desktop"];
|
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:async';
|
||||||
import 'dart:convert';
|
|
||||||
import 'package:dartz/dartz.dart';
|
import 'package:dartz/dartz.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:shlink_app/API/Classes/ShlinkStats/ShlinkStats.dart';
|
||||||
import 'package:shlink_app/API/Classes/ShortURL.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 {
|
class ServerManager {
|
||||||
|
|
||||||
|
@ -53,80 +59,23 @@ class ServerManager {
|
||||||
|
|
||||||
FutureOr<Either<String, Failure>> connect() async {
|
FutureOr<Either<String, Failure>> connect() async {
|
||||||
_loadCredentials();
|
_loadCredentials();
|
||||||
try {
|
return API_connect(_api_key, _server_url, apiVersion);
|
||||||
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()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FutureOr<Either<List<ShortURL>, Failure>> getShortUrls() async {
|
FutureOr<Either<List<ShortURL>, Failure>> getShortUrls() async {
|
||||||
var _currentPage = 1;
|
return API_getShortUrls(_api_key, _server_url, apiVersion);
|
||||||
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!);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FutureOr<Either<ShortURLPageResponse, Failure>> _getShortUrlPage(int page) async {
|
FutureOr<Either<ShlinkStats, Failure>> getShlinkStats() async {
|
||||||
try {
|
return API_getShlinkStats(_api_key, _server_url, apiVersion);
|
||||||
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 {
|
FutureOr<Either<String, Failure>> submitShortUrl(ShortURLSubmission shortUrl) async {
|
||||||
var jsonBody = jsonDecode(response.body);
|
return API_submitShortUrl(shortUrl, _api_key, _server_url, apiVersion);
|
||||||
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>> deleteShortUrl(String shortCode) async {
|
||||||
|
return API_deleteShortUrl(shortCode, _api_key, _server_url, apiVersion);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,6 +100,7 @@ class ApiFailure extends Failure {
|
||||||
String detail;
|
String detail;
|
||||||
String title;
|
String title;
|
||||||
int status;
|
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: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;
|
import 'globals.dart' as globals;
|
||||||
|
|
||||||
class HomeView extends StatefulWidget {
|
class HomeView extends StatefulWidget {
|
||||||
|
@ -10,10 +13,34 @@ class HomeView extends StatefulWidget {
|
||||||
|
|
||||||
class _HomeViewState extends State<HomeView> {
|
class _HomeViewState extends State<HomeView> {
|
||||||
|
|
||||||
|
ShlinkStats? shlinkStats;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
// TODO: implement initState
|
// TODO: implement initState
|
||||||
super.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
|
@override
|
||||||
|
@ -30,9 +57,66 @@ class _HomeViewState extends State<HomeView> {
|
||||||
Text(globals.serverManager.getServerUrl(), style: TextStyle(fontSize: 16, color: Colors.grey[600]))
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Center(
|
body: views.elementAt(_selectedView),
|
||||||
child: views.elementAt(_selectedView),
|
|
||||||
),
|
|
||||||
bottomNavigationBar: NavigationBar(
|
bottomNavigationBar: NavigationBar(
|
||||||
destinations: [
|
destinations: [
|
||||||
NavigationDestination(icon: Icon(Icons.home), label: "Home"),
|
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:flutter/material.dart';
|
||||||
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
|
import 'package:qr_flutter/qr_flutter.dart';
|
||||||
import 'package:shlink_app/API/Classes/ShortURL.dart';
|
import 'package:shlink_app/API/Classes/ShortURL/ShortURL.dart';
|
||||||
import 'package:shlink_app/API/ServerManager.dart';
|
import 'package:shlink_app/API/ServerManager.dart';
|
||||||
|
import 'package:shlink_app/URLDetailView.dart';
|
||||||
import 'globals.dart' as globals;
|
import 'globals.dart' as globals;
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
class URLListView extends StatefulWidget {
|
class URLListView extends StatefulWidget {
|
||||||
const URLListView({Key? key}) : super(key: key);
|
const URLListView({Key? key}) : super(key: key);
|
||||||
|
@ -14,6 +17,8 @@ class URLListView extends StatefulWidget {
|
||||||
class _URLListViewState extends State<URLListView> {
|
class _URLListViewState extends State<URLListView> {
|
||||||
|
|
||||||
List<ShortURL> shortUrls = [];
|
List<ShortURL> shortUrls = [];
|
||||||
|
bool _qrCodeShown = false;
|
||||||
|
String _qrUrl = "";
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
@ -23,14 +28,13 @@ class _URLListViewState extends State<URLListView> {
|
||||||
.addPostFrameCallback((_) => loadAllShortUrls());
|
.addPostFrameCallback((_) => loadAllShortUrls());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> loadAllShortUrls() async {
|
||||||
|
|
||||||
void loadAllShortUrls() async {
|
|
||||||
final response = await globals.serverManager.getShortUrls();
|
final response = await globals.serverManager.getShortUrls();
|
||||||
response.fold((l) {
|
response.fold((l) {
|
||||||
setState(() {
|
setState(() {
|
||||||
shortUrls = l;
|
shortUrls = l;
|
||||||
});
|
});
|
||||||
|
return true;
|
||||||
}, (r) {
|
}, (r) {
|
||||||
var text = "";
|
var text = "";
|
||||||
if (r is RequestFailure) {
|
if (r is RequestFailure) {
|
||||||
|
@ -40,24 +44,42 @@ class _URLListViewState extends State<URLListView> {
|
||||||
text = (r as ApiFailure).detail;
|
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);
|
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||||
|
return false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
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: [
|
slivers: [
|
||||||
SliverAppBar.medium(
|
SliverAppBar.medium(
|
||||||
title: Text("All short URLs", style: TextStyle(fontWeight: FontWeight.bold))
|
title: Text("Short URLs", style: TextStyle(fontWeight: FontWeight.bold))
|
||||||
),
|
),
|
||||||
SliverList(delegate: SliverChildBuilderDelegate(
|
SliverList(delegate: SliverChildBuilderDelegate(
|
||||||
(BuildContext context, int index) {
|
(BuildContext _context, int index) {
|
||||||
final shortURL = shortUrls[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),
|
padding: EdgeInsets.all(8),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: EdgeInsets.only(top: 8, bottom: 8),
|
padding: EdgeInsets.only(top: 8, bottom: 8),
|
||||||
|
@ -75,23 +97,90 @@ class _URLListViewState extends State<URLListView> {
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text("${shortURL.title ?? shortURL.shortCode}", textScaleFactor: 1.4, style: TextStyle(fontWeight: FontWeight.bold),),
|
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: () {
|
IconButton(onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_qrUrl = shortURL.shortUrl;
|
||||||
|
_qrCodeShown = true;
|
||||||
|
});
|
||||||
}, icon: Icon(Icons.qr_code))
|
}, icon: Icon(Icons.qr_code))
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
childCount: shortUrls.length
|
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/material.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|
||||||
import 'package:shlink_app/LoginView.dart';
|
import 'package:shlink_app/LoginView.dart';
|
||||||
import 'package:shlink_app/NavigationBarView.dart';
|
import 'package:shlink_app/NavigationBarView.dart';
|
||||||
import 'globals.dart' as globals;
|
import 'globals.dart' as globals;
|
||||||
|
import 'package:dynamic_color/dynamic_color.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
runApp(const MyApp());
|
runApp(const MyApp());
|
||||||
|
@ -11,18 +11,44 @@ void main() {
|
||||||
class MyApp extends StatelessWidget {
|
class MyApp extends StatelessWidget {
|
||||||
const MyApp({super.key});
|
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.
|
// This widget is the root of your application.
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
return DynamicColorBuilder(builder: (lightColorScheme, darkColorScheme) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
title: 'Flutter Demo',
|
title: 'Shlink',
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
primarySwatch: Colors.blue,
|
appBarTheme: AppBarTheme(
|
||||||
brightness: Brightness.light,
|
backgroundColor: Color(0xfffafafa),
|
||||||
|
),
|
||||||
|
colorScheme: lightColorScheme ?? _defaultLightColorScheme,
|
||||||
useMaterial3: true
|
useMaterial3: true
|
||||||
),
|
),
|
||||||
darkTheme: ThemeData(
|
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,
|
primarySwatch: Colors.blue,
|
||||||
brightness: Brightness.dark,
|
brightness: Brightness.dark,
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
|
@ -31,10 +57,11 @@ class MyApp extends StatelessWidget {
|
||||||
surface: Color(0xff0d0d0d),
|
surface: Color(0xff0d0d0d),
|
||||||
secondaryContainer: Colors.grey[300]
|
secondaryContainer: Colors.grey[300]
|
||||||
)
|
)
|
||||||
),
|
),*/
|
||||||
themeMode: ThemeMode.system,
|
themeMode: ThemeMode.system,
|
||||||
home: const InitialPage(),
|
home: InitialPage()
|
||||||
);
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
48
pubspec.lock
48
pubspec.lock
|
@ -57,6 +57,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.10.1"
|
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:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -160,6 +168,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.2"
|
version: "4.0.2"
|
||||||
|
intl:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: intl
|
||||||
|
sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.18.1"
|
||||||
js:
|
js:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -200,14 +216,6 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.8.0"
|
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:
|
path:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -224,6 +232,22 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.4"
|
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:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
|
@ -277,6 +301,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.4.16"
|
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:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -19,8 +19,8 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||||
version: 1.0.0+1
|
version: 1.0.0+1
|
||||||
|
|
||||||
environment:
|
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.
|
# Dependencies specify other packages that your package needs in order to work.
|
||||||
# To automatically upgrade your package dependencies to the latest versions
|
# To automatically upgrade your package dependencies to the latest versions
|
||||||
# consider running `flutter pub upgrade --major-versions`. Alternatively,
|
# consider running `flutter pub upgrade --major-versions`. Alternatively,
|
||||||
|
@ -39,7 +39,10 @@ dependencies:
|
||||||
flutter_process_text: ^1.1.2
|
flutter_process_text: ^1.1.2
|
||||||
flutter_secure_storage: ^8.0.0
|
flutter_secure_storage: ^8.0.0
|
||||||
dartz: ^0.10.1
|
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:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
Loading…
Reference in New Issue
Block a user