Compare commits

..

46 Commits
v1.1.1 ... main

Author SHA1 Message Date
d11d0cfe5c
bump version to 1.4.0 2024-08-01 04:49:21 +02:00
21042fa2b7
fixed bug with newly created tags 2024-07-27 00:24:00 +02:00
8507aaa8bd
added link to shlink docs (create api key) 2024-07-26 23:59:48 +02:00
e673dd7b64
deleted linux/ 2024-07-26 23:36:15 +02:00
5c318c6237
added back spacing (some weird bug) 2024-07-26 23:09:02 +02:00
d33f5757c9
changed tag spacing 2024-07-26 23:04:47 +02:00
c5ab724a9e
minor fixes 2024-07-26 23:00:03 +02:00
50ec0cb49f
FEATURE: add and remove tags from short URLs 2024-07-26 22:52:44 +02:00
202ab20747
a bit of refactoring 2024-07-26 20:30:13 +02:00
4f8cc9e4b6
added global theme 2024-07-26 19:32:25 +02:00
e1c9bc4d80
bump version to 1.3.1 2024-07-25 20:17:55 +02:00
413275df38
fixed bug where server list stays empty 2024-07-25 20:17:27 +02:00
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
69c8870997
support multiple shlink instances 2024-07-25 19:11:24 +02:00
ce28066d29
fixed license 2024-07-25 17:40:17 +02:00
e9f98c2171
edited README 2024-07-25 17:39:42 +02:00
rainloreley
39483dca54 Apply automatic changes 2024-07-25 15:38:31 +00:00
ba058e2af3
formatting 2024-07-25 17:37:17 +02:00
0eea6ee9a2
dependency 2024-07-25 17:35:12 +02:00
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
6a5cebdb68
change view title when editing short URL 2024-07-25 14:50:16 +02:00
6d654210fa
Update README.md 2024-04-01 19:46:01 +02:00
a3dc9e130e
Update fix-formatting-and-license.yml 2024-04-01 19:45:10 +02:00
a1b0f37fa1
update gradle 2024-03-31 22:59:22 +02:00
99e1b317ad
update gradle 2024-03-31 22:50:16 +02:00
8590fbb89a
bump version to 1.2.0 2024-03-31 22:21:07 +02:00
rainloreley
ec4820a1d1 Apply automatic changes 2024-03-31 19:59:49 +00:00
65ee8d1879
formatting 2024-03-31 21:58:31 +02:00
rainloreley
e1a7235a90 Apply automatic changes 2024-03-31 19:56:51 +00:00
b29429c6b2
Merge remote-tracking branch 'origin/main' 2024-03-31 21:55:36 +02:00
07db92643a
changed README.md 2024-03-31 21:55:27 +02:00
0cb9debd88
Update fix-formatting-and-license.yml 2024-03-31 21:52:13 +02:00
508a392a94
Update fix-formatting-and-license.yml 2024-03-31 21:46:07 +02:00
3849ae09ed
Update fix-formatting-and-license.yml 2024-03-31 21:41:11 +02:00
cfa47371e7
Update fix-formatting-and-license.yml 2024-03-31 21:38:12 +02:00
9a31ea73df
Update fix-formatting-and-license.yml 2024-03-31 21:35:48 +02:00
0af6d188e3
Create fix-formatting-and-license.yml 2024-03-31 21:29:41 +02:00
1ee99f367d
added explicit type annotation to variables 2024-03-31 21:17:12 +02:00
0594452584
made urls in short url detail view clickable 2024-03-31 21:08:58 +02:00
b0988fcb3d
fixed missing version info 2024-03-31 21:08:41 +02:00
dc03457397
tiny code change 2024-03-31 21:08:23 +02:00
4dcce26caf
added share sheet intent 2024-03-31 21:07:30 +02:00
591713a444
Revert "added core files with l10n"
This reverts commit 9a2d3a712503c7bf15073ce5226ebd638a0924dc.
2024-03-31 19:25:49 +02:00
9a2d3a7125
added core files with l10n 2024-03-31 17:55:01 +02:00
45 changed files with 2316 additions and 1257 deletions

View File

