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.
include: package:flutter_lints/flutter.yaml
linter:
rules:
# Style rules
- camel_case_types
- library_names
- avoid_catching_errors
- avoid_empty_else
- unnecessary_brace_in_string_interps
- avoid_redundant_argument_values
- leading_newlines_in_multiline_strings
# formatting
- lines_longer_than_80_chars
- curly_braces_in_flow_control_structures
# doc comments
- slash_for_doc_comments
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
@ -21,7 +37,6 @@ linter:
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,25 +5,28 @@ import 'package:http/http.dart' as http;
import '../server_manager.dart';
/// Tries to connect to the Shlink server
FutureOr<Either<String, Failure>> apiConnect(String? apiKey, String? serverUrl, String apiVersion) async {
FutureOr<Either<String, Failure>> apiConnect(
String? apiKey, String? serverUrl, String apiVersion) async {
try {
final response = await http.get(Uri.parse("$serverUrl/rest/v$apiVersion/short-urls"), headers: {
final response = await http
.get(Uri.parse("$serverUrl/rest/v$apiVersion/short-urls"), headers: {
"X-Api-Key": apiKey ?? "",
});
if (response.statusCode == 200) {
return left("");
}
else {
} else {
try {
var jsonBody = jsonDecode(response.body);
return right(ApiFailure(type: jsonBody["type"], detail: jsonBody["detail"], title: jsonBody["title"], status: jsonBody["status"]));
}
catch(resErr) {
return right(ApiFailure(
type: jsonBody["type"],
detail: jsonBody["detail"],
title: jsonBody["title"],
status: jsonBody["status"]));
} catch (resErr) {
return right(RequestFailure(response.statusCode, resErr.toString()));
}
}
}
catch(reqErr) {
} catch (reqErr) {
return right(RequestFailure(0, reqErr.toString()));
}
}
}

View File

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

View File

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

View File

@ -5,26 +5,30 @@ import 'package:http/http.dart' as http;
import '../server_manager.dart';
/// Gets the status of the server and health information
FutureOr<Either<ServerHealthResponse, Failure>> apiGetServerHealth(String? apiKey, String? serverUrl, String apiVersion) async {
FutureOr<Either<ServerHealthResponse, Failure>> apiGetServerHealth(
String? apiKey, String? serverUrl, String apiVersion) async {
try {
final response = await http.get(Uri.parse("$serverUrl/rest/v$apiVersion/health"), headers: {
final response = await http
.get(Uri.parse("$serverUrl/rest/v$apiVersion/health"), headers: {
"X-Api-Key": apiKey ?? "",
});
if (response.statusCode == 200) {
var jsonData = jsonDecode(response.body);
return left(ServerHealthResponse(status: jsonData["status"], version: jsonData["version"]));
}
else {
return left(ServerHealthResponse(
status: jsonData["status"], version: jsonData["version"]));
} else {
try {
var jsonBody = jsonDecode(response.body);
return right(ApiFailure(type: jsonBody["type"], detail: jsonBody["detail"], title: jsonBody["title"], status: jsonBody["status"]));
}
catch(resErr) {
return right(ApiFailure(
type: jsonBody["type"],
detail: jsonBody["detail"],
title: jsonBody["title"],
status: jsonBody["status"]));
} catch (resErr) {
return right(RequestFailure(response.statusCode, resErr.toString()));
}
}
}
catch(reqErr) {
} catch (reqErr) {
return right(RequestFailure(0, reqErr.toString()));
}
}
}

View File

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

View File

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

View File

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

View File

@ -15,7 +15,6 @@ import 'Methods/delete_short_url.dart';
import 'Methods/submit_short_url.dart';
class ServerManager {
/// The URL of the Shlink server
String? serverUrl;
@ -69,9 +68,9 @@ class ServerManager {
storage.write(key: "shlink_apikey", value: apiKey);
}
/// Saves provided server credentials and tries to establish a connection
FutureOr<Either<String, Failure>> initAndConnect(String url, String apiKey) async {
FutureOr<Either<String, Failure>> initAndConnect(
String url, String apiKey) async {
// TODO: convert url to correct format
serverUrl = url;
this.apiKey = apiKey;
@ -100,7 +99,8 @@ class ServerManager {
}
/// Saves a new short URL to the server
FutureOr<Either<String, Failure>> submitShortUrl(ShortURLSubmission shortUrl) async {
FutureOr<Either<String, Failure>> submitShortUrl(
ShortURLSubmission shortUrl) async {
return apiSubmitShortUrl(shortUrl, apiKey, serverUrl, apiVersion);
}
@ -139,7 +139,8 @@ class ServerHealthResponse {
/// Failure class, used for the API
abstract class Failure {}
/// Used when a request to a server fails (due to networking issues or an unexpected response)
/// Used when a request to a server fails
/// (due to networking issues or an unexpected response)
class RequestFailure extends Failure {
int statusCode;
String description;
@ -155,5 +156,10 @@ class ApiFailure extends Failure {
int status;
List<dynamic>? invalidElements;
ApiFailure({required this.type, required this.detail, required this.title, required this.status, this.invalidElements});
}
ApiFailure(
{required this.type,
required this.detail,
required this.title,
required this.status,
this.invalidElements});
}

View File

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

View File

@ -28,7 +28,8 @@ class LicenseUtil {
return [
const License(
name: r'cupertino_icons',
license: r'''The MIT License (MIT)
license: r'''
The MIT License (MIT)
Copyright (c) 2016 Vladimir Kharlampidi
@ -49,12 +50,13 @@ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''',
version: r'^1.0.5',
homepage: null,
repository: r'https://github.com/flutter/packages/tree/main/third_party/packages/cupertino_icons',
repository:
r'https://github.com/flutter/packages/tree/main/third_party/packages/cupertino_icons',
),
const License(
name: r'dartz',
license: r'''The MIT License (MIT)
license: r'''
The MIT License (MIT)
Copyright (c) 2016, 2017, 2018, 2019, 2020, 2021 Björn Sperber
@ -78,11 +80,11 @@ SOFTWARE.
''',
version: r'^0.10.1',
homepage: r'https://github.com/spebbe/dartz',
repository: null,
),
const License(
name: r'dynamic_color',
license: r''' Apache License
license: r'''
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
@ -285,12 +287,13 @@ SOFTWARE.
limitations under the License.
''',
version: r'^1.6.6',
homepage: null,
repository: r'https://github.com/material-foundation/flutter-packages/tree/main/packages/dynamic_color',
repository:
r'https://github.com/material-foundation/flutter-packages/tree/main/packages/dynamic_color',
),
const License(
name: r'flutter',
license: r'''Copyright 2014 The Flutter Authors. All rights reserved.
license: r'''
Copyright 2014 The Flutter Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
@ -316,13 +319,13 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
''',
version: null,
homepage: r'https://flutter.dev/',
repository: r'https://github.com/flutter/flutter',
),
const License(
name: r'flutter_launcher_icons',
license: r'''MIT License
license: r'''
MIT License
Copyright (c) 2019 Mark O'Sullivan
@ -346,11 +349,13 @@ SOFTWARE.
''',
version: r'0.13.1',
homepage: r'https://github.com/fluttercommunity/flutter_launcher_icons',
repository: r'https://github.com/fluttercommunity/flutter_launcher_icons/',
repository:
r'https://github.com/fluttercommunity/flutter_launcher_icons/',
),
const License(
name: r'flutter_lints',
license: r'''Copyright 2013 The Flutter Authors. All rights reserved.
license: r'''
Copyright 2013 The Flutter Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
@ -377,12 +382,13 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
''',
version: r'^2.0.2',
homepage: null,
repository: r'https://github.com/flutter/packages/tree/main/packages/flutter_lints',
repository:
r'https://github.com/flutter/packages/tree/main/packages/flutter_lints',
),
const License(
name: r'flutter_process_text',
license: r'''BSD 3-Clause License
license: r'''
BSD 3-Clause License
(c) Copyright 2021 divshekhar (Divyanshu Shekhar)
@ -410,12 +416,12 @@ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABI
TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
version: r'^1.1.2',
homepage: null,
repository: r'https://github.com/DevsOnFlutter/flutter_process_text',
),
const License(
name: r'flutter_secure_storage',
license: r'''BSD 3-Clause License
license: r'''
BSD 3-Clause License
Copyright 2017 German Saprykin
All rights reserved.
@ -445,12 +451,13 @@ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
version: r'^8.0.0',
homepage: null,
repository: r'https://github.com/mogol/flutter_secure_storage/tree/develop/flutter_secure_storage',
repository:
r'https://github.com/mogol/flutter_secure_storage/tree/develop/flutter_secure_storage',
),
const License(
name: r'flutter_test',
license: r'''Copyright 2014 The Flutter Authors. All rights reserved.
license: r'''
Copyright 2014 The Flutter Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
@ -476,13 +483,13 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
''',
version: null,
homepage: r'https://flutter.dev/',
repository: r'https://github.com/flutter/flutter',
),
const License(
name: r'http',
license: r'''Copyright 2014, the Dart project authors.
license: r'''
Copyright 2014, the Dart project authors.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
@ -511,12 +518,12 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
''',
version: r'^0.13.6',
homepage: null,
repository: r'https://github.com/dart-lang/http/tree/master/pkgs/http',
),
const License(
name: r'intl',
license: r'''Copyright 2013, the Dart project authors.
license: r'''
Copyright 2013, the Dart project authors.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
@ -545,12 +552,12 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
''',
version: r'^0.18.1',
homepage: null,
repository: r'https://github.com/dart-lang/i18n/tree/main/pkgs/intl',
),
const License(
name: r'license_generator',
license: r'''MIT License
license: r'''
MIT License
Copyright (c) 2022 icapps
@ -574,11 +581,11 @@ SOFTWARE.
''',
version: r'^1.0.5',
homepage: r'https://github.com/icapps/flutter-icapps-license',
repository: null,
),
const License(
name: r'package_info_plus',
license: r'''Copyright 2017 The Chromium Authors. All rights reserved.
license: r'''
Copyright 2017 The Chromium Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
@ -608,11 +615,13 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
''',
version: r'^4.0.2',
homepage: r'https://plus.fluttercommunity.dev/',
repository: r'https://github.com/fluttercommunity/plus_plugins/tree/main/packages/',
repository:
r'https://github.com/fluttercommunity/plus_plugins/tree/main/packages/',
),
const License(
name: r'qr_flutter',
license: r'''BSD 3-Clause License
license: r'''
BSD 3-Clause License
Copyright (c) 2020, Luke Freeman.
All rights reserved.
@ -644,11 +653,11 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
''',
version: r'^4.1.0',
homepage: r'https://github.com/theyakka/qr.flutter',
repository: null,
),
const License(
name: r'shared_preferences',
license: r'''Copyright 2013 The Flutter Authors. All rights reserved.
license: r'''
Copyright 2013 The Flutter Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
@ -675,12 +684,13 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
''',
version: r'^2.2.2',
homepage: null,
repository: r'https://github.com/flutter/packages/tree/main/packages/shared_preferences/shared_preferences',
repository:
r'https://github.com/flutter/packages/tree/main/packages/shared_preferences/shared_preferences',
),
const License(
name: r'tuple',
license: r'''Copyright (c) 2014, the tuple project authors.
license: r'''
Copyright (c) 2014, the tuple project authors.
All rights reserved.
Redistribution and use in source and binary forms, with or without
@ -703,12 +713,12 @@ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
version: r'^2.0.2',
homepage: null,
repository: r'https://github.com/google/tuple.dart',
),
const License(
name: r'url_launcher',
license: r'''Copyright 2013 The Flutter Authors. All rights reserved.
license: r'''
Copyright 2013 The Flutter Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
@ -735,8 +745,8 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
''',
version: r'6.1.9',
homepage: null,
repository: r'https://github.com/flutter/plugins/tree/main/packages/url_launcher/url_launcher',
repository:
r'https://github.com/flutter/plugins/tree/main/packages/url_launcher/url_launcher',
),
];
}

View File

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

View File

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

View File

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

View File

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

View File

@ -13,24 +13,19 @@ class SettingsView extends StatefulWidget {
State<SettingsView> createState() => _SettingsViewState();
}
enum ServerStatus {
connected,
connecting,
disconnected
}
enum ServerStatus { connected, connecting, disconnected }
class _SettingsViewState extends State<SettingsView> {
var _serverVersion = "---";
ServerStatus _serverStatus = ServerStatus.connecting;
PackageInfo packageInfo = PackageInfo(appName: "", packageName: "", version: "", buildNumber: "");
PackageInfo packageInfo =
PackageInfo(appName: "", packageName: "", version: "", buildNumber: "");
@override
void initState() {
// TODO: implement initState
super.initState();
WidgetsBinding.instance
.addPostFrameCallback((_) => getServerHealth());
WidgetsBinding.instance.addPostFrameCallback((_) => getServerHealth());
}
void getServerHealth() async {
@ -52,12 +47,14 @@ class _SettingsViewState extends State<SettingsView> {
var text = "";
if (r is RequestFailure) {
text = r.description;
}
else {
} else {
text = (r as ApiFailure).detail;
}
final snackBar = SnackBar(content: Text(text), backgroundColor: Colors.red[400], behavior: SnackBarBehavior.floating);
final snackBar = SnackBar(
content: Text(text),
backgroundColor: Colors.red[400],
behavior: SnackBarBehavior.floating);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
});
}
@ -65,68 +62,89 @@ class _SettingsViewState extends State<SettingsView> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: [
SliverAppBar.medium(
expandedHeight: 120,
title: const Text("Settings", style: TextStyle(fontWeight: FontWeight.bold),),
actions: [
PopupMenuButton(
itemBuilder: (context) {
return [
const PopupMenuItem(
value: 0,
child: Text("Log out...", style: TextStyle(color: Colors.red)),
)
];
},
onSelected: (value) {
if (value == 0) {
globals.serverManager.logOut().then((value) => Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => const LoginView())
));
}
},
)
],
body: CustomScrollView(
slivers: [
SliverAppBar.medium(
expandedHeight: 120,
title: const Text(
"Settings",
style: TextStyle(fontWeight: FontWeight.bold),
),
SliverToBoxAdapter(
child: Padding(
actions: [
PopupMenuButton(
itemBuilder: (context) {
return [
const PopupMenuItem(
value: 0,
child:
Text("Log out...", style: TextStyle(color: Colors.red)),
)
];
},
onSelected: (value) {
if (value == 0) {
globals.serverManager.logOut().then((value) =>
Navigator.of(context).pushReplacement(MaterialPageRoute(
builder: (context) => const LoginView())));
}
},
)
],
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
children: [
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Theme.of(context).brightness == Brightness.light ? Colors.grey[100] : Colors.grey[900],
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Theme.of(context).brightness == Brightness.light
? Colors.grey[100]
: Colors.grey[900],
),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
children: [
Icon(Icons.dns_outlined, color: (() {
switch (_serverStatus) {
case ServerStatus.connected:
return Colors.green;
case ServerStatus.connecting:
return Colors.orange;
case ServerStatus.disconnected:
return Colors.red;
}
}())),
Icon(Icons.dns_outlined,
color: (() {
switch (_serverStatus) {
case ServerStatus.connected:
return Colors.green;
case ServerStatus.connecting:
return Colors.orange;
case ServerStatus.disconnected:
return Colors.red;
}
}())),
const SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text("Connected to", style: TextStyle(color: Colors.grey)),
Text(globals.serverManager.getServerUrl(), style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
const Text("Connected to",
style: TextStyle(color: Colors.grey)),
Text(globals.serverManager.getServerUrl(),
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16)),
Row(
children: [
const Text("API Version: ", style: TextStyle(color: Colors.grey, fontWeight: FontWeight.w600)),
Text(globals.serverManager.getApiVersion(), style: const TextStyle(color: Colors.grey)),
const Text("API Version: ",
style: TextStyle(
color: Colors.grey,
fontWeight: FontWeight.w600)),
Text(globals.serverManager.getApiVersion(),
style:
const TextStyle(color: Colors.grey)),
const SizedBox(width: 16),
const Text("Server Version: ", style: TextStyle(color: Colors.grey, fontWeight: FontWeight.w600)),
Text(_serverVersion, style: const TextStyle(color: Colors.grey))
const Text("Server Version: ",
style: TextStyle(
color: Colors.grey,
fontWeight: FontWeight.w600)),
Text(_serverVersion,
style:
const TextStyle(color: Colors.grey))
],
),
],
@ -140,17 +158,20 @@ class _SettingsViewState extends State<SettingsView> {
const SizedBox(height: 8),
GestureDetector(
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const OpenSourceLicensesView())
);
Navigator.of(context).push(MaterialPageRoute(
builder: (context) =>
const OpenSourceLicensesView()));
},
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Theme.of(context).brightness == Brightness.light ? Colors.grey[100] : Colors.grey[900],
color: Theme.of(context).brightness == Brightness.light
? Colors.grey[100]
: Colors.grey[900],
),
child: const Padding(
padding: EdgeInsets.only(left: 12, right: 12, top: 20, bottom: 20),
padding: EdgeInsets.only(
left: 12, right: 12, top: 20, bottom: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@ -158,50 +179,21 @@ class _SettingsViewState extends State<SettingsView> {
children: [
Icon(Icons.policy_outlined),
SizedBox(width: 8),
Text("Open Source Licenses", style: TextStyle(fontWeight: FontWeight.w500)),
Text("Open Source Licenses",
style: TextStyle(
fontWeight: FontWeight.w500)),
],
),
Icon(Icons.chevron_right)
]
),
]),
),
),
),
const SizedBox(height: 16),
GestureDetector(
onTap: () async {
var url = Uri.parse("https://github.com/rainloreley/shlink-mobile-app");
if (await canLaunchUrl(url)) {
launchUrl(url, mode: LaunchMode.externalApplication);
}
},
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Theme.of(context).brightness == Brightness.light ? Colors.grey[100] : Colors.grey[900],
),
child: const Padding(
padding: EdgeInsets.only(left: 12, right: 12, top: 20, bottom: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(Icons.code),
SizedBox(width: 8),
Text("GitHub", style: TextStyle(fontWeight: FontWeight.w500)),
],
),
Icon(Icons.chevron_right)
]
),
),
),
),
const SizedBox(height: 16),
GestureDetector(
onTap: () async {
var url = Uri.parse("https://abmgrt.dev/shlink-manager/privacy");
var url = Uri.parse(
"https://github.com/rainloreley/shlink-mobile-app");
if (await canLaunchUrl(url)) {
launchUrl(url, mode: LaunchMode.externalApplication);
}
@ -209,10 +201,49 @@ class _SettingsViewState extends State<SettingsView> {
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Theme.of(context).brightness == Brightness.light ? Colors.grey[100] : Colors.grey[900],
color: Theme.of(context).brightness == Brightness.light
? Colors.grey[100]
: Colors.grey[900],
),
child: const Padding(
padding: EdgeInsets.only(left: 12, right: 12, top: 20, bottom: 20),
padding: EdgeInsets.only(
left: 12, right: 12, top: 20, bottom: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(Icons.code),
SizedBox(width: 8),
Text("GitHub",
style: TextStyle(
fontWeight: FontWeight.w500)),
],
),
Icon(Icons.chevron_right)
]),
),
),
),
const SizedBox(height: 16),
GestureDetector(
onTap: () async {
var url = Uri.parse(
"https://abmgrt.dev/shlink-manager/privacy");
if (await canLaunchUrl(url)) {
launchUrl(url, mode: LaunchMode.externalApplication);
}
},
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Theme.of(context).brightness == Brightness.light
? Colors.grey[100]
: Colors.grey[900],
),
child: const Padding(
padding: EdgeInsets.only(
left: 12, right: 12, top: 20, bottom: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@ -220,12 +251,13 @@ class _SettingsViewState extends State<SettingsView> {
children: [
Icon(Icons.lock),
SizedBox(width: 8),
Text("Privacy Policy", style: TextStyle(fontWeight: FontWeight.w500)),
Text("Privacy Policy",
style: TextStyle(
fontWeight: FontWeight.w500)),
],
),
Icon(Icons.chevron_right)
]
),
]),
),
),
),
@ -234,14 +266,16 @@ class _SettingsViewState extends State<SettingsView> {
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text("${packageInfo.appName}, v${packageInfo.version} (${packageInfo.buildNumber})", style: const TextStyle(color: Colors.grey),),],
Text(
"${packageInfo.appName}, v${packageInfo.version} (${packageInfo.buildNumber})",
style: const TextStyle(color: Colors.grey),
),
],
)
],
)
),
)
],
)
);
)),
)
],
));
}
}

View File

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

View File

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

View File

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

View File

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

View File

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