a lot of work

This commit is contained in:
Adrian Baumgart 2023-07-09 23:00:00 +02:00
parent 68130104da
commit 622496174c
No known key found for this signature in database
21 changed files with 1188 additions and 152 deletions

View File

@ -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);
}

View File

@ -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<String, dynamic> json)
: total = json["total"],
nonBots = json["nonBots"],
bots = json["bots"];
}

View File

@ -1,6 +1,6 @@
import 'package:shlink_app/API/Classes/ShortURL_DeviceLongUrls.dart'; import 'package:shlink_app/API/Classes/ShortURL/ShortURL_DeviceLongUrls.dart';
import 'package:shlink_app/API/Classes/ShortURL_Meta.dart'; import 'package:shlink_app/API/Classes/ShortURL/ShortURL_Meta.dart';
import 'package:shlink_app/API/Classes/ShortURL_VisitsSummary.dart'; import 'package:shlink_app/API/Classes/ShortURL/ShortURL_VisitsSummary.dart';
class ShortURL { class ShortURL {
String shortCode; String shortCode;

View File

@ -1,5 +1,3 @@
import 'dart:convert';
class ShortURL_DeviceLongUrls { class ShortURL_DeviceLongUrls {
final String? android; final String? android;
final String? ios; final String? ios;
@ -11,4 +9,10 @@ class ShortURL_DeviceLongUrls {
: android = json["android"], : android = json["android"],
ios = json["ios"], ios = json["ios"],
desktop = json["desktop"]; desktop = json["desktop"];
Map<String, dynamic> toJson() => {
"android": android,
"ios": ios,
"desktop": desktop
};
} }

View File

@ -0,0 +1,37 @@
import '../ShortURL/ShortURL_DeviceLongUrls.dart';
class ShortURLSubmission {
String longUrl;
ShortURL_DeviceLongUrls? deviceLongUrls;
String? validSince;
String? validUntil;
int? maxVisits;
List<String> 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<String, dynamic> 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
};
}
}

View File

@ -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<Either<String, Failure>> 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()));
}
}

View File

@ -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<Either<String, Failure>> 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()));
}
}

View File

@ -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<Either<ShlinkStats, Failure>> 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<tuple.Tuple3<FutureOr<Either<_ShlinkVisitStats, Failure>>, FutureOr<Either<int, Failure>>, FutureOr<Either<int, Failure>>>> waiterFunction(String? api_key, String? server_url, String apiVersion) async {
late FutureOr<Either<_ShlinkVisitStats, Failure>> visits;
late FutureOr<Either<int, Failure>> shortUrlsCount;
late FutureOr<Either<int, Failure>> tagsCount;
await Future.wait([
_getVisitStats(api_key, server_url, apiVersion).then((value) => visits = value),
_getShortUrlsCount(api_key, server_url, apiVersion).then((value) => shortUrlsCount = value),
_getTagsCount(api_key, server_url, apiVersion).then((value) => tagsCount = value),
]);
return Future.value(tuple.Tuple3(visits, shortUrlsCount, tagsCount));
}*/
class _ShlinkVisitStats {
ShlinkStats_Visits nonOrphanVisits;
ShlinkStats_Visits orphanVisits;
_ShlinkVisitStats(this.nonOrphanVisits, this.orphanVisits);
}
FutureOr<Either<_ShlinkVisitStats, Failure>> _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<Either<int, Failure>> _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<Either<int, Failure>> _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()));
}
}

View File

@ -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<Either<List<ShortURL>, Failure>> API_getShortUrls(String? api_key, String? server_url, String apiVersion) async {
var _currentPage = 1;
var _maxPages = 2;
List<ShortURL> _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<Either<ShortURLPageResponse, Failure>> _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<ShortURL> shortURLs = (jsonResponse["shortUrls"]["data"] as List<dynamic>).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()));
}
}

View File

@ -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<Either<String, Failure>> 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()));
}
}

View File

