diff --git a/analysis_options.yaml b/analysis_options.yaml index 61b6c4d..287915f 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -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 diff --git a/lib/API/Classes/ShlinkStats/shlink_stats.dart b/lib/API/Classes/ShlinkStats/shlink_stats.dart index 490d0a8..529abb3 100644 --- a/lib/API/Classes/ShlinkStats/shlink_stats.dart +++ b/lib/API/Classes/ShlinkStats/shlink_stats.dart @@ -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); -} \ No newline at end of file + ShlinkStats(this.nonOrphanVisits, this.orphanVisits, this.shortUrlsCount, + this.tagsCount); +} diff --git a/lib/API/Classes/ShlinkStats/shlink_stats_visits.dart b/lib/API/Classes/ShlinkStats/shlink_stats_visits.dart index f92cb0b..2b74024 100644 --- a/lib/API/Classes/ShlinkStats/shlink_stats_visits.dart +++ b/lib/API/Classes/ShlinkStats/shlink_stats_visits.dart @@ -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"]; -} \ No newline at end of file +} diff --git a/lib/API/Classes/ShortURL/device_long_urls.dart b/lib/API/Classes/ShortURL/device_long_urls.dart index ad6b96b..e1e17d8 100644 --- a/lib/API/Classes/ShortURL/device_long_urls.dart +++ b/lib/API/Classes/ShortURL/device_long_urls.dart @@ -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 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 toJson() => { - "android": android, - "ios": ios, - "desktop": desktop - }; -} \ No newline at end of file + Map toJson() => + {"android": android, "ios": ios, "desktop": desktop}; +} diff --git a/lib/API/Classes/ShortURL/short_url.dart b/lib/API/Classes/ShortURL/short_url.dart index fd70ee3..7825840 100644 --- a/lib/API/Classes/ShortURL/short_url.dart +++ b/lib/API/Classes/ShortURL/short_url.dart @@ -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 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 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"]; - -} \ No newline at end of file + ShortURL.fromJson(Map 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"]; +} diff --git a/lib/API/Classes/ShortURL/short_url_meta.dart b/lib/API/Classes/ShortURL/short_url_meta.dart index d0d01c4..fc6bf42 100644 --- a/lib/API/Classes/ShortURL/short_url_meta.dart +++ b/lib/API/Classes/ShortURL/short_url_meta.dart @@ -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 json): - validSince = json["validSince"] != null ? DateTime.parse(json["validSince"]) : null, - validUntil = json["validUntil"] != null ? DateTime.parse(json["validUntil"]) : null, - maxVisits = json["maxVisits"]; -} \ No newline at end of file + ShortURLMeta.fromJson(Map json) + : validSince = json["validSince"] != null + ? DateTime.parse(json["validSince"]) + : null, + validUntil = json["validUntil"] != null + ? DateTime.parse(json["validUntil"]) + : null, + maxVisits = json["maxVisits"]; +} diff --git a/lib/API/Classes/ShortURL/visits_summary.dart b/lib/API/Classes/ShortURL/visits_summary.dart index 9936796..ef5104d 100644 --- a/lib/API/Classes/ShortURL/visits_summary.dart +++ b/lib/API/Classes/ShortURL/visits_summary.dart @@ -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 json): - total = json["total"] as int, - nonBots = json["nonBots"] as int, - bots = json["bots"] as int; -} \ No newline at end of file + VisitsSummary.fromJson(Map json) + : total = json["total"] as int, + nonBots = json["nonBots"] as int, + bots = json["bots"] as int; +} diff --git a/lib/API/Classes/ShortURLSubmission/short_url_submission.dart b/lib/API/Classes/ShortURLSubmission/short_url_submission.dart index 9b41771..c1cd695 100644 --- a/lib/API/Classes/ShortURLSubmission/short_url_submission.dart +++ b/lib/API/Classes/ShortURLSubmission/short_url_submission.dart @@ -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 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 toJson() { @@ -49,4 +74,4 @@ class ShortURLSubmission { "shortCodeLength": shortCodeLength }; } -} \ No newline at end of file +} diff --git a/lib/API/Methods/connect.dart b/lib/API/Methods/connect.dart index f5ea29b..2feaadc 100644 --- a/lib/API/Methods/connect.dart +++ b/lib/API/Methods/connect.dart @@ -5,25 +5,28 @@ import 'package:http/http.dart' as http; import '../server_manager.dart'; /// Tries to connect to the Shlink server -FutureOr> apiConnect(String? apiKey, String? serverUrl, String apiVersion) async { +FutureOr> 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())); } -} \ No newline at end of file +} diff --git a/lib/API/Methods/delete_short_url.dart b/lib/API/Methods/delete_short_url.dart index 9085cbf..3246615 100644 --- a/lib/API/Methods/delete_short_url.dart +++ b/lib/API/Methods/delete_short_url.dart @@ -5,26 +5,30 @@ import 'package:http/http.dart' as http; import '../server_manager.dart'; /// Deletes a short URL from the server -FutureOr> apiDeleteShortUrl(String shortCode, String? apiKey, String? serverUrl, String apiVersion) async { +FutureOr> 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())); } -} \ No newline at end of file +} diff --git a/lib/API/Methods/get_recent_short_urls.dart b/lib/API/Methods/get_recent_short_urls.dart index 3633ee1..bb5db14 100644 --- a/lib/API/Methods/get_recent_short_urls.dart +++ b/lib/API/Methods/get_recent_short_urls.dart @@ -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, Failure>> apiGetRecentShortUrls(String? apiKey, String? serverUrl, String apiVersion) async { +FutureOr, 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 shortURLs = (jsonResponse["shortUrls"]["data"] as List).map((e) { + List shortURLs = + (jsonResponse["shortUrls"]["data"] as List).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())); } -} \ No newline at end of file +} diff --git a/lib/API/Methods/get_server_health.dart b/lib/API/Methods/get_server_health.dart index 8a6b619..55b7db6 100644 --- a/lib/API/Methods/get_server_health.dart +++ b/lib/API/Methods/get_server_health.dart @@ -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> apiGetServerHealth(String? apiKey, String? serverUrl, String apiVersion) async { +FutureOr> 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())); } -} \ No newline at end of file +} diff --git a/lib/API/Methods/get_shlink_stats.dart b/lib/API/Methods/get_shlink_stats.dart index 5164f6c..9f26ed4 100644 --- a/lib/API/Methods/get_shlink_stats.dart +++ b/lib/API/Methods/get_shlink_stats.dart @@ -7,14 +7,14 @@ import '../Classes/ShlinkStats/shlink_stats.dart'; import '../server_manager.dart'; /// Gets statistics about the Shlink server -FutureOr> apiGetShlinkStats(String? apiKey, String? serverUrl, String apiVersion) async { - +FutureOr> 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> 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> 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> _getVisitStats(String? apiKey, String? serverUrl, String apiVersion) async { +FutureOr> _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> _getShortUrlsCount(String? apiKey, String? serverUrl, String apiVersion) async { +FutureOr> _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> _getTagsCount(String? apiKey, String? serverUrl, String apiVersion) async { +FutureOr> _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())); } -} \ No newline at end of file +} diff --git a/lib/API/Methods/get_short_urls.dart b/lib/API/Methods/get_short_urls.dart index d66241a..116d45e 100644 --- a/lib/API/Methods/get_short_urls.dart +++ b/lib/API/Methods/get_short_urls.dart @@ -6,7 +6,8 @@ import 'package:shlink_app/API/Classes/ShortURL/short_url.dart'; import '../server_manager.dart'; /// Gets all short URLs -FutureOr, Failure>> apiGetShortUrls(String? apiKey, String? serverUrl, String apiVersion) async { +FutureOr, Failure>> apiGetShortUrls( + String? apiKey, String? serverUrl, String apiVersion) async { var currentPage = 1; var maxPages = 2; List allUrls = []; @@ -14,7 +15,8 @@ FutureOr, 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, 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> _getShortUrlPage(int page, String? apiKey, String? serverUrl, String apiVersion) async { +FutureOr> _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 shortURLs = (jsonResponse["shortUrls"]["data"] as List).map((e) { + var pagesCount = + jsonResponse["shortUrls"]["pagination"]["pagesCount"] as int; + List shortURLs = + (jsonResponse["shortUrls"]["data"] as List).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())); } -} \ No newline at end of file +} diff --git a/lib/API/Methods/submit_short_url.dart b/lib/API/Methods/submit_short_url.dart index 4aa6f1c..9d3f580 100644 --- a/lib/API/Methods/submit_short_url.dart +++ b/lib/API/Methods/submit_short_url.dart @@ -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> apiSubmitShortUrl(ShortURLSubmission shortUrl, String? apiKey, String? serverUrl, String apiVersion) async { +FutureOr> 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())); } -} \ No newline at end of file +} diff --git a/lib/API/server_manager.dart b/lib/API/server_manager.dart index 892efc8..cdaeaf9 100644 --- a/lib/API/server_manager.dart +++ b/lib/API/server_manager.dart @@ -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> initAndConnect(String url, String apiKey) async { + FutureOr> 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> submitShortUrl(ShortURLSubmission shortUrl) async { + FutureOr> 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? invalidElements; - ApiFailure({required this.type, required this.detail, required this.title, required this.status, this.invalidElements}); -} \ No newline at end of file + ApiFailure( + {required this.type, + required this.detail, + required this.title, + required this.status, + this.invalidElements}); +} diff --git a/lib/globals.dart b/lib/globals.dart index a75c20a..f7b6ed8 100644 --- a/lib/globals.dart +++ b/lib/globals.dart @@ -1,4 +1,5 @@ library dev.abmgrt.shlink_app.globals; + import 'package:shlink_app/API/server_manager.dart'; -ServerManager serverManager = ServerManager(); \ No newline at end of file +ServerManager serverManager = ServerManager(); diff --git a/lib/main.dart b/lib/main.dart index a16fc50..f8cd4b4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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 { - @override void initState() { super.initState(); @@ -63,26 +60,20 @@ class _InitialPageState extends State { } 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("")), ); } } diff --git a/lib/util/license.dart b/lib/util/license.dart index 237205a..a28c987 100644 --- a/lib/util/license.dart +++ b/lib/util/license.dart @@ -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', ), ]; } diff --git a/lib/views/home_view.dart b/lib/views/home_view.dart index 3e31b7d..69a9bd5 100644 --- a/lib/views/home_view.dart +++ b/lib/views/home_view.dart @@ -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 createState() => _HomeViewState(); } class _HomeViewState extends State { - ShlinkStats? shlinkStats; List shortUrls = []; @@ -27,9 +26,8 @@ class _HomeViewState extends State { void initState() { // TODO: implement initState super.initState(); - WidgetsBinding.instance - .addPostFrameCallback((_) { - loadAllData(); + WidgetsBinding.instance.addPostFrameCallback((_) { + loadAllData(); }); } @@ -49,12 +47,14 @@ class _HomeViewState extends State { 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 { 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 { @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)), ) ], ), - ) - ), + )), ); } } diff --git a/lib/views/login_view.dart b/lib/views/login_view.dart index ad10db6..0210f39 100644 --- a/lib/views/login_view.dart +++ b/lib/views/login_view.dart @@ -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 createState() => _LoginViewState(); @@ -30,11 +30,11 @@ class _LoginViewState extends State { _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 { _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 { }); } - @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 { 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 { 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)) ], ), ) ], ), - ) - ) - ], - ) - ); + )) + ], + )); } } - diff --git a/lib/views/navigationbar_view.dart b/lib/views/navigationbar_view.dart index 65bd9e6..cd1f71f 100644 --- a/lib/views/navigationbar_view.dart +++ b/lib/views/navigationbar_view.dart @@ -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 createState() => _NavigationBarViewState(); } class _NavigationBarViewState extends State { - - final List views = [const HomeView(), const URLListView(), const SettingsView()]; + final List views = [ + const HomeView(), + const URLListView(), + const SettingsView() + ]; int _selectedView = 0; @override diff --git a/lib/views/opensource_licenses_view.dart b/lib/views/opensource_licenses_view.dart index 968219b..6fceca1 100644 --- a/lib/views/opensource_licenses_view.dart +++ b/lib/views/opensource_licenses_view.dart @@ -16,55 +16,68 @@ class _OpenSourceLicensesViewState extends State { 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, + ), + )) ], ), ); diff --git a/lib/views/settings_view.dart b/lib/views/settings_view.dart index 092a29f..8983cc6 100644 --- a/lib/views/settings_view.dart +++ b/lib/views/settings_view.dart @@ -13,24 +13,19 @@ class SettingsView extends StatefulWidget { State createState() => _SettingsViewState(); } -enum ServerStatus { - connected, - connecting, - disconnected -} +enum ServerStatus { connected, connecting, disconnected } class _SettingsViewState extends State { - 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 { 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 { @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 { 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 { 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 { 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 { 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 { 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), + ), + ], ) ], - ) - ), - ) - ], - ) - ); + )), + ) + ], + )); } } diff --git a/lib/views/short_url_edit_view.dart b/lib/views/short_url_edit_view.dart index 7623a40..cd1a14b 100644 --- a/lib/views/short_url_edit_view.dart +++ b/lib/views/short_url_edit_view.dart @@ -11,8 +11,8 @@ class ShortURLEditView extends StatefulWidget { State createState() => _ShortURLEditViewState(); } -class _ShortURLEditViewState extends State with SingleTickerProviderStateMixin { - +class _ShortURLEditViewState extends State + with SingleTickerProviderStateMixin { final longUrlController = TextEditingController(); final customSlugController = TextEditingController(); final titleController = TextEditingController(); @@ -51,13 +51,17 @@ class _ShortURLEditViewState extends State 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 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 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 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 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)), ); } } diff --git a/lib/views/url_detail_view.dart b/lib/views/url_detail_view.dart index 298180f..cc060fa 100644 --- a/lib/views/url_detail_view.dart +++ b/lib/views/url_detail_view.dart @@ -15,7 +15,6 @@ class URLDetailView extends StatefulWidget { } class _URLDetailViewState extends State { - Future showDeletionConfirmation() { return showDialog( context: context, @@ -27,58 +26,75 @@ class _URLDetailViewState extends State { 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 { 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 { } 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") + ], ), - ) - ); + ), + )); } } - diff --git a/lib/views/url_list_view.dart b/lib/views/url_list_view.dart index 8e92d27..c9a3744 100644 --- a/lib/views/url_list_view.dart +++ b/lib/views/url_list_view.dart @@ -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 createState() => _URLListViewState(); } class _URLListViewState extends State { - List 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 loadAllShortUrls() async { @@ -43,120 +41,144 @@ class _URLListViewState extends State { 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 { 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)) + ], + )), + )); } } - diff --git a/pubspec.lock b/pubspec.lock index 2ebd85b..736cccc 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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: diff --git a/pubspec.yaml b/pubspec.yaml index 21829f3..e2c0d35 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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