mirror of
https://github.com/rainloreley/shlink-manager.git
synced 2025-01-15 19:53:27 +01:00
Compare commits
No commits in common. "21042fa2b7bb84d71864ae63ebba953972db8904" and "202ab20747bdaf8dee441a80616a475ca3f32e36" have entirely different histories.
21042fa2b7
...
202ab20747
@ -1,20 +0,0 @@
|
|||||||
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"]);
|
|
||||||
}
|
|
@ -1,68 +0,0 @@
|
|||||||
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,14 +7,12 @@ 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';
|
||||||
|
|
||||||
@ -183,11 +181,6 @@ 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);
|
||||||
@ -241,14 +234,6 @@ 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;
|
||||||
|
@ -18,7 +18,7 @@ class GlobalTheme {
|
|||||||
canvasColor: colorScheme.surface,
|
canvasColor: colorScheme.surface,
|
||||||
scaffoldBackgroundColor: colorScheme.surface,
|
scaffoldBackgroundColor: colorScheme.surface,
|
||||||
highlightColor: Colors.transparent,
|
highlightColor: Colors.transparent,
|
||||||
dividerColor: colorScheme.shadow,
|
dividerColor: colorScheme.outline,
|
||||||
focusColor: focusColor,
|
focusColor: focusColor,
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
appBarTheme: AppBarTheme(
|
appBarTheme: AppBarTheme(
|
||||||
@ -38,8 +38,7 @@ class GlobalTheme {
|
|||||||
tertiary: Colors.grey[300],
|
tertiary: Colors.grey[300],
|
||||||
onTertiary: Colors.grey[700],
|
onTertiary: Colors.grey[700],
|
||||||
surfaceContainer: (Colors.grey[100])!,
|
surfaceContainer: (Colors.grey[100])!,
|
||||||
outline: (Colors.grey[500])!,
|
outline: (Colors.grey[400])!,
|
||||||
shadow: (Colors.grey[300])!,
|
|
||||||
error: (Colors.red[400])!,
|
error: (Colors.red[400])!,
|
||||||
onError: Colors.white,
|
onError: Colors.white,
|
||||||
surface: Color(0xFFFAFBFB),
|
surface: Color(0xFFFAFBFB),
|
||||||
@ -58,8 +57,7 @@ class GlobalTheme {
|
|||||||
onSurfaceVariant: Colors.grey[400],
|
onSurfaceVariant: Colors.grey[400],
|
||||||
tertiary: Colors.grey[900],
|
tertiary: Colors.grey[900],
|
||||||
onTertiary: Colors.grey,
|
onTertiary: Colors.grey,
|
||||||
outline: (Colors.grey[700])!,
|
outline: (Colors.grey[800])!,
|
||||||
shadow: (Colors.grey[800])!,
|
|
||||||
error: (Colors.red[400])!,
|
error: (Colors.red[400])!,
|
||||||
onError: Colors.white,
|
onError: Colors.white,
|
||||||
onPrimary: Colors.white,
|
onPrimary: Colors.white,
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
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 {
|
||||||
@ -59,7 +58,6 @@ 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",
|
||||||
@ -67,9 +65,6 @@ class _LoginViewState extends State<LoginView> {
|
|||||||
SliverFillRemaining(
|
SliverFillRemaining(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
Align(
|
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@ -144,31 +139,6 @@ class _LoginViewState extends State<LoginView> {
|
|||||||
textAlign: TextAlign.center))
|
textAlign: TextAlign.center))
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.bottomCenter,
|
|
||||||
child: TextButton(
|
|
||||||
onPressed: () async {
|
|
||||||
final Uri url = Uri.parse('https://shlink.io/documentation/api-docs/authentication/');
|
|
||||||
try {
|
|
||||||
if (!await launchUrl(url)) {
|
|
||||||
throw Exception();
|
|
||||||
}
|
|
||||||
} 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"),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -93,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, color: Colors.white))
|
child: CircularProgressIndicator(strokeWidth: 3))
|
||||||
: const Icon(Icons.save))
|
: const Icon(Icons.save))
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -1,14 +1,10 @@
|
|||||||
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/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 {
|
||||||
@ -27,7 +23,6 @@ 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;
|
||||||
@ -69,7 +64,6 @@ 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 ?? "";
|
||||||
@ -110,7 +104,7 @@ class _ShortURLEditViewState extends State<ShortURLEditView>
|
|||||||
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,
|
||||||
@ -172,7 +166,7 @@ class _ShortURLEditViewState extends State<ShortURLEditView>
|
|||||||
),
|
),
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(top: 16, left: 8, right: 8),
|
padding: EdgeInsets.only(top: 16, left: 8, right: 8),
|
||||||
child: Wrap(
|
child: Wrap(
|
||||||
runSpacing: 16,
|
runSpacing: 16,
|
||||||
children: [
|
children: [
|
||||||
@ -223,11 +217,8 @@ class _ShortURLEditViewState extends State<ShortURLEditView>
|
|||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
if (widget.shortUrl == null)
|
RotationTransition(
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.only(left: 8),
|
|
||||||
child: RotationTransition(
|
|
||||||
turns: Tween(begin: 0.0, end: 3.0).animate(
|
turns: Tween(begin: 0.0, end: 3.0).animate(
|
||||||
CurvedAnimation(
|
CurvedAnimation(
|
||||||
parent: _customSlugDiceAnimationController,
|
parent: _customSlugDiceAnimationController,
|
||||||
@ -250,11 +241,10 @@ class _ShortURLEditViewState extends State<ShortURLEditView>
|
|||||||
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 && widget.shortUrl == null)
|
if (randomSlug)
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
@ -290,57 +280,6 @@ class _ShortURLEditViewState extends State<ShortURLEditView>
|
|||||||
],
|
],
|
||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
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: [
|
||||||
@ -383,7 +322,6 @@ class _ShortURLEditViewState extends State<ShortURLEditView>
|
|||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 150)
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -397,8 +335,7 @@ class _ShortURLEditViewState extends State<ShortURLEditView>
|
|||||||
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)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,246 +0,0 @@
|
|||||||
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
linux/.gitignore
vendored
Normal file
1
linux/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
flutter/ephemeral
|
139
linux/CMakeLists.txt
Normal file
139
linux/CMakeLists.txt
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
# 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()
|
88
linux/flutter/CMakeLists.txt
Normal file
88
linux/flutter/CMakeLists.txt
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
# 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}
|
||||||
|
)
|
23
linux/flutter/generated_plugin_registrant.cc
Normal file
23
linux/flutter/generated_plugin_registrant.cc
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
//
|
||||||
|
// 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);
|
||||||
|
}
|
15
linux/flutter/generated_plugin_registrant.h
Normal file
15
linux/flutter/generated_plugin_registrant.h
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
//
|
||||||
|
// 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_
|
26
linux/flutter/generated_plugins.cmake
Normal file
26
linux/flutter/generated_plugins.cmake
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
#
|
||||||
|
# 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)
|
6
linux/main.cc
Normal file
6
linux/main.cc
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
#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);
|
||||||
|
}
|
104
linux/my_application.cc
Normal file
104
linux/my_application.cc
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
#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));
|
||||||
|
}
|
18
linux/my_application.h
Normal file
18
linux/my_application.h
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
#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_
|
Loading…
x
Reference in New Issue
Block a user