@ -12,22 +12,22 @@ Shlink Manager is an app for Android to see and manage all shortened URLs create
<img src="https://raw.githubusercontent.com/steverichey/google-play-badge-svg/master/img/en_get.svg" alt="Play Store download" width="30%"/> <img src="https://raw.githubusercontent.com/steverichey/google-play-badge-svg/master/img/en_get.svg" alt="Play Store download" width="30%"/>
</a> </a>
[![Codemagic build status](https://api.codemagic.io/apps/66096ec96d57699debb805f8/66096ec96d57699debb805f7/status_badge.svg)](https://codemagic.io/apps/66096ec96d57699debb805f8/66096ec96d57699debb805f7/latest_build)
## 📱 Current Features ## 📱 Current Features
✅ List all short URLs<br/> ✅ List all short URLs<br/>
✅ Create new short URLs<br/> ✅ Create, edit and delete short URLs<br/>
✅ Delete short URLs<br/>
✅ See overall statistics<br/> ✅ See overall statistics<br/>
✅ Detailed statistics for each short URL<br/> ✅ Detailed statistics for each short URL<br/>
✅ Display tags<br/> ✅ Display tags & QR code<br/>
✅ Display QR code<br/>
✅ Dark mode support<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 ## 🔨 To Do
- [ ] Add support for iOS (maybe in the future) - [ ] Add support for iOS (maybe in the future)
- [ ] add tags
- [ ] specify individual long URLs per device
- [ ] improve app icon - [ ] improve app icon
- [ ] Refactor code - [ ] Refactor code
- [ ] ...and more - [ ] ...and more

View File

@ -52,10 +52,12 @@ android {
applicationId "dev.abmgrt.shlink_app" applicationId "dev.abmgrt.shlink_app"
// You can update the following values to match your application needs. // 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. // 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 targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
multiDexEnabled true
} }
signingConfigs { signingConfigs {
@ -79,4 +81,5 @@ flutter {
dependencies { dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.multidex:multidex:2.0.1'
} }

View File

@ -8,6 +8,7 @@
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" /> <data android:scheme="https" />
</intent> </intent>
</queries> </queries>
<application <application
android:label="Shlink Manager" android:label="Shlink Manager"
@ -33,17 +34,11 @@
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
</activity> <intent-filter
<activity android:label="Create Short URL">
android:name=".ProcessURLActivity" <action android:name="android.intent.action.SEND" />
android:label="Shorten URL"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay"
tools:targetApi="cupcake">
<intent-filter>
<action android:name="android.intent.action.PROCESS_TEXT" />
<data android:mimeType="text/plain"/>
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/*" />
</intent-filter> </intent-filter>
</activity> </activity>

View File

@ -6,7 +6,7 @@ buildscript {
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:7.2.0' classpath 'com.android.tools.build:gradle:7.3.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
} }
} }

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip

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/short_url_meta.dart';
import 'package:shlink_app/API/Classes/ShortURL/visits_summary.dart'; import 'package:shlink_app/API/Classes/ShortURL/visits_summary.dart';
import 'RedirectRule/redirect_rule.dart';
/// Data about a short URL /// Data about a short URL
class ShortURL { class ShortURL {
/// Slug of the short URL used in the URL /// 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 /// Whether the short URL is crawlable by a web crawler
bool crawlable; bool crawlable;
List<RedirectRule>? redirectRules;
ShortURL( ShortURL(
this.shortCode, this.shortCode,
this.shortUrl, this.shortUrl,
@ -52,20 +56,23 @@ class ShortURL {
longUrl = json["longUrl"], longUrl = json["longUrl"],
dateCreated = DateTime.parse(json["dateCreated"]), dateCreated = DateTime.parse(json["dateCreated"]),
visitsSummary = VisitsSummary.fromJson(json["visitsSummary"]), visitsSummary = VisitsSummary.fromJson(json["visitsSummary"]),
tags = (json["tags"] as List<dynamic>).map((e) => e.toString()).toList(), tags =
(json["tags"] as List<dynamic>).map((e) => e.toString()).toList(),
meta = ShortURLMeta.fromJson(json["meta"]), meta = ShortURLMeta.fromJson(json["meta"]),
domain = json["domain"], domain = json["domain"],
title = json["title"], title = json["title"],
crawlable = json["crawlable"]; crawlable = json["crawlable"];
/// Returns an empty class of [ShortURL]
ShortURL.empty() ShortURL.empty()
: shortCode = "", : shortCode = "",
shortUrl = "", shortUrl = "",
longUrl = "", longUrl = "",
dateCreated = DateTime.now(), dateCreated = DateTime.now(),
visitsSummary = VisitsSummary(0, 0, 0), visitsSummary = VisitsSummary(0, 0, 0),
tags = [], tags = [],
meta = ShortURLMeta(DateTime.now(), DateTime.now(), 0), meta = ShortURLMeta(DateTime.now(), DateTime.now(), 0),
domain = "", domain = "",
title = "", title = "",
crawlable = false; crawlable = false;
} }

View File

@ -0,0 +1,20 @@
import 'package:shlink_app/API/Classes/ShortURL/visits_summary.dart';
/// Tag with stats data
class TagWithStats {
/// Tag name
String tag;
/// Amount of short URLs using this tag
int shortUrlsCount;
/// visits summary for tag
VisitsSummary visitsSummary;
TagWithStats(this.tag, this.shortUrlsCount, this.visitsSummary);
TagWithStats.fromJson(Map<String, dynamic> json)
: tag = json["tag"] as String,
shortUrlsCount = json["shortUrlsCount"] as int,
visitsSummary = VisitsSummary.fromJson(json["visitsSummary"]);
}

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

@ -9,11 +9,11 @@ import '../server_manager.dart';
/// Gets statistics about the Shlink server /// Gets statistics about the Shlink server
FutureOr<Either<ShlinkStats, Failure>> apiGetShlinkStats( FutureOr<Either<ShlinkStats, Failure>> apiGetShlinkStats(
String? apiKey, String? serverUrl, String apiVersion) async { String? apiKey, String? serverUrl, String apiVersion) async {
var nonOrphanVisits; VisitsSummary? nonOrphanVisits;
var orphanVisits; VisitsSummary? orphanVisits;
var shortUrlsCount; int shortUrlsCount = 0;
var tagsCount; int tagsCount = 0;
var failure; Failure? failure;
var visitStatsResponse = await _getVisitStats(apiKey, serverUrl, apiVersion); var visitStatsResponse = await _getVisitStats(apiKey, serverUrl, apiVersion);
visitStatsResponse.fold((l) { visitStatsResponse.fold((l) {
@ -46,10 +46,10 @@ FutureOr<Either<ShlinkStats, Failure>> apiGetShlinkStats(
} }
if (failure != null) { if (failure != null) {
return right(failure); return right(failure!);
} }
return left( return left(
ShlinkStats(nonOrphanVisits, orphanVisits, shortUrlsCount, tagsCount)); ShlinkStats(nonOrphanVisits!, orphanVisits!, shortUrlsCount, tagsCount));
} }
class _ShlinkVisitStats { class _ShlinkVisitStats {

View File

@ -0,0 +1,68 @@
import 'dart:async';
import 'dart:convert';
import 'package:dartz/dartz.dart';
import 'package:http/http.dart' as http;
import 'package:shlink_app/API/Classes/Tag/tag_with_stats.dart';
import '../server_manager.dart';
/// Gets all tags
FutureOr<Either<List<TagWithStats>, Failure>> apiGetTagsWithStats(
String? apiKey, String? serverUrl, String apiVersion) async {
var currentPage = 1;
var maxPages = 2;
List<TagWithStats> allTags = [];
Failure? error;
while (currentPage <= maxPages) {
final response =
await _getTagsWithStatsPage(currentPage, apiKey, serverUrl, apiVersion);
response.fold((l) {
allTags.addAll(l.tags);
maxPages = l.totalPages;
currentPage++;
}, (r) {
maxPages = 0;
error = r;
});
}
if (error == null) {
return left(allTags);
} else {
return right(error!);
}
}
/// Gets all tags from a specific page
FutureOr<Either<TagsWithStatsPageResponse, Failure>> _getTagsWithStatsPage(
int page, String? apiKey, String? serverUrl, String apiVersion) async {
try {
final response = await http.get(
Uri.parse("$serverUrl/rest/v$apiVersion/tags/stats?page=$page"),
headers: {
"X-Api-Key": apiKey ?? "",
});
if (response.statusCode == 200) {
var jsonResponse = jsonDecode(response.body);
var pagesCount = jsonResponse["tags"]["pagination"]["pagesCount"] as int;
List<TagWithStats> tags =
(jsonResponse["tags"]["data"] as List<dynamic>).map((e) {
return TagWithStats.fromJson(e);
}).toList();
return left(TagsWithStatsPageResponse(tags, 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,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

@ -7,8 +7,11 @@ import 'package:shlink_app/API/Classes/ShortURLSubmission/short_url_submission.d
import '../server_manager.dart'; import '../server_manager.dart';
/// Submits a short URL to a server for it to be added /// Submits a short URL to a server for it to be added
FutureOr<Either<ShortURL, Failure>> apiSubmitShortUrl(ShortURLSubmission shortUrl, FutureOr<Either<ShortURL, Failure>> apiSubmitShortUrl(
String? apiKey, String? serverUrl, String apiVersion) async { ShortURLSubmission shortUrl,
String? apiKey,
String? serverUrl,
String apiVersion) async {
try { try {
final response = final response =
await http.post(Uri.parse("$serverUrl/rest/v$apiVersion/short-urls"), await http.post(Uri.parse("$serverUrl/rest/v$apiVersion/short-urls"),

View File

@ -7,7 +7,11 @@ import 'package:shlink_app/API/Classes/ShortURLSubmission/short_url_submission.d
import '../server_manager.dart'; import '../server_manager.dart';
/// Updates an existing short URL /// Updates an existing short URL
FutureOr<Either<ShortURL, Failure>> apiUpdateShortUrl(ShortURLSubmission shortUrl, String? apiKey, String? serverUrl, String apiVersion) async { FutureOr<Either<ShortURL, Failure>> apiUpdateShortUrl(
ShortURLSubmission shortUrl,
String? apiKey,
String? serverUrl,
String apiVersion) async {
String shortCode = shortUrl.customSlug ?? ""; String shortCode = shortUrl.customSlug ?? "";
if (shortCode == "") { if (shortCode == "") {
return right(RequestFailure(0, "Missing short code")); return right(RequestFailure(0, "Missing short code"));
@ -16,11 +20,12 @@ FutureOr<Either<ShortURL, Failure>> apiUpdateShortUrl(ShortURLSubmission shortUr
shortUrlData.remove("shortCode"); shortUrlData.remove("shortCode");
shortUrlData.remove("shortUrl"); shortUrlData.remove("shortUrl");
try { try {
final response = await http.patch(Uri.parse("$serverUrl/rest/v$apiVersion/short-urls/$shortCode"), final response = await http.patch(
headers: { Uri.parse("$serverUrl/rest/v$apiVersion/short-urls/$shortCode"),
"X-Api-Key": apiKey ?? "", headers: {
}, "X-Api-Key": apiKey ?? "",
body: jsonEncode(shortUrlData)); },
body: jsonEncode(shortUrlData));
if (response.statusCode == 200) { if (response.statusCode == 200) {
// get returned short url // get returned short url
@ -42,4 +47,4 @@ FutureOr<Either<ShortURL, Failure>> apiUpdateShortUrl(ShortURLSubmission shortUr
} catch (reqErr) { } catch (reqErr) {
return right(RequestFailure(0, reqErr.toString())); return right(RequestFailure(0, reqErr.toString()));
} }
} }

View File

@ -1,15 +1,21 @@
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:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:shlink_app/API/Classes/ShlinkStats/shlink_stats.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/ShortURL/short_url.dart';
import 'package:shlink_app/API/Classes/ShortURLSubmission/short_url_submission.dart'; import 'package:shlink_app/API/Classes/ShortURLSubmission/short_url_submission.dart';
import 'package:shlink_app/API/Classes/Tag/tag_with_stats.dart';
import 'package:shlink_app/API/Methods/connect.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_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_server_health.dart';
import 'package:shlink_app/API/Methods/get_shlink_stats.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/get_short_urls.dart';
import 'package:shlink_app/API/Methods/get_tags_with_stats.dart';
import 'package:shlink_app/API/Methods/set_redirect_rules.dart';
import 'package:shlink_app/API/Methods/update_short_url.dart'; import 'package:shlink_app/API/Methods/update_short_url.dart';
import 'Methods/delete_short_url.dart'; import 'Methods/delete_short_url.dart';
@ -41,10 +47,41 @@ class ServerManager {
} }
/// Logs out the user and removes data about the Shlink server /// Logs out the user and removes data about the Shlink server
Future<void> logOut() async { Future<void> logOut(String url) async {
const storage = FlutterSecureStorage(); const storage = FlutterSecureStorage();
await storage.delete(key: "shlink_url"); final prefs = await SharedPreferences.getInstance();
await storage.delete(key: "shlink_apikey");
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] /// Loads the server credentials from [FlutterSecureStorage]
@ -53,21 +90,72 @@ class ServerManager {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
if (prefs.getBool('first_run') ?? true) { if (prefs.getBool('first_run') ?? true) {
FlutterSecureStorage storage = const FlutterSecureStorage();
await storage.deleteAll(); await storage.deleteAll();
prefs.setBool('first_run', false); prefs.setBool('first_run', false);
} else {
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;
} }
serverUrl = await storage.read(key: "shlink_url");
apiKey = await storage.read(key: "shlink_apikey");
} }
/// Saves the provided server credentials to [FlutterSecureStorage] /// Saves the provided server credentials to [FlutterSecureStorage]
void _saveCredentials(String url, String apiKey) async { void _saveCredentials(String url, String apiKey) async {
const storage = FlutterSecureStorage(); const storage = FlutterSecureStorage();
storage.write(key: "shlink_url", value: url); final prefs = await SharedPreferences.getInstance();
storage.write(key: "shlink_apikey", value: apiKey); 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 /// Saves provided server credentials and tries to establish a connection
@ -79,7 +167,7 @@ class ServerManager {
_saveCredentials(url, apiKey); _saveCredentials(url, apiKey);
final result = await connect(); final result = await connect();
result.fold((l) => null, (r) { result.fold((l) => null, (r) {
logOut(); logOut(url);
}); });
return result; return result;
} }
@ -95,6 +183,11 @@ class ServerManager {
return apiGetShortUrls(apiKey, serverUrl, apiVersion); return apiGetShortUrls(apiKey, serverUrl, apiVersion);
} }
/// Gets all tags from the server
FutureOr<Either<List<TagWithStats>, Failure>> getTags() async {
return apiGetTagsWithStats(apiKey, serverUrl, apiVersion);
}
/// Gets statistics about the Shlink instance /// Gets statistics about the Shlink instance
FutureOr<Either<ShlinkStats, Failure>> getShlinkStats() async { FutureOr<Either<ShlinkStats, Failure>> getShlinkStats() async {
return apiGetShlinkStats(apiKey, serverUrl, apiVersion); return apiGetShlinkStats(apiKey, serverUrl, apiVersion);
@ -125,6 +218,19 @@ class ServerManager {
FutureOr<Either<List<ShortURL>, Failure>> getRecentShortUrls() async { FutureOr<Either<List<ShortURL>, Failure>> getRecentShortUrls() async {
return apiGetRecentShortUrls(apiKey, serverUrl, apiVersion); 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 /// Server response data type about a page of short URLs from the server
@ -135,6 +241,14 @@ class ShortURLPageResponse {
ShortURLPageResponse(this.urls, this.totalPages); ShortURLPageResponse(this.urls, this.totalPages);
} }
/// Server response data type about a page of tags from the server
class TagsWithStatsPageResponse {
List<TagWithStats> tags;
int totalPages;
TagsWithStatsPageResponse(this.tags, this.totalPages);
}
/// Server response data type about the health status of the server /// Server response data type about the health status of the server
class ServerHealthResponse { class ServerHealthResponse {
String status; String status;

71
lib/global_theme.dart Normal file
View File

@ -0,0 +1,71 @@
import 'package:dynamic_color/dynamic_color.dart';
import 'package:flutter/material.dart';
class GlobalTheme {
static final Color _lightFocusColor = Colors.black.withOpacity(0.12);
static final Color _darkFocusColor = Colors.white.withOpacity(0.12);
static ThemeData lightThemeData(ColorScheme? dynamicColorScheme) {
return themeData(lightColorScheme, dynamicColorScheme, _lightFocusColor);
}
static ThemeData darkThemeData(ColorScheme? dynamicColorScheme) {
return themeData(darkColorScheme, dynamicColorScheme, _darkFocusColor);
}
static ThemeData themeData(ColorScheme colorScheme, ColorScheme? dynamic,
Color focusColor) {
return ThemeData(
colorScheme: colorScheme,
canvasColor: colorScheme.surface,
scaffoldBackgroundColor: colorScheme.surface,
highlightColor: Colors.transparent,
dividerColor: colorScheme.shadow,
focusColor: focusColor,
useMaterial3: true,
appBarTheme: AppBarTheme(
backgroundColor: colorScheme.surface,
foregroundColor: colorScheme.onSurface,
elevation: 0
)
);
}
static ColorScheme get lightColorScheme {
return ColorScheme(
primary: Color(0xff747ab5),
onPrimary: Colors.white,
secondary: Color(0x335d63a6),// Color(0xFFDDE0E0),
onSecondary: Color(0xFF322942),
tertiary: Colors.grey[300],
onTertiary: Colors.grey[700],
surfaceContainer: (Colors.grey[100])!,
outline: (Colors.grey[500])!,
shadow: (Colors.grey[300])!,
error: (Colors.red[400])!,
onError: Colors.white,
surface: Color(0xFFFAFBFB),
onSurface: Color(0xFF241E30),
brightness: Brightness.light,
);
}
static ColorScheme get darkColorScheme {
return ColorScheme(
primary: Color(0xff5d63a6),
secondary: Colors.blue.shade500,
secondaryContainer: Color(0xff1c1c1c),
surface: Colors.black,
surfaceContainer: Color(0xff0f0f0f),
onSurfaceVariant: Colors.grey[400],
tertiary: Colors.grey[900],
onTertiary: Colors.grey,
outline: (Colors.grey[700])!,
shadow: (Colors.grey[800])!,
error: (Colors.red[400])!,
onError: Colors.white,
onPrimary: Colors.white,
onSecondary: (Colors.grey[400])!,
onSurface: Colors.white,
brightness: Brightness.dark,
);
}
}

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shlink_app/global_theme.dart';
import 'package:shlink_app/views/login_view.dart'; import 'package:shlink_app/views/login_view.dart';
import 'package:shlink_app/views/navigationbar_view.dart'; import 'package:shlink_app/views/navigationbar_view.dart';
import 'globals.dart' as globals; import 'globals.dart' as globals;
@ -11,11 +12,13 @@ void main() {
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
const MyApp({super.key}); const MyApp({super.key});
static const _defaultLightColorScheme = ColorScheme static final ColorScheme _defaultLightColorScheme =
.light(); //.fromSwatch(primarySwatch: Colors.blue, backgroundColor: Colors.white); ColorScheme.fromSeed(seedColor: Colors.blue);
static final _defaultDarkColorScheme = static final _defaultDarkColorScheme = ColorScheme.fromSeed(
ColorScheme.fromSwatch(brightness: Brightness.dark); brightness: Brightness.dark,
seedColor: Colors.blue,
background: Colors.black);
// This widget is the root of your application. // This widget is the root of your application.
@override @override
@ -24,7 +27,9 @@ class MyApp extends StatelessWidget {
return MaterialApp( return MaterialApp(
title: 'Shlink', title: 'Shlink',
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
theme: ThemeData( theme: GlobalTheme.lightThemeData(lightColorScheme),
darkTheme: GlobalTheme.darkThemeData(darkColorScheme),
/*theme: ThemeData(
appBarTheme: const AppBarTheme( appBarTheme: const AppBarTheme(
backgroundColor: Color(0xfffafafa), backgroundColor: Color(0xfffafafa),
), ),
@ -36,10 +41,10 @@ class MyApp extends StatelessWidget {
foregroundColor: Colors.white, foregroundColor: Colors.white,
elevation: 0, elevation: 0,
), ),
colorScheme: darkColorScheme?.copyWith(background: Colors.black) ?? colorScheme: darkColorScheme?.copyWith(surface: Colors.black) ??
_defaultDarkColorScheme, _defaultDarkColorScheme,
useMaterial3: true, useMaterial3: true,
), ),*/
home: const InitialPage()); home: const InitialPage());
}); });
} }
@ -62,11 +67,13 @@ class _InitialPageState extends State<InitialPage> {
void checkLogin() async { void checkLogin() async {
bool result = await globals.serverManager.checkLogin(); bool result = await globals.serverManager.checkLogin();
if (result) { if (result) {
Navigator.of(context).pushReplacement( Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (context) => const NavigationBarView())); MaterialPageRoute(builder: (context) => const NavigationBarView()),
(Route<dynamic> route) => false);
} else { } else {
Navigator.of(context).pushReplacement( Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (context) => const LoginView())); MaterialPageRoute(builder: (context) => const LoginView()),
(Route<dynamic> route) => false);
} }
} }

View File

@ -0,0 +1,22 @@
import 'package:flutter/material.dart';
import 'package:shlink_app/API/server_manager.dart';
SnackBar buildApiErrorSnackbar(Failure r, BuildContext context) {
var text = "";
if (r is RequestFailure) {
text = r.description;
} else {
text = (r as ApiFailure).detail;
if ((r).invalidElements != null) {
text = "$text: ${(r).invalidElements}";
}
}
final snackBar = SnackBar(
content: Text(text, style: TextStyle(color: Theme.of(context).colorScheme.onError)),
backgroundColor: Theme.of(context).colorScheme.error,
behavior: SnackBarBehavior.floating);
return snackBar;
}

View File

