formatting

This commit is contained in:
Adrian Baumgart 2024-01-28 00:32:09 +01:00
parent 7b16683d10
commit 086ca47fc0
No known key found for this signature in database
29 changed files with 1330 additions and 968 deletions

View File

@ -9,7 +9,23 @@
# packages, and plugins designed to encourage good coding practices. # packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml include: package:flutter_lints/flutter.yaml
linter: 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 # The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml` # 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 # 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 # 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 # `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint. # producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule # avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule

View File

@ -4,12 +4,16 @@ import 'package:shlink_app/API/Classes/ShortURL/visits_summary.dart';
class ShlinkStats { class ShlinkStats {
/// Data about non-orphan visits /// Data about non-orphan visits
VisitsSummary nonOrphanVisits; VisitsSummary nonOrphanVisits;
/// Data about orphan visits (without any valid slug assigned) /// Data about orphan visits (without any valid slug assigned)
VisitsSummary orphanVisits; VisitsSummary orphanVisits;
/// Total count of all short URLs /// Total count of all short URLs
int shortUrlsCount; int shortUrlsCount;
/// Total count all all tags /// Total count all all tags
int tagsCount; int tagsCount;
ShlinkStats(this.nonOrphanVisits, this.orphanVisits, this.shortUrlsCount, this.tagsCount); ShlinkStats(this.nonOrphanVisits, this.orphanVisits, this.shortUrlsCount,
} this.tagsCount);
}

View File

@ -2,8 +2,10 @@
class ShlinkStatsVisits { class ShlinkStatsVisits {
/// Count of URL visits /// Count of URL visits
int total; int total;
/// Count of URL visits from humans /// Count of URL visits from humans
int nonBots; int nonBots;
/// Count of URL visits from bots/crawlers /// Count of URL visits from bots/crawlers
int bots; int bots;
@ -14,4 +16,4 @@ class ShlinkStatsVisits {
: total = json["total"], : total = json["total"],
nonBots = json["nonBots"], nonBots = json["nonBots"],
bots = json["bots"]; bots = json["bots"];
} }

View File

@ -2,8 +2,10 @@
class DeviceLongUrls { class DeviceLongUrls {
/// Custom URL for Android devices /// Custom URL for Android devices
final String? android; final String? android;
/// Custom URL for iOS devices /// Custom URL for iOS devices
final String? ios; final String? ios;
/// Custom URL for desktop /// Custom URL for desktop
final String? desktop; final String? desktop;
@ -11,14 +13,11 @@ class DeviceLongUrls {
/// Converts JSON data from the API to an instance of [DeviceLongUrls] /// Converts JSON data from the API to an instance of [DeviceLongUrls]
DeviceLongUrls.fromJson(Map<String, dynamic> json) DeviceLongUrls.fromJson(Map<String, dynamic> json)
: android = json["android"], : android = json["android"],
ios = json["ios"], ios = json["ios"],
desktop = json["desktop"]; desktop = json["desktop"];
/// Converts data from this class to an JSON object of type /// Converts data from this class to an JSON object of type
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() =>
"android": android, {"android": android, "ios": ios, "desktop": desktop};
"ios": ios, }
"desktop": desktop
};
}

View File

@ -6,41 +6,61 @@ import 'package:shlink_app/API/Classes/ShortURL/visits_summary.dart';
class ShortURL { class ShortURL {
/// Slug of the short URL used in the URL /// Slug of the short URL used in the URL
String shortCode; String shortCode;
/// Entire short URL /// Entire short URL
String shortUrl; String shortUrl;
/// Long URL where the user gets redirected to /// Long URL where the user gets redirected to
String longUrl; String longUrl;
/// Device-specific long URLs /// Device-specific long URLs
DeviceLongUrls deviceLongUrls; DeviceLongUrls deviceLongUrls;
/// Creation date of the short URL /// Creation date of the short URL
DateTime dateCreated; DateTime dateCreated;
/// Visitor data /// Visitor data
VisitsSummary visitsSummary; VisitsSummary visitsSummary;
/// List of tags assigned to this short URL /// List of tags assigned to this short URL
List<dynamic> tags; List<dynamic> tags;
/// Metadata /// Metadata
ShortURLMeta meta; ShortURLMeta meta;
/// Associated domain /// Associated domain
String? domain; String? domain;
/// Optional title /// Optional title
String? title; String? title;
/// Whether the short URL is crawlable by a web crawler /// Whether the short URL is crawlable by a web crawler
bool crawlable; 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] /// Converts the JSON data from the API to an instance of [ShortURL]
ShortURL.fromJson(Map<String, dynamic> json): ShortURL.fromJson(Map<String, dynamic> json)
shortCode = json["shortCode"], : shortCode = json["shortCode"],
shortUrl = json["shortUrl"], shortUrl = json["shortUrl"],
longUrl = json["longUrl"], longUrl = json["longUrl"],
deviceLongUrls = DeviceLongUrls.fromJson(json["deviceLongUrls"]), deviceLongUrls = DeviceLongUrls.fromJson(json["deviceLongUrls"]),
dateCreated = DateTime.parse(json["dateCreated"]), dateCreated = DateTime.parse(json["dateCreated"]),
visitsSummary = VisitsSummary.fromJson(json["visitsSummary"]), visitsSummary = VisitsSummary.fromJson(json["visitsSummary"]),
tags = json["tags"], tags = json["tags"],
meta = ShortURLMeta.fromJson(json["meta"]), meta = ShortURLMeta.fromJson(json["meta"]),
domain = json["domain"], domain = json["domain"],
title = json["title"], title = json["title"],
crawlable = json["crawlable"]; crawlable = json["crawlable"];
}
}

View File

@ -2,16 +2,22 @@
class ShortURLMeta { class ShortURLMeta {
/// The date since when this short URL has been valid /// The date since when this short URL has been valid
DateTime? validSince; DateTime? validSince;
/// The data when this short URL expires /// The data when this short URL expires
DateTime? validUntil; DateTime? validUntil;
/// Amount of maximum visits allowed to this short URL /// Amount of maximum visits allowed to this short URL
int? maxVisits; int? maxVisits;
ShortURLMeta(this.validSince, this.validUntil, this.maxVisits); ShortURLMeta(this.validSince, this.validUntil, this.maxVisits);
/// Converts JSON data from the API to an instance of [ShortURLMeta] /// Converts JSON data from the API to an instance of [ShortURLMeta]
ShortURLMeta.fromJson(Map<String, dynamic> json): ShortURLMeta.fromJson(Map<String, dynamic> json)
validSince = json["validSince"] != null ? DateTime.parse(json["validSince"]) : null, : validSince = json["validSince"] != null
validUntil = json["validUntil"] != null ? DateTime.parse(json["validUntil"]) : null, ? DateTime.parse(json["validSince"])
maxVisits = json["maxVisits"]; : null,
} validUntil = json["validUntil"] != null
? DateTime.parse(json["validUntil"])
: null,
maxVisits = json["maxVisits"];
}

View File

@ -2,16 +2,18 @@
class VisitsSummary { class VisitsSummary {
/// Count of total visits /// Count of total visits
int total; int total;
/// Count of visits from humans /// Count of visits from humans
int nonBots; int nonBots;
/// Count of visits from bots/crawlers /// Count of visits from bots/crawlers
int bots; int bots;
VisitsSummary(this.total, this.nonBots, this.bots); VisitsSummary(this.total, this.nonBots, this.bots);
/// Converts JSON data from the API to an instance of [VisitsSummary] /// Converts JSON data from the API to an instance of [VisitsSummary]
VisitsSummary.fromJson(Map<String, dynamic> json): VisitsSummary.fromJson(Map<String, dynamic> json)
total = json["total"] as int, : total = json["total"] as int,
nonBots = json["nonBots"] as int, nonBots = json["nonBots"] as int,
bots = json["bots"] as int; bots = json["bots"] as int;
} }

View File

@ -4,32 +4,57 @@ import '../ShortURL/device_long_urls.dart';
class ShortURLSubmission { class ShortURLSubmission {
/// Long URL to redirect to /// Long URL to redirect to
String longUrl; String longUrl;
/// Device-specific long URLs /// Device-specific long URLs
DeviceLongUrls? deviceLongUrls; DeviceLongUrls? deviceLongUrls;
/// Date since when this short URL is valid in ISO8601 format /// Date since when this short URL is valid in ISO8601 format
String? validSince; String? validSince;
/// Date until when this short URL is valid in ISO8601 format /// Date until when this short URL is valid in ISO8601 format
String? validUntil; String? validUntil;
/// Amount of maximum visits allowed to this short URLs /// Amount of maximum visits allowed to this short URLs
int? maxVisits; int? maxVisits;
/// List of tags assigned to this short URL /// List of tags assigned to this short URL
List<String> tags; List<String> tags;
/// Title of the page /// Title of the page
String? title; String? title;
/// Whether the short URL is crawlable by web crawlers /// Whether the short URL is crawlable by web crawlers
bool crawlable; bool crawlable;
/// Whether to forward query parameters /// Whether to forward query parameters
bool forwardQuery; bool forwardQuery;
/// Custom slug (if not provided a random one will be generated) /// Custom slug (if not provided a random one will be generated)
String? customSlug; String? customSlug;
/// Whether to use an existing short URL if the slug matches /// Whether to use an existing short URL if the slug matches
bool findIfExists; bool findIfExists;
/// Domain to use /// Domain to use
String? domain; String? domain;
/// Length of the slug if a custom one is not provided /// Length of the slug if a custom one is not provided
int? shortCodeLength; 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 /// Converts class data to a JSON object
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
@ -49,4 +74,4 @@ class ShortURLSubmission {
"shortCodeLength": shortCodeLength "shortCodeLength": shortCodeLength
}; };
} }
} }

View File

@ -5,25 +5,28 @@ import 'package:http/http.dart' as http;
import '../server_manager.dart'; import '../server_manager.dart';
/// Tries to connect to the Shlink server /// 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 { 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 ?? "", "X-Api-Key": apiKey ?? "",
}); });
if (response.statusCode == 200) { if (response.statusCode == 200) {
return left(""); return left("");
} } else {
else {
try { try {
var jsonBody = jsonDecode(response.body); var jsonBody = jsonDecode(response.body);
return right(ApiFailure(type: jsonBody["type"], detail: jsonBody["detail"], title: jsonBody["title"], status: jsonBody["status"])); return right(ApiFailure(
} type: jsonBody["type"],
catch(resErr) { detail: jsonBody["detail"],
title: jsonBody["title"],
status: jsonBody["status"]));
} catch (resErr) {
return right(RequestFailure(response.statusCode, resErr.toString())); return right(RequestFailure(response.statusCode, resErr.toString()));
} }
} }
} } catch (reqErr) {
catch(reqErr) {
return right(RequestFailure(0, reqErr.toString())); return right(RequestFailure(0, reqErr.toString()));
} }
} }

