Compare commits

...

13 Commits

Author SHA1 Message Date
Adrian Baumgart e1c9bc4d80
bump version to 1.3.1 2024-07-25 20:17:55 +02:00
Adrian Baumgart 413275df38
fixed bug where server list stays empty 2024-07-25 20:17:27 +02:00
Adrian Baumgart 975b2ea3d8
cleaning and bump version to 1.3.0 2024-07-25 19:23:21 +02:00
rainloreley f9c6a58db1 Apply automatic changes 2024-07-25 17:12:51 +00:00
Adrian Baumgart 69c8870997
support multiple shlink instances 2024-07-25 19:11:24 +02:00
Adrian Baumgart ce28066d29
fixed license 2024-07-25 17:40:17 +02:00
Adrian Baumgart e9f98c2171
edited README 2024-07-25 17:39:42 +02:00
rainloreley 39483dca54 Apply automatic changes 2024-07-25 15:38:31 +00:00
Adrian Baumgart ba058e2af3
formatting 2024-07-25 17:37:17 +02:00
Adrian Baumgart 0eea6ee9a2
dependency 2024-07-25 17:35:12 +02:00
Adrian Baumgart 0ade61faea
added support for redirect rules 2024-07-25 17:31:54 +02:00
rainloreley 00bb9af82e Apply automatic changes 2024-07-25 12:51:42 +00:00
Adrian Baumgart 6a5cebdb68
change view title when editing short URL 2024-07-25 14:50:16 +02:00
21 changed files with 1048 additions and 302 deletions

View File

@ -1,42 +0,0 @@
name: Fix formatting and license file
on:
push:
branches: [ "main" ]
paths:
- '**.dart'
- 'pubspec.yaml'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK
uses: actions/setup-java@v4
with:
distribution: 'oracle'
java-version: '21'
- name: Setup Android SDK
uses: android-actions/setup-android@v2
- name: Setup Flutter SDK
uses: flutter-actions/setup-flutter@v2
with:
channel: stable
version: 3.13.7
- name: Install dependencies
run: dart pub get
- name: Verify formatting
run: dart format --output=none .
- name: Update license file
run: flutter packages pub run license_generator generate
- name: Commit and push
uses: stefanzweifel/git-auto-commit-action@v5

View File

@ -17,20 +17,18 @@ Shlink Manager is an app for Android to see and manage all shortened URLs create
## 📱 Current Features
✅ List all short URLs<br/>
✅ Create new short URLs<br/>
✅ Delete short URLs<br/>
✅ Create, edit and delete short URLs<br/>
✅ See overall statistics<br/>
✅ Detailed statistics for each short URL<br/>
✅ Display tags<br/>
✅ Display QR code<br/>
✅ Display tags & QR code<br/>
✅ Dark mode support<br/>
✅ Edit existing short URLs<br/>
✅ Quickly create Short URL via Share Sheet<br/>
✅ View rule-based redirects (no editing yet)<br/>
✅ Use multiple Shlink instances<br/>
## 🔨 To Do
- [ ] Add support for iOS (maybe in the future)
- [ ] add tags
- [ ] Add dynamic rule-based redirects system (Shlink v4.0.0)
- [ ] improve app icon
- [ ] Refactor code
- [ ] ...and more

View File