@ -28,7 +28,7 @@ class LicenseUtil {
return [ return [
const License( const License(
name: r'cupertino_icons', name: r'cupertino_icons',
license: r''' license: r'''
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2016 Vladimir Kharlampidi Copyright (c) 2016 Vladimir Kharlampidi
@ -50,12 +50,11 @@ 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 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.''', CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''',
version: r'^1.0.5', version: r'^1.0.5',
repository: repository: r'https://github.com/flutter/packages/tree/main/third_party/packages/cupertino_icons',
r'https://github.com/flutter/packages/tree/main/third_party/packages/cupertino_icons',
), ),
const License( const License(
name: r'dartz', name: r'dartz',
license: r''' license: r'''
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2016, 2017, 2018, 2019, 2020, 2021 Björn Sperber Copyright (c) 2016, 2017, 2018, 2019, 2020, 2021 Björn Sperber
@ -83,7 +82,7 @@ SOFTWARE.
), ),
const License( const License(
name: r'dynamic_color', name: r'dynamic_color',
license: r''' license: r'''
Apache License Apache License
Version 2.0, January 2004 Version 2.0, January 2004
http://www.apache.org/licenses/ http://www.apache.org/licenses/
@ -287,12 +286,11 @@ SOFTWARE.
limitations under the License. limitations under the License.
''', ''',
version: r'^1.6.6', version: r'^1.6.6',
repository: repository: r'https://github.com/material-foundation/flutter-packages/tree/main/packages/dynamic_color',
r'https://github.com/material-foundation/flutter-packages/tree/main/packages/dynamic_color',
), ),
const License( const License(
name: r'flutter', name: r'flutter',
license: r''' license: r'''
Copyright 2014 The Flutter Authors. All rights reserved. Copyright 2014 The Flutter Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, Redistribution and use in source and binary forms, with or without modification,
@ -324,7 +322,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
), ),
const License( const License(
name: r'flutter_launcher_icons', name: r'flutter_launcher_icons',
license: r''' license: r'''
MIT License MIT License
Copyright (c) 2019 Mark O'Sullivan Copyright (c) 2019 Mark O'Sullivan
@ -347,14 +345,13 @@ 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 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
''', ''',
version: r'0.13.1', version: r'^0.13.1',
homepage: r'https://github.com/fluttercommunity/flutter_launcher_icons', homepage: r'https://github.com/fluttercommunity/flutter_launcher_icons',
repository: repository: r'https://github.com/fluttercommunity/flutter_launcher_icons/',
r'https://github.com/fluttercommunity/flutter_launcher_icons/',
), ),
const License( const License(
name: r'flutter_lints', name: r'flutter_lints',
license: r''' license: r'''
Copyright 2013 The Flutter Authors. All rights reserved. Copyright 2013 The Flutter Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, Redistribution and use in source and binary forms, with or without modification,
@ -381,46 +378,45 @@ 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 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
''', ''',
version: r'^2.0.2', version: r'^3.0.1',
repository: repository: r'https://github.com/flutter/packages/tree/main/packages/flutter_lints',
r'https://github.com/flutter/packages/tree/main/packages/flutter_lints',
), ),
const License( const License(
name: r'flutter_process_text', name: r'flutter_process_text',
license: r''' license: r'''
BSD 3-Clause License BSD 3-Clause License
(c) Copyright 2021 divshekhar (Divyanshu Shekhar) (c) Copyright 2021 divshekhar (Divyanshu Shekhar)
Redistribution and use in source and binary forms, with or without modification, Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met: are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, 1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer. this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, 2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution. and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors 3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software without may be used to endorse or promote products derived from this software without
specific prior written permission. specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR HOWEVER 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, 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.''', EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
version: r'^1.1.2', version: r'^1.1.2',
repository: r'https://github.com/DevsOnFlutter/flutter_process_text', repository: r'https://github.com/DevsOnFlutter/flutter_process_text',
), ),
const License( const License(
name: r'flutter_secure_storage', name: r'flutter_secure_storage',
license: r''' license: r'''
BSD 3-Clause License BSD 3-Clause License
Copyright 2017 German Saprykin Copyright 2017 German Saprykin
@ -450,13 +446,219 @@ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 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 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.''', OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
version: r'^8.0.0', version: r'^9.0.0',
repository: repository: r'https://github.com/mogol/flutter_secure_storage/tree/develop/flutter_secure_storage',
r'https://github.com/mogol/flutter_secure_storage/tree/develop/flutter_secure_storage', ),
const License(
name: r'flutter_sharing_intent',
license: r'''
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.''',
version: r'^1.1.1',
homepage: r'https://github.com/bhagat-techind/flutter_sharing_intent.git',
), ),
const License( const License(
name: r'flutter_test', name: r'flutter_test',
license: r''' license: r'''
Copyright 2014 The Flutter Authors. All rights reserved. Copyright 2014 The Flutter Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, Redistribution and use in source and binary forms, with or without modification,
@ -488,7 +690,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
), ),
const License( const License(
name: r'http', name: r'http',
license: r''' license: r'''
Copyright 2014, the Dart project authors. Copyright 2014, the Dart project authors.
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without
@ -517,12 +719,12 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
''', ''',
version: r'^0.13.6', version: r'^1.1.0',
repository: r'https://github.com/dart-lang/http/tree/master/pkgs/http', repository: r'https://github.com/dart-lang/http/tree/master/pkgs/http',
), ),
const License( const License(
name: r'intl', name: r'intl',
license: r''' license: r'''
Copyright 2013, the Dart project authors. Copyright 2013, the Dart project authors.
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without
@ -551,12 +753,12 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
''', ''',
version: r'^0.18.1', version: r'^0.19.0',
repository: r'https://github.com/dart-lang/i18n/tree/main/pkgs/intl', repository: r'https://github.com/dart-lang/i18n/tree/main/pkgs/intl',
), ),
const License( const License(
name: r'license_generator', name: r'license_generator',
license: r''' license: r'''
MIT License MIT License
Copyright (c) 2022 icapps Copyright (c) 2022 icapps
@ -579,12 +781,12 @@ 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 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
''', ''',
version: r'^1.0.5', version: r'^2.0.0',
homepage: r'https://github.com/icapps/flutter-icapps-license', homepage: r'https://github.com/icapps/flutter-icapps-license',
), ),
const License( const License(
name: r'package_info_plus', name: r'package_info_plus',
license: r''' license: r'''
Copyright 2017 The Chromium Authors. All rights reserved. Copyright 2017 The Chromium Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without
@ -615,12 +817,11 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
''', ''',
version: r'^4.0.2', version: r'^4.0.2',
homepage: r'https://plus.fluttercommunity.dev/', homepage: r'https://plus.fluttercommunity.dev/',
repository: repository: r'https://github.com/fluttercommunity/plus_plugins/tree/main/packages/package_info_plus/package_info_plus',
r'https://github.com/fluttercommunity/plus_plugins/tree/main/packages/',
), ),
const License( const License(
name: r'qr_flutter', name: r'qr_flutter',
license: r''' license: r'''
BSD 3-Clause License BSD 3-Clause License
Copyright (c) 2020, Luke Freeman. Copyright (c) 2020, Luke Freeman.
@ -656,7 +857,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
), ),
const License( const License(
name: r'shared_preferences', name: r'shared_preferences',
license: r''' license: r'''
Copyright 2013 The Flutter Authors. All rights reserved. Copyright 2013 The Flutter Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, Redistribution and use in source and binary forms, with or without modification,
@ -684,12 +885,11 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
''', ''',
version: r'^2.2.2', version: r'^2.2.2',
repository: repository: r'https://github.com/flutter/packages/tree/main/packages/shared_preferences/shared_preferences',
r'https://github.com/flutter/packages/tree/main/packages/shared_preferences/shared_preferences',
), ),
const License( const License(
name: r'tuple', name: r'tuple',
license: r''' license: r'''
Copyright (c) 2014, the tuple project authors. Copyright (c) 2014, the tuple project authors.
All rights reserved. All rights reserved.
@ -717,7 +917,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
), ),
const License( const License(
name: r'url_launcher', name: r'url_launcher',
license: r''' license: r'''
Copyright 2013 The Flutter Authors. All rights reserved. Copyright 2013 The Flutter Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, Redistribution and use in source and binary forms, with or without modification,
@ -744,9 +944,8 @@ 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 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
''', ''',
version: r'6.1.9', version: r'^6.2.4',
repository: repository: r'https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher',
r'https://github.com/flutter/plugins/tree/main/packages/url_launcher/url_launcher',
), ),
]; ];
} }

View File

@ -16,4 +16,4 @@ Color stringToColor(String string) {
return const Color(0xff000000); return const Color(0xff000000);
} }
return Color.fromARGB(1, rgb[0], rgb[1], rgb[2]); return Color.fromARGB(1, rgb[0], rgb[1], rgb[2]);
} }

View File

