mirror of
https://github.com/rainloreley/shlink-manager.git
synced 2024-11-24 02:33:01 +01:00
formatting
This commit is contained in:
parent
7b16683d10
commit
086ca47fc0
|
@ -9,7 +9,23 @@
|
|||
# packages, and plugins designed to encourage good coding practices.
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
|
||||
|
||||
linter:
|
||||
rules:
|
||||
# Style rules
|
||||
- camel_case_types
|
||||
- library_names
|
||||
- avoid_catching_errors
|
||||
- avoid_empty_else
|
||||
- unnecessary_brace_in_string_interps
|
||||
- avoid_redundant_argument_values
|
||||
- leading_newlines_in_multiline_strings
|
||||
# formatting
|
||||
- lines_longer_than_80_chars
|
||||
- curly_braces_in_flow_control_structures
|
||||
# doc comments
|
||||
- slash_for_doc_comments
|
||||
# The lint rules applied to this project can be customized in the
|
||||
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||
# included above or to enable additional rules. A list of all available lints
|
||||
|
@ -21,7 +37,6 @@ linter:
|
|||
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||
# producing the lint.
|
||||
rules:
|
||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||
|
||||
|
|
|
@ -4,12 +4,16 @@ import 'package:shlink_app/API/Classes/ShortURL/visits_summary.dart';
|
|||
class ShlinkStats {
|
||||
/// Data about non-orphan visits
|
||||
VisitsSummary nonOrphanVisits;
|
||||
|
||||
/// Data about orphan visits (without any valid slug assigned)
|
||||
VisitsSummary orphanVisits;
|
||||
|
||||
/// Total count of all short URLs
|
||||
int shortUrlsCount;
|
||||
|
||||
/// Total count all all tags
|
||||
int tagsCount;
|
||||
|
||||
ShlinkStats(this.nonOrphanVisits, this.orphanVisits, this.shortUrlsCount, this.tagsCount);
|
||||
}
|
||||
ShlinkStats(this.nonOrphanVisits, this.orphanVisits, this.shortUrlsCount,
|
||||
this.tagsCount);
|
||||
}
|
||||
|
|
|
@ -2,8 +2,10 @@
|
|||
class ShlinkStatsVisits {
|
||||
/// Count of URL visits
|
||||
int total;
|
||||
|
||||
/// Count of URL visits from humans
|
||||
int nonBots;
|
||||
|
||||
/// Count of URL visits from bots/crawlers
|
||||
int bots;
|
||||
|
||||
|
@ -14,4 +16,4 @@ class ShlinkStatsVisits {
|
|||
: total = json["total"],
|
||||
nonBots = json["nonBots"],
|
||||
bots = json["bots"];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,8 +2,10 @@
|
|||
class DeviceLongUrls {
|
||||
/// Custom URL for Android devices
|
||||
final String? android;
|
||||
|
||||
/// Custom URL for iOS devices
|
||||
final String? ios;
|
||||
|
||||
/// Custom URL for desktop
|
||||
final String? desktop;
|
||||
|
||||
|
@ -11,14 +13,11 @@ class DeviceLongUrls {
|
|||
|
||||
/// Converts JSON data from the API to an instance of [DeviceLongUrls]
|
||||
DeviceLongUrls.fromJson(Map<String, dynamic> json)
|
||||
: android = json["android"],
|
||||
ios = json["ios"],
|
||||
desktop = json["desktop"];
|
||||
: android = json["android"],
|
||||
ios = json["ios"],
|
||||
desktop = json["desktop"];
|
||||
|
||||
/// Converts data from this class to an JSON object of type
|
||||
Map<String, dynamic> toJson() => {
|
||||
"android": android,
|
||||
"ios": ios,
|
||||
"desktop": desktop
|
||||
};
|
||||
}
|
||||
Map<String, dynamic> toJson() =>
|
||||
{"android": android, "ios": ios, "desktop": desktop};
|
||||
}
|
||||
|
|
|
@ -6,41 +6,61 @@ import 'package:shlink_app/API/Classes/ShortURL/visits_summary.dart';
|
|||
class ShortURL {
|
||||
/// Slug of the short URL used in the URL
|
||||
String shortCode;
|
||||
|
||||
/// Entire short URL
|
||||
String shortUrl;
|
||||
|
||||
/// Long URL where the user gets redirected to
|
||||
String longUrl;
|
||||
|
||||
/// Device-specific long URLs
|
||||
DeviceLongUrls deviceLongUrls;
|
||||
|
||||
/// Creation date of the short URL
|
||||
DateTime dateCreated;
|
||||
|
||||
/// Visitor data
|
||||
VisitsSummary visitsSummary;
|
||||
|
||||
/// List of tags assigned to this short URL
|
||||
List<dynamic> tags;
|
||||
|
||||
/// Metadata
|
||||
ShortURLMeta meta;
|
||||
|
||||
/// Associated domain
|
||||
String? domain;
|
||||
|
||||
/// Optional title
|
||||
String? title;
|
||||
|
||||
/// Whether the short URL is crawlable by a web crawler
|
||||
bool crawlable;
|
||||
|
||||
ShortURL(this.shortCode, this.shortUrl, this.longUrl, this.deviceLongUrls, this.dateCreated, this.visitsSummary, this.tags, this.meta, this.domain, this.title, this.crawlable);
|
||||
ShortURL(
|
||||
this.shortCode,
|
||||
this.shortUrl,
|
||||
this.longUrl,
|
||||
this.deviceLongUrls,
|
||||
this.dateCreated,
|
||||
this.visitsSummary,
|
||||
this.tags,
|
||||
this.meta,
|
||||
this.domain,
|
||||
this.title,
|
||||
this.crawlable);
|
||||
|
||||
/// Converts the JSON data from the API to an instance of [ShortURL]
|
||||
ShortURL.fromJson(Map<String, dynamic> json):
|
||||
shortCode = json["shortCode"],
|
||||
shortUrl = json["shortUrl"],
|
||||
longUrl = json["longUrl"],
|
||||
deviceLongUrls = DeviceLongUrls.fromJson(json["deviceLongUrls"]),
|
||||
dateCreated = DateTime.parse(json["dateCreated"]),
|
||||
visitsSummary = VisitsSummary.fromJson(json["visitsSummary"]),
|
||||
tags = json["tags"],
|
||||
meta = ShortURLMeta.fromJson(json["meta"]),
|
||||
domain = json["domain"],
|
||||
title = json["title"],
|
||||
crawlable = json["crawlable"];
|
||||
|
||||
}
|
||||
ShortURL.fromJson(Map<String, dynamic> json)
|
||||
: shortCode = json["shortCode"],
|
||||
shortUrl = json["shortUrl"],
|
||||
longUrl = json["longUrl"],
|
||||
deviceLongUrls = DeviceLongUrls.fromJson(json["deviceLongUrls"]),
|
||||
dateCreated = DateTime.parse(json["dateCreated"]),
|
||||
visitsSummary = VisitsSummary.fromJson(json["visitsSummary"]),
|
||||
tags = json["tags"],
|
||||
meta = ShortURLMeta.fromJson(json["meta"]),
|
||||
domain = json["domain"],
|
||||
title = json["title"],
|
||||
crawlable = json["crawlable"];
|
||||
}
|
||||
|
|
|
@ -2,16 +2,22 @@
|
|||
class ShortURLMeta {
|
||||
/// The date since when this short URL has been valid
|
||||
DateTime? validSince;
|
||||
|
||||
/// The data when this short URL expires
|
||||
DateTime? validUntil;
|
||||
|
||||
/// Amount of maximum visits allowed to this short URL
|
||||
int? maxVisits;
|
||||
|
||||
ShortURLMeta(this.validSince, this.validUntil, this.maxVisits);
|
||||
|
||||
/// Converts JSON data from the API to an instance of [ShortURLMeta]
|
||||
ShortURLMeta.fromJson(Map<String, dynamic> json):
|
||||
validSince = json["validSince"] != null ? DateTime.parse(json["validSince"]) : null,
|
||||
validUntil = json["validUntil"] != null ? DateTime.parse(json["validUntil"]) : null,
|
||||
maxVisits = json["maxVisits"];
|
||||
}
|
||||
ShortURLMeta.fromJson(Map<String, dynamic> json)
|
||||
: validSince = json["validSince"] != null
|
||||
? DateTime.parse(json["validSince"])
|
||||
: null,
|
||||
validUntil = json["validUntil"] != null
|
||||
? DateTime.parse(json["validUntil"])
|
||||
: null,
|
||||
maxVisits = json["maxVisits"];
|
||||
}
|
||||
|
|
|
@ -2,16 +2,18 @@
|
|||
class VisitsSummary {
|
||||
/// Count of total visits
|
||||
int total;
|
||||
|
||||
/// Count of visits from humans
|
||||
int nonBots;
|
||||
|
||||
/// Count of visits from bots/crawlers
|
||||
int bots;
|
||||
|
||||
VisitsSummary(this.total, this.nonBots, this.bots);
|
||||
|
||||
/// Converts JSON data from the API to an instance of [VisitsSummary]
|
||||
VisitsSummary.fromJson(Map<String, dynamic> json):
|
||||
total = json["total"] as int,
|
||||
nonBots = json["nonBots"] as int,
|
||||
bots = json["bots"] as int;
|
||||
}
|
||||
VisitsSummary.fromJson(Map<String, dynamic> json)
|
||||
: total = json["total"] as int,
|
||||
nonBots = json["nonBots"] as int,
|
||||
bots = json["bots"] as int;
|
||||
}
|
||||
|
|
|
@ -4,32 +4,57 @@ import '../ShortURL/device_long_urls.dart';
|
|||
class ShortURLSubmission {
|
||||
/// Long URL to redirect to
|
||||
String longUrl;
|
||||
|
||||
/// Device-specific long URLs
|
||||
DeviceLongUrls? deviceLongUrls;
|
||||
|
||||
/// Date since when this short URL is valid in ISO8601 format
|
||||
String? validSince;
|
||||
|
||||
/// Date until when this short URL is valid in ISO8601 format
|
||||
String? validUntil;
|
||||
|
||||
/// Amount of maximum visits allowed to this short URLs
|
||||
int? maxVisits;
|
||||
|
||||
/// List of tags assigned to this short URL
|
||||
List<String> tags;
|
||||
|
||||
/// Title of the page
|
||||
String? title;
|
||||
|
||||
/// Whether the short URL is crawlable by web crawlers
|
||||
bool crawlable;
|
||||
|
||||
/// Whether to forward query parameters
|
||||
bool forwardQuery;
|
||||
|
||||
/// Custom slug (if not provided a random one will be generated)
|
||||
String? customSlug;
|
||||
|
||||
/// Whether to use an existing short URL if the slug matches
|
||||
bool findIfExists;
|
||||
|
||||
/// Domain to use
|
||||
String? domain;
|
||||
|
||||
/// Length of the slug if a custom one is not provided
|
||||
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});
|
||||
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});
|
||||
|
||||
/// Converts class data to a JSON object
|
||||
Map<String, dynamic> toJson() {
|
||||
|
@ -49,4 +74,4 @@ class ShortURLSubmission {
|
|||
"shortCodeLength": shortCodeLength
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,25 +5,28 @@ import 'package:http/http.dart' as http;
|
|||
import '../server_manager.dart';
|
||||
|
||||
/// Tries to connect to the Shlink server
|
||||
FutureOr<Either<String, Failure>> apiConnect(String? apiKey, String? serverUrl, String apiVersion) async {
|
||||
FutureOr<Either<String, Failure>> apiConnect(
|
||||
String? apiKey, String? serverUrl, String apiVersion) async {
|
||||
try {
|
||||
final response = await http.get(Uri.parse("$serverUrl/rest/v$apiVersion/short-urls"), headers: {
|
||||
final response = await http
|
||||
.get(Uri.parse("$serverUrl/rest/v$apiVersion/short-urls"), headers: {
|
||||
"X-Api-Key": apiKey ?? "",
|
||||
});
|
||||
if (response.statusCode == 200) {
|
||||
return left("");
|
||||
}
|
||||
else {
|
||||
} 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(ApiFailure(
|
||||
type: jsonBody["type"],
|
||||
detail: jsonBody["detail"],
|
||||
title: jsonBody["title"],
|
||||
status: jsonBody["status"]));
|
||||
} catch (resErr) {
|
||||
return right(RequestFailure(response.statusCode, resErr.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(reqErr) {
|
||||
} catch (reqErr) {
|
||||
return right(RequestFailure(0, reqErr.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,26 +5,30 @@ import 'package:http/http.dart' as http;
|
|||
import '../server_manager.dart';
|
||||
|
||||
/// Deletes a short URL from the server
|
||||
FutureOr<Either<String, Failure>> apiDeleteShortUrl(String shortCode, String? apiKey, String? serverUrl, String apiVersion) async {
|
||||
FutureOr<Either<String, Failure>> apiDeleteShortUrl(String shortCode,
|
||||
String? apiKey, String? serverUrl, String apiVersion) async {
|
||||
try {
|
||||
final response = await http.delete(Uri.parse("$serverUrl/rest/v$apiVersion/short-urls/$shortCode"), headers: {
|
||||
"X-Api-Key": apiKey ?? "",
|
||||
});
|
||||
final response = await http.delete(
|
||||
Uri.parse("$serverUrl/rest/v$apiVersion/short-urls/$shortCode"),
|
||||
headers: {
|
||||
"X-Api-Key": apiKey ?? "",
|
||||
});
|
||||
if (response.statusCode == 204) {
|
||||
// get returned short url
|
||||
return left("");
|
||||
}
|
||||
else {
|
||||
} 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(ApiFailure(
|
||||
type: jsonBody["type"],
|
||||
detail: jsonBody["detail"],
|
||||
title: jsonBody["title"],
|
||||
status: jsonBody["status"]));
|
||||
} catch (resErr) {
|
||||
return right(RequestFailure(response.statusCode, resErr.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(reqErr) {
|
||||
} catch (reqErr) {
|
||||
return right(RequestFailure(0, reqErr.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,29 +6,35 @@ import 'package:shlink_app/API/Classes/ShortURL/short_url.dart';
|
|||
import '../server_manager.dart';
|
||||
|
||||
/// Gets recently created short URLs from the server
|
||||
FutureOr<Either<List<ShortURL>, Failure>> apiGetRecentShortUrls(String? apiKey, String? serverUrl, String apiVersion) async {
|
||||
FutureOr<Either<List<ShortURL>, Failure>> apiGetRecentShortUrls(
|
||||
String? apiKey, String? serverUrl, String apiVersion) async {
|
||||
try {
|
||||
final response = await http.get(Uri.parse("$serverUrl/rest/v$apiVersion/short-urls?itemsPerPage=5&orderBy=dateCreated-DESC"), headers: {
|
||||
"X-Api-Key": apiKey ?? "",
|
||||
});
|
||||
final response = await http.get(
|
||||
Uri.parse(
|
||||
"$serverUrl/rest/v$apiVersion/short-urls?itemsPerPage=5&orderBy=dateCreated-DESC"),
|
||||
headers: {
|
||||
"X-Api-Key": apiKey ?? "",
|
||||
});
|
||||
if (response.statusCode == 200) {
|
||||
var jsonResponse = jsonDecode(response.body);
|
||||
List<ShortURL> shortURLs = (jsonResponse["shortUrls"]["data"] as List<dynamic>).map((e) {
|
||||
List<ShortURL> shortURLs =
|
||||
(jsonResponse["shortUrls"]["data"] as List<dynamic>).map((e) {
|
||||
return ShortURL.fromJson(e);
|
||||
}).toList();
|
||||
return left(shortURLs);
|
||||
}
|
||||
else {
|
||||
} 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(ApiFailure(
|
||||
type: jsonBody["type"],
|
||||
detail: jsonBody["detail"],
|
||||
title: jsonBody["title"],
|
||||
status: jsonBody["status"]));
|
||||
} catch (resErr) {
|
||||
return right(RequestFailure(response.statusCode, resErr.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(reqErr) {
|
||||
} catch (reqErr) {
|
||||
return right(RequestFailure(0, reqErr.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,26 +5,30 @@ import 'package:http/http.dart' as http;
|
|||
import '../server_manager.dart';
|
||||
|
||||
/// Gets the status of the server and health information
|
||||
FutureOr<Either<ServerHealthResponse, Failure>> apiGetServerHealth(String? apiKey, String? serverUrl, String apiVersion) async {
|
||||
FutureOr<Either<ServerHealthResponse, Failure>> apiGetServerHealth(
|
||||
String? apiKey, String? serverUrl, String apiVersion) async {
|
||||
try {
|
||||
final response = await http.get(Uri.parse("$serverUrl/rest/v$apiVersion/health"), headers: {
|
||||
final response = await http
|
||||
.get(Uri.parse("$serverUrl/rest/v$apiVersion/health"), headers: {
|
||||
"X-Api-Key": apiKey ?? "",
|
||||
});
|
||||
if (response.statusCode == 200) {
|
||||
var jsonData = jsonDecode(response.body);
|
||||
return left(ServerHealthResponse(status: jsonData["status"], version: jsonData["version"]));
|
||||
}
|
||||
else {
|
||||
return left(ServerHealthResponse(
|
||||
status: jsonData["status"], version: jsonData["version"]));
|
||||
} 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(ApiFailure(
|
||||
type: jsonBody["type"],
|
||||
detail: jsonBody["detail"],
|
||||
title: jsonBody["title"],
|
||||
status: jsonBody["status"]));
|
||||
} catch (resErr) {
|
||||
return right(RequestFailure(response.statusCode, resErr.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(reqErr) {
|
||||
} catch (reqErr) {
|
||||
return right(RequestFailure(0, reqErr.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,14 +7,14 @@ import '../Classes/ShlinkStats/shlink_stats.dart';
|
|||
import '../server_manager.dart';
|
||||
|
||||
/// Gets statistics about the Shlink server
|
||||
FutureOr<Either<ShlinkStats, Failure>> apiGetShlinkStats(String? apiKey, String? serverUrl, String apiVersion) async {
|
||||
|
||||
FutureOr<Either<ShlinkStats, Failure>> apiGetShlinkStats(
|
||||
String? apiKey, String? serverUrl, String apiVersion) async {
|
||||
var nonOrphanVisits;
|
||||
var orphanVisits;
|
||||
var shortUrlsCount;
|
||||
var tagsCount;
|
||||
var failure;
|
||||
|
||||
|
||||
var visitStatsResponse = await _getVisitStats(apiKey, serverUrl, apiVersion);
|
||||
visitStatsResponse.fold((l) {
|
||||
nonOrphanVisits = l.nonOrphanVisits;
|
||||
|
@ -24,7 +24,8 @@ FutureOr<Either<ShlinkStats, Failure>> apiGetShlinkStats(String? apiKey, String?
|
|||
return right(r);
|
||||
});
|
||||
|
||||
var shortUrlsCountResponse = await _getShortUrlsCount(apiKey, serverUrl, apiVersion);
|
||||
var shortUrlsCountResponse =
|
||||
await _getShortUrlsCount(apiKey, serverUrl, apiVersion);
|
||||
shortUrlsCountResponse.fold((l) {
|
||||
shortUrlsCount = l;
|
||||
}, (r) {
|
||||
|
@ -40,14 +41,15 @@ FutureOr<Either<ShlinkStats, Failure>> apiGetShlinkStats(String? apiKey, String?
|
|||
return right(r);
|
||||
});
|
||||
|
||||
while(failure == null && (nonOrphanVisits == null || orphanVisits == null || shortUrlsCount == null || tagsCount == null)) {
|
||||
while (failure == null && (orphanVisits == null)) {
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
}
|
||||
|
||||
if (failure != null) {
|
||||
return right(failure);
|
||||
}
|
||||
return left(ShlinkStats(nonOrphanVisits, orphanVisits, shortUrlsCount, tagsCount));
|
||||
return left(
|
||||
ShlinkStats(nonOrphanVisits, orphanVisits, shortUrlsCount, tagsCount));
|
||||
}
|
||||
|
||||
class _ShlinkVisitStats {
|
||||
|
@ -58,79 +60,89 @@ class _ShlinkVisitStats {
|
|||
}
|
||||
|
||||
/// Gets visitor statistics about the entire server
|
||||
FutureOr<Either<_ShlinkVisitStats, Failure>> _getVisitStats(String? apiKey, String? serverUrl, String apiVersion) async {
|
||||
FutureOr<Either<_ShlinkVisitStats, Failure>> _getVisitStats(
|
||||
String? apiKey, String? serverUrl, String apiVersion) async {
|
||||
try {
|
||||
final response = await http.get(Uri.parse("$serverUrl/rest/v$apiVersion/visits"), headers: {
|
||||
final response = await http
|
||||
.get(Uri.parse("$serverUrl/rest/v$apiVersion/visits"), headers: {
|
||||
"X-Api-Key": apiKey ?? "",
|
||||
});
|
||||
if (response.statusCode == 200) {
|
||||
var jsonResponse = jsonDecode(response.body);
|
||||
var nonOrphanVisits = VisitsSummary.fromJson(jsonResponse["visits"]["nonOrphanVisits"]);
|
||||
var orphanVisits = VisitsSummary.fromJson(jsonResponse["visits"]["orphanVisits"]);
|
||||
var nonOrphanVisits =
|
||||
VisitsSummary.fromJson(jsonResponse["visits"]["nonOrphanVisits"]);
|
||||
var orphanVisits =
|
||||
VisitsSummary.fromJson(jsonResponse["visits"]["orphanVisits"]);
|
||||
return left(_ShlinkVisitStats(nonOrphanVisits, orphanVisits));
|
||||
|
||||
}
|
||||
else {
|
||||
} 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(ApiFailure(
|
||||
type: jsonBody["type"],
|
||||
detail: jsonBody["detail"],
|
||||
title: jsonBody["title"],
|
||||
status: jsonBody["status"]));
|
||||
} catch (resErr) {
|
||||
return right(RequestFailure(response.statusCode, resErr.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(reqErr) {
|
||||
} catch (reqErr) {
|
||||
return right(RequestFailure(0, reqErr.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets amount of short URLs
|
||||
FutureOr<Either<int, Failure>> _getShortUrlsCount(String? apiKey, String? serverUrl, String apiVersion) async {
|
||||
FutureOr<Either<int, Failure>> _getShortUrlsCount(
|
||||
String? apiKey, String? serverUrl, String apiVersion) async {
|
||||
try {
|
||||
final response = await http.get(Uri.parse("$serverUrl/rest/v$apiVersion/short-urls"), headers: {
|
||||
final response = await http
|
||||
.get(Uri.parse("$serverUrl/rest/v$apiVersion/short-urls"), headers: {
|
||||
"X-Api-Key": apiKey ?? "",
|
||||
});
|
||||
if (response.statusCode == 200) {
|
||||
var jsonResponse = jsonDecode(response.body);
|
||||
return left(jsonResponse["shortUrls"]["pagination"]["totalItems"]);
|
||||
}
|
||||
else {
|
||||
} 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(ApiFailure(
|
||||
type: jsonBody["type"],
|
||||
detail: jsonBody["detail"],
|
||||
title: jsonBody["title"],
|
||||
status: jsonBody["status"]));
|
||||
} catch (resErr) {
|
||||
return right(RequestFailure(response.statusCode, resErr.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(reqErr) {
|
||||
} catch (reqErr) {
|
||||
return right(RequestFailure(0, reqErr.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets amount of tags
|
||||
FutureOr<Either<int, Failure>> _getTagsCount(String? apiKey, String? serverUrl, String apiVersion) async {
|
||||
FutureOr<Either<int, Failure>> _getTagsCount(
|
||||
String? apiKey, String? serverUrl, String apiVersion) async {
|
||||
try {
|
||||
final response = await http.get(Uri.parse("$serverUrl/rest/v$apiVersion/tags"), headers: {
|
||||
final response = await http
|
||||
.get(Uri.parse("$serverUrl/rest/v$apiVersion/tags"), headers: {
|
||||
"X-Api-Key": apiKey ?? "",
|
||||
});
|
||||
if (response.statusCode == 200) {
|
||||
var jsonResponse = jsonDecode(response.body);
|
||||
return left(jsonResponse["tags"]["pagination"]["totalItems"]);
|
||||
}
|
||||
else {
|
||||
} 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(ApiFailure(
|
||||
type: jsonBody["type"],
|
||||
detail: jsonBody["detail"],
|
||||
title: jsonBody["title"],
|
||||
status: jsonBody["status"]));
|
||||
} catch (resErr) {
|
||||
return right(RequestFailure(response.statusCode, resErr.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(reqErr) {
|
||||
} catch (reqErr) {
|
||||
return right(RequestFailure(0, reqErr.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,8 @@ import 'package:shlink_app/API/Classes/ShortURL/short_url.dart';
|
|||
import '../server_manager.dart';
|
||||
|
||||
/// Gets all short URLs
|
||||
FutureOr<Either<List<ShortURL>, Failure>> apiGetShortUrls(String? apiKey, String? serverUrl, String apiVersion) async {
|
||||
FutureOr<Either<List<ShortURL>, Failure>> apiGetShortUrls(
|
||||
String? apiKey, String? serverUrl, String apiVersion) async {
|
||||
var currentPage = 1;
|
||||
var maxPages = 2;
|
||||
List<ShortURL> allUrls = [];
|
||||
|
@ -14,7 +15,8 @@ FutureOr<Either<List<ShortURL>, Failure>> apiGetShortUrls(String? apiKey, String
|
|||
Failure? error;
|
||||
|
||||
while (currentPage <= maxPages) {
|
||||
final response = await _getShortUrlPage(currentPage, apiKey, serverUrl, apiVersion);
|
||||
final response =
|
||||
await _getShortUrlPage(currentPage, apiKey, serverUrl, apiVersion);
|
||||
response.fold((l) {
|
||||
allUrls.addAll(l.urls);
|
||||
maxPages = l.totalPages;
|
||||
|
@ -26,37 +28,42 @@ FutureOr<Either<List<ShortURL>, Failure>> apiGetShortUrls(String? apiKey, String
|
|||
}
|
||||
if (error == null) {
|
||||
return left(allUrls);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return right(error!);
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets all short URLs from a specific page
|
||||
FutureOr<Either<ShortURLPageResponse, Failure>> _getShortUrlPage(int page, String? apiKey, String? serverUrl, String apiVersion) async {
|
||||
FutureOr<Either<ShortURLPageResponse, Failure>> _getShortUrlPage(
|
||||
int page, String? apiKey, String? serverUrl, String apiVersion) async {
|
||||
try {
|
||||
final response = await http.get(Uri.parse("$serverUrl/rest/v$apiVersion/short-urls?page=$page"), headers: {
|
||||
"X-Api-Key": apiKey ?? "",
|
||||
});
|
||||
final response = await http.get(
|
||||
Uri.parse("$serverUrl/rest/v$apiVersion/short-urls?page=$page"),
|
||||
headers: {
|
||||
"X-Api-Key": apiKey ?? "",
|
||||
});
|
||||
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) {
|
||||
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 {
|
||||
} 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(ApiFailure(
|
||||
type: jsonBody["type"],
|
||||
detail: jsonBody["detail"],
|
||||
title: jsonBody["title"],
|
||||
status: jsonBody["status"]));
|
||||
} catch (resErr) {
|
||||
return right(RequestFailure(response.statusCode, resErr.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(reqErr) {
|
||||
} catch (reqErr) {
|
||||
return right(RequestFailure(0, reqErr.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,27 +6,33 @@ import 'package:shlink_app/API/Classes/ShortURLSubmission/short_url_submission.d
|
|||
import '../server_manager.dart';
|
||||
|
||||
/// Submits a short URL to a server for it to be added
|
||||
FutureOr<Either<String, Failure>> apiSubmitShortUrl(ShortURLSubmission shortUrl, String? apiKey, String? serverUrl, String apiVersion) async {
|
||||
FutureOr<Either<String, Failure>> apiSubmitShortUrl(ShortURLSubmission shortUrl,
|
||||
String? apiKey, String? serverUrl, String apiVersion) async {
|
||||
try {
|
||||
final response = await http.post(Uri.parse("$serverUrl/rest/v$apiVersion/short-urls"), headers: {
|
||||
"X-Api-Key": apiKey ?? "",
|
||||
}, body: jsonEncode(shortUrl.toJson()));
|
||||
final response =
|
||||
await http.post(Uri.parse("$serverUrl/rest/v$apiVersion/short-urls"),
|
||||
headers: {
|
||||
"X-Api-Key": apiKey ?? "",
|
||||
},
|
||||
body: jsonEncode(shortUrl.toJson()));
|
||||
if (response.statusCode == 200) {
|
||||
// get returned short url
|
||||
var jsonBody = jsonDecode(response.body);
|
||||
return left(jsonBody["shortUrl"]);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
try {
|
||||
var jsonBody = jsonDecode(response.body);
|
||||
return right(ApiFailure(type: jsonBody["type"], detail: jsonBody["detail"], title: jsonBody["title"], status: jsonBody["status"], invalidElements: jsonBody["invalidElements"]));
|
||||
}
|
||||
catch(resErr) {
|
||||
return right(ApiFailure(
|
||||
type: jsonBody["type"],
|
||||
detail: jsonBody["detail"],
|
||||
title: jsonBody["title"],
|
||||
status: jsonBody["status"],
|
||||
invalidElements: jsonBody["invalidElements"]));
|
||||
} catch (resErr) {
|
||||
return right(RequestFailure(response.statusCode, resErr.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(reqErr) {
|
||||
} catch (reqErr) {
|
||||
return right(RequestFailure(0, reqErr.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,6 @@ import 'Methods/delete_short_url.dart';
|
|||
import 'Methods/submit_short_url.dart';
|
||||
|
||||
class ServerManager {
|
||||
|
||||
/// The URL of the Shlink server
|
||||
String? serverUrl;
|
||||
|
||||
|
@ -69,9 +68,9 @@ class ServerManager {
|
|||
storage.write(key: "shlink_apikey", value: apiKey);
|
||||
}
|
||||
|
||||
|
||||
/// Saves provided server credentials and tries to establish a connection
|
||||
FutureOr<Either<String, Failure>> initAndConnect(String url, String apiKey) async {
|
||||
FutureOr<Either<String, Failure>> initAndConnect(
|
||||
String url, String apiKey) async {
|
||||
// TODO: convert url to correct format
|
||||
serverUrl = url;
|
||||
this.apiKey = apiKey;
|
||||
|
@ -100,7 +99,8 @@ class ServerManager {
|
|||
}
|
||||
|
||||
/// Saves a new short URL to the server
|
||||
FutureOr<Either<String, Failure>> submitShortUrl(ShortURLSubmission shortUrl) async {
|
||||
FutureOr<Either<String, Failure>> submitShortUrl(
|
||||
ShortURLSubmission shortUrl) async {
|
||||
return apiSubmitShortUrl(shortUrl, apiKey, serverUrl, apiVersion);
|
||||
}
|
||||
|
||||
|
@ -139,7 +139,8 @@ class ServerHealthResponse {
|
|||
/// Failure class, used for the API
|
||||
abstract class Failure {}
|
||||
|
||||
/// Used when a request to a server fails (due to networking issues or an unexpected response)
|
||||
/// Used when a request to a server fails
|
||||
/// (due to networking issues or an unexpected response)
|
||||
class RequestFailure extends Failure {
|
||||
int statusCode;
|
||||
String description;
|
||||
|
@ -155,5 +156,10 @@ class ApiFailure extends Failure {
|
|||
int status;
|
||||
List<dynamic>? invalidElements;
|
||||
|
||||
ApiFailure({required this.type, required this.detail, required this.title, required this.status, this.invalidElements});
|
||||
}
|
||||
ApiFailure(
|
||||
{required this.type,
|
||||
required this.detail,
|
||||
required this.title,
|
||||
required this.status,
|
||||
this.invalidElements});
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
library dev.abmgrt.shlink_app.globals;
|
||||
|
||||
import 'package:shlink_app/API/server_manager.dart';
|
||||
|
||||
ServerManager serverManager = ServerManager();
|
||||
ServerManager serverManager = ServerManager();
|
||||
|
|
|
@ -11,11 +11,11 @@ void main() {
|
|||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
static const _defaultLightColorScheme =
|
||||
ColorScheme.light();//.fromSwatch(primarySwatch: Colors.blue, backgroundColor: Colors.white);
|
||||
static const _defaultLightColorScheme = ColorScheme
|
||||
.light(); //.fromSwatch(primarySwatch: Colors.blue, backgroundColor: Colors.white);
|
||||
|
||||
static final _defaultDarkColorScheme = ColorScheme.fromSwatch(
|
||||
primarySwatch: Colors.blue, brightness: Brightness.dark);
|
||||
static final _defaultDarkColorScheme =
|
||||
ColorScheme.fromSwatch(brightness: Brightness.dark);
|
||||
|
||||
// This widget is the root of your application.
|
||||
@override
|
||||
|
@ -25,24 +25,22 @@ class MyApp extends StatelessWidget {
|
|||
title: 'Shlink',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: ThemeData(
|
||||
appBarTheme: const AppBarTheme(
|
||||
backgroundColor: Color(0xfffafafa),
|
||||
),
|
||||
appBarTheme: const AppBarTheme(
|
||||
backgroundColor: Color(0xfffafafa),
|
||||
),
|
||||
colorScheme: lightColorScheme ?? _defaultLightColorScheme,
|
||||
useMaterial3: true
|
||||
),
|
||||
useMaterial3: true),
|
||||
darkTheme: ThemeData(
|
||||
appBarTheme: const AppBarTheme(
|
||||
backgroundColor: Color(0xff0d0d0d),
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
),
|
||||
colorScheme: darkColorScheme?.copyWith(background: Colors.black) ?? _defaultDarkColorScheme,
|
||||
colorScheme: darkColorScheme?.copyWith(background: Colors.black) ??
|
||||
_defaultDarkColorScheme,
|
||||
useMaterial3: true,
|
||||
),
|
||||
themeMode: ThemeMode.system,
|
||||
home: const InitialPage()
|
||||
);
|
||||
home: const InitialPage());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -55,7 +53,6 @@ class InitialPage extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _InitialPageState extends State<InitialPage> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
@ -63,26 +60,20 @@ class _InitialPageState extends State<InitialPage> {
|
|||
}
|
||||
|
||||
void checkLogin() async {
|
||||
|
||||
bool result = await globals.serverManager.checkLogin();
|
||||
if (result) {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (context) => const NavigationBarView())
|
||||
);
|
||||
}
|
||||
else {
|
||||
MaterialPageRoute(builder: (context) => const NavigationBarView()));
|
||||
} else {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (context) => const LoginView())
|
||||
);
|
||||
MaterialPageRoute(builder: (context) => const LoginView()));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Scaffold(
|
||||
body: Center(
|
||||
child: Text("")
|
||||
),
|
||||
body: Center(child: Text("")),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,7 +28,8 @@ class LicenseUtil {
|
|||
return [
|
||||
const License(
|
||||
name: r'cupertino_icons',
|
||||
license: r'''The MIT License (MIT)
|
||||
license: r'''
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Vladimir Kharlampidi
|
||||
|
||||
|
@ -49,12 +50,13 @@ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''',
|
||||
version: r'^1.0.5',
|
||||
homepage: null,
|
||||
repository: r'https://github.com/flutter/packages/tree/main/third_party/packages/cupertino_icons',
|
||||
repository:
|
||||
r'https://github.com/flutter/packages/tree/main/third_party/packages/cupertino_icons',
|
||||
),
|
||||
const License(
|
||||
name: r'dartz',
|
||||
license: r'''The MIT License (MIT)
|
||||
license: r'''
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016, 2017, 2018, 2019, 2020, 2021 Björn Sperber
|
||||
|
||||
|
@ -78,11 +80,11 @@ SOFTWARE.
|
|||
''',
|
||||
version: r'^0.10.1',
|
||||
homepage: r'https://github.com/spebbe/dartz',
|
||||
repository: null,
|
||||
),
|
||||
const License(
|
||||
name: r'dynamic_color',
|
||||
license: r''' Apache License
|
||||
license: r'''
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
|
@ -285,12 +287,13 @@ SOFTWARE.
|
|||
limitations under the License.
|
||||
''',
|
||||
version: r'^1.6.6',
|
||||
homepage: null,
|
||||
repository: r'https://github.com/material-foundation/flutter-packages/tree/main/packages/dynamic_color',
|
||||
repository:
|
||||
r'https://github.com/material-foundation/flutter-packages/tree/main/packages/dynamic_color',
|
||||
),
|
||||
const License(
|
||||
name: r'flutter',
|
||||
license: r'''Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
license: r'''
|
||||
Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
@ -316,13 +319,13 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
''',
|
||||
version: null,
|
||||
homepage: r'https://flutter.dev/',
|
||||
repository: r'https://github.com/flutter/flutter',
|
||||
),
|
||||
const License(
|
||||
name: r'flutter_launcher_icons',
|
||||
license: r'''MIT License
|
||||
license: r'''
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Mark O'Sullivan
|
||||
|
||||
|
@ -346,11 +349,13 @@ SOFTWARE.
|
|||
''',
|
||||
version: r'0.13.1',
|
||||
homepage: r'https://github.com/fluttercommunity/flutter_launcher_icons',
|
||||
repository: r'https://github.com/fluttercommunity/flutter_launcher_icons/',
|
||||
repository:
|
||||
r'https://github.com/fluttercommunity/flutter_launcher_icons/',
|
||||
),
|
||||
const License(
|
||||
name: r'flutter_lints',
|
||||
license: r'''Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
license: r'''
|
||||
Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
@ -377,12 +382,13 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
''',
|
||||
version: r'^2.0.2',
|
||||
homepage: null,
|
||||
repository: r'https://github.com/flutter/packages/tree/main/packages/flutter_lints',
|
||||
repository:
|
||||
r'https://github.com/flutter/packages/tree/main/packages/flutter_lints',
|
||||
),
|
||||
const License(
|
||||
name: r'flutter_process_text',
|
||||
license: r'''BSD 3-Clause License
|
||||
license: r'''
|
||||
BSD 3-Clause License
|
||||
|
||||
(c) Copyright 2021 divshekhar (Divyanshu Shekhar)
|
||||
|
||||
|
@ -410,12 +416,12 @@ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABI
|
|||
TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
|
||||
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
|
||||
version: r'^1.1.2',
|
||||
homepage: null,
|
||||
repository: r'https://github.com/DevsOnFlutter/flutter_process_text',
|
||||
),
|
||||
const License(
|
||||
name: r'flutter_secure_storage',
|
||||
license: r'''BSD 3-Clause License
|
||||
license: r'''
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright 2017 German Saprykin
|
||||
All rights reserved.
|
||||
|
@ -445,12 +451,13 @@ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
|
||||
version: r'^8.0.0',
|
||||
homepage: null,
|
||||
repository: r'https://github.com/mogol/flutter_secure_storage/tree/develop/flutter_secure_storage',
|
||||
repository:
|
||||
r'https://github.com/mogol/flutter_secure_storage/tree/develop/flutter_secure_storage',
|
||||
),
|
||||
const License(
|
||||
name: r'flutter_test',
|
||||
license: r'''Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
license: r'''
|
||||
Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
@ -476,13 +483,13 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
''',
|
||||
version: null,
|
||||
homepage: r'https://flutter.dev/',
|
||||
repository: r'https://github.com/flutter/flutter',
|
||||
),
|
||||
const License(
|
||||
name: r'http',
|
||||
license: r'''Copyright 2014, the Dart project authors.
|
||||
license: r'''
|
||||
Copyright 2014, the Dart project authors.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
|
@ -511,12 +518,12 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
''',
|
||||
version: r'^0.13.6',
|
||||
homepage: null,
|
||||
repository: r'https://github.com/dart-lang/http/tree/master/pkgs/http',
|
||||
),
|
||||
const License(
|
||||
name: r'intl',
|
||||
license: r'''Copyright 2013, the Dart project authors.
|
||||
license: r'''
|
||||
Copyright 2013, the Dart project authors.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
|
@ -545,12 +552,12 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
''',
|
||||
version: r'^0.18.1',
|
||||
homepage: null,
|
||||
repository: r'https://github.com/dart-lang/i18n/tree/main/pkgs/intl',
|
||||
),
|
||||
const License(
|
||||
name: r'license_generator',
|
||||
license: r'''MIT License
|
||||
license: r'''
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 icapps
|
||||
|
||||
|
@ -574,11 +581,11 @@ SOFTWARE.
|
|||
''',
|
||||
version: r'^1.0.5',
|
||||
homepage: r'https://github.com/icapps/flutter-icapps-license',
|
||||
repository: null,
|
||||
),
|
||||
const License(
|
||||
name: r'package_info_plus',
|
||||
license: r'''Copyright 2017 The Chromium Authors. All rights reserved.
|
||||
license: r'''
|
||||
Copyright 2017 The Chromium Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
|
@ -608,11 +615,13 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
''',
|
||||
version: r'^4.0.2',
|
||||
homepage: r'https://plus.fluttercommunity.dev/',
|
||||
repository: r'https://github.com/fluttercommunity/plus_plugins/tree/main/packages/',
|
||||
repository:
|
||||
r'https://github.com/fluttercommunity/plus_plugins/tree/main/packages/',
|
||||
),
|
||||
const License(
|
||||
name: r'qr_flutter',
|
||||
license: r'''BSD 3-Clause License
|
||||
license: r'''
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2020, Luke Freeman.
|
||||
All rights reserved.
|
||||
|
@ -644,11 +653,11 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
''',
|
||||
version: r'^4.1.0',
|
||||
homepage: r'https://github.com/theyakka/qr.flutter',
|
||||
repository: null,
|
||||
),
|
||||
const License(
|
||||
name: r'shared_preferences',
|
||||
license: r'''Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
license: r'''
|
||||
Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
@ -675,12 +684,13 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
''',
|
||||
version: r'^2.2.2',
|
||||
homepage: null,
|
||||
repository: r'https://github.com/flutter/packages/tree/main/packages/shared_preferences/shared_preferences',
|
||||
repository:
|
||||
r'https://github.com/flutter/packages/tree/main/packages/shared_preferences/shared_preferences',
|
||||
),
|
||||
const License(
|
||||
name: r'tuple',
|
||||
license: r'''Copyright (c) 2014, the tuple project authors.
|
||||
license: r'''
|
||||
Copyright (c) 2014, the tuple project authors.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
|
@ -703,12 +713,12 @@ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
|
||||
version: r'^2.0.2',
|
||||
homepage: null,
|
||||
repository: r'https://github.com/google/tuple.dart',
|
||||
),
|
||||
const License(
|
||||
name: r'url_launcher',
|
||||
license: r'''Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
license: r'''
|
||||
Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
@ -735,8 +745,8 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
''',
|
||||
version: r'6.1.9',
|
||||
homepage: null,
|
||||
repository: r'https://github.com/flutter/plugins/tree/main/packages/url_launcher/url_launcher',
|
||||
repository:
|
||||
r'https://github.com/flutter/plugins/tree/main/packages/url_launcher/url_launcher',
|
||||
),
|
||||
];
|
||||
}
|
||||
|
|
|
@ -8,14 +8,13 @@ import '../API/Classes/ShortURL/short_url.dart';
|
|||
import '../globals.dart' as globals;
|
||||
|
||||
class HomeView extends StatefulWidget {
|
||||
const HomeView({Key? key}) : super(key: key);
|
||||
const HomeView({super.key});
|
||||
|
||||
@override
|
||||
State<HomeView> createState() => _HomeViewState();
|
||||
}
|
||||
|
||||
class _HomeViewState extends State<HomeView> {
|
||||
|
||||
ShlinkStats? shlinkStats;
|
||||
|
||||
List<ShortURL> shortUrls = [];
|
||||
|
@ -27,9 +26,8 @@ class _HomeViewState extends State<HomeView> {
|
|||
void initState() {
|
||||
// TODO: implement initState
|
||||
super.initState();
|
||||
WidgetsBinding.instance
|
||||
.addPostFrameCallback((_) {
|
||||
loadAllData();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
loadAllData();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -49,12 +47,14 @@ class _HomeViewState extends State<HomeView> {
|
|||
var text = "";
|
||||
if (r is RequestFailure) {
|
||||
text = r.description;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
text = (r as ApiFailure).detail;
|
||||
}
|
||||
|
||||
final snackBar = SnackBar(content: Text(text), backgroundColor: Colors.red[400], behavior: SnackBarBehavior.floating);
|
||||
final snackBar = SnackBar(
|
||||
content: Text(text),
|
||||
backgroundColor: Colors.red[400],
|
||||
behavior: SnackBarBehavior.floating);
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
});
|
||||
}
|
||||
|
@ -70,12 +70,14 @@ class _HomeViewState extends State<HomeView> {
|
|||
var text = "";
|
||||
if (r is RequestFailure) {
|
||||
text = r.description;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
text = (r as ApiFailure).detail;
|
||||
}
|
||||
|
||||
final snackBar = SnackBar(content: Text(text), backgroundColor: Colors.red[400], behavior: SnackBarBehavior.floating);
|
||||
final snackBar = SnackBar(
|
||||
content: Text(text),
|
||||
backgroundColor: Colors.red[400],
|
||||
behavior: SnackBarBehavior.floating);
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
});
|
||||
}
|
||||
|
@ -83,134 +85,171 @@ class _HomeViewState extends State<HomeView> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
ColorFiltered(
|
||||
colorFilter: ColorFilter.mode(Colors.black.withOpacity(_qrCodeShown ? 0.4 : 0), BlendMode.srcOver),
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
return loadAllData();
|
||||
},
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
SliverAppBar.medium(
|
||||
expandedHeight: 160,
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text("Shlink", style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (shortUrlsLoaded && shortUrls.isEmpty)
|
||||
body: Stack(
|
||||
children: [
|
||||
ColorFiltered(
|
||||
colorFilter: ColorFilter.mode(
|
||||
Colors.black.withOpacity(_qrCodeShown ? 0.4 : 0),
|
||||
BlendMode.srcOver),
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
return loadAllData();
|
||||
},
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
SliverAppBar.medium(
|
||||
expandedHeight: 160,
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text("Shlink",
|
||||
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
Text(globals.serverManager.getServerUrl(),
|
||||
style: TextStyle(
|
||||
fontSize: 16, color: Colors.grey[600]))
|
||||
],
|
||||
)),
|
||||
SliverToBoxAdapter(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 50),
|
||||
child: Column(
|
||||
children: [
|
||||
const Text("No Short URLs", style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Text('Create one by tapping the "+" button below', style: TextStyle(fontSize: 16, color: Colors.grey[600]),),
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
else
|
||||
SliverList(delegate: SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) {
|
||||
if (index == 0) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.only(top: 16, left: 12, right: 12),
|
||||
child: Text("Recent Short URLs", style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
|
||||
);
|
||||
}
|
||||
else {
|
||||
final shortURL = shortUrls[index - 1];
|
||||
return ShortURLCell(shortURL: shortURL, reload: () {
|
||||
loadRecentShortUrls();
|
||||
}, showQRCode: (String url) {
|
||||
setState(() {
|
||||
_qrUrl = url;
|
||||
_qrCodeShown = true;
|
||||
});
|
||||
}, isLast: index == shortUrls.length);
|
||||
}
|
||||
},
|
||||
childCount: shortUrls.length + 1
|
||||
))
|
||||
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
)
|
||||
)
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (shortUrlsLoaded && shortUrls.isEmpty)
|
||||
SliverToBoxAdapter(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 50),
|
||||
child: Column(
|
||||
children: [
|
||||
const Text(
|
||||
"No Short URLs",
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Text(
|
||||
'Create one by tapping the "+" button below',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey[600]),
|
||||
),
|
||||
)
|
||||
],
|
||||
))))
|
||||
else
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) {
|
||||
if (index == 0) {
|
||||
return const Padding(
|
||||
padding:
|
||||
EdgeInsets.only(top: 16, left: 12, right: 12),
|
||||
child: Text("Recent Short URLs",
|
||||
style: TextStyle(
|
||||
fontSize: 20, fontWeight: FontWeight.bold)),
|
||||
);
|
||||
} else {
|
||||
final shortURL = shortUrls[index - 1];
|
||||
return ShortURLCell(
|
||||
shortURL: shortURL,
|
||||
reload: () {
|
||||
loadRecentShortUrls();
|
||||
},
|
||||
showQRCode: (String url) {
|
||||
setState(() {
|
||||
_qrUrl = url;
|
||||
_qrCodeShown = true;
|
||||
});
|
||||
},
|
||||
isLast: index == shortUrls.length);
|
||||
}
|
||||
}, childCount: shortUrls.length + 1))
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () async {
|
||||
await Navigator.of(context).push(MaterialPageRoute(builder: (context) => const ShortURLEditView()));
|
||||
loadRecentShortUrls();
|
||||
},
|
||||
child: const Icon(Icons.add),
|
||||
)
|
||||
);
|
||||
),
|
||||
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,
|
||||
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,
|
||||
),
|
||||
))),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () async {
|
||||
await Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => const ShortURLEditView()));
|
||||
loadRecentShortUrls();
|
||||
},
|
||||
child: const Icon(Icons.add),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// stats card widget
|
||||
class _ShlinkStatsCardWidget extends StatefulWidget {
|
||||
const _ShlinkStatsCardWidget({required this.text, required this.icon, this.borderColor});
|
||||
const _ShlinkStatsCardWidget(
|
||||
{required this.text, required this.icon, this.borderColor});
|
||||
|
||||
final IconData icon;
|
||||
final Color? borderColor;
|
||||
|
@ -230,20 +269,19 @@ class _ShlinkStatsCardWidgetState extends State<_ShlinkStatsCardWidget> {
|
|||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: widget.borderColor ?? randomColor),
|
||||
borderRadius: BorderRadius.circular(8)
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8)),
|
||||
child: SizedBox(
|
||||
child: Wrap(
|
||||
children: [
|
||||
Icon(widget.icon),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4),
|
||||
child: Text(widget.text, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
child: Text(widget.text,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
),
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import 'package:shlink_app/main.dart';
|
|||
import '../globals.dart' as globals;
|
||||
|
||||
class LoginView extends StatefulWidget {
|
||||
const LoginView({Key? key}) : super(key: key);
|
||||
const LoginView({super.key});
|
||||
|
||||
@override
|
||||
State<LoginView> createState() => _LoginViewState();
|
||||
|
@ -30,11 +30,11 @@ class _LoginViewState extends State<LoginView> {
|
|||
_isLoggingIn = true;
|
||||
_errorMessage = "";
|
||||
});
|
||||
final connectResult = await globals.serverManager.initAndConnect(_serverUrlController.text, _apiKeyController.text);
|
||||
final connectResult = await globals.serverManager
|
||||
.initAndConnect(_serverUrlController.text, _apiKeyController.text);
|
||||
connectResult.fold((l) {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (context) => const InitialPage())
|
||||
);
|
||||
MaterialPageRoute(builder: (context) => const InitialPage()));
|
||||
setState(() {
|
||||
_isLoggingIn = false;
|
||||
});
|
||||
|
@ -44,8 +44,7 @@ class _LoginViewState extends State<LoginView> {
|
|||
_errorMessage = r.detail;
|
||||
_isLoggingIn = false;
|
||||
});
|
||||
}
|
||||
else if (r is RequestFailure) {
|
||||
} else if (r is RequestFailure) {
|
||||
setState(() {
|
||||
_errorMessage = r.description;
|
||||
_isLoggingIn = false;
|
||||
|
@ -54,55 +53,58 @@ class _LoginViewState extends State<LoginView> {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
extendBody: true,
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
const SliverAppBar.medium(
|
||||
title: Text("Add server", style: TextStyle(fontWeight: FontWeight.bold))
|
||||
),
|
||||
SliverFillRemaining(
|
||||
child: Padding(
|
||||
extendBody: true,
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
const SliverAppBar.medium(
|
||||
title: Text("Add server",
|
||||
style: TextStyle(fontWeight: FontWeight.bold))),
|
||||
SliverFillRemaining(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Padding(padding: EdgeInsets.only(bottom: 8),
|
||||
child: Text("Server URL", style: TextStyle(fontWeight: FontWeight.bold),)),
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(bottom: 8),
|
||||
child: Text(
|
||||
"Server URL",
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
)),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.dns_outlined),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: TextField(
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _serverUrlController,
|
||||
keyboardType: TextInputType.url,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
labelText: "https://shlink.example.com"
|
||||
),
|
||||
labelText: "https://shlink.example.com"),
|
||||
))
|
||||
],
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: 8, bottom: 8),
|
||||
child: Text("API Key", style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
child: Text("API Key",
|
||||
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.key),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: TextField(
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _apiKeyController,
|
||||
keyboardType: TextInputType.text,
|
||||
obscureText: true,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
labelText: "..."
|
||||
),
|
||||
border: OutlineInputBorder(), labelText: "..."),
|
||||
))
|
||||
],
|
||||
),
|
||||
|
@ -112,15 +114,16 @@ class _LoginViewState extends State<LoginView> {
|
|||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
FilledButton.tonal(
|
||||
onPressed: () => {
|
||||
_connect()
|
||||
},
|
||||
child: _isLoggingIn ? Container(
|
||||
width: 34,
|
||||
height: 34,
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: const CircularProgressIndicator(),
|
||||
) : const Text("Connect", style: TextStyle(fontSize: 20)),
|
||||
onPressed: () => {_connect()},
|
||||
child: _isLoggingIn
|
||||
? Container(
|
||||
width: 34,
|
||||
height: 34,
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: const CircularProgressIndicator(),
|
||||
)
|
||||
: const Text("Connect",
|
||||
style: TextStyle(fontSize: 20)),
|
||||
)
|
||||
],
|
||||
),
|
||||
|
@ -130,17 +133,17 @@ class _LoginViewState extends State<LoginView> {
|
|||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Flexible(child: Text(_errorMessage, style: const TextStyle(color: Colors.red), textAlign: TextAlign.center))
|
||||
Flexible(
|
||||
child: Text(_errorMessage,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
textAlign: TextAlign.center))
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
)
|
||||
],
|
||||
)
|
||||
);
|
||||
))
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,15 +4,18 @@ import 'package:shlink_app/views/home_view.dart';
|
|||
import 'package:shlink_app/views/url_list_view.dart';
|
||||
|
||||
class NavigationBarView extends StatefulWidget {
|
||||
const NavigationBarView({Key? key}) : super(key: key);
|
||||
const NavigationBarView({super.key});
|
||||
|
||||
@override
|
||||
State<NavigationBarView> createState() => _NavigationBarViewState();
|
||||
}
|
||||
|
||||
class _NavigationBarViewState extends State<NavigationBarView> {
|
||||
|
||||
final List<Widget> views = [const HomeView(), const URLListView(), const SettingsView()];
|
||||
final List<Widget> views = [
|
||||
const HomeView(),
|
||||
const URLListView(),
|
||||
const SettingsView()
|
||||
];
|
||||
int _selectedView = 0;
|
||||
|
||||
@override
|
||||
|
|
|
@ -16,55 +16,68 @@ class _OpenSourceLicensesViewState extends State<OpenSourceLicensesView> {
|
|||
body: CustomScrollView(
|
||||
slivers: [
|
||||
const SliverAppBar.medium(
|
||||
expandedHeight: 120,
|
||||
title: Text("Open Source Licenses", style: TextStyle(fontWeight: FontWeight.bold),)
|
||||
),
|
||||
expandedHeight: 120,
|
||||
title: Text(
|
||||
"Open Source Licenses",
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
)),
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) {
|
||||
final currentLicense = LicenseUtil.getLicenses()[index];
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
if (currentLicense.repository != null) {
|
||||
if (await canLaunchUrl(Uri.parse(currentLicense.repository ?? ""))) {
|
||||
launchUrl(Uri.parse(currentLicense.repository ?? ""), mode: LaunchMode.externalApplication);
|
||||
}
|
||||
}
|
||||
},
|
||||
delegate:
|
||||
SliverChildBuilderDelegate((BuildContext context, int index) {
|
||||
final currentLicense = LicenseUtil.getLicenses()[index];
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
if (currentLicense.repository != null) {
|
||||
if (await canLaunchUrl(
|
||||
Uri.parse(currentLicense.repository ?? ""))) {
|
||||
launchUrl(Uri.parse(currentLicense.repository ?? ""),
|
||||
mode: LaunchMode.externalApplication);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Theme.of(context).brightness == Brightness.light
|
||||
? Colors.grey[100]
|
||||
: Colors.grey[900],
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Theme.of(context).brightness == Brightness.light ? Colors.grey[100] : Colors.grey[900],
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 12, right: 12, top: 20, bottom: 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(currentLicense.name, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
|
||||
Text("Version: ${currentLicense.version ?? "N/A"}", style: const TextStyle(color: Colors.grey)),
|
||||
const SizedBox(height: 8),
|
||||
const Divider(),
|
||||
const SizedBox(height: 8),
|
||||
Text(currentLicense.license, textAlign: TextAlign.justify, style: const TextStyle(color: Colors.grey)),
|
||||
],
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.only(
|
||||
left: 12, right: 12, top: 20, bottom: 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(currentLicense.name,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold, fontSize: 18)),
|
||||
Text("Version: ${currentLicense.version ?? "N/A"}",
|
||||
style: const TextStyle(color: Colors.grey)),
|
||||
const SizedBox(height: 8),
|
||||
const Divider(),
|
||||
const SizedBox(height: 8),
|
||||
Text(currentLicense.license,
|
||||
textAlign: TextAlign.justify,
|
||||
style: const TextStyle(color: Colors.grey)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
childCount: LicenseUtil.getLicenses().length
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}, childCount: LicenseUtil.getLicenses().length),
|
||||
),
|
||||
const SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: 8, bottom: 20),
|
||||
child: Text("Thank you to all maintainers of these repositories 💝", style: TextStyle(color: Colors.grey), textAlign: TextAlign.center,),
|
||||
)
|
||||
)
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: 8, bottom: 20),
|
||||
child: Text(
|
||||
"Thank you to all maintainers of these repositories 💝",
|
||||
style: TextStyle(color: Colors.grey),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
))
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
@ -13,24 +13,19 @@ class SettingsView extends StatefulWidget {
|
|||
State<SettingsView> createState() => _SettingsViewState();
|
||||
}
|
||||
|
||||
enum ServerStatus {
|
||||
connected,
|
||||
connecting,
|
||||
disconnected
|
||||
}
|
||||
enum ServerStatus { connected, connecting, disconnected }
|
||||
|
||||
class _SettingsViewState extends State<SettingsView> {
|
||||
|
||||
var _serverVersion = "---";
|
||||
ServerStatus _serverStatus = ServerStatus.connecting;
|
||||
PackageInfo packageInfo = PackageInfo(appName: "", packageName: "", version: "", buildNumber: "");
|
||||
PackageInfo packageInfo =
|
||||
PackageInfo(appName: "", packageName: "", version: "", buildNumber: "");
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
// TODO: implement initState
|
||||
super.initState();
|
||||
WidgetsBinding.instance
|
||||
.addPostFrameCallback((_) => getServerHealth());
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => getServerHealth());
|
||||
}
|
||||
|
||||
void getServerHealth() async {
|
||||
|
@ -52,12 +47,14 @@ class _SettingsViewState extends State<SettingsView> {
|
|||
var text = "";
|
||||
if (r is RequestFailure) {
|
||||
text = r.description;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
text = (r as ApiFailure).detail;
|
||||
}
|
||||
|
||||
final snackBar = SnackBar(content: Text(text), backgroundColor: Colors.red[400], behavior: SnackBarBehavior.floating);
|
||||
final snackBar = SnackBar(
|
||||
content: Text(text),
|
||||
backgroundColor: Colors.red[400],
|
||||
behavior: SnackBarBehavior.floating);
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
});
|
||||
}
|
||||
|
@ -65,68 +62,89 @@ class _SettingsViewState extends State<SettingsView> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
SliverAppBar.medium(
|
||||
expandedHeight: 120,
|
||||
title: const Text("Settings", style: TextStyle(fontWeight: FontWeight.bold),),
|
||||
actions: [
|
||||
PopupMenuButton(
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
const PopupMenuItem(
|
||||
value: 0,
|
||||
child: Text("Log out...", style: TextStyle(color: Colors.red)),
|
||||
)
|
||||
];
|
||||
},
|
||||
onSelected: (value) {
|
||||
if (value == 0) {
|
||||
globals.serverManager.logOut().then((value) => Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (context) => const LoginView())
|
||||
));
|
||||
}
|
||||
},
|
||||
)
|
||||
],
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
SliverAppBar.medium(
|
||||
expandedHeight: 120,
|
||||
title: const Text(
|
||||
"Settings",
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
actions: [
|
||||
PopupMenuButton(
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
const PopupMenuItem(
|
||||
value: 0,
|
||||
child:
|
||||
Text("Log out...", style: TextStyle(color: Colors.red)),
|
||||
)
|
||||
];
|
||||
},
|
||||
onSelected: (value) {
|
||||
if (value == 0) {
|
||||
globals.serverManager.logOut().then((value) =>
|
||||
Navigator.of(context).pushReplacement(MaterialPageRoute(
|
||||
builder: (context) => const LoginView())));
|
||||
}
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Theme.of(context).brightness == Brightness.light ? Colors.grey[100] : Colors.grey[900],
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Theme.of(context).brightness == Brightness.light
|
||||
? Colors.grey[100]
|
||||
: Colors.grey[900],
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.dns_outlined, color: (() {
|
||||
switch (_serverStatus) {
|
||||
case ServerStatus.connected:
|
||||
return Colors.green;
|
||||
case ServerStatus.connecting:
|
||||
return Colors.orange;
|
||||
case ServerStatus.disconnected:
|
||||
return Colors.red;
|
||||
}
|
||||
}())),
|
||||
Icon(Icons.dns_outlined,
|
||||
color: (() {
|
||||
switch (_serverStatus) {
|
||||
case ServerStatus.connected:
|
||||
return Colors.green;
|
||||
case ServerStatus.connecting:
|
||||
return Colors.orange;
|
||||
case ServerStatus.disconnected:
|
||||
return Colors.red;
|
||||
}
|
||||
}())),
|
||||
const SizedBox(width: 8),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text("Connected to", style: TextStyle(color: Colors.grey)),
|
||||
Text(globals.serverManager.getServerUrl(), style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
|
||||
const Text("Connected to",
|
||||
style: TextStyle(color: Colors.grey)),
|
||||
Text(globals.serverManager.getServerUrl(),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16)),
|
||||
Row(
|
||||
children: [
|
||||
const Text("API Version: ", style: TextStyle(color: Colors.grey, fontWeight: FontWeight.w600)),
|
||||
Text(globals.serverManager.getApiVersion(), style: const TextStyle(color: Colors.grey)),
|
||||
const Text("API Version: ",
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
fontWeight: FontWeight.w600)),
|
||||
Text(globals.serverManager.getApiVersion(),
|
||||
style:
|
||||
const TextStyle(color: Colors.grey)),
|
||||
const SizedBox(width: 16),
|
||||
const Text("Server Version: ", style: TextStyle(color: Colors.grey, fontWeight: FontWeight.w600)),
|
||||
Text(_serverVersion, style: const TextStyle(color: Colors.grey))
|
||||
const Text("Server Version: ",
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
fontWeight: FontWeight.w600)),
|
||||
Text(_serverVersion,
|
||||
style:
|
||||
const TextStyle(color: Colors.grey))
|
||||
],
|
||||
),
|
||||
],
|
||||
|
@ -140,17 +158,20 @@ class _SettingsViewState extends State<SettingsView> {
|
|||
const SizedBox(height: 8),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => const OpenSourceLicensesView())
|
||||
);
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
const OpenSourceLicensesView()));
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Theme.of(context).brightness == Brightness.light ? Colors.grey[100] : Colors.grey[900],
|
||||
color: Theme.of(context).brightness == Brightness.light
|
||||
? Colors.grey[100]
|
||||
: Colors.grey[900],
|
||||
),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.only(left: 12, right: 12, top: 20, bottom: 20),
|
||||
padding: EdgeInsets.only(
|
||||
left: 12, right: 12, top: 20, bottom: 20),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
|
@ -158,50 +179,21 @@ class _SettingsViewState extends State<SettingsView> {
|
|||
children: [
|
||||
Icon(Icons.policy_outlined),
|
||||
SizedBox(width: 8),
|
||||
Text("Open Source Licenses", style: TextStyle(fontWeight: FontWeight.w500)),
|
||||
Text("Open Source Licenses",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500)),
|
||||
],
|
||||
),
|
||||
Icon(Icons.chevron_right)
|
||||
]
|
||||
),
|
||||
]),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
var url = Uri.parse("https://github.com/rainloreley/shlink-mobile-app");
|
||||
if (await canLaunchUrl(url)) {
|
||||
launchUrl(url, mode: LaunchMode.externalApplication);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Theme.of(context).brightness == Brightness.light ? Colors.grey[100] : Colors.grey[900],
|
||||
),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.only(left: 12, right: 12, top: 20, bottom: 20),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.code),
|
||||
SizedBox(width: 8),
|
||||
Text("GitHub", style: TextStyle(fontWeight: FontWeight.w500)),
|
||||
],
|
||||
),
|
||||
Icon(Icons.chevron_right)
|
||||
]
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
var url = Uri.parse("https://abmgrt.dev/shlink-manager/privacy");
|
||||
var url = Uri.parse(
|
||||
"https://github.com/rainloreley/shlink-mobile-app");
|
||||
if (await canLaunchUrl(url)) {
|
||||
launchUrl(url, mode: LaunchMode.externalApplication);
|
||||
}
|
||||
|
@ -209,10 +201,49 @@ class _SettingsViewState extends State<SettingsView> {
|
|||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Theme.of(context).brightness == Brightness.light ? Colors.grey[100] : Colors.grey[900],
|
||||
color: Theme.of(context).brightness == Brightness.light
|
||||
? Colors.grey[100]
|
||||
: Colors.grey[900],
|
||||
),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.only(left: 12, right: 12, top: 20, bottom: 20),
|
||||
padding: EdgeInsets.only(
|
||||
left: 12, right: 12, top: 20, bottom: 20),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.code),
|
||||
SizedBox(width: 8),
|
||||
Text("GitHub",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500)),
|
||||
],
|
||||
),
|
||||
Icon(Icons.chevron_right)
|
||||
]),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
var url = Uri.parse(
|
||||
"https://abmgrt.dev/shlink-manager/privacy");
|
||||
if (await canLaunchUrl(url)) {
|
||||
launchUrl(url, mode: LaunchMode.externalApplication);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Theme.of(context).brightness == Brightness.light
|
||||
? Colors.grey[100]
|
||||
: Colors.grey[900],
|
||||
),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 12, right: 12, top: 20, bottom: 20),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
|
@ -220,12 +251,13 @@ class _SettingsViewState extends State<SettingsView> {
|
|||
children: [
|
||||
Icon(Icons.lock),
|
||||
SizedBox(width: 8),
|
||||
Text("Privacy Policy", style: TextStyle(fontWeight: FontWeight.w500)),
|
||||
Text("Privacy Policy",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500)),
|
||||
],
|
||||
),
|
||||
Icon(Icons.chevron_right)
|
||||
]
|
||||
),
|
||||
]),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -234,14 +266,16 @@ class _SettingsViewState extends State<SettingsView> {
|
|||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Text("${packageInfo.appName}, v${packageInfo.version} (${packageInfo.buildNumber})", style: const TextStyle(color: Colors.grey),),],
|
||||
Text(
|
||||
"${packageInfo.appName}, v${packageInfo.version} (${packageInfo.buildNumber})",
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
);
|
||||
)),
|
||||
)
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,8 +11,8 @@ class ShortURLEditView extends StatefulWidget {
|
|||
State<ShortURLEditView> createState() => _ShortURLEditViewState();
|
||||
}
|
||||
|
||||
class _ShortURLEditViewState extends State<ShortURLEditView> with SingleTickerProviderStateMixin {
|
||||
|
||||
class _ShortURLEditViewState extends State<ShortURLEditView>
|
||||
with SingleTickerProviderStateMixin {
|
||||
final longUrlController = TextEditingController();
|
||||
final customSlugController = TextEditingController();
|
||||
final titleController = TextEditingController();
|
||||
|
@ -51,13 +51,17 @@ class _ShortURLEditViewState extends State<ShortURLEditView> with SingleTickerPr
|
|||
void _submitShortUrl() async {
|
||||
var newSubmission = ShortURLSubmission(
|
||||
longUrl: longUrlController.text,
|
||||
deviceLongUrls: null, tags: [],
|
||||
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);
|
||||
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 {
|
||||
|
@ -67,11 +71,16 @@ class _ShortURLEditViewState extends State<ShortURLEditView> with SingleTickerPr
|
|||
|
||||
if (copyToClipboard) {
|
||||
await Clipboard.setData(ClipboardData(text: l));
|
||||
final snackBar = SnackBar(content: const Text("Copied to clipboard!"), backgroundColor: Colors.green[400], behavior: SnackBarBehavior.floating);
|
||||
final snackBar = SnackBar(
|
||||
content: const Text("Copied to clipboard!"),
|
||||
backgroundColor: Colors.green[400],
|
||||
behavior: SnackBarBehavior.floating);
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
}
|
||||
else {
|
||||
final snackBar = SnackBar(content: const Text("Short URL created!"), backgroundColor: Colors.green[400], behavior: SnackBarBehavior.floating);
|
||||
} else {
|
||||
final snackBar = SnackBar(
|
||||
content: const Text("Short URL created!"),
|
||||
backgroundColor: Colors.green[400],
|
||||
behavior: SnackBarBehavior.floating);
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
}
|
||||
Navigator.pop(context);
|
||||
|
@ -86,38 +95,39 @@ class _ShortURLEditViewState extends State<ShortURLEditView> with SingleTickerPr
|
|||
|
||||
if (r is RequestFailure) {
|
||||
text = r.description;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
text = (r as ApiFailure).detail;
|
||||
if ((r).invalidElements != null) {
|
||||
text = "$text: ${(r).invalidElements}";
|
||||
}
|
||||
}
|
||||
|
||||
final snackBar = SnackBar(content: Text(text), backgroundColor: Colors.red[400], 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(
|
||||
slivers: [
|
||||
const SliverAppBar.medium(
|
||||
title: Text("New Short URL", style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
title: Text("New Short URL",
|
||||
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16, top: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
TextField(
|
||||
controller: longUrlController,
|
||||
decoration: InputDecoration(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16, top: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
TextField(
|
||||
controller: longUrlController,
|
||||
decoration: InputDecoration(
|
||||
errorText: longUrlError != "" ? longUrlError : null,
|
||||
border: const OutlineInputBorder(),
|
||||
label: const Row(
|
||||
|
@ -126,87 +136,99 @@ class _ShortURLEditViewState extends State<ShortURLEditView> with SingleTickerPr
|
|||
SizedBox(width: 8),
|
||||
Text("Long URL")
|
||||
],
|
||||
)
|
||||
),
|
||||
),
|
||||
const 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(() {
|
||||
)),
|
||||
),
|
||||
const 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(
|
||||
}
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
label: Row(
|
||||
children: [
|
||||
const Icon(Icons.link),
|
||||
const SizedBox(width: 8),
|
||||
Text("${randomSlug ? "Random" : "Custom"} slug", style: TextStyle(fontStyle: randomSlug ? FontStyle.italic : FontStyle.normal),)
|
||||
Text(
|
||||
"${randomSlug ? "Random" : "Custom"} slug",
|
||||
style: TextStyle(
|
||||
fontStyle: randomSlug
|
||||
? FontStyle.italic
|
||||
: FontStyle.normal),
|
||||
)
|
||||
],
|
||||
)
|
||||
),
|
||||
),
|
||||
)),
|
||||
),
|
||||
const 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 : Colors.grey)
|
||||
),
|
||||
)
|
||||
),
|
||||
const 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 : Colors.grey)),
|
||||
)
|
||||
],
|
||||
),
|
||||
if (randomSlug) const SizedBox(height: 16),
|
||||
if (randomSlug)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text("Random slug length"),
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: TextField(
|
||||
controller: randomSlugLengthController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
errorText:
|
||||
randomSlugLengthError != "" ? "" : null,
|
||||
border: const OutlineInputBorder(),
|
||||
label: const Row(
|
||||
children: [
|
||||
Icon(Icons.tag),
|
||||
SizedBox(width: 8),
|
||||
Text("Length")
|
||||
],
|
||||
)),
|
||||
))
|
||||
],
|
||||
),
|
||||
if (randomSlug)
|
||||
const SizedBox(height: 16),
|
||||
|
||||
if (randomSlug)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text("Random slug length"),
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: TextField(
|
||||
controller: randomSlugLengthController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
errorText: randomSlugLengthError != "" ? "" : null,
|
||||
border: const OutlineInputBorder(),
|
||||
label: const Row(
|
||||
children: [
|
||||
Icon(Icons.tag),
|
||||
SizedBox(width: 8),
|
||||
Text("Length")
|
||||
],
|
||||
)
|
||||
),
|
||||
))
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: titleController,
|
||||
decoration: const InputDecoration(
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: titleController,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
label: Row(
|
||||
children: [
|
||||
|
@ -214,89 +236,91 @@ class _ShortURLEditViewState extends State<ShortURLEditView> with SingleTickerPr
|
|||
SizedBox(width: 8),
|
||||
Text("Title")
|
||||
],
|
||||
)
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text("Crawlable"),
|
||||
Switch(
|
||||
value: isCrawlable,
|
||||
onChanged: (_) {
|
||||
setState(() {
|
||||
isCrawlable = !isCrawlable;
|
||||
});
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text("Forward query params"),
|
||||
Switch(
|
||||
value: forwardQuery,
|
||||
onChanged: (_) {
|
||||
setState(() {
|
||||
forwardQuery = !forwardQuery;
|
||||
});
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text("Copy to clipboard"),
|
||||
Switch(
|
||||
value: copyToClipboard,
|
||||
onChanged: (_) {
|
||||
setState(() {
|
||||
copyToClipboard = !copyToClipboard;
|
||||
});
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
)
|
||||
)),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text("Crawlable"),
|
||||
Switch(
|
||||
value: isCrawlable,
|
||||
onChanged: (_) {
|
||||
setState(() {
|
||||
isCrawlable = !isCrawlable;
|
||||
});
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text("Forward query params"),
|
||||
Switch(
|
||||
value: forwardQuery,
|
||||
onChanged: (_) {
|
||||
setState(() {
|
||||
forwardQuery = !forwardQuery;
|
||||
});
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text("Copy to clipboard"),
|
||||
Switch(
|
||||
value: copyToClipboard,
|
||||
onChanged: (_) {
|
||||
setState(() {
|
||||
copyToClipboard = !copyToClipboard;
|
||||
});
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
))
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () {
|
||||
if (!isSaving) {
|
||||
setState(() {
|
||||
isSaving = true;
|
||||
longUrlError = "";
|
||||
randomSlugLengthError = "";
|
||||
});
|
||||
if (longUrlController.text == "") {
|
||||
onPressed: () {
|
||||
if (!isSaving) {
|
||||
setState(() {
|
||||
longUrlError = "URL cannot be empty";
|
||||
isSaving = false;
|
||||
isSaving = true;
|
||||
longUrlError = "";
|
||||
randomSlugLengthError = "";
|
||||
});
|
||||
return;
|
||||
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();
|
||||
}
|
||||
}
|
||||
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 ? const Padding(padding: EdgeInsets.all(16), child: CircularProgressIndicator(strokeWidth: 3)) : const Icon(Icons.save)
|
||||
),
|
||||
},
|
||||
child: isSaving
|
||||
? const Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: CircularProgressIndicator(strokeWidth: 3))
|
||||
: const Icon(Icons.save)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,6 @@ class URLDetailView extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _URLDetailViewState extends State<URLDetailView> {
|
||||
|
||||
Future showDeletionConfirmation() {
|
||||
return showDialog(
|
||||
context: context,
|
||||
|
@ -27,58 +26,75 @@ class _URLDetailViewState extends State<URLDetailView> {
|
|||
children: [
|
||||
const Text("You're about to delete"),
|
||||
const SizedBox(height: 4),
|
||||
Text(widget.shortURL.title ?? widget.shortURL.shortCode, style: const TextStyle(fontStyle: FontStyle.italic),),
|
||||
Text(
|
||||
widget.shortURL.title ?? widget.shortURL.shortCode,
|
||||
style: const TextStyle(fontStyle: FontStyle.italic),
|
||||
),
|
||||
const 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: () => {Navigator.of(context).pop()},
|
||||
child: const Text("Cancel")),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
var response = await globals.serverManager.deleteShortUrl(widget.shortURL.shortCode);
|
||||
var response = await globals.serverManager
|
||||
.deleteShortUrl(widget.shortURL.shortCode);
|
||||
|
||||
response.fold((l) {
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context, "reload");
|
||||
|
||||
final snackBar = SnackBar(content: const Text("Short URL deleted!"), backgroundColor: Colors.green[400], behavior: SnackBarBehavior.floating);
|
||||
final snackBar = SnackBar(
|
||||
content: const 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 {
|
||||
} else {
|
||||
text = (r as ApiFailure).detail;
|
||||
}
|
||||
|
||||
final snackBar = SnackBar(content: Text(text), backgroundColor: Colors.red[400], behavior: SnackBarBehavior.floating);
|
||||
final snackBar = SnackBar(
|
||||
content: Text(text),
|
||||
backgroundColor: Colors.red[400],
|
||||
behavior: SnackBarBehavior.floating);
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
return false;
|
||||
});
|
||||
},
|
||||
child: const Text("Delete", style: TextStyle(color: Colors.red)),
|
||||
child:
|
||||
const 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: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
title: Text(widget.shortURL.title ?? widget.shortURL.shortCode,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
actions: [
|
||||
IconButton(onPressed: () {
|
||||
showDeletionConfirmation();
|
||||
}, icon: const Icon(Icons.delete, color: Colors.red,))
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
showDeletionConfirmation();
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.delete,
|
||||
color: Colors.red,
|
||||
))
|
||||
],
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
|
@ -86,41 +102,78 @@ class _URLDetailViewState extends State<URLDetailView> {
|
|||
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: const EdgeInsets.only(right: 4, top: 4),
|
||||
child: Container(
|
||||
padding: const 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()
|
||||
),
|
||||
var randomColor = ([...Colors.primaries]..shuffle())
|
||||
.first
|
||||
.harmonizeWith(Theme.of(context).colorScheme.primary);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 4, top: 4),
|
||||
child: Container(
|
||||
padding: const 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: "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),
|
||||
const _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: "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),
|
||||
const _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: "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)
|
||||
|
||||
|
||||
_ListCell(
|
||||
title: "Crawlable",
|
||||
content: widget.shortURL.crawlable,
|
||||
last: true)
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -128,7 +181,11 @@ class _URLDetailViewState extends State<URLDetailView> {
|
|||
}
|
||||
|
||||
class _ListCell extends StatefulWidget {
|
||||
const _ListCell({required this.title, required this.content, this.sub = false, this.last = false});
|
||||
const _ListCell(
|
||||
{required this.title,
|
||||
required this.content,
|
||||
this.sub = false,
|
||||
this.last = false});
|
||||
|
||||
final String title;
|
||||
final dynamic content;
|
||||
|
@ -143,51 +200,66 @@ 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: const 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: const EdgeInsets.only(right: 4),
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
height: 6,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Theme.of(context).brightness == Brightness.dark ? Colors.grey[700] : Colors.grey[300],
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: 16, bottom: widget.last ? 30 : 0),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(top: 16, left: 8, right: 8),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
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: const EdgeInsets.only(right: 4),
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
height: 6,
|
||||
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: const 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
|
||||
const Text("N/A")
|
||||
],
|
||||
),
|
||||
),
|
||||
Text(
|
||||
widget.title,
|
||||
style: const 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
|
||||
const Text("N/A")
|
||||
],
|
||||
),
|
||||
)
|
||||
);
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,26 +9,24 @@ import '../globals.dart' as globals;
|
|||
import 'package:flutter/services.dart';
|
||||
|
||||
class URLListView extends StatefulWidget {
|
||||
const URLListView({Key? key}) : super(key: key);
|
||||
const URLListView({super.key});
|
||||
|
||||
@override
|
||||
State<URLListView> createState() => _URLListViewState();
|
||||
}
|
||||
|
||||
class _URLListViewState extends State<URLListView> {
|
||||
|
||||
List<ShortURL> shortUrls = [];
|
||||
bool _qrCodeShown = false;
|
||||
String _qrUrl = "";
|
||||
|
||||
bool shortUrlsLoaded = false;
|
||||
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
// TODO: implement initState
|
||||
super.initState();
|
||||
WidgetsBinding.instance
|
||||
.addPostFrameCallback((_) => loadAllShortUrls());
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => loadAllShortUrls());
|
||||
}
|
||||
|
||||
Future<void> loadAllShortUrls() async {
|
||||
|
@ -43,120 +41,144 @@ class _URLListViewState extends State<URLListView> {
|
|||
var text = "";
|
||||
if (r is RequestFailure) {
|
||||
text = r.description;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
text = (r as ApiFailure).detail;
|
||||
}
|
||||
|
||||
final snackBar = SnackBar(content: Text(text), backgroundColor: Colors.red[400], 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(
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () async {
|
||||
await Navigator.of(context).push(MaterialPageRoute(builder: (context) => const ShortURLEditView()));
|
||||
loadAllShortUrls();
|
||||
},
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
ColorFiltered(
|
||||
colorFilter: ColorFilter.mode(Colors.black.withOpacity(_qrCodeShown ? 0.4 : 0), BlendMode.srcOver),
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
return loadAllShortUrls();
|
||||
},
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
const SliverAppBar.medium(
|
||||
title: Text("Short URLs", style: TextStyle(fontWeight: FontWeight.bold))
|
||||
),
|
||||
if (shortUrlsLoaded && shortUrls.isEmpty)
|
||||
SliverToBoxAdapter(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 50),
|
||||
child: Column(
|
||||
children: [
|
||||
const Text("No Short URLs", style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Text('Create one by tapping the "+" button below', style: TextStyle(fontSize: 16, color: Colors.grey[600]),),
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
else
|
||||
SliverList(delegate: SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) {
|
||||
final shortURL = shortUrls[index];
|
||||
return ShortURLCell(shortURL: shortURL, reload: () {
|
||||
loadAllShortUrls();
|
||||
}, showQRCode: (String url) {
|
||||
setState(() {
|
||||
_qrUrl = url;
|
||||
_qrCodeShown = true;
|
||||
});
|
||||
}, isLast: index == shortUrls.length - 1);
|
||||
},
|
||||
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,
|
||||
),
|
||||
)
|
||||
)
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () async {
|
||||
await Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => const ShortURLEditView()));
|
||||
loadAllShortUrls();
|
||||
},
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
ColorFiltered(
|
||||
colorFilter: ColorFilter.mode(
|
||||
Colors.black.withOpacity(_qrCodeShown ? 0.4 : 0),
|
||||
BlendMode.srcOver),
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
return loadAllShortUrls();
|
||||
},
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
const SliverAppBar.medium(
|
||||
title: Text("Short URLs",
|
||||
style: TextStyle(fontWeight: FontWeight.bold))),
|
||||
if (shortUrlsLoaded && shortUrls.isEmpty)
|
||||
SliverToBoxAdapter(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 50),
|
||||
child: Column(
|
||||
children: [
|
||||
const Text(
|
||||
"No Short URLs",
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Text(
|
||||
'Create one by tapping the "+" button below',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey[600]),
|
||||
),
|
||||
)
|
||||
],
|
||||
))))
|
||||
else
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) {
|
||||
final shortURL = shortUrls[index];
|
||||
return ShortURLCell(
|
||||
shortURL: shortURL,
|
||||
reload: () {
|
||||
loadAllShortUrls();
|
||||
},
|
||||
showQRCode: (String url) {
|
||||
setState(() {
|
||||
_qrUrl = url;
|
||||
_qrCodeShown = true;
|
||||
});
|
||||
},
|
||||
isLast: index == shortUrls.length - 1);
|
||||
}, 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,
|
||||
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,
|
||||
),
|
||||
))),
|
||||
),
|
||||
)
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
class ShortURLCell extends StatefulWidget {
|
||||
const ShortURLCell({super.key, required this.shortURL, required this.reload, required this.showQRCode, required this.isLast});
|
||||
const ShortURLCell(
|
||||
{super.key,
|
||||
required this.shortURL,
|
||||
required this.reload,
|
||||
required this.showQRCode,
|
||||
required this.isLast});
|
||||
|
||||
final ShortURL shortURL;
|
||||
final Function() reload;
|
||||
|
@ -172,63 +194,93 @@ class _ShortURLCellState extends State<ShortURLCell> {
|
|||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
final result = await Navigator.of(context).push(MaterialPageRoute(builder: (context) => URLDetailView(shortURL: widget.shortURL)));
|
||||
final result = await Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => URLDetailView(shortURL: widget.shortURL)));
|
||||
|
||||
if (result == "reload") {
|
||||
widget.reload();
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(left: 8, right: 8, bottom: widget.isLast ? 90 : 0),
|
||||
padding: EdgeInsets.only(
|
||||
left: 8, right: 8, bottom: widget.isLast ? 90 : 0),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(left: 8, right: 8, bottom: 16, top: 16),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(bottom: BorderSide(color: MediaQuery.of(context).platformBrightness == Brightness.dark ? Colors.grey[800]! : Colors.grey[300]!)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Text(widget.shortURL.title ?? widget.shortURL.shortCode, textScaleFactor: 1.4, style: const TextStyle(fontWeight: FontWeight.bold),),
|
||||
Text(widget.shortURL.longUrl,maxLines: 1, overflow: TextOverflow.ellipsis, textScaleFactor: 0.9, style: TextStyle(color: Colors.grey[600]),),
|
||||
// List tags in a row
|
||||
Wrap(
|
||||
children: widget.shortURL.tags.map((tag) {
|
||||
var randomColor = ([...Colors.primaries]..shuffle()).first.harmonizeWith(Theme.of(context).colorScheme.primary);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 4, top: 4),
|
||||
child: Container(
|
||||
padding: const 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),),
|
||||
padding:
|
||||
const EdgeInsets.only(left: 8, right: 8, bottom: 16, top: 16),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: MediaQuery.of(context).platformBrightness ==
|
||||
Brightness.dark
|
||||
? Colors.grey[800]!
|
||||
: Colors.grey[300]!)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.shortURL.title ?? widget.shortURL.shortCode,
|
||||
textScaleFactor: 1.4,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(
|
||||
widget.shortURL.longUrl,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textScaleFactor: 0.9,
|
||||
style: TextStyle(color: Colors.grey[600]),
|
||||
),
|
||||
// List tags in a row
|
||||
Wrap(
|
||||
children: widget.shortURL.tags.map((tag) {
|
||||
var randomColor = ([...Colors.primaries]..shuffle())
|
||||
.first
|
||||
.harmonizeWith(
|
||||
Theme.of(context).colorScheme.primary);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 4, top: 4),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 4, bottom: 4, left: 12, right: 12),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
color: randomColor,
|
||||
),
|
||||
);
|
||||
}).toList()
|
||||
|
||||
)
|
||||
],
|
||||
child: Text(
|
||||
tag,
|
||||
style: TextStyle(
|
||||
color: randomColor.computeLuminance() < 0.5
|
||||
? Colors.white
|
||||
: Colors.black),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList())
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(onPressed: () async {
|
||||
await Clipboard.setData(ClipboardData(text: widget.shortURL.shortUrl));
|
||||
final snackBar = SnackBar(content: const Text("Copied to clipboard!"), behavior: SnackBarBehavior.floating, backgroundColor: Colors.green[400]);
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
}, icon: const Icon(Icons.copy)),
|
||||
IconButton(onPressed: () {
|
||||
widget.showQRCode(widget.shortURL.shortUrl);
|
||||
}, icon: const Icon(Icons.qr_code))
|
||||
],
|
||||
)
|
||||
),
|
||||
)
|
||||
);
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
await Clipboard.setData(
|
||||
ClipboardData(text: widget.shortURL.shortUrl));
|
||||
final snackBar = SnackBar(
|
||||
content: const Text("Copied to clipboard!"),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
backgroundColor: Colors.green[400]);
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
},
|
||||
icon: const Icon(Icons.copy)),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
widget.showQRCode(widget.shortURL.shortUrl);
|
||||
},
|
||||
icon: const Icon(Icons.qr_code))
|
||||
],
|
||||
)),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -155,10 +155,10 @@ packages:
|
|||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_lints
|
||||
sha256: "2118df84ef0c3ca93f96123a616ae8540879991b8b57af2f81b76a7ada49b2a4"
|
||||
sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
version: "3.0.1"
|
||||
flutter_process_text:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -285,10 +285,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: lints
|
||||
sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593"
|
||||
sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
version: "3.0.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
@ -52,7 +52,7 @@ dev_dependencies:
|
|||
url: https://github.com/OutdatedGuy/flutter_launcher_icons.git
|
||||
ref: feat/monochrome-icons-support
|
||||
|
||||
flutter_lints: ^2.0.2
|
||||
flutter_lints: ^3.0.1
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
|
|
Loading…
Reference in New Issue
Block a user