View File

@ -5,26 +5,30 @@ import 'package:http/http.dart' as http;
import '../server_manager.dart'; import '../server_manager.dart';
/// Deletes a short URL from the server /// 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 { try {
final response = await http.delete(Uri.parse("$serverUrl/rest/v$apiVersion/short-urls/$shortCode"), headers: { final response = await http.delete(
"X-Api-Key": apiKey ?? "", Uri.parse("$serverUrl/rest/v$apiVersion/short-urls/$shortCode"),
}); headers: {
"X-Api-Key": apiKey ?? "",
});
if (response.statusCode == 204) { if (response.statusCode == 204) {
// get returned short url // get returned short url
return left(""); return left("");
} } else {
else {
try { try {
var jsonBody = jsonDecode(response.body); var jsonBody = jsonDecode(response.body);
return right(ApiFailure(type: jsonBody["type"], detail: jsonBody["detail"], title: jsonBody["title"], status: jsonBody["status"])); return right(ApiFailure(
} type: jsonBody["type"],
catch(resErr) { detail: jsonBody["detail"],
title: jsonBody["title"],
status: jsonBody["status"]));
} catch (resErr) {
return right(RequestFailure(response.statusCode, resErr.toString())); return right(RequestFailure(response.statusCode, resErr.toString()));
} }
} }
} } catch (reqErr) {
catch(reqErr) {
return right(RequestFailure(0, reqErr.toString())); return right(RequestFailure(0, reqErr.toString()));
} }
} }

View File

@ -6,29 +6,35 @@ import 'package:shlink_app/API/Classes/ShortURL/short_url.dart';
import '../server_manager.dart'; import '../server_manager.dart';
/// Gets recently created short URLs from the server /// 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 { try {
final response = await http.get(Uri.parse("$serverUrl/rest/v$apiVersion/short-urls?itemsPerPage=5&orderBy=dateCreated-DESC"), headers: { final response = await http.get(
"X-Api-Key": apiKey ?? "", Uri.parse(
}); "$serverUrl/rest/v$apiVersion/short-urls?itemsPerPage=5&orderBy=dateCreated-DESC"),
headers: {
"X-Api-Key": apiKey ?? "",
});
if (response.statusCode == 200) { if (response.statusCode == 200) {
var jsonResponse = jsonDecode(response.body); 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); return ShortURL.fromJson(e);
}).toList(); }).toList();
return left(shortURLs); return left(shortURLs);
} } else {
else {
try { try {
var jsonBody = jsonDecode(response.body); var jsonBody = jsonDecode(response.body);
return right(ApiFailure(type: jsonBody["type"], detail: jsonBody["detail"], title: jsonBody["title"], status: jsonBody["status"])); return right(ApiFailure(
} type: jsonBody["type"],
catch(resErr) { detail: jsonBody["detail"],
title: jsonBody["title"],
status: jsonBody["status"]));
} catch (resErr) {
return right(RequestFailure(response.statusCode, resErr.toString())); return right(RequestFailure(response.statusCode, resErr.toString()));
} }
} }
} } catch (reqErr) {
catch(reqErr) {
return right(RequestFailure(0, reqErr.toString())); return right(RequestFailure(0, reqErr.toString()));
} }
} }

View File

@ -5,26 +5,30 @@ import 'package:http/http.dart' as http;
import '../server_manager.dart'; import '../server_manager.dart';
/// Gets the status of the server and health information /// 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 { 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 ?? "", "X-Api-Key": apiKey ?? "",
}); });
if (response.statusCode == 200) { if (response.statusCode == 200) {
var jsonData = jsonDecode(response.body); var jsonData = jsonDecode(response.body);
return left(ServerHealthResponse(status: jsonData["status"], version: jsonData["version"])); return left(ServerHealthResponse(
} status: jsonData["status"], version: jsonData["version"]));
else { } else {
try { try {
var jsonBody = jsonDecode(response.body); var jsonBody = jsonDecode(response.body);
return right(ApiFailure(type: jsonBody["type"], detail: jsonBody["detail"], title: jsonBody["title"], status: jsonBody["status"])); return right(ApiFailure(
} type: jsonBody["type"],
catch(resErr) { detail: jsonBody["detail"],
title: jsonBody["title"],
status: jsonBody["status"]));
} catch (resErr) {
return right(RequestFailure(response.statusCode, resErr.toString())); return right(RequestFailure(response.statusCode, resErr.toString()));
} }
} }
} } catch (reqErr) {
catch(reqErr) {
return right(RequestFailure(0, reqErr.toString())); return right(RequestFailure(0, reqErr.toString()));
} }
} }

View File

@ -7,14 +7,14 @@ import '../Classes/ShlinkStats/shlink_stats.dart';
import '../server_manager.dart'; import '../server_manager.dart';
/// Gets statistics about the Shlink server /// 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 nonOrphanVisits;
var orphanVisits; var orphanVisits;
var shortUrlsCount; var shortUrlsCount;
var tagsCount; var tagsCount;
var failure; var failure;
var visitStatsResponse = await _getVisitStats(apiKey, serverUrl, apiVersion); var visitStatsResponse = await _getVisitStats(apiKey, serverUrl, apiVersion);
visitStatsResponse.fold((l) { visitStatsResponse.fold((l) {
nonOrphanVisits = l.nonOrphanVisits; nonOrphanVisits = l.nonOrphanVisits;
@ -24,7 +24,8 @@ FutureOr<Either<ShlinkStats, Failure>> apiGetShlinkStats(String? apiKey, String?
return right(r); return right(r);
}); });
var shortUrlsCountResponse = await _getShortUrlsCount(apiKey, serverUrl, apiVersion); var shortUrlsCountResponse =
await _getShortUrlsCount(apiKey, serverUrl, apiVersion);
shortUrlsCountResponse.fold((l) { shortUrlsCountResponse.fold((l) {
shortUrlsCount = l; shortUrlsCount = l;
}, (r) { }, (r) {
@ -40,14 +41,15 @@ FutureOr<Either<ShlinkStats, Failure>> apiGetShlinkStats(String? apiKey, String?
return right(r); 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)); await Future.delayed(const Duration(milliseconds: 100));
} }
if (failure != null) { if (failure != null) {
return right(failure); return right(failure);
} }
return left(ShlinkStats(nonOrphanVisits, orphanVisits, shortUrlsCount, tagsCount)); return left(
ShlinkStats(nonOrphanVisits, orphanVisits, shortUrlsCount, tagsCount));
} }
class _ShlinkVisitStats { class _ShlinkVisitStats {
@ -58,79 +60,89 @@ class _ShlinkVisitStats {
} }
/// Gets visitor statistics about the entire server /// 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 { 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 ?? "", "X-Api-Key": apiKey ?? "",
}); });
if (response.statusCode == 200) { if (response.statusCode == 200) {
var jsonResponse = jsonDecode(response.body); var jsonResponse = jsonDecode(response.body);
var nonOrphanVisits = VisitsSummary.fromJson(jsonResponse["visits"]["nonOrphanVisits"]); var nonOrphanVisits =
var orphanVisits = VisitsSummary.fromJson(jsonResponse["visits"]["orphanVisits"]); VisitsSummary.fromJson(jsonResponse["visits"]["nonOrphanVisits"]);
var orphanVisits =
VisitsSummary.fromJson(jsonResponse["visits"]["orphanVisits"]);
return left(_ShlinkVisitStats(nonOrphanVisits, orphanVisits)); return left(_ShlinkVisitStats(nonOrphanVisits, orphanVisits));
} else {
}
else {
try { try {
var jsonBody = jsonDecode(response.body); var jsonBody = jsonDecode(response.body);
return right(ApiFailure(type: jsonBody["type"], detail: jsonBody["detail"], title: jsonBody["title"], status: jsonBody["status"])); return right(ApiFailure(
} type: jsonBody["type"],
catch(resErr) { detail: jsonBody["detail"],
title: jsonBody["title"],
status: jsonBody["status"]));
} catch (resErr) {
return right(RequestFailure(response.statusCode, resErr.toString())); return right(RequestFailure(response.statusCode, resErr.toString()));
} }
} }
} } catch (reqErr) {
catch(reqErr) {
return right(RequestFailure(0, reqErr.toString())); return right(RequestFailure(0, reqErr.toString()));
} }
} }
/// Gets amount of short URLs /// 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 { 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 ?? "", "X-Api-Key": apiKey ?? "",
}); });
if (response.statusCode == 200) { if (response.statusCode == 200) {
var jsonResponse = jsonDecode(response.body); var jsonResponse = jsonDecode(response.body);
return left(jsonResponse["shortUrls"]["pagination"]["totalItems"]); return left(jsonResponse["shortUrls"]["pagination"]["totalItems"]);
} } else {
else {
try { try {
var jsonBody = jsonDecode(response.body); var jsonBody = jsonDecode(response.body);
return right(ApiFailure(type: jsonBody["type"], detail: jsonBody["detail"], title: jsonBody["title"], status: jsonBody["status"])); return right(ApiFailure(
} type: jsonBody["type"],
catch(resErr) { detail: jsonBody["detail"],
title: jsonBody["title"],
status: jsonBody["status"]));
} catch (resErr) {
return right(RequestFailure(response.statusCode, resErr.toString())); return right(RequestFailure(response.statusCode, resErr.toString()));
} }
} }
} } catch (reqErr) {
catch(reqErr) {
return right(RequestFailure(0, reqErr.toString())); return right(RequestFailure(0, reqErr.toString()));
} }
} }
/// Gets amount of tags /// 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 { 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 ?? "", "X-Api-Key": apiKey ?? "",
}); });
if (response.statusCode == 200) { if (response.statusCode == 200) {
var jsonResponse = jsonDecode(response.body); var jsonResponse = jsonDecode(response.body);
return left(jsonResponse["tags"]["pagination"]["totalItems"]); return left(jsonResponse["tags"]["pagination"]["totalItems"]);
} } else {
else {
try { try {
var jsonBody = jsonDecode(response.body); var jsonBody = jsonDecode(response.body);
return right(ApiFailure(type: jsonBody["type"], detail: jsonBody["detail"], title: jsonBody["title"], status: jsonBody["status"])); return right(ApiFailure(
} type: jsonBody["type"],
catch(resErr) { detail: jsonBody["detail"],
title: jsonBody["title"],
status: jsonBody["status"]));
} catch (resErr) {
return right(RequestFailure(response.statusCode, resErr.toString())); return right(RequestFailure(response.statusCode, resErr.toString()));
} }
} }
} } catch (reqErr) {
catch(reqErr) {
return right(RequestFailure(0, reqErr.toString())); return right(RequestFailure(0, reqErr.toString()));
} }
} }

View File

