a ton of refactoring and commenting

This commit is contained in:
Adrian Baumgart 2024-01-27 23:07:06 +01:00
parent de8bce4b5c
commit 7b16683d10
No known key found for this signature in database
33 changed files with 457 additions and 410 deletions

View File

@ -1,10 +0,0 @@
import 'package:shlink_app/API/Classes/ShlinkStats/ShlinkStats_Visits.dart';
class ShlinkStats {
ShlinkStats_Visits nonOrphanVisits;
ShlinkStats_Visits orphanVisits;
int shortUrlsCount;
int tagsCount;
ShlinkStats(this.nonOrphanVisits, this.orphanVisits, this.shortUrlsCount, this.tagsCount);
}

View File

@ -1,12 +0,0 @@
class ShlinkStats_Visits {
int total;
int nonBots;
int bots;
ShlinkStats_Visits(this.total, this.nonBots, this.bots);
ShlinkStats_Visits.fromJson(Map<String, dynamic> json)
: total = json["total"],
nonBots = json["nonBots"],
bots = json["bots"];
}

View File

@ -0,0 +1,15 @@
import 'package:shlink_app/API/Classes/ShortURL/visits_summary.dart';
/// Includes data about the statistics of a Shlink instance
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);
}

View File

@ -0,0 +1,17 @@
/// Visitor data
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;
ShlinkStatsVisits(this.total, this.nonBots, this.bots);
/// Converts the JSON data from the API to an instance of [ShlinkStatsVisits]
ShlinkStatsVisits.fromJson(Map<String, dynamic> json)
: total = json["total"],
nonBots = json["nonBots"],
bots = json["bots"];
}

View File

@ -1,33 +0,0 @@
import 'package:shlink_app/API/Classes/ShortURL/ShortURL_DeviceLongUrls.dart';
import 'package:shlink_app/API/Classes/ShortURL/ShortURL_Meta.dart';
import 'package:shlink_app/API/Classes/ShortURL/ShortURL_VisitsSummary.dart';
class ShortURL {
String shortCode;
String shortUrl;
String longUrl;
ShortURL_DeviceLongUrls deviceLongUrls;
DateTime dateCreated;
ShortURL_VisitsSummary visitsSummary;
List<dynamic> tags;
ShortURL_Meta meta;
String? domain;
String? title;
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.fromJson(Map<String, dynamic> json):
shortCode = json["shortCode"],
shortUrl = json["shortUrl"],
longUrl = json["longUrl"],
deviceLongUrls = ShortURL_DeviceLongUrls.fromJson(json["deviceLongUrls"]),
dateCreated = DateTime.parse(json["dateCreated"]),
visitsSummary = ShortURL_VisitsSummary.fromJson(json["visitsSummary"]),
tags = json["tags"],
meta = ShortURL_Meta.fromJson(json["meta"]),
domain = json["domain"],
title = json["title"],
crawlable = json["crawlable"];
}

View File

@ -1,18 +0,0 @@
class ShortURL_DeviceLongUrls {
final String? android;
final String? ios;
final String? desktop;
ShortURL_DeviceLongUrls(this.android, this.ios, this.desktop);
ShortURL_DeviceLongUrls.fromJson(Map<String, dynamic> json)
: android = json["android"],
ios = json["ios"],
desktop = json["desktop"];
Map<String, dynamic> toJson() => {
"android": android,
"ios": ios,
"desktop": desktop
};
}

View File

@ -1,12 +0,0 @@
class ShortURL_Meta {
DateTime? validSince;
DateTime? validUntil;
int? maxVisits;
ShortURL_Meta(this.validSince, this.validUntil, this.maxVisits);
ShortURL_Meta.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

@ -1,12 +0,0 @@
class ShortURL_VisitsSummary {
int total;
int nonBots;
int bots;
ShortURL_VisitsSummary(this.total, this.nonBots, this.bots);
ShortURL_VisitsSummary.fromJson(Map<String, dynamic> json):
total = json["total"] as int,
nonBots = json["nonBots"] as int,
bots = json["bots"] as int;
}

View File

@ -0,0 +1,24 @@
/// Data about device-specific long URLs for one short URL
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;
DeviceLongUrls(this.android, this.ios, this.desktop);
/// 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"];
/// Converts data from this class to an JSON object of type
Map<String, dynamic> toJson() => {
"android": android,
"ios": ios,
"desktop": desktop
};
}

View File

@ -0,0 +1,46 @@
import 'package:shlink_app/API/Classes/ShortURL/device_long_urls.dart';
import 'package:shlink_app/API/Classes/ShortURL/short_url_meta.dart';
import 'package:shlink_app/API/Classes/ShortURL/visits_summary.dart';
/// Data about a short URL
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);
/// 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"];
}

View File

@ -0,0 +1,17 @@
/// Metadata for a short URL
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"];
}

View File

@ -0,0 +1,17 @@
/// Visitor data
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;
}

View File

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

View File