@ -1,9 +1,15 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_sharing_intent/flutter_sharing_intent.dart';
import 'package:flutter_sharing_intent/model/sharing_file.dart';
import 'package:qr_flutter/qr_flutter.dart'; import 'package:qr_flutter/qr_flutter.dart';
import 'package:shlink_app/API/Classes/ShlinkStats/shlink_stats.dart'; import 'package:shlink_app/API/Classes/ShlinkStats/shlink_stats.dart';
import 'package:shlink_app/API/server_manager.dart'; import 'package:shlink_app/util/build_api_error_snackbar.dart';
import 'package:shlink_app/views/short_url_edit_view.dart'; import 'package:shlink_app/views/short_url_edit_view.dart';
import 'package:shlink_app/views/url_list_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 '../API/Classes/ShortURL/short_url.dart';
import '../globals.dart' as globals; import '../globals.dart' as globals;
@ -22,15 +28,33 @@ class _HomeViewState extends State<HomeView> {
bool _qrCodeShown = false; bool _qrCodeShown = false;
String _qrUrl = ""; String _qrUrl = "";
late StreamSubscription _intentDataStreamSubscription;
@override @override
void initState() { void initState() {
// TODO: implement initState
super.initState(); super.initState();
initializeActionProcessText();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
loadAllData(); loadAllData();
}); });
} }
Future<void> initializeActionProcessText() async {
_intentDataStreamSubscription =
FlutterSharingIntent.instance.getMediaStream().listen(_handleIntentUrl);
FlutterSharingIntent.instance.getInitialSharing().then(_handleIntentUrl);
}
Future<void> _handleIntentUrl(List<SharedFile> value) async {
String inputUrlText = value.firstOrNull?.value ?? "";
if (await canLaunchUrlString(inputUrlText)) {
await Navigator.of(context).push(MaterialPageRoute(
builder: (context) => ShortURLEditView(longUrl: inputUrlText)));
await loadAllData();
}
}
Future<void> loadAllData() async { Future<void> loadAllData() async {
await loadShlinkStats(); await loadShlinkStats();
await loadRecentShortUrls(); await loadRecentShortUrls();
@ -44,18 +68,9 @@ class _HomeViewState extends State<HomeView> {
shlinkStats = l; shlinkStats = l;
}); });
}, (r) { }, (r) {
var text = ""; ScaffoldMessenger.of(context).showSnackBar(
if (r is RequestFailure) { buildApiErrorSnackbar(r, context)
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);
}); });
} }
@ -67,18 +82,9 @@ class _HomeViewState extends State<HomeView> {
shortUrlsLoaded = true; shortUrlsLoaded = true;
}); });
}, (r) { }, (r) {
var text = ""; ScaffoldMessenger.of(context).showSnackBar(
if (r is RequestFailure) { buildApiErrorSnackbar(r, context)
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);
}); });
} }
@ -98,15 +104,25 @@ class _HomeViewState extends State<HomeView> {
child: CustomScrollView( child: CustomScrollView(
slivers: [ slivers: [
SliverAppBar.medium( SliverAppBar.medium(
automaticallyImplyLeading: false,
expandedHeight: 160, expandedHeight: 160,
title: Column( title: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Text("Shlink", const Text("Shlink",
style: TextStyle(fontWeight: FontWeight.bold)), style: TextStyle(fontWeight: FontWeight.bold)),
Text(globals.serverManager.getServerUrl(), GestureDetector(
style: TextStyle( onTap: () {
fontSize: 16, color: Colors.grey[600])) showModalBottomSheet(
context: context,
builder: (BuildContext context) {
return const AvailableServerBottomSheet();
});
},
child: Text(globals.serverManager.getServerUrl(),
style: TextStyle(
fontSize: 16, color: Theme.of(context).colorScheme.onTertiary)),
)
], ],
)), )),
SliverToBoxAdapter( SliverToBoxAdapter(
@ -155,7 +171,7 @@ class _HomeViewState extends State<HomeView> {
'Create one by tapping the "+" button below', 'Create one by tapping the "+" button below',
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
color: Colors.grey[600]), color: Theme.of(context).colorScheme.onSecondary),
), ),
) )
], ],
@ -217,18 +233,12 @@ class _HomeViewState extends State<HomeView> {
eyeStyle: QrEyeStyle( eyeStyle: QrEyeStyle(
eyeShape: QrEyeShape.square, eyeShape: QrEyeShape.square,
color: color:
MediaQuery.of(context).platformBrightness == Theme.of(context).colorScheme.onPrimary
Brightness.dark
? Colors.white
: Colors.black,
), ),
dataModuleStyle: QrDataModuleStyle( dataModuleStyle: QrDataModuleStyle(
dataModuleShape: QrDataModuleShape.square, dataModuleShape: QrDataModuleShape.square,
color: color:
MediaQuery.of(context).platformBrightness == Theme.of(context).colorScheme.onPrimary
Brightness.dark
? Colors.white
: Colors.black,
), ),
))), ))),
), ),

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shlink_app/API/server_manager.dart'; import 'package:shlink_app/API/server_manager.dart';
import 'package:shlink_app/main.dart'; import 'package:shlink_app/main.dart';
import 'package:url_launcher/url_launcher.dart';
import '../globals.dart' as globals; import '../globals.dart' as globals;
class LoginView extends StatefulWidget { class LoginView extends StatefulWidget {
@ -58,6 +59,7 @@ class _LoginViewState extends State<LoginView> {
return Scaffold( return Scaffold(
extendBody: true, extendBody: true,
body: CustomScrollView( body: CustomScrollView(
physics: const NeverScrollableScrollPhysics(),
slivers: [ slivers: [
const SliverAppBar.medium( const SliverAppBar.medium(
title: Text("Add server", title: Text("Add server",
@ -65,79 +67,107 @@ class _LoginViewState extends State<LoginView> {
SliverFillRemaining( SliverFillRemaining(
child: Padding( child: Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Column( child: Stack(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Padding( Align(
padding: EdgeInsets.only(bottom: 8), child: Column(
child: Text(
"Server URL",
style: TextStyle(fontWeight: FontWeight.bold),
)),
Row(
children: [
const Icon(Icons.dns_outlined),
const SizedBox(width: 8),
Expanded(
child: TextField(
controller: _serverUrlController,
keyboardType: TextInputType.url,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: "https://shlink.example.com"),
))
],
),
const Padding(
padding: EdgeInsets.only(top: 8, bottom: 8),
child: Text("API Key",
style: TextStyle(fontWeight: FontWeight.bold)),
),
Row(
children: [
const Icon(Icons.key),
const SizedBox(width: 8),
Expanded(
child: TextField(
controller: _apiKeyController,
keyboardType: TextInputType.text,
obscureText: true,
decoration: const InputDecoration(
border: OutlineInputBorder(), labelText: "..."),
))
],
),
Padding(
padding: const EdgeInsets.only(top: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
FilledButton.tonal( const Padding(
onPressed: () => {_connect()}, padding: EdgeInsets.only(bottom: 8),
child: _isLoggingIn child: Text(
? Container( "Server URL",
style: TextStyle(fontWeight: FontWeight.bold),
)),
Row(
children: [
const Icon(Icons.dns_outlined),
const SizedBox(width: 8),
Expanded(
child: TextField(
controller: _serverUrlController,
keyboardType: TextInputType.url,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: "https://shlink.example.com"),
))
],
),
const Padding(
padding: EdgeInsets.only(top: 8, bottom: 8),
child: Text("API Key",
style: TextStyle(fontWeight: FontWeight.bold)),
),
Row(
children: [
const Icon(Icons.key),
const SizedBox(width: 8),
Expanded(
child: TextField(
controller: _apiKeyController,
keyboardType: TextInputType.text,
obscureText: true,
decoration: const InputDecoration(
border: OutlineInputBorder(), labelText: "..."),
))
],
),
Padding(
padding: const EdgeInsets.only(top: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FilledButton.tonal(
onPressed: () => {_connect()},
child: _isLoggingIn
? Container(
width: 34, width: 34,
height: 34, height: 34,
padding: const EdgeInsets.all(4), padding: const EdgeInsets.all(4),
child: const CircularProgressIndicator(), child: const CircularProgressIndicator(),
) )
: const Text("Connect", : const Text("Connect",
style: TextStyle(fontSize: 20)), style: TextStyle(fontSize: 20)),
) )
],
),
),
Padding(
padding: const EdgeInsets.only(top: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Flexible(
child: Text(_errorMessage,
style: TextStyle(color: Theme.of(context).colorScheme.onError),
textAlign: TextAlign.center))
],
),
),
], ],
), ),
), ),
Padding( Align(
padding: const EdgeInsets.only(top: 16), alignment: Alignment.bottomCenter,
child: Row( child: TextButton(
mainAxisAlignment: MainAxisAlignment.center, onPressed: () async {
children: [ final Uri url = Uri.parse('https://shlink.io/documentation/api-docs/authentication/');
Flexible( try {
child: Text(_errorMessage, if (!await launchUrl(url)) {
style: const TextStyle(color: Colors.red), throw Exception();
textAlign: TextAlign.center)) }
], } catch (e) {
final snackBar = SnackBar(
content: Text("Unable to launch url. See Shlink docs for more information.",
style: TextStyle(color: Theme.of(context).colorScheme.onError)),
backgroundColor: Theme.of(context).colorScheme.error,
behavior: SnackBarBehavior.floating);
ScaffoldMessenger.of(context).showSnackBar(
snackBar);
}
},
child: Text("How to create an API Key"),
), ),
) )
], ],

View File

@ -40,9 +40,7 @@ class _OpenSourceLicensesViewState extends State<OpenSourceLicensesView> {
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
color: Theme.of(context).brightness == Brightness.light color: Theme.of(context).colorScheme.surfaceContainer,
? Colors.grey[100]
: Colors.grey[900],
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
@ -54,13 +52,13 @@ class _OpenSourceLicensesViewState extends State<OpenSourceLicensesView> {
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.bold, fontSize: 18)), fontWeight: FontWeight.bold, fontSize: 18)),
Text("Version: ${currentLicense.version ?? "N/A"}", Text("Version: ${currentLicense.version ?? "N/A"}",
style: const TextStyle(color: Colors.grey)), style: TextStyle(color: Theme.of(context).colorScheme.onTertiary)),
const SizedBox(height: 8), const SizedBox(height: 8),
const Divider(), Divider(color: Theme.of(context).dividerColor),
const SizedBox(height: 8), const SizedBox(height: 8),
Text(currentLicense.license, Text(currentLicense.license,
textAlign: TextAlign.justify, textAlign: TextAlign.justify,
style: const TextStyle(color: Colors.grey)), style: TextStyle(color: Theme.of(context).colorScheme.onTertiary)),
], ],
), ),
), ),
@ -69,12 +67,12 @@ class _OpenSourceLicensesViewState extends State<OpenSourceLicensesView> {
); );
}, childCount: LicenseUtil.getLicenses().length), }, childCount: LicenseUtil.getLicenses().length),
), ),
const SliverToBoxAdapter( SliverToBoxAdapter(
child: Padding( child: Padding(
padding: EdgeInsets.only(top: 8, bottom: 20), padding: EdgeInsets.only(top: 8, bottom: 20),
child: Text( child: Text(
"Thank you to all maintainers of these repositories 💝", "Thank you to all maintainers of these repositories 💝",
style: TextStyle(color: Colors.grey), style: TextStyle(color: Theme.of(context).colorScheme.onTertiary),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
)) ))

View File

@ -0,0 +1,267 @@
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/util/build_api_error_snackbar.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) {
ScaffoldMessenger.of(context).showSnackBar(
buildApiErrorSnackbar(r, context)
);
return false;
});
}
void _saveRedirectRules() async {
final response = await globals.serverManager
.setRedirectRules(widget.shortURL.shortCode, redirectRules);
response.fold((l) {
Navigator.pop(context);
}, (r) {
ScaffoldMessenger.of(context).showSnackBar(
buildApiErrorSnackbar(r, context)
);
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, color: Colors.white))
: 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: Theme.of(context).colorScheme.onSecondary),
),
)
],
))))
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: Theme.of(context).dividerColor)),
),
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:
Theme.of(context).colorScheme.tertiary,
),
child: Text(_conditionToTagString(condition)),
),
);
}).toList(),
),
)
],
),
Wrap(
children: [
IconButton(
disabledColor:
Theme.of(context).disabledColor,
onPressed: widget.moveUp,
icon: const Icon(Icons.arrow_upward),
),
IconButton(
disabledColor:
Theme.of(context).disabledColor,
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:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:shlink_app/API/server_manager.dart'; import 'package:shlink_app/util/build_api_error_snackbar.dart';
import 'package:shlink_app/views/login_view.dart';
import 'package:shlink_app/views/opensource_licenses_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 'package:url_launcher/url_launcher.dart';
import '../globals.dart' as globals; import '../globals.dart' as globals;
@ -31,7 +31,7 @@ class _SettingsViewState extends State<SettingsView> {
void getServerHealth() async { void getServerHealth() async {
var packageInfo = await PackageInfo.fromPlatform(); var packageInfo = await PackageInfo.fromPlatform();
setState(() { setState(() {
packageInfo = packageInfo; this.packageInfo = packageInfo;
}); });
final response = await globals.serverManager.getServerHealth(); final response = await globals.serverManager.getServerHealth();
response.fold((l) { response.fold((l) {
@ -43,19 +43,9 @@ class _SettingsViewState extends State<SettingsView> {
setState(() { setState(() {
_serverStatus = ServerStatus.disconnected; _serverStatus = ServerStatus.disconnected;
}); });
ScaffoldMessenger.of(context).showSnackBar(
var text = ""; buildApiErrorSnackbar(r, context)
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);
}); });
} }
@ -64,97 +54,84 @@ class _SettingsViewState extends State<SettingsView> {
return Scaffold( return Scaffold(
body: CustomScrollView( body: CustomScrollView(
slivers: [ slivers: [
SliverAppBar.medium( const SliverAppBar.medium(
expandedHeight: 120, expandedHeight: 120,
title: const Text( title: Text(
"Settings", "Settings",
style: TextStyle(fontWeight: FontWeight.bold), 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( SliverToBoxAdapter(
child: Padding( child: Padding(
padding: const EdgeInsets.all(12.0), padding: const EdgeInsets.all(12.0),
child: Column( child: Column(
children: [ children: [
Container( GestureDetector(
decoration: BoxDecoration( onTap: () {
borderRadius: BorderRadius.circular(8), showModalBottomSheet(
color: Theme.of(context).brightness == Brightness.light context: context,
? Colors.grey[100] builder: (BuildContext context) {
: Colors.grey[900], return const AvailableServerBottomSheet();
), });
child: Padding( },
padding: const EdgeInsets.all(12.0), child: Container(
child: Row( decoration: BoxDecoration(
children: [ borderRadius: BorderRadius.circular(8),
Icon(Icons.dns_outlined, color: Theme.of(context).colorScheme.surfaceContainer
color: (() { ),
switch (_serverStatus) { child: Padding(
case ServerStatus.connected: padding: const EdgeInsets.all(12.0),
return Colors.green; child: Row(
case ServerStatus.connecting: children: [
return Colors.orange; Icon(Icons.dns_outlined,
case ServerStatus.disconnected: color: (() {
return Colors.red; switch (_serverStatus) {
} case ServerStatus.connected:
}())), return Colors.green;
const SizedBox(width: 8), case ServerStatus.connecting:
Column( return Colors.orange;
crossAxisAlignment: CrossAxisAlignment.start, case ServerStatus.disconnected:
children: [ return Colors.red;
const Text("Connected to", }
style: TextStyle(color: Colors.grey)), }())),
Text(globals.serverManager.getServerUrl(), const SizedBox(width: 8),
style: const TextStyle( Column(
fontWeight: FontWeight.bold, crossAxisAlignment: CrossAxisAlignment.start,
fontSize: 16)), children: [
Row( Text("Connected to",
children: [ style: TextStyle(color: Theme.of(context).colorScheme.onTertiary)),
const Text("API Version: ", Text(globals.serverManager.getServerUrl(),
style: TextStyle( style: const TextStyle(
color: Colors.grey, fontWeight: FontWeight.bold,
fontWeight: FontWeight.w600)), fontSize: 16)),
Text(globals.serverManager.getApiVersion(), Row(
style: children: [
const TextStyle(color: Colors.grey)), Text("API Version: ",
const SizedBox(width: 16), style: TextStyle(
const Text("Server Version: ", color: Theme.of(context).colorScheme.onTertiary,
style: TextStyle( fontWeight: FontWeight.w600)),
color: Colors.grey, Text(globals.serverManager.getApiVersion(),
fontWeight: FontWeight.w600)), style: TextStyle(
Text(_serverVersion, color: Theme.of(context).colorScheme.onTertiary)),
style: const SizedBox(width: 16),
const TextStyle(color: Colors.grey)) Text("Server Version: ",
], style: TextStyle(
), color: Theme.of(context).colorScheme.onTertiary,
], fontWeight: FontWeight.w600)),
) Text(_serverVersion,
], style:
TextStyle(color: Theme.of(context).colorScheme.onTertiary))
],
),
],
)
],
),
), ),
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
const Divider(), Divider(color: Theme.of(context).dividerColor),
const SizedBox(height: 8), const SizedBox(height: 8),
GestureDetector( GestureDetector(
onTap: () { onTap: () {
@ -165,9 +142,7 @@ class _SettingsViewState extends State<SettingsView> {
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
color: Theme.of(context).brightness == Brightness.light color: Theme.of(context).colorScheme.surfaceContainer
? Colors.grey[100]
: Colors.grey[900],
), ),
child: const Padding( child: const Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(
@ -201,9 +176,7 @@ class _SettingsViewState extends State<SettingsView> {
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
color: Theme.of(context).brightness == Brightness.light color: Theme.of(context).colorScheme.surfaceContainer
? Colors.grey[100]
: Colors.grey[900],
), ),
child: const Padding( child: const Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(
@ -237,9 +210,7 @@ class _SettingsViewState extends State<SettingsView> {
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
color: Theme.of(context).brightness == Brightness.light color: Theme.of(context).colorScheme.surfaceContainer
? Colors.grey[100]
: Colors.grey[900],
), ),
child: const Padding( child: const Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(
@ -266,10 +237,19 @@ class _SettingsViewState extends State<SettingsView> {
Row( Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
Text( Container(
"${packageInfo.appName}, v${packageInfo.version} (${packageInfo.buildNumber})", padding: const EdgeInsets.only(
style: const TextStyle(color: Colors.grey), left: 8, right: 8, top: 4, bottom: 4),
), decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color:
Theme.of(context).colorScheme.surfaceContainer
),
child: Text(
"${packageInfo.appName}, v${packageInfo.version} (${packageInfo.buildNumber})",
style: TextStyle(color: Theme.of(context).colorScheme.onSecondary),
),
)
], ],
) )
], ],

View File

@ -1,15 +1,21 @@
import 'package:dartz/dartz.dart' as dartz; import 'package:dartz/dartz.dart' as dartz;
import 'package:dynamic_color/dynamic_color.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:shlink_app/API/Classes/ShortURL/short_url.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/Classes/ShortURLSubmission/short_url_submission.dart';
import 'package:shlink_app/API/server_manager.dart'; import 'package:shlink_app/API/server_manager.dart';
import 'package:shlink_app/util/build_api_error_snackbar.dart';
import 'package:shlink_app/util/string_to_color.dart';
import 'package:shlink_app/views/tag_selector_view.dart';
import 'package:shlink_app/widgets/url_tags_list_widget.dart';
import '../globals.dart' as globals; import '../globals.dart' as globals;
class ShortURLEditView extends StatefulWidget { class ShortURLEditView extends StatefulWidget {
const ShortURLEditView({super.key, this.shortUrl}); const ShortURLEditView({super.key, this.shortUrl, this.longUrl});
final ShortURL? shortUrl; final ShortURL? shortUrl;
final String? longUrl;
@override @override
State<ShortURLEditView> createState() => _ShortURLEditViewState(); State<ShortURLEditView> createState() => _ShortURLEditViewState();
@ -21,6 +27,7 @@ class _ShortURLEditViewState extends State<ShortURLEditView>
final customSlugController = TextEditingController(); final customSlugController = TextEditingController();
final titleController = TextEditingController(); final titleController = TextEditingController();
final randomSlugLengthController = TextEditingController(text: "5"); final randomSlugLengthController = TextEditingController(text: "5");
List<String> tags = [];
bool randomSlug = true; bool randomSlug = true;
bool isCrawlable = true; bool isCrawlable = true;
@ -43,6 +50,9 @@ class _ShortURLEditViewState extends State<ShortURLEditView>
duration: const Duration(milliseconds: 500), duration: const Duration(milliseconds: 500),
); );
loadExistingUrl(); loadExistingUrl();
if (widget.longUrl != null) {
longUrlController.text = widget.longUrl!;
}
super.initState(); super.initState();
} }
@ -59,6 +69,7 @@ class _ShortURLEditViewState extends State<ShortURLEditView>
if (widget.shortUrl != null) { if (widget.shortUrl != null) {
longUrlController.text = widget.shortUrl!.longUrl; longUrlController.text = widget.shortUrl!.longUrl;
isCrawlable = widget.shortUrl!.crawlable; isCrawlable = widget.shortUrl!.crawlable;
tags = widget.shortUrl!.tags;
// for some reason this attribute is not returned by the api // for some reason this attribute is not returned by the api
forwardQuery = true; forwardQuery = true;
titleController.text = widget.shortUrl!.title ?? ""; titleController.text = widget.shortUrl!.title ?? "";
@ -68,10 +79,38 @@ class _ShortURLEditViewState extends State<ShortURLEditView>
} }
} }
void _saveButtonPressed() {
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();
}
}
}
void _submitShortUrl() async { void _submitShortUrl() async {
var newSubmission = ShortURLSubmission( var newSubmission = ShortURLSubmission(
longUrl: longUrlController.text, longUrl: longUrlController.text,
tags: [], tags: tags,
crawlable: isCrawlable, crawlable: isCrawlable,
forwardQuery: forwardQuery, forwardQuery: forwardQuery,
findIfExists: true, findIfExists: true,
@ -115,22 +154,9 @@ class _ShortURLEditViewState extends State<ShortURLEditView>
isSaving = false; isSaving = false;
}); });
var text = ""; ScaffoldMessenger.of(context).showSnackBar(
buildApiErrorSnackbar(r, context)
if (r is RequestFailure) { );
text = r.description;
} else {
text = (r as ApiFailure).detail;
if ((r).invalidElements != null) {
text = "$text: ${(r).invalidElements}";
}
}
final snackBar = SnackBar(
content: Text(text),
backgroundColor: Colors.red[400],
behavior: SnackBarBehavior.floating);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
return false; return false;
}); });
} }
@ -140,211 +166,239 @@ class _ShortURLEditViewState extends State<ShortURLEditView>
return Scaffold( return Scaffold(
body: CustomScrollView( body: CustomScrollView(
slivers: [ slivers: [
const SliverAppBar.medium( SliverAppBar.medium(
title: Text("New Short URL", title: Text("${disableSlugEditor ? "Edit" : "New"} Short URL",
style: TextStyle(fontWeight: FontWeight.bold)), style: const TextStyle(fontWeight: FontWeight.bold)),
), ),
SliverToBoxAdapter( SliverToBoxAdapter(
child: Padding( child: Padding(
padding: const EdgeInsets.only(left: 16, right: 16, top: 16), padding: const EdgeInsets.only(top: 16, left: 8, right: 8),
child: Column( child: Wrap(
children: [ runSpacing: 16,
TextField( children: [
controller: longUrlController, TextField(
decoration: InputDecoration( controller: longUrlController,
errorText: longUrlError != "" ? longUrlError : null, decoration: InputDecoration(
border: const OutlineInputBorder(), errorText: longUrlError != "" ? longUrlError : null,
label: const Row( border: const OutlineInputBorder(),
children: [ label: const Row(
Icon(Icons.public), children: [
SizedBox(width: 8), Icon(Icons.public),
Text("Long URL") SizedBox(width: 8),
], Text("Long URL")
)), ],
), )),
const SizedBox(height: 16), ),
Row( Row(
children: [ children: [
Expanded( Expanded(
child: TextField( child: TextField(
enabled: !disableSlugEditor, enabled: !disableSlugEditor,
controller: customSlugController, controller: customSlugController,
style: TextStyle( style: TextStyle(
color: randomSlug color: randomSlug
? Colors.grey ? Theme.of(context).colorScheme.onTertiary
: Theme.of(context).brightness == : Theme.of(context).colorScheme.onPrimary),
Brightness.light onChanged: (_) {
? Colors.black if (randomSlug) {
: Colors.white), setState(() {
onChanged: (_) { randomSlug = false;
if (randomSlug) { });
setState(() { }
randomSlug = false; },
}); decoration: InputDecoration(
} border: const OutlineInputBorder(),
}, label: Row(
decoration: InputDecoration( children: [
border: const OutlineInputBorder(), const Icon(Icons.link),
const SizedBox(width: 8),
Text(
"${randomSlug ? "Random" : "Custom"} slug",
style: TextStyle(
fontStyle: randomSlug
? FontStyle.italic
: FontStyle.normal),
)
],
)),
),
),
if (widget.shortUrl == null)
Container(
padding: const EdgeInsets.only(left: 8),
child: RotationTransition(
turns: Tween(begin: 0.0, end: 3.0).animate(
CurvedAnimation(
parent: _customSlugDiceAnimationController,
curve: Curves.easeInOutExpo)),
child: IconButton(
onPressed: disableSlugEditor
? null
: () {
if (randomSlug) {
_customSlugDiceAnimationController.reverse(
from: 1);
} else {
_customSlugDiceAnimationController.forward(
from: 0);
}
setState(() {
randomSlug = !randomSlug;
});
},
icon: Icon(
randomSlug ? Icons.casino : Icons.casino_outlined,
color: randomSlug ? Colors.green : Colors.grey)),
),
)
],
),
if (randomSlug && widget.shortUrl == null)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text("Random slug length"),
SizedBox(
width: 100,
child: TextField(
controller: randomSlugLengthController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
errorText:
randomSlugLengthError != "" ? "" : null,
border: const OutlineInputBorder(),
label: const Row(
children: [
Icon(Icons.tag),
SizedBox(width: 8),
Text("Length")
],
)),
))
],
),
TextField(
controller: titleController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
label: Row(
children: [
Icon(Icons.badge),
SizedBox(width: 8),
Text("Title")
],
)),
),
GestureDetector(
onTap: () async {
List<String>? selectedTags = await Navigator.of(context).
push(MaterialPageRoute(
builder: (context) =>
TagSelectorView(alreadySelectedTags: tags)));
if (selectedTags != null) {
setState(() {
tags = selectedTags;
});
}
},
child: InputDecorator(
isEmpty: tags.isEmpty,
decoration: const InputDecoration(
border: OutlineInputBorder(),
label: Row( label: Row(
children: [ children: [
const Icon(Icons.link), Icon(Icons.label_outline),
const SizedBox(width: 8), SizedBox(width: 8),
Text( Text("Tags")
"${randomSlug ? "Random" : "Custom"} slug",
style: TextStyle(
fontStyle: randomSlug
? FontStyle.italic
: FontStyle.normal),
)
], ],
)), )),
), child: Wrap(
runSpacing: 8,
spacing: 8,
children: tags.map((tag) {
var boxColor = stringToColor(tag)
.harmonizeWith(Theme.of(context).colorScheme.
primary);
var textColor = boxColor.computeLuminance() < 0.5
? Colors.white
: Colors.black;
return InputChip(
label: Text(tag, style: TextStyle(
color: textColor
)),
backgroundColor: boxColor,
deleteIcon: Icon(Icons.close,
size: 18,
color: textColor),
onDeleted: () {
setState(() {
tags.remove(tag);
});
},
);
}).toList(),
)
), ),
const SizedBox(width: 8), ),
RotationTransition(
turns: Tween(begin: 0.0, end: 3.0).animate(
CurvedAnimation(
parent: _customSlugDiceAnimationController,
curve: Curves.easeInOutExpo)),
child: IconButton(
onPressed: disableSlugEditor ? null : () {
if (randomSlug) {
_customSlugDiceAnimationController.reverse(
from: 1);
} else {
_customSlugDiceAnimationController.forward(
from: 0);
}
setState(() {
randomSlug = !randomSlug;
});
},
icon: Icon(
randomSlug ? Icons.casino : Icons.casino_outlined,
color: randomSlug ? Colors.green : Colors.grey)),
)
],
),
if (randomSlug) const SizedBox(height: 16),
if (randomSlug)
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
const Text("Random slug length"), const Text("Crawlable"),
SizedBox( Switch(
width: 100, value: isCrawlable,
child: TextField( onChanged: (_) {
controller: randomSlugLengthController, setState(() {
keyboardType: TextInputType.number, isCrawlable = !isCrawlable;
decoration: InputDecoration( });
errorText: },
randomSlugLengthError != "" ? "" : null, )
border: const OutlineInputBorder(),
label: const Row(
children: [
Icon(Icons.tag),
SizedBox(width: 8),
Text("Length")
],
)),
))
], ],
), ),
const SizedBox(height: 16), Row(
TextField( mainAxisAlignment: MainAxisAlignment.spaceBetween,
controller: titleController, children: [
decoration: const InputDecoration( const Text("Forward query params"),
border: OutlineInputBorder(), Switch(
label: Row( value: forwardQuery,
children: [ onChanged: (_) {
Icon(Icons.badge), setState(() {
SizedBox(width: 8), forwardQuery = !forwardQuery;
Text("Title") });
], },
)), )
), ],
const SizedBox(height: 16), ),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
const Text("Crawlable"), const Text("Copy to clipboard"),
Switch( Switch(
value: isCrawlable, value: copyToClipboard,
onChanged: (_) { onChanged: (_) {
setState(() { setState(() {
isCrawlable = !isCrawlable; copyToClipboard = !copyToClipboard;
}); });
}, },
) )
], ],
), ),
const SizedBox(height: 16), const SizedBox(height: 150)
Row( ],
mainAxisAlignment: MainAxisAlignment.spaceBetween, ),
children: [ )
const Text("Forward query params"), )
Switch(
value: forwardQuery,
onChanged: (_) {
setState(() {
forwardQuery = !forwardQuery;
});
},
)
],
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text("Copy to clipboard"),
Switch(
value: copyToClipboard,
onChanged: (_) {
setState(() {
copyToClipboard = !copyToClipboard;
});
},
)
],
),
],
),
))
], ],
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
onPressed: () { onPressed: () {
if (!isSaving) { _saveButtonPressed();
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 child: isSaving
? const Padding( ? const Padding(
padding: EdgeInsets.all(16), padding: EdgeInsets.all(16),
child: CircularProgressIndicator(strokeWidth: 3)) child: CircularProgressIndicator(strokeWidth: 3,
color: Colors.white))
: const Icon(Icons.save)), : const Icon(Icons.save)),
); );
} }

View File

@ -0,0 +1,246 @@
import 'package:dynamic_color/dynamic_color.dart';
import 'package:flutter/material.dart';
import 'package:shlink_app/API/Classes/ShortURL/visits_summary.dart';
import 'package:shlink_app/API/Classes/Tag/tag_with_stats.dart';
import 'package:shlink_app/util/build_api_error_snackbar.dart';
import 'package:shlink_app/util/string_to_color.dart';
import '../globals.dart' as globals;
class TagSelectorView extends StatefulWidget {
const TagSelectorView({super.key, this.alreadySelectedTags = const []});
final List<String> alreadySelectedTags;
@override
State<TagSelectorView> createState() => _TagSelectorViewState();
}
class _TagSelectorViewState extends State<TagSelectorView> {
final FocusNode searchTagFocusNode = FocusNode();
final searchTagController = TextEditingController();
List<TagWithStats> availableTags = [];
List<TagWithStats> selectedTags = [];
List<TagWithStats> filteredTags = [];
bool tagsLoaded = false;
@override
void initState() {
super.initState();
selectedTags = [];
searchTagController.text = "";
filteredTags = [];
searchTagFocusNode.requestFocus();
WidgetsBinding.instance.addPostFrameCallback((_) => loadTags());
}
@override
void dispose() {
searchTagFocusNode.dispose();
searchTagController.dispose();
super.dispose();
}
Future<void> loadTags() async {
final response =
await globals.serverManager.getTags();
response.fold((l) {
List<TagWithStats> mappedAlreadySelectedTags =
widget.alreadySelectedTags.map((e) {
return l.firstWhere((t) => t.tag == e, orElse: () {
// account for newly created tags
return TagWithStats(e, 0, VisitsSummary(0,0,0));
});
}).toList();
setState(() {
availableTags = (l + [... mappedAlreadySelectedTags]).toSet().toList();
selectedTags = [...mappedAlreadySelectedTags];
filteredTags = availableTags;
tagsLoaded = true;
});
_sortLists();
return true;
}, (r) {
ScaffoldMessenger.of(context).showSnackBar(
buildApiErrorSnackbar(r, context)
);
return false;
});
}
void _sortLists() {
setState(() {
availableTags.sort((a, b) => a.tag.compareTo(b.tag));
filteredTags.sort((a, b) => a.tag.compareTo(b.tag));
});
}
void _searchTextChanged(String text) {
if (text == "") {
setState(() {
filteredTags = availableTags;
});
} else {
setState(() {
filteredTags = availableTags.where((t) => t.tag.toLowerCase()
.contains(text.toLowerCase())).toList();
});
}
_sortLists();
}
void _addNewTag(String tag) {
bool tagExists = availableTags.where((t) => t.tag == tag).toList().isNotEmpty;
if (tag != "" && !tagExists) {
TagWithStats tagWithStats = TagWithStats(tag, 0, VisitsSummary(0, 0, 0));
setState(() {
availableTags.add(tagWithStats);
selectedTags.add(tagWithStats);
_searchTextChanged(tag);
});
_sortLists();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: TextField(
controller: searchTagController,
focusNode: searchTagFocusNode,
onChanged: _searchTextChanged,
decoration: const InputDecoration(
hintText: "Start typing...",
border: InputBorder.none,
icon: Icon(Icons.label_outline),
),
),
actions: [
IconButton(
onPressed: () {
Navigator.pop(context, selectedTags.map((t) => t.tag).toList());
},
icon: const Icon(Icons.check),
)
],
),
body: CustomScrollView(
slivers: [
if (!tagsLoaded)
const SliverToBoxAdapter(
child: Center(
child: Padding(
padding: EdgeInsets.all(16),
child: CircularProgressIndicator(strokeWidth: 3),
),
),
)
else if (tagsLoaded && availableTags.isEmpty)
SliverToBoxAdapter(
child: Center(
child: Padding(
padding: const EdgeInsets.only(top: 50),
child: Column(
children: [
const Text(
"No Tags",
style: TextStyle(
fontSize: 24, fontWeight: FontWeight.bold),
),
Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(
'Start typing to add new tags!',
style: TextStyle(
fontSize: 16, color: Theme.of(context).colorScheme.onSecondary),
),
)
],
))))
else
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
bool _isSelected = selectedTags.contains(filteredTags[index]);
TagWithStats _tag = filteredTags[index];
return GestureDetector(
onTap: () {
if (_isSelected) {
setState(() {
selectedTags.remove(_tag);
});
} else {
setState(() {
selectedTags.add(_tag);
});
}
},
child: Container(
padding: const EdgeInsets.only(left: 16, right: 16,
top: 16, bottom: 16),
decoration: BoxDecoration(
color: _isSelected ? Theme.of(context).colorScheme.primary : null,
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Wrap(
spacing: 10,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Container(
width: 30,
height: 30,
decoration: BoxDecoration(
color: stringToColor(_tag.tag)
.harmonizeWith(Theme.of(context).colorScheme.primary),
borderRadius: BorderRadius.circular(15)
),
),
Text(_tag.tag)
],
),
Text("${_tag.shortUrlsCount} short URL"
"${_tag.shortUrlsCount == 1 ? "" : "s"}",
style: TextStyle(
color: Theme.of(context).colorScheme.onTertiary,
fontSize: 12
),)
],
)
)
);
}, childCount: filteredTags.length
),
),
if (searchTagController.text != "" &&
!availableTags.contains(searchTagController.text))
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(top: 8, bottom: 8,
left: 16, right: 16),
child: Center(
child: TextButton(
onPressed: () {
_addNewTag(searchTagController.text);
},
child: Text('Add tag "${searchTagController.text}"'),
),
),
),
)
],
)
);
}
}