@ -6,7 +6,8 @@ import 'package:shlink_app/API/Classes/ShortURL/short_url.dart';
import '../server_manager.dart'; import '../server_manager.dart';
/// Gets all short URLs /// 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 currentPage = 1;
var maxPages = 2; var maxPages = 2;
List<ShortURL> allUrls = []; List<ShortURL> allUrls = [];
@ -14,7 +15,8 @@ FutureOr<Either<List<ShortURL>, Failure>> apiGetShortUrls(String? apiKey, String
Failure? error; Failure? error;
while (currentPage <= maxPages) { while (currentPage <= maxPages) {
final response = await _getShortUrlPage(currentPage, apiKey, serverUrl, apiVersion); final response =
await _getShortUrlPage(currentPage, apiKey, serverUrl, apiVersion);
response.fold((l) { response.fold((l) {
allUrls.addAll(l.urls); allUrls.addAll(l.urls);
maxPages = l.totalPages; maxPages = l.totalPages;
@ -26,37 +28,42 @@ FutureOr<Either<List<ShortURL>, Failure>> apiGetShortUrls(String? apiKey, String
} }
if (error == null) { if (error == null) {
return left(allUrls); return left(allUrls);
} } else {
else {
return right(error!); return right(error!);
} }
} }
/// Gets all short URLs from a specific page /// 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 { try {
final response = await http.get(Uri.parse("$serverUrl/rest/v$apiVersion/short-urls?page=$page"), headers: { final response = await http.get(
"X-Api-Key": apiKey ?? "", Uri.parse("$serverUrl/rest/v$apiVersion/short-urls?page=$page"),
}); headers: {
"X-Api-Key": apiKey ?? "",
});
if (response.statusCode == 200) { if (response.statusCode == 200) {
var jsonResponse = jsonDecode(response.body); var jsonResponse = jsonDecode(response.body);
var pagesCount = jsonResponse["shortUrls"]["pagination"]["pagesCount"] as int; var pagesCount =
List<ShortURL> shortURLs = (jsonResponse["shortUrls"]["data"] as List<dynamic>).map((e) { jsonResponse["shortUrls"]["pagination"]["pagesCount"] as int;
List<ShortURL> shortURLs =
(jsonResponse["shortUrls"]["data"] as List<dynamic>).map((e) {
return ShortURL.fromJson(e); return ShortURL.fromJson(e);
}).toList(); }).toList();
return left(ShortURLPageResponse(shortURLs, pagesCount)); return left(ShortURLPageResponse(shortURLs, pagesCount));
} } else {
else {
try { try {
var jsonBody = jsonDecode(response.body); var jsonBody = jsonDecode(response.body);
return right(ApiFailure(type: jsonBody["type"], detail: jsonBody["detail"], title: jsonBody["title"], status: jsonBody["status"])); return right(ApiFailure(
} type: jsonBody["type"],
catch(resErr) { detail: jsonBody["detail"],
title: jsonBody["title"],
status: jsonBody["status"]));
} catch (resErr) {
return right(RequestFailure(response.statusCode, resErr.toString())); return right(RequestFailure(response.statusCode, resErr.toString()));
} }
} }
} } catch (reqErr) {
catch(reqErr) {
return right(RequestFailure(0, reqErr.toString())); return right(RequestFailure(0, reqErr.toString()));
} }
} }

View File

@ -6,27 +6,33 @@ import 'package:shlink_app/API/Classes/ShortURLSubmission/short_url_submission.d
import '../server_manager.dart'; import '../server_manager.dart';
/// Submits a short URL to a server for it to be added /// 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 { try {
final response = await http.post(Uri.parse("$serverUrl/rest/v$apiVersion/short-urls"), headers: { final response =
"X-Api-Key": apiKey ?? "", await http.post(Uri.parse("$serverUrl/rest/v$apiVersion/short-urls"),
}, body: jsonEncode(shortUrl.toJson())); headers: {
"X-Api-Key": apiKey ?? "",
},
body: jsonEncode(shortUrl.toJson()));
if (response.statusCode == 200) { if (response.statusCode == 200) {
// get returned short url // get returned short url
var jsonBody = jsonDecode(response.body); var jsonBody = jsonDecode(response.body);
return left(jsonBody["shortUrl"]); return left(jsonBody["shortUrl"]);
} } else {
else {
try { try {
var jsonBody = jsonDecode(response.body); var jsonBody = jsonDecode(response.body);
return right(ApiFailure(type: jsonBody["type"], detail: jsonBody["detail"], title: jsonBody["title"], status: jsonBody["status"], invalidElements: jsonBody["invalidElements"])); return right(ApiFailure(
} type: jsonBody["type"],
catch(resErr) { detail: jsonBody["detail"],
title: jsonBody["title"],
status: jsonBody["status"],
invalidElements: jsonBody["invalidElements"]));
} catch (resErr) {
return right(RequestFailure(response.statusCode, resErr.toString())); return right(RequestFailure(response.statusCode, resErr.toString()));
} }
} }
} } catch (reqErr) {
catch(reqErr) {
return right(RequestFailure(0, reqErr.toString())); return right(RequestFailure(0, reqErr.toString()));
} }
} }

View File

@ -15,7 +15,6 @@ import 'Methods/delete_short_url.dart';
import 'Methods/submit_short_url.dart'; import 'Methods/submit_short_url.dart';
class ServerManager { class ServerManager {
/// The URL of the Shlink server /// The URL of the Shlink server
String? serverUrl; String? serverUrl;
@ -69,9 +68,9 @@ class ServerManager {
storage.write(key: "shlink_apikey", value: apiKey); storage.write(key: "shlink_apikey", value: apiKey);
} }
/// Saves provided server credentials and tries to establish a connection /// 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 // TODO: convert url to correct format
serverUrl = url; serverUrl = url;
this.apiKey = apiKey; this.apiKey = apiKey;
@ -100,7 +99,8 @@ class ServerManager {
} }
/// Saves a new short URL to the server /// 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); return apiSubmitShortUrl(shortUrl, apiKey, serverUrl, apiVersion);
} }
@ -139,7 +139,8 @@ class ServerHealthResponse {
/// Failure class, used for the API /// Failure class, used for the API
abstract class Failure {} 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 { class RequestFailure extends Failure {
int statusCode; int statusCode;
String description; String description;
@ -155,5 +156,10 @@ class ApiFailure extends Failure {
int status; int status;
List<dynamic>? invalidElements; 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});
}

View File

@ -1,4 +1,5 @@
library dev.abmgrt.shlink_app.globals; library dev.abmgrt.shlink_app.globals;
import 'package:shlink_app/API/server_manager.dart'; import 'package:shlink_app/API/server_manager.dart';
ServerManager serverManager = ServerManager(); ServerManager serverManager = ServerManager();

View File

@ -11,11 +11,11 @@ void main() {
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
const MyApp({super.key}); const MyApp({super.key});
static const _defaultLightColorScheme = static const _defaultLightColorScheme = ColorScheme
ColorScheme.light();//.fromSwatch(primarySwatch: Colors.blue, backgroundColor: Colors.white); .light(); //.fromSwatch(primarySwatch: Colors.blue, backgroundColor: Colors.white);
static final _defaultDarkColorScheme = ColorScheme.fromSwatch( static final _defaultDarkColorScheme =
primarySwatch: Colors.blue, brightness: Brightness.dark); ColorScheme.fromSwatch(brightness: Brightness.dark);
// This widget is the root of your application. // This widget is the root of your application.
@override @override
@ -25,24 +25,22 @@ class MyApp extends StatelessWidget {
title: 'Shlink', title: 'Shlink',
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
theme: ThemeData( theme: ThemeData(
appBarTheme: const AppBarTheme( appBarTheme: const AppBarTheme(
backgroundColor: Color(0xfffafafa), backgroundColor: Color(0xfffafafa),
), ),
colorScheme: lightColorScheme ?? _defaultLightColorScheme, colorScheme: lightColorScheme ?? _defaultLightColorScheme,
useMaterial3: true useMaterial3: true),
),
darkTheme: ThemeData( darkTheme: ThemeData(
appBarTheme: const AppBarTheme( appBarTheme: const AppBarTheme(
backgroundColor: Color(0xff0d0d0d), backgroundColor: Color(0xff0d0d0d),
foregroundColor: Colors.white, foregroundColor: Colors.white,
elevation: 0, elevation: 0,
), ),
colorScheme: darkColorScheme?.copyWith(background: Colors.black) ?? _defaultDarkColorScheme, colorScheme: darkColorScheme?.copyWith(background: Colors.black) ??
_defaultDarkColorScheme,
useMaterial3: true, useMaterial3: true,
), ),
themeMode: ThemeMode.system, home: const InitialPage());
home: const InitialPage()
);
}); });
} }
} }
@ -55,7 +53,6 @@ class InitialPage extends StatefulWidget {
} }
class _InitialPageState extends State<InitialPage> { class _InitialPageState extends State<InitialPage> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -63,26 +60,20 @@ class _InitialPageState extends State<InitialPage> {
} }
void checkLogin() async { void checkLogin() async {
bool result = await globals.serverManager.checkLogin(); bool result = await globals.serverManager.checkLogin();
if (result) { if (result) {
Navigator.of(context).pushReplacement( Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => const NavigationBarView()) MaterialPageRoute(builder: (context) => const NavigationBarView()));
); } else {
}
else {
Navigator.of(context).pushReplacement( Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => const LoginView()) MaterialPageRoute(builder: (context) => const LoginView()));
);
} }
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return const Scaffold( return const Scaffold(
body: Center( body: Center(child: Text("")),
child: Text("")
),
); );
} }
} }

View File