@ -52,10 +52,12 @@ android {
applicationId "dev.abmgrt.shlink_app"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdkVersion 19 //flutter.minSdkVersion
minSdkVersion flutter.minSdkVersion //flutter.minSdkVersion
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
multiDexEnabled true
}
signingConfigs {
@ -79,4 +81,5 @@ flutter {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.multidex:multidex:2.0.1'
}

View File

@ -0,0 +1,41 @@
enum ConditionDeviceType {
IOS,
ANDROID,
DESKTOP;
static ConditionDeviceType fromApi(String api) {
switch (api) {
case "ios":
return ConditionDeviceType.IOS;
case "android":
return ConditionDeviceType.ANDROID;
case "desktop":
return ConditionDeviceType.DESKTOP;
}
throw ArgumentError("Invalid type $api");
}
}
extension ConditionTypeExtension on ConditionDeviceType {
String get api {
switch (this) {
case ConditionDeviceType.IOS:
return "ios";
case ConditionDeviceType.ANDROID:
return "android";
case ConditionDeviceType.DESKTOP:
return "desktop";
}
}
String get humanReadable {
switch (this) {
case ConditionDeviceType.IOS:
return "iOS";
case ConditionDeviceType.ANDROID:
return "Android";
case ConditionDeviceType.DESKTOP:
return "Desktop";
}
}
}

View File

@ -0,0 +1,24 @@
import 'package:shlink_app/API/Classes/ShortURL/RedirectRule/redirect_rule_condition.dart';
/// Single redirect rule for a short URL.
class RedirectRule {
String longUrl;
int priority;
List<RedirectRuleCondition> conditions;
RedirectRule(this.longUrl, this.priority, this.conditions);
RedirectRule.fromJson(Map<String, dynamic> json)
: longUrl = json["longUrl"],
priority = json["priority"],
conditions = (json["conditions"] as List<dynamic>)
.map((e) => RedirectRuleCondition.fromJson(e))
.toList();
Map<String, dynamic> toJson() {
return {
"longUrl": longUrl,
"conditions": conditions.map((e) => e.toJson()).toList()
};
}
}

View File

@ -0,0 +1,19 @@
import 'package:shlink_app/API/Classes/ShortURL/RedirectRule/redirect_rule_condition_type.dart';
class RedirectRuleCondition {
RedirectRuleConditionType type;
String matchValue;
String? matchKey;
RedirectRuleCondition(String type, this.matchValue, this.matchKey)
: type = RedirectRuleConditionType.fromApi(type);
RedirectRuleCondition.fromJson(Map<String, dynamic> json)
: type = RedirectRuleConditionType.fromApi(json["type"]),
matchValue = json["matchValue"],
matchKey = json["matchKey"];
Map<String, dynamic> toJson() {
return {"type": type.api, "matchValue": matchValue, "matchKey": matchKey};
}
}

View File

@ -0,0 +1,41 @@
enum RedirectRuleConditionType {
DEVICE,
LANGUAGE,
QUERY_PARAM;
static RedirectRuleConditionType fromApi(String api) {
switch (api) {
case "device":
return RedirectRuleConditionType.DEVICE;
case "language":
return RedirectRuleConditionType.LANGUAGE;
case "query-param":
return RedirectRuleConditionType.QUERY_PARAM;
}
throw ArgumentError("Invalid type $api");
}
}
extension ConditionTypeExtension on RedirectRuleConditionType {
String get api {
switch (this) {
case RedirectRuleConditionType.DEVICE:
return "device";
case RedirectRuleConditionType.LANGUAGE:
return "language";
case RedirectRuleConditionType.QUERY_PARAM:
return "query-param";
}
}
String get humanReadable {
switch (this) {
case RedirectRuleConditionType.DEVICE:
return "Device";
case RedirectRuleConditionType.LANGUAGE:
return "Language";
case RedirectRuleConditionType.QUERY_PARAM:
return "Query parameter";
}
}
}

View File

@ -1,6 +1,8 @@
import 'package:shlink_app/API/Classes/ShortURL/short_url_meta.dart';
import 'package:shlink_app/API/Classes/ShortURL/visits_summary.dart';
import 'RedirectRule/redirect_rule.dart';
/// Data about a short URL
class ShortURL {
/// Slug of the short URL used in the URL
@ -33,6 +35,8 @@ class ShortURL {
/// Whether the short URL is crawlable by a web crawler
bool crawlable;
List<RedirectRule>? redirectRules;
ShortURL(
this.shortCode,
this.shortUrl,

View File

@ -0,0 +1,48 @@
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/RedirectRule/redirect_rule.dart';
import '../server_manager.dart';
/// Gets redirect rules for a given short URL (code).
FutureOr<Either<List<RedirectRule>, Failure>> apiGetRedirectRules(
String shortCode,
String? apiKey,
String? serverUrl,
String apiVersion) async {
try {
final response = await http.get(
Uri.parse(
"$serverUrl/rest/v$apiVersion/short-urls/$shortCode/redirect-rules"),
headers: {
"X-Api-Key": apiKey ?? "",
});
if (response.statusCode == 200) {
// get returned redirect rules
var jsonBody = jsonDecode(response.body) as Map<String, dynamic>;
// convert json array to object array
List<RedirectRule> redirectRules =
(jsonBody["redirectRules"] as List<dynamic>)
.map((e) => RedirectRule.fromJson(e))
.toList();
return left(redirectRules);
} else {
try {
var jsonBody = jsonDecode(response.body);
return right(ApiFailure(
type: jsonBody["type"],
detail: jsonBody["detail"],
title: jsonBody["title"],
status: jsonBody["status"],
invalidElements: jsonBody["invalidElements"]));
} catch (resErr) {
return right(RequestFailure(response.statusCode, resErr.toString()));
}
}
} catch (reqErr) {
return right(RequestFailure(0, reqErr.toString()));
}
}

View File

@ -0,0 +1,45 @@
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/RedirectRule/redirect_rule.dart';
import '../server_manager.dart';
/// Saves the redirect rules for a given short URL (code).
FutureOr<Either<bool, Failure>> apiSetRedirectRules(
String shortCode,
List<RedirectRule> redirectRules,
String? apiKey,
String? serverUrl,
String apiVersion) async {
try {
Map<String, dynamic> body = {};
List<Map<String, dynamic>> redirectRulesJson =
redirectRules.map((e) => e.toJson()).toList();
body["redirectRules"] = redirectRulesJson;
final response = await http.post(
Uri.parse(
"$serverUrl/rest/v$apiVersion/short-urls/$shortCode/redirect-rules"),
headers: {
"X-Api-Key": apiKey ?? "",
},
body: jsonEncode(body));
if (response.statusCode == 200) {
return left(true);
} else {
try {
var jsonBody = jsonDecode(response.body);
return right(ApiFailure(
type: jsonBody["type"],
detail: jsonBody["detail"],
title: jsonBody["title"],
status: jsonBody["status"],
invalidElements: jsonBody["invalidElements"]));
} catch (resErr) {
return right(RequestFailure(response.statusCode, resErr.toString()));
}
}
} catch (reqErr) {
return right(RequestFailure(0, reqErr.toString()));
}
}

View File

@ -1,15 +1,19 @@
import 'dart:async';
import 'dart:convert';
import 'package:dartz/dartz.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:shlink_app/API/Classes/ShlinkStats/shlink_stats.dart';
import 'package:shlink_app/API/Classes/ShortURL/RedirectRule/redirect_rule.dart';
import 'package:shlink_app/API/Classes/ShortURL/short_url.dart';
import 'package:shlink_app/API/Classes/ShortURLSubmission/short_url_submission.dart';
import 'package:shlink_app/API/Methods/connect.dart';
import 'package:shlink_app/API/Methods/get_recent_short_urls.dart';
import 'package:shlink_app/API/Methods/get_redirect_rules.dart';
import 'package:shlink_app/API/Methods/get_server_health.dart';
import 'package:shlink_app/API/Methods/get_shlink_stats.dart';
import 'package:shlink_app/API/Methods/get_short_urls.dart';
import 'package:shlink_app/API/Methods/set_redirect_rules.dart';
import 'package:shlink_app/API/Methods/update_short_url.dart';
import 'Methods/delete_short_url.dart';
@ -41,10 +45,41 @@ class ServerManager {
}
/// Logs out the user and removes data about the Shlink server
Future<void> logOut() async {
Future<void> logOut(String url) async {
const storage = FlutterSecureStorage();
await storage.delete(key: "shlink_url");
await storage.delete(key: "shlink_apikey");
final prefs = await SharedPreferences.getInstance();
String? serverMapSerialized = await storage.read(key: "server_map");
if (serverMapSerialized != null) {
Map<String, String> serverMap =
Map.castFrom(jsonDecode(serverMapSerialized));
serverMap.remove(url);
if (serverMap.isEmpty) {
storage.delete(key: "server_map");
} else {
storage.write(key: "server_map", value: jsonEncode(serverMap));
}
if (serverUrl == url) {
serverUrl = null;
apiKey = null;
prefs.remove("lastusedserver");
}
}
}
/// Returns all servers saved in the app
Future<List<String>> getAvailableServers() async {
const storage = FlutterSecureStorage();
String? serverMapSerialized = await storage.read(key: "server_map");
if (serverMapSerialized != null) {
Map<String, String> serverMap =
Map.castFrom(jsonDecode(serverMapSerialized));
return serverMap.keys.toList();
} else {
return [];
}
}
/// Loads the server credentials from [FlutterSecureStorage]
@ -57,16 +92,68 @@ class ServerManager {
prefs.setBool('first_run', false);
} else {
serverUrl = await storage.read(key: "shlink_url");
apiKey = await storage.read(key: "shlink_apikey");
if (await _replaceDeprecatedStorageMethod()) {
_loadCredentials();
return;
}
String? serverMapSerialized = await storage.read(key: "server_map");
String? lastUsedServer = prefs.getString("lastusedserver");
if (serverMapSerialized != null) {
Map<String, String> serverMap =
Map.castFrom(jsonDecode(serverMapSerialized));
if (lastUsedServer != null) {
serverUrl = lastUsedServer;
apiKey = serverMap[lastUsedServer]!;
} else {
List<String> availableServers = serverMap.keys.toList();
if (availableServers.isNotEmpty) {
serverUrl = availableServers.first;
apiKey = serverMap[serverUrl];
prefs.setString("lastusedserver", serverUrl!);
}
}
}
}
}
Future<bool> _replaceDeprecatedStorageMethod() async {
const storage = FlutterSecureStorage();
// deprecated data storage method, replaced because of multi-server support
var v1DataServerurl = await storage.read(key: "shlink_url");
var v1DataApikey = await storage.read(key: "shlink_apikey");
if (v1DataServerurl != null && v1DataApikey != null) {
// conversion to new data storage method
Map<String, String> serverMap = {};
serverMap[v1DataServerurl] = v1DataApikey;
storage.write(key: "server_map", value: jsonEncode(serverMap));
storage.delete(key: "shlink_url");
storage.delete(key: "shlink_apikey");
return true;
} else {
return false;
}
}
/// Saves the provided server credentials to [FlutterSecureStorage]
void _saveCredentials(String url, String apiKey) async {
const storage = FlutterSecureStorage();
storage.write(key: "shlink_url", value: url);
storage.write(key: "shlink_apikey", value: apiKey);
final prefs = await SharedPreferences.getInstance();
String? serverMapSerialized = await storage.read(key: "server_map");
Map<String, String> serverMap;
if (serverMapSerialized != null) {
serverMap = Map.castFrom(jsonDecode(serverMapSerialized));
} else {
serverMap = {};
}
serverMap[url] = apiKey;
storage.write(key: "server_map", value: jsonEncode(serverMap));
prefs.setString("lastusedserver", url);
}
/// Saves provided server credentials and tries to establish a connection
@ -78,7 +165,7 @@ class ServerManager {
_saveCredentials(url, apiKey);
final result = await connect();
result.fold((l) => null, (r) {
logOut();
logOut(url);
});
return result;
}
@ -124,6 +211,19 @@ class ServerManager {
FutureOr<Either<List<ShortURL>, Failure>> getRecentShortUrls() async {
return apiGetRecentShortUrls(apiKey, serverUrl, apiVersion);
}
/// Gets redirect rules for a given short URL (code)
FutureOr<Either<List<RedirectRule>, Failure>> getRedirectRules(
String shortCode) async {
return apiGetRedirectRules(shortCode, apiKey, serverUrl, apiVersion);
}
/// Sets redirect rules for a given short URL (code)
FutureOr<Either<bool, Failure>> setRedirectRules(
String shortCode, List<RedirectRule> redirectRules) async {
return apiSetRedirectRules(
shortCode, redirectRules, apiKey, serverUrl, apiVersion);
}
}
/// Server response data type about a page of short URLs from the server

View File

@ -11,13 +11,13 @@ void main() {
class MyApp extends StatelessWidget {
const MyApp({super.key});
static final ColorScheme _defaultLightColorScheme = ColorScheme
.fromSeed(seedColor: Colors.blue);
static final ColorScheme _defaultLightColorScheme =
ColorScheme.fromSeed(seedColor: Colors.blue);
static final _defaultDarkColorScheme = ColorScheme.fromSeed(
brightness: Brightness.dark,
seedColor: Colors.blue,
background: Colors.black);
brightness: Brightness.dark,
seedColor: Colors.blue,
background: Colors.black);
// This widget is the root of your application.
@override
@ -38,7 +38,7 @@ class MyApp extends StatelessWidget {
foregroundColor: Colors.white,
elevation: 0,
),
colorScheme: darkColorScheme?.copyWith(background: Colors.black) ??
colorScheme: darkColorScheme?.copyWith(surface: Colors.black) ??
_defaultDarkColorScheme,
useMaterial3: true,
),
@ -64,11 +64,13 @@ class _InitialPageState extends State<InitialPage> {
void checkLogin() async {
bool result = await globals.serverManager.checkLogin();
if (result) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => const NavigationBarView()));
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (context) => const NavigationBarView()),
(Route<dynamic> route) => false);
} else {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => const LoginView()));
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (context) => const LoginView()),
(Route<dynamic> route) => false);
}
}

