mirror of
https://github.com/rainloreley/shlink-manager.git
synced 2024-10-04 09:12:58 +02:00
Compare commits
13 Commits
6d654210fa
...
e1c9bc4d80
Author | SHA1 | Date | |
---|---|---|---|
Adrian Baumgart | e1c9bc4d80 | ||
Adrian Baumgart | 413275df38 | ||
Adrian Baumgart | 975b2ea3d8 | ||
f9c6a58db1 | |||
Adrian Baumgart | 69c8870997 | ||
Adrian Baumgart | ce28066d29 | ||
Adrian Baumgart | e9f98c2171 | ||
39483dca54 | |||
Adrian Baumgart | ba058e2af3 | ||
Adrian Baumgart | 0eea6ee9a2 | ||
Adrian Baumgart | 0ade61faea | ||
00bb9af82e | |||
Adrian Baumgart | 6a5cebdb68 |
|
@ -1,42 +0,0 @@
|
|||
name: Fix formatting and license file
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
- '**.dart'
|
||||
- 'pubspec.yaml'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'oracle'
|
||||
java-version: '21'
|
||||
|
||||
- name: Setup Android SDK
|
||||
uses: android-actions/setup-android@v2
|
||||
|
||||
- name: Setup Flutter SDK
|
||||
uses: flutter-actions/setup-flutter@v2
|
||||
with:
|
||||
channel: stable
|
||||
version: 3.13.7
|
||||
|
||||
- name: Install dependencies
|
||||
run: dart pub get
|
||||
|
||||
- name: Verify formatting
|
||||
run: dart format --output=none .
|
||||
|
||||
- name: Update license file
|
||||
run: flutter packages pub run license_generator generate
|
||||
|
||||
- name: Commit and push
|
||||
uses: stefanzweifel/git-auto-commit-action@v5
|
10
README.md
10
README.md
|
@ -17,20 +17,18 @@ Shlink Manager is an app for Android to see and manage all shortened URLs create
|
|||
## 📱 Current Features
|
||||
|
||||
✅ List all short URLs<br/>
|
||||
✅ Create new short URLs<br/>
|
||||
✅ Delete short URLs<br/>
|
||||
✅ Create, edit and delete short URLs<br/>
|
||||
✅ See overall statistics<br/>
|
||||
✅ Detailed statistics for each short URL<br/>
|
||||
✅ Display tags<br/>
|
||||
✅ Display QR code<br/>
|
||||
✅ Display tags & QR code<br/>
|
||||
✅ Dark mode support<br/>
|
||||
✅ Edit existing short URLs<br/>
|
||||
✅ Quickly create Short URL via Share Sheet<br/>
|
||||
✅ View rule-based redirects (no editing yet)<br/>
|
||||
✅ Use multiple Shlink instances<br/>
|
||||
|
||||
## 🔨 To Do
|
||||
- [ ] Add support for iOS (maybe in the future)
|
||||
- [ ] add tags
|
||||
- [ ] Add dynamic rule-based redirects system (Shlink v4.0.0)
|
||||
- [ ] improve app icon
|
||||
- [ ] Refactor code
|
||||
- [ ] ...and more
|
||||
|
|
|
@ -52,10 +52,12 @@ android {
|
|||
applicationId "dev.abmgrt.shlink_app"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
|
||||
minSdkVersion 19 //flutter.minSdkVersion
|
||||
minSdkVersion flutter.minSdkVersion //flutter.minSdkVersion
|
||||
targetSdkVersion flutter.targetSdkVersion
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
|
||||
multiDexEnabled true
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
|
@ -79,4 +81,5 @@ flutter {
|
|||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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};
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
import 'package:shlink_app/API/Classes/ShortURL/short_url_meta.dart';
|
||||
import 'package:shlink_app/API/Classes/ShortURL/visits_summary.dart';
|
||||
|
||||
import 'RedirectRule/redirect_rule.dart';
|
||||
|
||||
/// Data about a short URL
|
||||
class ShortURL {
|
||||
/// Slug of the short URL used in the URL
|
||||
|
@ -33,6 +35,8 @@ class ShortURL {
|
|||
/// Whether the short URL is crawlable by a web crawler
|
||||
bool crawlable;
|
||||
|
||||
List<RedirectRule>? redirectRules;
|
||||
|
||||
ShortURL(
|
||||
this.shortCode,
|
||||
this.shortUrl,
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
}
|
|
@ -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()));
|
||||
}
|
||||
}
|
|
@ -1,15 +1,19 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:shlink_app/API/Classes/ShlinkStats/shlink_stats.dart';
|
||||
import 'package:shlink_app/API/Classes/ShortURL/RedirectRule/redirect_rule.dart';
|
||||
import 'package:shlink_app/API/Classes/ShortURL/short_url.dart';
|
||||
import 'package:shlink_app/API/Classes/ShortURLSubmission/short_url_submission.dart';
|
||||
import 'package:shlink_app/API/Methods/connect.dart';
|
||||
import 'package:shlink_app/API/Methods/get_recent_short_urls.dart';
|
||||
import 'package:shlink_app/API/Methods/get_redirect_rules.dart';
|
||||
import 'package:shlink_app/API/Methods/get_server_health.dart';
|
||||
import 'package:shlink_app/API/Methods/get_shlink_stats.dart';
|
||||
import 'package:shlink_app/API/Methods/get_short_urls.dart';
|
||||
import 'package:shlink_app/API/Methods/set_redirect_rules.dart';
|
||||
import 'package:shlink_app/API/Methods/update_short_url.dart';
|
||||
|
||||
import 'Methods/delete_short_url.dart';
|
||||
|
@ -41,10 +45,41 @@ class ServerManager {
|
|||
}
|
||||
|
||||
/// Logs out the user and removes data about the Shlink server
|
||||
Future<void> logOut() async {
|
||||
Future<void> logOut(String url) async {
|
||||
const storage = FlutterSecureStorage();
|
||||
await storage.delete(key: "shlink_url");
|
||||
await storage.delete(key: "shlink_apikey");
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
String? serverMapSerialized = await storage.read(key: "server_map");
|
||||
|
||||
if (serverMapSerialized != null) {
|
||||
Map<String, String> serverMap =
|
||||
Map.castFrom(jsonDecode(serverMapSerialized));
|
||||
serverMap.remove(url);
|
||||
if (serverMap.isEmpty) {
|
||||
storage.delete(key: "server_map");
|
||||
} else {
|
||||
storage.write(key: "server_map", value: jsonEncode(serverMap));
|
||||
}
|
||||
if (serverUrl == url) {
|
||||
serverUrl = null;
|
||||
apiKey = null;
|
||||
prefs.remove("lastusedserver");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns all servers saved in the app
|
||||
Future<List<String>> getAvailableServers() async {
|
||||
const storage = FlutterSecureStorage();
|
||||
String? serverMapSerialized = await storage.read(key: "server_map");
|
||||
|
||||
if (serverMapSerialized != null) {
|
||||
Map<String, String> serverMap =
|
||||
Map.castFrom(jsonDecode(serverMapSerialized));
|
||||
return serverMap.keys.toList();
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads the server credentials from [FlutterSecureStorage]
|
||||
|
@ -57,16 +92,68 @@ class ServerManager {
|
|||
|
||||
prefs.setBool('first_run', false);
|
||||
} else {
|
||||
serverUrl = await storage.read(key: "shlink_url");
|
||||
apiKey = await storage.read(key: "shlink_apikey");
|
||||
if (await _replaceDeprecatedStorageMethod()) {
|
||||
_loadCredentials();
|
||||
return;
|
||||
}
|
||||
|
||||
String? serverMapSerialized = await storage.read(key: "server_map");
|
||||
String? lastUsedServer = prefs.getString("lastusedserver");
|
||||
|
||||
if (serverMapSerialized != null) {
|
||||
Map<String, String> serverMap =
|
||||
Map.castFrom(jsonDecode(serverMapSerialized));
|
||||
if (lastUsedServer != null) {
|
||||
serverUrl = lastUsedServer;
|
||||
apiKey = serverMap[lastUsedServer]!;
|
||||
} else {
|
||||
List<String> availableServers = serverMap.keys.toList();
|
||||
if (availableServers.isNotEmpty) {
|
||||
serverUrl = availableServers.first;
|
||||
apiKey = serverMap[serverUrl];
|
||||
prefs.setString("lastusedserver", serverUrl!);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _replaceDeprecatedStorageMethod() async {
|
||||
const storage = FlutterSecureStorage();
|
||||
// deprecated data storage method, replaced because of multi-server support
|
||||
var v1DataServerurl = await storage.read(key: "shlink_url");
|
||||
var v1DataApikey = await storage.read(key: "shlink_apikey");
|
||||
|
||||
if (v1DataServerurl != null && v1DataApikey != null) {
|
||||
// conversion to new data storage method
|
||||
Map<String, String> serverMap = {};
|
||||
serverMap[v1DataServerurl] = v1DataApikey;
|
||||
|
||||
storage.write(key: "server_map", value: jsonEncode(serverMap));
|
||||
|
||||
storage.delete(key: "shlink_url");
|
||||
storage.delete(key: "shlink_apikey");
|
||||
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Saves the provided server credentials to [FlutterSecureStorage]
|
||||
void _saveCredentials(String url, String apiKey) async {
|
||||
const storage = FlutterSecureStorage();
|
||||
storage.write(key: "shlink_url", value: url);
|
||||
storage.write(key: "shlink_apikey", value: apiKey);
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
String? serverMapSerialized = await storage.read(key: "server_map");
|
||||
Map<String, String> serverMap;
|
||||
if (serverMapSerialized != null) {
|
||||
serverMap = Map.castFrom(jsonDecode(serverMapSerialized));
|
||||
} else {
|
||||
serverMap = {};
|
||||
}
|
||||
serverMap[url] = apiKey;
|
||||
storage.write(key: "server_map", value: jsonEncode(serverMap));
|
||||
prefs.setString("lastusedserver", url);
|
||||
}
|
||||
|
||||
/// Saves provided server credentials and tries to establish a connection
|
||||
|
@ -78,7 +165,7 @@ class ServerManager {
|
|||
_saveCredentials(url, apiKey);
|
||||
final result = await connect();
|
||||
result.fold((l) => null, (r) {
|
||||
logOut();
|
||||
logOut(url);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
@ -124,6 +211,19 @@ class ServerManager {
|
|||
FutureOr<Either<List<ShortURL>, Failure>> getRecentShortUrls() async {
|
||||
return apiGetRecentShortUrls(apiKey, serverUrl, apiVersion);
|
||||
}
|
||||
|
||||
/// Gets redirect rules for a given short URL (code)
|
||||
FutureOr<Either<List<RedirectRule>, Failure>> getRedirectRules(
|
||||
String shortCode) async {
|
||||
return apiGetRedirectRules(shortCode, apiKey, serverUrl, apiVersion);
|
||||
}
|
||||
|
||||
/// Sets redirect rules for a given short URL (code)
|
||||
FutureOr<Either<bool, Failure>> setRedirectRules(
|
||||
String shortCode, List<RedirectRule> redirectRules) async {
|
||||
return apiSetRedirectRules(
|
||||
shortCode, redirectRules, apiKey, serverUrl, apiVersion);
|
||||
}
|
||||
}
|
||||
|
||||
/// Server response data type about a page of short URLs from the server
|
||||
|
|
|
@ -11,13 +11,13 @@ void main() {
|
|||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
static final ColorScheme _defaultLightColorScheme = ColorScheme
|
||||
.fromSeed(seedColor: Colors.blue);
|
||||
static final ColorScheme _defaultLightColorScheme =
|
||||
ColorScheme.fromSeed(seedColor: Colors.blue);
|
||||
|
||||
static final _defaultDarkColorScheme = ColorScheme.fromSeed(
|
||||
brightness: Brightness.dark,
|
||||
seedColor: Colors.blue,
|
||||
background: Colors.black);
|
||||
brightness: Brightness.dark,
|
||||
seedColor: Colors.blue,
|
||||
background: Colors.black);
|
||||
|
||||
// This widget is the root of your application.
|
||||
@override
|
||||
|
@ -38,7 +38,7 @@ class MyApp extends StatelessWidget {
|
|||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
),
|
||||
colorScheme: darkColorScheme?.copyWith(background: Colors.black) ??
|
||||
colorScheme: darkColorScheme?.copyWith(surface: Colors.black) ??
|
||||
_defaultDarkColorScheme,
|
||||
useMaterial3: true,
|
||||
),
|
||||
|
@ -64,11 +64,13 @@ class _InitialPageState extends State<InitialPage> {
|
|||
void checkLogin() async {
|
||||
bool result = await globals.serverManager.checkLogin();
|
||||
if (result) {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (context) => const NavigationBarView()));
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(builder: (context) => const NavigationBarView()),
|
||||
(Route<dynamic> route) => false);
|
||||
} else {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (context) => const LoginView()));
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(builder: (context) => const LoginView()),
|
||||
(Route<dynamic> route) => false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -28,7 +28,8 @@ class LicenseUtil {
|
|||
return [
|
||||
const License(
|
||||
name: r'cupertino_icons',
|
||||
license: r'''The MIT License (MIT)
|
||||
license: r'''
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Vladimir Kharlampidi
|
||||
|
||||
|
@ -49,12 +50,12 @@ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''',
|
||||
version: r'^1.0.5',
|
||||
homepage: null,
|
||||
repository: r'https://github.com/flutter/packages/tree/main/third_party/packages/cupertino_icons',
|
||||
),
|
||||
const License(
|
||||
name: r'dartz',
|
||||
license: r'''The MIT License (MIT)
|
||||
license: r'''
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016, 2017, 2018, 2019, 2020, 2021 Björn Sperber
|
||||
|
||||
|
@ -78,11 +79,11 @@ SOFTWARE.
|
|||
''',
|
||||
version: r'^0.10.1',
|
||||
homepage: r'https://github.com/spebbe/dartz',
|
||||
repository: null,
|
||||
),
|
||||
const License(
|
||||
name: r'dynamic_color',
|
||||
license: r''' Apache License
|
||||
license: r'''
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
|
@ -285,12 +286,12 @@ SOFTWARE.
|
|||
limitations under the License.
|
||||
''',
|
||||
version: r'^1.6.6',
|
||||
homepage: null,
|
||||
repository: r'https://github.com/material-foundation/flutter-packages/tree/main/packages/dynamic_color',
|
||||
),
|
||||
const License(
|
||||
name: r'flutter',
|
||||
license: r'''Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
license: r'''
|
||||
Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
@ -316,13 +317,13 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
''',
|
||||
version: null,
|
||||
homepage: r'https://flutter.dev/',
|
||||
repository: r'https://github.com/flutter/flutter',
|
||||
),
|
||||
const License(
|
||||
name: r'flutter_launcher_icons',
|
||||
license: r'''MIT License
|
||||
license: r'''
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Mark O'Sullivan
|
||||
|
||||
|
@ -350,7 +351,8 @@ SOFTWARE.
|
|||
),
|
||||
const License(
|
||||
name: r'flutter_lints',
|
||||
license: r'''Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
license: r'''
|
||||
Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
@ -377,12 +379,12 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
''',
|
||||
version: r'^3.0.1',
|
||||
homepage: null,
|
||||
repository: r'https://github.com/flutter/packages/tree/main/packages/flutter_lints',
|
||||
),
|
||||
const License(
|
||||
name: r'flutter_process_text',
|
||||
license: r'''BSD 3-Clause License
|
||||
license: r'''
|
||||
BSD 3-Clause License
|
||||
|
||||
(c) Copyright 2021 divshekhar (Divyanshu Shekhar)
|
||||
|
||||
|
@ -410,12 +412,12 @@ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABI
|
|||
TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
|
||||
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
|
||||
version: r'^1.1.2',
|
||||
homepage: null,
|
||||
repository: r'https://github.com/DevsOnFlutter/flutter_process_text',
|
||||
),
|
||||
const License(
|
||||
name: r'flutter_secure_storage',
|
||||
license: r'''BSD 3-Clause License
|
||||
license: r'''
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright 2017 German Saprykin
|
||||
All rights reserved.
|
||||
|
@ -445,12 +447,12 @@ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
|
||||
version: r'^9.0.0',
|
||||
homepage: null,
|
||||
repository: r'https://github.com/mogol/flutter_secure_storage/tree/develop/flutter_secure_storage',
|
||||
),
|
||||
const License(
|
||||
name: r'flutter_sharing_intent',
|
||||
license: r''' Apache License
|
||||
license: r'''
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
|
@ -653,11 +655,11 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
|
|||
limitations under the License.''',
|
||||
version: r'^1.1.1',
|
||||
homepage: r'https://github.com/bhagat-techind/flutter_sharing_intent.git',
|
||||
repository: null,
|
||||
),
|
||||
const License(
|
||||
name: r'flutter_test',
|
||||
license: r'''Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
license: r'''
|
||||
Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
@ -683,13 +685,13 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
''',
|
||||
version: null,
|
||||
homepage: r'https://flutter.dev/',
|
||||
repository: r'https://github.com/flutter/flutter',
|
||||
),
|
||||
const License(
|
||||
name: r'http',
|
||||
license: r'''Copyright 2014, the Dart project authors.
|
||||
license: r'''
|
||||
Copyright 2014, the Dart project authors.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
|
@ -718,12 +720,12 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
''',
|
||||
version: r'^1.1.0',
|
||||
homepage: null,
|
||||
repository: r'https://github.com/dart-lang/http/tree/master/pkgs/http',
|
||||
),
|
||||
const License(
|
||||
name: r'intl',
|
||||
license: r'''Copyright 2013, the Dart project authors.
|
||||
license: r'''
|
||||
Copyright 2013, the Dart project authors.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
|
@ -752,12 +754,12 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
''',
|
||||
version: r'^0.19.0',
|
||||
homepage: null,
|
||||
repository: r'https://github.com/dart-lang/i18n/tree/main/pkgs/intl',
|
||||
),
|
||||
const License(
|
||||
name: r'license_generator',
|
||||
license: r'''MIT License
|
||||
license: r'''
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 icapps
|
||||
|
||||
|
@ -781,11 +783,11 @@ SOFTWARE.
|
|||
''',
|
||||
version: r'^2.0.0',
|
||||
homepage: r'https://github.com/icapps/flutter-icapps-license',
|
||||
repository: null,
|
||||
),
|
||||
const License(
|
||||
name: r'package_info_plus',
|
||||
license: r'''Copyright 2017 The Chromium Authors. All rights reserved.
|
||||
license: r'''
|
||||
Copyright 2017 The Chromium Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
|
@ -819,7 +821,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
),
|
||||
const License(
|
||||
name: r'qr_flutter',
|
||||
license: r'''BSD 3-Clause License
|
||||
license: r'''
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2020, Luke Freeman.
|
||||
All rights reserved.
|
||||
|
@ -851,11 +854,11 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
''',
|
||||
version: r'^4.1.0',
|
||||
homepage: r'https://github.com/theyakka/qr.flutter',
|
||||
repository: null,
|
||||
),
|
||||
const License(
|
||||
name: r'shared_preferences',
|
||||
license: r'''Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
license: r'''
|
||||
Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
@ -882,12 +885,12 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
''',
|
||||
version: r'^2.2.2',
|
||||
homepage: null,
|
||||
repository: r'https://github.com/flutter/packages/tree/main/packages/shared_preferences/shared_preferences',
|
||||
),
|
||||
const License(
|
||||
name: r'tuple',
|
||||
license: r'''Copyright (c) 2014, the tuple project authors.
|
||||
license: r'''
|
||||
Copyright (c) 2014, the tuple project authors.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
|
@ -910,12 +913,12 @@ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
|
||||
version: r'^2.0.2',
|
||||
homepage: null,
|
||||
repository: r'https://github.com/google/tuple.dart',
|
||||
),
|
||||
const License(
|
||||
name: r'url_launcher',
|
||||
license: r'''Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
license: r'''
|
||||
Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
@ -942,7 +945,6 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
''',
|
||||
version: r'^6.2.4',
|
||||
homepage: null,
|
||||
repository: r'https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher',
|
||||
),
|
||||
];
|
||||
|
|
|
@ -8,6 +8,7 @@ import 'package:shlink_app/API/Classes/ShlinkStats/shlink_stats.dart';
|
|||
import 'package:shlink_app/API/server_manager.dart';
|
||||
import 'package:shlink_app/views/short_url_edit_view.dart';
|
||||
import 'package:shlink_app/views/url_list_view.dart';
|
||||
import 'package:shlink_app/widgets/available_servers_bottom_sheet.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
import '../API/Classes/ShortURL/short_url.dart';
|
||||
import '../globals.dart' as globals;
|
||||
|
@ -121,15 +122,25 @@ class _HomeViewState extends State<HomeView> {
|
|||
child: CustomScrollView(
|
||||
slivers: [
|
||||
SliverAppBar.medium(
|
||||
automaticallyImplyLeading: false,
|
||||
expandedHeight: 160,
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text("Shlink",
|
||||
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
Text(globals.serverManager.getServerUrl(),
|
||||
style: TextStyle(
|
||||
fontSize: 16, color: Colors.grey[600]))
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return const AvailableServerBottomSheet();
|
||||
});
|
||||
},
|
||||
child: Text(globals.serverManager.getServerUrl(),
|
||||
style: TextStyle(
|
||||
fontSize: 16, color: Colors.grey[600])),
|
||||
)
|
||||
],
|
||||
)),
|
||||
SliverToBoxAdapter(
|
||||
|
|
|
@ -0,0 +1,297 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:shlink_app/API/Classes/ShortURL/RedirectRule/condition_device_type.dart';
|
||||
import 'package:shlink_app/API/Classes/ShortURL/RedirectRule/redirect_rule_condition.dart';
|
||||
import 'package:shlink_app/API/Classes/ShortURL/RedirectRule/redirect_rule_condition_type.dart';
|
||||
import 'package:shlink_app/API/Classes/ShortURL/short_url.dart';
|
||||
import 'package:shlink_app/API/server_manager.dart';
|
||||
import '../globals.dart' as globals;
|
||||
import '../API/Classes/ShortURL/RedirectRule/redirect_rule.dart';
|
||||
|
||||
class RedirectRulesDetailView extends StatefulWidget {
|
||||
const RedirectRulesDetailView({super.key, required this.shortURL});
|
||||
|
||||
final ShortURL shortURL;
|
||||
|
||||
@override
|
||||
State<RedirectRulesDetailView> createState() =>
|
||||
_RedirectRulesDetailViewState();
|
||||
}
|
||||
|
||||
class _RedirectRulesDetailViewState extends State<RedirectRulesDetailView> {
|
||||
List<RedirectRule> redirectRules = [];
|
||||
|
||||
bool redirectRulesLoaded = false;
|
||||
|
||||
bool isSaving = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => loadRedirectRules());
|
||||
}
|
||||
|
||||
Future<void> loadRedirectRules() async {
|
||||
final response =
|
||||
await globals.serverManager.getRedirectRules(widget.shortURL.shortCode);
|
||||
response.fold((l) {
|
||||
setState(() {
|
||||
redirectRules = l;
|
||||
redirectRulesLoaded = true;
|
||||
});
|
||||
_sortListByPriority();
|
||||
return true;
|
||||
}, (r) {
|
||||
var text = "";
|
||||
if (r is RequestFailure) {
|
||||
text = r.description;
|
||||
} else {
|
||||
text = (r as ApiFailure).detail;
|
||||
}
|
||||
|
||||
final snackBar = SnackBar(
|
||||
content: Text(text),
|
||||
backgroundColor: Colors.red[400],
|
||||
behavior: SnackBarBehavior.floating);
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
void _saveRedirectRules() async {
|
||||
final response = await globals.serverManager
|
||||
.setRedirectRules(widget.shortURL.shortCode, redirectRules);
|
||||
response.fold((l) {
|
||||
Navigator.pop(context);
|
||||
}, (r) {
|
||||
var text = "";
|
||||
if (r is RequestFailure) {
|
||||
text = r.description;
|
||||
} else {
|
||||
text = (r as ApiFailure).detail;
|
||||
}
|
||||
|
||||
final snackBar = SnackBar(
|
||||
content: Text(text),
|
||||
backgroundColor: Colors.red[400],
|
||||
behavior: SnackBarBehavior.floating);
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
void _sortListByPriority() {
|
||||
setState(() {
|
||||
redirectRules.sort((a, b) => a.priority - b.priority);
|
||||
});
|
||||
}
|
||||
|
||||
void _fixPriorities() {
|
||||
for (int i = 0; i < redirectRules.length; i++) {
|
||||
setState(() {
|
||||
redirectRules[i].priority = i + 1;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
floatingActionButton: Wrap(
|
||||
spacing: 16,
|
||||
children: [
|
||||
FloatingActionButton(
|
||||
onPressed: () {
|
||||
if (!isSaving & redirectRulesLoaded) {
|
||||
setState(() {
|
||||
isSaving = true;
|
||||
});
|
||||
_saveRedirectRules();
|
||||
}
|
||||
},
|
||||
child: isSaving
|
||||
? const Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: CircularProgressIndicator(strokeWidth: 3))
|
||||
: const Icon(Icons.save))
|
||||
],
|
||||
),
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
const SliverAppBar.medium(
|
||||
expandedHeight: 120,
|
||||
title: Text(
|
||||
"Redirect Rules",
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
if (redirectRulesLoaded && redirectRules.isEmpty)
|
||||
SliverToBoxAdapter(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 50),
|
||||
child: Column(
|
||||
children: [
|
||||
const Text(
|
||||
"No Redirect Rules",
|
||||
style: TextStyle(
|
||||
fontSize: 24, fontWeight: FontWeight.bold),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Text(
|
||||
'Adding redirect rules will be supported soon!',
|
||||
style: TextStyle(
|
||||
fontSize: 16, color: Colors.grey[600]),
|
||||
),
|
||||
)
|
||||
],
|
||||
))))
|
||||
else
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) {
|
||||
return _ListCell(
|
||||
redirectRule: redirectRules[index],
|
||||
moveUp: index == 0
|
||||
? null
|
||||
: () {
|
||||
setState(() {
|
||||
redirectRules[index].priority -= 1;
|
||||
redirectRules[index - 1].priority += 1;
|
||||
});
|
||||
_sortListByPriority();
|
||||
},
|
||||
moveDown: index == (redirectRules.length - 1)
|
||||
? null
|
||||
: () {
|
||||
setState(() {
|
||||
redirectRules[index].priority += 1;
|
||||
redirectRules[index + 1].priority -= 1;
|
||||
});
|
||||
_sortListByPriority();
|
||||
},
|
||||
delete: () {
|
||||
setState(() {
|
||||
redirectRules.removeAt(index);
|
||||
});
|
||||
_fixPriorities();
|
||||
},
|
||||
);
|
||||
}, childCount: redirectRules.length))
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ListCell extends StatefulWidget {
|
||||
const _ListCell(
|
||||
{required this.redirectRule,
|
||||
required this.moveUp,
|
||||
required this.moveDown,
|
||||
required this.delete});
|
||||
|
||||
final VoidCallback? moveUp;
|
||||
final VoidCallback? moveDown;
|
||||
final VoidCallback delete;
|
||||
final RedirectRule redirectRule;
|
||||
|
||||
@override
|
||||
State<_ListCell> createState() => _ListCellState();
|
||||
}
|
||||
|
||||
class _ListCellState extends State<_ListCell> {
|
||||
String _conditionToTagString(RedirectRuleCondition condition) {
|
||||
switch (condition.type) {
|
||||
case RedirectRuleConditionType.DEVICE:
|
||||
return "Device is ${ConditionDeviceType.fromApi(condition.matchValue).humanReadable}";
|
||||
case RedirectRuleConditionType.LANGUAGE:
|
||||
return "Language is ${condition.matchValue}";
|
||||
case RedirectRuleConditionType.QUERY_PARAM:
|
||||
return "Query string contains ${condition.matchKey}=${condition.matchValue}";
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 8, right: 8),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(left: 8, right: 8, top: 16, bottom: 16),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: MediaQuery.of(context).platformBrightness ==
|
||||
Brightness.dark
|
||||
? Colors.grey[800]!
|
||||
: Colors.grey[300]!)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Text("Long URL ",
|
||||
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
Text(widget.redirectRule.longUrl)
|
||||
],
|
||||
),
|
||||
const Text("Conditions:",
|
||||
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Wrap(
|
||||
children:
|
||||
widget.redirectRule.conditions.map((condition) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 4, top: 4),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 4, bottom: 4, left: 12, right: 12),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
color:
|
||||
MediaQuery.of(context).platformBrightness ==
|
||||
Brightness.dark
|
||||
? Colors.grey[900]
|
||||
: Colors.grey[300],
|
||||
),
|
||||
child: Text(_conditionToTagString(condition)),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
Wrap(
|
||||
children: [
|
||||
IconButton(
|
||||
disabledColor:
|
||||
MediaQuery.of(context).platformBrightness ==
|
||||
Brightness.dark
|
||||
? Colors.grey[700]
|
||||
: Colors.grey[400],
|
||||
onPressed: widget.moveUp,
|
||||
icon: const Icon(Icons.arrow_upward),
|
||||
),
|
||||
IconButton(
|
||||
disabledColor:
|
||||
MediaQuery.of(context).platformBrightness ==
|
||||
Brightness.dark
|
||||
? Colors.grey[700]
|
||||
: Colors.grey[400],
|
||||
onPressed: widget.moveDown,
|
||||
icon: const Icon(Icons.arrow_downward),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: widget.delete,
|
||||
icon: const Icon(Icons.delete, color: Colors.red),
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
)));
|
||||
}
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:shlink_app/API/server_manager.dart';
|
||||
import 'package:shlink_app/views/login_view.dart';
|
||||
import 'package:shlink_app/views/opensource_licenses_view.dart';
|
||||
import 'package:shlink_app/widgets/available_servers_bottom_sheet.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import '../globals.dart' as globals;
|
||||
|
||||
|
@ -64,91 +64,81 @@ class _SettingsViewState extends State<SettingsView> {
|
|||
return Scaffold(
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
SliverAppBar.medium(
|
||||
const SliverAppBar.medium(
|
||||
expandedHeight: 120,
|
||||
title: const Text(
|
||||
title: Text(
|
||||
"Settings",
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
actions: [
|
||||
PopupMenuButton(
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
const PopupMenuItem(
|
||||
value: 0,
|
||||
child: Text("Log out", style: TextStyle(color: Colors.red)),
|
||||
)
|
||||
];
|
||||
},
|
||||
onSelected: (value) {
|
||||
if (value == 0) {
|
||||
globals.serverManager.logOut().then((value) =>
|
||||
Navigator.of(context).pushReplacement(MaterialPageRoute(
|
||||
builder: (context) => const LoginView())));
|
||||
}
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Theme.of(context).brightness == Brightness.light
|
||||
? Colors.grey[100]
|
||||
: Colors.grey[900],
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.dns_outlined,
|
||||
color: (() {
|
||||
switch (_serverStatus) {
|
||||
case ServerStatus.connected:
|
||||
return Colors.green;
|
||||
case ServerStatus.connecting:
|
||||
return Colors.orange;
|
||||
case ServerStatus.disconnected:
|
||||
return Colors.red;
|
||||
}
|
||||
}())),
|
||||
const SizedBox(width: 8),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text("Connected to",
|
||||
style: TextStyle(color: Colors.grey)),
|
||||
Text(globals.serverManager.getServerUrl(),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16)),
|
||||
Row(
|
||||
children: [
|
||||
const Text("API Version: ",
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
fontWeight: FontWeight.w600)),
|
||||
Text(globals.serverManager.getApiVersion(),
|
||||
style:
|
||||
const TextStyle(color: Colors.grey)),
|
||||
const SizedBox(width: 16),
|
||||
const Text("Server Version: ",
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
fontWeight: FontWeight.w600)),
|
||||
Text(_serverVersion,
|
||||
style:
|
||||
const TextStyle(color: Colors.grey))
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return const AvailableServerBottomSheet();
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Theme.of(context).brightness == Brightness.light
|
||||
? Colors.grey[100]
|
||||
: Colors.grey[900],
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.dns_outlined,
|
||||
color: (() {
|
||||
switch (_serverStatus) {
|
||||
case ServerStatus.connected:
|
||||
return Colors.green;
|
||||
case ServerStatus.connecting:
|
||||
return Colors.orange;
|
||||
case ServerStatus.disconnected:
|
||||
return Colors.red;
|
||||
}
|
||||
}())),
|
||||
const SizedBox(width: 8),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text("Connected to",
|
||||
style: TextStyle(color: Colors.grey)),
|
||||
Text(globals.serverManager.getServerUrl(),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||