@ -28,7 +28,8 @@ class LicenseUtil {
return [ return [
const License( const License(
name: r'cupertino_icons', name: r'cupertino_icons',
license: r'''The MIT License (MIT) license: r'''
The MIT License (MIT)
Copyright (c) 2016 Vladimir Kharlampidi 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 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.''', CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''',
version: r'^1.0.5', version: r'^1.0.5',
homepage: null, repository:
repository: r'https://github.com/flutter/packages/tree/main/third_party/packages/cupertino_icons', r'https://github.com/flutter/packages/tree/main/third_party/packages/cupertino_icons',
), ),
const License( const License(
name: r'dartz', 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 Copyright (c) 2016, 2017, 2018, 2019, 2020, 2021 Björn Sperber
@ -78,11 +80,11 @@ SOFTWARE.
''', ''',
version: r'^0.10.1', version: r'^0.10.1',
homepage: r'https://github.com/spebbe/dartz', homepage: r'https://github.com/spebbe/dartz',
repository: null,
), ),
const License( const License(
name: r'dynamic_color', name: r'dynamic_color',
license: r''' Apache License license: r'''
Apache License
Version 2.0, January 2004 Version 2.0, January 2004
http://www.apache.org/licenses/ http://www.apache.org/licenses/
@ -285,12 +287,13 @@ SOFTWARE.
limitations under the License. limitations under the License.
''', ''',
version: r'^1.6.6', version: r'^1.6.6',
homepage: null, repository:
repository: r'https://github.com/material-foundation/flutter-packages/tree/main/packages/dynamic_color', r'https://github.com/material-foundation/flutter-packages/tree/main/packages/dynamic_color',
), ),
const License( const License(
name: r'flutter', 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, Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met: 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 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
''', ''',
version: null,
homepage: r'https://flutter.dev/', homepage: r'https://flutter.dev/',
repository: r'https://github.com/flutter/flutter', repository: r'https://github.com/flutter/flutter',
), ),
const License( const License(
name: r'flutter_launcher_icons', name: r'flutter_launcher_icons',
license: r'''MIT License license: r'''
MIT License
Copyright (c) 2019 Mark O'Sullivan Copyright (c) 2019 Mark O'Sullivan
@ -346,11 +349,13 @@ SOFTWARE.
''', ''',
version: r'0.13.1', version: r'0.13.1',
homepage: r'https://github.com/fluttercommunity/flutter_launcher_icons', 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( const License(
name: r'flutter_lints', 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, Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met: 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. SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
''', ''',
version: r'^2.0.2', version: r'^2.0.2',
homepage: null, repository:
repository: r'https://github.com/flutter/packages/tree/main/packages/flutter_lints', r'https://github.com/flutter/packages/tree/main/packages/flutter_lints',
), ),
const License( const License(
name: r'flutter_process_text', name: r'flutter_process_text',
license: r'''BSD 3-Clause License license: r'''
BSD 3-Clause License
(c) Copyright 2021 divshekhar (Divyanshu Shekhar) (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, 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.''', EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
version: r'^1.1.2', version: r'^1.1.2',
homepage: null,
repository: r'https://github.com/DevsOnFlutter/flutter_process_text', repository: r'https://github.com/DevsOnFlutter/flutter_process_text',
), ),
const License( const License(
name: r'flutter_secure_storage', name: r'flutter_secure_storage',
license: r'''BSD 3-Clause License license: r'''
BSD 3-Clause License
Copyright 2017 German Saprykin Copyright 2017 German Saprykin
All rights reserved. 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 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.''', OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
version: r'^8.0.0', version: r'^8.0.0',
homepage: null, repository:
repository: r'https://github.com/mogol/flutter_secure_storage/tree/develop/flutter_secure_storage', r'https://github.com/mogol/flutter_secure_storage/tree/develop/flutter_secure_storage',
), ),
const License( const License(
name: r'flutter_test', 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, Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met: 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 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
''', ''',
version: null,
homepage: r'https://flutter.dev/', homepage: r'https://flutter.dev/',
repository: r'https://github.com/flutter/flutter', repository: r'https://github.com/flutter/flutter',
), ),
const License( const License(
name: r'http', 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 Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are 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. OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
''', ''',
version: r'^0.13.6', version: r'^0.13.6',
homepage: null,
repository: r'https://github.com/dart-lang/http/tree/master/pkgs/http', repository: r'https://github.com/dart-lang/http/tree/master/pkgs/http',
), ),
const License( const License(
name: r'intl', 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 Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are 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. OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
''', ''',
version: r'^0.18.1', version: r'^0.18.1',
homepage: null,
repository: r'https://github.com/dart-lang/i18n/tree/main/pkgs/intl', repository: r'https://github.com/dart-lang/i18n/tree/main/pkgs/intl',
), ),
const License( const License(
name: r'license_generator', name: r'license_generator',
license: r'''MIT License license: r'''
MIT License
Copyright (c) 2022 icapps Copyright (c) 2022 icapps
@ -574,11 +581,11 @@ SOFTWARE.
''', ''',
version: r'^1.0.5', version: r'^1.0.5',
homepage: r'https://github.com/icapps/flutter-icapps-license', homepage: r'https://github.com/icapps/flutter-icapps-license',
repository: null,
), ),
const License( const License(
name: r'package_info_plus', 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 Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are 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', version: r'^4.0.2',
homepage: r'https://plus.fluttercommunity.dev/', 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( const License(
name: r'qr_flutter', name: r'qr_flutter',
license: r'''BSD 3-Clause License license: r'''
BSD 3-Clause License
Copyright (c) 2020, Luke Freeman. Copyright (c) 2020, Luke Freeman.
All rights reserved. All rights reserved.
@ -644,11 +653,11 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
''', ''',
version: r'^4.1.0', version: r'^4.1.0',
homepage: r'https://github.com/theyakka/qr.flutter', homepage: r'https://github.com/theyakka/qr.flutter',
repository: null,
), ),
const License( const License(
name: r'shared_preferences', 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, Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met: 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. SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
''', ''',
version: r'^2.2.2', version: r'^2.2.2',
homepage: null, repository:
repository: r'https://github.com/flutter/packages/tree/main/packages/shared_preferences/shared_preferences', r'https://github.com/flutter/packages/tree/main/packages/shared_preferences/shared_preferences',
), ),
const License( const License(
name: r'tuple', name: r'tuple',
license: r'''Copyright (c) 2014, the tuple project authors. license: r'''
Copyright (c) 2014, the tuple project authors.
All rights reserved. All rights reserved.
Redistribution and use in source and binary forms, with or without 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 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
version: r'^2.0.2', version: r'^2.0.2',
homepage: null,
repository: r'https://github.com/google/tuple.dart', repository: r'https://github.com/google/tuple.dart',
), ),
const License( const License(
name: r'url_launcher', 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, Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met: 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. SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
''', ''',
version: r'6.1.9', version: r'6.1.9',
homepage: null, repository:
repository: r'https://github.com/flutter/plugins/tree/main/packages/url_launcher/url_launcher', r'https://github.com/flutter/plugins/tree/main/packages/url_launcher/url_launcher',
), ),
]; ];
} }

View File

@ -8,14 +8,13 @@ import '../API/Classes/ShortURL/short_url.dart';
import '../globals.dart' as globals; import '../globals.dart' as globals;
class HomeView extends StatefulWidget { class HomeView extends StatefulWidget {
const HomeView({Key? key}) : super(key: key); const HomeView({super.key});
@override @override
State<HomeView> createState() => _HomeViewState(); State<HomeView> createState() => _HomeViewState();
} }
class _HomeViewState extends State<HomeView> { class _HomeViewState extends State<HomeView> {
ShlinkStats? shlinkStats; ShlinkStats? shlinkStats;
List<ShortURL> shortUrls = []; List<ShortURL> shortUrls = [];
@ -27,9 +26,8 @@ class _HomeViewState extends State<HomeView> {
void initState() { void initState() {
// TODO: implement initState // TODO: implement initState
super.initState(); super.initState();
WidgetsBinding.instance WidgetsBinding.instance.addPostFrameCallback((_) {
.addPostFrameCallback((_) { loadAllData();
loadAllData();
}); });
} }
@ -49,12 +47,14 @@ class _HomeViewState extends State<HomeView> {
var text = ""; var text = "";
if (r is RequestFailure) { if (r is RequestFailure) {
text = r.description; text = r.description;
} } else {
else {
text = (r as ApiFailure).detail; 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); ScaffoldMessenger.of(context).showSnackBar(snackBar);
}); });
} }
@ -70,12 +70,14 @@ class _HomeViewState extends State<HomeView> {
var text = ""; var text = "";
if (r is RequestFailure) { if (r is RequestFailure) {
text = r.description; text = r.description;
} } else {
else {
text = (r as ApiFailure).detail; 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); ScaffoldMessenger.of(context).showSnackBar(snackBar);
}); });
} }
@ -83,134 +85,171 @@ class _HomeViewState extends State<HomeView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: Stack( body: Stack(
children: [ children: [
ColorFiltered( ColorFiltered(
colorFilter: ColorFilter.mode(Colors.black.withOpacity(_qrCodeShown ? 0.4 : 0), BlendMode.srcOver), colorFilter: ColorFilter.mode(
child: RefreshIndicator( Colors.black.withOpacity(_qrCodeShown ? 0.4 : 0),
onRefresh: () async { BlendMode.srcOver),
return loadAllData(); child: RefreshIndicator(
}, onRefresh: () async {
child: CustomScrollView( return loadAllData();
slivers: [ },
SliverAppBar.medium( child: CustomScrollView(
expandedHeight: 160, slivers: [
title: Column( SliverAppBar.medium(
crossAxisAlignment: CrossAxisAlignment.start, expandedHeight: 160,
children: [ title: Column(
const Text("Shlink", style: TextStyle(fontWeight: FontWeight.bold)), crossAxisAlignment: CrossAxisAlignment.start,
Text(globals.serverManager.getServerUrl(), style: TextStyle(fontSize: 16, color: Colors.grey[600])) children: [
], const Text("Shlink",
) style: TextStyle(fontWeight: FontWeight.bold)),
), Text(globals.serverManager.getServerUrl(),
SliverToBoxAdapter( style: TextStyle(
child: Wrap( fontSize: 16, color: Colors.grey[600]))
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( SliverToBoxAdapter(
child: Center( child: Wrap(
child: Padding( alignment: WrapAlignment.spaceEvenly,
padding: const EdgeInsets.only(top: 50), children: [
child: Column( _ShlinkStatsCardWidget(
children: [ icon: Icons.link,
const Text("No Short URLs", style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),), text:
Padding( "${shlinkStats?.shortUrlsCount.toString() ?? "0"} Short URLs",
padding: const EdgeInsets.only(top: 8), borderColor: Colors.blue),
child: Text('Create one by tapping the "+" button below', style: TextStyle(fontSize: 16, color: Colors.grey[600]),), _ShlinkStatsCardWidget(
) icon: Icons.remove_red_eye,
], text:
) "${shlinkStats?.nonOrphanVisits.total ?? "0"} Visits",
) borderColor: Colors.green),
) _ShlinkStatsCardWidget(
) icon: Icons.warning,
else text:
SliverList(delegate: SliverChildBuilderDelegate( "${shlinkStats?.orphanVisits.total ?? "0"} Orphan Visits",
(BuildContext context, int index) { borderColor: Colors.red),
if (index == 0) { _ShlinkStatsCardWidget(
return const Padding( icon: Icons.sell,
padding: EdgeInsets.only(top: 16, left: 12, right: 12), text:
child: Text("Recent Short URLs", style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), "${shlinkStats?.tagsCount.toString() ?? "0"} Tags",
); borderColor: Colors.purple),
} ],
else { ),
final shortURL = shortUrls[index - 1]; ),
return ShortURLCell(shortURL: shortURL, reload: () { if (shortUrlsLoaded && shortUrls.isEmpty)
loadRecentShortUrls(); SliverToBoxAdapter(
}, showQRCode: (String url) { child: Center(
setState(() { child: Padding(
_qrUrl = url; padding: const EdgeInsets.only(top: 50),
_qrCodeShown = true; child: Column(
}); children: [
}, isLast: index == shortUrls.length); const Text(
} "No Short URLs",
}, style: TextStyle(
childCount: shortUrls.length + 1 fontSize: 24,
)) fontWeight: FontWeight.bold),
),
], Padding(
), padding: const EdgeInsets.only(top: 8),
), child: Text(
), 'Create one by tapping the "+" button below',
if (_qrCodeShown) style: TextStyle(
GestureDetector( fontSize: 16,
onTap: () { color: Colors.grey[600]),
setState(() { ),
_qrCodeShown = false; )
}); ],
}, ))))
child: Container( else
color: Colors.black.withOpacity(0), SliverList(
), delegate: SliverChildBuilderDelegate(
), (BuildContext context, int index) {
if (_qrCodeShown) if (index == 0) {
Center( return const Padding(
child: SizedBox( padding:
width: MediaQuery.of(context).size.width / 1.7, EdgeInsets.only(top: 16, left: 12, right: 12),
height: MediaQuery.of(context).size.width / 1.7, child: Text("Recent Short URLs",
child: Card( style: TextStyle(
child: Padding( fontSize: 20, fontWeight: FontWeight.bold)),
padding: const EdgeInsets.all(16), );
child: QrImageView( } else {
data: _qrUrl, final shortURL = shortUrls[index - 1];
version: QrVersions.auto, return ShortURLCell(
size: 200.0, shortURL: shortURL,
eyeStyle: QrEyeStyle( reload: () {
eyeShape: QrEyeShape.square, loadRecentShortUrls();
color: MediaQuery.of(context).platformBrightness == Brightness.dark ? Colors.white : Colors.black, },
), showQRCode: (String url) {
dataModuleStyle: QrDataModuleStyle( setState(() {
dataModuleShape: QrDataModuleShape.square, _qrUrl = url;
color: MediaQuery.of(context).platformBrightness == Brightness.dark ? Colors.white : Colors.black, _qrCodeShown = true;
), });
) },
) isLast: index == shortUrls.length);
}
}, childCount: shortUrls.length + 1))
],
), ),
), ),
) ),
], if (_qrCodeShown)
), GestureDetector(
floatingActionButton: FloatingActionButton( onTap: () {
onPressed: () async { setState(() {
await Navigator.of(context).push(MaterialPageRoute(builder: (context) => const ShortURLEditView())); _qrCodeShown = false;
loadRecentShortUrls(); });
}, },
child: const Icon(Icons.add), 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 // stats card widget
class _ShlinkStatsCardWidget extends StatefulWidget { 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 IconData icon;
final Color? borderColor; final Color? borderColor;
@ -230,20 +269,19 @@ class _ShlinkStatsCardWidgetState extends State<_ShlinkStatsCardWidget> {
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all(color: widget.borderColor ?? randomColor), border: Border.all(color: widget.borderColor ?? randomColor),
borderRadius: BorderRadius.circular(8) borderRadius: BorderRadius.circular(8)),
),
child: SizedBox( child: SizedBox(
child: Wrap( child: Wrap(
children: [ children: [
Icon(widget.icon), Icon(widget.icon),
Padding( Padding(
padding: const EdgeInsets.only(left: 4), 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)),
) )
], ],
), ),
) )),
),
); );
} }
} }