View File

@ -28,7 +28,8 @@ class LicenseUtil {
return [
const License(
name: r'cupertino_icons',
license: r'''The MIT License (MIT)
license: r'''
The MIT License (MIT)
Copyright (c) 2016 Vladimir Kharlampidi
@ -49,12 +50,12 @@ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''',
version: r'^1.0.5',
homepage: null,
repository: r'https://github.com/flutter/packages/tree/main/third_party/packages/cupertino_icons',
),
const License(
name: r'dartz',
license: r'''The MIT License (MIT)
license: r'''
The MIT License (MIT)
Copyright (c) 2016, 2017, 2018, 2019, 2020, 2021 Björn Sperber
@ -78,11 +79,11 @@ SOFTWARE.
''',
version: r'^0.10.1',
homepage: r'https://github.com/spebbe/dartz',
repository: null,
),
const License(
name: r'dynamic_color',
license: r''' Apache License
license: r'''
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
@ -285,12 +286,12 @@ SOFTWARE.
limitations under the License.
''',
version: r'^1.6.6',
homepage: null,
repository: r'https://github.com/material-foundation/flutter-packages/tree/main/packages/dynamic_color',
),
const License(
name: r'flutter',
license: r'''Copyright 2014 The Flutter Authors. All rights reserved.
license: r'''
Copyright 2014 The Flutter Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
@ -316,13 +317,13 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
''',
version: null,
homepage: r'https://flutter.dev/',
repository: r'https://github.com/flutter/flutter',
),
const License(
name: r'flutter_launcher_icons',
license: r'''MIT License
license: r'''
MIT License
Copyright (c) 2019 Mark O'Sullivan
@ -350,7 +351,8 @@ SOFTWARE.
),
const License(
name: r'flutter_lints',
license: r'''Copyright 2013 The Flutter Authors. All rights reserved.
license: r'''
Copyright 2013 The Flutter Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
@ -377,12 +379,12 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
''',
version: r'^3.0.1',
homepage: null,
repository: r'https://github.com/flutter/packages/tree/main/packages/flutter_lints',
),
const License(
name: r'flutter_process_text',
license: r'''BSD 3-Clause License
license: r'''
BSD 3-Clause License
(c) Copyright 2021 divshekhar (Divyanshu Shekhar)
@ -410,12 +412,12 @@ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABI
TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
version: r'^1.1.2',
homepage: null,
repository: r'https://github.com/DevsOnFlutter/flutter_process_text',
),
const License(
name: r'flutter_secure_storage',
license: r'''BSD 3-Clause License
license: r'''
BSD 3-Clause License
Copyright 2017 German Saprykin
All rights reserved.
@ -445,12 +447,12 @@ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
version: r'^9.0.0',
homepage: null,
repository: r'https://github.com/mogol/flutter_secure_storage/tree/develop/flutter_secure_storage',
),
const License(
name: r'flutter_sharing_intent',
license: r''' Apache License
license: r'''
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
@ -653,11 +655,11 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
limitations under the License.''',
version: r'^1.1.1',
homepage: r'https://github.com/bhagat-techind/flutter_sharing_intent.git',
repository: null,
),
const License(
name: r'flutter_test',
license: r'''Copyright 2014 The Flutter Authors. All rights reserved.
license: r'''
Copyright 2014 The Flutter Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
@ -683,13 +685,13 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
''',
version: null,
homepage: r'https://flutter.dev/',
repository: r'https://github.com/flutter/flutter',
),
const License(
name: r'http',
license: r'''Copyright 2014, the Dart project authors.
license: r'''
Copyright 2014, the Dart project authors.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
@ -718,12 +720,12 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
''',
version: r'^1.1.0',
homepage: null,
repository: r'https://github.com/dart-lang/http/tree/master/pkgs/http',
),
const License(
name: r'intl',
license: r'''Copyright 2013, the Dart project authors.
license: r'''
Copyright 2013, the Dart project authors.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
@ -752,12 +754,12 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
''',
version: r'^0.19.0',
homepage: null,
repository: r'https://github.com/dart-lang/i18n/tree/main/pkgs/intl',
),
const License(
name: r'license_generator',
license: r'''MIT License
license: r'''
MIT License
Copyright (c) 2022 icapps
@ -781,11 +783,11 @@ SOFTWARE.
''',
version: r'^2.0.0',
homepage: r'https://github.com/icapps/flutter-icapps-license',
repository: null,
),
const License(
name: r'package_info_plus',
license: r'''Copyright 2017 The Chromium Authors. All rights reserved.
license: r'''
Copyright 2017 The Chromium Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
@ -819,7 +821,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
),
const License(
name: r'qr_flutter',
license: r'''BSD 3-Clause License
license: r'''
BSD 3-Clause License
Copyright (c) 2020, Luke Freeman.
All rights reserved.
@ -851,11 +854,11 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
''',
version: r'^4.1.0',
homepage: r'https://github.com/theyakka/qr.flutter',
repository: null,
),
const License(
name: r'shared_preferences',
license: r'''Copyright 2013 The Flutter Authors. All rights reserved.
license: r'''
Copyright 2013 The Flutter Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
@ -882,12 +885,12 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
''',
version: r'^2.2.2',
homepage: null,
repository: r'https://github.com/flutter/packages/tree/main/packages/shared_preferences/shared_preferences',
),
const License(
name: r'tuple',
license: r'''Copyright (c) 2014, the tuple project authors.
license: r'''
Copyright (c) 2014, the tuple project authors.
All rights reserved.
Redistribution and use in source and binary forms, with or without
@ -910,12 +913,12 @@ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
version: r'^2.0.2',
homepage: null,
repository: r'https://github.com/google/tuple.dart',
),
const License(
name: r'url_launcher',
license: r'''Copyright 2013 The Flutter Authors. All rights reserved.
license: r'''
Copyright 2013 The Flutter Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
@ -942,7 +945,6 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
''',
version: r'^6.2.4',
homepage: null,
repository: r'https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher',
),
];

View File

@ -8,6 +8,7 @@ import 'package:shlink_app/API/Classes/ShlinkStats/shlink_stats.dart';
import 'package:shlink_app/API/server_manager.dart';
import 'package:shlink_app/views/short_url_edit_view.dart';
import 'package:shlink_app/views/url_list_view.dart';
import 'package:shlink_app/widgets/available_servers_bottom_sheet.dart';
import 'package:url_launcher/url_launcher_string.dart';
import '../API/Classes/ShortURL/short_url.dart';
import '../globals.dart' as globals;
@ -121,15 +122,25 @@ class _HomeViewState extends State<HomeView> {
child: CustomScrollView(
slivers: [
SliverAppBar.medium(
automaticallyImplyLeading: false,
expandedHeight: 160,
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text("Shlink",
style: TextStyle(fontWeight: FontWeight.bold)),
Text(globals.serverManager.getServerUrl(),
style: TextStyle(
fontSize: 16, color: Colors.grey[600]))
GestureDetector(
onTap: () {
showModalBottomSheet(
context: context,
builder: (BuildContext context) {
return const AvailableServerBottomSheet();
});
},
child: Text(globals.serverManager.getServerUrl(),
style: TextStyle(
fontSize: 16, color: Colors.grey[600])),
)
],
)),
SliverToBoxAdapter(

View File

@ -0,0 +1,297 @@
import 'package:flutter/material.dart';
import 'package:shlink_app/API/Classes/ShortURL/RedirectRule/condition_device_type.dart';
import 'package:shlink_app/API/Classes/ShortURL/RedirectRule/redirect_rule_condition.dart';
import 'package:shlink_app/API/Classes/ShortURL/RedirectRule/redirect_rule_condition_type.dart';
import 'package:shlink_app/API/Classes/ShortURL/short_url.dart';
import 'package:shlink_app/API/server_manager.dart';
import '../globals.dart' as globals;
import '../API/Classes/ShortURL/RedirectRule/redirect_rule.dart';
class RedirectRulesDetailView extends StatefulWidget {
const RedirectRulesDetailView({super.key, required this.shortURL});
final ShortURL shortURL;
@override
State<RedirectRulesDetailView> createState() =>
_RedirectRulesDetailViewState();
}
class _RedirectRulesDetailViewState extends State<RedirectRulesDetailView> {
List<RedirectRule> redirectRules = [];
bool redirectRulesLoaded = false;
bool isSaving = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => loadRedirectRules());
}
Future<void> loadRedirectRules() async {
final response =
await globals.serverManager.getRedirectRules(widget.shortURL.shortCode);
response.fold((l) {
setState(() {
redirectRules = l;
redirectRulesLoaded = true;
});
_sortListByPriority();
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;
});
}
void _saveRedirectRules() async {
final response = await globals.serverManager
.setRedirectRules(widget.shortURL.shortCode, redirectRules);
response.fold((l) {
Navigator.pop(context);
}, (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;
});
}
void _sortListByPriority() {
setState(() {
redirectRules.sort((a, b) => a.priority - b.priority);
});
}
void _fixPriorities() {
for (int i = 0; i < redirectRules.length; i++) {
setState(() {
redirectRules[i].priority = i + 1;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: Wrap(
spacing: 16,
children: [
FloatingActionButton(
onPressed: () {
if (!isSaving & redirectRulesLoaded) {
setState(() {
isSaving = true;
});
_saveRedirectRules();
}
},
child: isSaving
? const Padding(
padding: EdgeInsets.all(16),
child: CircularProgressIndicator(strokeWidth: 3))
: const Icon(Icons.save))
],
),
body: CustomScrollView(
slivers: [
const SliverAppBar.medium(
expandedHeight: 120,
title: Text(
"Redirect Rules",
style: TextStyle(fontWeight: FontWeight.bold),
),
),
if (redirectRulesLoaded && redirectRules.isEmpty)
SliverToBoxAdapter(
child: Center(
child: Padding(
padding: const EdgeInsets.only(top: 50),
child: Column(
children: [
const Text(
"No Redirect Rules",
style: TextStyle(
fontSize: 24, fontWeight: FontWeight.bold),
),
Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(
'Adding redirect rules will be supported soon!',
style: TextStyle(
fontSize: 16, color: Colors.grey[600]),
),
)
],
))))
else
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return _ListCell(
redirectRule: redirectRules[index],
moveUp: index == 0
? null
: () {
setState(() {
redirectRules[index].priority -= 1;
redirectRules[index - 1].priority += 1;
});
_sortListByPriority();
},
moveDown: index == (redirectRules.length - 1)
? null
: () {
setState(() {
redirectRules[index].priority += 1;
redirectRules[index + 1].priority -= 1;
});
_sortListByPriority();
},
delete: () {
setState(() {
redirectRules.removeAt(index);
});
_fixPriorities();
},
);
}, childCount: redirectRules.length))
],
),
);
}
}
class _ListCell extends StatefulWidget {
const _ListCell(
{required this.redirectRule,
required this.moveUp,
required this.moveDown,
required this.delete});
final VoidCallback? moveUp;
final VoidCallback? moveDown;
final VoidCallback delete;
final RedirectRule redirectRule;
@override
State<_ListCell> createState() => _ListCellState();
}
class _ListCellState extends State<_ListCell> {
String _conditionToTagString(RedirectRuleCondition condition) {
switch (condition.type) {
case RedirectRuleConditionType.DEVICE:
return "Device is ${ConditionDeviceType.fromApi(condition.matchValue).humanReadable}";
case RedirectRuleConditionType.LANGUAGE:
return "Language is ${condition.matchValue}";
case RedirectRuleConditionType.QUERY_PARAM:
return "Query string contains ${condition.matchKey}=${condition.matchValue}";
}
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(left: 8, right: 8),
child: Container(
padding: const EdgeInsets.only(left: 8, right: 8, top: 16, bottom: 16),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: MediaQuery.of(context).platformBrightness ==
Brightness.dark
? Colors.grey[800]!
: Colors.grey[300]!)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Text("Long URL ",
style: TextStyle(fontWeight: FontWeight.bold)),
Text(widget.redirectRule.longUrl)
],
),
const Text("Conditions:",
style: TextStyle(fontWeight: FontWeight.bold)),
Row(
children: [
Expanded(
child: Wrap(
children:
widget.redirectRule.conditions.map((condition) {
return Padding(
padding: const EdgeInsets.only(right: 4, top: 4),
child: Container(
padding: const EdgeInsets.only(
top: 4, bottom: 4, left: 12, right: 12),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color:
MediaQuery.of(context).platformBrightness ==
Brightness.dark
? Colors.grey[900]
: Colors.grey[300],
),
child: Text(_conditionToTagString(condition)),
),
);
}).toList(),
),
)
],
),
Wrap(
children: [
IconButton(
disabledColor:
MediaQuery.of(context).platformBrightness ==
Brightness.dark
? Colors.grey[700]
: Colors.grey[400],
onPressed: widget.moveUp,
icon: const Icon(Icons.arrow_upward),
),
IconButton(
disabledColor:
MediaQuery.of(context).platformBrightness ==
Brightness.dark
? Colors.grey[700]
: Colors.grey[400],
onPressed: widget.moveDown,
icon: const Icon(Icons.arrow_downward),
),
IconButton(
onPressed: widget.delete,
icon: const Icon(Icons.delete, color: Colors.red),
)
],
)
],
)));
}
}

View File

@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:shlink_app/API/server_manager.dart';
import 'package:shlink_app/views/login_view.dart';
import 'package:shlink_app/views/opensource_licenses_view.dart';
import 'package:shlink_app/widgets/available_servers_bottom_sheet.dart';
import 'package:url_launcher/url_launcher.dart';
import '../globals.dart' as globals;
@ -64,91 +64,81 @@ class _SettingsViewState extends State<SettingsView> {
return Scaffold(
body: CustomScrollView(
slivers: [
SliverAppBar.medium(
const SliverAppBar.medium(
expandedHeight: 120,
title: const Text(
title: Text(
"Settings",
style: TextStyle(fontWeight: FontWeight.bold),
),
actions: [
PopupMenuButton(
itemBuilder: (context) {
return [
const PopupMenuItem(
value: 0,
child: Text("Log out", style: TextStyle(color: Colors.red)),
)
];
},
onSelected: (value) {
if (value == 0) {
globals.serverManager.logOut().then((value) =>
Navigator.of(context).pushReplacement(MaterialPageRoute(
builder: (context) => const LoginView())));
}
},
)
],
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
children: [
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Theme.of(context).brightness == Brightness.light
? Colors.grey[100]
: Colors.grey[900],
),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
children: [
Icon(Icons.dns_outlined,
color: (() {
switch (_serverStatus) {
case ServerStatus.connected:
return Colors.green;
case ServerStatus.connecting:
return Colors.orange;
case ServerStatus.disconnected:
return Colors.red;
}
}())),
const SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text("Connected to",
style: TextStyle(color: Colors.grey)),
Text(globals.serverManager.getServerUrl(),
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16)),
Row(
children: [
const Text("API Version: ",
style: TextStyle(
color: Colors.grey,
fontWeight: FontWeight.w600)),
Text(globals.serverManager.getApiVersion(),
style:
const TextStyle(color: Colors.grey)),
const SizedBox(width: 16),
const Text("Server Version: ",
style: TextStyle(
color: Colors.grey,
fontWeight: FontWeight.w600)),
Text(_serverVersion,
style:
const TextStyle(color: Colors.grey))
],
),
],
)
],
GestureDetector(
onTap: () {
showModalBottomSheet(
context: context,
builder: (BuildContext context) {
return const AvailableServerBottomSheet();
});
},
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Theme.of(context).brightness == Brightness.light
? Colors.grey[100]
: Colors.grey[900],
),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
children: [
Icon(Icons.dns_outlined,
color: (() {
switch (_serverStatus) {
case ServerStatus.connected:
return Colors.green;
case ServerStatus.connecting:
return Colors.orange;
case ServerStatus.disconnected:
return Colors.red;
}
}())),
const SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text("Connected to",
style: TextStyle(color: Colors.grey)),
Text(globals.serverManager.getServerUrl(),
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16)),
Row(
children: [
const Text("API Version: ",
style: TextStyle(
color: Colors.grey,
fontWeight: FontWeight.w600)),
Text(globals.serverManager.getApiVersion(),
style: const TextStyle(
color: Colors.grey)),
const SizedBox(width: 16),
const Text("Server Version: ",
style: TextStyle(
color: Colors.grey,
fontWeight: FontWeight.w600)),
Text(_serverVersion,
style:
const TextStyle(color: Colors.grey))
],
),
],
)
],
),
),
),
),

View File

@ -144,9 +144,9 @@ class _ShortURLEditViewState extends State<ShortURLEditView>
return Scaffold(
body: CustomScrollView(
slivers: [
const SliverAppBar.medium(
title: Text("New Short URL",
style: TextStyle(fontWeight: FontWeight.bold)),
SliverAppBar.medium(
title: Text("${disableSlugEditor ? "Edit" : "New"} Short URL",
style: const TextStyle(fontWeight: FontWeight.bold)),
),
SliverToBoxAdapter(
child: Padding(

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:shlink_app/API/Classes/ShortURL/short_url.dart';
import 'package:intl/intl.dart';
import 'package:shlink_app/API/server_manager.dart';
import 'package:shlink_app/views/redirect_rules_detail_view.dart';
import 'package:shlink_app/views/short_url_edit_view.dart';
import 'package:shlink_app/widgets/url_tags_list_widget.dart';
import 'package:url_launcher/url_launcher.dart';
@ -129,6 +130,10 @@ class _URLDetailViewState extends State<URLDetailView> {
title: "Short URL", content: shortURL.shortUrl, isUrl: true),
_ListCell(title: "Long URL", content: shortURL.longUrl, isUrl: true),
_ListCell(title: "Creation Date", content: shortURL.dateCreated),
_ListCell(
title: "Redirect Rules",
content: null,
clickableDetailView: RedirectRulesDetailView(shortURL: shortURL)),
const _ListCell(title: "Visits", content: ""),
_ListCell(
title: "Total", content: shortURL.visitsSummary.total, sub: true),
@ -163,13 +168,15 @@ class _ListCell extends StatefulWidget {
required this.content,
this.sub = false,
this.last = false,
this.isUrl = false});
this.isUrl = false,
this.clickableDetailView});
final String title;
final dynamic content;
final bool sub;
final bool last;
final bool isUrl;
final Widget? clickableDetailView;
@override
State<_ListCell> createState() => _ListCellState();
@ -183,11 +190,16 @@ class _ListCellState extends State<_ListCell> {
padding: EdgeInsets.only(top: 16, bottom: widget.last ? 30 : 0),
child: GestureDetector(
onTap: () async {
Uri? parsedUrl = Uri.tryParse(widget.content);
if (widget.isUrl &&
parsedUrl != null &&
await canLaunchUrl(parsedUrl)) {
launchUrl(parsedUrl);
if (widget.clickableDetailView != null) {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => widget.clickableDetailView!));
} else if (widget.content is String) {
Uri? parsedUrl = Uri.tryParse(widget.content);
if (widget.isUrl &&
parsedUrl != null &&
await canLaunchUrl(parsedUrl)) {
launchUrl(parsedUrl);
}
}
},
child: Container(
@ -245,6 +257,8 @@ class _ListCellState extends State<_ListCell> {
else if (widget.content is DateTime)
Text(DateFormat('yyyy-MM-dd - HH:mm')
.format(widget.content))
else if (widget.clickableDetailView != null)
const Icon(Icons.chevron_right)
else
const Text("N/A")
],

View File

@ -0,0 +1,133 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:shlink_app/main.dart';
import 'package:shlink_app/views/login_view.dart';
import '../globals.dart' as globals;
class AvailableServerBottomSheet extends StatefulWidget {
const AvailableServerBottomSheet({super.key});
@override
State<AvailableServerBottomSheet> createState() =>
_AvailableServerBottomSheetState();
}
class _AvailableServerBottomSheetState
extends State<AvailableServerBottomSheet> {
List<String> availableServers = [];
@override
void initState() {
super.initState();
_loadServers();
}
Future<void> _loadServers() async {
List<String> savedServers =
await globals.serverManager.getAvailableServers();
setState(() {
availableServers = savedServers;
});
}
@override
Widget build(BuildContext context) {
return CustomScrollView(
slivers: [
const SliverAppBar.medium(
expandedHeight: 120,
automaticallyImplyLeading: false,
title: Text(
"Available Servers",
style: TextStyle(fontWeight: FontWeight.bold),
),
),
SliverList(
delegate:
SliverChildBuilderDelegate((BuildContext context, int index) {
return GestureDetector(
onTap: () async {
final prefs = await SharedPreferences.getInstance();
prefs.setString("lastusedserver", availableServers[index]);
await Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (context) => const InitialPage()),
(Route<dynamic> route) => false);
},
child: Padding(
padding: const EdgeInsets.only(left: 8, right: 8),
child: Container(
padding:
const EdgeInsets.only(left: 8, right: 8, top: 16, bottom: 16),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color:
MediaQuery.of(context).platformBrightness ==
Brightness.dark
? Colors.grey[800]!
: Colors.grey[300]!)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Wrap(
spacing: 8,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
const Icon(Icons.dns_outlined),
Text(availableServers[index])
],
),
Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
children: [
if (availableServers[index] ==
globals.serverManager.serverUrl)
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.circular(4)),
),
IconButton(
onPressed: () async {
globals.serverManager
.logOut(availableServers[index]);
if (availableServers[index] ==
globals.serverManager.serverUrl) {
await Navigator.of(context)
.pushAndRemoveUntil(
MaterialPageRoute(
builder: (context) =>
const InitialPage()),
(Route<dynamic> route) => false);
} else {
Navigator.pop(context);
}
},
icon: const Icon(Icons.logout, color: Colors.red),
)
],
)
],
))),
);
}, childCount: availableServers.length)),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(top: 8),
child: Center(
child: ElevatedButton(
onPressed: () async {
await Navigator.of(context)
.push(MaterialPageRoute(builder: (context) => const LoginView()));
},
child: const Text("Add server..."),
),
),
))
],
);
}
}

View File

@ -5,18 +5,18 @@ packages:
dependency: transitive
description:
name: archive
sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d"
sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d
url: "https://pub.dev"
source: hosted
version: "3.4.10"
version: "3.6.1"
args:
dependency: transitive
description:
name: args
sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596
sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a"
url: "https://pub.dev"
source: hosted
version: "2.4.2"
version: "2.5.0"
async:
dependency: transitive
description:
@ -69,18 +69,10 @@ packages:
dependency: transitive
description:
name: collection
sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
url: "https://pub.dev"
source: hosted
version: "1.17.2"
convert:
dependency: transitive
description:
name: convert
sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592"
url: "https://pub.dev"
source: hosted
version: "3.1.1"
version: "1.18.0"
crypto:
dependency: transitive
description:
@ -93,10 +85,10 @@ packages:
dependency: "direct main"
description:
name: cupertino_icons
sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
url: "https://pub.dev"
source: hosted
version: "1.0.6"
version: "1.0.8"
dartz:
dependency: "direct main"
description:
@ -125,10 +117,10 @@ packages:
dependency: transitive
description:
name: ffi
sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878"
sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
version: "2.1.2"
file:
dependency: transitive
description:
@ -154,10 +146,10 @@ packages:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1"
sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
version: "4.0.0"
flutter_process_text:
dependency: "direct main"
description:
@ -170,50 +162,50 @@ packages:
dependency: "direct main"
description:
name: flutter_secure_storage
sha256: ffdbb60130e4665d2af814a0267c481bcf522c41ae2e43caf69fa0146876d685
sha256: "165164745e6afb5c0e3e3fcc72a012fb9e58496fb26ffb92cf22e16a821e85d0"
url: "https://pub.dev"
source: hosted
version: "9.0.0"
version: "9.2.2"
flutter_secure_storage_linux:
dependency: transitive
description:
name: flutter_secure_storage_linux
sha256: "3d5032e314774ee0e1a7d0a9f5e2793486f0dff2dd9ef5a23f4e3fb2a0ae6a9e"
sha256: "4d91bfc23047422cbcd73ac684bc169859ee766482517c22172c86596bf1464b"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
version: "1.2.1"
flutter_secure_storage_macos:
dependency: transitive
description:
name: flutter_secure_storage_macos
sha256: bd33935b4b628abd0b86c8ca20655c5b36275c3a3f5194769a7b3f37c905369c
sha256: "1693ab11121a5f925bbea0be725abfcfbbcf36c1e29e571f84a0c0f436147a81"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
version: "3.1.2"
flutter_secure_storage_platform_interface:
dependency: transitive
description:
name: flutter_secure_storage_platform_interface
sha256: "0d4d3a5dd4db28c96ae414d7ba3b8422fd735a8255642774803b2532c9a61d7e"
sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8
url: "https://pub.dev"
source: hosted
version: "1.0.2"
version: "1.1.2"
flutter_secure_storage_web:
dependency: transitive
description:
name: flutter_secure_storage_web
sha256: "30f84f102df9dcdaa2241866a958c2ec976902ebdaa8883fbfe525f1f2f3cf20"
sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9
url: "https://pub.dev"
source: hosted
version: "1.1.2"
version: "1.2.1"
flutter_secure_storage_windows:
dependency: transitive
description:
name: flutter_secure_storage_windows
sha256: "5809c66f9dd3b4b93b0a6e2e8561539405322ee767ac2f64d084e2ab5429d108"
sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709
url: "https://pub.dev"
source: hosted
version: "3.0.0"
version: "3.1.2"
flutter_sharing_intent:
dependency: "direct main"
description:
@ -236,10 +228,10 @@ packages:
dependency: "direct main"
description:
name: http
sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525"
sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010
url: "https://pub.dev"
source: hosted
version: "1.1.0"
version: "1.2.2"
http_parser:
dependency: transitive
description:
@ -252,10 +244,10 @@ packages:
dependency: transitive
description:
name: image
sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e"
sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8"
url: "https://pub.dev"
source: hosted
version: "4.1.7"
version: "4.2.0"
intl:
dependency: "direct main"
description:
@ -276,10 +268,34 @@ packages:
dependency: transitive
description:
name: json_annotation
sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
url: "https://pub.dev"
source: hosted
version: "4.8.1"
version: "4.9.0"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a"
url: "https://pub.dev"
source: hosted
version: "10.0.4"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8"
url: "https://pub.dev"
source: hosted
version: "3.0.3"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
license_generator:
dependency: "direct dev"
description:
@ -292,82 +308,82 @@ packages:
dependency: transitive
description:
name: lints
sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
version: "4.0.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
url: "https://pub.dev"
source: hosted
version: "0.12.16"
version: "0.12.16+1"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
url: "https://pub.dev"
source: hosted
version: "0.5.0"
version: "0.8.0"
meta:
dependency: transitive
description:
name: meta
sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3"
sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
version: "1.12.0"
package_info_plus:
dependency: "direct main"
description:
name: package_info_plus
sha256: "7e76fad405b3e4016cd39d08f455a4eb5199723cf594cd1b8916d47140d93017"
sha256: b93d8b4d624b4ea19b0a5a208b2d6eff06004bc3ce74c06040b120eeadd00ce0
url: "https://pub.dev"
source: hosted
version: "4.2.0"
version: "8.0.0"
package_info_plus_platform_interface:
dependency: transitive
description:
name: package_info_plus_platform_interface
sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6"
sha256: f49918f3433a3146047372f9d4f1f847511f2acd5cd030e1f44fe5a50036b70e
url: "https://pub.dev"
source: hosted
version: "2.0.1"
version: "3.0.0"
path:
dependency: transitive
description:
name: path
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
url: "https://pub.dev"
source: hosted
version: "1.8.3"
version: "1.9.0"
path_provider:
dependency: transitive
description:
name: path_provider
sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b
sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161
url: "https://pub.dev"
source: hosted
version: "2.1.2"
version: "2.1.3"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668"
sha256: "30c5aa827a6ae95ce2853cdc5fe3971daaac00f6f081c419c013f7f57bff2f5e"
url: "https://pub.dev"
source: hosted
version: "2.2.2"
version: "2.2.7"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f"
sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16
url: "https://pub.dev"
source: hosted
version: "2.3.2"
version: "2.4.0"
path_provider_linux:
dependency: transitive
description:
@ -388,26 +404,26 @@ packages:
dependency: transitive
description:
name: path_provider_windows
sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170"
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.2.1"
version: "2.3.0"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750
sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
url: "https://pub.dev"
source: hosted
version: "5.4.0"
version: "6.0.2"
platform:
dependency: transitive
description:
name: platform
sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
url: "https://pub.dev"
source: hosted
version: "3.1.4"
version: "3.1.5"
plugin_platform_interface:
dependency: transitive
description:
@ -416,22 +432,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.8"
pointycastle:
dependency: transitive
description:
name: pointycastle
sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29"
url: "https://pub.dev"
source: hosted
version: "3.7.4"
qr:
dependency: transitive
description:
name: qr
sha256: "64957a3930367bf97cc211a5af99551d630f2f4625e38af10edd6b19131b64b3"
sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
version: "3.0.2"
qr_flutter:
dependency: "direct main"
description:
@ -444,26 +452,26 @@ packages:
dependency: "direct main"
description:
name: shared_preferences
sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02"
sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180
url: "https://pub.dev"
source: hosted
version: "2.2.2"
version: "2.2.3"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06"
sha256: "93d0ec9dd902d85f326068e6a899487d1f65ffcd5798721a95330b26c8131577"
url: "https://pub.dev"
source: hosted
version: "2.2.1"
version: "2.2.3"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c"
sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7"
url: "https://pub.dev"
source: hosted
version: "2.3.5"
version: "2.4.0"
shared_preferences_linux:
dependency: transitive
description:
@ -476,18 +484,18 @@ packages:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b"
sha256: "034650b71e73629ca08a0bd789fd1d83cc63c2d1e405946f7cef7bc37432f93a"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
version: "2.4.0"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf
sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a"
url: "https://pub.dev"
source: hosted
version: "2.2.1"
version: "2.3.0"
shared_preferences_windows:
dependency: transitive
description:
@ -513,18 +521,18 @@ packages:
dependency: transitive
description:
name: stack_trace
sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
url: "https://pub.dev"
source: hosted
version: "1.11.0"
version: "1.11.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8"
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
url: "https://pub.dev"
source: hosted
version: "2.1.1"
version: "2.1.2"
string_scanner:
dependency: transitive
description:
@ -545,10 +553,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8"
sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f"
url: "https://pub.dev"
source: hosted
version: "0.6.0"
version: "0.7.0"
tuple:
dependency: "direct main"
description:
@ -569,26 +577,26 @@ packages:
dependency: "direct main"
description:
name: url_launcher
sha256: c512655380d241a337521703af62d2c122bf7b77a46ff7dd750092aa9433499c
sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3"
url: "https://pub.dev"
source: hosted
version: "6.2.4"
version: "6.3.0"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: d4ed0711849dd8e33eb2dd69c25db0d0d3fdc37e0a62e629fe32f57a22db2745
sha256: "95d8027db36a0e52caf55680f91e33ea6aa12a3ce608c90b06f4e429a21067ac"
url: "https://pub.dev"
source: hosted
version: "6.3.0"
version: "6.3.5"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: "75bb6fe3f60070407704282a2d295630cab232991eb52542b18347a8a941df03"
sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e
url: "https://pub.dev"
source: hosted
version: "6.2.4"
version: "6.3.1"
url_launcher_linux:
dependency: transitive
description:
@ -601,10 +609,10 @@ packages:
dependency: transitive
description:
name: url_launcher_macos
sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234
sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de"
url: "https://pub.dev"
source: hosted
version: "3.1.0"
version: "3.2.0"
url_launcher_platform_interface:
dependency: transitive
description:
@ -617,18 +625,18 @@ packages:
dependency: transitive
description:
name: url_launcher_web
sha256: "7fd2f55fe86cea2897b963e864dc01a7eb0719ecc65fcef4c1cc3d686d718bb2"
sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
version: "2.3.1"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7
sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185"
url: "https://pub.dev"
source: hosted
version: "3.1.1"
version: "3.1.2"
vector_math:
dependency: transitive
description:
@ -637,22 +645,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.4"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec"
url: "https://pub.dev"
source: hosted
version: "14.2.1"
web:
dependency: transitive
description:
name: web
sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10
sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27"
url: "https://pub.dev"
source: hosted
version: "0.1.4-beta"
version: "0.5.1"
win32:
dependency: transitive
description:
name: win32
sha256: b0f37db61ba2f2e9b7a78a1caece0052564d1bc70668156cf3a29d676fe4e574
sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4
url: "https://pub.dev"
source: hosted
version: "5.1.1"
version: "5.5.1"
xdg_directories:
dependency: transitive
description:
@ -665,10 +681,10 @@ packages:
dependency: transitive
description:
name: xml
sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84"
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
url: "https://pub.dev"
source: hosted
version: "6.3.0"
version: "6.5.0"
yaml:
dependency: transitive
description:
@ -678,5 +694,5 @@ packages:
source: hosted
version: "3.1.2"
sdks:
dart: ">=3.1.0 <4.0.0"
flutter: ">=3.13.0"
dart: ">=3.4.0 <4.0.0"
flutter: ">=3.22.0"

View File

@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.2.0+8
version: 1.3.1+10
environment:
sdk: ^3.0.0
@ -40,7 +40,7 @@ dependencies:
intl: ^0.19.0
dynamic_color: ^1.6.6
url_launcher: ^6.2.4
package_info_plus: ^4.0.2
package_info_plus: ^8.0.0
shared_preferences: ^2.2.2
flutter_sharing_intent: ^1.1.1
@ -50,7 +50,7 @@ dev_dependencies:
license_generator: ^2.0.0
flutter_launcher_icons: ^0.13.1
flutter_lints: ^3.0.1
flutter_lints: ^4.0.0
flutter:
uses-material-design: true