diff --git a/.gitea/ISSUE_TEMPLATE/bug.yaml b/.gitea/ISSUE_TEMPLATE/bug.yaml new file mode 100644 index 00000000..f9441722 --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/bug.yaml @@ -0,0 +1,68 @@ +name: Bug report +about: File a bug report +labels: + - Bug +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! Please provide a short but a descriptive title for your issue. + - type: textarea + id: expected-behaviour + attributes: + label: Expected Behavior + description: What did you expect to happen? + validations: + required: true + - type: textarea + id: actual-behaviour + attributes: + label: Actual Behavior + description: What actually happened? + validations: + required: true + - type: textarea + id: steps-to-reproduce + attributes: + label: Steps to Reproduce + description: What steps can we follow to reproduce this issue? + placeholder: | + 1. First step + 2. Second step + 3. and so on... + validations: + required: true + - type: textarea + id: context + attributes: + label: Context and notes + description: Additional information about environment or what were you trying to do. If you have an idea how to fix this issue, please describe it here too. + - type: textarea + id: logs + attributes: + label: Relevant log output + description: Please copy and paste any relevant log output, if you have any. This will be automatically formatted into code, so no need for backticks. + render: shell + - type: input + id: app-version + attributes: + label: App Version + description: What version of SelfPrivacy app are you running? You can find it in the "About" section of the app. + validations: + required: true + - type: input + id: api-version + attributes: + label: Server API Version + description: What version of SelfPrivacy API are you running? You can find it in the "About" section of the app. Leave it empty if your app is not connected to the server yet. + - type: dropdown + id: os + attributes: + label: Operating System + description: What operating system are you using? + options: + - Android + - iOS + - Linux + - macOS + - Windows diff --git a/.gitea/ISSUE_TEMPLATE/feature.yaml b/.gitea/ISSUE_TEMPLATE/feature.yaml new file mode 100644 index 00000000..8ff40a0f --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/feature.yaml @@ -0,0 +1,23 @@ +name: Feature request +about: Suggest an idea for this project +label: + - Feature request +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this feature request! Please provide a short but a descriptive title for your issue. + - type: textarea + id: description + attributes: + label: Description + description: Describe the feature you'd like to see. + placeholder: | + As a user, I want to be able to... + validations: + required: true + - type: textarea + id: context + attributes: + label: Context and notes + description: Additional information about environment and what were you trying to do. If you have an idea how to implement this feature, please describe it here too. diff --git a/.gitea/ISSUE_TEMPLATE/translation.yaml b/.gitea/ISSUE_TEMPLATE/translation.yaml new file mode 100644 index 00000000..c7348b94 --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/translation.yaml @@ -0,0 +1,29 @@ +name: Translation issue +about: File a translation (localization) issue +labels: + - Translations +body: + - type: markdown + attributes: + value: | + Translations can be modified and discussed on [Weblate](https://weblate.selfprivacy.org/projects/selfprivacy/). You can fix the mistranslation issue yourself there. Using the search, you can also find the string ID of the mistranslated string. If your issue is more complex, please file it here + + If you are a member of SelfPrivacy core team, you **must** fix the issue yourself on Weblate. + - type: input + id: language + attributes: + label: Language + description: What language is affected? + placeholder: | + English + validations: + required: true + - type: textarea + id: description + attributes: + label: Description + description: Describe the issue in detail. If you have an idea how to fix this issue, please describe it here too. Include the string ID of the mistranslated string, if possible. + placeholder: | + The string `string.id` is translated as "foo", but it should be "bar". + validations: + required: true diff --git a/assets/translations/en.json b/assets/translations/en.json index 07ffe375..82f682ba 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -35,7 +35,8 @@ "done": "Done", "continue": "Continue", "alert": "Alert", - "copied_to_clipboard": "Copied to clipboard!" + "copied_to_clipboard": "Copied to clipboard!", + "please_connect": "Please connect your server, domain and DNS provider to dive in!" }, "more_page": { "configuration_wizard": "Setup wizard", @@ -315,6 +316,7 @@ "in_menu": "Server is not set up yet. Please finish setup using setup wizard for further work." }, "service_page": { + "nothing_here": "Nothing here", "open_in_browser": "Open in browser", "restart": "Restart service", "disable": "Disable service", @@ -371,7 +373,6 @@ "add_new_user": "Add a first user", "new_user": "New user", "delete_user": "Delete user", - "not_ready": "Please connect server, domain and DNS in the Providers tab, to be able to add a first user", "nobody_here": "Nobody here", "login": "Login", "new_user_info_note": "New user will automatically be granted an access to all of the services", diff --git a/lib/logic/cubit/dns_records/dns_records_cubit.dart b/lib/logic/cubit/dns_records/dns_records_cubit.dart index 3fc2d199..6f295274 100644 --- a/lib/logic/cubit/dns_records/dns_records_cubit.dart +++ b/lib/logic/cubit/dns_records/dns_records_cubit.dart @@ -75,7 +75,7 @@ class DnsRecordsCubit @override Future clear() async { - emit(const DnsRecordsState(dnsState: DnsRecordsStatus.error)); + emit(const DnsRecordsState(dnsState: DnsRecordsStatus.uninitialized)); } Future refresh() async { diff --git a/lib/ui/helpers/empty_page_placeholder.dart b/lib/ui/helpers/empty_page_placeholder.dart new file mode 100644 index 00000000..e1bb0d7b --- /dev/null +++ b/lib/ui/helpers/empty_page_placeholder.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:selfprivacy/ui/components/not_ready_card/not_ready_card.dart'; + +class EmptyPagePlaceholder extends StatelessWidget { + const EmptyPagePlaceholder({ + required this.title, + required this.iconData, + required this.description, + this.showReadyCard = false, + super.key, + }); + + final String title; + final String description; + final IconData iconData; + final bool showReadyCard; + + @override + Widget build(final BuildContext context) => !showReadyCard + ? _expandedContent(context) + : Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Padding( + padding: EdgeInsets.symmetric(horizontal: 15), + child: NotReadyCard(), + ), + Expanded( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: Center( + child: _expandedContent(context), + ), + ), + ) + ], + ); + + Widget _expandedContent(final BuildContext context) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + iconData, + size: 50, + color: Theme.of(context).colorScheme.onBackground, + ), + const SizedBox(height: 16), + Text( + title, + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + color: Theme.of(context).colorScheme.onBackground, + ), + ), + const SizedBox(height: 8), + Text( + description, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleSmall?.copyWith( + color: Theme.of(context).colorScheme.onBackground, + ), + ), + ], + ), + ); +} diff --git a/lib/ui/pages/providers/providers.dart b/lib/ui/pages/providers/providers.dart index 64b87d34..5b2285f3 100644 --- a/lib/ui/pages/providers/providers.dart +++ b/lib/ui/pages/providers/providers.dart @@ -49,6 +49,8 @@ class _ProvidersPageState extends State { return StateType.stable; } + bool isClickable() => getServerStatus() != StateType.uninitialized; + StateType getDnsStatus() { if (dnsStatus == DnsRecordsStatus.uninitialized || dnsStatus == DnsRecordsStatus.refreshing) { @@ -83,7 +85,9 @@ class _ProvidersPageState extends State { subtitle: diskStatus.isDiskOkay ? 'storage.status_ok'.tr() : 'storage.status_error'.tr(), - onTap: () => context.pushRoute(const ServerDetailsRoute()), + onTap: isClickable() + ? () => context.pushRoute(const ServerDetailsRoute()) + : null, ), const SizedBox(height: 16), _Card( @@ -93,7 +97,9 @@ class _ProvidersPageState extends State { subtitle: appConfig.isDomainSelected ? appConfig.serverDomain!.domainName : '', - onTap: () => context.pushRoute(const DnsDetailsRoute()), + onTap: isClickable() + ? () => context.pushRoute(const DnsDetailsRoute()) + : null, ), const SizedBox(height: 16), _Card( @@ -103,7 +109,9 @@ class _ProvidersPageState extends State { icon: BrandIcons.save, title: 'backup.card_title'.tr(), subtitle: isBackupInitialized ? 'backup.card_subtitle'.tr() : '', - onTap: () => context.pushRoute(const BackupDetailsRoute()), + onTap: isClickable() + ? () => context.pushRoute(const BackupDetailsRoute()) + : null, ), ], ), diff --git a/lib/ui/pages/services/services.dart b/lib/ui/pages/services/services.dart index 08446e78..36af585f 100644 --- a/lib/ui/pages/services/services.dart +++ b/lib/ui/pages/services/services.dart @@ -7,9 +7,10 @@ import 'package:selfprivacy/logic/cubit/services/services_cubit.dart'; import 'package:selfprivacy/logic/models/service.dart'; import 'package:selfprivacy/logic/models/state_types.dart'; import 'package:selfprivacy/ui/components/brand_header/brand_header.dart'; +import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; import 'package:selfprivacy/ui/components/icon_status_mask/icon_status_mask.dart'; -import 'package:selfprivacy/ui/components/not_ready_card/not_ready_card.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:selfprivacy/ui/helpers/empty_page_placeholder.dart'; import 'package:selfprivacy/ui/router/router.dart'; import 'package:selfprivacy/utils/breakpoints.dart'; import 'package:selfprivacy/utils/launch_url.dart'; @@ -42,30 +43,36 @@ class _ServicesPageState extends State { ), ) : null, - body: RefreshIndicator( - onRefresh: () async { - await context.read().reload(); - }, - child: ListView( - padding: paddingH15V0, - children: [ - Text( - 'basis.services_title'.tr(), - style: Theme.of(context).textTheme.bodyLarge, - ), - const SizedBox(height: 24), - if (!isReady) ...[const NotReadyCard(), const SizedBox(height: 24)], - ...services.map( - (final service) => Padding( - padding: const EdgeInsets.only( - bottom: 30, - ), - child: _Card(service: service), - ), + body: !isReady + ? EmptyPagePlaceholder( + showReadyCard: true, + title: 'service_page.nothing_here'.tr(), + description: 'basis.please_connect'.tr(), + iconData: BrandIcons.box, ) - ], - ), - ), + : RefreshIndicator( + onRefresh: () async { + await context.read().reload(); + }, + child: ListView( + padding: paddingH15V0, + children: [ + Text( + 'basis.services_title'.tr(), + style: Theme.of(context).textTheme.bodyLarge, + ), + const SizedBox(height: 24), + ...services.map( + (final service) => Padding( + padding: const EdgeInsets.only( + bottom: 30, + ), + child: _Card(service: service), + ), + ) + ], + ), + ), ); } } @@ -129,16 +136,16 @@ class _Card extends StatelessWidget { ), ), ), + const SizedBox(width: 8), + Text( + service.displayName, + style: Theme.of(context).textTheme.headlineMedium, + ), ], ), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox(height: 12), - Text( - service.displayName, - style: Theme.of(context).textTheme.headlineMedium, - ), const SizedBox(height: 8), if (service.url != '' && service.url != null) Column( @@ -146,7 +153,7 @@ class _Card extends StatelessWidget { _ServiceLink( url: service.url ?? '', ), - const SizedBox(height: 10), + const SizedBox(height: 8), ], ), if (service.id == 'mailserver') @@ -156,21 +163,21 @@ class _Card extends StatelessWidget { url: domainName, isActive: false, ), - const SizedBox(height: 10), + const SizedBox(height: 8), ], ), Text( service.description, style: Theme.of(context).textTheme.bodyMedium, ), - const SizedBox(height: 10), + const SizedBox(height: 8), Text( service.loginInfo, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of(context).colorScheme.secondary, ), ), - const SizedBox(height: 10), + const SizedBox(height: 8), ], ) ], diff --git a/lib/ui/pages/users/empty.dart b/lib/ui/pages/users/empty.dart deleted file mode 100644 index f58dc740..00000000 --- a/lib/ui/pages/users/empty.dart +++ /dev/null @@ -1,73 +0,0 @@ -part of 'users.dart'; - -class _NoUsers extends StatelessWidget { - const _NoUsers({required this.text}); - - final String text; - - @override - Widget build(final BuildContext context) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - BrandIcons.users, - size: 50, - color: Theme.of(context).colorScheme.onBackground, - ), - const SizedBox(height: 20), - Text( - 'users.nobody_here'.tr(), - style: Theme.of(context).textTheme.headlineMedium?.copyWith( - color: Theme.of(context).colorScheme.onBackground, - ), - ), - const SizedBox(height: 10), - Text( - text, - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.titleSmall?.copyWith( - color: Theme.of(context).colorScheme.onBackground, - ), - ), - ], - ), - ); -} - -class _CouldNotLoadUsers extends StatelessWidget { - const _CouldNotLoadUsers({required this.text}); - - final String text; - - @override - Widget build(final BuildContext context) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - BrandIcons.users, - size: 50, - color: Theme.of(context).colorScheme.onBackground, - ), - const SizedBox(height: 20), - Text( - 'users.could_not_fetch_users'.tr(), - style: Theme.of(context).textTheme.headlineMedium?.copyWith( - color: Theme.of(context).colorScheme.onBackground, - ), - ), - const SizedBox(height: 10), - Text( - text, - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.titleSmall?.copyWith( - color: Theme.of(context).colorScheme.onBackground, - ), - ), - ], - ), - ); -} diff --git a/lib/ui/pages/users/users.dart b/lib/ui/pages/users/users.dart index 1ea9d76c..a3c0d319 100644 --- a/lib/ui/pages/users/users.dart +++ b/lib/ui/pages/users/users.dart @@ -16,17 +16,16 @@ import 'package:selfprivacy/ui/components/buttons/brand_button.dart'; import 'package:selfprivacy/ui/components/buttons/outlined_button.dart'; import 'package:selfprivacy/ui/components/cards/filled_card.dart'; import 'package:selfprivacy/ui/components/brand_header/brand_header.dart'; +import 'package:selfprivacy/ui/helpers/empty_page_placeholder.dart'; import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart'; import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; import 'package:selfprivacy/ui/components/info_box/info_box.dart'; import 'package:selfprivacy/ui/components/list_tiles/list_tile_on_surface_variant.dart'; -import 'package:selfprivacy/ui/components/not_ready_card/not_ready_card.dart'; import 'package:selfprivacy/ui/router/router.dart'; import 'package:selfprivacy/utils/breakpoints.dart'; import 'package:selfprivacy/utils/platform_adapter.dart'; import 'package:selfprivacy/utils/ui_helpers.dart'; -part 'empty.dart'; part 'new_user.dart'; part 'user.dart'; part 'user_details.dart'; @@ -43,7 +42,12 @@ class UsersPage extends StatelessWidget { Widget child; if (!isReady) { - child = isNotReady(); + child = EmptyPagePlaceholder( + showReadyCard: true, + title: 'users.nobody_here'.tr(), + description: 'basis.please_connect'.tr(), + iconData: BrandIcons.users, + ); } else { child = BlocBuilder( builder: (final BuildContext context, final UsersState state) { @@ -64,8 +68,10 @@ class UsersPage extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - _CouldNotLoadUsers( - text: 'users.could_not_fetch_description'.tr(), + EmptyPagePlaceholder( + title: 'users.could_not_fetch_users'.tr(), + description: 'users.could_not_fetch_description'.tr(), + iconData: BrandIcons.users, ), const SizedBox(height: 18), BrandOutlinedButton( @@ -132,24 +138,4 @@ class UsersPage extends StatelessWidget { body: child, ); } - - Widget isNotReady() => Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const Padding( - padding: EdgeInsets.symmetric(horizontal: 15), - child: NotReadyCard(), - ), - Expanded( - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 15), - child: Center( - child: _NoUsers( - text: 'users.not_ready'.tr(), - ), - ), - ), - ) - ], - ); }