@ -2,12 +2,13 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import '../ServerManager.dart'; import '../server_manager.dart';
FutureOr<Either<String, Failure>> API_connect(String? api_key, String? server_url, String apiVersion) async { /// Tries to connect to the Shlink server
FutureOr<Either<String, Failure>> apiConnect(String? apiKey, String? serverUrl, String apiVersion) async {
try { try {
final response = await http.get(Uri.parse("${server_url}/rest/v${apiVersion}/short-urls"), headers: { final response = await http.get(Uri.parse("$serverUrl/rest/v$apiVersion/short-urls"), headers: {
"X-Api-Key": api_key ?? "", "X-Api-Key": apiKey ?? "",
}); });
if (response.statusCode == 200) { if (response.statusCode == 200) {
return left(""); return left("");

View File

@ -2,12 +2,13 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import '../ServerManager.dart'; import '../server_manager.dart';
FutureOr<Either<String, Failure>> API_deleteShortUrl(String shortCode, String? api_key, String? server_url, String apiVersion) async { /// Deletes a short URL from the server
FutureOr<Either<String, Failure>> apiDeleteShortUrl(String shortCode, String? apiKey, String? serverUrl, String apiVersion) async {
try { try {
final response = await http.delete(Uri.parse("${server_url}/rest/v${apiVersion}/short-urls/${shortCode}"), headers: { final response = await http.delete(Uri.parse("$serverUrl/rest/v$apiVersion/short-urls/$shortCode"), headers: {
"X-Api-Key": api_key ?? "", "X-Api-Key": apiKey ?? "",
}); });
if (response.statusCode == 204) { if (response.statusCode == 204) {
// get returned short url // get returned short url

View File

@ -2,13 +2,14 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:shlink_app/API/Classes/ShortURL/ShortURL.dart'; import 'package:shlink_app/API/Classes/ShortURL/short_url.dart';
import '../ServerManager.dart'; import '../server_manager.dart';
FutureOr<Either<List<ShortURL>, Failure>> API_getRecentShortUrls(String? api_key, String? server_url, String apiVersion) async { /// Gets recently created short URLs from the server
FutureOr<Either<List<ShortURL>, Failure>> apiGetRecentShortUrls(String? apiKey, String? serverUrl, String apiVersion) async {
try { try {
final response = await http.get(Uri.parse("${server_url}/rest/v${apiVersion}/short-urls?itemsPerPage=5&orderBy=dateCreated-DESC"), headers: { final response = await http.get(Uri.parse("$serverUrl/rest/v$apiVersion/short-urls?itemsPerPage=5&orderBy=dateCreated-DESC"), headers: {
"X-Api-Key": api_key ?? "", "X-Api-Key": apiKey ?? "",
}); });
if (response.statusCode == 200) { if (response.statusCode == 200) {
var jsonResponse = jsonDecode(response.body); var jsonResponse = jsonDecode(response.body);

View File

@ -2,12 +2,13 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import '../ServerManager.dart'; import '../server_manager.dart';
FutureOr<Either<ServerHealthResponse, Failure>> API_getServerHealth(String? api_key, String? server_url, String apiVersion) async { /// Gets the status of the server and health information
FutureOr<Either<ServerHealthResponse, Failure>> apiGetServerHealth(String? apiKey, String? serverUrl, String apiVersion) async {
try { try {
final response = await http.get(Uri.parse("${server_url}/rest/v${apiVersion}/health"), headers: { final response = await http.get(Uri.parse("$serverUrl/rest/v$apiVersion/health"), headers: {
"X-Api-Key": api_key ?? "", "X-Api-Key": apiKey ?? "",
}); });
if (response.statusCode == 200) { if (response.statusCode == 200) {
var jsonData = jsonDecode(response.body); var jsonData = jsonDecode(response.body);

View File

@ -2,11 +2,12 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:shlink_app/API/Classes/ShlinkStats/ShlinkStats_Visits.dart'; import 'package:shlink_app/API/Classes/ShortURL/visits_summary.dart';
import '../Classes/ShlinkStats/ShlinkStats.dart'; import '../Classes/ShlinkStats/shlink_stats.dart';
import '../ServerManager.dart'; import '../server_manager.dart';
FutureOr<Either<ShlinkStats, Failure>> API_getShlinkStats(String? api_key, String? server_url, String apiVersion) async { /// Gets statistics about the Shlink server
FutureOr<Either<ShlinkStats, Failure>> apiGetShlinkStats(String? apiKey, String? serverUrl, String apiVersion) async {
var nonOrphanVisits; var nonOrphanVisits;
var orphanVisits; var orphanVisits;
@ -14,7 +15,7 @@ FutureOr<Either<ShlinkStats, Failure>> API_getShlinkStats(String? api_key, Strin
var tagsCount; var tagsCount;
var failure; var failure;
var visitStatsResponse = await _getVisitStats(api_key, server_url, apiVersion); var visitStatsResponse = await _getVisitStats(apiKey, serverUrl, apiVersion);
visitStatsResponse.fold((l) { visitStatsResponse.fold((l) {
nonOrphanVisits = l.nonOrphanVisits; nonOrphanVisits = l.nonOrphanVisits;
orphanVisits = l.orphanVisits; orphanVisits = l.orphanVisits;
@ -23,7 +24,7 @@ FutureOr<Either<ShlinkStats, Failure>> API_getShlinkStats(String? api_key, Strin
return right(r); return right(r);
}); });
var shortUrlsCountResponse = await _getShortUrlsCount(api_key, server_url, apiVersion); var shortUrlsCountResponse = await _getShortUrlsCount(apiKey, serverUrl, apiVersion);
shortUrlsCountResponse.fold((l) { shortUrlsCountResponse.fold((l) {
shortUrlsCount = l; shortUrlsCount = l;
}, (r) { }, (r) {
@ -31,7 +32,7 @@ FutureOr<Either<ShlinkStats, Failure>> API_getShlinkStats(String? api_key, Strin
return right(r); return right(r);
}); });
var tagsCountResponse = await _getTagsCount(api_key, server_url, apiVersion); var tagsCountResponse = await _getTagsCount(apiKey, serverUrl, apiVersion);
tagsCountResponse.fold((l) { tagsCountResponse.fold((l) {
tagsCount = l; tagsCount = l;
}, (r) { }, (r) {
@ -40,7 +41,7 @@ FutureOr<Either<ShlinkStats, Failure>> API_getShlinkStats(String? api_key, Strin
}); });
while(failure == null && (nonOrphanVisits == null || orphanVisits == null || shortUrlsCount == null || tagsCount == null)) { while(failure == null && (nonOrphanVisits == null || orphanVisits == null || shortUrlsCount == null || tagsCount == null)) {
await Future.delayed(Duration(milliseconds: 100)); await Future.delayed(const Duration(milliseconds: 100));
} }
if (failure != null) { if (failure != null) {
@ -49,37 +50,23 @@ FutureOr<Either<ShlinkStats, Failure>> API_getShlinkStats(String? api_key, Strin
return left(ShlinkStats(nonOrphanVisits, orphanVisits, shortUrlsCount, tagsCount)); return left(ShlinkStats(nonOrphanVisits, orphanVisits, shortUrlsCount, tagsCount));
} }
/*Future<tuple.Tuple3<FutureOr<Either<_ShlinkVisitStats, Failure>>, FutureOr<Either<int, Failure>>, FutureOr<Either<int, Failure>>>> waiterFunction(String? api_key, String? server_url, String apiVersion) async {
late FutureOr<Either<_ShlinkVisitStats, Failure>> visits;
late FutureOr<Either<int, Failure>> shortUrlsCount;
late FutureOr<Either<int, Failure>> tagsCount;
await Future.wait([
_getVisitStats(api_key, server_url, apiVersion).then((value) => visits = value),
_getShortUrlsCount(api_key, server_url, apiVersion).then((value) => shortUrlsCount = value),
_getTagsCount(api_key, server_url, apiVersion).then((value) => tagsCount = value),
]);
return Future.value(tuple.Tuple3(visits, shortUrlsCount, tagsCount));
}*/
class _ShlinkVisitStats { class _ShlinkVisitStats {
ShlinkStats_Visits nonOrphanVisits; VisitsSummary nonOrphanVisits;
ShlinkStats_Visits orphanVisits; VisitsSummary orphanVisits;
_ShlinkVisitStats(this.nonOrphanVisits, this.orphanVisits); _ShlinkVisitStats(this.nonOrphanVisits, this.orphanVisits);
} }
FutureOr<Either<_ShlinkVisitStats, Failure>> _getVisitStats(String? api_key, String? server_url, String apiVersion) async { /// Gets visitor statistics about the entire server
FutureOr<Either<_ShlinkVisitStats, Failure>> _getVisitStats(String? apiKey, String? serverUrl, String apiVersion) async {
try { try {
final response = await http.get(Uri.parse("${server_url}/rest/v${apiVersion}/visits"), headers: { final response = await http.get(Uri.parse("$serverUrl/rest/v$apiVersion/visits"), headers: {
"X-Api-Key": api_key ?? "", "X-Api-Key": apiKey ?? "",
}); });
if (response.statusCode == 200) { if (response.statusCode == 200) {
var jsonResponse = jsonDecode(response.body); var jsonResponse = jsonDecode(response.body);
var nonOrphanVisits = ShlinkStats_Visits.fromJson(jsonResponse["visits"]["nonOrphanVisits"]); var nonOrphanVisits = VisitsSummary.fromJson(jsonResponse["visits"]["nonOrphanVisits"]);
var orphanVisits = ShlinkStats_Visits.fromJson(jsonResponse["visits"]["orphanVisits"]); var orphanVisits = VisitsSummary.fromJson(jsonResponse["visits"]["orphanVisits"]);
return left(_ShlinkVisitStats(nonOrphanVisits, orphanVisits)); return left(_ShlinkVisitStats(nonOrphanVisits, orphanVisits));
} }
@ -98,11 +85,11 @@ FutureOr<Either<_ShlinkVisitStats, Failure>> _getVisitStats(String? api_key, Str
} }
} }
// get short urls count /// Gets amount of short URLs
FutureOr<Either<int, Failure>> _getShortUrlsCount(String? api_key, String? server_url, String apiVersion) async { FutureOr<Either<int, Failure>> _getShortUrlsCount(String? apiKey, String? serverUrl, String apiVersion) async {
try { try {
final response = await http.get(Uri.parse("${server_url}/rest/v${apiVersion}/short-urls"), headers: { final response = await http.get(Uri.parse("$serverUrl/rest/v$apiVersion/short-urls"), headers: {
"X-Api-Key": api_key ?? "", "X-Api-Key": apiKey ?? "",
}); });
if (response.statusCode == 200) { if (response.statusCode == 200) {
var jsonResponse = jsonDecode(response.body); var jsonResponse = jsonDecode(response.body);
@ -123,11 +110,11 @@ FutureOr<Either<int, Failure>> _getShortUrlsCount(String? api_key, String? serve
} }
} }
// get tags count /// Gets amount of tags
FutureOr<Either<int, Failure>> _getTagsCount(String? api_key, String? server_url, String apiVersion) async { FutureOr<Either<int, Failure>> _getTagsCount(String? apiKey, String? serverUrl, String apiVersion) async {
try { try {
final response = await http.get(Uri.parse("${server_url}/rest/v${apiVersion}/tags"), headers: { final response = await http.get(Uri.parse("$serverUrl/rest/v$apiVersion/tags"), headers: {
"X-Api-Key": api_key ?? "", "X-Api-Key": apiKey ?? "",
}); });
if (response.statusCode == 200) { if (response.statusCode == 200) {
var jsonResponse = jsonDecode(response.body); var jsonResponse = jsonDecode(response.body);

View File

@ -2,39 +2,41 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:shlink_app/API/Classes/ShortURL/ShortURL.dart'; import 'package:shlink_app/API/Classes/ShortURL/short_url.dart';
import '../ServerManager.dart'; import '../server_manager.dart';
FutureOr<Either<List<ShortURL>, Failure>> API_getShortUrls(String? api_key, String? server_url, String apiVersion) async { /// Gets all short URLs
var _currentPage = 1; FutureOr<Either<List<ShortURL>, Failure>> apiGetShortUrls(String? apiKey, String? serverUrl, String apiVersion) async {
var _maxPages = 2; var currentPage = 1;
List<ShortURL> _allUrls = []; var maxPages = 2;
List<ShortURL> allUrls = [];
Failure? error; Failure? error;
while (_currentPage <= _maxPages) { while (currentPage <= maxPages) {
final response = await _getShortUrlPage(_currentPage, api_key, server_url, apiVersion); final response = await _getShortUrlPage(currentPage, apiKey, serverUrl, apiVersion);
response.fold((l) { response.fold((l) {
_allUrls.addAll(l.urls); allUrls.addAll(l.urls);
_maxPages = l.totalPages; maxPages = l.totalPages;
_currentPage++; currentPage++;
}, (r) { }, (r) {
_maxPages = 0; maxPages = 0;
error = r; error = r;
}); });
} }
if (error == null) { if (error == null) {
return left(_allUrls); return left(allUrls);
} }
else { else {
return right(error!); return right(error!);
} }
} }
FutureOr<Either<ShortURLPageResponse, Failure>> _getShortUrlPage(int page, String? api_key, String? server_url, String apiVersion) async { /// Gets all short URLs from a specific page
FutureOr<Either<ShortURLPageResponse, Failure>> _getShortUrlPage(int page, String? apiKey, String? serverUrl, String apiVersion) async {
try { try {
final response = await http.get(Uri.parse("${server_url}/rest/v${apiVersion}/short-urls?page=${page}"), headers: { final response = await http.get(Uri.parse("$serverUrl/rest/v$apiVersion/short-urls?page=$page"), headers: {
"X-Api-Key": api_key ?? "", "X-Api-Key": apiKey ?? "",
}); });
if (response.statusCode == 200) { if (response.statusCode == 200) {
var jsonResponse = jsonDecode(response.body); var jsonResponse = jsonDecode(response.body);

View File

@ -2,13 +2,14 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:shlink_app/API/Classes/ShortURLSubmission/ShortURLSubmission.dart'; import 'package:shlink_app/API/Classes/ShortURLSubmission/short_url_submission.dart';
import '../ServerManager.dart'; import '../server_manager.dart';
FutureOr<Either<String, Failure>> API_submitShortUrl(ShortURLSubmission shortUrl, String? api_key, String? server_url, String apiVersion) async { /// 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 {
try { try {
final response = await http.post(Uri.parse("${server_url}/rest/v${apiVersion}/short-urls"), headers: { final response = await http.post(Uri.parse("$serverUrl/rest/v$apiVersion/short-urls"), headers: {
"X-Api-Key": api_key ?? "", "X-Api-Key": apiKey ?? "",
}, body: jsonEncode(shortUrl.toJson())); }, body: jsonEncode(shortUrl.toJson()));
if (response.statusCode == 200) { if (response.statusCode == 200) {
// get returned short url // get returned short url
@ -18,7 +19,7 @@ FutureOr<Either<String, Failure>> API_submitShortUrl(ShortURLSubmission shortUrl
else { else {
try { try {
var jsonBody = jsonDecode(response.body); var jsonBody = jsonDecode(response.body);
return right(ApiFailure(type: jsonBody["type"], detail: jsonBody["detail"], title: jsonBody["title"], status: jsonBody["status"], invalidElements: jsonBody["invalidElements"] ?? null)); return right(ApiFailure(type: jsonBody["type"], detail: jsonBody["detail"], title: jsonBody["title"], status: jsonBody["status"], invalidElements: jsonBody["invalidElements"]));
} }
catch(resErr) { catch(resErr) {
return right(RequestFailure(response.statusCode, resErr.toString())); return right(RequestFailure(response.statusCode, resErr.toString()));

View File

@ -2,113 +2,125 @@ import 'dart:async';
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:shlink_app/API/Classes/ShlinkStats/ShlinkStats.dart'; import 'package:shlink_app/API/Classes/ShlinkStats/shlink_stats.dart';
import 'package:shlink_app/API/Classes/ShortURL/ShortURL.dart'; import 'package:shlink_app/API/Classes/ShortURL/short_url.dart';
import 'package:shlink_app/API/Classes/ShortURLSubmission/ShortURLSubmission.dart'; import 'package:shlink_app/API/Classes/ShortURLSubmission/short_url_submission.dart';
import 'package:shlink_app/API/Methods/connect.dart'; import 'package:shlink_app/API/Methods/connect.dart';
import 'package:shlink_app/API/Methods/getRecentShortUrls.dart'; import 'package:shlink_app/API/Methods/get_recent_short_urls.dart';
import 'package:shlink_app/API/Methods/getServerHealth.dart'; import 'package:shlink_app/API/Methods/get_server_health.dart';
import 'package:shlink_app/API/Methods/getShlinkStats.dart'; import 'package:shlink_app/API/Methods/get_shlink_stats.dart';
import 'package:shlink_app/API/Methods/getShortUrls.dart'; import 'package:shlink_app/API/Methods/get_short_urls.dart';
import 'Methods/deleteShortUrl.dart'; import 'Methods/delete_short_url.dart';
import 'Methods/submitShortUrl.dart'; import 'Methods/submit_short_url.dart';
class ServerManager { class ServerManager {
String? _server_url; /// The URL of the Shlink server
String? _api_key; String? serverUrl;
/// The API key to access the server
String? apiKey;
/// Current Shlink API Version used by the app
static String apiVersion = "3"; static String apiVersion = "3";
String getServerUrl() { String getServerUrl() {
return _server_url ?? ""; return serverUrl ?? "";
} }
String getApiVersion() { String getApiVersion() {
return apiVersion; return apiVersion;
} }
/// Checks whether the user provided information about the server (url and apikey)
Future<bool> checkLogin() async { Future<bool> checkLogin() async {
await _loadCredentials(); await _loadCredentials();
return (_server_url != null); return (serverUrl != null);
} }
/// Logs out the user and removes data about the Shlink server
Future<void> logOut() async { Future<void> logOut() async {
const storage = FlutterSecureStorage(); const storage = FlutterSecureStorage();
await storage.delete(key: "shlink_url"); await storage.delete(key: "shlink_url");
await storage.delete(key: "shlink_apikey"); await storage.delete(key: "shlink_apikey");
} }
/// Loads the server credentials from [FlutterSecureStorage]
Future<void> _loadCredentials() async { Future<void> _loadCredentials() async {
const storage = FlutterSecureStorage(); const storage = FlutterSecureStorage();
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
if (prefs.getBool('first_run') ?? true) { if (prefs.getBool('first_run') ?? true) {
FlutterSecureStorage storage = FlutterSecureStorage(); FlutterSecureStorage storage = const FlutterSecureStorage();
await storage.deleteAll(); await storage.deleteAll();
prefs.setBool('first_run', false); prefs.setBool('first_run', false);
} }
_server_url = await storage.read(key: "shlink_url"); serverUrl = await storage.read(key: "shlink_url");
_api_key = await storage.read(key: "shlink_apikey"); apiKey = await storage.read(key: "shlink_apikey");
} }
/// Saves the provided server credentials to [FlutterSecureStorage]
void _saveCredentials(String url, String apiKey) async { void _saveCredentials(String url, String apiKey) async {
const storage = FlutterSecureStorage(); const storage = FlutterSecureStorage();
storage.write(key: "shlink_url", value: url); storage.write(key: "shlink_url", value: url);
storage.write(key: "shlink_apikey", value: apiKey); storage.write(key: "shlink_apikey", value: apiKey);
} }
void _removeCredentials() async {
const storage = FlutterSecureStorage();
storage.delete(key: "shlink_url");
storage.delete(key: "shlink_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 // TODO: convert url to correct format
_server_url = url; serverUrl = url;
_api_key = apiKey; this.apiKey = apiKey;
_saveCredentials(url, apiKey); _saveCredentials(url, apiKey);
final result = await connect(); final result = await connect();
result.fold((l) => null, (r) { result.fold((l) => null, (r) {
_removeCredentials(); logOut();
}); });
return result; return result;
} }
/// Establishes a connection to the server
FutureOr<Either<String, Failure>> connect() async { FutureOr<Either<String, Failure>> connect() async {
_loadCredentials(); _loadCredentials();
return API_connect(_api_key, _server_url, apiVersion); return apiConnect(apiKey, serverUrl, apiVersion);
} }
/// Gets all short URLs from the server
FutureOr<Either<List<ShortURL>, Failure>> getShortUrls() async { FutureOr<Either<List<ShortURL>, Failure>> getShortUrls() async {
return API_getShortUrls(_api_key, _server_url, apiVersion); return apiGetShortUrls(apiKey, serverUrl, apiVersion);
} }
/// Gets statistics about the Shlink instance
FutureOr<Either<ShlinkStats, Failure>> getShlinkStats() async { FutureOr<Either<ShlinkStats, Failure>> getShlinkStats() async {
return API_getShlinkStats(_api_key, _server_url, apiVersion); return apiGetShlinkStats(apiKey, serverUrl, apiVersion);
} }
/// 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 API_submitShortUrl(shortUrl, _api_key, _server_url, apiVersion); return apiSubmitShortUrl(shortUrl, apiKey, serverUrl, apiVersion);
} }
/// Deletes a short URL from the server, identified by its slug
FutureOr<Either<String, Failure>> deleteShortUrl(String shortCode) async { FutureOr<Either<String, Failure>> deleteShortUrl(String shortCode) async {
return API_deleteShortUrl(shortCode, _api_key, _server_url, apiVersion); return apiDeleteShortUrl(shortCode, apiKey, serverUrl, apiVersion);
} }
/// Gets health data about the server
FutureOr<Either<ServerHealthResponse, Failure>> getServerHealth() async { FutureOr<Either<ServerHealthResponse, Failure>> getServerHealth() async {
return API_getServerHealth(_api_key, _server_url, apiVersion); return apiGetServerHealth(apiKey, serverUrl, apiVersion);
} }
/// Gets recently created/used short URLs from the server
FutureOr<Either<List<ShortURL>, Failure>> getRecentShortUrls() async { FutureOr<Either<List<ShortURL>, Failure>> getRecentShortUrls() async {
return API_getRecentShortUrls(_api_key, _server_url, apiVersion); return apiGetRecentShortUrls(apiKey, serverUrl, apiVersion);
} }
} }
/// Server response data type about a page of short URLs from the server
class ShortURLPageResponse { class ShortURLPageResponse {
List<ShortURL> urls; List<ShortURL> urls;
int totalPages; int totalPages;
@ -116,6 +128,7 @@ class ShortURLPageResponse {
ShortURLPageResponse(this.urls, this.totalPages); ShortURLPageResponse(this.urls, this.totalPages);
} }
/// Server response data type about the health status of the server
class ServerHealthResponse { class ServerHealthResponse {
String status; String status;
String version; String version;
@ -123,8 +136,10 @@ class ServerHealthResponse {
ServerHealthResponse({required this.status, required this.version}); ServerHealthResponse({required this.status, required this.version});
} }
/// Failure class, used for the API
abstract class Failure {} abstract class Failure {}
/// Used when a request to a server fails (due to networking issues or an unexpected response)
class RequestFailure extends Failure { class RequestFailure extends Failure {
int statusCode; int statusCode;
String description; String description;
@ -132,6 +147,7 @@ class RequestFailure extends Failure {
RequestFailure(this.statusCode, this.description); RequestFailure(this.statusCode, this.description);
} }
/// Contains information about an error returned by the Shlink API
class ApiFailure extends Failure { class ApiFailure extends Failure {
String type; String type;
String detail; String detail;

View File

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

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shlink_app/LoginView.dart'; import 'package:shlink_app/views/login_view.dart';
import 'package:shlink_app/NavigationBarView.dart'; import 'package:shlink_app/views/navigationbar_view.dart';
import 'globals.dart' as globals; import 'globals.dart' as globals;
import 'package:dynamic_color/dynamic_color.dart'; import 'package:dynamic_color/dynamic_color.dart';
@ -11,7 +11,7 @@ void main() {
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
const MyApp({super.key}); const MyApp({super.key});
static final _defaultLightColorScheme = static const _defaultLightColorScheme =
ColorScheme.light();//.fromSwatch(primarySwatch: Colors.blue, backgroundColor: Colors.white); ColorScheme.light();//.fromSwatch(primarySwatch: Colors.blue, backgroundColor: Colors.white);
static final _defaultDarkColorScheme = ColorScheme.fromSwatch( static final _defaultDarkColorScheme = ColorScheme.fromSwatch(
@ -25,14 +25,14 @@ class MyApp extends StatelessWidget {
title: 'Shlink', title: 'Shlink',
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
theme: ThemeData( theme: ThemeData(
appBarTheme: AppBarTheme( appBarTheme: const AppBarTheme(
backgroundColor: Color(0xfffafafa), backgroundColor: Color(0xfffafafa),
), ),
colorScheme: lightColorScheme ?? _defaultLightColorScheme, colorScheme: lightColorScheme ?? _defaultLightColorScheme,
useMaterial3: true useMaterial3: true
), ),
darkTheme: ThemeData( darkTheme: ThemeData(
appBarTheme: AppBarTheme( appBarTheme: const AppBarTheme(
backgroundColor: Color(0xff0d0d0d), backgroundColor: Color(0xff0d0d0d),
foregroundColor: Colors.white, foregroundColor: Colors.white,
elevation: 0, elevation: 0,
@ -40,26 +40,8 @@ class MyApp extends StatelessWidget {
colorScheme: darkColorScheme?.copyWith(background: Colors.black) ?? _defaultDarkColorScheme, colorScheme: darkColorScheme?.copyWith(background: Colors.black) ?? _defaultDarkColorScheme,
useMaterial3: true, useMaterial3: true,
), ),
/*theme: ThemeData(
primarySwatch: Colors.blue,
brightness: Brightness.light,
useMaterial3: true,
colorScheme: ColorScheme.fromSwatch().copyWith(
secondary: Colors.orange
)
),*/
/*darkTheme: ThemeData(
primarySwatch: Colors.blue,
brightness: Brightness.dark,
useMaterial3: true,
colorScheme: ColorScheme.dark(
background: Colors.black,
surface: Color(0xff0d0d0d),
secondaryContainer: Colors.grey[300]
)
),*/
themeMode: ThemeMode.system, themeMode: ThemeMode.system,
home: InitialPage() home: const InitialPage()
); );
}); });
} }
@ -97,7 +79,7 @@ class _InitialPageState extends State<InitialPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return const Scaffold(
body: Center( body: Center(
child: Text("") child: Text("")
), ),

View File

@ -26,7 +26,7 @@ class LicenseUtil {
static List<License> getLicenses() { static List<License> getLicenses() {
return [ return [
License( const License(
name: r'cupertino_icons', name: r'cupertino_icons',
license: r'''The MIT License (MIT) license: r'''The MIT License (MIT)
@ -52,7 +52,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''',
homepage: null, 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',
), ),
License( const License(
name: r'dartz', name: r'dartz',
license: r'''The MIT License (MIT) license: r'''The MIT License (MIT)
@ -80,7 +80,7 @@ SOFTWARE.
homepage: r'https://github.com/spebbe/dartz', homepage: r'https://github.com/spebbe/dartz',
repository: null, repository: null,
), ),
License( const License(
name: r'dynamic_color', name: r'dynamic_color',
license: r''' Apache License license: r''' Apache License
Version 2.0, January 2004 Version 2.0, January 2004
@ -288,7 +288,7 @@ SOFTWARE.
homepage: null, 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',
), ),
License( const License(
name: r'flutter', name: r'flutter',
license: r'''Copyright 2014 The Flutter Authors. All rights reserved. license: r'''Copyright 2014 The Flutter Authors. All rights reserved.
@ -320,7 +320,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
homepage: r'https://flutter.dev/', homepage: r'https://flutter.dev/',
repository: r'https://github.com/flutter/flutter', repository: r'https://github.com/flutter/flutter',
), ),
License( const License(
name: r'flutter_launcher_icons', name: r'flutter_launcher_icons',
license: r'''MIT License license: r'''MIT License
@ -348,7 +348,7 @@ SOFTWARE.
homepage: r'https://github.com/fluttercommunity/flutter_launcher_icons', homepage: r'https://github.com/fluttercommunity/flutter_launcher_icons',
repository: r'https://github.com/fluttercommunity/flutter_launcher_icons/', repository: r'https://github.com/fluttercommunity/flutter_launcher_icons/',
), ),
License( const License(
name: r'flutter_lints', name: r'flutter_lints',
license: r'''Copyright 2013 The Flutter Authors. All rights reserved. license: r'''Copyright 2013 The Flutter Authors. All rights reserved.
@ -380,7 +380,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
homepage: null, 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',
), ),
License( const License(
name: r'flutter_process_text', name: r'flutter_process_text',
license: r'''BSD 3-Clause License license: r'''BSD 3-Clause License
@ -413,7 +413,7 @@ EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
homepage: null, homepage: null,
repository: r'https://github.com/DevsOnFlutter/flutter_process_text', repository: r'https://github.com/DevsOnFlutter/flutter_process_text',
), ),
License( const License(
name: r'flutter_secure_storage', name: r'flutter_secure_storage',
license: r'''BSD 3-Clause License license: r'''BSD 3-Clause License
@ -448,7 +448,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
homepage: null, 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',
), ),
License( const License(
name: r'flutter_test', name: r'flutter_test',
license: r'''Copyright 2014 The Flutter Authors. All rights reserved. license: r'''Copyright 2014 The Flutter Authors. All rights reserved.
@ -480,7 +480,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
homepage: r'https://flutter.dev/', homepage: r'https://flutter.dev/',
repository: r'https://github.com/flutter/flutter', repository: r'https://github.com/flutter/flutter',
), ),
License( const License(
name: r'http', name: r'http',
license: r'''Copyright 2014, the Dart project authors. license: r'''Copyright 2014, the Dart project authors.
@ -514,7 +514,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
homepage: null, homepage: null,
repository: r'https://github.com/dart-lang/http/tree/master/pkgs/http', repository: r'https://github.com/dart-lang/http/tree/master/pkgs/http',
), ),
License( const License(
name: r'intl', name: r'intl',
license: r'''Copyright 2013, the Dart project authors. license: r'''Copyright 2013, the Dart project authors.
@ -548,7 +548,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
homepage: null, homepage: null,
repository: r'https://github.com/dart-lang/i18n/tree/main/pkgs/intl', repository: r'https://github.com/dart-lang/i18n/tree/main/pkgs/intl',
), ),
License( const License(
name: r'license_generator', name: r'license_generator',
license: r'''MIT License license: r'''MIT License
@ -576,7 +576,7 @@ SOFTWARE.
homepage: r'https://github.com/icapps/flutter-icapps-license', homepage: r'https://github.com/icapps/flutter-icapps-license',
repository: null, repository: null,
), ),
License( const License(
name: r'package_info_plus', name: r'package_info_plus',
license: r'''Copyright 2017 The Chromium Authors. All rights reserved. license: r'''Copyright 2017 The Chromium Authors. All rights reserved.
@ -610,7 +610,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
homepage: r'https://plus.fluttercommunity.dev/', homepage: r'https://plus.fluttercommunity.dev/',
repository: r'https://github.com/fluttercommunity/plus_plugins/tree/main/packages/', repository: r'https://github.com/fluttercommunity/plus_plugins/tree/main/packages/',
), ),
License( const License(
name: r'qr_flutter', name: r'qr_flutter',
license: r'''BSD 3-Clause License license: r'''BSD 3-Clause License
@ -646,7 +646,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
homepage: r'https://github.com/theyakka/qr.flutter', homepage: r'https://github.com/theyakka/qr.flutter',
repository: null, repository: null,
), ),
License( const License(
name: r'shared_preferences', name: r'shared_preferences',
license: r'''Copyright 2013 The Flutter Authors. All rights reserved. license: r'''Copyright 2013 The Flutter Authors. All rights reserved.
@ -678,7 +678,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
homepage: null, 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',
), ),
License( const License(
name: r'tuple', name: r'tuple',
license: r'''Copyright (c) 2014, the tuple project authors. license: r'''Copyright (c) 2014, the tuple project authors.
All rights reserved. All rights reserved.
@ -706,7 +706,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
homepage: null, homepage: null,
repository: r'https://github.com/google/tuple.dart', repository: r'https://github.com/google/tuple.dart',
), ),
License( const License(
name: r'url_launcher', name: r'url_launcher',
license: r'''Copyright 2013 The Flutter Authors. All rights reserved. license: r'''Copyright 2013 The Flutter Authors. All rights reserved.

View File

@ -1,12 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:qr_flutter/qr_flutter.dart'; import 'package:qr_flutter/qr_flutter.dart';
import 'package:shlink_app/API/Classes/ShlinkStats/ShlinkStats.dart'; import 'package:shlink_app/API/Classes/ShlinkStats/shlink_stats.dart';
import 'package:shlink_app/API/ServerManager.dart'; import 'package:shlink_app/API/server_manager.dart';
import 'package:shlink_app/LoginView.dart'; import 'package:shlink_app/views/short_url_edit_view.dart';
import 'package:shlink_app/ShortURLEditView.dart'; import 'package:shlink_app/views/url_list_view.dart';
import 'package:shlink_app/URLListView.dart'; import '../API/Classes/ShortURL/short_url.dart';
import 'API/Classes/ShortURL/ShortURL.dart'; import '../globals.dart' as globals;
import 'globals.dart' as globals;
class HomeView extends StatefulWidget { class HomeView extends StatefulWidget {
const HomeView({Key? key}) : super(key: key); const HomeView({Key? key}) : super(key: key);
@ -35,8 +34,8 @@ class _HomeViewState extends State<HomeView> {
} }
Future<void> loadAllData() async { Future<void> loadAllData() async {
var resultStats = await loadShlinkStats(); await loadShlinkStats();
var resultShortUrls = await loadRecentShortUrls(); await loadRecentShortUrls();
return; return;
} }
@ -99,7 +98,7 @@ class _HomeViewState extends State<HomeView> {
title: Column( title: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text("Shlink", style: TextStyle(fontWeight: FontWeight.bold)), const Text("Shlink", style: TextStyle(fontWeight: FontWeight.bold)),
Text(globals.serverManager.getServerUrl(), style: TextStyle(fontSize: 16, color: Colors.grey[600])) Text(globals.serverManager.getServerUrl(), style: TextStyle(fontSize: 16, color: Colors.grey[600]))
], ],
) )
@ -119,12 +118,12 @@ class _HomeViewState extends State<HomeView> {
SliverToBoxAdapter( SliverToBoxAdapter(
child: Center( child: Center(
child: Padding( child: Padding(
padding: EdgeInsets.only(top: 50), padding: const EdgeInsets.only(top: 50),
child: Column( child: Column(
children: [ children: [
Text("No Short URLs", style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),), const Text("No Short URLs", style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),),
Padding( Padding(
padding: EdgeInsets.only(top: 8), padding: const EdgeInsets.only(top: 8),
child: Text('Create one by tapping the "+" button below', style: TextStyle(fontSize: 16, color: Colors.grey[600]),), child: Text('Create one by tapping the "+" button below', style: TextStyle(fontSize: 16, color: Colors.grey[600]),),
) )
], ],
@ -134,9 +133,9 @@ class _HomeViewState extends State<HomeView> {
) )
else else
SliverList(delegate: SliverChildBuilderDelegate( SliverList(delegate: SliverChildBuilderDelegate(
(BuildContext _context, int index) { (BuildContext context, int index) {
if (index == 0) { if (index == 0) {
return Padding( return const Padding(
padding: EdgeInsets.only(top: 16, left: 12, right: 12), padding: EdgeInsets.only(top: 16, left: 12, right: 12),
child: Text("Recent Short URLs", style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), child: Text("Recent Short URLs", style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
); );
@ -200,10 +199,10 @@ class _HomeViewState extends State<HomeView> {
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
onPressed: () async { onPressed: () async {
final result = await Navigator.of(context).push(MaterialPageRoute(builder: (context) => const ShortURLEditView())); await Navigator.of(context).push(MaterialPageRoute(builder: (context) => const ShortURLEditView()));
loadRecentShortUrls(); loadRecentShortUrls();
}, },
child: Icon(Icons.add), child: const Icon(Icons.add),
) )
); );
} }
@ -211,11 +210,11 @@ class _HomeViewState extends State<HomeView> {
// stats card widget // stats card widget
class _ShlinkStatsCardWidget extends StatefulWidget { class _ShlinkStatsCardWidget extends StatefulWidget {
const _ShlinkStatsCardWidget({this.text, this.icon, this.borderColor}); const _ShlinkStatsCardWidget({required this.text, required this.icon, this.borderColor});
final icon; final IconData icon;
final borderColor; final Color? borderColor;
final text; final String text;
@override @override
State<_ShlinkStatsCardWidget> createState() => _ShlinkStatsCardWidgetState(); State<_ShlinkStatsCardWidget> createState() => _ShlinkStatsCardWidgetState();
@ -226,9 +225,9 @@ class _ShlinkStatsCardWidgetState extends State<_ShlinkStatsCardWidget> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
var randomColor = ([...Colors.primaries]..shuffle()).first; var randomColor = ([...Colors.primaries]..shuffle()).first;
return Padding( return Padding(
padding: EdgeInsets.all(4), padding: const EdgeInsets.all(4),
child: Container( child: Container(
padding: EdgeInsets.all(12), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all(color: widget.borderColor ?? randomColor), border: Border.all(color: widget.borderColor ?? randomColor),
borderRadius: BorderRadius.circular(8) borderRadius: BorderRadius.circular(8)
@ -238,8 +237,8 @@ class _ShlinkStatsCardWidgetState extends State<_ShlinkStatsCardWidget> {
children: [ children: [
Icon(widget.icon), Icon(widget.icon),
Padding( Padding(
padding: EdgeInsets.only(left: 4), padding: const EdgeInsets.only(left: 4),
child: Text(widget.text, style: TextStyle(fontWeight: FontWeight.bold)), child: Text(widget.text, style: const TextStyle(fontWeight: FontWeight.bold)),
) )
], ],
), ),

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shlink_app/API/ServerManager.dart'; import 'package:shlink_app/API/server_manager.dart';
import 'package:shlink_app/main.dart'; import 'package:shlink_app/main.dart';
import 'globals.dart' as globals; import '../globals.dart' as globals;
class LoginView extends StatefulWidget { class LoginView extends StatefulWidget {
const LoginView({Key? key}) : super(key: key); const LoginView({Key? key}) : super(key: key);
@ -11,8 +11,8 @@ class LoginView extends StatefulWidget {
} }
class _LoginViewState extends State<LoginView> { class _LoginViewState extends State<LoginView> {
late TextEditingController _server_url_controller; late TextEditingController _serverUrlController;
late TextEditingController _apikey_controller; late TextEditingController _apiKeyController;
bool _isLoggingIn = false; bool _isLoggingIn = false;
String _errorMessage = ""; String _errorMessage = "";
@ -21,8 +21,8 @@ class _LoginViewState extends State<LoginView> {
void initState() { void initState() {
// TODO: implement initState // TODO: implement initState
super.initState(); super.initState();
_server_url_controller = TextEditingController(); _serverUrlController = TextEditingController();
_apikey_controller = TextEditingController(); _apiKeyController = TextEditingController();
} }
void _connect() async { void _connect() async {
@ -30,7 +30,7 @@ class _LoginViewState extends State<LoginView> {
_isLoggingIn = true; _isLoggingIn = true;
_errorMessage = ""; _errorMessage = "";
}); });
final connectResult = await globals.serverManager.initAndConnect(_server_url_controller.text, _apikey_controller.text); final connectResult = await globals.serverManager.initAndConnect(_serverUrlController.text, _apiKeyController.text);
connectResult.fold((l) { connectResult.fold((l) {
Navigator.of(context).pushReplacement( Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => const InitialPage()) MaterialPageRoute(builder: (context) => const InitialPage())
@ -61,8 +61,8 @@ class _LoginViewState extends State<LoginView> {
extendBody: true, extendBody: true,
body: CustomScrollView( body: CustomScrollView(
slivers: [ slivers: [
SliverAppBar.medium( const SliverAppBar.medium(
title: const Text("Add server", style: TextStyle(fontWeight: FontWeight.bold)) title: Text("Add server", style: TextStyle(fontWeight: FontWeight.bold))
), ),
SliverFillRemaining( SliverFillRemaining(
child: Padding( child: Padding(
@ -75,10 +75,10 @@ class _LoginViewState extends State<LoginView> {
child: Text("Server URL", style: TextStyle(fontWeight: FontWeight.bold),)), child: Text("Server URL", style: TextStyle(fontWeight: FontWeight.bold),)),
Row( Row(
children: [ children: [
Icon(Icons.dns_outlined), const Icon(Icons.dns_outlined),
SizedBox(width: 8), const SizedBox(width: 8),
Expanded(child: TextField( Expanded(child: TextField(
controller: _server_url_controller, controller: _serverUrlController,
keyboardType: TextInputType.url, keyboardType: TextInputType.url,
decoration: const InputDecoration( decoration: const InputDecoration(
border: OutlineInputBorder(), border: OutlineInputBorder(),
@ -93,10 +93,10 @@ class _LoginViewState extends State<LoginView> {
), ),
Row( Row(
children: [ children: [
Icon(Icons.key), const Icon(Icons.key),
SizedBox(width: 8), const SizedBox(width: 8),
Expanded(child: TextField( Expanded(child: TextField(
controller: _apikey_controller, controller: _apiKeyController,
keyboardType: TextInputType.text, keyboardType: TextInputType.text,
obscureText: true, obscureText: true,
decoration: const InputDecoration( decoration: const InputDecoration(
@ -130,7 +130,7 @@ class _LoginViewState extends State<LoginView> {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Flexible(child: Text(_errorMessage, style: TextStyle(color: Colors.red), textAlign: TextAlign.center)) Flexible(child: Text(_errorMessage, style: const TextStyle(color: Colors.red), textAlign: TextAlign.center))
], ],
), ),
) )

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shlink_app/SettingsView.dart'; import 'package:shlink_app/views/settings_view.dart';
import 'package:shlink_app/HomeView.dart'; import 'package:shlink_app/views/home_view.dart';
import 'package:shlink_app/URLListView.dart'; import 'package:shlink_app/views/url_list_view.dart';
class NavigationBarView extends StatefulWidget { class NavigationBarView extends StatefulWidget {
const NavigationBarView({Key? key}) : super(key: key); const NavigationBarView({Key? key}) : super(key: key);
@ -12,7 +12,7 @@ class NavigationBarView extends StatefulWidget {
class _NavigationBarViewState extends State<NavigationBarView> { class _NavigationBarViewState extends State<NavigationBarView> {
final List<Widget> views = [HomeView(), URLListView(), SettingsView()]; final List<Widget> views = [const HomeView(), const URLListView(), const SettingsView()];
int _selectedView = 0; int _selectedView = 0;
@override @override
@ -20,7 +20,7 @@ class _NavigationBarViewState extends State<NavigationBarView> {
return Scaffold( return Scaffold(
body: views.elementAt(_selectedView), body: views.elementAt(_selectedView),
bottomNavigationBar: NavigationBar( bottomNavigationBar: NavigationBar(
destinations: [ destinations: const [
NavigationDestination(icon: Icon(Icons.home), label: "Home"), NavigationDestination(icon: Icon(Icons.home), label: "Home"),
NavigationDestination(icon: Icon(Icons.link), label: "Short URLs"), NavigationDestination(icon: Icon(Icons.link), label: "Short URLs"),
NavigationDestination(icon: Icon(Icons.settings), label: "Settings") NavigationDestination(icon: Icon(Icons.settings), label: "Settings")

View File

@ -15,9 +15,9 @@ class _OpenSourceLicensesViewState extends State<OpenSourceLicensesView> {
return Scaffold( return Scaffold(
body: CustomScrollView( body: CustomScrollView(
slivers: [ slivers: [
SliverAppBar.medium( const SliverAppBar.medium(
expandedHeight: 120, expandedHeight: 120,
title: const Text("Open Source Licenses", style: TextStyle(fontWeight: FontWeight.bold),) title: Text("Open Source Licenses", style: TextStyle(fontWeight: FontWeight.bold),)
), ),
SliverList( SliverList(
delegate: SliverChildBuilderDelegate( delegate: SliverChildBuilderDelegate(
@ -32,23 +32,23 @@ class _OpenSourceLicensesViewState extends State<OpenSourceLicensesView> {
} }
}, },
child: Padding( child: Padding(
padding: EdgeInsets.all(12), padding: const EdgeInsets.all(12),
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
color: Theme.of(context).brightness == Brightness.light ? Colors.grey[100] : Colors.grey[900], color: Theme.of(context).brightness == Brightness.light ? Colors.grey[100] : Colors.grey[900],
), ),
child: Padding( child: Padding(
padding: EdgeInsets.only(left: 12, right: 12, top: 20, bottom: 20), padding: const EdgeInsets.only(left: 12, right: 12, top: 20, bottom: 20),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text("${currentLicense.name}", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)), Text(currentLicense.name, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
Text("Version: ${currentLicense.version ?? "N/A"}", style: TextStyle(color: Colors.grey)), Text("Version: ${currentLicense.version ?? "N/A"}", style: const TextStyle(color: Colors.grey)),
SizedBox(height: 8), const SizedBox(height: 8),
Divider(), const Divider(),
SizedBox(height: 8), const SizedBox(height: 8),
Text("${currentLicense.license}", textAlign: TextAlign.justify, style: TextStyle(color: Colors.grey)), Text(currentLicense.license, textAlign: TextAlign.justify, style: const TextStyle(color: Colors.grey)),
], ],
), ),
), ),
@ -59,7 +59,7 @@ class _OpenSourceLicensesViewState extends State<OpenSourceLicensesView> {
childCount: LicenseUtil.getLicenses().length childCount: LicenseUtil.getLicenses().length
), ),
), ),
SliverToBoxAdapter( const SliverToBoxAdapter(
child: Padding( child: Padding(
padding: EdgeInsets.only(top: 8, bottom: 20), padding: EdgeInsets.only(top: 8, bottom: 20),
child: Text("Thank you to all maintainers of these repositories 💝", style: TextStyle(color: Colors.grey), textAlign: TextAlign.center,), child: Text("Thank you to all maintainers of these repositories 💝", style: TextStyle(color: Colors.grey), textAlign: TextAlign.center,),

View File

@ -1,10 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:shlink_app/API/ServerManager.dart'; import 'package:shlink_app/API/server_manager.dart';
import 'package:shlink_app/LoginView.dart'; import 'package:shlink_app/views/login_view.dart';
import 'package:shlink_app/OpenSourceLicensesView.dart'; import 'package:shlink_app/views/opensource_licenses_view.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'globals.dart' as globals; import '../globals.dart' as globals;
class SettingsView extends StatefulWidget { class SettingsView extends StatefulWidget {
const SettingsView({super.key}); const SettingsView({super.key});
@ -21,9 +21,9 @@ enum ServerStatus {
class _SettingsViewState extends State<SettingsView> { class _SettingsViewState extends State<SettingsView> {
var _server_version = "---"; var _serverVersion = "---";
ServerStatus _server_status = ServerStatus.connecting; ServerStatus _serverStatus = ServerStatus.connecting;
var packageInfo = null; PackageInfo packageInfo = PackageInfo(appName: "", packageName: "", version: "", buildNumber: "");
@override @override
void initState() { void initState() {
@ -34,19 +34,19 @@ class _SettingsViewState extends State<SettingsView> {
} }
void getServerHealth() async { void getServerHealth() async {
var _packageInfo = await PackageInfo.fromPlatform(); var packageInfo = await PackageInfo.fromPlatform();
setState(() { setState(() {
packageInfo = _packageInfo; packageInfo = packageInfo;
}); });
final response = await globals.serverManager.getServerHealth(); final response = await globals.serverManager.getServerHealth();
response.fold((l) { response.fold((l) {
setState(() { setState(() {
_server_version = l.version; _serverVersion = l.version;
_server_status = ServerStatus.connected; _serverStatus = ServerStatus.connected;
}); });
}, (r) { }, (r) {
setState(() { setState(() {
_server_status = ServerStatus.disconnected; _serverStatus = ServerStatus.disconnected;
}); });
var text = ""; var text = "";
@ -74,7 +74,7 @@ class _SettingsViewState extends State<SettingsView> {
PopupMenuButton( PopupMenuButton(
itemBuilder: (context) { itemBuilder: (context) {
return [ return [
PopupMenuItem( const PopupMenuItem(
value: 0, value: 0,
child: Text("Log out...", style: TextStyle(color: Colors.red)), child: Text("Log out...", style: TextStyle(color: Colors.red)),
) )
@ -105,7 +105,7 @@ class _SettingsViewState extends State<SettingsView> {
child: Row( child: Row(
children: [ children: [
Icon(Icons.dns_outlined, color: (() { Icon(Icons.dns_outlined, color: (() {
switch (_server_status) { switch (_serverStatus) {
case ServerStatus.connected: case ServerStatus.connected:
return Colors.green; return Colors.green;
case ServerStatus.connecting: case ServerStatus.connecting:
@ -114,19 +114,19 @@ class _SettingsViewState extends State<SettingsView> {
return Colors.red; return Colors.red;
} }
}())), }())),
SizedBox(width: 8), const SizedBox(width: 8),
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text("Connected to", style: TextStyle(color: Colors.grey)), const Text("Connected to", style: TextStyle(color: Colors.grey)),
Text(globals.serverManager.getServerUrl(), style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), Text(globals.serverManager.getServerUrl(), style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
Row( Row(
children: [ children: [
Text("API Version: ", style: TextStyle(color: Colors.grey, fontWeight: FontWeight.w600)), const Text("API Version: ", style: TextStyle(color: Colors.grey, fontWeight: FontWeight.w600)),
Text("${globals.serverManager.getApiVersion()}", style: TextStyle(color: Colors.grey)), Text(globals.serverManager.getApiVersion(), style: const TextStyle(color: Colors.grey)),
SizedBox(width: 16), const SizedBox(width: 16),
Text("Server Version: ", style: TextStyle(color: Colors.grey, fontWeight: FontWeight.w600)), const Text("Server Version: ", style: TextStyle(color: Colors.grey, fontWeight: FontWeight.w600)),
Text("${_server_version}", style: TextStyle(color: Colors.grey)) Text(_serverVersion, style: const TextStyle(color: Colors.grey))
], ],
), ),
], ],
@ -135,9 +135,9 @@ class _SettingsViewState extends State<SettingsView> {
), ),
), ),
), ),
SizedBox(height: 8), const SizedBox(height: 8),
Divider(), const Divider(),
SizedBox(height: 8), const SizedBox(height: 8),
GestureDetector( GestureDetector(
onTap: () { onTap: () {
Navigator.of(context).push( Navigator.of(context).push(
@ -149,7 +149,7 @@ class _SettingsViewState extends State<SettingsView> {
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
color: Theme.of(context).brightness == Brightness.light ? Colors.grey[100] : Colors.grey[900], color: Theme.of(context).brightness == Brightness.light ? Colors.grey[100] : Colors.grey[900],
), ),
child: Padding( child: const Padding(
padding: EdgeInsets.only(left: 12, right: 12, top: 20, bottom: 20), padding: EdgeInsets.only(left: 12, right: 12, top: 20, bottom: 20),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
@ -167,7 +167,7 @@ class _SettingsViewState extends State<SettingsView> {
), ),
), ),
), ),
SizedBox(height: 16), const SizedBox(height: 16),
GestureDetector( GestureDetector(
onTap: () async { onTap: () async {
var url = Uri.parse("https://github.com/rainloreley/shlink-mobile-app"); var url = Uri.parse("https://github.com/rainloreley/shlink-mobile-app");
@ -180,7 +180,7 @@ class _SettingsViewState extends State<SettingsView> {
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
color: Theme.of(context).brightness == Brightness.light ? Colors.grey[100] : Colors.grey[900], color: Theme.of(context).brightness == Brightness.light ? Colors.grey[100] : Colors.grey[900],
), ),
child: Padding( child: const Padding(
padding: EdgeInsets.only(left: 12, right: 12, top: 20, bottom: 20), padding: EdgeInsets.only(left: 12, right: 12, top: 20, bottom: 20),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
@ -198,7 +198,7 @@ class _SettingsViewState extends State<SettingsView> {
), ),
), ),
), ),
SizedBox(height: 16), const SizedBox(height: 16),
GestureDetector( GestureDetector(
onTap: () async { onTap: () async {
var url = Uri.parse("https://abmgrt.dev/shlink-manager/privacy"); var url = Uri.parse("https://abmgrt.dev/shlink-manager/privacy");
@ -211,7 +211,7 @@ class _SettingsViewState extends State<SettingsView> {
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
color: Theme.of(context).brightness == Brightness.light ? Colors.grey[100] : Colors.grey[900], color: Theme.of(context).brightness == Brightness.light ? Colors.grey[100] : Colors.grey[900],
), ),
child: Padding( child: const Padding(
padding: EdgeInsets.only(left: 12, right: 12, top: 20, bottom: 20), padding: EdgeInsets.only(left: 12, right: 12, top: 20, bottom: 20),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
@ -229,12 +229,12 @@ class _SettingsViewState extends State<SettingsView> {
), ),
), ),
), ),
SizedBox(height: 16), const SizedBox(height: 16),
if (packageInfo != null) if (packageInfo.appName != "")
Row( Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
Text("${packageInfo.appName}, v${packageInfo.version} (${packageInfo.buildNumber})", style: TextStyle(color: Colors.grey),),], Text("${packageInfo.appName}, v${packageInfo.version} (${packageInfo.buildNumber})", style: const TextStyle(color: Colors.grey),),],
) )
], ],
) )

View File

@ -1,8 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:shlink_app/API/Classes/ShortURLSubmission/ShortURLSubmission.dart'; import 'package:shlink_app/API/Classes/ShortURLSubmission/short_url_submission.dart';
import 'package:shlink_app/API/ServerManager.dart'; import 'package:shlink_app/API/server_manager.dart';
import 'globals.dart' as globals; import '../globals.dart' as globals;
class ShortURLEditView extends StatefulWidget { class ShortURLEditView extends StatefulWidget {
const ShortURLEditView({super.key}); const ShortURLEditView({super.key});
@ -34,7 +34,7 @@ class _ShortURLEditViewState extends State<ShortURLEditView> with SingleTickerPr
void initState() { void initState() {
_customSlugDiceAnimationController = AnimationController( _customSlugDiceAnimationController = AnimationController(
vsync: this, vsync: this,
duration: Duration(milliseconds: 500), duration: const Duration(milliseconds: 500),
); );
super.initState(); super.initState();
} }
@ -67,11 +67,11 @@ class _ShortURLEditViewState extends State<ShortURLEditView> with SingleTickerPr
if (copyToClipboard) { if (copyToClipboard) {
await Clipboard.setData(ClipboardData(text: l)); await Clipboard.setData(ClipboardData(text: l));
final snackBar = SnackBar(content: Text("Copied to clipboard!"), backgroundColor: Colors.green[400], behavior: SnackBarBehavior.floating); final snackBar = SnackBar(content: const Text("Copied to clipboard!"), backgroundColor: Colors.green[400], behavior: SnackBarBehavior.floating);
ScaffoldMessenger.of(context).showSnackBar(snackBar); ScaffoldMessenger.of(context).showSnackBar(snackBar);
} }
else { else {
final snackBar = SnackBar(content: Text("Short URL created!"), backgroundColor: Colors.green[400], behavior: SnackBarBehavior.floating); final snackBar = SnackBar(content: const Text("Short URL created!"), backgroundColor: Colors.green[400], behavior: SnackBarBehavior.floating);
ScaffoldMessenger.of(context).showSnackBar(snackBar); ScaffoldMessenger.of(context).showSnackBar(snackBar);
} }
Navigator.pop(context); Navigator.pop(context);
@ -89,8 +89,8 @@ class _ShortURLEditViewState extends State<ShortURLEditView> with SingleTickerPr
} }
else { else {
text = (r as ApiFailure).detail; text = (r as ApiFailure).detail;
if ((r as ApiFailure).invalidElements != null) { if ((r).invalidElements != null) {
text = text + ": " + (r as ApiFailure).invalidElements.toString(); text = "$text: ${(r).invalidElements}";
} }
} }
@ -106,12 +106,12 @@ class _ShortURLEditViewState extends State<ShortURLEditView> with SingleTickerPr
return Scaffold( return Scaffold(
body: CustomScrollView( body: CustomScrollView(
slivers: [ slivers: [
SliverAppBar.medium( const SliverAppBar.medium(
title: Text("New Short URL", style: TextStyle(fontWeight: FontWeight.bold)), title: Text("New Short URL", style: TextStyle(fontWeight: FontWeight.bold)),
), ),
SliverToBoxAdapter( SliverToBoxAdapter(
child: Padding( child: Padding(
padding: EdgeInsets.only(left: 16, right: 16, top: 16), padding: const EdgeInsets.only(left: 16, right: 16, top: 16),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
@ -119,8 +119,8 @@ class _ShortURLEditViewState extends State<ShortURLEditView> with SingleTickerPr
controller: longUrlController, controller: longUrlController,
decoration: InputDecoration( decoration: InputDecoration(
errorText: longUrlError != "" ? longUrlError : null, errorText: longUrlError != "" ? longUrlError : null,
border: OutlineInputBorder(), border: const OutlineInputBorder(),
label: Row( label: const Row(
children: [ children: [
Icon(Icons.public), Icon(Icons.public),
SizedBox(width: 8), SizedBox(width: 8),
@ -129,7 +129,7 @@ class _ShortURLEditViewState extends State<ShortURLEditView> with SingleTickerPr
) )
), ),
), ),
SizedBox(height: 16), const SizedBox(height: 16),
Row( Row(
children: [ children: [
Expanded( Expanded(
@ -137,23 +137,25 @@ class _ShortURLEditViewState extends State<ShortURLEditView> with SingleTickerPr
controller: customSlugController, controller: customSlugController,
style: TextStyle(color: randomSlug ? Colors.grey : Theme.of(context).brightness == Brightness.light ? Colors.black : Colors.white), style: TextStyle(color: randomSlug ? Colors.grey : Theme.of(context).brightness == Brightness.light ? Colors.black : Colors.white),
onChanged: (_) { onChanged: (_) {
if (randomSlug) setState(() { if (randomSlug) {
setState(() {
randomSlug = false; randomSlug = false;
}); });
}
}, },
decoration: InputDecoration( decoration: InputDecoration(
border: OutlineInputBorder(), border: const OutlineInputBorder(),
label: Row( label: Row(
children: [ children: [
Icon(Icons.link), const Icon(Icons.link),
SizedBox(width: 8), const SizedBox(width: 8),
Text("${randomSlug ? "Random" : "Custom"} slug", style: TextStyle(fontStyle: randomSlug ? FontStyle.italic : FontStyle.normal),) Text("${randomSlug ? "Random" : "Custom"} slug", style: TextStyle(fontStyle: randomSlug ? FontStyle.italic : FontStyle.normal),)
], ],
) )
), ),
), ),
), ),
SizedBox(width: 8), const SizedBox(width: 8),
RotationTransition( RotationTransition(
turns: Tween(begin: 0.0, end: 3.0).animate(CurvedAnimation(parent: _customSlugDiceAnimationController, curve: Curves.easeInOutExpo)), turns: Tween(begin: 0.0, end: 3.0).animate(CurvedAnimation(parent: _customSlugDiceAnimationController, curve: Curves.easeInOutExpo)),
child: IconButton( child: IconButton(
@ -175,13 +177,13 @@ class _ShortURLEditViewState extends State<ShortURLEditView> with SingleTickerPr
], ],
), ),
if (randomSlug) if (randomSlug)
SizedBox(height: 16), const SizedBox(height: 16),
if (randomSlug) if (randomSlug)
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text("Random slug length"), const Text("Random slug length"),
SizedBox( SizedBox(
width: 100, width: 100,
child: TextField( child: TextField(
@ -189,8 +191,8 @@ class _ShortURLEditViewState extends State<ShortURLEditView> with SingleTickerPr
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
decoration: InputDecoration( decoration: InputDecoration(
errorText: randomSlugLengthError != "" ? "" : null, errorText: randomSlugLengthError != "" ? "" : null,
border: OutlineInputBorder(), border: const OutlineInputBorder(),
label: Row( label: const Row(
children: [ children: [
Icon(Icons.tag), Icon(Icons.tag),
SizedBox(width: 8), SizedBox(width: 8),
@ -201,10 +203,10 @@ class _ShortURLEditViewState extends State<ShortURLEditView> with SingleTickerPr
)) ))
], ],
), ),
SizedBox(height: 16), const SizedBox(height: 16),
TextField( TextField(
controller: titleController, controller: titleController,
decoration: InputDecoration( decoration: const InputDecoration(
border: OutlineInputBorder(), border: OutlineInputBorder(),
label: Row( label: Row(
children: [ children: [
@ -215,11 +217,11 @@ class _ShortURLEditViewState extends State<ShortURLEditView> with SingleTickerPr
) )
), ),
), ),
SizedBox(height: 16), const SizedBox(height: 16),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text("Crawlable"), const Text("Crawlable"),
Switch( Switch(
value: isCrawlable, value: isCrawlable,
onChanged: (_) { onChanged: (_) {
@ -230,11 +232,11 @@ class _ShortURLEditViewState extends State<ShortURLEditView> with SingleTickerPr
) )
], ],
), ),
SizedBox(height: 16), const SizedBox(height: 16),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text("Forward query params"), const Text("Forward query params"),
Switch( Switch(
value: forwardQuery, value: forwardQuery,
onChanged: (_) { onChanged: (_) {
@ -245,11 +247,11 @@ class _ShortURLEditViewState extends State<ShortURLEditView> with SingleTickerPr
) )
], ],
), ),
SizedBox(height: 16), const SizedBox(height: 16),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text("Copy to clipboard"), const Text("Copy to clipboard"),
Switch( Switch(
value: copyToClipboard, value: copyToClipboard,
onChanged: (_) { onChanged: (_) {
@ -293,7 +295,7 @@ class _ShortURLEditViewState extends State<ShortURLEditView> with SingleTickerPr
} }
} }
}, },
child: isSaving ? Padding(padding: EdgeInsets.all(16), child: CircularProgressIndicator(strokeWidth: 3)) : Icon(Icons.save) child: isSaving ? const Padding(padding: EdgeInsets.all(16), child: CircularProgressIndicator(strokeWidth: 3)) : const Icon(Icons.save)
), ),
); );
} }

View File

@ -1,9 +1,9 @@
import 'package:dynamic_color/dynamic_color.dart'; import 'package:dynamic_color/dynamic_color.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shlink_app/API/Classes/ShortURL/ShortURL.dart'; import 'package:shlink_app/API/Classes/ShortURL/short_url.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:shlink_app/API/ServerManager.dart'; import 'package:shlink_app/API/server_manager.dart';
import 'globals.dart' as globals; import '../globals.dart' as globals;
class URLDetailView extends StatefulWidget { class URLDetailView extends StatefulWidget {
const URLDetailView({super.key, required this.shortURL}); const URLDetailView({super.key, required this.shortURL});
@ -26,9 +26,9 @@ class _URLDetailViewState extends State<URLDetailView> {
child: ListBody( child: ListBody(
children: [ children: [
const Text("You're about to delete"), const Text("You're about to delete"),
SizedBox(height: 4), const SizedBox(height: 4),
Text("${widget.shortURL.title ?? widget.shortURL.shortCode}", style: TextStyle(fontStyle: FontStyle.italic),), Text(widget.shortURL.title ?? widget.shortURL.shortCode, style: const TextStyle(fontStyle: FontStyle.italic),),
SizedBox(height: 4), const SizedBox(height: 4),
const Text("It'll be gone forever! (a very long time)") const Text("It'll be gone forever! (a very long time)")
], ],
), ),
@ -43,7 +43,7 @@ class _URLDetailViewState extends State<URLDetailView> {
Navigator.pop(context); Navigator.pop(context);
Navigator.pop(context, "reload"); Navigator.pop(context, "reload");
final snackBar = SnackBar(content: Text("Short URL deleted!"), backgroundColor: Colors.green[400], behavior: SnackBarBehavior.floating); final snackBar = SnackBar(content: const Text("Short URL deleted!"), backgroundColor: Colors.green[400], behavior: SnackBarBehavior.floating);
ScaffoldMessenger.of(context).showSnackBar(snackBar); ScaffoldMessenger.of(context).showSnackBar(snackBar);
return true; return true;
}, (r) { }, (r) {
@ -60,7 +60,7 @@ class _URLDetailViewState extends State<URLDetailView> {
return false; return false;
}); });
}, },
child: Text("Delete", style: TextStyle(color: Colors.red)), child: const Text("Delete", style: TextStyle(color: Colors.red)),
) )
], ],
); );
@ -74,11 +74,11 @@ class _URLDetailViewState extends State<URLDetailView> {
body: CustomScrollView( body: CustomScrollView(
slivers: [ slivers: [
SliverAppBar.medium( SliverAppBar.medium(
title: Text(widget.shortURL.title ?? widget.shortURL.shortCode, style: TextStyle(fontWeight: FontWeight.bold)), title: Text(widget.shortURL.title ?? widget.shortURL.shortCode, style: const TextStyle(fontWeight: FontWeight.bold)),
actions: [ actions: [
IconButton(onPressed: () { IconButton(onPressed: () {
showDeletionConfirmation(); showDeletionConfirmation();
}, icon: Icon(Icons.delete, color: Colors.red,)) }, icon: const Icon(Icons.delete, color: Colors.red,))
], ],
), ),
SliverToBoxAdapter( SliverToBoxAdapter(
@ -88,9 +88,9 @@ class _URLDetailViewState extends State<URLDetailView> {
children: widget.shortURL.tags.map((tag) { children: widget.shortURL.tags.map((tag) {
var randomColor = ([...Colors.primaries]..shuffle()).first.harmonizeWith(Theme.of(context).colorScheme.primary); var randomColor = ([...Colors.primaries]..shuffle()).first.harmonizeWith(Theme.of(context).colorScheme.primary);
return Padding( return Padding(
padding: EdgeInsets.only(right: 4, top: 4), padding: const EdgeInsets.only(right: 4, top: 4),
child: Container( child: Container(
padding: EdgeInsets.only(top: 4, bottom: 4, left: 12, right: 12), padding: const EdgeInsets.only(top: 4, bottom: 4, left: 12, right: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
color: randomColor, color: randomColor,
@ -109,11 +109,11 @@ class _URLDetailViewState extends State<URLDetailView> {
_ListCell(title: "Android", content: widget.shortURL.deviceLongUrls.android, sub: true), _ListCell(title: "Android", content: widget.shortURL.deviceLongUrls.android, sub: true),
_ListCell(title: "Desktop", content: widget.shortURL.deviceLongUrls.desktop, sub: true), _ListCell(title: "Desktop", content: widget.shortURL.deviceLongUrls.desktop, sub: true),
_ListCell(title: "Creation Date", content: widget.shortURL.dateCreated), _ListCell(title: "Creation Date", content: widget.shortURL.dateCreated),
_ListCell(title: "Visits", content: ""), const _ListCell(title: "Visits", content: ""),
_ListCell(title: "Total", content: widget.shortURL.visitsSummary.total, 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: "Non-Bots", content: widget.shortURL.visitsSummary.nonBots, sub: true),
_ListCell(title: "Bots", content: widget.shortURL.visitsSummary.bots, sub: true), _ListCell(title: "Bots", content: widget.shortURL.visitsSummary.bots, sub: true),
_ListCell(title: "Meta", content: ""), const _ListCell(title: "Meta", content: ""),
_ListCell(title: "Valid Since", content: widget.shortURL.meta.validSince, 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: "Valid Until", content: widget.shortURL.meta.validUntil, sub: true),
_ListCell(title: "Max Visits", content: widget.shortURL.meta.maxVisits, sub: true), _ListCell(title: "Max Visits", content: widget.shortURL.meta.maxVisits, sub: true),
@ -146,7 +146,7 @@ class _ListCellState extends State<_ListCell> {
child: Padding( child: Padding(
padding: EdgeInsets.only(top: 16, bottom: widget.last ? 30 : 0), padding: EdgeInsets.only(top: 16, bottom: widget.last ? 30 : 0),
child: Container( child: Container(
padding: EdgeInsets.only(top: 16, left: 8, right: 8), padding: const EdgeInsets.only(top: 16, left: 8, right: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border(top: BorderSide(width: 1, color: MediaQuery.of(context).platformBrightness == Brightness.dark ? Colors.grey[800]! : Colors.grey[300]!)), border: Border(top: BorderSide(width: 1, color: MediaQuery.of(context).platformBrightness == Brightness.dark ? Colors.grey[800]! : Colors.grey[300]!)),
), ),
@ -157,7 +157,7 @@ class _ListCellState extends State<_ListCell> {
children: [ children: [
if (widget.sub) if (widget.sub)
Padding( Padding(
padding: EdgeInsets.only(right: 4), padding: const EdgeInsets.only(right: 4),
child: SizedBox( child: SizedBox(
width: 20, width: 20,
height: 6, height: 6,
@ -169,7 +169,7 @@ class _ListCellState extends State<_ListCell> {
), ),
), ),
), ),
Text(widget.title, style: TextStyle(fontWeight: FontWeight.bold),)], Text(widget.title, style: const TextStyle(fontWeight: FontWeight.bold),)],
), ),
if (widget.content is bool) if (widget.content is bool)
Icon(widget.content ? Icons.check : Icons.close, color: widget.content ? Colors.green : Colors.red) Icon(widget.content ? Icons.check : Icons.close, color: widget.content ? Colors.green : Colors.red)

View File

@ -1,11 +1,11 @@
import 'package:dynamic_color/dynamic_color.dart'; import 'package:dynamic_color/dynamic_color.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:qr_flutter/qr_flutter.dart'; import 'package:qr_flutter/qr_flutter.dart';
import 'package:shlink_app/API/Classes/ShortURL/ShortURL.dart'; import 'package:shlink_app/API/Classes/ShortURL/short_url.dart';
import 'package:shlink_app/API/ServerManager.dart'; import 'package:shlink_app/API/server_manager.dart';
import 'package:shlink_app/ShortURLEditView.dart'; import 'package:shlink_app/views/short_url_edit_view.dart';
import 'package:shlink_app/URLDetailView.dart'; import 'package:shlink_app/views/url_detail_view.dart';
import 'globals.dart' as globals; import '../globals.dart' as globals;
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
class URLListView extends StatefulWidget { class URLListView extends StatefulWidget {
@ -59,10 +59,10 @@ class _URLListViewState extends State<URLListView> {
return Scaffold( return Scaffold(
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
onPressed: () async { onPressed: () async {
final result = await Navigator.of(context).push(MaterialPageRoute(builder: (context) => ShortURLEditView())); await Navigator.of(context).push(MaterialPageRoute(builder: (context) => const ShortURLEditView()));
loadAllShortUrls(); loadAllShortUrls();
}, },
child: Icon(Icons.add), child: const Icon(Icons.add),
), ),
body: Stack( body: Stack(
children: [ children: [
@ -74,19 +74,19 @@ class _URLListViewState extends State<URLListView> {
}, },
child: CustomScrollView( child: CustomScrollView(
slivers: [ slivers: [
SliverAppBar.medium( const SliverAppBar.medium(
title: Text("Short URLs", style: TextStyle(fontWeight: FontWeight.bold)) title: Text("Short URLs", style: TextStyle(fontWeight: FontWeight.bold))
), ),
if (shortUrlsLoaded && shortUrls.length == 0) if (shortUrlsLoaded && shortUrls.isEmpty)
SliverToBoxAdapter( SliverToBoxAdapter(
child: Center( child: Center(
child: Padding( child: Padding(
padding: EdgeInsets.only(top: 50), padding: const EdgeInsets.only(top: 50),
child: Column( child: Column(
children: [ children: [
Text("No Short URLs", style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),), const Text("No Short URLs", style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),),
Padding( Padding(
padding: EdgeInsets.only(top: 8), padding: const EdgeInsets.only(top: 8),
child: Text('Create one by tapping the "+" button below', style: TextStyle(fontSize: 16, color: Colors.grey[600]),), child: Text('Create one by tapping the "+" button below', style: TextStyle(fontSize: 16, color: Colors.grey[600]),),
) )
], ],
@ -96,7 +96,7 @@ class _URLListViewState extends State<URLListView> {
) )
else else
SliverList(delegate: SliverChildBuilderDelegate( SliverList(delegate: SliverChildBuilderDelegate(
(BuildContext _context, int index) { (BuildContext context, int index) {
final shortURL = shortUrls[index]; final shortURL = shortUrls[index];
return ShortURLCell(shortURL: shortURL, reload: () { return ShortURLCell(shortURL: shortURL, reload: () {
loadAllShortUrls(); loadAllShortUrls();
@ -181,7 +181,7 @@ class _ShortURLCellState extends State<ShortURLCell> {
child: Padding( child: Padding(
padding: EdgeInsets.only(left: 8, right: 8, bottom: widget.isLast ? 90 : 0), padding: EdgeInsets.only(left: 8, right: 8, bottom: widget.isLast ? 90 : 0),
child: Container( child: Container(
padding: EdgeInsets.only(left: 8, right: 8, bottom: 16, top: 16), padding: const EdgeInsets.only(left: 8, right: 8, bottom: 16, top: 16),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border(bottom: BorderSide(color: MediaQuery.of(context).platformBrightness == Brightness.dark ? Colors.grey[800]! : Colors.grey[300]!)), border: Border(bottom: BorderSide(color: MediaQuery.of(context).platformBrightness == Brightness.dark ? Colors.grey[800]! : Colors.grey[300]!)),
), ),
@ -193,16 +193,16 @@ class _ShortURLCellState extends State<ShortURLCell> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [
Text("${widget.shortURL.title ?? widget.shortURL.shortCode}", textScaleFactor: 1.4, style: TextStyle(fontWeight: FontWeight.bold),), 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]),), Text(widget.shortURL.longUrl,maxLines: 1, overflow: TextOverflow.ellipsis, textScaleFactor: 0.9, style: TextStyle(color: Colors.grey[600]),),
// List tags in a row // List tags in a row
Wrap( Wrap(
children: widget.shortURL.tags.map((tag) { children: widget.shortURL.tags.map((tag) {
var randomColor = ([...Colors.primaries]..shuffle()).first.harmonizeWith(Theme.of(context).colorScheme.primary); var randomColor = ([...Colors.primaries]..shuffle()).first.harmonizeWith(Theme.of(context).colorScheme.primary);
return Padding( return Padding(
padding: EdgeInsets.only(right: 4, top: 4), padding: const EdgeInsets.only(right: 4, top: 4),
child: Container( child: Container(
padding: EdgeInsets.only(top: 4, bottom: 4, left: 12, right: 12), padding: const EdgeInsets.only(top: 4, bottom: 4, left: 12, right: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
color: randomColor, color: randomColor,
@ -218,12 +218,12 @@ class _ShortURLCellState extends State<ShortURLCell> {
), ),
IconButton(onPressed: () async { IconButton(onPressed: () async {
await Clipboard.setData(ClipboardData(text: widget.shortURL.shortUrl)); await Clipboard.setData(ClipboardData(text: widget.shortURL.shortUrl));
final snackBar = SnackBar(content: Text("Copied to clipboard!"), behavior: SnackBarBehavior.floating, backgroundColor: Colors.green[400]); final snackBar = SnackBar(content: const Text("Copied to clipboard!"), behavior: SnackBarBehavior.floating, backgroundColor: Colors.green[400]);
ScaffoldMessenger.of(context).showSnackBar(snackBar); ScaffoldMessenger.of(context).showSnackBar(snackBar);
}, icon: Icon(Icons.copy)), }, icon: const Icon(Icons.copy)),
IconButton(onPressed: () { IconButton(onPressed: () {
widget.showQRCode(widget.shortURL.shortUrl); widget.showQRCode(widget.shortURL.shortUrl);
}, icon: Icon(Icons.qr_code)) }, icon: const Icon(Icons.qr_code))
], ],
) )
), ),

View File

@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 0.9.0+4 version: 0.9.1+5
environment: environment:
sdk: ^2.19.0 sdk: ^2.19.0