@ -1,9 +1,15 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:http/http.dart' as http; import 'package:shlink_app/API/Classes/ShlinkStats/ShlinkStats.dart';
import 'package:shlink_app/API/Classes/ShortURL.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 { class ServerManager {
@ -53,80 +59,23 @@ class ServerManager {
FutureOr<Either<String, Failure>> connect() async { FutureOr<Either<String, Failure>> connect() async {
_loadCredentials(); _loadCredentials();
try { return API_connect(_api_key, _server_url, apiVersion);
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()));
}
} }
FutureOr<Either<List<ShortURL>, Failure>> getShortUrls() async { FutureOr<Either<List<ShortURL>, Failure>> getShortUrls() async {
var _currentPage = 1; return API_getShortUrls(_api_key, _server_url, apiVersion);
var _maxPages = 2;
List<ShortURL> _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!);
}
} }
FutureOr<Either<ShortURLPageResponse, Failure>> _getShortUrlPage(int page) async { FutureOr<Either<ShlinkStats, Failure>> getShlinkStats() async {
try { return API_getShlinkStats(_api_key, _server_url, apiVersion);
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<ShortURL> shortURLs = (jsonResponse["shortUrls"]["data"] as List<dynamic>).map((e) {
return ShortURL.fromJson(e);
}).toList();
return left(ShortURLPageResponse(shortURLs, pagesCount));
} }
else {
try { FutureOr<Either<String, Failure>> submitShortUrl(ShortURLSubmission shortUrl) async {
var jsonBody = jsonDecode(response.body); return API_submitShortUrl(shortUrl, _api_key, _server_url, apiVersion);
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<Either<String, Failure>> deleteShortUrl(String shortCode) async {
return API_deleteShortUrl(shortCode, _api_key, _server_url, apiVersion);
} }
} }
@ -151,6 +100,7 @@ class ApiFailure extends Failure {
String detail; String detail;
String title; String title;
int status; int status;
List<dynamic>? 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});
} }

View File

@ -1,4 +1,7 @@
import 'package:flutter/material.dart'; 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; import 'globals.dart' as globals;
class HomeView extends StatefulWidget { class HomeView extends StatefulWidget {
@ -10,10 +13,34 @@ class HomeView extends StatefulWidget {
class _HomeViewState extends State<HomeView> { class _HomeViewState extends State<HomeView> {
ShlinkStats? shlinkStats;
@override @override
void initState() { void initState() {
// TODO: implement initState // TODO: implement initState
super.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 @override
@ -30,9 +57,66 @@ class _HomeViewState extends State<HomeView> {
Text(globals.serverManager.getServerUrl(), style: TextStyle(fontSize: 16, color: Colors.grey[600])) 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)),
)
],
),
)
),
); );
} }
} }

View File

@ -17,9 +17,7 @@ class _NavigationBarViewState extends State<NavigationBarView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: Center( body: views.elementAt(_selectedView),
child: views.elementAt(_selectedView),
),
bottomNavigationBar: NavigationBar( bottomNavigationBar: NavigationBar(
destinations: [ destinations: [
NavigationDestination(icon: Icon(Icons.home), label: "Home"), NavigationDestination(icon: Icon(Icons.home), label: "Home"),

300
lib/ShortURLEditView.dart Normal file
View File

@ -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<ShortURLEditView> createState() => _ShortURLEditViewState();
}
class _ShortURLEditViewState extends State<ShortURLEditView> 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)
),
);
}
}

193
lib/URLDetailView.dart Normal file
View File

@ -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<URLDetailView> createState() => _URLDetailViewState();
}
class _URLDetailViewState extends State<URLDetailView> {
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")
],
),
),
)
);
}
}

View File

@ -1,8 +1,11 @@
import 'package:dynamic_color/dynamic_color.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; import 'package:qr_flutter/qr_flutter.dart';
import 'package:shlink_app/API/Classes/ShortURL.dart'; import 'package:shlink_app/API/Classes/ShortURL/ShortURL.dart';
import 'package:shlink_app/API/ServerManager.dart'; import 'package:shlink_app/API/ServerManager.dart';
import 'package:shlink_app/URLDetailView.dart';
import 'globals.dart' as globals; import 'globals.dart' as globals;
import 'package:flutter/services.dart';
class URLListView extends StatefulWidget { class URLListView extends StatefulWidget {
const URLListView({Key? key}) : super(key: key); const URLListView({Key? key}) : super(key: key);
@ -14,6 +17,8 @@ class URLListView extends StatefulWidget {
class _URLListViewState extends State<URLListView> { class _URLListViewState extends State<URLListView> {
List<ShortURL> shortUrls = []; List<ShortURL> shortUrls = [];
bool _qrCodeShown = false;
String _qrUrl = "";
@override @override
void initState() { void initState() {
@ -23,14 +28,13 @@ class _URLListViewState extends State<URLListView> {
.addPostFrameCallback((_) => loadAllShortUrls()); .addPostFrameCallback((_) => loadAllShortUrls());
} }
Future<void> loadAllShortUrls() async {
void loadAllShortUrls() async {
final response = await globals.serverManager.getShortUrls(); final response = await globals.serverManager.getShortUrls();
response.fold((l) { response.fold((l) {
setState(() { setState(() {
shortUrls = l; shortUrls = l;
}); });
return true;
}, (r) { }, (r) {
var text = ""; var text = "";
if (r is RequestFailure) { if (r is RequestFailure) {
@ -40,24 +44,42 @@ class _URLListViewState extends State<URLListView> {
text = (r as ApiFailure).detail; 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); ScaffoldMessenger.of(context).showSnackBar(snackBar);
return false;
}); });
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: CustomScrollView( 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);
},
child: CustomScrollView(
slivers: [ slivers: [
SliverAppBar.medium( SliverAppBar.medium(
title: Text("All short URLs", style: TextStyle(fontWeight: FontWeight.bold)) title: Text("Short URLs", style: TextStyle(fontWeight: FontWeight.bold))
), ),
SliverList(delegate: SliverChildBuilderDelegate( SliverList(delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) { (BuildContext _context, int index) {
final shortURL = shortUrls[index]; final shortURL = shortUrls[index];
return Padding( 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), padding: EdgeInsets.all(8),
child: Container( child: Container(
padding: EdgeInsets.only(top: 8, bottom: 8), padding: EdgeInsets.only(top: 8, bottom: 8),
@ -75,23 +97,90 @@ class _URLListViewState extends State<URLListView> {
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [
Text("${shortURL.title ?? shortURL.shortCode}", textScaleFactor: 1.4, style: TextStyle(fontWeight: FontWeight.bold),), 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]),) 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: () { IconButton(onPressed: () {
setState(() {
_qrUrl = shortURL.shortUrl;
_qrCodeShown = true;
});
}, icon: Icon(Icons.qr_code)) }, icon: Icon(Icons.qr_code))
], ],
) )
), ),
), ),
)
); );
}, },
childCount: shortUrls.length 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,
),
)
)
),
),
)
],
)
); );
} }
} }