View File

@ -1,9 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shlink_app/API/Classes/ShortURL/short_url.dart'; import 'package:shlink_app/API/Classes/ShortURL/short_url.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:shlink_app/API/server_manager.dart'; import 'package:shlink_app/util/build_api_error_snackbar.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/views/short_url_edit_view.dart';
import 'package:shlink_app/widgets/url_tags_list_widget.dart'; import 'package:shlink_app/widgets/url_tags_list_widget.dart';
import 'package:url_launcher/url_launcher.dart';
import '../globals.dart' as globals; import '../globals.dart' as globals;
class URLDetailView extends StatefulWidget { class URLDetailView extends StatefulWidget {
@ -16,7 +18,6 @@ class URLDetailView extends StatefulWidget {
} }
class _URLDetailViewState extends State<URLDetailView> { class _URLDetailViewState extends State<URLDetailView> {
ShortURL shortURL = ShortURL.empty(); ShortURL shortURL = ShortURL.empty();
@override @override
void initState() { void initState() {
@ -66,18 +67,9 @@ class _URLDetailViewState extends State<URLDetailView> {
ScaffoldMessenger.of(context).showSnackBar(snackBar); ScaffoldMessenger.of(context).showSnackBar(snackBar);
return true; return true;
}, (r) { }, (r) {
var text = ""; ScaffoldMessenger.of(context).showSnackBar(
if (r is RequestFailure) { buildApiErrorSnackbar(r, context)
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; return false;
}); });
}, },
@ -99,17 +91,16 @@ class _URLDetailViewState extends State<URLDetailView> {
style: const TextStyle(fontWeight: FontWeight.bold)), style: const TextStyle(fontWeight: FontWeight.bold)),
actions: [ actions: [
IconButton( IconButton(
onPressed: () async { onPressed: () async {
ShortURL updatedUrl = await Navigator.of(context).push(MaterialPageRoute( ShortURL updatedUrl = await Navigator.of(context).push(
builder: (context) => ShortURLEditView(shortUrl: shortURL))); MaterialPageRoute(
setState(() { builder: (context) =>
shortURL = updatedUrl; ShortURLEditView(shortUrl: shortURL)));
}); setState(() {
}, shortURL = updatedUrl;
icon: const Icon( });
Icons.edit },
) icon: const Icon(Icons.edit)),
),
IconButton( IconButton(
onPressed: () { onPressed: () {
showDeletionConfirmation(); showDeletionConfirmation();
@ -122,28 +113,27 @@ class _URLDetailViewState extends State<URLDetailView> {
), ),
SliverToBoxAdapter( SliverToBoxAdapter(
child: Padding( child: Padding(
padding: const EdgeInsets.only(left: 16.0, right: 16.0), padding: const EdgeInsets.only(left: 16.0, right: 16.0),
child: UrlTagsListWidget(tags: shortURL.tags) child: UrlTagsListWidget(tags: shortURL.tags)),
),
), ),
_ListCell(title: "Short Code", content: shortURL.shortCode), _ListCell(title: "Short Code", content: shortURL.shortCode),
_ListCell(title: "Short URL", content: shortURL.shortUrl),
_ListCell(title: "Long URL", content: shortURL.longUrl),
_ListCell( _ListCell(
title: "Creation Date", content: shortURL.dateCreated), 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: ""), const _ListCell(title: "Visits", content: ""),
_ListCell( _ListCell(
title: "Total", title: "Total", content: shortURL.visitsSummary.total, sub: true),
content: shortURL.visitsSummary.total,
sub: true),
_ListCell( _ListCell(
title: "Non-Bots", title: "Non-Bots",
content: shortURL.visitsSummary.nonBots, content: shortURL.visitsSummary.nonBots,
sub: true), sub: true),
_ListCell( _ListCell(
title: "Bots", title: "Bots", content: shortURL.visitsSummary.bots, sub: true),
content: shortURL.visitsSummary.bots,
sub: true),
const _ListCell(title: "Meta", content: ""), const _ListCell(title: "Meta", content: ""),
_ListCell( _ListCell(
title: "Valid Since", title: "Valid Since",
@ -154,14 +144,9 @@ class _URLDetailViewState extends State<URLDetailView> {
content: shortURL.meta.validUntil, content: shortURL.meta.validUntil,
sub: true), sub: true),
_ListCell( _ListCell(
title: "Max Visits", title: "Max Visits", content: shortURL.meta.maxVisits, sub: true),
content: shortURL.meta.maxVisits,
sub: true),
_ListCell(title: "Domain", content: shortURL.domain), _ListCell(title: "Domain", content: shortURL.domain),
_ListCell( _ListCell(title: "Crawlable", content: shortURL.crawlable, last: true)
title: "Crawlable",
content: shortURL.crawlable,
last: true)
], ],
), ),
); );
@ -173,12 +158,16 @@ class _ListCell extends StatefulWidget {
{required this.title, {required this.title,
required this.content, required this.content,
this.sub = false, this.sub = false,
this.last = false}); this.last = false,
this.isUrl = false,
this.clickableDetailView});
final String title; final String title;
final dynamic content; final dynamic content;
final bool sub; final bool sub;
final bool last; final bool last;
final bool isUrl;
final Widget? clickableDetailView;
@override @override
State<_ListCell> createState() => _ListCellState(); State<_ListCell> createState() => _ListCellState();
@ -189,65 +178,77 @@ class _ListCellState extends State<_ListCell> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SliverToBoxAdapter( return SliverToBoxAdapter(
child: Padding( child: Padding(
padding: EdgeInsets.only(top: 16, bottom: widget.last ? 30 : 0), padding: EdgeInsets.only(top: 16, bottom: widget.last ? 30 : 0),
child: Container( child: GestureDetector(
padding: const EdgeInsets.only(top: 16, left: 8, right: 8), onTap: () async {
decoration: BoxDecoration( if (widget.clickableDetailView != null) {
border: Border( Navigator.of(context).push(MaterialPageRoute(
top: BorderSide( builder: (context) => widget.clickableDetailView!));
color: MediaQuery.of(context).platformBrightness == } else if (widget.content is String) {
Brightness.dark Uri? parsedUrl = Uri.tryParse(widget.content);
? Colors.grey[800]! if (widget.isUrl &&
: Colors.grey[300]!)), parsedUrl != null &&
), await canLaunchUrl(parsedUrl)) {
child: Row( launchUrl(parsedUrl);
mainAxisAlignment: MainAxisAlignment.spaceBetween, }
children: [ }
Row( },
children: [ child: Container(
if (widget.sub) padding: const EdgeInsets.only(top: 16, left: 8, right: 8),
Padding( decoration: BoxDecoration(
padding: const EdgeInsets.only(right: 4), border: Border(
child: SizedBox( top: BorderSide(
width: 20, color: Theme.of(context).dividerColor)),
height: 6,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Theme.of(context).brightness == Brightness.dark
? Colors.grey[700]
: Colors.grey[300],
),
),
),
),
Text(
widget.title,
style: const TextStyle(fontWeight: FontWeight.bold),
)
],
),
if (widget.content is bool)
Icon(widget.content ? Icons.check : Icons.close,
color: widget.content ? Colors.green : Colors.red)
else if (widget.content is int)
Text(widget.content.toString())
else if (widget.content is String)
Expanded(
child: Text(
widget.content,
textAlign: TextAlign.end,
overflow: TextOverflow.ellipsis,
maxLines: 1,
), ),
) child: Row(
else if (widget.content is DateTime) mainAxisAlignment: MainAxisAlignment.spaceBetween,
Text(DateFormat('yyyy-MM-dd - HH:mm').format(widget.content)) children: [
else Row(
const Text("N/A") children: [
], if (widget.sub)
), Padding(
), padding: const EdgeInsets.only(right: 4),
)); child: SizedBox(
width: 20,
height: 6,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Theme.of(context).colorScheme.outline,
),
),
),
),
Text(
widget.title,
style: const TextStyle(fontWeight: FontWeight.bold),
)
],
),
if (widget.content is bool)
Icon(widget.content ? Icons.check : Icons.close,
color: widget.content ? Colors.green : Colors.red)
else if (widget.content is int)
Text(widget.content.toString())
else if (widget.content is String)
Expanded(
child: Text(
widget.content,
textAlign: TextAlign.end,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
)
else if (widget.content is DateTime)
Text(DateFormat('yyyy-MM-dd - HH:mm')
.format(widget.content))
else if (widget.clickableDetailView != null)
const Icon(Icons.chevron_right)
else
const Text("N/A")
],
),
),
)));
} }
} }

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:qr_flutter/qr_flutter.dart'; import 'package:qr_flutter/qr_flutter.dart';
import 'package:shlink_app/API/Classes/ShortURL/short_url.dart'; import 'package:shlink_app/API/Classes/ShortURL/short_url.dart';
import 'package:shlink_app/API/server_manager.dart'; import 'package:shlink_app/util/build_api_error_snackbar.dart';
import 'package:shlink_app/views/short_url_edit_view.dart'; import 'package:shlink_app/views/short_url_edit_view.dart';
import 'package:shlink_app/views/url_detail_view.dart'; import 'package:shlink_app/views/url_detail_view.dart';
import 'package:shlink_app/widgets/url_tags_list_widget.dart'; import 'package:shlink_app/widgets/url_tags_list_widget.dart';
@ -38,18 +38,9 @@ class _URLListViewState extends State<URLListView> {
}); });
return true; return true;
}, (r) { }, (r) {
var text = ""; ScaffoldMessenger.of(context).showSnackBar(
if (r is RequestFailure) { buildApiErrorSnackbar(r, context)
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; return false;
}); });
} }
@ -99,7 +90,7 @@ class _URLListViewState extends State<URLListView> {
'Create one by tapping the "+" button below', 'Create one by tapping the "+" button below',
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
color: Colors.grey[600]), color: Theme.of(context).colorScheme.onSecondary),
), ),
) )
], ],
@ -151,18 +142,11 @@ class _URLListViewState extends State<URLListView> {
eyeStyle: QrEyeStyle( eyeStyle: QrEyeStyle(
eyeShape: QrEyeShape.square, eyeShape: QrEyeShape.square,
color: color:
MediaQuery.of(context).platformBrightness == Theme.of(context).colorScheme.onPrimary,
Brightness.dark
? Colors.white
: Colors.black,
), ),
dataModuleStyle: QrDataModuleStyle( dataModuleStyle: QrDataModuleStyle(
dataModuleShape: QrDataModuleShape.square, dataModuleShape: QrDataModuleShape.square,
color: color: Theme.of(context).colorScheme.onPrimary,
MediaQuery.of(context).platformBrightness ==
Brightness.dark
? Colors.white
: Colors.black,
), ),
))), ))),
), ),
@ -194,11 +178,11 @@ class _ShortURLCellState extends State<ShortURLCell> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return GestureDetector(
onTap: () async { onTap: () async {
await Navigator.of(context).push(MaterialPageRoute( await Navigator.of(context)
builder: (context) => URLDetailView(shortURL: widget.shortURL))) .push(MaterialPageRoute(
.then((a) => { builder: (context) =>
widget.reload() URLDetailView(shortURL: widget.shortURL)))
}); .then((a) => {widget.reload()});
}, },
child: Padding( child: Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(
@ -209,10 +193,7 @@ class _ShortURLCellState extends State<ShortURLCell> {
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border( border: Border(
bottom: BorderSide( bottom: BorderSide(
color: MediaQuery.of(context).platformBrightness == color: Theme.of(context).dividerColor)),
Brightness.dark
? Colors.grey[800]!
: Colors.grey[300]!)),
), ),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
@ -231,7 +212,7 @@ class _ShortURLCellState extends State<ShortURLCell> {
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
textScaleFactor: 0.9, textScaleFactor: 0.9,
style: TextStyle(color: Colors.grey[600]), style: TextStyle(color: Theme.of(context).colorScheme.onTertiary),
), ),
// List tags in a row // List tags in a row
UrlTagsListWidget(tags: widget.shortURL.tags) UrlTagsListWidget(tags: widget.shortURL.tags)

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

@ -16,27 +16,26 @@ class _UrlTagsListWidgetState extends State<UrlTagsListWidget> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Wrap( return Wrap(
children: widget.tags.map((tag) { children: widget.tags.map((tag) {
var boxColor = stringToColor(tag) var boxColor = stringToColor(tag)
.harmonizeWith( .harmonizeWith(Theme.of(context).colorScheme.primary);
Theme.of(context).colorScheme.primary); return Padding(
return Padding( padding: const EdgeInsets.only(right: 4, top: 4),
padding: const EdgeInsets.only(right: 4, top: 4), child: Container(
child: Container( padding:
padding: const EdgeInsets.only( const EdgeInsets.only(top: 4, bottom: 4, left: 12, right: 12),
top: 4, bottom: 4, left: 12, right: 12), decoration: BoxDecoration(
decoration: BoxDecoration( borderRadius: BorderRadius.circular(4),
borderRadius: BorderRadius.circular(4), color: boxColor,
color: boxColor, ),
), child: Text(
child: Text( tag,
tag, style: TextStyle(
style: TextStyle( color: boxColor.computeLuminance() < 0.5
color: boxColor.computeLuminance() < 0.5 ? Colors.white
? Colors.white : Colors.black),
: Colors.black), ),
), ),
), );
); }).toList());
}).toList());
} }
} }