View File

@ -4,7 +4,7 @@ import 'package:shlink_app/main.dart';
import '../globals.dart' as globals; import '../globals.dart' as globals;
class LoginView extends StatefulWidget { class LoginView extends StatefulWidget {
const LoginView({Key? key}) : super(key: key); const LoginView({super.key});
@override @override
State<LoginView> createState() => _LoginViewState(); State<LoginView> createState() => _LoginViewState();
@ -30,11 +30,11 @@ class _LoginViewState extends State<LoginView> {
_isLoggingIn = true; _isLoggingIn = true;
_errorMessage = ""; _errorMessage = "";
}); });
final connectResult = await globals.serverManager.initAndConnect(_serverUrlController.text, _apiKeyController.text); final connectResult = await globals.serverManager
.initAndConnect(_serverUrlController.text, _apiKeyController.text);
connectResult.fold((l) { connectResult.fold((l) {
Navigator.of(context).pushReplacement( Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => const InitialPage()) MaterialPageRoute(builder: (context) => const InitialPage()));
);
setState(() { setState(() {
_isLoggingIn = false; _isLoggingIn = false;
}); });
@ -44,8 +44,7 @@ class _LoginViewState extends State<LoginView> {
_errorMessage = r.detail; _errorMessage = r.detail;
_isLoggingIn = false; _isLoggingIn = false;
}); });
} } else if (r is RequestFailure) {
else if (r is RequestFailure) {
setState(() { setState(() {
_errorMessage = r.description; _errorMessage = r.description;
_isLoggingIn = false; _isLoggingIn = false;
@ -54,55 +53,58 @@ class _LoginViewState extends State<LoginView> {
}); });
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
extendBody: true, extendBody: true,
body: CustomScrollView( body: CustomScrollView(
slivers: [ slivers: [
const SliverAppBar.medium( const SliverAppBar.medium(
title: Text("Add server", style: TextStyle(fontWeight: FontWeight.bold)) title: Text("Add server",
), style: TextStyle(fontWeight: FontWeight.bold))),
SliverFillRemaining( SliverFillRemaining(
child: Padding( child: Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Padding(padding: EdgeInsets.only(bottom: 8), const Padding(
child: Text("Server URL", style: TextStyle(fontWeight: FontWeight.bold),)), padding: EdgeInsets.only(bottom: 8),
child: Text(
"Server URL",
style: TextStyle(fontWeight: FontWeight.bold),
)),
Row( Row(
children: [ children: [
const Icon(Icons.dns_outlined), const Icon(Icons.dns_outlined),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded(child: TextField( Expanded(
child: TextField(
controller: _serverUrlController, controller: _serverUrlController,
keyboardType: TextInputType.url, keyboardType: TextInputType.url,
decoration: const InputDecoration( decoration: const InputDecoration(
border: OutlineInputBorder(), border: OutlineInputBorder(),
labelText: "https://shlink.example.com" labelText: "https://shlink.example.com"),
),
)) ))
], ],
), ),
const Padding( const Padding(
padding: EdgeInsets.only(top: 8, bottom: 8), 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( Row(
children: [ children: [
const Icon(Icons.key), const Icon(Icons.key),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded(child: TextField( Expanded(
child: TextField(
controller: _apiKeyController, controller: _apiKeyController,
keyboardType: TextInputType.text, keyboardType: TextInputType.text,
obscureText: true, obscureText: true,
decoration: const InputDecoration( decoration: const InputDecoration(
border: OutlineInputBorder(), border: OutlineInputBorder(), labelText: "..."),
labelText: "..."
),
)) ))
], ],
), ),
@ -112,15 +114,16 @@ class _LoginViewState extends State<LoginView> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
FilledButton.tonal( FilledButton.tonal(
onPressed: () => { onPressed: () => {_connect()},
_connect() child: _isLoggingIn
}, ? Container(
child: _isLoggingIn ? Container( width: 34,
width: 34, height: 34,
height: 34, padding: const EdgeInsets.all(4),
padding: const EdgeInsets.all(4), child: const CircularProgressIndicator(),
child: const CircularProgressIndicator(), )
) : const Text("Connect", style: TextStyle(fontSize: 20)), : const Text("Connect",
style: TextStyle(fontSize: 20)),
) )
], ],
), ),
@ -130,17 +133,17 @@ class _LoginViewState extends State<LoginView> {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ 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))
], ],
), ),
) )
], ],
), ),
) ))
) ],
], ));
)
);
} }
} }

View File

