mirror of
https://github.com/rainloreley/shlink-manager.git
synced 2025-04-19 22:39:38 +02:00
Compare commits
10 Commits
Author | SHA1 | Date | |
---|---|---|---|
d11d0cfe5c | |||
21042fa2b7 | |||
8507aaa8bd | |||
e673dd7b64 | |||
5c318c6237 | |||
d33f5757c9 | |||
c5ab724a9e | |||
50ec0cb49f | |||
202ab20747 | |||
4f8cc9e4b6 |
@ -28,7 +28,6 @@ Shlink Manager is an app for Android to see and manage all shortened URLs create
|
|||||||
|
|
||||||
## 🔨 To Do
|
## 🔨 To Do
|
||||||
- [ ] Add support for iOS (maybe in the future)
|
- [ ] Add support for iOS (maybe in the future)
|
||||||
- [ ] add tags
|
|
||||||
- [ ] improve app icon
|
- [ ] improve app icon
|
||||||
- [ ] Refactor code
|
- [ ] Refactor code
|
||||||
- [ ] ...and more
|
- [ ] ...and more
|
||||||
|
20
lib/API/Classes/Tag/tag_with_stats.dart
Normal file
20
lib/API/Classes/Tag/tag_with_stats.dart
Normal 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"]);
|
||||||
|
}
|
68
lib/API/Methods/get_tags_with_stats.dart
Normal file
68
lib/API/Methods/get_tags_with_stats.dart
Normal 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()));
|
||||||
|
}
|
||||||
|
}
|
@ -7,12 +7,14 @@ 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/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_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/set_redirect_rules.dart';
|
||||||
import 'package:shlink_app/API/Methods/update_short_url.dart';
|
import 'package:shlink_app/API/Methods/update_short_url.dart';
|
||||||
|
|
||||||
@ -181,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);
|
||||||
@ -234,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
71
lib/global_theme.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
@ -26,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),
|
||||||
),
|
),
|
||||||
@ -41,7 +44,7 @@ class MyApp extends StatelessWidget {
|
|||||||
colorScheme: darkColorScheme?.copyWith(surface: Colors.black) ??
|
colorScheme: darkColorScheme?.copyWith(surface: Colors.black) ??
|
||||||
_defaultDarkColorScheme,
|
_defaultDarkColorScheme,
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
),
|
),*/
|
||||||
home: const InitialPage());
|
home: const InitialPage());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
22
lib/util/build_api_error_snackbar.dart
Normal file
22
lib/util/build_api_error_snackbar.dart
Normal 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;
|
||||||
|
}
|
@ -5,7 +5,7 @@ import 'package:flutter_sharing_intent/flutter_sharing_intent.dart';
|
|||||||
import 'package:flutter_sharing_intent/model/sharing_file.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:shlink_app/widgets/available_servers_bottom_sheet.dart';
|
||||||
@ -68,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);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,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);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,7 +121,7 @@ class _HomeViewState extends State<HomeView> {
|
|||||||
},
|
},
|
||||||
child: Text(globals.serverManager.getServerUrl(),
|
child: Text(globals.serverManager.getServerUrl(),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16, color: Colors.grey[600])),
|
fontSize: 16, color: Theme.of(context).colorScheme.onTertiary)),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
)),
|
)),
|
||||||
@ -189,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),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@ -251,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,
|
|
||||||
),
|
),
|
||||||
))),
|
))),
|
||||||
),
|
),
|
||||||
|
@ -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"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
@ -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,
|
||||||
),
|
),
|
||||||
))
|
))
|
||||||
|
@ -3,7 +3,7 @@ import 'package:shlink_app/API/Classes/ShortURL/RedirectRule/condition_device_ty
|
|||||||
import 'package:shlink_app/API/Classes/ShortURL/RedirectRule/redirect_rule_condition.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/RedirectRule/redirect_rule_condition_type.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 '../globals.dart' as globals;
|
import '../globals.dart' as globals;
|
||||||
import '../API/Classes/ShortURL/RedirectRule/redirect_rule.dart';
|
import '../API/Classes/ShortURL/RedirectRule/redirect_rule.dart';
|
||||||
|
|
||||||
@ -41,18 +41,9 @@ class _RedirectRulesDetailViewState extends State<RedirectRulesDetailView> {
|
|||||||
_sortListByPriority();
|
_sortListByPriority();
|
||||||
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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -63,18 +54,9 @@ class _RedirectRulesDetailViewState extends State<RedirectRulesDetailView> {
|
|||||||
response.fold((l) {
|
response.fold((l) {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
}, (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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -111,7 +93,7 @@ class _RedirectRulesDetailViewState extends State<RedirectRulesDetailView> {
|
|||||||
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))
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -141,7 +123,7 @@ class _RedirectRulesDetailViewState extends State<RedirectRulesDetailView> {
|
|||||||
child: Text(
|
child: Text(
|
||||||
'Adding redirect rules will be supported soon!',
|
'Adding redirect rules will be supported soon!',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16, color: Colors.grey[600]),
|
fontSize: 16, color: Theme.of(context).colorScheme.onSecondary),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@ -221,10 +203,7 @@ class _ListCellState extends State<_ListCell> {
|
|||||||
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: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@ -252,10 +231,7 @@ class _ListCellState extends State<_ListCell> {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
color:
|
color:
|
||||||
MediaQuery.of(context).platformBrightness ==
|
Theme.of(context).colorScheme.tertiary,
|
||||||
Brightness.dark
|
|
||||||
? Colors.grey[900]
|
|
||||||
: Colors.grey[300],
|
|
||||||
),
|
),
|
||||||
child: Text(_conditionToTagString(condition)),
|
child: Text(_conditionToTagString(condition)),
|
||||||
),
|
),
|
||||||
@ -269,19 +245,13 @@ class _ListCellState extends State<_ListCell> {
|
|||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
disabledColor:
|
disabledColor:
|
||||||
MediaQuery.of(context).platformBrightness ==
|
Theme.of(context).disabledColor,
|
||||||
Brightness.dark
|
|
||||||
? Colors.grey[700]
|
|
||||||
: Colors.grey[400],
|
|
||||||
onPressed: widget.moveUp,
|
onPressed: widget.moveUp,
|
||||||
icon: const Icon(Icons.arrow_upward),
|
icon: const Icon(Icons.arrow_upward),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
disabledColor:
|
disabledColor:
|
||||||
MediaQuery.of(context).platformBrightness ==
|
Theme.of(context).disabledColor,
|
||||||
Brightness.dark
|
|
||||||
? Colors.grey[700]
|
|
||||||
: Colors.grey[400],
|
|
||||||
onPressed: widget.moveDown,
|
onPressed: widget.moveDown,
|
||||||
icon: const Icon(Icons.arrow_downward),
|
icon: const Icon(Icons.arrow_downward),
|
||||||
),
|
),
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
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/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:shlink_app/widgets/available_servers_bottom_sheet.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
@ -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);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,9 +77,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: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(12.0),
|
padding: const EdgeInsets.all(12.0),
|
||||||
@ -110,29 +98,29 @@ class _SettingsViewState extends State<SettingsView> {
|
|||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Text("Connected to",
|
Text("Connected to",
|
||||||
style: TextStyle(color: Colors.grey)),
|
style: TextStyle(color: Theme.of(context).colorScheme.onTertiary)),
|
||||||
Text(globals.serverManager.getServerUrl(),
|
Text(globals.serverManager.getServerUrl(),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 16)),
|
fontSize: 16)),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const Text("API Version: ",
|
Text("API Version: ",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.grey,
|
color: Theme.of(context).colorScheme.onTertiary,
|
||||||
fontWeight: FontWeight.w600)),
|
fontWeight: FontWeight.w600)),
|
||||||
Text(globals.serverManager.getApiVersion(),
|
Text(globals.serverManager.getApiVersion(),
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.grey)),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
const Text("Server Version: ",
|
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.grey,
|
color: Theme.of(context).colorScheme.onTertiary)),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Text("Server Version: ",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onTertiary,
|
||||||
fontWeight: FontWeight.w600)),
|
fontWeight: FontWeight.w600)),
|
||||||
Text(_serverVersion,
|
Text(_serverVersion,
|
||||||
style:
|
style:
|
||||||
const TextStyle(color: Colors.grey))
|
TextStyle(color: Theme.of(context).colorScheme.onTertiary))
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -143,7 +131,7 @@ class _SettingsViewState extends State<SettingsView> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
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: () {
|
||||||
@ -154,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(
|
||||||
@ -190,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(
|
||||||
@ -226,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(
|
||||||
@ -261,13 +243,11 @@ class _SettingsViewState extends State<SettingsView> {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
color:
|
color:
|
||||||
Theme.of(context).brightness == Brightness.light
|
Theme.of(context).colorScheme.surfaceContainer
|
||||||
? Colors.grey[100]
|
|
||||||
: Colors.grey[900],
|
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
"${packageInfo.appName}, v${packageInfo.version} (${packageInfo.buildNumber})",
|
"${packageInfo.appName}, v${packageInfo.version} (${packageInfo.buildNumber})",
|
||||||
style: const TextStyle(color: Colors.grey),
|
style: TextStyle(color: Theme.of(context).colorScheme.onSecondary),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
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 {
|
||||||
@ -22,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;
|
||||||
@ -63,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 ?? "";
|
||||||
@ -72,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,
|
||||||
@ -119,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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -149,71 +171,71 @@ class _ShortURLEditViewState extends State<ShortURLEditView>
|
|||||||
style: const 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),
|
||||||
label: Row(
|
const SizedBox(width: 8),
|
||||||
children: [
|
Text(
|
||||||
const Icon(Icons.link),
|
"${randomSlug ? "Random" : "Custom"} slug",
|
||||||
const SizedBox(width: 8),
|
style: TextStyle(
|
||||||
Text(
|
fontStyle: randomSlug
|
||||||
"${randomSlug ? "Random" : "Custom"} slug",
|
? FontStyle.italic
|
||||||
style: TextStyle(
|
: FontStyle.normal),
|
||||||
fontStyle: randomSlug
|
)
|
||||||
? FontStyle.italic
|
],
|
||||||
: FontStyle.normal),
|
)),
|
||||||
)
|
),
|
||||||
],
|
|
||||||
)),
|
|
||||||
),
|
),
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
if (widget.shortUrl == null)
|
||||||
RotationTransition(
|
Container(
|
||||||
turns: Tween(begin: 0.0, end: 3.0).animate(
|
padding: const EdgeInsets.only(left: 8),
|
||||||
CurvedAnimation(
|
child: RotationTransition(
|
||||||
parent: _customSlugDiceAnimationController,
|
turns: Tween(begin: 0.0, end: 3.0).animate(
|
||||||
curve: Curves.easeInOutExpo)),
|
CurvedAnimation(
|
||||||
child: IconButton(
|
parent: _customSlugDiceAnimationController,
|
||||||
onPressed: disableSlugEditor
|
curve: Curves.easeInOutExpo)),
|
||||||
? null
|
child: IconButton(
|
||||||
: () {
|
onPressed: disableSlugEditor
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
if (randomSlug) {
|
if (randomSlug) {
|
||||||
_customSlugDiceAnimationController.reverse(
|
_customSlugDiceAnimationController.reverse(
|
||||||
from: 1);
|
from: 1);
|
||||||
@ -225,132 +247,158 @@ class _ShortURLEditViewState extends State<ShortURLEditView>
|
|||||||
randomSlug = !randomSlug;
|
randomSlug = !randomSlug;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
randomSlug ? Icons.casino : Icons.casino_outlined,
|
randomSlug ? Icons.casino : Icons.casino_outlined,
|
||||||
color: randomSlug ? Colors.green : Colors.grey)),
|
color: randomSlug ? Colors.green : Colors.grey)),
|
||||||
)
|
),
|
||||||
],
|
)
|
||||||
),
|
],
|
||||||
if (randomSlug) const SizedBox(height: 16),
|
),
|
||||||
if (randomSlug)
|
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(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.label_outline),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text("Tags")
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
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(),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
),
|
||||||
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)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
246
lib/views/tag_selector_view.dart
Normal file
246
lib/views/tag_selector_view.dart
Normal 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}"'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
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/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';
|
||||||
@ -67,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;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -207,10 +198,7 @@ class _ListCellState extends State<_ListCell> {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border(
|
border: Border(
|
||||||
top: BorderSide(
|
top: 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,
|
||||||
@ -226,10 +214,7 @@ class _ListCellState extends State<_ListCell> {
|
|||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
color: Theme.of(context).brightness ==
|
color: Theme.of(context).colorScheme.outline,
|
||||||
Brightness.dark
|
|
||||||
? Colors.grey[700]
|
|
||||||
: Colors.grey[300],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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,
|
|
||||||
),
|
),
|
||||||
))),
|
))),
|
||||||
),
|
),
|
||||||
@ -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)
|
||||||
|
1
linux/.gitignore
vendored
1
linux/.gitignore
vendored
@ -1 +0,0 @@
|
|||||||
flutter/ephemeral
|
|
@ -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()
|
|
@ -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}
|
|
||||||
)
|
|
@ -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);
|
|
||||||
}
|
|
@ -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_
|
|
@ -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)
|
|
@ -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);
|
|
||||||
}
|
|
@ -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));
|
|
||||||
}
|
|
@ -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_
|
|
@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# 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.3.1+10
|
version: 1.4.0+11
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.0.0
|
sdk: ^3.0.0
|
||||||
|
Loading…
x
Reference in New Issue
Block a user