1
linux/.gitignore vendored
View File

@ -1 +0,0 @@
flutter/ephemeral

View File

@ -1,139 +0,0 @@
# Project-level configuration.
cmake_minimum_required(VERSION 3.10)
project(runner LANGUAGES CXX)
# The name of the executable created for the application. Change this to change
# the on-disk name of your application.
set(BINARY_NAME "shlink_manager")
# The unique GTK application identifier for this application. See:
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
set(APPLICATION_ID "dev.abmgrt.shlink_manager")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.
cmake_policy(SET CMP0063 NEW)
# Load bundled libraries from the lib/ directory relative to the binary.
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
# Root filesystem for cross-building.
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
endif()
# Define build configuration options.
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "Debug" CACHE
STRING "Flutter build mode" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
"Debug" "Profile" "Release")
endif()
# Compilation settings that should be applied to most targets.
#
# Be cautious about adding new options here, as plugins use this function by
# default. In most cases, you should add new options to specific targets instead
# of modifying this function.
function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_14)
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
endfunction()
# Flutter library and tool build rules.
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
add_subdirectory(${FLUTTER_MANAGED_DIR})
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
# Define the application target. To change its name, change BINARY_NAME above,
# not the value here, or `flutter run` will no longer work.
#
# Any new source files that you add to the application should be added here.
add_executable(${BINARY_NAME}
"main.cc"
"my_application.cc"
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
)
# Apply the standard set of build settings. This can be removed for applications
# that need different build settings.
apply_standard_settings(${BINARY_NAME})
# Add dependency libraries. Add any application-specific dependencies here.
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
# Run the Flutter tool portions of the build. This must not be removed.
add_dependencies(${BINARY_NAME} flutter_assemble)
# Only the install-generated bundle's copy of the executable will launch
# correctly, since the resources must in the right relative locations. To avoid
# people trying to run the unbundled copy, put it in a subdirectory instead of
# the default top-level location.
set_target_properties(${BINARY_NAME}
PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
)
# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
# === Installation ===
# By default, "installing" just makes a relocatable bundle in the build
# directory.
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
endif()
# Start with a clean build bundle directory every time.
install(CODE "
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
" COMPONENT Runtime)
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
install(FILES "${bundled_library}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endforeach(bundled_library)
# Fully re-copy the assets directory on each build to avoid having stale files
# from a previous install.
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
install(CODE "
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
" COMPONENT Runtime)
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
# Install the AOT library on non-Debug builds only.
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()

View File

@ -1,88 +0,0 @@
# This file controls Flutter-level build steps. It should not be edited.
cmake_minimum_required(VERSION 3.10)
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
# Configuration provided via flutter tool.
include(${EPHEMERAL_DIR}/generated_config.cmake)
# TODO: Move the rest of this into files in ephemeral. See
# https://github.com/flutter/flutter/issues/57146.
# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
# which isn't available in 3.10.
function(list_prepend LIST_NAME PREFIX)
set(NEW_LIST "")
foreach(element ${${LIST_NAME}})
list(APPEND NEW_LIST "${PREFIX}${element}")
endforeach(element)
set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
endfunction()
# === Flutter Library ===
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
# Published to parent scope for install step.
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
list(APPEND FLUTTER_LIBRARY_HEADERS
"fl_basic_message_channel.h"
"fl_binary_codec.h"
"fl_binary_messenger.h"
"fl_dart_project.h"
"fl_engine.h"
"fl_json_message_codec.h"
"fl_json_method_codec.h"
"fl_message_codec.h"
"fl_method_call.h"
"fl_method_channel.h"
"fl_method_codec.h"
"fl_method_response.h"
"fl_plugin_registrar.h"
"fl_plugin_registry.h"
"fl_standard_message_codec.h"
"fl_standard_method_codec.h"
"fl_string_codec.h"
"fl_value.h"
"fl_view.h"
"flutter_linux.h"
)
list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
add_library(flutter INTERFACE)
target_include_directories(flutter INTERFACE
"${EPHEMERAL_DIR}"
)
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
target_link_libraries(flutter INTERFACE
PkgConfig::GTK
PkgConfig::GLIB
PkgConfig::GIO
)
add_dependencies(flutter flutter_assemble)
# === Flutter tool backend ===
# _phony_ is a non-existent file to force this command to run every time,
# since currently there's no way to get a full input/output list from the
# flutter tool.
add_custom_command(
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
${CMAKE_CURRENT_BINARY_DIR}/_phony_
COMMAND ${CMAKE_COMMAND} -E env
${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
VERBATIM
)
add_custom_target(flutter_assemble DEPENDS
"${FLUTTER_LIBRARY}"
${FLUTTER_LIBRARY_HEADERS}
)

View File

@ -1,23 +0,0 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
#include <dynamic_color/dynamic_color_plugin.h>
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) dynamic_color_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin");
dynamic_color_plugin_register_with_registrar(dynamic_color_registrar);
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
}

View File

@ -1,15 +0,0 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter_linux/flutter_linux.h>
// Registers Flutter plugins.
void fl_register_plugins(FlPluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_

View File

@ -1,26 +0,0 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
dynamic_color
flutter_secure_storage_linux
url_launcher_linux
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)

View File

@ -1,6 +0,0 @@
#include "my_application.h"
int main(int argc, char** argv) {
g_autoptr(MyApplication) app = my_application_new();
return g_application_run(G_APPLICATION(app), argc, argv);
}

View File

@ -1,104 +0,0 @@
#include "my_application.h"
#include <flutter_linux/flutter_linux.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
#endif
#include "flutter/generated_plugin_registrant.h"
struct _MyApplication {
GtkApplication parent_instance;
char** dart_entrypoint_arguments;
};
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
// Implements GApplication::activate.
static void my_application_activate(GApplication* application) {
MyApplication* self = MY_APPLICATION(application);
GtkWindow* window =
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
// Use a header bar when running in GNOME as this is the common style used
// by applications and is the setup most users will be using (e.g. Ubuntu
// desktop).
// If running on X and not using GNOME then just use a traditional title bar
// in case the window manager does more exotic layout, e.g. tiling.
// If running on Wayland assume the header bar will work (may need changing
// if future cases occur).
gboolean use_header_bar = TRUE;
#ifdef GDK_WINDOWING_X11
GdkScreen* screen = gtk_window_get_screen(window);
if (GDK_IS_X11_SCREEN(screen)) {
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
use_header_bar = FALSE;
}
}
#endif
if (use_header_bar) {
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
gtk_widget_show(GTK_WIDGET(header_bar));
gtk_header_bar_set_title(header_bar, "shlink_manager");
gtk_header_bar_set_show_close_button(header_bar, TRUE);
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
} else {
gtk_window_set_title(window, "shlink_manager");
}
gtk_window_set_default_size(window, 1280, 720);
gtk_widget_show(GTK_WIDGET(window));
g_autoptr(FlDartProject) project = fl_dart_project_new();
fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
FlView* view = fl_view_new(project);
gtk_widget_show(GTK_WIDGET(view));
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
gtk_widget_grab_focus(GTK_WIDGET(view));
}
// Implements GApplication::local_command_line.
static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) {
MyApplication* self = MY_APPLICATION(application);
// Strip out the first argument as it is the binary name.
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
g_autoptr(GError) error = nullptr;
if (!g_application_register(application, nullptr, &error)) {
g_warning("Failed to register: %s", error->message);
*exit_status = 1;
return TRUE;
}
g_application_activate(application);
*exit_status = 0;
return TRUE;
}
// Implements GObject::dispose.
static void my_application_dispose(GObject* object) {
MyApplication* self = MY_APPLICATION(object);
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
}
static void my_application_class_init(MyApplicationClass* klass) {
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line;
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
}
static void my_application_init(MyApplication* self) {}
MyApplication* my_application_new() {
return MY_APPLICATION(g_object_new(my_application_get_type(),
"application-id", APPLICATION_ID,
"flags", G_APPLICATION_NON_UNIQUE,
nullptr));
}

