diff --git a/lib/API/Classes/ShlinkStats/ShlinkStats.dart b/lib/API/Classes/ShlinkStats/ShlinkStats.dart new file mode 100644 index 0000000..232d000 --- /dev/null +++ b/lib/API/Classes/ShlinkStats/ShlinkStats.dart @@ -0,0 +1,10 @@ +import 'package:shlink_app/API/Classes/ShlinkStats/ShlinkStats_Visits.dart'; + +class ShlinkStats { + ShlinkStats_Visits nonOrphanVisits; + ShlinkStats_Visits orphanVisits; + int shortUrlsCount; + int tagsCount; + + ShlinkStats(this.nonOrphanVisits, this.orphanVisits, this.shortUrlsCount, this.tagsCount); +} \ No newline at end of file diff --git a/lib/API/Classes/ShlinkStats/ShlinkStats_Visits.dart b/lib/API/Classes/ShlinkStats/ShlinkStats_Visits.dart new file mode 100644 index 0000000..d43872f --- /dev/null +++ b/lib/API/Classes/ShlinkStats/ShlinkStats_Visits.dart @@ -0,0 +1,12 @@ +class ShlinkStats_Visits { + int total; + int nonBots; + int bots; + + ShlinkStats_Visits(this.total, this.nonBots, this.bots); + + ShlinkStats_Visits.fromJson(Map json) + : total = json["total"], + nonBots = json["nonBots"], + bots = json["bots"]; +} \ No newline at end of file diff --git a/lib/API/Classes/ShortURL.dart b/lib/API/Classes/ShortURL/ShortURL.dart similarity index 82% rename from lib/API/Classes/ShortURL.dart rename to lib/API/Classes/ShortURL/ShortURL.dart index 892c903..89359f4 100644 --- a/lib/API/Classes/ShortURL.dart +++ b/lib/API/Classes/ShortURL/ShortURL.dart @@ -1,6 +1,6 @@ -import 'package:shlink_app/API/Classes/ShortURL_DeviceLongUrls.dart'; -import 'package:shlink_app/API/Classes/ShortURL_Meta.dart'; -import 'package:shlink_app/API/Classes/ShortURL_VisitsSummary.dart'; +import 'package:shlink_app/API/Classes/ShortURL/ShortURL_DeviceLongUrls.dart'; +import 'package:shlink_app/API/Classes/ShortURL/ShortURL_Meta.dart'; +import 'package:shlink_app/API/Classes/ShortURL/ShortURL_VisitsSummary.dart'; class ShortURL { String shortCode; diff --git a/lib/API/Classes/ShortURL_DeviceLongUrls.dart b/lib/API/Classes/ShortURL/ShortURL_DeviceLongUrls.dart similarity index 75% rename from lib/API/Classes/ShortURL_DeviceLongUrls.dart rename to lib/API/Classes/ShortURL/ShortURL_DeviceLongUrls.dart index 87a1b89..48b786f 100644 --- a/lib/API/Classes/ShortURL_DeviceLongUrls.dart +++ b/lib/API/Classes/ShortURL/ShortURL_DeviceLongUrls.dart @@ -1,5 +1,3 @@ -import 'dart:convert'; - class ShortURL_DeviceLongUrls { final String? android; final String? ios; @@ -11,4 +9,10 @@ class ShortURL_DeviceLongUrls { : android = json["android"], ios = json["ios"], desktop = json["desktop"]; + + Map toJson() => { + "android": android, + "ios": ios, + "desktop": desktop + }; } \ No newline at end of file diff --git a/lib/API/Classes/ShortURL_Meta.dart b/lib/API/Classes/ShortURL/ShortURL_Meta.dart similarity index 100% rename from lib/API/Classes/ShortURL_Meta.dart rename to lib/API/Classes/ShortURL/ShortURL_Meta.dart diff --git a/lib/API/Classes/ShortURL_VisitsSummary.dart b/lib/API/Classes/ShortURL/ShortURL_VisitsSummary.dart similarity index 100% rename from lib/API/Classes/ShortURL_VisitsSummary.dart rename to lib/API/Classes/ShortURL/ShortURL_VisitsSummary.dart diff --git a/lib/API/Classes/ShortURLSubmission/ShortURLSubmission.dart b/lib/API/Classes/ShortURLSubmission/ShortURLSubmission.dart new file mode 100644 index 0000000..a2cc76f --- /dev/null +++ b/lib/API/Classes/ShortURLSubmission/ShortURLSubmission.dart @@ -0,0 +1,37 @@ +import '../ShortURL/ShortURL_DeviceLongUrls.dart'; + +class ShortURLSubmission { + String longUrl; + ShortURL_DeviceLongUrls? deviceLongUrls; + String? validSince; + String? validUntil; + int? maxVisits; + List tags; + String? title; + bool crawlable; + bool forwardQuery; + String? customSlug; + bool findIfExists; + String? domain; + 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}); + + Map toJson() { + return { + "longUrl": longUrl, + "deviceLongUrls": deviceLongUrls?.toJson(), + "validSince": validSince, + "validUntil": validUntil, + "maxVisits": maxVisits, + "tags": tags, + "title": title, + "crawlable": crawlable, + "forwardQuery": forwardQuery, + "customSlug": customSlug, + "findIfExists": findIfExists, + "domain": domain, + "shortCodeLength": shortCodeLength + }; + } +} \ No newline at end of file diff --git a/lib/API/Methods/connect.dart b/lib/API/Methods/connect.dart new file mode 100644 index 0000000..77dbbb2 --- /dev/null +++ b/lib/API/Methods/connect.dart @@ -0,0 +1,28 @@ +import 'dart:async'; +import 'dart:convert'; +import 'package:dartz/dartz.dart'; +import 'package:http/http.dart' as http; +import '../ServerManager.dart'; + +FutureOr> API_connect(String? api_key, String? server_url, String apiVersion) async { + try { + final response = await http.get(Uri.parse("${server_url}/rest/v${apiVersion}/short-urls"), headers: { + "X-Api-Key": api_key ?? "", + }); + if (response.statusCode == 200) { + return left(""); + } + 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(RequestFailure(response.statusCode, resErr.toString())); + } + } + } + catch(reqErr) { + return right(RequestFailure(0, reqErr.toString())); + } +} \ No newline at end of file diff --git a/lib/API/Methods/deleteShortUrl.dart b/lib/API/Methods/deleteShortUrl.dart new file mode 100644 index 0000000..a983ec2 --- /dev/null +++ b/lib/API/Methods/deleteShortUrl.dart @@ -0,0 +1,29 @@ +import 'dart:async'; +import 'dart:convert'; +import 'package:dartz/dartz.dart'; +import 'package:http/http.dart' as http; +import '../ServerManager.dart'; + +FutureOr> API_deleteShortUrl(String shortCode, String? api_key, String? server_url, String apiVersion) async { + try { + final response = await http.delete(Uri.parse("${server_url}/rest/v${apiVersion}/short-urls/${shortCode}"), headers: { + "X-Api-Key": api_key ?? "", + }); + if (response.statusCode == 204) { + // get returned short url + return left(""); + } + 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(RequestFailure(response.statusCode, resErr.toString())); + } + } + } + catch(reqErr) { + return right(RequestFailure(0, reqErr.toString())); + } +} \ No newline at end of file diff --git a/lib/API/Methods/getShlinkStats.dart b/lib/API/Methods/getShlinkStats.dart new file mode 100644 index 0000000..b6a7b4f --- /dev/null +++ b/lib/API/Methods/getShlinkStats.dart @@ -0,0 +1,149 @@ +import 'dart:async'; +import 'dart:convert'; +import 'package:dartz/dartz.dart'; +import 'package:http/http.dart' as http; +import 'package:shlink_app/API/Classes/ShlinkStats/ShlinkStats_Visits.dart'; +import '../Classes/ShlinkStats/ShlinkStats.dart'; +import '../ServerManager.dart'; + +FutureOr> API_getShlinkStats(String? api_key, String? server_url, String apiVersion) async { + + var nonOrphanVisits; + var orphanVisits; + var shortUrlsCount; + var tagsCount; + var failure; + + var visitStatsResponse = await _getVisitStats(api_key, server_url, apiVersion); + visitStatsResponse.fold((l) { + nonOrphanVisits = l.nonOrphanVisits; + orphanVisits = l.orphanVisits; + }, (r) { + failure = r; + return right(r); + }); + + var shortUrlsCountResponse = await _getShortUrlsCount(api_key, server_url, apiVersion); + shortUrlsCountResponse.fold((l) { + shortUrlsCount = l; + }, (r) { + failure = r; + return right(r); + }); + + var tagsCountResponse = await _getTagsCount(api_key, server_url, apiVersion); + tagsCountResponse.fold((l) { + tagsCount = l; + }, (r) { + failure = r; + return right(r); + }); + + while(failure == null && (nonOrphanVisits == null || orphanVisits == null || shortUrlsCount == null || tagsCount == null)) { + await Future.delayed(Duration(milliseconds: 100)); + } + + if (failure != null) { + return right(failure); + } + return left(ShlinkStats(nonOrphanVisits, orphanVisits, shortUrlsCount, tagsCount)); +} + +/*Future>, FutureOr>, FutureOr>>> waiterFunction(String? api_key, String? server_url, String apiVersion) async { + late FutureOr> visits; + late FutureOr> shortUrlsCount; + late FutureOr> tagsCount; + + await Future.wait([ + _getVisitStats(api_key, server_url, apiVersion).then((value) => visits = value), + _getShortUrlsCount(api_key, server_url, apiVersion).then((value) => shortUrlsCount = value), + _getTagsCount(api_key, server_url, apiVersion).then((value) => tagsCount = value), + ]); + + return Future.value(tuple.Tuple3(visits, shortUrlsCount, tagsCount)); + +}*/ + +class _ShlinkVisitStats { + ShlinkStats_Visits nonOrphanVisits; + ShlinkStats_Visits orphanVisits; + + _ShlinkVisitStats(this.nonOrphanVisits, this.orphanVisits); +} + +FutureOr> _getVisitStats(String? api_key, String? server_url, String apiVersion) async { + try { + final response = await http.get(Uri.parse("${server_url}/rest/v${apiVersion}/visits"), headers: { + "X-Api-Key": api_key ?? "", + }); + if (response.statusCode == 200) { + var jsonResponse = jsonDecode(response.body); + var nonOrphanVisits = ShlinkStats_Visits.fromJson(jsonResponse["visits"]["nonOrphanVisits"]); + var orphanVisits = ShlinkStats_Visits.fromJson(jsonResponse["visits"]["orphanVisits"]); + return left(_ShlinkVisitStats(nonOrphanVisits, orphanVisits)); + + } + 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(RequestFailure(response.statusCode, resErr.toString())); + } + } + } + catch(reqErr) { + return right(RequestFailure(0, reqErr.toString())); + } +} + +// get short urls count +FutureOr> _getShortUrlsCount(String? api_key, String? server_url, String apiVersion) async { + try { + final response = await http.get(Uri.parse("${server_url}/rest/v${apiVersion}/short-urls"), headers: { + "X-Api-Key": api_key ?? "", + }); + if (response.statusCode == 200) { + var jsonResponse = jsonDecode(response.body); + return left(jsonResponse["shortUrls"]["pagination"]["totalItems"]); + } + 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(RequestFailure(response.statusCode, resErr.toString())); + } + } + } + catch(reqErr) { + return right(RequestFailure(0, reqErr.toString())); + } +} + +// get tags count +FutureOr> _getTagsCount(String? api_key, String? server_url, String apiVersion) async { + try { + final response = await http.get(Uri.parse("${server_url}/rest/v${apiVersion}/tags"), headers: { + "X-Api-Key": api_key ?? "", + }); + if (response.statusCode == 200) { + var jsonResponse = jsonDecode(response.body); + return left(jsonResponse["tags"]["pagination"]["totalItems"]); + } + 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(RequestFailure(response.statusCode, resErr.toString())); + } + } + } + catch(reqErr) { + return right(RequestFailure(0, reqErr.toString())); + } +} \ No newline at end of file diff --git a/lib/API/Methods/getShortUrls.dart b/lib/API/Methods/getShortUrls.dart new file mode 100644 index 0000000..252ee25 --- /dev/null +++ b/lib/API/Methods/getShortUrls.dart @@ -0,0 +1,60 @@ +import 'dart:async'; +import 'dart:convert'; +import 'package:dartz/dartz.dart'; +import 'package:http/http.dart' as http; +import 'package:shlink_app/API/Classes/ShortURL/ShortURL.dart'; +import '../ServerManager.dart'; + +FutureOr, Failure>> API_getShortUrls(String? api_key, String? server_url, String apiVersion) async { + var _currentPage = 1; + var _maxPages = 2; + List _allUrls = []; + + Failure? error; + + while (_currentPage <= _maxPages) { + final response = await _getShortUrlPage(_currentPage, api_key, server_url, apiVersion); + response.fold((l) { + _allUrls.addAll(l.urls); + _maxPages = l.totalPages; + _currentPage++; + }, (r) { + _maxPages = 0; + error = r; + }); + } + if (error == null) { + return left(_allUrls); + } + else { + return right(error!); + } +} + +FutureOr> _getShortUrlPage(int page, String? api_key, String? server_url, String apiVersion) async { + try { + final response = await http.get(Uri.parse("${server_url}/rest/v${apiVersion}/short-urls?page=${page}"), headers: { + "X-Api-Key": api_key ?? "", + }); + 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) { + return ShortURL.fromJson(e); + }).toList(); + return left(ShortURLPageResponse(shortURLs, pagesCount)); + } + 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(RequestFailure(response.statusCode, resErr.toString())); + } + } + } + catch(reqErr) { + return right(RequestFailure(0, reqErr.toString())); + } +} \ No newline at end of file diff --git a/lib/API/Methods/submitShortUrl.dart b/lib/API/Methods/submitShortUrl.dart new file mode 100644 index 0000000..bf339cd --- /dev/null +++ b/lib/API/Methods/submitShortUrl.dart @@ -0,0 +1,31 @@ +import 'dart:async'; +import 'dart:convert'; +import 'package:dartz/dartz.dart'; +import 'package:http/http.dart' as http; +import 'package:shlink_app/API/Classes/ShortURLSubmission/ShortURLSubmission.dart'; +import '../ServerManager.dart'; + +FutureOr> API_submitShortUrl(ShortURLSubmission shortUrl, String? api_key, String? server_url, String apiVersion) async { + try { + final response = await http.post(Uri.parse("${server_url}/rest/v${apiVersion}/short-urls"), headers: { + "X-Api-Key": api_key ?? "", + }, body: jsonEncode(shortUrl.toJson())); + if (response.statusCode == 200) { + // get returned short url + var jsonBody = jsonDecode(response.body); + return left(jsonBody["shortUrl"]); + } + 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"] ?? null)); + } + catch(resErr) { + return right(RequestFailure(response.statusCode, resErr.toString())); + } + } + } + catch(reqErr) { + return right(RequestFailure(0, reqErr.toString())); + } +} \ No newline at end of file diff --git a/lib/API/ServerManager.dart b/lib/API/ServerManager.dart index 41421b1..e600594 100644 --- a/lib/API/ServerManager.dart +++ b/lib/API/ServerManager.dart @@ -1,9 +1,15 @@ import 'dart:async'; -import 'dart:convert'; import 'package:dartz/dartz.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:http/http.dart' as http; -import 'package:shlink_app/API/Classes/ShortURL.dart'; +import 'package:shlink_app/API/Classes/ShlinkStats/ShlinkStats.dart'; +import 'package:shlink_app/API/Classes/ShortURL/ShortURL.dart'; +import 'package:shlink_app/API/Classes/ShortURLSubmission/ShortURLSubmission.dart'; +import 'package:shlink_app/API/Methods/connect.dart'; +import 'package:shlink_app/API/Methods/getShlinkStats.dart'; +import 'package:shlink_app/API/Methods/getShortUrls.dart'; + +import 'Methods/deleteShortUrl.dart'; +import 'Methods/submitShortUrl.dart'; class ServerManager { @@ -53,80 +59,23 @@ class ServerManager { FutureOr> connect() async { _loadCredentials(); - try { - final response = await http.get(Uri.parse("${_server_url}/rest/v${apiVersion}/short-urls"), headers: { - "X-Api-Key": _api_key ?? "", - }); - if (response.statusCode == 200) { - return left(""); - } - else { - try { - var jsonBody = jsonDecode(response.body); - return right(ApiFailure(jsonBody["type"], jsonBody["detail"], jsonBody["title"], jsonBody["status"])); - } - catch(resErr) { - return right(RequestFailure(response.statusCode, resErr.toString())); - } - } - } - catch(reqErr) { - return right(RequestFailure(0, reqErr.toString())); - } + return API_connect(_api_key, _server_url, apiVersion); } FutureOr, Failure>> getShortUrls() async { - var _currentPage = 1; - var _maxPages = 2; - List _allUrls = []; - - Failure? error; - - while (_currentPage <= _maxPages) { - final response = await _getShortUrlPage(_currentPage); - response.fold((l) { - _allUrls.addAll(l.urls); - _maxPages = l.totalPages; - _currentPage++; - }, (r) { - _maxPages = 0; - error = r; - }); - } - if (error == null) { - return left(_allUrls); - } - else { - return right(error!); - } + return API_getShortUrls(_api_key, _server_url, apiVersion); } - FutureOr> _getShortUrlPage(int page) async { - try { - final response = await http.get(Uri.parse("${_server_url}/rest/v${apiVersion}/short-urls?page=${page}"), headers: { - "X-Api-Key": _api_key ?? "", - }); - 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) { - return ShortURL.fromJson(e); - }).toList(); - return left(ShortURLPageResponse(shortURLs, pagesCount)); - } - else { - try { - var jsonBody = jsonDecode(response.body); - return right(ApiFailure(jsonBody["type"], jsonBody["detail"], jsonBody["title"], jsonBody["status"])); - } - catch(resErr) { - return right(RequestFailure(response.statusCode, resErr.toString())); - } - } - } - catch(reqErr) { - return right(RequestFailure(0, reqErr.toString())); - } + FutureOr> getShlinkStats() async { + return API_getShlinkStats(_api_key, _server_url, apiVersion); + } + + FutureOr> submitShortUrl(ShortURLSubmission shortUrl) async { + return API_submitShortUrl(shortUrl, _api_key, _server_url, apiVersion); + } + + FutureOr> deleteShortUrl(String shortCode) async { + return API_deleteShortUrl(shortCode, _api_key, _server_url, apiVersion); } } @@ -151,6 +100,7 @@ class ApiFailure extends Failure { String detail; String title; int status; + List? invalidElements; - ApiFailure(this.type, this.detail, this.title, this.status); + ApiFailure({required this.type, required this.detail, required this.title, required this.status, this.invalidElements}); } \ No newline at end of file diff --git a/lib/HomeView.dart b/lib/HomeView.dart index 993426c..0fa3f02 100644 --- a/lib/HomeView.dart +++ b/lib/HomeView.dart @@ -1,4 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:shlink_app/API/Classes/ShlinkStats/ShlinkStats.dart'; +import 'package:shlink_app/API/ServerManager.dart'; +import 'package:shlink_app/ShortURLEditView.dart'; import 'globals.dart' as globals; class HomeView extends StatefulWidget { @@ -10,10 +13,34 @@ class HomeView extends StatefulWidget { class _HomeViewState extends State { + ShlinkStats? shlinkStats; + @override void initState() { // TODO: implement initState super.initState(); + WidgetsBinding.instance + .addPostFrameCallback((_) => loadShlinkStats()); + } + + void loadShlinkStats() async { + final response = await globals.serverManager.getShlinkStats(); + response.fold((l) { + setState(() { + shlinkStats = l; + }); + }, (r) { + var text = ""; + if (r is RequestFailure) { + text = r.description; + } + else { + text = (r as ApiFailure).detail; + } + + final snackBar = SnackBar(content: Text(text), backgroundColor: Colors.red[400], behavior: SnackBarBehavior.floating); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + }); } @override @@ -30,9 +57,66 @@ class _HomeViewState extends State { 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), + ], + ), ) ], ), + floatingActionButton: FloatingActionButton( + onPressed: () { + Navigator.of(context).push(MaterialPageRoute(builder: (context) => ShortURLEditView())); + }, + child: Icon(Icons.add), + ) + ); + } +} + +// stats card widget +class _ShlinkStatsCardWidget extends StatefulWidget { + const _ShlinkStatsCardWidget({this.text, this.icon, this.borderColor}); + + final icon; + final borderColor; + final text; + + @override + State<_ShlinkStatsCardWidget> createState() => _ShlinkStatsCardWidgetState(); +} + +class _ShlinkStatsCardWidgetState extends State<_ShlinkStatsCardWidget> { + @override + Widget build(BuildContext context) { + var randomColor = ([...Colors.primaries]..shuffle()).first; + return Padding( + padding: EdgeInsets.all(4), + child: Container( + padding: EdgeInsets.all(12), + decoration: BoxDecoration( + border: Border.all(color: widget.borderColor ?? randomColor), + borderRadius: BorderRadius.circular(8) + ), + child: SizedBox( + child: Wrap( + children: [ + Icon(widget.icon), + Padding( + padding: EdgeInsets.only(left: 4), + child: Text(widget.text, style: TextStyle(fontWeight: FontWeight.bold)), + ) + ], + ), + ) + ), ); } } diff --git a/lib/NavigationBarView.dart b/lib/NavigationBarView.dart index 2e6e60c..af63c5c 100644 --- a/lib/NavigationBarView.dart +++ b/lib/NavigationBarView.dart @@ -17,9 +17,7 @@ class _NavigationBarViewState extends State { @override Widget build(BuildContext context) { return Scaffold( - body: Center( - child: views.elementAt(_selectedView), - ), + body: views.elementAt(_selectedView), bottomNavigationBar: NavigationBar( destinations: [ NavigationDestination(icon: Icon(Icons.home), label: "Home"), diff --git a/lib/ShortURLEditView.dart b/lib/ShortURLEditView.dart new file mode 100644 index 0000000..1e06cfd --- /dev/null +++ b/lib/ShortURLEditView.dart @@ -0,0 +1,300 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:shlink_app/API/Classes/ShortURLSubmission/ShortURLSubmission.dart'; +import 'package:shlink_app/API/ServerManager.dart'; +import 'globals.dart' as globals; + +class ShortURLEditView extends StatefulWidget { + const ShortURLEditView({super.key}); + + @override + State createState() => _ShortURLEditViewState(); +} + +class _ShortURLEditViewState extends State with SingleTickerProviderStateMixin { + + final longUrlController = TextEditingController(); + final customSlugController = TextEditingController(); + final titleController = TextEditingController(); + final randomSlugLengthController = TextEditingController(text: "5"); + + bool randomSlug = true; + bool isCrawlable = true; + bool forwardQuery = true; + bool copyToClipboard = true; + + String longUrlError = ""; + String randomSlugLengthError = ""; + + bool isSaving = false; + + late AnimationController _customSlugDiceAnimationController; + + @override + void initState() { + _customSlugDiceAnimationController = AnimationController( + vsync: this, + duration: Duration(milliseconds: 500), + ); + super.initState(); + } + + @override + void dispose() { + longUrlController.dispose(); + customSlugController.dispose(); + titleController.dispose(); + randomSlugLengthController.dispose(); + super.dispose(); + } + + void _submitShortUrl() async { + var newSubmission = ShortURLSubmission( + longUrl: longUrlController.text, + 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); + var response = await globals.serverManager.submitShortUrl(newSubmission); + + response.fold((l) async { + setState(() { + isSaving = false; + }); + + if (copyToClipboard) { + await Clipboard.setData(ClipboardData(text: l)); + final snackBar = SnackBar(content: Text("Copied to clipboard!"), backgroundColor: Colors.green[400], behavior: SnackBarBehavior.floating); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + } + else { + final snackBar = SnackBar(content: Text("Short URL created!"), backgroundColor: Colors.green[400], behavior: SnackBarBehavior.floating); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + } + Navigator.pop(context); + + return true; + }, (r) { + setState(() { + isSaving = false; + }); + + var text = ""; + + if (r is RequestFailure) { + text = r.description; + } + else { + text = (r as ApiFailure).detail; + if ((r as ApiFailure).invalidElements != null) { + text = text + ": " + (r as ApiFailure).invalidElements.toString(); + } + } + + 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: [ + SliverAppBar.medium( + title: Text("New Short URL", style: TextStyle(fontWeight: FontWeight.bold)), + ), + SliverToBoxAdapter( + child: Padding( + padding: EdgeInsets.only(left: 16, right: 16, top: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + TextField( + controller: longUrlController, + decoration: InputDecoration( + errorText: longUrlError != "" ? longUrlError : null, + border: OutlineInputBorder(), + label: Row( + children: [ + Icon(Icons.public), + SizedBox(width: 8), + Text("Long URL") + ], + ) + ), + ), + 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( + border: OutlineInputBorder(), + label: Row( + children: [ + Icon(Icons.link), + SizedBox(width: 8), + Text("${randomSlug ? "Random" : "Custom"} slug", style: TextStyle(fontStyle: randomSlug ? FontStyle.italic : FontStyle.normal),) + ], + ) + ), + ), + ), + 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 : Theme.of(context).primaryColor,) + ), + ) + ], + ), + if (randomSlug) + SizedBox(height: 16), + + if (randomSlug) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Random slug length"), + SizedBox( + width: 100, + child: TextField( + controller: randomSlugLengthController, + keyboardType: TextInputType.number, + decoration: InputDecoration( + errorText: randomSlugLengthError != "" ? "" : null, + border: OutlineInputBorder(), + label: Row( + children: [ + Icon(Icons.tag), + SizedBox(width: 8), + Text("Length") + ], + ) + ), + )) + ], + ), + SizedBox(height: 16), + TextField( + controller: titleController, + decoration: InputDecoration( + border: OutlineInputBorder(), + label: Row( + children: [ + Icon(Icons.badge), + SizedBox(width: 8), + Text("Title") + ], + ) + ), + ), + SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Crawlable"), + Switch( + value: isCrawlable, + onChanged: (_) { + setState(() { + isCrawlable = !isCrawlable; + }); + }, + ) + ], + ), + SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Forward query params"), + Switch( + value: forwardQuery, + onChanged: (_) { + setState(() { + forwardQuery = !forwardQuery; + }); + }, + ) + ], + ), + SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Copy to clipboard"), + Switch( + value: copyToClipboard, + onChanged: (_) { + setState(() { + copyToClipboard = !copyToClipboard; + }); + }, + ) + ], + ), + ], + ), + ) + ) + ], + ), + floatingActionButton: FloatingActionButton( + onPressed: () { + if (!isSaving) { + setState(() { + isSaving = true; + longUrlError = ""; + randomSlugLengthError = ""; + }); + 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(); + } + } + }, + child: isSaving ? Padding(padding: EdgeInsets.all(16), child: CircularProgressIndicator(strokeWidth: 3)) : Icon(Icons.save) + ), + ); + } +} diff --git a/lib/URLDetailView.dart b/lib/URLDetailView.dart new file mode 100644 index 0000000..17a2c90 --- /dev/null +++ b/lib/URLDetailView.dart @@ -0,0 +1,193 @@ +import 'package:dynamic_color/dynamic_color.dart'; +import 'package:flutter/material.dart'; +import 'package:shlink_app/API/Classes/ShortURL/ShortURL.dart'; +import 'package:intl/intl.dart'; +import 'package:shlink_app/API/ServerManager.dart'; +import 'globals.dart' as globals; + +class URLDetailView extends StatefulWidget { + const URLDetailView({super.key, required this.shortURL}); + + final ShortURL shortURL; + + @override + State createState() => _URLDetailViewState(); +} + +class _URLDetailViewState extends State { + + Future showDeletionConfirmation() { + return showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text("Delete Short URL"), + content: SingleChildScrollView( + child: ListBody( + children: [ + const Text("You're about to delete"), + SizedBox(height: 4), + Text("${widget.shortURL.title ?? widget.shortURL.shortCode}", style: TextStyle(fontStyle: FontStyle.italic),), + 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: () async { + var response = await globals.serverManager.deleteShortUrl(widget.shortURL.shortCode); + + response.fold((l) { + Navigator.pop(context); + Navigator.pop(context, "reload"); + + final snackBar = SnackBar(content: 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 { + text = (r as ApiFailure).detail; + } + + final snackBar = SnackBar(content: Text(text), backgroundColor: Colors.red[400], behavior: SnackBarBehavior.floating); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + return false; + }); + }, + child: 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: TextStyle(fontWeight: FontWeight.bold)), + actions: [ + IconButton(onPressed: () { + showDeletionConfirmation(); + }, icon: Icon(Icons.delete, color: Colors.red,)) + ], + ), + SliverToBoxAdapter( + child: Padding( + 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: EdgeInsets.only(right: 4, top: 4), + child: Container( + padding: 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: "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: "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: "Domain", content: widget.shortURL.domain), + _ListCell(title: "Crawlable", content: widget.shortURL.crawlable, last: true) + + + ], + ), + ); + } +} + +class _ListCell extends StatefulWidget { + const _ListCell({required this.title, required this.content, this.sub = false, this.last = false}); + + final String title; + final dynamic content; + final bool sub; + final bool last; + + @override + State<_ListCell> createState() => _ListCellState(); +} + +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: 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: EdgeInsets.only(right: 4), + child: SizedBox( + width: 20, + height: 8, + 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: 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 + Text("N/A") + ], + ), + ), + ) + ); + } +} + diff --git a/lib/URLListView.dart b/lib/URLListView.dart index 349a99c..97f3ba4 100644 --- a/lib/URLListView.dart +++ b/lib/URLListView.dart @@ -1,8 +1,11 @@ +import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/material.dart'; -import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; -import 'package:shlink_app/API/Classes/ShortURL.dart'; +import 'package:qr_flutter/qr_flutter.dart'; +import 'package:shlink_app/API/Classes/ShortURL/ShortURL.dart'; import 'package:shlink_app/API/ServerManager.dart'; +import 'package:shlink_app/URLDetailView.dart'; import 'globals.dart' as globals; +import 'package:flutter/services.dart'; class URLListView extends StatefulWidget { const URLListView({Key? key}) : super(key: key); @@ -14,6 +17,8 @@ class URLListView extends StatefulWidget { class _URLListViewState extends State { List shortUrls = []; + bool _qrCodeShown = false; + String _qrUrl = ""; @override void initState() { @@ -23,14 +28,13 @@ class _URLListViewState extends State { .addPostFrameCallback((_) => loadAllShortUrls()); } - - - void loadAllShortUrls() async { + Future loadAllShortUrls() async { final response = await globals.serverManager.getShortUrls(); response.fold((l) { setState(() { shortUrls = l; }); + return true; }, (r) { var text = ""; if (r is RequestFailure) { @@ -40,58 +44,143 @@ class _URLListViewState extends State { text = (r as ApiFailure).detail; } - final snackBar = SnackBar(content: Text(text), 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: [ - SliverAppBar.medium( - title: Text("All short URLs", style: TextStyle(fontWeight: FontWeight.bold)) - ), - SliverList(delegate: SliverChildBuilderDelegate( - (BuildContext context, int index) { - final shortURL = shortUrls[index]; - return Padding( - padding: EdgeInsets.all(8), - child: Container( - padding: EdgeInsets.only(top: 8, bottom: 8), - decoration: BoxDecoration( - border: Border(bottom: BorderSide(color: MediaQuery.of(context).platformBrightness == Brightness.dark ? Colors.grey[800]! : Colors.grey[300]!)), - ), - child: Padding( - padding: EdgeInsets.all(8), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Text("${shortURL.title ?? shortURL.shortCode}", textScaleFactor: 1.4, style: TextStyle(fontWeight: FontWeight.bold),), - Text("${shortURL.longUrl}",maxLines: 1, overflow: TextOverflow.ellipsis, textScaleFactor: 0.9, style: TextStyle(color: Colors.grey[600]),) - ], - ), - ), - IconButton(onPressed: () { - - }, icon: Icon(Icons.qr_code)) - ], - ) - ), - ), - ); + body: Stack( + children: [ + ColorFiltered( + colorFilter: ColorFilter.mode(Colors.black.withOpacity(_qrCodeShown ? 0.4 : 0), BlendMode.srcOver), + child: RefreshIndicator( + onRefresh: () async { + //loadAllShortUrls(); + return loadAllShortUrls(); + //Future.value(true); }, - childCount: shortUrls.length - )) + child: CustomScrollView( + slivers: [ + SliverAppBar.medium( + title: Text("Short URLs", style: TextStyle(fontWeight: FontWeight.bold)) + ), + SliverList(delegate: SliverChildBuilderDelegate( + (BuildContext _context, int index) { + final shortURL = shortUrls[index]; + return GestureDetector( + onTap: () async { + final result = await Navigator.of(context).push(MaterialPageRoute(builder: (context) => URLDetailView(shortURL: shortURL))); + + if (result == "reload") { + loadAllShortUrls(); + } + }, + child: Padding( + padding: EdgeInsets.all(8), + child: Container( + padding: EdgeInsets.only(top: 8, bottom: 8), + decoration: BoxDecoration( + border: Border(bottom: BorderSide(color: MediaQuery.of(context).platformBrightness == Brightness.dark ? Colors.grey[800]! : Colors.grey[300]!)), + ), + child: Padding( + padding: EdgeInsets.all(8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text("${shortURL.title ?? shortURL.shortCode}", textScaleFactor: 1.4, style: TextStyle(fontWeight: FontWeight.bold),), + Text("${shortURL.longUrl}",maxLines: 1, overflow: TextOverflow.ellipsis, textScaleFactor: 0.9, style: TextStyle(color: Colors.grey[600]),), + // List tags in a row + Wrap( + children: shortURL.tags.map((tag) { + var randomColor = ([...Colors.primaries]..shuffle()).first.harmonizeWith(Theme.of(context).colorScheme.primary); + return Padding( + padding: EdgeInsets.only(right: 4, top: 4), + child: Container( + padding: 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() + + ) + ], + ), + ), + IconButton(onPressed: () async { + await Clipboard.setData(ClipboardData(text: shortURL.shortUrl)); + final snackBar = SnackBar(content: Text("Copied to clipboard!"), behavior: SnackBarBehavior.floating, backgroundColor: Colors.green[400]); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + }, icon: Icon(Icons.copy)), + IconButton(onPressed: () { + setState(() { + _qrUrl = shortURL.shortUrl; + _qrCodeShown = true; + }); + }, icon: Icon(Icons.qr_code)) + ], + ) + ), + ), + ) + ); + }, + 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, + ), + ) + ) + ), + ), + ) ], - ), + ) ); } } diff --git a/lib/main.dart b/lib/main.dart index d944c95..c48f786 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:shlink_app/LoginView.dart'; import 'package:shlink_app/NavigationBarView.dart'; import 'globals.dart' as globals; +import 'package:dynamic_color/dynamic_color.dart'; void main() { runApp(const MyApp()); @@ -11,18 +11,44 @@ void main() { class MyApp extends StatelessWidget { const MyApp({super.key}); + static final _defaultLightColorScheme = + ColorScheme.fromSwatch(primarySwatch: Colors.blue); + + static final _defaultDarkColorScheme = ColorScheme.fromSwatch( + primarySwatch: Colors.blue, brightness: Brightness.dark); + // This widget is the root of your application. @override Widget build(BuildContext context) { - return MaterialApp( - title: 'Flutter Demo', - debugShowCheckedModeBanner: false, - theme: ThemeData( + return DynamicColorBuilder(builder: (lightColorScheme, darkColorScheme) { + return MaterialApp( + title: 'Shlink', + debugShowCheckedModeBanner: false, + theme: ThemeData( + appBarTheme: AppBarTheme( + backgroundColor: Color(0xfffafafa), + ), + colorScheme: lightColorScheme ?? _defaultLightColorScheme, + useMaterial3: true + ), + darkTheme: ThemeData( + appBarTheme: AppBarTheme( + backgroundColor: Color(0xff0d0d0d), + foregroundColor: Colors.white, + elevation: 0, + ), + colorScheme: darkColorScheme?.copyWith(background: Colors.black, primary: Colors.blue) ?? _defaultDarkColorScheme, + useMaterial3: true, + ), + /*theme: ThemeData( primarySwatch: Colors.blue, brightness: Brightness.light, - useMaterial3: true - ), - darkTheme: ThemeData( + useMaterial3: true, + colorScheme: ColorScheme.fromSwatch().copyWith( + secondary: Colors.orange + ) + ),*/ + /*darkTheme: ThemeData( primarySwatch: Colors.blue, brightness: Brightness.dark, useMaterial3: true, @@ -31,10 +57,11 @@ class MyApp extends StatelessWidget { surface: Color(0xff0d0d0d), secondaryContainer: Colors.grey[300] ) - ), - themeMode: ThemeMode.system, - home: const InitialPage(), - ); + ),*/ + themeMode: ThemeMode.system, + home: InitialPage() + ); + }); } } diff --git a/pubspec.lock b/pubspec.lock index a402d62..9f4f3c7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -57,6 +57,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.10.1" + dynamic_color: + dependency: "direct main" + description: + name: dynamic_color + sha256: de4798a7069121aee12d5895315680258415de9b00e717723a1bd73d58f0126d + url: "https://pub.dev" + source: hosted + version: "1.6.6" fake_async: dependency: transitive description: @@ -160,6 +168,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + url: "https://pub.dev" + source: hosted + version: "0.18.1" js: dependency: transitive description: @@ -200,14 +216,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.8.0" - modal_bottom_sheet: - dependency: "direct main" - description: - name: modal_bottom_sheet - sha256: "3bba63c62d35c931bce7f8ae23a47f9a05836d8cb3c11122ada64e0b2f3d718f" - url: "https://pub.dev" - source: hosted - version: "3.0.0-pre" path: dependency: transitive description: @@ -224,6 +232,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + qr: + dependency: transitive + description: + name: qr + sha256: "64957a3930367bf97cc211a5af99551d630f2f4625e38af10edd6b19131b64b3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + qr_flutter: + dependency: "direct main" + description: + name: qr_flutter + sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097" + url: "https://pub.dev" + source: hosted + version: "4.1.0" sky_engine: dependency: transitive description: flutter @@ -277,6 +301,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.16" + tuple: + dependency: "direct main" + description: + name: tuple + sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 + url: "https://pub.dev" + source: hosted + version: "2.0.2" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 38f53bc..9d1fd30 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -19,8 +19,8 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: - sdk: '>=2.19.6 <3.0.0' - + #sdk: '>=2.19.6 <3.0.0' + sdk: ^2.19.0 # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions # consider running `flutter pub upgrade --major-versions`. Alternatively, @@ -39,7 +39,10 @@ dependencies: flutter_process_text: ^1.1.2 flutter_secure_storage: ^8.0.0 dartz: ^0.10.1 - modal_bottom_sheet: ^3.0.0-pre + qr_flutter: ^4.1.0 + tuple: ^2.0.2 + intl: ^0.18.1 + dynamic_color: ^1.6.6 dev_dependencies: flutter_test: