mirror of
https://github.com/rainloreley/shlink-manager.git
synced 2025-01-15 03:33:27 +01:00
Compare commits
7 Commits
202ab20747
...
21042fa2b7
Author | SHA1 | Date | |
---|---|---|---|
21042fa2b7 | |||
8507aaa8bd | |||
e673dd7b64 | |||
5c318c6237 | |||
d33f5757c9 | |||
c5ab724a9e | |||
50ec0cb49f |
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/short_url.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/get_recent_short_urls.dart';
|
||||
import 'package:shlink_app/API/Methods/get_redirect_rules.dart';
|
||||
import 'package:shlink_app/API/Methods/get_server_health.dart';
|
||||
import 'package:shlink_app/API/Methods/get_shlink_stats.dart';
|
||||
import 'package:shlink_app/API/Methods/get_short_urls.dart';
|
||||
import 'package:shlink_app/API/Methods/get_tags_with_stats.dart';
|
||||
import 'package:shlink_app/API/Methods/set_redirect_rules.dart';
|
||||
import 'package:shlink_app/API/Methods/update_short_url.dart';
|
||||
|
||||
@ -181,6 +183,11 @@ class ServerManager {
|
||||
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
|
||||
FutureOr<Either<ShlinkStats, Failure>> getShlinkStats() async {
|
||||
return apiGetShlinkStats(apiKey, serverUrl, apiVersion);
|
||||
@ -234,6 +241,14 @@ class ShortURLPageResponse {
|
||||
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
|
||||
class ServerHealthResponse {
|
||||
String status;
|
||||
|
@ -18,7 +18,7 @@ class GlobalTheme {
|
||||
canvasColor: colorScheme.surface,
|
||||
scaffoldBackgroundColor: colorScheme.surface,
|
||||
highlightColor: Colors.transparent,
|
||||
dividerColor: colorScheme.outline,
|
||||
dividerColor: colorScheme.shadow,
|
||||
focusColor: focusColor,
|
||||
useMaterial3: true,
|
||||
appBarTheme: AppBarTheme(
|
||||
@ -38,7 +38,8 @@ class GlobalTheme {
|
||||
tertiary: Colors.grey[300],
|
||||
onTertiary: Colors.grey[700],
|
||||
surfaceContainer: (Colors.grey[100])!,
|
||||
outline: (Colors.grey[400])!,
|
||||
outline: (Colors.grey[500])!,
|
||||
shadow: (Colors.grey[300])!,
|
||||
error: (Colors.red[400])!,
|
||||
onError: Colors.white,
|
||||
surface: Color(0xFFFAFBFB),
|
||||
@ -57,7 +58,8 @@ class GlobalTheme {
|
||||
onSurfaceVariant: Colors.grey[400],
|
||||
tertiary: Colors.grey[900],
|
||||
onTertiary: Colors.grey,
|
||||
outline: (Colors.grey[800])!,
|
||||
outline: (Colors.grey[700])!,
|
||||
shadow: (Colors.grey[800])!,
|
||||
error: (Colors.red[400])!,
|
||||
onError: Colors.white,
|
||||
onPrimary: Colors.white,
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shlink_app/API/server_manager.dart';
|
||||
import 'package:shlink_app/main.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import '../globals.dart' as globals;
|
||||
|
||||
class LoginView extends StatefulWidget {
|
||||
@ -58,6 +59,7 @@ class _LoginViewState extends State<LoginView> {
|
||||
return Scaffold(
|
||||
extendBody: true,
|
||||
body: CustomScrollView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
slivers: [
|
||||
const SliverAppBar.medium(
|
||||
title: Text("Add server",
|
||||
@ -65,79 +67,107 @@ class _LoginViewState extends State<LoginView> {
|
||||
SliverFillRemaining(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
child: Stack(
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(bottom: 8),
|
||||
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(
|
||||
Align(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
FilledButton.tonal(
|
||||
onPressed: () => {_connect()},
|
||||
child: _isLoggingIn
|
||||
? Container(
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(bottom: 8),
|
||||
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,
|
||||
children: [
|
||||
FilledButton.tonal(
|
||||
onPressed: () => {_connect()},
|
||||
child: _isLoggingIn
|
||||
? Container(
|
||||
width: 34,
|
||||
height: 34,
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: const CircularProgressIndicator(),
|
||||
)
|
||||
: const Text("Connect",
|
||||
style: TextStyle(fontSize: 20)),
|
||||
)
|
||||
: const Text("Connect",
|
||||
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(
|
||||
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))
|
||||
],
|
||||
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
|
||||
? const Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: CircularProgressIndicator(strokeWidth: 3))
|
||||
child: CircularProgressIndicator(strokeWidth: 3, color: Colors.white))
|
||||
: const Icon(Icons.save))
|
||||
],
|
||||
),
|
||||
|
@ -1,10 +1,14 @@
|
||||
import 'package:dartz/dartz.dart' as dartz;
|
||||
import 'package:dynamic_color/dynamic_color.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.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/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;
|
||||
|
||||
class ShortURLEditView extends StatefulWidget {
|
||||
@ -23,6 +27,7 @@ class _ShortURLEditViewState extends State<ShortURLEditView>
|
||||
final customSlugController = TextEditingController();
|
||||
final titleController = TextEditingController();
|
||||
final randomSlugLengthController = TextEditingController(text: "5");
|
||||
List<String> tags = [];
|
||||
|
||||
bool randomSlug = true;
|
||||
bool isCrawlable = true;
|
||||
@ -64,6 +69,7 @@ class _ShortURLEditViewState extends State<ShortURLEditView>
|
||||
if (widget.shortUrl != null) {
|
||||
longUrlController.text = widget.shortUrl!.longUrl;
|
||||
isCrawlable = widget.shortUrl!.crawlable;
|
||||
tags = widget.shortUrl!.tags;
|
||||
// for some reason this attribute is not returned by the api
|
||||
forwardQuery = true;
|
||||
titleController.text = widget.shortUrl!.title ?? "";
|
||||
@ -104,7 +110,7 @@ class _ShortURLEditViewState extends State<ShortURLEditView>
|
||||
void _submitShortUrl() async {
|
||||
var newSubmission = ShortURLSubmission(
|
||||
longUrl: longUrlController.text,
|
||||
tags: [],
|
||||
tags: tags,
|
||||
crawlable: isCrawlable,
|
||||
forwardQuery: forwardQuery,
|
||||
findIfExists: true,
|
||||
@ -166,7 +172,7 @@ class _ShortURLEditViewState extends State<ShortURLEditView>
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: 16, left: 8, right: 8),
|
||||
padding: const EdgeInsets.only(top: 16, left: 8, right: 8),
|
||||
child: Wrap(
|
||||
runSpacing: 16,
|
||||
children: [
|
||||
@ -217,34 +223,38 @@ class _ShortURLEditViewState extends State<ShortURLEditView>
|
||||
)),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
RotationTransition(
|
||||
turns: Tween(begin: 0.0, end: 3.0).animate(
|
||||
CurvedAnimation(
|
||||
parent: _customSlugDiceAnimationController,
|
||||
curve: Curves.easeInOutExpo)),
|
||||
child: IconButton(
|
||||
onPressed: disableSlugEditor
|
||||
? null
|
||||
: () {
|
||||
if (randomSlug) {
|
||||
_customSlugDiceAnimationController.reverse(
|
||||
from: 1);
|
||||
} else {
|
||||
_customSlugDiceAnimationController.forward(
|
||||
from: 0);
|
||||
}
|
||||
setState(() {
|
||||
randomSlug = !randomSlug;
|
||||
});
|
||||
},
|
||||
icon: Icon(
|
||||
randomSlug ? Icons.casino : Icons.casino_outlined,
|
||||
color: randomSlug ? Colors.green : Colors.grey)),
|
||||
)
|
||||
|
||||
if (widget.shortUrl == null)
|
||||
Container(
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
child: RotationTransition(
|
||||
turns: Tween(begin: 0.0, end: 3.0).animate(
|
||||
CurvedAnimation(
|
||||
parent: _customSlugDiceAnimationController,
|
||||
curve: Curves.easeInOutExpo)),
|
||||
child: IconButton(
|
||||
onPressed: disableSlugEditor
|
||||
? null
|
||||
: () {
|
||||
if (randomSlug) {
|
||||
_customSlugDiceAnimationController.reverse(
|
||||
from: 1);
|
||||
} else {
|
||||
_customSlugDiceAnimationController.forward(
|
||||
from: 0);
|
||||
}
|
||||
setState(() {
|
||||
randomSlug = !randomSlug;
|
||||
});
|
||||
},
|
||||
icon: Icon(
|
||||
randomSlug ? Icons.casino : Icons.casino_outlined,
|
||||
color: randomSlug ? Colors.green : Colors.grey)),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
if (randomSlug)
|
||||
if (randomSlug && widget.shortUrl == null)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
@ -280,6 +290,57 @@ 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(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
@ -322,6 +383,7 @@ class _ShortURLEditViewState extends State<ShortURLEditView>
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 150)
|
||||
],
|
||||
),
|
||||
)
|
||||
@ -335,7 +397,8 @@ class _ShortURLEditViewState extends State<ShortURLEditView>
|
||||
child: isSaving
|
||||
? const Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: CircularProgressIndicator(strokeWidth: 3))
|
||||
child: CircularProgressIndicator(strokeWidth: 3,
|
||||
color: Colors.white))
|
||||
: 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
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_
|
Loading…
x
Reference in New Issue
Block a user