@ -4,15 +4,18 @@ import 'package:shlink_app/views/home_view.dart';
import 'package:shlink_app/views/url_list_view.dart'; import 'package:shlink_app/views/url_list_view.dart';
class NavigationBarView extends StatefulWidget { class NavigationBarView extends StatefulWidget {
const NavigationBarView({Key? key}) : super(key: key); const NavigationBarView({super.key});
@override @override
State<NavigationBarView> createState() => _NavigationBarViewState(); State<NavigationBarView> createState() => _NavigationBarViewState();
} }
class _NavigationBarViewState extends State<NavigationBarView> { class _NavigationBarViewState extends State<NavigationBarView> {
final List<Widget> views = [
final List<Widget> views = [const HomeView(), const URLListView(), const SettingsView()]; const HomeView(),
const URLListView(),
const SettingsView()
];
int _selectedView = 0; int _selectedView = 0;
@override @override

View File

@ -16,55 +16,68 @@ class _OpenSourceLicensesViewState extends State<OpenSourceLicensesView> {
body: CustomScrollView( body: CustomScrollView(
slivers: [ slivers: [
const SliverAppBar.medium( const SliverAppBar.medium(
expandedHeight: 120, expandedHeight: 120,
title: Text("Open Source Licenses", style: TextStyle(fontWeight: FontWeight.bold),) title: Text(
), "Open Source Licenses",
style: TextStyle(fontWeight: FontWeight.bold),
)),
SliverList( SliverList(
delegate: SliverChildBuilderDelegate( delegate:
(BuildContext context, int index) { SliverChildBuilderDelegate((BuildContext context, int index) {
final currentLicense = LicenseUtil.getLicenses()[index]; final currentLicense = LicenseUtil.getLicenses()[index];
return GestureDetector( return GestureDetector(
onTap: () async { onTap: () async {
if (currentLicense.repository != null) { if (currentLicense.repository != null) {
if (await canLaunchUrl(Uri.parse(currentLicense.repository ?? ""))) { if (await canLaunchUrl(
launchUrl(Uri.parse(currentLicense.repository ?? ""), mode: LaunchMode.externalApplication); 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( child: Padding(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.only(
child: Container( left: 12, right: 12, top: 20, bottom: 20),
decoration: BoxDecoration( child: Column(
borderRadius: BorderRadius.circular(8), crossAxisAlignment: CrossAxisAlignment.start,
color: Theme.of(context).brightness == Brightness.light ? Colors.grey[100] : Colors.grey[900], children: [
), Text(currentLicense.name,
child: Padding( style: const TextStyle(
padding: const EdgeInsets.only(left: 12, right: 12, top: 20, bottom: 20), fontWeight: FontWeight.bold, fontSize: 18)),
child: Column( Text("Version: ${currentLicense.version ?? "N/A"}",
crossAxisAlignment: CrossAxisAlignment.start, style: const TextStyle(color: Colors.grey)),
children: [ const SizedBox(height: 8),
Text(currentLicense.name, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18)), const Divider(),
Text("Version: ${currentLicense.version ?? "N/A"}", style: const TextStyle(color: Colors.grey)), const SizedBox(height: 8),
const SizedBox(height: 8), Text(currentLicense.license,
const Divider(), textAlign: TextAlign.justify,
const SizedBox(height: 8), style: const TextStyle(color: Colors.grey)),
Text(currentLicense.license, textAlign: TextAlign.justify, style: const TextStyle(color: Colors.grey)), ],
],
),
),
), ),
), ),
); ),
}, ),
childCount: LicenseUtil.getLicenses().length );
), }, childCount: LicenseUtil.getLicenses().length),
), ),
const SliverToBoxAdapter( const SliverToBoxAdapter(
child: Padding( child: Padding(
padding: EdgeInsets.only(top: 8, bottom: 20), 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: Text(
) "Thank you to all maintainers of these repositories 💝",
) style: TextStyle(color: Colors.grey),
textAlign: TextAlign.center,
),
))
], ],
), ),
); );

View File

@ -13,24 +13,19 @@ class SettingsView extends StatefulWidget {
State<SettingsView> createState() => _SettingsViewState(); State<SettingsView> createState() => _SettingsViewState();
} }
enum ServerStatus { enum ServerStatus { connected, connecting, disconnected }
connected,
connecting,
disconnected
}
class _SettingsViewState extends State<SettingsView> { class _SettingsViewState extends State<SettingsView> {
var _serverVersion = "---"; var _serverVersion = "---";
ServerStatus _serverStatus = ServerStatus.connecting; ServerStatus _serverStatus = ServerStatus.connecting;
PackageInfo packageInfo = PackageInfo(appName: "", packageName: "", version: "", buildNumber: ""); PackageInfo packageInfo =
PackageInfo(appName: "", packageName: "", version: "", buildNumber: "");
@override @override
void initState() { void initState() {
// TODO: implement initState // TODO: implement initState
super.initState(); super.initState();
WidgetsBinding.instance WidgetsBinding.instance.addPostFrameCallback((_) => getServerHealth());
.addPostFrameCallback((_) => getServerHealth());
} }
void getServerHealth() async { void getServerHealth() async {
@ -52,12 +47,14 @@ class _SettingsViewState extends State<SettingsView> {
var text = ""; var text = "";
if (r is RequestFailure) { if (r is RequestFailure) {
text = r.description; text = r.description;
} } else {
else {
text = (r as ApiFailure).detail; 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); ScaffoldMessenger.of(context).showSnackBar(snackBar);
}); });
} }
@ -65,68 +62,89 @@ class _SettingsViewState extends State<SettingsView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: CustomScrollView( body: CustomScrollView(
slivers: [ slivers: [
SliverAppBar.medium( SliverAppBar.medium(
expandedHeight: 120, expandedHeight: 120,
title: const Text("Settings", style: TextStyle(fontWeight: FontWeight.bold),), title: const Text(
actions: [ "Settings",
PopupMenuButton( style: TextStyle(fontWeight: FontWeight.bold),
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( actions: [
child: Padding( 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), padding: const EdgeInsets.all(12.0),
child: Column( child: Column(
children: [ children: [
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8), 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: Padding( child: Padding(
padding: const EdgeInsets.all(12.0), padding: const EdgeInsets.all(12.0),
child: Row( child: Row(
children: [ children: [
Icon(Icons.dns_outlined, color: (() { Icon(Icons.dns_outlined,
switch (_serverStatus) { color: (() {
case ServerStatus.connected: switch (_serverStatus) {
return Colors.green; case ServerStatus.connected:
case ServerStatus.connecting: return Colors.green;
return Colors.orange; case ServerStatus.connecting:
case ServerStatus.disconnected: return Colors.orange;
return Colors.red; case ServerStatus.disconnected:
} return Colors.red;
}())), }
}())),
const SizedBox(width: 8), const SizedBox(width: 8),
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Text("Connected to", style: TextStyle(color: Colors.grey)), const Text("Connected to",
Text(globals.serverManager.getServerUrl(), style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), style: TextStyle(color: Colors.grey)),
Text(globals.serverManager.getServerUrl(),
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16)),
Row( Row(
children: [ children: [
const Text("API Version: ", style: TextStyle(color: Colors.grey, fontWeight: FontWeight.w600)), const Text("API Version: ",
Text(globals.serverManager.getApiVersion(), style: const TextStyle(color: Colors.grey)), style: TextStyle(
color: Colors.grey,
fontWeight: FontWeight.w600)),
Text(globals.serverManager.getApiVersion(),
style:
const TextStyle(color: Colors.grey)),
const SizedBox(width: 16), const SizedBox(width: 16),
const Text("Server Version: ", style: TextStyle(color: Colors.grey, fontWeight: FontWeight.w600)), const Text("Server Version: ",
Text(_serverVersion, style: const TextStyle(color: Colors.grey)) 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), const SizedBox(height: 8),
GestureDetector( GestureDetector(
onTap: () { onTap: () {
Navigator.of(context).push( Navigator.of(context).push(MaterialPageRoute(
MaterialPageRoute(builder: (context) => const OpenSourceLicensesView()) builder: (context) =>
); const OpenSourceLicensesView()));
}, },
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8), 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( 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( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
@ -158,50 +179,21 @@ class _SettingsViewState extends State<SettingsView> {
children: [ children: [
Icon(Icons.policy_outlined), Icon(Icons.policy_outlined),
SizedBox(width: 8), 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) Icon(Icons.chevron_right)
] ]),
),
), ),
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
GestureDetector( GestureDetector(
onTap: () async { onTap: () async {
var url = Uri.parse("https://github.com/rainloreley/shlink-mobile-app"); var url = Uri.parse(
if (await canLaunchUrl(url)) { "https://github.com/rainloreley/shlink-mobile-app");
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");
if (await canLaunchUrl(url)) { if (await canLaunchUrl(url)) {
launchUrl(url, mode: LaunchMode.externalApplication); launchUrl(url, mode: LaunchMode.externalApplication);
} }
@ -209,10 +201,49 @@ class _SettingsViewState extends State<SettingsView> {
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8), 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( 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( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
@ -220,12 +251,13 @@ class _SettingsViewState extends State<SettingsView> {
children: [ children: [
Icon(Icons.lock), Icon(Icons.lock),
SizedBox(width: 8), SizedBox(width: 8),
Text("Privacy Policy", style: TextStyle(fontWeight: FontWeight.w500)), Text("Privacy Policy",
style: TextStyle(
fontWeight: FontWeight.w500)),
], ],
), ),
Icon(Icons.chevron_right) Icon(Icons.chevron_right)
] ]),
),
), ),
), ),
), ),
@ -234,14 +266,16 @@ class _SettingsViewState extends State<SettingsView> {
Row( Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ 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),
),
],
) )
], ],
) )),
), )
) ],
], ));
)
);
} }
} }

View File

@ -11,8 +11,8 @@ class ShortURLEditView extends StatefulWidget {
State<ShortURLEditView> createState() => _ShortURLEditViewState(); State<ShortURLEditView> createState() => _ShortURLEditViewState();
} }
class _ShortURLEditViewState extends State<ShortURLEditView> with SingleTickerProviderStateMixin { class _ShortURLEditViewState extends State<ShortURLEditView>
with SingleTickerProviderStateMixin {
final longUrlController = TextEditingController(); final longUrlController = TextEditingController();
final customSlugController = TextEditingController(); final customSlugController = TextEditingController();
final titleController = TextEditingController(); final titleController = TextEditingController();
@ -51,13 +51,17 @@ class _ShortURLEditViewState extends State<ShortURLEditView> with SingleTickerPr
void _submitShortUrl() async { void _submitShortUrl() async {
var newSubmission = ShortURLSubmission( var newSubmission = ShortURLSubmission(
longUrl: longUrlController.text, longUrl: longUrlController.text,
deviceLongUrls: null, tags: [], deviceLongUrls: null,
tags: [],
crawlable: isCrawlable, crawlable: isCrawlable,
forwardQuery: forwardQuery, forwardQuery: forwardQuery,
findIfExists: true, findIfExists: true,
title: titleController.text != "" ? titleController.text : null, title: titleController.text != "" ? titleController.text : null,
customSlug: customSlugController.text != "" && !randomSlug ? customSlugController.text : null, customSlug: customSlugController.text != "" && !randomSlug
shortCodeLength: randomSlug ? int.parse(randomSlugLengthController.text) : null); ? customSlugController.text
: null,
shortCodeLength:
randomSlug ? int.parse(randomSlugLengthController.text) : null);
var response = await globals.serverManager.submitShortUrl(newSubmission); var response = await globals.serverManager.submitShortUrl(newSubmission);
response.fold((l) async { response.fold((l) async {
@ -67,11 +71,16 @@ class _ShortURLEditViewState extends State<ShortURLEditView> with SingleTickerPr
if (copyToClipboard) { if (copyToClipboard) {
await Clipboard.setData(ClipboardData(text: l)); 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); ScaffoldMessenger.of(context).showSnackBar(snackBar);
} } else {
else { final snackBar = SnackBar(
final snackBar = SnackBar(content: const Text("Short URL created!"), backgroundColor: Colors.green[400], behavior: SnackBarBehavior.floating); content: const Text("Short URL created!"),
backgroundColor: Colors.green[400],
behavior: SnackBarBehavior.floating);
ScaffoldMessenger.of(context).showSnackBar(snackBar); ScaffoldMessenger.of(context).showSnackBar(snackBar);
} }
Navigator.pop(context); Navigator.pop(context);
@ -86,38 +95,39 @@ class _ShortURLEditViewState extends State<ShortURLEditView> with SingleTickerPr
if (r is RequestFailure) { if (r is RequestFailure) {
text = r.description; text = r.description;
} } else {
else {
text = (r as ApiFailure).detail; text = (r as ApiFailure).detail;
if ((r).invalidElements != null) { if ((r).invalidElements != null) {
text = "$text: ${(r).invalidElements}"; 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); ScaffoldMessenger.of(context).showSnackBar(snackBar);
return false; return false;
}); });
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: CustomScrollView( body: CustomScrollView(
slivers: [ slivers: [
const SliverAppBar.medium( const SliverAppBar.medium(
title: Text("New Short URL", style: TextStyle(fontWeight: FontWeight.bold)), title: Text("New Short URL",
style: TextStyle(fontWeight: FontWeight.bold)),
), ),
SliverToBoxAdapter( SliverToBoxAdapter(
child: Padding( child: Padding(
padding: const EdgeInsets.only(left: 16, right: 16, top: 16), padding: const EdgeInsets.only(left: 16, right: 16, top: 16),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.center, children: [
children: [ TextField(
TextField( controller: longUrlController,
controller: longUrlController, decoration: InputDecoration(
decoration: InputDecoration(
errorText: longUrlError != "" ? longUrlError : null, errorText: longUrlError != "" ? longUrlError : null,
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
label: const Row( label: const Row(
@ -126,87 +136,99 @@ class _ShortURLEditViewState extends State<ShortURLEditView> with SingleTickerPr
SizedBox(width: 8), SizedBox(width: 8),
Text("Long URL") Text("Long URL")
], ],
) )),
), ),
), const SizedBox(height: 16),
const SizedBox(height: 16), Row(
Row( children: [
children: [ Expanded(
Expanded( child: TextField(
child: TextField( controller: customSlugController,
controller: customSlugController, style: TextStyle(
style: TextStyle(color: randomSlug ? Colors.grey : Theme.of(context).brightness == Brightness.light ? Colors.black : Colors.white), color: randomSlug
onChanged: (_) { ? Colors.grey
if (randomSlug) { : Theme.of(context).brightness ==
setState(() { Brightness.light
? Colors.black
: Colors.white),
onChanged: (_) {
if (randomSlug) {
setState(() {
randomSlug = false; randomSlug = false;
}); });
} }
}, },
decoration: InputDecoration( decoration: InputDecoration(
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
label: Row( label: Row(
children: [ children: [
const Icon(Icons.link), const Icon(Icons.link),
const SizedBox(width: 8), 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( const SizedBox(width: 8),
turns: Tween(begin: 0.0, end: 3.0).animate(CurvedAnimation(parent: _customSlugDiceAnimationController, curve: Curves.easeInOutExpo)), RotationTransition(
child: IconButton( turns: Tween(begin: 0.0, end: 3.0).animate(
onPressed: () { CurvedAnimation(
if (randomSlug) { parent: _customSlugDiceAnimationController,
_customSlugDiceAnimationController.reverse(from: 1); curve: Curves.easeInOutExpo)),
} child: IconButton(
else { onPressed: () {
_customSlugDiceAnimationController.forward(from: 0); if (randomSlug) {
} _customSlugDiceAnimationController.reverse(
setState(() { from: 1);
randomSlug = !randomSlug; } else {
}); _customSlugDiceAnimationController.forward(
from: 0);
}, }
icon: Icon(randomSlug ? Icons.casino : Icons.casino_outlined, color: randomSlug ? Colors.green : Colors.grey) 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),
const SizedBox(height: 16), TextField(
controller: titleController,
if (randomSlug) decoration: const InputDecoration(
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(
border: OutlineInputBorder(), border: OutlineInputBorder(),
label: Row( label: Row(
children: [ children: [
@ -214,89 +236,91 @@ class _ShortURLEditViewState extends State<ShortURLEditView> with SingleTickerPr
SizedBox(width: 8), SizedBox(width: 8),
Text("Title") Text("Title")
], ],
) )),
), ),
), const SizedBox(height: 16),
const SizedBox(height: 16), Row(
Row( mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
children: [ const Text("Crawlable"),
const Text("Crawlable"), Switch(
Switch( value: isCrawlable,
value: isCrawlable, onChanged: (_) {
onChanged: (_) { setState(() {
setState(() { isCrawlable = !isCrawlable;
isCrawlable = !isCrawlable; });
}); },
}, )
) ],
], ),
), const SizedBox(height: 16),
const SizedBox(height: 16), Row(
Row( mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
children: [ const Text("Forward query params"),
const Text("Forward query params"), Switch(
Switch( value: forwardQuery,
value: forwardQuery, onChanged: (_) {
onChanged: (_) { setState(() {
setState(() { forwardQuery = !forwardQuery;
forwardQuery = !forwardQuery; });
}); },
}, )
) ],
], ),
), const SizedBox(height: 16),
const SizedBox(height: 16), Row(
Row( mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
children: [ const Text("Copy to clipboard"),
const Text("Copy to clipboard"), Switch(
Switch( value: copyToClipboard,
value: copyToClipboard, onChanged: (_) {
onChanged: (_) { setState(() {
setState(() { copyToClipboard = !copyToClipboard;
copyToClipboard = !copyToClipboard; });
}); },
}, )
) ],
], ),
), ],
], ),
), ))
)
)
], ],
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
onPressed: () { onPressed: () {
if (!isSaving) { if (!isSaving) {
setState(() {
isSaving = true;
longUrlError = "";
randomSlugLengthError = "";
});
if (longUrlController.text == "") {
setState(() { setState(() {
longUrlError = "URL cannot be empty"; isSaving = true;
isSaving = false; 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(() { child: isSaving
randomSlugLengthError = "invalid number"; ? const Padding(
isSaving = false; padding: EdgeInsets.all(16),
}); child: CircularProgressIndicator(strokeWidth: 3))
return; : const Icon(Icons.save)),
}
else {
_submitShortUrl();
}
}
},
child: isSaving ? const Padding(padding: EdgeInsets.all(16), child: CircularProgressIndicator(strokeWidth: 3)) : const Icon(Icons.save)
),
); );
} }
} }

View File

@ -15,7 +15,6 @@ class URLDetailView extends StatefulWidget {
} }
class _URLDetailViewState extends State<URLDetailView> { class _URLDetailViewState extends State<URLDetailView> {
Future showDeletionConfirmation() { Future showDeletionConfirmation() {
return showDialog( return showDialog(
context: context, context: context,
@ -27,58 +26,75 @@ class _URLDetailViewState extends State<URLDetailView> {
children: [ children: [
const Text("You're about to delete"), const Text("You're about to delete"),
const SizedBox(height: 4), 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 SizedBox(height: 4),
const Text("It'll be gone forever! (a very long time)") const Text("It'll be gone forever! (a very long time)")
], ],
), ),
), ),
actions: [ actions: [
TextButton(onPressed: () => { Navigator.of(context).pop() }, child: const Text("Cancel")), TextButton(
onPressed: () => {Navigator.of(context).pop()},
child: const Text("Cancel")),
TextButton( TextButton(
onPressed: () async { onPressed: () async {
var response = await globals.serverManager.deleteShortUrl(widget.shortURL.shortCode); var response = await globals.serverManager
.deleteShortUrl(widget.shortURL.shortCode);
response.fold((l) { response.fold((l) {
Navigator.pop(context); Navigator.pop(context);
Navigator.pop(context, "reload"); 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); ScaffoldMessenger.of(context).showSnackBar(snackBar);
return true; return true;
}, (r) { }, (r) {
var text = ""; var text = "";
if (r is RequestFailure) { if (r is RequestFailure) {
text = r.description; text = r.description;
} } else {
else {
text = (r as ApiFailure).detail; 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); ScaffoldMessenger.of(context).showSnackBar(snackBar);
return false; return false;
}); });
}, },
child: const Text("Delete", style: TextStyle(color: Colors.red)), child:
const Text("Delete", style: TextStyle(color: Colors.red)),
) )
], ],
); );
}); });
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: CustomScrollView( body: CustomScrollView(
slivers: [ slivers: [
SliverAppBar.medium( 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: [ actions: [
IconButton(onPressed: () { IconButton(
showDeletionConfirmation(); onPressed: () {
}, icon: const Icon(Icons.delete, color: Colors.red,)) showDeletionConfirmation();
},
icon: const Icon(
Icons.delete,
color: Colors.red,
))
], ],
), ),
SliverToBoxAdapter( SliverToBoxAdapter(
@ -86,41 +102,78 @@ class _URLDetailViewState extends State<URLDetailView> {
padding: const EdgeInsets.only(left: 16.0, right: 16.0), padding: const EdgeInsets.only(left: 16.0, right: 16.0),
child: Wrap( child: Wrap(
children: widget.shortURL.tags.map((tag) { children: widget.shortURL.tags.map((tag) {
var randomColor = ([...Colors.primaries]..shuffle()).first.harmonizeWith(Theme.of(context).colorScheme.primary); var randomColor = ([...Colors.primaries]..shuffle())
return Padding( .first
padding: const EdgeInsets.only(right: 4, top: 4), .harmonizeWith(Theme.of(context).colorScheme.primary);
child: Container( return Padding(
padding: const EdgeInsets.only(top: 4, bottom: 4, left: 12, right: 12), padding: const EdgeInsets.only(right: 4, top: 4),
decoration: BoxDecoration( child: Container(
borderRadius: BorderRadius.circular(4), padding: const EdgeInsets.only(
color: randomColor, top: 4, bottom: 4, left: 12, right: 12),
), decoration: BoxDecoration(
child: Text(tag, style: TextStyle(color: randomColor.computeLuminance() < 0.5 ? Colors.white : Colors.black),), borderRadius: BorderRadius.circular(4),
), color: randomColor,
); ),
}).toList() 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 Code", content: widget.shortURL.shortCode),
_ListCell(title: "Short URL", content: widget.shortURL.shortUrl), _ListCell(title: "Short URL", content: widget.shortURL.shortUrl),
_ListCell(title: "Long URL", content: widget.shortURL.longUrl), _ListCell(title: "Long URL", content: widget.shortURL.longUrl),
_ListCell(title: "iOS", content: widget.shortURL.deviceLongUrls.ios, sub: true), _ListCell(
_ListCell(title: "Android", content: widget.shortURL.deviceLongUrls.android, sub: true), title: "iOS",
_ListCell(title: "Desktop", content: widget.shortURL.deviceLongUrls.desktop, sub: true), content: widget.shortURL.deviceLongUrls.ios,
_ListCell(title: "Creation Date", content: widget.shortURL.dateCreated), 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: ""), const _ListCell(title: "Visits", content: ""),
_ListCell(title: "Total", content: widget.shortURL.visitsSummary.total, sub: true), _ListCell(
_ListCell(title: "Non-Bots", content: widget.shortURL.visitsSummary.nonBots, sub: true), title: "Total",
_ListCell(title: "Bots", content: widget.shortURL.visitsSummary.bots, sub: true), 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: ""), const _ListCell(title: "Meta", content: ""),
_ListCell(title: "Valid Since", content: widget.shortURL.meta.validSince, sub: true), _ListCell(
_ListCell(title: "Valid Until", content: widget.shortURL.meta.validUntil, sub: true), title: "Valid Since",
_ListCell(title: "Max Visits", content: widget.shortURL.meta.maxVisits, sub: true), 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: "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 { 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 String title;
final dynamic content; final dynamic content;
@ -143,51 +200,66 @@ class _ListCellState extends State<_ListCell> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SliverToBoxAdapter( return SliverToBoxAdapter(
child: Padding( child: Padding(
padding: EdgeInsets.only(top: 16, bottom: widget.last ? 30 : 0), padding: EdgeInsets.only(top: 16, bottom: widget.last ? 30 : 0),
child: Container( child: Container(
padding: const EdgeInsets.only(top: 16, left: 8, right: 8), padding: const EdgeInsets.only(top: 16, left: 8, right: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border(top: BorderSide(width: 1, color: MediaQuery.of(context).platformBrightness == Brightness.dark ? Colors.grey[800]! : Colors.grey[300]!)), border: Border(
), top: BorderSide(
child: Row( color: MediaQuery.of(context).platformBrightness ==
mainAxisAlignment: MainAxisAlignment.spaceBetween, Brightness.dark
children: [ ? Colors.grey[800]!
Row( : Colors.grey[300]!)),
children: [ ),
if (widget.sub) child: Row(
Padding( mainAxisAlignment: MainAxisAlignment.spaceBetween,
padding: const EdgeInsets.only(right: 4), children: [
child: SizedBox( Row(
width: 20, children: [
height: 6, if (widget.sub)
child: Container( Padding(
decoration: BoxDecoration( padding: const EdgeInsets.only(right: 4),
borderRadius: BorderRadius.circular(8), child: SizedBox(
color: Theme.of(context).brightness == Brightness.dark ? Colors.grey[700] : Colors.grey[300], 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),)], ),
), Text(
if (widget.content is bool) widget.title,
Icon(widget.content ? Icons.check : Icons.close, color: widget.content ? Colors.green : Colors.red) style: const TextStyle(fontWeight: FontWeight.bold),
else if (widget.content is int) )
Text(widget.content.toString()) ],
else if (widget.content is String) ),
Expanded( if (widget.content is bool)
child: Text(widget.content, textAlign: TextAlign.end, overflow: TextOverflow.ellipsis, maxLines: 1,), Icon(widget.content ? Icons.check : Icons.close,
) color: widget.content ? Colors.green : Colors.red)
else if (widget.content is DateTime) else if (widget.content is int)
Text(DateFormat('yyyy-MM-dd - HH:mm').format(widget.content)) Text(widget.content.toString())
else else if (widget.content is String)
const Text("N/A") 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")
],
), ),
) ),
); ));
} }
} }

View File

@ -9,26 +9,24 @@ import '../globals.dart' as globals;
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
class URLListView extends StatefulWidget { class URLListView extends StatefulWidget {
const URLListView({Key? key}) : super(key: key); const URLListView({super.key});
@override @override
State<URLListView> createState() => _URLListViewState(); State<URLListView> createState() => _URLListViewState();
} }
class _URLListViewState extends State<URLListView> { class _URLListViewState extends State<URLListView> {
List<ShortURL> shortUrls = []; List<ShortURL> shortUrls = [];
bool _qrCodeShown = false; bool _qrCodeShown = false;
String _qrUrl = ""; String _qrUrl = "";
bool shortUrlsLoaded = false; bool shortUrlsLoaded = false;
@override @override
void initState() { void initState() {
// TODO: implement initState // TODO: implement initState
super.initState(); super.initState();
WidgetsBinding.instance WidgetsBinding.instance.addPostFrameCallback((_) => loadAllShortUrls());
.addPostFrameCallback((_) => loadAllShortUrls());
} }
Future<void> loadAllShortUrls() async { Future<void> loadAllShortUrls() async {
@ -43,120 +41,144 @@ class _URLListViewState extends State<URLListView> {
var text = ""; var text = "";
if (r is RequestFailure) { if (r is RequestFailure) {
text = r.description; text = r.description;
} } else {
else {
text = (r as ApiFailure).detail; 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); ScaffoldMessenger.of(context).showSnackBar(snackBar);
return false; return false;
}); });
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
onPressed: () async { onPressed: () async {
await Navigator.of(context).push(MaterialPageRoute(builder: (context) => const ShortURLEditView())); await Navigator.of(context).push(MaterialPageRoute(
loadAllShortUrls(); builder: (context) => const ShortURLEditView()));
}, loadAllShortUrls();
child: const Icon(Icons.add), },
), child: const Icon(Icons.add),
body: Stack( ),
children: [ body: Stack(
ColorFiltered( children: [
colorFilter: ColorFilter.mode(Colors.black.withOpacity(_qrCodeShown ? 0.4 : 0), BlendMode.srcOver), ColorFiltered(
child: RefreshIndicator( colorFilter: ColorFilter.mode(
onRefresh: () async { Colors.black.withOpacity(_qrCodeShown ? 0.4 : 0),
return loadAllShortUrls(); BlendMode.srcOver),
}, child: RefreshIndicator(
child: CustomScrollView( onRefresh: () async {
slivers: [ return loadAllShortUrls();
const SliverAppBar.medium( },
title: Text("Short URLs", style: TextStyle(fontWeight: FontWeight.bold)) child: CustomScrollView(
), slivers: [
if (shortUrlsLoaded && shortUrls.isEmpty) const SliverAppBar.medium(
SliverToBoxAdapter( title: Text("Short URLs",
child: Center( style: TextStyle(fontWeight: FontWeight.bold))),
child: Padding( if (shortUrlsLoaded && shortUrls.isEmpty)
padding: const EdgeInsets.only(top: 50), SliverToBoxAdapter(
child: Column( child: Center(
children: [ child: Padding(
const Text("No Short URLs", style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),), padding: const EdgeInsets.only(top: 50),
Padding( child: Column(
padding: const EdgeInsets.only(top: 8), children: [
child: Text('Create one by tapping the "+" button below', style: TextStyle(fontSize: 16, color: Colors.grey[600]),), const Text(
) "No Short URLs",
], style: TextStyle(
) fontSize: 24,
) fontWeight: FontWeight.bold),
) ),
) Padding(
else padding: const EdgeInsets.only(top: 8),
SliverList(delegate: SliverChildBuilderDelegate( child: Text(
(BuildContext context, int index) { 'Create one by tapping the "+" button below',
final shortURL = shortUrls[index]; style: TextStyle(
return ShortURLCell(shortURL: shortURL, reload: () { fontSize: 16,
loadAllShortUrls(); color: Colors.grey[600]),
}, showQRCode: (String url) { ),
setState(() { )
_qrUrl = url; ],
_qrCodeShown = true; ))))
}); else
}, isLast: index == shortUrls.length - 1); SliverList(
}, delegate: SliverChildBuilderDelegate(
childCount: shortUrls.length (BuildContext context, int index) {
)) final shortURL = shortUrls[index];
], return ShortURLCell(
), shortURL: shortURL,
), reload: () {
), loadAllShortUrls();
if (_qrCodeShown) },
GestureDetector( showQRCode: (String url) {
onTap: () { setState(() {
setState(() { _qrUrl = url;
_qrCodeShown = false; _qrCodeShown = true;
}); });
}, },
child: Container( isLast: index == shortUrls.length - 1);
color: Colors.black.withOpacity(0), }, childCount: shortUrls.length))
), ],
),
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,
),
)
)
), ),
), ),
) ),
], 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 { 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 ShortURL shortURL;
final Function() reload; final Function() reload;
@ -172,63 +194,93 @@ class _ShortURLCellState extends State<ShortURLCell> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return GestureDetector(
onTap: () async { 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") { if (result == "reload") {
widget.reload(); widget.reload();
} }
}, },
child: Padding( 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( child: Container(
padding: const EdgeInsets.only(left: 8, right: 8, bottom: 16, top: 16), padding:
decoration: BoxDecoration( const EdgeInsets.only(left: 8, right: 8, bottom: 16, top: 16),
border: Border(bottom: BorderSide(color: MediaQuery.of(context).platformBrightness == Brightness.dark ? Colors.grey[800]! : Colors.grey[300]!)), decoration: BoxDecoration(
), border: Border(
child: Row( bottom: BorderSide(
mainAxisAlignment: MainAxisAlignment.spaceBetween, color: MediaQuery.of(context).platformBrightness ==
children: [ Brightness.dark
Expanded( ? Colors.grey[800]!
child: Column( : Colors.grey[300]!)),
crossAxisAlignment: CrossAxisAlignment.start, ),
mainAxisAlignment: MainAxisAlignment.start, child: Row(
children: [ mainAxisAlignment: MainAxisAlignment.spaceBetween,
Text(widget.shortURL.title ?? widget.shortURL.shortCode, textScaleFactor: 1.4, style: const TextStyle(fontWeight: FontWeight.bold),), children: [
Text(widget.shortURL.longUrl,maxLines: 1, overflow: TextOverflow.ellipsis, textScaleFactor: 0.9, style: TextStyle(color: Colors.grey[600]),), Expanded(
// List tags in a row child: Column(
Wrap( crossAxisAlignment: CrossAxisAlignment.start,
children: widget.shortURL.tags.map((tag) { children: [
var randomColor = ([...Colors.primaries]..shuffle()).first.harmonizeWith(Theme.of(context).colorScheme.primary); Text(
return Padding( widget.shortURL.title ?? widget.shortURL.shortCode,
padding: const EdgeInsets.only(right: 4, top: 4), textScaleFactor: 1.4,
child: Container( style: const TextStyle(fontWeight: FontWeight.bold),
padding: const EdgeInsets.only(top: 4, bottom: 4, left: 12, right: 12), ),
decoration: BoxDecoration( Text(
borderRadius: BorderRadius.circular(4), widget.shortURL.longUrl,
color: randomColor, maxLines: 1,
), overflow: TextOverflow.ellipsis,
child: Text(tag, style: TextStyle(color: randomColor.computeLuminance() < 0.5 ? Colors.white : Colors.black),), 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(
}).toList() tag,
style: TextStyle(
) color: randomColor.computeLuminance() < 0.5
], ? Colors.white
: Colors.black),
),
),
);
}).toList())
],
),
), ),
), IconButton(
IconButton(onPressed: () async { onPressed: () async {
await Clipboard.setData(ClipboardData(text: widget.shortURL.shortUrl)); await Clipboard.setData(
final snackBar = SnackBar(content: const Text("Copied to clipboard!"), behavior: SnackBarBehavior.floating, backgroundColor: Colors.green[400]); ClipboardData(text: widget.shortURL.shortUrl));
ScaffoldMessenger.of(context).showSnackBar(snackBar); final snackBar = SnackBar(
}, icon: const Icon(Icons.copy)), content: const Text("Copied to clipboard!"),
IconButton(onPressed: () { behavior: SnackBarBehavior.floating,
widget.showQRCode(widget.shortURL.shortUrl); backgroundColor: Colors.green[400]);
}, icon: const Icon(Icons.qr_code)) ScaffoldMessenger.of(context).showSnackBar(snackBar);
], },
) icon: const Icon(Icons.copy)),
), IconButton(
) onPressed: () {
); widget.showQRCode(widget.shortURL.shortUrl);
},
icon: const Icon(Icons.qr_code))
],
)),
));
} }
} }

View File

@ -155,10 +155,10 @@ packages:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: flutter_lints name: flutter_lints
sha256: "2118df84ef0c3ca93f96123a616ae8540879991b8b57af2f81b76a7ada49b2a4" sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.2" version: "3.0.1"
flutter_process_text: flutter_process_text:
dependency: "direct main" dependency: "direct main"
description: description:
@ -285,10 +285,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: lints name: lints
sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.1" version: "3.0.0"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:

View File

@ -52,7 +52,7 @@ dev_dependencies:
url: https://github.com/OutdatedGuy/flutter_launcher_icons.git url: https://github.com/OutdatedGuy/flutter_launcher_icons.git
ref: feat/monochrome-icons-support ref: feat/monochrome-icons-support
flutter_lints: ^2.0.2 flutter_lints: ^3.0.1
flutter: flutter:
uses-material-design: true uses-material-design: true