View File

@ -1,18 +0,0 @@
#ifndef FLUTTER_MY_APPLICATION_H_
#define FLUTTER_MY_APPLICATION_H_
#include <gtk/gtk.h>
G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION,
GtkApplication)
/**
* my_application_new:
*
* Creates a new Flutter-based application.
*
* Returns: a new #MyApplication.
*/
MyApplication* my_application_new();
#endif // FLUTTER_MY_APPLICATION_H_

View File

@ -5,18 +5,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: archive name: archive
sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.4.10" version: "3.6.1"
args: args:
dependency: transitive dependency: transitive
description: description:
name: args name: args
sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.2" version: "2.5.0"
async: async:
dependency: transitive dependency: transitive
description: description:
@ -69,18 +69,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: collection name: collection
sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.17.2" version: "1.18.0"
convert:
dependency: transitive
description:
name: convert
sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592"
url: "https://pub.dev"
source: hosted
version: "3.1.1"
crypto: crypto:
dependency: transitive dependency: transitive
description: description:
@ -93,10 +85,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: cupertino_icons name: cupertino_icons
sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.6" version: "1.0.8"
dartz: dartz:
dependency: "direct main" dependency: "direct main"
description: description:
@ -109,10 +101,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: dynamic_color name: dynamic_color
sha256: a866f1f8947bfdaf674d7928e769eac7230388a2e7a2542824fad4bb5b87be3b sha256: eae98052fa6e2826bdac3dd2e921c6ce2903be15c6b7f8b6d8a5d49b5086298d
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.6.9" version: "1.7.0"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:
@ -125,10 +117,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: ffi name: ffi
sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.0" version: "2.1.2"
file: file:
dependency: transitive dependency: transitive
description: description:
@ -145,20 +137,19 @@ packages:
flutter_launcher_icons: flutter_launcher_icons:
dependency: "direct dev" dependency: "direct dev"
description: description:
path: "." name: flutter_launcher_icons
ref: "feat/monochrome-icons-support" sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea"
resolved-ref: "1902eba83da89b0350a70672ac7c963cd995e017" url: "https://pub.dev"
url: "https://github.com/OutdatedGuy/flutter_launcher_icons.git" source: hosted
source: git
version: "0.13.1" version: "0.13.1"
flutter_lints: flutter_lints:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: flutter_lints name: flutter_lints
sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7 sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.1" version: "4.0.0"
flutter_process_text: flutter_process_text:
dependency: "direct main" dependency: "direct main"
description: description:
@ -171,50 +162,58 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_secure_storage name: flutter_secure_storage
sha256: "22dbf16f23a4bcf9d35e51be1c84ad5bb6f627750565edd70dab70f3ff5fff8f" sha256: "165164745e6afb5c0e3e3fcc72a012fb9e58496fb26ffb92cf22e16a821e85d0"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.1.0" version: "9.2.2"
flutter_secure_storage_linux: flutter_secure_storage_linux:
dependency: transitive dependency: transitive
description: description:
name: flutter_secure_storage_linux name: flutter_secure_storage_linux
sha256: "3d5032e314774ee0e1a7d0a9f5e2793486f0dff2dd9ef5a23f4e3fb2a0ae6a9e" sha256: "4d91bfc23047422cbcd73ac684bc169859ee766482517c22172c86596bf1464b"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.0" version: "1.2.1"
flutter_secure_storage_macos: flutter_secure_storage_macos:
dependency: transitive dependency: transitive
description: description:
name: flutter_secure_storage_macos name: flutter_secure_storage_macos
sha256: bd33935b4b628abd0b86c8ca20655c5b36275c3a3f5194769a7b3f37c905369c sha256: "1693ab11121a5f925bbea0be725abfcfbbcf36c1e29e571f84a0c0f436147a81"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.1" version: "3.1.2"
flutter_secure_storage_platform_interface: flutter_secure_storage_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: flutter_secure_storage_platform_interface name: flutter_secure_storage_platform_interface
sha256: "0d4d3a5dd4db28c96ae414d7ba3b8422fd735a8255642774803b2532c9a61d7e" sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.2" version: "1.1.2"
flutter_secure_storage_web: flutter_secure_storage_web:
dependency: transitive dependency: transitive
description: description:
name: flutter_secure_storage_web name: flutter_secure_storage_web
sha256: "30f84f102df9dcdaa2241866a958c2ec976902ebdaa8883fbfe525f1f2f3cf20" sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.2" version: "1.2.1"
flutter_secure_storage_windows: flutter_secure_storage_windows:
dependency: transitive dependency: transitive
description: description:
name: flutter_secure_storage_windows name: flutter_secure_storage_windows
sha256: "38f9501c7cb6f38961ef0e1eacacee2b2d4715c63cc83fe56449c4d3d0b47255" sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.1" version: "3.1.2"
flutter_sharing_intent:
dependency: "direct main"
description:
name: flutter_sharing_intent
sha256: "785ffc391822641457f930eb477c91c2f598a888f50b8fbb40d481ee01c7e719"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@ -229,10 +228,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: http name: http
sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.13.6" version: "1.2.2"
http_parser: http_parser:
dependency: transitive dependency: transitive
description: description:
@ -245,18 +244,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: image name: image
sha256: "004a2e90ce080f8627b5a04aecb4cdfac87d2c3f3b520aa291260be5a32c033d" sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.4" version: "4.2.0"
intl: intl:
dependency: "direct main" dependency: "direct main"
description: description:
name: intl name: intl
sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.18.1" version: "0.19.0"
js: js:
dependency: transitive dependency: transitive
description: description:
@ -269,74 +268,122 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: json_annotation name: json_annotation
sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted 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: license_generator:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: license_generator name: license_generator
sha256: "147605cac9b1ca9ab7f52ed235498927fe7a9edaef56d1b4687f22b9b03f6bad" sha256: "0b111c03cbccfa36a68a8738e3b2a54392a269673b5258d5fc6a83302d675a9e"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.5" version: "2.0.0"
lints: lints:
dependency: transitive dependency: transitive
description: description:
name: lints name: lints
sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.0" version: "4.0.0"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
name: matcher name: matcher
sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.12.16" version: "0.12.16+1"
material_color_utilities: material_color_utilities:
dependency: transitive dependency: transitive
description: description:
name: material_color_utilities name: material_color_utilities
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.5.0" version: "0.8.0"
meta: meta:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.9.1" version: "1.12.0"
package_info_plus: package_info_plus:
dependency: "direct main" dependency: "direct main"
description: description:
name: package_info_plus name: package_info_plus
sha256: "7e76fad405b3e4016cd39d08f455a4eb5199723cf594cd1b8916d47140d93017" sha256: b93d8b4d624b4ea19b0a5a208b2d6eff06004bc3ce74c06040b120eeadd00ce0
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.2.0" version: "8.0.0"
package_info_plus_platform_interface: package_info_plus_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: package_info_plus_platform_interface name: package_info_plus_platform_interface
sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6" sha256: f49918f3433a3146047372f9d4f1f847511f2acd5cd030e1f44fe5a50036b70e
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.1" version: "3.0.0"
path: path:
dependency: transitive dependency: transitive
description: description:
name: path name: path
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.8.3" version: "1.9.0"
path_provider:
dependency: transitive
description:
name: path_provider
sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161
url: "https://pub.dev"
source: hosted
version: "2.1.3"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: "30c5aa827a6ae95ce2853cdc5fe3971daaac00f6f081c419c013f7f57bff2f5e"
url: "https://pub.dev"
source: hosted
version: "2.2.7"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16
url: "https://pub.dev"
source: hosted
version: "2.4.0"
path_provider_linux: path_provider_linux:
dependency: transitive dependency: transitive
description: description:
@ -357,26 +404,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: path_provider_windows name: path_provider_windows
sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.1" version: "2.3.0"
petitparser: petitparser:
dependency: transitive dependency: transitive
description: description:
name: petitparser name: petitparser
sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.4.0" version: "6.0.2"
platform: platform:
dependency: transitive dependency: transitive
description: description:
name: platform name: platform
sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.4" version: "3.1.5"
plugin_platform_interface: plugin_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -385,22 +432,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.8" version: "2.1.8"
pointycastle:
dependency: transitive
description:
name: pointycastle
sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29"
url: "https://pub.dev"
source: hosted
version: "3.7.4"
qr: qr:
dependency: transitive dependency: transitive
description: description:
name: qr name: qr
sha256: "64957a3930367bf97cc211a5af99551d630f2f4625e38af10edd6b19131b64b3" sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.1" version: "3.0.2"
qr_flutter: qr_flutter:
dependency: "direct main" dependency: "direct main"
description: description:
@ -413,26 +452,26 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: shared_preferences name: shared_preferences
sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.2" version: "2.2.3"
shared_preferences_android: shared_preferences_android:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_android name: shared_preferences_android
sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" sha256: "93d0ec9dd902d85f326068e6a899487d1f65ffcd5798721a95330b26c8131577"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.1" version: "2.2.3"
shared_preferences_foundation: shared_preferences_foundation:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_foundation name: shared_preferences_foundation
sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c" sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.5" version: "2.4.0"
shared_preferences_linux: shared_preferences_linux:
dependency: transitive dependency: transitive
description: description:
@ -445,18 +484,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_platform_interface name: shared_preferences_platform_interface
sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" sha256: "034650b71e73629ca08a0bd789fd1d83cc63c2d1e405946f7cef7bc37432f93a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.2" version: "2.4.0"
shared_preferences_web: shared_preferences_web:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_web name: shared_preferences_web
sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.1" version: "2.3.0"
shared_preferences_windows: shared_preferences_windows:
dependency: transitive dependency: transitive
description: description:
@ -482,18 +521,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: stack_trace name: stack_trace
sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.11.0" version: "1.11.1"
stream_channel: stream_channel:
dependency: transitive dependency: transitive
description: description:
name: stream_channel name: stream_channel
sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.1" version: "2.1.2"
string_scanner: string_scanner:
dependency: transitive dependency: transitive
description: description:
@ -514,10 +553,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.6.0" version: "0.7.0"
tuple: tuple:
dependency: "direct main" dependency: "direct main"
description: description:
@ -538,26 +577,26 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: url_launcher name: url_launcher
sha256: e8f2efc804810c0f2f5b485f49e7942179f56eabcfe81dce3387fec4bb55876b sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.1.9" version: "6.3.0"
url_launcher_android: url_launcher_android:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_android name: url_launcher_android
sha256: "507dc655b1d9cb5ebc756032eb785f114e415f91557b73bf60b7e201dfedeb2f" sha256: "95d8027db36a0e52caf55680f91e33ea6aa12a3ce608c90b06f4e429a21067ac"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.2.2" version: "6.3.5"
url_launcher_ios: url_launcher_ios:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_ios name: url_launcher_ios
sha256: "75bb6fe3f60070407704282a2d295630cab232991eb52542b18347a8a941df03" sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.2.4" version: "6.3.1"
url_launcher_linux: url_launcher_linux:
dependency: transitive dependency: transitive
description: description:
@ -570,34 +609,34 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_macos name: url_launcher_macos
sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.0" version: "3.2.0"
url_launcher_platform_interface: url_launcher_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_platform_interface name: url_launcher_platform_interface
sha256: a932c3a8082e118f80a475ce692fde89dc20fddb24c57360b96bc56f7035de1f sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.1" version: "2.3.2"
url_launcher_web: url_launcher_web:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_web name: url_launcher_web
sha256: "7fd2f55fe86cea2897b963e864dc01a7eb0719ecc65fcef4c1cc3d686d718bb2" sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.0" version: "2.3.1"
url_launcher_windows: url_launcher_windows:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_windows name: url_launcher_windows
sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.1" version: "3.1.2"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:
@ -606,22 +645,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.4" 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: web:
dependency: transitive dependency: transitive
description: description:
name: web name: web
sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.1.4-beta" version: "0.5.1"
win32: win32:
dependency: transitive dependency: transitive
description: description:
name: win32 name: win32
sha256: b0f37db61ba2f2e9b7a78a1caece0052564d1bc70668156cf3a29d676fe4e574 sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.1.1" version: "5.5.1"
xdg_directories: xdg_directories:
dependency: transitive dependency: transitive
description: description:
@ -634,10 +681,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: xml name: xml
sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84" sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.3.0" version: "6.5.0"
yaml: yaml:
dependency: transitive dependency: transitive
description: description:
@ -647,5 +694,5 @@ packages:
source: hosted source: hosted
version: "3.1.2" version: "3.1.2"
sdks: sdks:
dart: ">=3.1.0 <4.0.0" dart: ">=3.4.0 <4.0.0"
flutter: ">=3.13.0" flutter: ">=3.22.0"

View File

@ -16,10 +16,10 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 1.1.1+7 version: 1.4.0+11
environment: environment:
sdk: ^2.19.0 sdk: ^3.0.0
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
@ -31,25 +31,26 @@ dependencies:
# #
cupertino_icons: ^1.0.5 cupertino_icons: ^1.0.5
http: ^0.13.6 http: ^1.1.0
flutter_process_text: ^1.1.2 flutter_process_text: ^1.1.2
flutter_secure_storage: ^8.0.0 flutter_secure_storage: ^9.0.0
dartz: ^0.10.1 dartz: ^0.10.1
qr_flutter: ^4.1.0 qr_flutter: ^4.1.0
tuple: ^2.0.2 tuple: ^2.0.2
intl: ^0.18.1 intl: ^0.19.0
dynamic_color: ^1.6.6 dynamic_color: ^1.6.6
url_launcher: 6.1.9 url_launcher: ^6.2.4
package_info_plus: ^4.0.2 package_info_plus: ^8.0.0
shared_preferences: ^2.2.2 shared_preferences: ^2.2.2
flutter_sharing_intent: ^1.1.1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
license_generator: ^1.0.5 license_generator: ^2.0.0
flutter_launcher_icons: ^0.13.1 flutter_launcher_icons: ^0.13.1
flutter_lints: ^3.0.1 flutter_lints: ^4.0.0
flutter: flutter:
uses-material-design: true uses-material-design: true