View File

@ -1,8 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:shlink_app/LoginView.dart'; import 'package:shlink_app/LoginView.dart';
import 'package:shlink_app/NavigationBarView.dart'; import 'package:shlink_app/NavigationBarView.dart';
import 'globals.dart' as globals; import 'globals.dart' as globals;
import 'package:dynamic_color/dynamic_color.dart';
void main() { void main() {
runApp(const MyApp()); runApp(const MyApp());
@ -11,18 +11,44 @@ void main() {
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
const MyApp({super.key}); 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. // This widget is the root of your application.
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return DynamicColorBuilder(builder: (lightColorScheme, darkColorScheme) {
return MaterialApp( return MaterialApp(
title: 'Flutter Demo', title: 'Shlink',
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
theme: ThemeData( theme: ThemeData(
primarySwatch: Colors.blue, appBarTheme: AppBarTheme(
brightness: Brightness.light, backgroundColor: Color(0xfffafafa),
),
colorScheme: lightColorScheme ?? _defaultLightColorScheme,
useMaterial3: true useMaterial3: true
), ),
darkTheme: ThemeData( 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,
colorScheme: ColorScheme.fromSwatch().copyWith(
secondary: Colors.orange
)
),*/
/*darkTheme: ThemeData(
primarySwatch: Colors.blue, primarySwatch: Colors.blue,
brightness: Brightness.dark, brightness: Brightness.dark,
useMaterial3: true, useMaterial3: true,
@ -31,10 +57,11 @@ class MyApp extends StatelessWidget {
surface: Color(0xff0d0d0d), surface: Color(0xff0d0d0d),
secondaryContainer: Colors.grey[300] secondaryContainer: Colors.grey[300]
) )
), ),*/
themeMode: ThemeMode.system, themeMode: ThemeMode.system,
home: const InitialPage(), home: InitialPage()
); );
});
} }
} }

View File

@ -57,6 +57,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.10.1" 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: fake_async:
dependency: transitive dependency: transitive
description: description:
@ -160,6 +168,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.2" version: "4.0.2"
intl:
dependency: "direct main"
description:
name: intl
sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d"
url: "https://pub.dev"
source: hosted
version: "0.18.1"
js: js:
dependency: transitive dependency: transitive
description: description:
@ -200,14 +216,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.8.0" 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: path:
dependency: transitive dependency: transitive
description: description:
@ -224,6 +232,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.4" 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: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
@ -277,6 +301,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.4.16" 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: typed_data:
dependency: transitive dependency: transitive
description: description:

View File

@ -19,8 +19,8 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1 version: 1.0.0+1
environment: 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. # Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions # To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively, # consider running `flutter pub upgrade --major-versions`. Alternatively,
@ -39,7 +39,10 @@ dependencies:
flutter_process_text: ^1.1.2 flutter_process_text: ^1.1.2
flutter_secure_storage: ^8.0.0 flutter_secure_storage: ^8.0.0
dartz: ^0.10.1 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: dev_dependencies:
flutter_test: flutter_test: