diff --git a/.github/workflows/analyze.yml b/.github/workflows/analyze.yml index 286bc5b41..2a8782ab5 100644 --- a/.github/workflows/analyze.yml +++ b/.github/workflows/analyze.yml @@ -17,7 +17,6 @@ jobs: - uses: subosito/flutter-action@v2 with: channel: "stable" - - # Consider passing '--fatal-infos' for slightly stricter analysis. - - name: Analyze project source - run: flutter analyze + flutter-version: "3.10.x" + - run: dart pub get + - run: flutter analyze diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 03365e26c..815c777e1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,6 +17,7 @@ jobs: - uses: subosito/flutter-action@v2 with: channel: "stable" + flutter-version: "3.10.x" - run: sudo apt update - run: sudo apt -y install git curl cmake ninja-build make clang libgtk-3-dev pkg-config - run: flutter build linux -v @@ -28,5 +29,5 @@ jobs: - uses: actions/checkout@v3 - run: sudo snap install flutter --classic - run: flutter doctor -v - - run: git -C $HOME/snap/flutter/common/flutter checkout 3.7.0 + - run: git -C $HOME/snap/flutter/common/flutter checkout 3.10.0 - run: flutter build linux -v diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index 4569499b4..bc5f985d0 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -1,5 +1,7 @@ name: cla-check -on: [pull_request_target] +on: + pull_request_target: + branches: [main] jobs: cla-check: diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index a2186fa1e..1409792b1 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -17,7 +17,8 @@ jobs: - uses: subosito/flutter-action@v2 with: channel: "stable" + flutter-version: "3.10.x" # Consider passing '--fatal-infos' for slightly stricter analysis. - name: format - run: flutter format --set-exit-if-changed . + run: dart format --set-exit-if-changed . diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7f7e3eee1..92e15f3a2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,6 +15,7 @@ jobs: - uses: subosito/flutter-action@v2 with: channel: "stable" + flutter-version: "3.10.x" - run: flutter test --coverage - uses: codecov/codecov-action@v3 with: @@ -27,10 +28,10 @@ jobs: - uses: subosito/flutter-action@v2 with: channel: "stable" - flutter-version: "3.7.x" + flutter-version: "3.10.x" - run: sudo apt update - run: sudo apt install -y clang cmake libblkid-dev libglib2.0-dev libgtk-3-dev liblzma-dev network-manager ninja-build packagekit pkg-config polkitd xfce4-notifyd xvfb - - run: sudo netplan set renderer=NetworkManager + - run: sudo cp integration_test/assets/10-network-manager.yaml /etc/netplan/ - run: sudo netplan apply - run: sudo cp integration_test/assets/packagekit-ci.pkla /var/lib/polkit-1/localauthority/50-local.d/ - run: sudo cp integration_test/assets/network-manager-ci.pkla /var/lib/polkit-1/localauthority/50-local.d/ diff --git a/integration_test/assets/10-network-manager.yaml b/integration_test/assets/10-network-manager.yaml new file mode 100644 index 000000000..b65476879 --- /dev/null +++ b/integration_test/assets/10-network-manager.yaml @@ -0,0 +1,3 @@ +network: + version: 2 + renderer: NetworkManager diff --git a/integration_test/software_test.dart b/integration_test/software_test.dart index 20899dbbc..c0fd550c0 100644 --- a/integration_test/software_test.dart +++ b/integration_test/software_test.dart @@ -48,8 +48,8 @@ void main() { await app.main([]); await tester.pumpUntil( - find.byType(StartPage), - timeout: const Duration(seconds: 80), + find.byType(ExploreAllPage), + timeout: const Duration(seconds: 120), ); await tester.pumpAndSettle(); diff --git a/lib/app/app.dart b/lib/app/app.dart index 3916bf782..4032da10a 100644 --- a/lib/app/app.dart +++ b/lib/app/app.dart @@ -23,15 +23,16 @@ import 'package:launcher_entry/launcher_entry.dart'; import 'package:provider/provider.dart'; import 'package:software/app/app_model.dart'; import 'package:software/app/app_splash_screen.dart'; +import 'package:software/app/collection/collection_page.dart'; import 'package:software/app/common/close_confirmation_dialog.dart'; import 'package:software/app/common/connectivity_notifier.dart'; import 'package:software/app/common/page_item.dart'; import 'package:software/app/common/rating_model.dart'; +import 'package:software/app/common/snap/snap_section.dart'; +import 'package:software/app/explore/explore_model.dart'; import 'package:software/app/explore/explore_page.dart'; -import 'package:software/app/installed/installed_page.dart'; import 'package:software/app/package_installer/package_installer_page.dart'; import 'package:software/app/settings/settings_page.dart'; -import 'package:software/app/updates/updates_page.dart'; import 'package:software/l10n/l10n.dart'; import 'package:software/services/appstream/appstream_service.dart'; import 'package:software/services/odrs_service.dart'; @@ -60,6 +61,13 @@ class App extends StatelessWidget { ChangeNotifierProvider( create: (_) => RatingModel(getService()), ), + ChangeNotifierProvider( + create: (_) => ExploreModel( + getService(), + getService(), + getService(), + )..init(), + ) ], child: const App(), ); @@ -119,31 +127,20 @@ class __AppState extends State<_App> { gtkNotifier.addCommandLineListener(_commandLineListener); final model = context.read(); - var closeConfirmDialogOpen = false; - model.init( - onAskForQuit: () { - if (closeConfirmDialogOpen) { - return; - } + model.init().then((_) { + setState(() => _initialized = true); + }); - closeConfirmDialogOpen = true; - showDialog( - context: context, - barrierDismissible: false, - builder: (c) { - return CloseWindowConfirmDialog( - onConfirm: () { - model.quit(); - }, - ); - }, - ).then((_) => closeConfirmDialogOpen = false); - }, - ).then((_) { - setState(() { - _initialized = true; - }); + YaruWindow.onClose(context, () { + if (!context.mounted || model.readyToQuit) { + return true; + } + return showDialog( + context: context, + barrierDismissible: false, + builder: (c) => const CloseWindowConfirmDialog(), + ).then((result) => result ?? false); }); } @@ -155,7 +152,7 @@ class __AppState extends State<_App> { .firstOrNull ?.substring(7); if (debPath != null || snapName != null) { - _initialIndex = 3; + _initialIndex = 6; } }); } @@ -169,39 +166,27 @@ class __AppState extends State<_App> { .setupNotifications(updatesAvailable: context.l10n.updateAvailable); final badgeCount = context.select((AppModel m) => m.snapChanges.length); final processing = context.select((AppModel m) => m.snapChanges.isNotEmpty); - final errorMessage = context.select((AppModel m) => m.errorMessage); final updateAmount = context.select((AppModel m) => m.updateAmount); final updatesProcessing = context.select((AppModel m) => m.updatesProcessing); final setSelectedIndex = context.select((AppModel m) => m.setSelectedIndex); final pageItems = [ + _createExplorePageItem(SnapSection.all), + _createExplorePageItem(SnapSection.productivity), + _createExplorePageItem(SnapSection.development), + _createExplorePageItem(SnapSection.games), + _createExplorePageItem(SnapSection.art_and_design), PageItem( - titleBuilder: ExplorePage.createTitle, - builder: (context) => ExplorePage.create(context, errorMessage), - iconBuilder: ExplorePage.createIcon, - ), - PageItem( - titleBuilder: InstalledPage.createTitle, - builder: (context) => InstalledPage.create(context), - iconBuilder: (context, selected) => InstalledPage.createIcon( + titleBuilder: CollectionPage.createTitle, + builder: (context) => CollectionPage.create(context), + iconBuilder: (context, selected) => CollectionPage.createIcon( context: context, selected: selected, badgeCount: badgeCount, processing: processing, - ), - ), - PageItem( - titleBuilder: UpdatesPage.createTitle, - builder: (context) => UpdatesPage.create( - context: context, - windowWidth: width, - ), - iconBuilder: (context, selected) => UpdatesPage.createIcon( - context: context, - selected: selected, - badgeCount: updateAmount, - processing: updatesProcessing, + updateCount: updateAmount, + updateProcessing: updatesProcessing, ), ), if (debPath != null || snapName != null) @@ -215,12 +200,6 @@ class __AppState extends State<_App> { iconBuilder: (context, selected) => PackageInstallerPage.createIcon(context, selected), ), - PageItem( - titleBuilder: SettingsPage.createTitle, - builder: SettingsPage.create, - iconBuilder: (context, selected) => - SettingsPage.createIcon(context, selected), - ), ]; var normalWindowSize = width > 800 && width < 1200; @@ -233,6 +212,18 @@ class __AppState extends State<_App> { return _initialized ? YaruNavigationPage( + trailing: Padding( + padding: const EdgeInsets.only(bottom: 10), + child: YaruNavigationRailItem( + icon: SettingsPage.createIcon(context, false), + label: SettingsPage.createTitle(context), + style: itemStyle, + onTap: () => showDialog( + context: context, + builder: (context) => SettingsPage.create(context), + ), + ), + ), leading: AnimatedContainer( width: normalWindowSize ? 100 @@ -258,4 +249,15 @@ class __AppState extends State<_App> { ) : const StoreSplashScreen(); } + + PageItem _createExplorePageItem(SnapSection snapSection) => PageItem( + titleBuilder: (context) => + ExplorePage.createTitle(context, snapSection), + builder: (context) => ExplorePage(section: snapSection), + iconBuilder: (context, selected) => ExplorePage.createIcon( + context: context, + selected: selected, + snapSection: snapSection, + ), + ); } diff --git a/lib/app/app_model.dart b/lib/app/app_model.dart index 3817f3755..b9855587f 100644 --- a/lib/app/app_model.dart +++ b/lib/app/app_model.dart @@ -24,9 +24,8 @@ import 'package:software/services/appstream/appstream_service.dart'; import 'package:software/services/packagekit/package_service.dart'; import 'package:software/services/snap_service.dart'; import 'package:software/services/packagekit/updates_state.dart'; -import 'package:window_manager/window_manager.dart'; -class AppModel extends SafeChangeNotifier implements WindowListener { +class AppModel extends SafeChangeNotifier { AppModel( this._snapService, this._appstreamService, @@ -74,19 +73,14 @@ class AppModel extends SafeChangeNotifier implements WindowListener { } } - int get updateAmount => _packageService.updates.length; + int get updateAmount => + _packageService.updates.length + _snapService.snapsWithUpdate.length; bool get updatesProcessing => updatesState == UpdatesState.checkingForUpdates || updatesState == UpdatesState.updating; - void Function()? _onAskForQuit; - - Future init({required void Function() onAskForQuit}) async { - _onAskForQuit = onAskForQuit; - windowManager.setPreventClose(true); - windowManager.addListener(this); - + Future init() async { try { _snapService.init(); } on SnapdException catch (e) { @@ -145,63 +139,8 @@ class AppModel extends SafeChangeNotifier implements WindowListener { notifyListeners(); } - void quit() { - windowManager.setPreventClose(false); - windowManager.close(); - } - bool get readyToQuit => updatesState == null || updatesState == UpdatesState.readyToUpdate || updatesState == UpdatesState.noUpdates; - - @override - void onWindowBlur() {} - - @override - void onWindowClose() { - if (readyToQuit) { - quit(); - } else { - if (_onAskForQuit != null) { - _onAskForQuit!(); - } - } - } - - @override - void onWindowEnterFullScreen() {} - - @override - void onWindowEvent(String eventName) {} - - @override - void onWindowFocus() {} - - @override - void onWindowLeaveFullScreen() {} - - @override - void onWindowMaximize() {} - - @override - void onWindowMinimize() {} - - @override - void onWindowMove() {} - - @override - void onWindowMoved() {} - - @override - void onWindowResize() {} - - @override - void onWindowResized() {} - - @override - void onWindowRestore() {} - - @override - void onWindowUnmaximize() {} } diff --git a/lib/app/collection/collection_model.dart b/lib/app/collection/collection_model.dart new file mode 100644 index 000000000..699f07b2b --- /dev/null +++ b/lib/app/collection/collection_model.dart @@ -0,0 +1,167 @@ +import 'dart:async'; + +import 'package:packagekit/packagekit.dart'; +import 'package:safe_change_notifier/safe_change_notifier.dart'; +import 'package:snapd/snapd.dart'; +import 'package:software/app/common/app_format.dart'; +import 'package:software/app/common/packagekit/package_model.dart'; +import 'package:software/app/common/snap/snap_sort.dart'; +import 'package:software/app/common/snap/snap_utils.dart'; +import 'package:software/services/packagekit/package_service.dart'; +import 'package:software/services/snap_service.dart'; + +class CollectionModel extends SafeChangeNotifier { + CollectionModel( + this._snapService, + this._packageService, + ); + + final SnapService _snapService; + StreamSubscription? _snapChangesSub; + StreamSubscription? _packagesChanged; + + final PackageService _packageService; + + Future init() async { + _snapChangesSub = _snapService.snapChangesInserted.listen((_) async { + if (_snapService.snapChanges.isEmpty) { + await loadSnaps(); + } + }); + _enabledAppFormats.add(AppFormat.snap); + _appFormat = AppFormat.snap; + + if (_packageService.isAvailable) { + _enabledAppFormats.add(AppFormat.packageKit); + await _packageService.getInstalledPackages(filters: _packageKitFilters); + _installedPackages = _packageService.installedPackages; + + _packagesChanged = + _packageService.installedPackagesChanged.listen((event) { + _installedPackages = _packageService.installedPackages; + notifyListeners(); + }); + + notifyListeners(); + } + await loadSnaps(); + } + + @override + void dispose() { + _snapChangesSub?.cancel(); + _packagesChanged?.cancel(); + super.dispose(); + } + + AppFormat? _appFormat; + AppFormat? get appFormat => _appFormat; + set appFormat(AppFormat? value) { + if (value == null || value == _appFormat) return; + _appFormat = value; + notifyListeners(); + } + + final Set _enabledAppFormats = {}; + Set get enabledAppFormats => _enabledAppFormats; + + void setAppFormat(AppFormat value) { + if (value == _appFormat) return; + _appFormat = value; + notifyListeners(); + } + + // SNAPS + + List? get installedSnaps { + final snaps = _snapService.localSnaps; + if (snaps != null) { + sortSnaps(snapSort: snapSort, snaps: snaps); + } + return searchQuery?.isEmpty == false + ? snaps?.where((s) => s.name.contains(searchQuery!)).toList() + : snaps; + } + + List get snapsWithUpdate => _snapService.snapsWithUpdate; + + Future loadSnaps() async { + _snapService.loadLocalSnaps().then((_) => notifyListeners()); + checkingForSnapUpdates = true; + _snapService + .loadSnapsWithUpdate() + .then((_) => checkingForSnapUpdates = false); + } + + String? _searchQuery; + String? get searchQuery => _searchQuery; + void setSearchQuery(String? value) { + if (value == _searchQuery) return; + _searchQuery = value; + notifyListeners(); + } + + bool _checkingForSnapUpdates = false; + bool get checkingForSnapUpdates => _checkingForSnapUpdates; + set checkingForSnapUpdates(bool value) { + if (value == _checkingForSnapUpdates) return; + _checkingForSnapUpdates = value; + notifyListeners(); + } + + Future refreshAllSnapsWithUpdates({required String doneMessage}) => + _snapService.refreshAll(doneMessage: doneMessage); + + SnapSort _snapSort = SnapSort.name; + SnapSort get snapSort => _snapSort; + void setSnapSort(SnapSort value) { + if (value == _snapSort) return; + _snapSort = value; + notifyListeners(); + } + + // PACKAGEKIT PACKAGES + + List? _installedPackages; + List? get installedPackages { + if (!_packageService.isAvailable) { + return []; + } else { + if (searchQuery?.isEmpty ?? true) { + return _installedPackages?.toList(); + } + return _installedPackages + ?.where((e) => e.name.contains(searchQuery!)) + .toList(); + } + } + + bool? _loadPackagesWithUpdates; + bool? get loadPackagesWithUpdates => _loadPackagesWithUpdates; + void setLoadPackagesWithUpdates(bool? value) { + if (value == null || value == _loadPackagesWithUpdates) return; + _loadPackagesWithUpdates = value; + notifyListeners(); + } + + final Set _packageKitFilters = { + PackageKitFilter.installed, + PackageKitFilter.application, + PackageKitFilter.notSource, + PackageKitFilter.notDevelopment, + }; + Set get packageKitFilters => _packageKitFilters; + Future handleFilter(bool value, PackageKitFilter filter) async { + if (!_packageService.isAvailable) return; + if (value) { + _packageKitFilters.add(filter); + } else { + _packageKitFilters.remove(filter); + } + await _packageService.getInstalledPackages(filters: packageKitFilters); + notifyListeners(); + } + + Future remove(PackageModel model) => + _packageService.remove(model: model); +} diff --git a/lib/app/collection/collection_page.dart b/lib/app/collection/collection_page.dart new file mode 100644 index 000000000..65400ed0e --- /dev/null +++ b/lib/app/collection/collection_page.dart @@ -0,0 +1,322 @@ +import 'package:badges/badges.dart' as badges; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:software/app/collection/collection_model.dart'; +import 'package:software/app/collection/collection_toggle.dart'; +import 'package:software/app/collection/package_collection.dart'; +import 'package:software/app/collection/package_updates_model.dart'; +import 'package:software/app/collection/snap_collection.dart'; +import 'package:software/app/common/app_format.dart'; +import 'package:software/app/common/constants.dart'; +import 'package:software/app/common/indeterminate_circular_progress_icon.dart'; +import 'package:software/app/common/packagekit/packagekit_filter_button.dart'; +import 'package:software/app/common/search_field.dart'; +import 'package:software/app/common/snap/snap_sort_popup.dart'; +import 'package:software/l10n/l10n.dart'; +import 'package:software/services/packagekit/package_service.dart'; +import 'package:software/services/packagekit/updates_state.dart'; +import 'package:software/services/snap_service.dart'; +import 'package:ubuntu_service/ubuntu_service.dart'; +import 'package:ubuntu_session/ubuntu_session.dart'; +import 'package:yaru_icons/yaru_icons.dart'; +import 'package:yaru_widgets/yaru_widgets.dart'; + +class CollectionPage extends StatefulWidget { + const CollectionPage({super.key}); + + static Widget create(BuildContext context) { + return MultiProvider( + providers: [ + ChangeNotifierProvider( + create: (context) => CollectionModel( + getService(), + getService(), + )..init(), + ), + ChangeNotifierProvider( + create: (_) => PackageUpdatesModel( + getService(), + getService(), + ), + ) + ], + child: const CollectionPage(), + ); + } + + static Widget createIcon({ + required BuildContext context, + required bool selected, + int? badgeCount, + bool? processing, + int? updateCount, + bool? updateProcessing, + }) { + return _CollectionIcon( + count: (badgeCount ?? 0) + (updateCount ?? 0), + processing: (processing ?? false) || (updateProcessing ?? false), + ); + } + + static Widget createTitle(BuildContext context) => Text(context.l10n.manage); + + @override + State createState() => _CollectionPageState(); +} + +class _CollectionPageState extends State { + late ScrollController _controller; + bool _showFab = false; + late int _packageAmount; + + @override + void initState() { + super.initState(); + _packageAmount = 30; + + _controller = ScrollController(); + _controller.addListener(() { + if (_controller.position.maxScrollExtent == _controller.offset) { + setState(() { + _packageAmount++; + }); + } + if (_controller.offset > 50.0) { + setState(() => _showFab = true); + } else { + setState(() => _showFab = false); + } + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final searchQuery = context.select((CollectionModel m) => m.searchQuery); + final setSearchQuery = + context.select((CollectionModel m) => m.setSearchQuery); + final appFormat = context.select((CollectionModel m) => m.appFormat); + final setAppFormat = context.select((CollectionModel m) => m.setAppFormat); + final enabledAppFormats = + context.select((CollectionModel m) => m.enabledAppFormats); + + final loadSnaps = context.select((CollectionModel m) => m.loadSnaps); + final snapsWithUpdate = + context.select((CollectionModel m) => m.snapsWithUpdate); + final checkingForSnapUpdates = + context.select((CollectionModel m) => m.checkingForSnapUpdates); + final refreshAllSnapsWithUpdates = + context.select((CollectionModel m) => m.refreshAllSnapsWithUpdates); + final snapSort = context.select((CollectionModel m) => m.snapSort); + final setSnapSort = context.select((CollectionModel m) => m.setSnapSort); + + final packageKitFilters = + context.select((CollectionModel m) => m.packageKitFilters); + final handleFilter = context.select((CollectionModel m) => m.handleFilter); + + final checkForPackageUpdates = + context.select((PackageUpdatesModel m) => m.refresh); + final checkingForPackageUpdates = context.select( + (PackageUpdatesModel m) => + m.updatesState == UpdatesState.checkingForUpdates, + ); + final updateAllPackages = + context.select((PackageUpdatesModel m) => m.updateAll); + final selectedUpdatesLength = + context.select((PackageUpdatesModel m) => m.selectedUpdatesLength); + final availablePackageUpdatesLength = + context.select((PackageUpdatesModel m) => m.updates.length); + + final snapChildren = [ + SnapSortPopup( + value: snapSort, + onSelected: (value) => setSnapSort(value), + ), + OutlinedButton( + onPressed: checkingForSnapUpdates == true ? null : () => loadSnaps(), + child: Text(context.l10n.refreshButton), + ), + if (checkingForSnapUpdates == true) + const _ProgressIndicator() + else if (snapsWithUpdate.isNotEmpty) + ElevatedButton( + onPressed: () => refreshAllSnapsWithUpdates( + doneMessage: context.l10n.done, + ), + child: Text( + '${context.l10n.updateButton} (${snapsWithUpdate.length})', + ), + ), + ]; + + final packageKitChildren = [ + PackageKitFilterButton( + onTap: handleFilter, + filters: packageKitFilters, + ), + OutlinedButton( + onPressed: + checkingForPackageUpdates ? null : () => checkForPackageUpdates(), + child: Text(context.l10n.refreshButton), + ), + if (checkingForPackageUpdates) + const _ProgressIndicator() + else + ElevatedButton( + onPressed: selectedUpdatesLength == 0 + ? null + : () => updateAllPackages( + updatesComplete: context.l10n.updatesComplete, + updatesAvailable: context.l10n.updateAvailable, + ), + child: Text( + '${context.l10n.updateButton} ($selectedUpdatesLength)', + ), + ), + ]; + + final floatingActionButton = FloatingActionButton( + foregroundColor: theme.colorScheme.onInverseSurface, + backgroundColor: theme.colorScheme.inverseSurface, + shape: const CircleBorder(), + onPressed: () => _controller.animateTo( + 0, + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOutCubic, + ), + child: const Icon(YaruIcons.pan_up), + ); + + final content = Center( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(20.0), + child: Wrap( + spacing: 10, + runSpacing: 20, + alignment: WrapAlignment.start, + runAlignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + CollectionToggle( + onSelected: (appFormat) => setAppFormat(appFormat), + appFormat: appFormat ?? AppFormat.snap, + enabledAppFormats: enabledAppFormats, + badgedAppFormats: { + AppFormat.snap: snapsWithUpdate.length, + AppFormat.packageKit: availablePackageUpdatesLength, + }, + ), + if (appFormat == AppFormat.snap) + ...snapChildren + else + ...packageKitChildren + ], + ), + ), + Expanded( + child: Stack( + children: [ + SingleChildScrollView( + controller: _controller, + child: (appFormat == AppFormat.snap) + ? const SnapCollection() + : PackageCollection( + enabled: !checkingForPackageUpdates, + amount: _packageAmount, + ), + ), + if (_showFab) + Align( + alignment: Alignment.bottomRight, + child: Padding( + padding: const EdgeInsets.all(kYaruPagePadding), + child: floatingActionButton, + ), + ) + ], + ), + ) + ], + ), + ); + + return Scaffold( + appBar: YaruWindowTitleBar( + leading: const SizedBox(width: kLeadingGap), + title: SearchField( + searchQuery: searchQuery ?? '', + onChanged: setSearchQuery, + hintText: context.l10n.searchHintInstalled, + ), + ), + body: content, + ); + } +} + +class _ProgressIndicator extends StatelessWidget { + const _ProgressIndicator(); + + @override + Widget build(BuildContext context) { + return const SizedBox( + height: 25, + width: 25, + child: Center( + child: YaruCircularProgressIndicator(strokeWidth: 3), + ), + ); + } +} + +class _CollectionIcon extends StatelessWidget { + const _CollectionIcon({ + // ignore: unused_element + super.key, + required this.count, + required this.processing, + }); + + final int count; + final bool processing; + + @override + Widget build(BuildContext context) { + const icon = Icon(YaruIcons.app_grid); + final theme = Theme.of(context); + if (processing && count > 0) { + return badges.Badge( + position: badges.BadgePosition.topEnd(), + badgeColor: count > 0 ? theme.primaryColor : Colors.transparent, + badgeContent: count > 0 + ? Text( + count.toString(), + style: badgeTextStyle, + ) + : null, + child: const IndeterminateCircularProgressIcon(), + ); + } else if (processing && count == 0) { + return const IndeterminateCircularProgressIcon(); + } else if (!processing && count > 0) { + return badges.Badge( + badgeColor: theme.primaryColor, + badgeContent: Text( + count.toString(), + style: badgeTextStyle, + ), + child: icon, + ); + } + return icon; + } +} diff --git a/lib/app/collection/collection_tile.dart b/lib/app/collection/collection_tile.dart new file mode 100644 index 000000000..7c6722e5d --- /dev/null +++ b/lib/app/collection/collection_tile.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; +import 'package:software/app/common/app_icon.dart'; +import 'package:yaru_widgets/yaru_widgets.dart'; + +enum CollectionTilePosition { + top, + middle, + bottom, + only; +} + +const _kRadius = 10.0; + +const _kTopChildShape = RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(_kRadius), + topRight: Radius.circular(_kRadius), + ), +); + +const _kBottomChildShape = RoundedRectangleBorder( + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(_kRadius), + bottomRight: Radius.circular(_kRadius), + ), +); + +const _kOnlyChildShape = RoundedRectangleBorder( + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(_kRadius), + bottomRight: Radius.circular(_kRadius), + topLeft: Radius.circular(_kRadius), + topRight: Radius.circular(_kRadius), + ), +); + +RoundedRectangleBorder? createTileShape( + CollectionTilePosition collectionTilePosition, +) { + return collectionTilePosition == CollectionTilePosition.middle + ? null + : (collectionTilePosition == CollectionTilePosition.top + ? _kTopChildShape + : (collectionTilePosition == CollectionTilePosition.bottom + ? _kBottomChildShape + : _kOnlyChildShape)); +} + +class CollectionTile extends StatelessWidget { + const CollectionTile({ + super.key, + this.enabled = true, + required this.collectionTilePosition, + this.iconUrl, + required this.name, + required this.onTap, + this.trailing, + }); + + final bool enabled; + final CollectionTilePosition collectionTilePosition; + final String? iconUrl; + final String name; + final void Function() onTap; + final Widget? trailing; + + @override + Widget build(BuildContext context) { + return ListTile( + shape: createTileShape(collectionTilePosition), + key: key, + enabled: enabled, + contentPadding: const EdgeInsets.symmetric( + horizontal: kYaruPagePadding, + vertical: 10, + ), + onTap: onTap, + leading: AppIcon( + iconUrl: iconUrl, + size: 25, + ), + title: Text( + name, + ), + trailing: trailing, + ); + } +} diff --git a/lib/app/collection/collection_toggle.dart b/lib/app/collection/collection_toggle.dart new file mode 100644 index 000000000..d4f09b21e --- /dev/null +++ b/lib/app/collection/collection_toggle.dart @@ -0,0 +1,82 @@ +import 'package:badges/badges.dart' as badges; +import 'package:flutter/material.dart'; +import 'package:software/app/common/app_format.dart'; +import 'package:software/app/common/app_page/app_format_toggle_buttons.dart'; +import 'package:yaru_widgets/yaru_widgets.dart'; + +class CollectionToggle extends StatelessWidget { + const CollectionToggle({ + super.key, + required this.onSelected, + required this.appFormat, + required this.enabledAppFormats, + this.badgedAppFormats, + }); + + final void Function(AppFormat appFormat) onSelected; + final AppFormat appFormat; + final Set enabledAppFormats; + final Map? badgedAppFormats; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + if (!enabledAppFormats.contains(AppFormat.packageKit)) { + return YaruBorderContainer( + color: theme.colorScheme.outline, + padding: const EdgeInsets.symmetric(horizontal: 5), + borderRadius: + const BorderRadius.all(Radius.circular(kYaruButtonRadius)), + child: const SizedBox( + height: 39, + child: AppFormatLabel( + appFormat: AppFormat.snap, + isSelected: true, + ), + ), + ); + } + + var appFormatToggleButtons = AppFormatToggleButtons( + onPressed: (index) => onSelected( + index == 0 ? AppFormat.snap : AppFormat.packageKit, + ), + isSelected: [ + appFormat == AppFormat.snap, + appFormat == AppFormat.packageKit + ], + ); + + if (badgedAppFormats == null) { + return appFormatToggleButtons; + } + + Widget toggle() { + if (badgedAppFormats![AppFormat.snap] != null && + badgedAppFormats![AppFormat.snap]! > 0) { + return badges.Badge( + position: badges.BadgePosition.topStart(start: -3, top: -3), + badgeColor: theme.primaryColor, + showBadge: appFormat == AppFormat.packageKit, + child: appFormatToggleButtons, + ); + } else { + if (badgedAppFormats![AppFormat.packageKit] != null && + badgedAppFormats![AppFormat.packageKit]! > 0) { + return badges.Badge( + animationType: badges.BadgeAnimationType.fade, + position: badges.BadgePosition.topEnd(top: -3, end: -3), + badgeColor: theme.primaryColor, + showBadge: appFormat == AppFormat.snap, + child: appFormatToggleButtons, + ); + } else { + return appFormatToggleButtons; + } + } + } + + return toggle(); + } +} diff --git a/lib/app/updates/no_updates_page.dart b/lib/app/collection/no_updates_page.dart similarity index 100% rename from lib/app/updates/no_updates_page.dart rename to lib/app/collection/no_updates_page.dart diff --git a/lib/app/collection/package_collection.dart b/lib/app/collection/package_collection.dart new file mode 100644 index 000000000..ff9786372 --- /dev/null +++ b/lib/app/collection/package_collection.dart @@ -0,0 +1,135 @@ +import 'package:flutter/material.dart'; +import 'package:packagekit/packagekit.dart'; +import 'package:provider/provider.dart'; +import 'package:software/app/collection/collection_model.dart'; +import 'package:software/app/collection/collection_tile.dart'; +import 'package:software/app/collection/package_updates_page.dart'; +import 'package:software/app/common/border_container.dart'; +import 'package:software/app/common/packagekit/package_controls.dart'; +import 'package:software/app/common/packagekit/package_model.dart'; +import 'package:software/app/common/packagekit/package_page.dart'; +import 'package:software/services/packagekit/package_service.dart'; +import 'package:ubuntu_service/ubuntu_service.dart'; +import 'package:yaru_widgets/yaru_widgets.dart'; + +class PackageCollection extends StatelessWidget { + const PackageCollection({super.key, this.enabled = true, this.amount = 40}); + final bool enabled; + final int amount; + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + children: [ + const PackageUpdatesPage(), + _InstalledPackagesList( + enabled: enabled, + amount: amount, + ) + ], + ), + ); + } +} + +class _InstalledPackagesList extends StatelessWidget { + const _InstalledPackagesList({required this.enabled, required this.amount}); + + final bool enabled; + final int amount; + + @override + Widget build(BuildContext context) { + final installedPackages = + context.select((CollectionModel m) => m.installedPackages ?? []); + + return installedPackages.isNotEmpty + ? BorderContainer( + padding: EdgeInsets.zero, + margin: const EdgeInsets.only( + left: kYaruPagePadding, + right: kYaruPagePadding, + bottom: kYaruPagePadding, + ), + child: ListView.builder( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: installedPackages.take(amount).length, + itemBuilder: (context, index) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + _PackageTile.create( + context, + installedPackages[index], + installedPackages.length == 1 + ? CollectionTilePosition.only + : (index == 0 + ? CollectionTilePosition.top + : (index == installedPackages.length - 1 + ? CollectionTilePosition.bottom + : CollectionTilePosition.middle)), + enabled, + ), + if ((index == 0 && installedPackages.length > 1) || + (index != installedPackages.length - 1)) + const Divider( + thickness: 0.0, + height: 0, + ) + ], + ); + }, + ), + ) + : const SizedBox(); + } +} + +class _PackageTile extends StatelessWidget { + const _PackageTile({ + required this.id, + required this.tileShape, + this.enabled = true, + }); + + final PackageKitPackageId id; + final CollectionTilePosition tileShape; + final bool enabled; + + static Widget create( + BuildContext context, + PackageKitPackageId id, + CollectionTilePosition tileShape, + bool enabled, + ) { + return ChangeNotifierProvider( + key: ValueKey(id), + create: (_) => + PackageModel(packageId: id, service: getService()) + ..isInstalled = true, + child: _PackageTile( + enabled: enabled, + id: id, + tileShape: tileShape, + ), + ); + } + + @override + Widget build(BuildContext context) { + return CollectionTile( + enabled: enabled, + collectionTilePosition: tileShape, + name: id.name, + key: ValueKey(id), + trailing: const PackageControls(), + onTap: () => PackagePage.push( + context, + id: id, + enableSearch: false, + ), + ); + } +} diff --git a/lib/app/updates/update_banner.dart b/lib/app/collection/package_update_banner.dart similarity index 81% rename from lib/app/updates/update_banner.dart rename to lib/app/collection/package_update_banner.dart index 4747216a4..8457f175e 100644 --- a/lib/app/updates/update_banner.dart +++ b/lib/app/collection/package_update_banner.dart @@ -17,14 +17,15 @@ import 'package:flutter/material.dart'; import 'package:packagekit/packagekit.dart'; +import 'package:software/app/common/app_icon.dart'; import 'package:software/app/common/constants.dart'; -import 'package:software/app/updates/update_dialog.dart'; +import 'package:software/app/collection/package_update_dialog.dart'; import 'package:yaru_colors/yaru_colors.dart'; import 'package:yaru_icons/yaru_icons.dart'; import 'package:yaru_widgets/yaru_widgets.dart'; -class UpdateBanner extends StatelessWidget { - const UpdateBanner({ +class PackageUpdateBanner extends StatelessWidget { + const PackageUpdateBanner({ super.key, required this.selected, this.onChanged, @@ -41,12 +42,10 @@ class UpdateBanner extends StatelessWidget { @override Widget build(BuildContext context) { return ListTile( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), + contentPadding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 6), onTap: () => showDialog( context: context, - builder: (_) => UpdateDialog.create( + builder: (_) => PackageUpdateDialog.create( context: context, id: updateId, installedId: installedId, @@ -82,12 +81,11 @@ class UpdateBanner extends StatelessWidget { leading: group == PackageKitGroup.system || group == PackageKitGroup.security ? const _SystemUpdateIcon() - : Padding( - padding: const EdgeInsets.only(bottom: 5), - child: Icon( - YaruIcons.package_deb_filled, - size: 50, - color: Colors.brown[300], + : const Padding( + padding: EdgeInsets.only(bottom: 8, left: 4), + child: AppIcon( + iconUrl: null, + size: 25, ), ), trailing: YaruCheckbox( @@ -109,11 +107,11 @@ class _SystemUpdateIcon extends StatelessWidget { return Stack( children: [ Positioned( - bottom: 2, - left: 13, + bottom: 5, + left: 8, child: Container( - height: 25, - width: 25, + height: 18, + width: 18, decoration: const BoxDecoration( shape: BoxShape.circle, color: Colors.white, @@ -122,23 +120,23 @@ class _SystemUpdateIcon extends StatelessWidget { ), const Icon( YaruIcons.ubuntu_logo_large, - size: 50, + size: 35, color: YaruColors.orange, ), Positioned( - top: -1, - right: 2, + top: -2, + right: -2, child: Icon( - YaruIcons.shield, + YaruIcons.shield_filled, size: 26, color: Colors.white.withOpacity(0.8), ), ), Positioned( - top: 0, - right: 2, + top: -1, + right: -1, child: Icon( - YaruIcons.shield, + YaruIcons.shield_filled, size: 25, color: Colors.amber[800], ), diff --git a/lib/app/updates/update_dialog.dart b/lib/app/collection/package_update_dialog.dart similarity index 92% rename from lib/app/updates/update_dialog.dart rename to lib/app/collection/package_update_dialog.dart index f346ead87..e72e73d58 100644 --- a/lib/app/updates/update_dialog.dart +++ b/lib/app/collection/package_update_dialog.dart @@ -28,9 +28,10 @@ import 'package:url_launcher/url_launcher.dart'; import 'package:yaru_icons/yaru_icons.dart'; import 'package:yaru_widgets/yaru_widgets.dart'; import 'package:software/app/common/border_container.dart'; +import 'package:software/app/common/link.dart'; -class UpdateDialog extends StatefulWidget { - const UpdateDialog({ +class PackageUpdateDialog extends StatefulWidget { + const PackageUpdateDialog({ super.key, required this.id, required this.installedId, @@ -48,7 +49,7 @@ class UpdateDialog extends StatefulWidget { return ChangeNotifierProvider( create: (context) => PackageModel(service: getService(), packageId: id), - child: UpdateDialog( + child: PackageUpdateDialog( id: id, installedId: installedId, ), @@ -56,16 +57,19 @@ class UpdateDialog extends StatefulWidget { } @override - State createState() => _UpdateDialogState(); + State createState() => _PackageUpdateDialogState(); } -class _UpdateDialogState extends State { +class _PackageUpdateDialogState extends State { @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; - context.read().init(getUpdateDetail: true); + context.read().init( + getUpdateDetail: true, + getDependencies: false, + ); }); } @@ -97,6 +101,7 @@ class _UpdateDialogState extends State { child: Padding( padding: const EdgeInsets.only(bottom: 16), child: BorderContainer( + width: double.infinity, child: MarkdownBody( data: model.changelog.length > 4000 ? '${model.changelog.substring(0, 4000)}\n\n ... ${context.l10n.changelogTooLong} ${model.url}' @@ -105,6 +110,9 @@ class _UpdateDialogState extends State { selectable: true, onTapLink: (text, href, title) => href != null ? launchUrl(Uri.parse(href)) : null, + styleSheet: MarkdownStyleSheet( + a: TextStyle(color: context.linkColor), + ), ), ), ), diff --git a/lib/app/updates/package_updates_model.dart b/lib/app/collection/package_updates_model.dart similarity index 99% rename from lib/app/updates/package_updates_model.dart rename to lib/app/collection/package_updates_model.dart index def86f7c6..f502395ee 100644 --- a/lib/app/updates/package_updates_model.dart +++ b/lib/app/collection/package_updates_model.dart @@ -96,7 +96,7 @@ class PackageUpdatesModel extends SafeChangeNotifier { notifyListeners(); }); - _service.getInstalledPackages(); + _service.getInstalledPackages(forUpdates: true); if (loadRepoList == true) { _service.loadRepoList(); } diff --git a/lib/app/collection/package_updates_page.dart b/lib/app/collection/package_updates_page.dart new file mode 100644 index 000000000..19fef0afa --- /dev/null +++ b/lib/app/collection/package_updates_page.dart @@ -0,0 +1,286 @@ +/* + * Copyright (C) 2022 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:software/app/collection/package_update_banner.dart'; +import 'package:software/app/collection/package_updates_model.dart'; +import 'package:software/app/common/border_container.dart'; +import 'package:software/app/common/constants.dart'; +import 'package:software/app/common/message_bar.dart'; +import 'package:software/l10n/l10n.dart'; +import 'package:software/services/packagekit/updates_state.dart'; +import 'package:yaru_widgets/yaru_widgets.dart'; +import '../common/expandable_title.dart'; + +class PackageUpdatesPage extends StatefulWidget { + const PackageUpdatesPage({ + super.key, + }); + + @override + State createState() => _PackageUpdatesPageState(); +} + +class _PackageUpdatesPageState extends State { + @override + void initState() { + super.initState(); + final model = context.read(); + model.init(handleError: () => showSnackBar()); + } + + void showSnackBar() { + if (!mounted) return; + final model = context.read(); + if (model.errorMessage.isNotEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + duration: const Duration(minutes: 1), + padding: EdgeInsets.zero, + content: MessageBar( + message: model.errorMessage, + copyMessage: context.l10n.copyErrorMessage, + ), + ), + ); + } + } + + @override + Widget build(BuildContext context) { + final model = context.watch(); + if (model.updatesState == UpdatesState.readyToUpdate) { + return const _UpdatesListView(); + } + if (model.updatesState == UpdatesState.updating) { + return const _UpdatingPage(); + } else { + return const SizedBox.shrink(); + } + } +} + +class _UpdatingPage extends StatefulWidget { + const _UpdatingPage(); + + @override + State<_UpdatingPage> createState() => _UpdatingPageState(); +} + +class _UpdatingPageState extends State<_UpdatingPage> { + @override + Widget build(BuildContext context) { + final model = context.watch(); + + final children = [ + const SizedBox( + height: 50, + ), + Text( + model.info != null ? model.info!.name : '', + style: Theme.of(context).textTheme.headlineMedium, + ), + const SizedBox( + height: 20, + ), + Text( + model.processedId != null ? model.processedId!.name : '', + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox( + height: 20, + ), + YaruLinearProgressIndicator( + value: model.percentage != null ? model.percentage! / 100 : 0, + ), + const SizedBox( + height: 100, + ), + ]; + + return Center( + child: SizedBox( + width: 500, + child: Column( + children: [ + for (final child in children) + Center( + child: child, + ) + ], + ), + ), + ); + } +} + +class PackageUpdatesHeader extends StatelessWidget { + const PackageUpdatesHeader({super.key}); + + @override + Widget build(BuildContext context) { + final model = context.watch(); + + return Align( + alignment: Alignment.center, + child: Padding( + padding: const EdgeInsets.all(kPagePadding), + child: Wrap( + direction: Axis.horizontal, + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.start, + runAlignment: WrapAlignment.start, + textDirection: TextDirection.rtl, + spacing: 10, + runSpacing: 10, + children: [ + if (model.updates.isNotEmpty) + ElevatedButton( + onPressed: model.updatesState == UpdatesState.readyToUpdate && + !model.nothingSelected + ? () => model.updateAll( + updatesComplete: context.l10n.updatesComplete, + updatesAvailable: context.l10n.updateAvailable, + ) + : null, + child: Text(context.l10n.updateButton), + ), + if (model.updatesState == UpdatesState.noUpdates) + if (model.requireRestartApp) + ElevatedButton( + onPressed: () => model.exitApp(), + child: Text(context.l10n.requireRestartApp), + ) + else if (model.requireRestartSession) + ElevatedButton( + onPressed: () => model.logout(), + child: Text(context.l10n.requireRestartSession), + ) + else if (model.requireRestartSystem) + ElevatedButton( + onPressed: () => model.reboot(), + child: Text(context.l10n.requireRestartSystem), + ), + ], + ), + ), + ); + } +} + +class _UpdatesListView extends StatefulWidget { + // ignore: unused_element + const _UpdatesListView({super.key}); + + @override + State<_UpdatesListView> createState() => _UpdatesListViewState(); +} + +class _UpdatesListViewState extends State<_UpdatesListView> { + bool _isExpanded = false; + + @override + Widget build(BuildContext context) { + final model = context.watch(); + + return BorderContainer( + margin: const EdgeInsets.only( + left: kYaruPagePadding, + right: kYaruPagePadding, + bottom: kYaruPagePadding, + ), + padding: EdgeInsets.zero, + child: YaruExpandable( + expandIconPadding: const EdgeInsets.only(right: 10), + isExpanded: _isExpanded, + onChange: (isExpanded) => setState(() => _isExpanded = isExpanded), + header: MouseRegion( + cursor: SystemMouseCursors.click, + child: Padding( + padding: const EdgeInsets.only( + top: kYaruPagePadding, + left: kYaruPagePadding - 3, + bottom: 20, + right: kYaruPagePadding, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + YaruCheckbox( + value: model.allSelected + ? true + : model.nothingSelected + ? false + : null, + tristate: true, + onChanged: (v) => + v != null ? model.selectAll() : model.deselectAll(), + ), + const SizedBox( + width: 10, + ), + Expanded( + child: ExpandableContainerTitle( + '${model.selectedUpdatesLength}/${model.updates.length} ${context.l10n.xSelected}', + ), + ) + ], + ), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Divider( + thickness: 0.0, + height: 0, + ), + ListView.builder( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: model.updates.length, + itemBuilder: (context, index) { + final update = model.getUpdate(index); + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + PackageUpdateBanner( + group: model.getGroup(update), + selected: model.isUpdateSelected(update), + updateId: update, + installedId: model.getInstalledId(update.name) ?? update, + onChanged: + model.updatesState == UpdatesState.checkingForUpdates + ? null + : (v) => model.selectUpdate(update, v!), + ), + if (index != model.updates.length - 1) + const Divider( + thickness: 0.0, + height: 0, + ) + ], + ); + }, + ), + ], + ), + ), + ); + } +} diff --git a/lib/app/collection/simple_snap_controls.dart b/lib/app/collection/simple_snap_controls.dart new file mode 100644 index 000000000..f28e5e739 --- /dev/null +++ b/lib/app/collection/simple_snap_controls.dart @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2022 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:snapcraft_launcher/snapcraft_launcher.dart'; +import 'package:snapd/snapd.dart'; +import 'package:software/app/collection/simple_snap_model.dart'; +import 'package:software/app/common/constants.dart'; +import 'package:software/l10n/l10n.dart'; +import 'package:software/services/snap_service.dart'; +import 'package:software/snapd_change_x.dart'; +import 'package:ubuntu_service/ubuntu_service.dart'; +import 'package:yaru_widgets/yaru_widgets.dart'; + +class SimpleSnapControls extends StatelessWidget { + const SimpleSnapControls({ + super.key, + required this.hasUpdate, + required this.enabled, + }); + + static Widget create({ + required BuildContext context, + required Snap snap, + required bool hasUpdate, + required bool enabled, + }) { + return ChangeNotifierProvider( + create: (_) { + return SimpleSnapModel( + getService(), + getService(), + snap: snap, + )..init(); + }, + child: SimpleSnapControls( + hasUpdate: hasUpdate, + enabled: enabled, + ), + ); + } + + final bool hasUpdate; + final bool enabled; + + @override + Widget build(BuildContext context) { + final model = context.watch(); + final theme = Theme.of(context); + final light = theme.brightness == Brightness.light; + + return Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + alignment: WrapAlignment.center, + runAlignment: WrapAlignment.start, + spacing: 10, + runSpacing: 10, + children: model.change != null + ? [ + SizedBox( + height: 20, + child: YaruCircularProgressIndicator( + strokeWidth: 3, + value: model.change?.progress, + ), + ), + if (model.change != null) ...[ + Text( + getChangeMessage( + context: context, + changeKind: model.change!.kind, + ), + ), + if (model.change!.kind != 'remove-snap') + OutlinedButton( + onPressed: model.abortChange, + child: Text(context.l10n.cancel), + ), + ] + ] + : [ + if (hasUpdate) + OutlinedButton( + onPressed: model.change == null && enabled + ? () => model.refresh(context.l10n.done) + : null, + child: Text( + context.l10n.updateButton, + style: enabled + ? TextStyle( + color: light ? kGreenLight : kGreenDark, + ) + : null, + ), + ), + if (model.isLaunchable && enabled) + OutlinedButton( + onPressed: model.open, + child: Text( + context.l10n.open, + ), + ), + OutlinedButton( + onPressed: enabled + ? () { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text( + context.l10n + .removePackage(model.snap.apps.first.name), + ), + content: Text( + context.l10n.confirmRemove, + ), + actions: [ + OutlinedButton( + child: Text(context.l10n.cancel), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: + Theme.of(context).colorScheme.error, + ), + child: Text(context.l10n.remove), + onPressed: () { + model.remove(context.l10n.done); + Navigator.of(context).pop(); + }, + ), + ], + ); + }, + ); + } + : null, + child: Text(context.l10n.remove), + ), + ], + ); + } + + String getChangeMessage({ + required BuildContext context, + required String? changeKind, + }) => + switch (changeKind) { + 'install-snap' => context.l10n.installing, + 'remove-snap' => context.l10n.removing, + 'refresh-snap' => context.l10n.refreshing, + 'connect-snap' => context.l10n.changingPermissions, + 'disconnect-snap' => context.l10n.changingPermissions, + _ => '' + }; +} diff --git a/lib/app/collection/simple_snap_model.dart b/lib/app/collection/simple_snap_model.dart new file mode 100644 index 000000000..7cd03c4bd --- /dev/null +++ b/lib/app/collection/simple_snap_model.dart @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2022 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +import 'dart:async'; +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:path/path.dart'; +import 'package:safe_change_notifier/safe_change_notifier.dart'; +import 'package:snapcraft_launcher/snapcraft_launcher.dart'; +import 'package:snapd/snapd.dart'; +import 'package:software/services/snap_service.dart'; + +class SimpleSnapModel extends SafeChangeNotifier { + SimpleSnapModel( + this._snapService, + this._launcher, { + required this.snap, + }); + + Future init() async { + await _snapService.authorize(); + await _launcher.connect(); + await _loadChange(); + + _snapChangesSub = _snapService.snapChangesInserted.listen((_) async { + await _loadChange(); + + notifyListeners(); + }); + + notifyListeners(); + } + + @override + Future dispose() async { + await _snapChangesSub?.cancel(); + super.dispose(); + } + + /// The service to handle all snap related actions. + final SnapService _snapService; + + /// Snapcraft launcher that allows launching desktop snap applications. + final PrivilegedDesktopLauncher _launcher; + + /// Mainly used for the information about the install [SnapChannel] and + /// the [SnapConnection]s. It is used as a fallback for some information + /// if the snap is offline. + final Snap snap; + + /// [StreamSubscription] to listen to snap changes. + StreamSubscription? _snapChangesSub; + + /// Checks if the app is started as a snap. + bool get isSnapEnv => Platform.environment['SNAP']?.isNotEmpty == true; + + /// The first change in progress for [huskSnapName] + SnapdChange? _change; + SnapdChange? get change => _change; + set change(SnapdChange? value) { + if (value == _change) return; + _change = value; + notifyListeners(); + } + + /// Loads the first change in progress for [huskSnapName] from [SnapService] + Future _loadChange() async { + change = (await _snapService.getSnapChanges(name: snap.name)); + } + + Future abortChange() async { + await _snapService.abortChange(snap); + return _loadChange(); + } + + Future remove(String doneMessage) async { + await _snapService.remove(snap, doneMessage); + notifyListeners(); + } + + Future refresh(String doneMessage) async { + await _snapService.refresh( + snap: snap, + message: doneMessage, + channel: snap.channel, + confinement: snap.confinement, + ); + notifyListeners(); + } + + String? get _desktopFile => + snap.apps.firstWhereOrNull((app) => app.desktopFile != null)?.desktopFile; + bool get isLaunchable => + snap.type == 'app' && _desktopFile != null && _launcher.isAvailable; + + void open() { + if (_desktopFile == null) return; + _launcher.openDesktopEntry(basename(_desktopFile!)); + } +} diff --git a/lib/app/collection/snap_collection.dart b/lib/app/collection/snap_collection.dart new file mode 100644 index 000000000..581e04393 --- /dev/null +++ b/lib/app/collection/snap_collection.dart @@ -0,0 +1,146 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:software/app/collection/collection_model.dart'; +import 'package:software/app/collection/collection_tile.dart'; +import 'package:software/app/collection/simple_snap_controls.dart'; +import 'package:software/app/common/border_container.dart'; +import 'package:software/app/common/snap/snap_page.dart'; +import 'package:software/l10n/l10n.dart'; +import 'package:software/snapx.dart'; +import 'package:yaru_widgets/yaru_widgets.dart'; + +class SnapCollection extends StatelessWidget { + const SnapCollection({super.key}); + + @override + Widget build(BuildContext context) { + final installedSnaps = + context.select((CollectionModel m) => m.installedSnaps); + final snapUpdates = + context.select((CollectionModel m) => m.snapsWithUpdate); + + final checkingForSnapUpdates = + context.select((CollectionModel m) => m.checkingForSnapUpdates); + + if (checkingForSnapUpdates == false && + installedSnaps != null && + installedSnaps.isEmpty) { + return Center( + child: Text(context.l10n.noSnapsInstalled), + ); + } + + return Center( + child: Column( + children: [ + if (snapUpdates.isNotEmpty) + BorderContainer( + padding: EdgeInsets.zero, + margin: const EdgeInsets.only( + left: kYaruPagePadding, + right: kYaruPagePadding, + bottom: kYaruPagePadding, + ), + child: ListView.builder( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: snapUpdates.length, + itemBuilder: (context, i) { + final snap = snapUpdates[i]; + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + CollectionTile( + key: ValueKey(snap), + iconUrl: snap.iconUrl, + name: snap.name, + collectionTilePosition: snapUpdates.length == 1 + ? CollectionTilePosition.only + : (i == 0 + ? CollectionTilePosition.top + : (i == snapUpdates.length - 1 + ? CollectionTilePosition.bottom + : CollectionTilePosition.middle)), + enabled: checkingForSnapUpdates == false, + onTap: () => SnapPage.push( + context: context, + snap: snap, + enableSearch: false, + ), + trailing: SimpleSnapControls.create( + context: context, + snap: snap, + hasUpdate: true, + enabled: checkingForSnapUpdates == false, + ), + ), + if ((i == 0 && snapUpdates.length > 1) || + (i != snapUpdates.length - 1)) + const Divider( + thickness: 0.0, + height: 0, + ) + ], + ); + }, + ), + ), + if (installedSnaps == null) + const SizedBox.shrink() + else if (installedSnaps.isNotEmpty) + BorderContainer( + padding: EdgeInsets.zero, + margin: const EdgeInsets.only( + left: kYaruPagePadding, + right: kYaruPagePadding, + bottom: kYaruPagePadding, + ), + child: ListView.builder( + physics: const NeverScrollableScrollPhysics(), + itemCount: installedSnaps.length, + shrinkWrap: true, + itemBuilder: (context, i) { + final snap = installedSnaps[i]; + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + CollectionTile( + key: ValueKey(snap), + iconUrl: snap.iconUrl, + name: snap.name, + collectionTilePosition: installedSnaps.length == 1 + ? CollectionTilePosition.only + : (i == 0 + ? CollectionTilePosition.top + : (i == installedSnaps.length - 1 + ? CollectionTilePosition.bottom + : CollectionTilePosition.middle)), + enabled: true, + onTap: () => SnapPage.push( + context: context, + snap: snap, + enableSearch: false, + ), + trailing: SimpleSnapControls.create( + context: context, + snap: snap, + hasUpdate: false, + enabled: true, + ), + ), + if ((i == 0 && installedSnaps.length > 1) || + (i != installedSnaps.length - 1)) + const Divider( + thickness: 0.0, + height: 0, + ) + ], + ); + }, + ), + ) + ], + ), + ); + } +} diff --git a/lib/app/common/app_banner.dart b/lib/app/common/app_banner.dart index 0454ef783..5dbd12f53 100644 --- a/lib/app/common/app_banner.dart +++ b/lib/app/common/app_banner.dart @@ -15,6 +15,7 @@ * */ +import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_rating_bar/flutter_rating_bar.dart'; import 'package:provider/provider.dart'; @@ -26,7 +27,6 @@ import 'package:software/app/common/app_rating.dart'; import 'package:software/app/common/constants.dart'; import 'package:software/app/common/packagekit/package_page.dart'; import 'package:software/app/common/rating_model.dart'; -import 'package:software/app/common/safe_network_image.dart'; import 'package:software/app/common/snap/snap_page.dart'; import 'package:software/l10n/l10n.dart'; import 'package:software/services/appstream/appstream_utils.dart' @@ -41,11 +41,15 @@ class AppBanner extends StatelessWidget { required this.appFinding, required this.showSnap, required this.showPackageKit, + this.enableSearch = true, + this.preferSnap = true, }); final MapEntry appFinding; final bool showSnap; final bool showPackageKit; + final bool enableSearch; + final bool preferSnap; @override Widget build(BuildContext context) { @@ -53,17 +57,26 @@ class AppBanner extends StatelessWidget { appFinding.value.appstream != null && showSnap && showPackageKit - ? () => SnapPage.push( - context: context, - snap: appFinding.value.snap!, - appstream: appFinding.value.appstream, - ) + ? () => preferSnap + ? SnapPage.push( + context: context, + snap: appFinding.value.snap!, + appstream: appFinding.value.appstream, + enableSearch: enableSearch, + ) + : PackagePage.push( + context, + appstream: appFinding.value.appstream!, + snap: appFinding.value.snap, + enableSearch: enableSearch, + ) : () { if (appFinding.value.appstream != null && showPackageKit) { PackagePage.push( context, appstream: appFinding.value.appstream!, snap: appFinding.value.snap, + enableSearch: enableSearch, ); } if (appFinding.value.snap != null && showSnap) { @@ -71,6 +84,7 @@ class AppBanner extends StatelessWidget { context: context, snap: appFinding.value.snap!, appstream: appFinding.value.appstream, + enableSearch: enableSearch, ); } }; @@ -143,10 +157,11 @@ class AppImageBanner extends StatelessWidget { topLeft: Radius.circular(10), topRight: Radius.circular(10), ), - child: SafeNetworkImage( - fallBackIcon: fallBackLoadingIcon, - url: snap.bannerUrl, + child: CachedNetworkImage( + imageUrl: snap.bannerUrl!, fit: BoxFit.cover, + placeholder: (context, url) => fallBackLoadingIcon, + errorWidget: (context, url, error) => fallBackLoadingIcon, ), ), ), @@ -155,6 +170,13 @@ class AppImageBanner extends StatelessWidget { ), Expanded( child: YaruTile( + leading: Padding( + padding: const EdgeInsets.only(bottom: 55, right: 5), + child: AppIcon( + size: 40, + iconUrl: snap.iconUrl, + ), + ), style: YaruTileStyle.banner, padding: const EdgeInsets.only( left: 15, @@ -194,27 +216,20 @@ class SearchBannerSubtitle extends StatelessWidget { final theme = Theme.of(context); final light = theme.brightness == Brightness.light; - String? ratingId; - var publisherName = context.l10n.unknown; - - if (appFinding.snap != null && - appFinding.snap!.publisher != null && - showSnap) { - publisherName = appFinding.snap!.publisher!.displayName; - ratingId = appFinding.snap!.ratingId; - } - - if (appFinding.appstream != null && showPackageKit && !showSnap) { - publisherName = appFinding.appstream!.developerName[WidgetsBinding - .instance.window.locale.countryCode - ?.toLowerCase()] ?? - appFinding.appstream!.developerName['C'] ?? - appFinding.appstream!.localizedName(); - ratingId = appFinding.appstream!.ratingId; - } + String? ratingId = + appFinding.snap?.ratingId ?? appFinding.appstream?.ratingId; + final publisherName = appFinding.snap?.publisher?.displayName ?? + appFinding.appstream?.developerName[View.of(context) + .platformDispatcher + .locale + .countryCode + ?.toLowerCase()] ?? + appFinding.appstream?.developerName['C'] ?? + appFinding.appstream?.localizedName() ?? + context.l10n.unknown; final rating = ratingId != null - ? context.select((RatingModel m) => m.getRating(ratingId!)) + ? context.select((RatingModel m) => m.getRating(ratingId)) : null; return Column( @@ -230,8 +245,7 @@ class SearchBannerSubtitle extends StatelessWidget { maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( - fontStyle: FontStyle.italic, - color: theme.colorScheme.onSurface.withOpacity(0.5), + color: theme.hintColor, ), ), ), @@ -245,11 +259,11 @@ class SearchBannerSubtitle extends StatelessWidget { ), ), if (appFinding.snap?.starredDeveloper == true) - Padding( - padding: const EdgeInsets.only(left: 5), + const Padding( + padding: EdgeInsets.only(left: 5), child: Stack( alignment: Alignment.center, - children: const [ + children: [ Icon( Icons.circle, color: Colors.white, diff --git a/lib/app/common/app_data.dart b/lib/app/common/app_data.dart index d360297ea..93ddae014 100644 --- a/lib/app/common/app_data.dart +++ b/lib/app/common/app_data.dart @@ -16,6 +16,7 @@ */ import 'package:software/app/common/app_format.dart'; +import 'package:software/app/common/app_rating.dart'; class AppData { final String title; @@ -30,12 +31,13 @@ class AppData { final bool verified; final bool starredDeveloper; final String publisherName; + final String publisherUsername; final String website; final String contact; final List screenShotUrls; final String description; final bool versionChanged; - final double averageRating; + final AppRating? appRating; final List userReviews; final AppFormat appFormat; final String appSize; @@ -58,12 +60,13 @@ class AppData { required this.screenShotUrls, required this.description, required this.versionChanged, - required this.averageRating, + required this.appRating, required this.userReviews, required this.appFormat, required this.appSize, required this.releasedAt, required this.contact, + required this.publisherUsername, }); } diff --git a/lib/app/common/app_format.dart b/lib/app/common/app_format.dart index c5f8d31c4..fd404f190 100644 --- a/lib/app/common/app_format.dart +++ b/lib/app/common/app_format.dart @@ -24,14 +24,10 @@ enum AppFormat { packageKit; String localize(AppLocalizations l10n) { - switch (this) { - case AppFormat.snap: - return l10n.snapPackages; - case AppFormat.packageKit: - return l10n.debianPackages; - default: - return ''; - } + return switch (this) { + AppFormat.snap => l10n.snapPackages, + AppFormat.packageKit => l10n.debianPackages, + }; } } diff --git a/lib/app/common/app_format_popup.dart b/lib/app/common/app_format_popup.dart index 88ee4143c..79ced4dc0 100644 --- a/lib/app/common/app_format_popup.dart +++ b/lib/app/common/app_format_popup.dart @@ -14,42 +14,91 @@ * along with this program. If not, see . * */ - +import 'package:badges/badges.dart' as badges; import 'package:flutter/material.dart'; import 'package:software/l10n/l10n.dart'; import 'package:software/app/common/app_format.dart'; import 'package:yaru_widgets/yaru_widgets.dart'; +import 'constants.dart'; + class AppFormatPopup extends StatelessWidget { const AppFormatPopup({ super.key, required this.onSelected, required this.appFormat, required this.enabledAppFormats, + this.badgedAppFormats, }); final void Function(AppFormat appFormat) onSelected; final AppFormat appFormat; final Set enabledAppFormats; + final Map? badgedAppFormats; @override Widget build(BuildContext context) { - return YaruPopupMenuButton( - initialValue: appFormat, - tooltip: context.l10n.appFormat, - itemBuilder: (v) => [ - for (var appFormat in enabledAppFormats) - PopupMenuItem( - value: appFormat, - onTap: () => onSelected(appFormat), - child: Text( - appFormat.localize(context.l10n), - style: Theme.of(context).textTheme.bodyMedium, - ), - ) - ], - onSelected: onSelected, - child: Text(appFormat.localize(context.l10n)), + final theme = Theme.of(context); + + var isButtonBadged = false; + if (badgedAppFormats != null) { + for (var entry in badgedAppFormats!.entries) { + if (entry.key != appFormat && entry.value > 0) { + isButtonBadged = true; + break; + } + } + } + + Widget maybeBuildItemBadge({ + required AppFormat appFormat, + required Widget child, + }) { + final value = badgedAppFormats?[appFormat]; + + if (value == null || value <= 0) { + return child; + } + + return badges.Badge( + animationDuration: Duration.zero, + badgeContent: Text( + value.toString(), + style: badgeTextStyle, + ), + position: badges.BadgePosition.topEnd(top: -2, end: -30), + badgeColor: theme.primaryColor, + alignment: AlignmentDirectional.centerEnd, + child: child, + ); + } + + return badges.Badge( + position: badges.BadgePosition.topEnd(top: -3, end: -3), + badgeColor: theme.primaryColor, + showBadge: isButtonBadged, + child: YaruPopupMenuButton( + initialValue: appFormat, + tooltip: context.l10n.appFormat, + itemBuilder: (v) => [ + for (var appFormat in enabledAppFormats) + PopupMenuItem( + value: appFormat, + onTap: () => onSelected(appFormat), + child: maybeBuildItemBadge( + appFormat: appFormat, + child: Text( + appFormat.localize(context.l10n), + style: Theme.of(context).textTheme.bodyMedium, + ), + ), + ) + ], + onSelected: onSelected, + child: Text( + appFormat.localize(context.l10n), + ), + ), ); } } @@ -79,6 +128,7 @@ class MultiAppFormatPopup extends StatelessWidget { value: appFormat, checked: selectedAppFormats.contains(appFormat), child: Text( + style: Theme.of(context).textTheme.bodyMedium, appFormat.localize(context.l10n), ), ), diff --git a/lib/app/common/app_icon.dart b/lib/app/common/app_icon.dart index 219037715..23d4f7c9b 100644 --- a/lib/app/common/app_icon.dart +++ b/lib/app/common/app_icon.dart @@ -15,6 +15,7 @@ * */ +import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:shimmer/shimmer.dart'; import 'package:software/app/common/constants.dart'; @@ -54,19 +55,15 @@ class AppIcon extends StatelessWidget { : SizedBox( height: size, width: size, - child: Image.network( - iconUrl!, - filterQuality: FilterQuality.medium, - fit: BoxFit.fitHeight, - frameBuilder: (context, child, frame, wasSynchronouslyLoaded) { - return frame == null - ? fallBackLoadingIcon - : AnimatedContainer( - duration: const Duration(milliseconds: 500), - child: child, - ); - }, - errorBuilder: (context, error, stackTrace) => fallBackIcon, + child: CachedNetworkImage( + imageUrl: iconUrl!, + imageBuilder: (context, imageProvider) => Image( + image: imageProvider, + filterQuality: FilterQuality.medium, + fit: BoxFit.fitHeight, + ), + placeholder: (context, url) => fallBackLoadingIcon, + errorWidget: (context, url, error) => fallBackIcon, ), ), ); diff --git a/lib/app/common/app_page/app_description.dart b/lib/app/common/app_page/app_description.dart index ffc7f3ed0..d835f0ebf 100644 --- a/lib/app/common/app_page/app_description.dart +++ b/lib/app/common/app_page/app_description.dart @@ -21,6 +21,8 @@ import 'package:software/l10n/l10n.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:yaru_icons/yaru_icons.dart'; import 'package:yaru_widgets/yaru_widgets.dart'; +import '../expandable_title.dart'; +import 'package:software/app/common/link.dart'; class AppDescription extends StatelessWidget { const AppDescription({super.key, required this.description}); @@ -32,12 +34,12 @@ class AppDescription extends StatelessWidget { return YaruExpandable( isExpanded: true, expandIcon: const Icon(YaruIcons.pan_end), - header: Text( + header: ExpandableContainerTitle( context.l10n.description, - style: Theme.of(context).textTheme.titleLarge, ), - child: Padding( + child: Container( padding: const EdgeInsets.only(top: 20), + width: double.infinity, child: MarkdownBody( data: description, shrinkWrap: true, @@ -45,7 +47,8 @@ class AppDescription extends StatelessWidget { onTapLink: (text, href, title) => href != null ? launchUrl(Uri.parse(href)) : null, styleSheet: MarkdownStyleSheet( - p: Theme.of(context).textTheme.bodyMedium, + a: TextStyle(color: context.linkColor), + textAlign: WrapAlignment.start, ), ), ), diff --git a/lib/app/common/app_page/app_format_toggle_buttons.dart b/lib/app/common/app_page/app_format_toggle_buttons.dart index d669d825c..bd0a85e87 100644 --- a/lib/app/common/app_page/app_format_toggle_buttons.dart +++ b/lib/app/common/app_page/app_format_toggle_buttons.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:software/app/common/app_format.dart'; import 'package:software/l10n/l10n.dart'; import 'package:yaru_icons/yaru_icons.dart'; @@ -19,72 +20,76 @@ class AppFormatToggleButtons extends StatelessWidget { child: ToggleButtons( isSelected: isSelected, onPressed: onPressed, - children: const [ - SnapLabel(), - DebianLabel(), + children: [ + AppFormatLabel( + appFormat: AppFormat.snap, + isSelected: isSelected[0], + ), + AppFormatLabel( + appFormat: AppFormat.packageKit, + isSelected: isSelected[1], + ) ], ), ); } } -class SnapLabel extends StatelessWidget { - const SnapLabel({ +class AppFormatLabel extends StatelessWidget { + const AppFormatLabel({ super.key, + required this.appFormat, + required this.isSelected, }); + final AppFormat appFormat; + final bool isSelected; + @override Widget build(BuildContext context) { final theme = Theme.of(context); + return Row( mainAxisSize: MainAxisSize.min, children: [ const SizedBox( width: 10, ), - Icon( - YaruIcons.snapcraft, - color: theme.colorScheme.onSurface, - size: 16, - ), + if (appFormat == AppFormat.snap) + Icon( + YaruIcons.snapcraft, + color: theme.colorScheme.onSurface, + size: 16, + ) + else + Icon( + YaruIcons.debian, + color: theme.colorScheme.onSurface, + size: 16, + ), const SizedBox( width: 5, ), - Text(context.l10n.snapPackage), - const SizedBox( - width: 10, - ), - ], - ); - } -} - -class DebianLabel extends StatelessWidget { - const DebianLabel({ - super.key, - }); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - return Row( - mainAxisSize: MainAxisSize.min, - children: [ + if (appFormat == AppFormat.snap) + Text( + context.l10n.snapPackage, + style: isSelected + ? const TextStyle(fontWeight: FontWeight.w500) + : null, + ) + else + Text( + context.l10n.debianPackage, + style: isSelected + ? const TextStyle(fontWeight: FontWeight.w500) + : null, + ), const SizedBox( width: 10, ), - Icon( - YaruIcons.debian, - color: theme.colorScheme.onSurface, - size: 16, - ), - const SizedBox( - width: 5, - ), - Text(context.l10n.debianPackage), const SizedBox( width: 10, - ), + ) ], ); } diff --git a/lib/app/common/app_page/app_header.dart b/lib/app/common/app_page/app_header.dart index 30152899e..313d1208f 100644 --- a/lib/app/common/app_page/app_header.dart +++ b/lib/app/common/app_page/app_header.dart @@ -18,7 +18,9 @@ import 'package:flutter/material.dart'; import 'package:software/app/common/app_data.dart'; import 'package:software/app/common/app_page/publisher_name.dart'; +import 'package:yaru_icons/yaru_icons.dart'; import 'package:yaru_widgets/yaru_widgets.dart'; +import 'package:software/l10n/l10n.dart'; const headerStyle = TextStyle(fontWeight: FontWeight.w500, fontSize: 14); const iconSize = 108.0; @@ -31,6 +33,8 @@ class BannerAppHeader extends StatelessWidget { required this.icon, required this.windowSize, this.subControls, + this.onShare, + this.onPublisherSearch, }); final AppData appData; @@ -39,12 +43,14 @@ class BannerAppHeader extends StatelessWidget { final Widget icon; final Size windowSize; + final Function()? onShare; + final void Function()? onPublisherSearch; @override Widget build(BuildContext context) { final theme = Theme.of(context); return SizedBox( - height: 170, + height: 174, child: Column( children: [ Row( @@ -65,13 +71,13 @@ class BannerAppHeader extends StatelessWidget { children: [ Text( appData.title, - style: theme.textTheme.displaySmall!.copyWith( - fontSize: 20, - color: theme.colorScheme.onSurface, - ), + style: theme.textTheme.titleLarge!.copyWith(fontSize: 24), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), PublisherName( - height: 18, + onPublisherSearch: onPublisherSearch, + height: 14, publisherName: appData.publisherName, website: appData.website, verified: appData.verified, @@ -91,6 +97,12 @@ class BannerAppHeader extends StatelessWidget { ], ), ), + if (onShare != null) + YaruIconButton( + tooltip: context.l10n.share, + icon: const Icon(YaruIcons.share), + onPressed: onShare, + ) ], ), ], @@ -106,16 +118,19 @@ class PageAppHeader extends StatelessWidget { required this.controls, required this.icon, this.subControls, + this.onShare, + this.onPublisherSearch, }); final AppData appData; final Widget controls; final Widget icon; final Widget? subControls; + final Function()? onShare; + final void Function()? onPublisherSearch; @override Widget build(BuildContext context) { - final scaledFontSize = (800 / appData.title.length.toDouble()); final theme = Theme.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.center, @@ -140,16 +155,15 @@ class PageAppHeader extends StatelessWidget { children: [ Text( appData.title, - style: theme.textTheme.displaySmall!.copyWith( - fontSize: scaledFontSize > 44 ? 44 : scaledFontSize, - color: theme.colorScheme.onSurface, + style: theme.textTheme.titleLarge!.copyWith( + fontSize: 24, ), - overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, ), Center( child: PublisherName( - height: 20, + onPublisherSearch: onPublisherSearch, + height: 14, publisherName: appData.publisherName, website: appData.website, verified: appData.verified, @@ -161,6 +175,12 @@ class PageAppHeader extends StatelessWidget { ], ), ), + if (onShare != null) + YaruIconButton( + tooltip: context.l10n.share, + icon: const Icon(YaruIcons.share), + onPressed: onShare, + ) ], ), controls, diff --git a/lib/app/common/app_page/app_infos/additional_information.dart b/lib/app/common/app_page/app_infos/additional_information.dart index 7db2d926c..5041636a9 100644 --- a/lib/app/common/app_page/app_infos/additional_information.dart +++ b/lib/app/common/app_page/app_infos/additional_information.dart @@ -7,6 +7,7 @@ import 'package:software/app/common/app_page/app_infos/publisher_info_fragment.d import 'package:software/app/common/app_page/app_infos/released_at_info_fragment.dart'; import 'package:software/l10n/l10n.dart'; import 'package:yaru_widgets/yaru_widgets.dart'; +import '../../expandable_title.dart'; const headerStyle = TextStyle(fontWeight: FontWeight.w500, fontSize: 14); @@ -22,51 +23,48 @@ class AdditionalInformation extends StatelessWidget { Widget build(BuildContext context) { return YaruExpandable( isExpanded: true, - header: Text( + header: ExpandableContainerTitle( context.l10n.additionalInformation, - style: Theme.of(context).textTheme.titleLarge, ), - child: ConstrainedBox( - constraints: BoxConstraints.loose(const Size(1000, 200)), - child: ScrollConfiguration( - behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), - child: GridView( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 200, - mainAxisExtent: 100, + child: ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), + child: GridView( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 200, + mainAxisExtent: 100, + crossAxisSpacing: kYaruPagePadding, + ), + children: [ + PublisherInfoFragment( + publisherName: appData.publisherName, + website: appData.website, + verified: appData.verified, + starDev: appData.starredDeveloper, + ), + ReleasedAtInfoFragment(releasedAt: appData.releasedAt), + LicenseInfoFragment( + headerStyle: headerStyle, + license: appData.license, ), - children: [ - PublisherInfoFragment( - publisherName: appData.publisherName, - website: appData.website, - verified: appData.verified, - starDev: appData.starredDeveloper, - ), - ReleasedAtInfoFragment(releasedAt: appData.releasedAt), - LicenseInfoFragment( - headerStyle: headerStyle, - license: appData.license, - ), - // TODO: the category is currently not provided - // by snapd, and thus not by snapd.dart - // when a snap is found by name - // See: https://bugs.launchpad.net/snapd/+bug/1838786/comments/5 + // TODO: the category is currently not provided + // by snapd, and thus not by snapd.dart + // when a snap is found by name + // See: https://bugs.launchpad.net/snapd/+bug/1838786/comments/5 - // AppInfoFragment( - // crossAxisAlignment: CrossAxisAlignment.start, - // header: 'Category', - // tooltipMessage: '', - // child: Text(context.l10n.unknown), - // ), - InstallDateInfoFragment( - installDateIsoNorm: appData.installDateIsoNorm, - installDate: appData.installDate, - ), - LinksInfoFragment(appData: appData), - ], - ), + // AppInfoFragment( + // crossAxisAlignment: CrossAxisAlignment.start, + // header: 'Category', + // tooltipMessage: '', + // child: Text(context.l10n.unknown), + // ), + InstallDateInfoFragment( + installDateIsoNorm: appData.installDateIsoNorm, + installDate: appData.installDate, + ), + LinksInfoFragment(appData: appData), + ], ), ), ); diff --git a/lib/app/common/app_page/app_infos/app_infos.dart b/lib/app/common/app_page/app_infos/app_infos.dart index f5e2a454b..2593facec 100644 --- a/lib/app/common/app_page/app_infos/app_infos.dart +++ b/lib/app/common/app_page/app_infos/app_infos.dart @@ -48,7 +48,10 @@ class AppInfos extends StatelessWidget { @override Widget build(BuildContext context) { final appInfos = [ - RatingInfoFragment(averageRating: appData.averageRating), + RatingInfoFragment( + averageRating: appData.appRating?.average ?? 0.0, + totalRatings: appData.appRating?.total ?? 0, + ), ConfinementInfoFragment( strict: appData.strict, confinementName: appData.confinementName, @@ -74,9 +77,11 @@ class RatingInfoFragment extends StatelessWidget { const RatingInfoFragment({ super.key, required this.averageRating, + required this.totalRatings, }); final double averageRating; + final int totalRatings; @override Widget build(BuildContext context) { @@ -101,13 +106,23 @@ class RatingInfoFragment extends StatelessWidget { ); return AppInfoFragment( - header: context.l10n.rating, - tooltipMessage: averageRating.toString(), - child: Align( - alignment: Alignment.center, - child: Padding( - padding: const EdgeInsets.all(3.0), - child: bar, + header: context.l10n.ratings, + child: Padding( + padding: const EdgeInsets.all(3.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + bar, + const SizedBox(width: 5), + Flexible( + child: Text( + '($totalRatings)', + style: theme.textTheme.bodySmall, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + ), + ), + ], ), ), ); diff --git a/lib/app/common/app_page/app_infos/app_size_fragment.dart b/lib/app/common/app_page/app_infos/app_size_fragment.dart index ef42f4a8f..e5462eec9 100644 --- a/lib/app/common/app_page/app_infos/app_size_fragment.dart +++ b/lib/app/common/app_page/app_infos/app_size_fragment.dart @@ -11,7 +11,6 @@ class AppSizeFragment extends StatelessWidget { Widget build(BuildContext context) { return AppInfoFragment( header: context.l10n.size, - tooltipMessage: appSize, child: Text( appSize, textAlign: TextAlign.center, diff --git a/lib/app/common/app_page/app_infos/confinment_info_fragment.dart b/lib/app/common/app_page/app_infos/confinment_info_fragment.dart index dde35b3b2..9f8c50c2a 100644 --- a/lib/app/common/app_page/app_infos/confinment_info_fragment.dart +++ b/lib/app/common/app_page/app_infos/confinment_info_fragment.dart @@ -17,7 +17,6 @@ class ConfinementInfoFragment extends StatelessWidget { Widget build(BuildContext context) { return AppInfoFragment( header: context.l10n.confinement, - tooltipMessage: confinementName, child: Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, diff --git a/lib/app/common/app_page/app_infos/install_date_info_fragment.dart b/lib/app/common/app_page/app_infos/install_date_info_fragment.dart index 7f27a4f40..611cbeed6 100644 --- a/lib/app/common/app_page/app_infos/install_date_info_fragment.dart +++ b/lib/app/common/app_page/app_infos/install_date_info_fragment.dart @@ -17,7 +17,6 @@ class InstallDateInfoFragment extends StatelessWidget { return AppInfoFragment( crossAxisAlignment: CrossAxisAlignment.start, header: context.l10n.installDate, - tooltipMessage: installDateIsoNorm, child: Text( installDate.isNotEmpty ? installDate : context.l10n.notInstalled, maxLines: 1, diff --git a/lib/app/common/app_page/app_infos/license_info_fragment.dart b/lib/app/common/app_page/app_infos/license_info_fragment.dart index daaf2288d..8511fd9e5 100644 --- a/lib/app/common/app_page/app_infos/license_info_fragment.dart +++ b/lib/app/common/app_page/app_infos/license_info_fragment.dart @@ -17,7 +17,7 @@ class LicenseInfoFragment extends StatelessWidget { return AppInfoFragment( crossAxisAlignment: CrossAxisAlignment.start, header: context.l10n.license, - tooltipMessage: license, + tooltipMessage: license.length > 20 ? license : null, child: Text( license, overflow: TextOverflow.ellipsis, diff --git a/lib/app/common/app_page/app_infos/publisher_info_fragment.dart b/lib/app/common/app_page/app_infos/publisher_info_fragment.dart index de9fe22e4..70563a4ba 100644 --- a/lib/app/common/app_page/app_infos/publisher_info_fragment.dart +++ b/lib/app/common/app_page/app_infos/publisher_info_fragment.dart @@ -28,7 +28,6 @@ class PublisherInfoFragment extends StatelessWidget { required this.publisherName, this.starDev = false, required this.website, - this.limitChildWidth = true, this.height = 14, this.enhanceChildText = false, }); @@ -37,7 +36,6 @@ class PublisherInfoFragment extends StatelessWidget { final bool starDev; final String publisherName; final String website; - final bool limitChildWidth; final double height; final bool enhanceChildText; @@ -46,7 +44,7 @@ class PublisherInfoFragment extends StatelessWidget { final theme = Theme.of(context); final light = theme.brightness == Brightness.light; var child = Text( - publisherName, + publisherName.replaceAll(' ', '\u00A0'), style: Theme.of(context).textTheme.bodyMedium!.copyWith( fontSize: height, fontStyle: enhanceChildText ? FontStyle.italic : FontStyle.normal, @@ -55,22 +53,19 @@ class PublisherInfoFragment extends StatelessWidget { : null, ), overflow: TextOverflow.ellipsis, + maxLines: 1, ); final box = SizedBox( child: Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start, children: [ - if (limitChildWidth) - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 110), - child: child, - ) - else - child, + Flexible( + child: child, + ), if (verified) Padding( - padding: EdgeInsets.only(left: height * 0.2), + padding: const EdgeInsets.only(left: 5), child: Icon( Icons.verified, color: light ? kGreenLight : kGreenDark, diff --git a/lib/app/common/app_page/app_infos/released_at_info_fragment.dart b/lib/app/common/app_page/app_infos/released_at_info_fragment.dart index 4d4293ed2..f4480372f 100644 --- a/lib/app/common/app_page/app_infos/released_at_info_fragment.dart +++ b/lib/app/common/app_page/app_infos/released_at_info_fragment.dart @@ -11,8 +11,7 @@ class ReleasedAtInfoFragment extends StatelessWidget { Widget build(BuildContext context) { return AppInfoFragment( crossAxisAlignment: CrossAxisAlignment.start, - header: context.l10n.releasedAt, - tooltipMessage: releasedAt, + header: context.l10n.lastUpdated, child: Text( releasedAt, textAlign: TextAlign.center, diff --git a/lib/app/common/app_page/app_infos/version_info_fragment.dart b/lib/app/common/app_page/app_infos/version_info_fragment.dart index 3973274e7..c09955905 100644 --- a/lib/app/common/app_page/app_infos/version_info_fragment.dart +++ b/lib/app/common/app_page/app_infos/version_info_fragment.dart @@ -17,7 +17,7 @@ class VersionInfoFragment extends StatelessWidget { Widget build(BuildContext context) { return AppInfoFragment( header: context.l10n.version, - tooltipMessage: version, + tooltipMessage: version.length > 12 ? version : null, child: Text( version, overflow: TextOverflow.ellipsis, diff --git a/lib/app/common/app_page/app_page.dart b/lib/app/common/app_page/app_page.dart index 444de7430..7cc2c77bb 100644 --- a/lib/app/common/app_page/app_page.dart +++ b/lib/app/common/app_page/app_page.dart @@ -17,6 +17,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:provider/provider.dart'; import 'package:software/app/common/app_data.dart'; import 'package:software/app/common/app_page/app_description.dart'; import 'package:software/app/common/app_page/app_header.dart'; @@ -28,10 +29,14 @@ import 'package:software/app/common/app_page/media_tile.dart'; import 'package:software/app/common/app_page/page_layouts.dart'; import 'package:software/app/common/border_container.dart'; import 'package:software/app/common/custom_back_button.dart'; +import 'package:software/app/common/link.dart'; import 'package:software/app/common/safe_network_image.dart'; +import 'package:software/app/explore/explore_model.dart'; import 'package:software/l10n/l10n.dart'; import 'package:yaru_icons/yaru_icons.dart'; import 'package:yaru_widgets/yaru_widgets.dart'; +import '../expandable_title.dart'; +import 'package:yaru_colors/yaru_colors.dart'; class AppPage extends StatefulWidget { const AppPage({ @@ -54,6 +59,7 @@ class AppPage extends StatefulWidget { this.onVote, this.onFlag, this.initialized = false, + this.enableSearch = true, }); final bool initialized; @@ -63,6 +69,7 @@ class AppPage extends StatefulWidget { final Widget? controls; final Widget? subDescription; final bool appIsInstalled; + final bool enableSearch; final double? reviewRating; final String? review; @@ -101,19 +108,20 @@ class _AppPageState extends State { Widget build(BuildContext context) { final windowSize = MediaQuery.of(context).size; final windowWidth = windowSize.width; - final windowHeight = windowSize.height; final isWindowNormalSized = windowWidth > 800 && windowWidth < 1200; final isWindowWide = windowWidth > 1200; final icon = widget.icon; + final searchByPublisher = + context.select((ExploreModel m) => m.searchByPublisher); + final media = BorderContainer( initialized: widget.initialized, child: YaruExpandable( isExpanded: true, - header: Text( + header: ExpandableContainerTitle( context.l10n.gallery, - style: Theme.of(context).textTheme.titleLarge, ), child: YaruCarousel( controller: controller, @@ -129,9 +137,7 @@ class _AppPageState extends State { onTap: () => showDialog( context: context, builder: (c) => _CarouselDialog( - windowHeight: windowHeight, appData: widget.appData, - windowWidth: windowWidth, initialIndex: i, ), ), @@ -152,7 +158,7 @@ class _AppPageState extends State { review: widget.review, reviewTitle: widget.reviewTitle, reviewUser: widget.reviewUser, - averageRating: widget.appData.averageRating, + appRating: widget.appData.appRating, userReviews: widget.appData.userReviews, appIsInstalled: widget.appIsInstalled, onRatingUpdate: widget.onRatingUpdate, @@ -178,14 +184,51 @@ class _AppPageState extends State { ), ); + void onShare(AppData appData) { + final colorScheme = Theme.of(context).colorScheme; + final linkColorInvert = colorScheme.brightness == Brightness.light + ? YaruColors.blue[500]! + : YaruColors.blue[700]!; + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text('${context.l10n.copiedToClipboard}: '), + Link( + url: appData.website, + linkText: appData.website, + textStyle: TextStyle(color: linkColorInvert), + ), + ], + ), + ), + ); + + Clipboard.setData(ClipboardData(text: appData.website)); + } + + final onPublisherSearch = + widget.enableSearch == false || !widget.initialized + ? null + : () async { + await searchByPublisher(widget.appData.publisherUsername); + if (context.mounted) { + Navigator.of(context).pop(); + } + }; + final normalWindowAppHeader = BorderContainer( initialized: widget.initialized, child: BannerAppHeader( + onPublisherSearch: onPublisherSearch, windowSize: windowSize, appData: widget.appData, controls: widget.preControls, subControls: widget.controls, icon: icon, + onShare: () => onShare(widget.appData), ), ); @@ -193,10 +236,12 @@ class _AppPageState extends State { initialized: widget.initialized, width: 500, child: PageAppHeader( + onPublisherSearch: onPublisherSearch, appData: widget.appData, icon: icon, controls: widget.preControls, subControls: widget.controls, + onShare: () => onShare(widget.appData), ), ); @@ -204,10 +249,12 @@ class _AppPageState extends State { initialized: widget.initialized, height: 700, child: PageAppHeader( + onPublisherSearch: onPublisherSearch, appData: widget.appData, icon: icon, controls: widget.preControls, subControls: widget.controls, + onShare: () => onShare(widget.appData), ), ); @@ -259,8 +306,7 @@ class _AppPageState extends State { return Scaffold( appBar: YaruWindowTitleBar( - title: Text(widget.appData.title), - titleSpacing: 0, + title: Center(child: Text(widget.appData.title)), leading: const CustomBackButton(), ), body: BackGesture( @@ -272,15 +318,11 @@ class _AppPageState extends State { class _CarouselDialog extends StatefulWidget { const _CarouselDialog({ - required this.windowHeight, required this.appData, - required this.windowWidth, required this.initialIndex, }); - final double windowHeight; final AppData appData; - final double windowWidth; final int initialIndex; @override @@ -307,6 +349,7 @@ class _CarouselDialogState extends State<_CarouselDialog> { @override Widget build(BuildContext context) { + final size = MediaQuery.of(context).size; return KeyboardListener( focusNode: FocusNode(), onKeyEvent: (value) { @@ -317,24 +360,28 @@ class _CarouselDialogState extends State<_CarouselDialog> { } }, child: SimpleDialog( - title: const YaruCloseButton( - alignment: Alignment.centerRight, + title: YaruDialogTitleBar( + title: Text(widget.appData.name), ), - contentPadding: const EdgeInsets.only(bottom: 20), - titlePadding: const EdgeInsets.fromLTRB(12.0, 12.0, 12.0, 6.0), + contentPadding: const EdgeInsets.only(bottom: 20, top: 20), + titlePadding: EdgeInsets.zero, children: [ SizedBox( - height: widget.windowHeight - 150, + height: size.height - 150, + width: size.width, child: YaruCarousel( controller: controller, nextIcon: const Icon(YaruIcons.go_next), previousIcon: const Icon(YaruIcons.go_previous), navigationControls: widget.appData.screenShotUrls.length > 1, - width: widget.windowWidth, + width: size.width, placeIndicatorMarginTop: 20.0, children: [ for (final url in widget.appData.screenShotUrls) - SafeNetworkImage(url: url) + SafeNetworkImage( + url: url, + fit: BoxFit.fitWidth, + ) ], ), ) diff --git a/lib/app/common/app_page/app_reviews.dart b/lib/app/common/app_page/app_reviews.dart index 97098e26f..d557ed911 100644 --- a/lib/app/common/app_page/app_reviews.dart +++ b/lib/app/common/app_page/app_reviews.dart @@ -3,17 +3,27 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_rating_bar/flutter_rating_bar.dart'; import 'package:intl/intl.dart'; -import 'package:software/l10n/l10n.dart'; import 'package:software/app/common/app_data.dart'; +import 'package:software/app/common/app_rating.dart'; import 'package:software/app/common/border_container.dart'; import 'package:software/app/common/constants.dart'; +import 'package:software/app/common/rating_chart.dart'; +import 'package:software/l10n/l10n.dart'; import 'package:yaru_icons/yaru_icons.dart'; import 'package:yaru_widgets/yaru_widgets.dart'; +import '../expandable_title.dart'; + +// https://github.com/GNOME/odrs-web/blob/master/odrs/views_api.py +const _kMinReviewLength = 2; +const _kMaxReviewLength = 3000; +const _kMinTitleLength = 2; +const _kMaxTitleLength = 70; + class AppReviews extends StatefulWidget { const AppReviews({ super.key, - this.averageRating, + this.appRating, this.userReviews, this.onRatingUpdate, this.onReviewSend, @@ -30,7 +40,7 @@ class AppReviews extends StatefulWidget { required this.initialized, }); - final double? averageRating; + final AppRating? appRating; final double? reviewRating; final String? reviewTitle; final String? review; @@ -71,37 +81,54 @@ class _AppReviewsState extends State { return BorderContainer( initialized: widget.initialized, child: YaruExpandable( - isExpanded: false, - header: Text( - context.l10n.reviewsAndRatings, - style: Theme.of(context).textTheme.titleLarge, - overflow: TextOverflow.ellipsis, + isExpanded: true, + header: ExpandableContainerTitle( + context.l10n.ratingsAndReviews, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _ReviewPanel( - appIsInstalled: widget.appIsInstalled, - averageRating: widget.averageRating, - reviewRating: widget.reviewRating, - review: widget.review, - reviewTitle: widget.reviewTitle, - reviewUser: widget.reviewUser, - onRatingUpdate: widget.onRatingUpdate, - onReviewSend: widget.onReviewSend, - onReviewChanged: widget.onReviewChanged, - onReviewTitleChanged: widget.onReviewTitleChanged, - onReviewUserChanged: widget.onReviewUserChanged, + const SizedBox( + height: 10, + ), + if (widget.appRating != null) + RatingChart( + appRating: widget.appRating!, + ), + const Padding( + padding: EdgeInsets.only(top: 30, bottom: 30), + child: Divider( + height: 0, + ), ), - const Divider(), - _ReviewsCarousel( + if (widget.appIsInstalled) + _ReviewPanel( + appIsInstalled: widget.appIsInstalled, + averageRating: widget.appRating?.average, + reviewRating: widget.reviewRating, + review: widget.review, + reviewTitle: widget.reviewTitle, + reviewUser: widget.reviewUser, + onRatingUpdate: widget.onRatingUpdate, + onReviewSend: widget.onReviewSend, + onReviewChanged: widget.onReviewChanged, + onReviewTitleChanged: widget.onReviewTitleChanged, + onReviewUserChanged: widget.onReviewUserChanged, + ), + if (widget.appIsInstalled) + const Padding( + padding: EdgeInsets.only(top: 30, bottom: 30), + child: Divider( + height: 0, + ), + ), + _ReviewsTrailer( userReviews: widget.userReviews, controller: _controller, onVote: widget.onVote, onFlag: widget.onFlag, ), Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ OutlinedButton( onPressed: () => showDialog( @@ -114,19 +141,6 @@ class _AppReviewsState extends State { ), child: Text(context.l10n.showAllReviews), ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - YaruIconButton( - icon: const Icon(YaruIcons.go_previous), - onPressed: () => _controller.previousPage(), - ), - YaruIconButton( - icon: const Icon(YaruIcons.go_next), - onPressed: () => _controller.nextPage(), - ) - ], - ) ], ) ], @@ -151,50 +165,17 @@ class _ReviewDetailsDialog extends StatelessWidget { Widget build(BuildContext context) { return SimpleDialog( title: YaruDialogTitleBar( - title: Text(context.l10n.reviewsAndRatings), + title: Text(context.l10n.ratingsAndReviews), ), titlePadding: EdgeInsets.zero, - contentPadding: const EdgeInsets.only( - top: kYaruPagePadding, - bottom: kYaruPagePadding, - ), + contentPadding: const EdgeInsets.all(kYaruPagePadding), children: userReviews == null ? [] : userReviews! .map( - (e) => BorderContainer( - margin: const EdgeInsets.only( - left: kYaruPagePadding, - right: kYaruPagePadding, - bottom: kYaruPagePadding, - ), - padding: const EdgeInsets.only( - right: kYaruPagePadding, - left: kYaruPagePadding, - top: 10, - bottom: kYaruPagePadding, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - _RatingHeader( - userReview: e, - onVote: onVote, - onFlag: onFlag, - ), - const SizedBox( - height: 10, - ), - SizedBox( - width: 400, - child: Text( - e.review ?? '', - overflow: TextOverflow.visible, - ), - ), - ], - ), + (e) => SizedBox( + width: 500, + child: _Review(userReview: e, onFlag: onFlag, onVote: onVote), ), ) .toList(), @@ -236,31 +217,35 @@ class _ReviewPanel extends StatelessWidget { Widget build(BuildContext context) { final theme = Theme.of(context); return Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - const SizedBox( - height: kYaruPagePadding, - ), Row( + crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, + Row( children: [ + Text( + '${context.l10n.rate}:', + style: Theme.of(context).textTheme.bodySmall, + ), + const SizedBox( + width: 10, + ), RatingBar.builder( initialRating: reviewRating ?? 0, minRating: 1, direction: Axis.horizontal, - allowHalfRating: true, itemCount: 5, itemPadding: const EdgeInsets.only(right: 5), - itemSize: 35, - itemBuilder: (context, _) => const Icon( - YaruIcons.star_filled, - color: kStarColor, - size: 2, + itemSize: 40, + itemBuilder: (context, _) => const MouseRegion( + cursor: SystemMouseCursors.click, + child: Icon( + YaruIcons.star_filled, + color: kStarColor, + size: 2, + ), ), unratedColor: theme.colorScheme.onSurface.withOpacity(0.2), onRatingUpdate: (rating) { @@ -270,50 +255,31 @@ class _ReviewPanel extends StatelessWidget { }, ignoreGestures: !appIsInstalled, ), - const SizedBox( - width: 10, - ), - Padding( - padding: const EdgeInsets.all(5.0), - child: Text( - appIsInstalled - ? context.l10n.clickToRate - : context.l10n.notInstalled, - style: Theme.of(context).textTheme.bodySmall, - ), - ), ], ), - const SizedBox( - width: kYaruPagePadding, - ), - if (appIsInstalled) - ElevatedButton( - onPressed: () => showDialog( - context: context, - builder: (context) => _MyReviewDialog( - reviewRating: reviewRating, - review: review, - reviewTitle: reviewTitle, - reviewUser: reviewUser, - onRatingUpdate: (rating) { - if (onRatingUpdate != null) { - onRatingUpdate!(rating); - } - }, - onReviewSend: onReviewSend, - onReviewChanged: onReviewChanged, - onReviewTitleChanged: onReviewTitleChanged, - onReviewUserChanged: onReviewUserChanged, - ), + ElevatedButton( + onPressed: () => showDialog( + context: context, + builder: (context) => _MyReviewDialog( + reviewRating: reviewRating, + review: review, + reviewTitle: reviewTitle, + reviewUser: reviewUser, + onRatingUpdate: (rating) { + if (onRatingUpdate != null) { + onRatingUpdate!(rating); + } + }, + onReviewSend: onReviewSend, + onReviewChanged: onReviewChanged, + onReviewTitleChanged: onReviewTitleChanged, + onReviewUserChanged: onReviewUserChanged, ), - child: Text(context.l10n.yourReview), - ) + ), + child: Text(context.l10n.yourReview), + ) ], ), - const SizedBox( - height: kYaruPagePadding, - ), ], ); } @@ -350,6 +316,7 @@ class _MyReviewDialog extends StatefulWidget { } class _MyReviewDialogState extends State<_MyReviewDialog> { + late double? _reviewRating; late TextEditingController _reviewController, _reviewTitleController, _reviewUserController; @@ -357,42 +324,55 @@ class _MyReviewDialogState extends State<_MyReviewDialog> { @override void initState() { super.initState(); + _reviewRating = widget.reviewRating; _reviewController = TextEditingController(text: widget.review); _reviewTitleController = TextEditingController(text: widget.reviewTitle); _reviewUserController = TextEditingController(text: widget.reviewUser); } + bool get _isReviewValid => + _reviewRating != null && + _reviewController.text.length >= _kMinReviewLength && + _reviewTitleController.text.length >= _kMinTitleLength && + _reviewUserController.text.isNotEmpty; + @override Widget build(BuildContext context) { final theme = Theme.of(context); return AlertDialog( titlePadding: EdgeInsets.zero, title: YaruDialogTitleBar( - title: Text(context.l10n.yourReview), - leading: const Icon(YaruIcons.star_filled), + title: Text(context.l10n.writeAreview), ), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - RatingBar.builder( - initialRating: widget.reviewRating ?? 0, - minRating: 1, - direction: Axis.horizontal, - allowHalfRating: true, - itemCount: 5, - itemPadding: const EdgeInsets.only(right: 10), - itemSize: 50, - itemBuilder: (context, _) => const Icon( - YaruIcons.star_filled, - color: kStarColor, - size: 2, - ), - unratedColor: theme.colorScheme.onSurface.withOpacity(0.2), - onRatingUpdate: (rating) { - if (widget.onRatingUpdate == null) return; - widget.onRatingUpdate!(rating); - }, + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RatingBar.builder( + initialRating: _reviewRating ?? 0, + minRating: 1, + direction: Axis.horizontal, + itemCount: 5, + itemPadding: const EdgeInsets.only(right: 5), + itemSize: 40, + itemBuilder: (context, _) => const MouseRegion( + cursor: SystemMouseCursors.click, + child: Icon( + YaruIcons.star_filled, + color: kStarColor, + size: 2, + ), + ), + unratedColor: theme.colorScheme.onSurface.withOpacity(0.2), + onRatingUpdate: (rating) { + setState(() => _reviewRating = rating); + widget.onRatingUpdate?.call(rating); + }, + ), + ], ), const SizedBox( height: kYaruPagePadding, @@ -400,54 +380,109 @@ class _MyReviewDialogState extends State<_MyReviewDialog> { TextField( controller: _reviewUserController, onChanged: widget.onReviewUserChanged, - decoration: InputDecoration(hintText: context.l10n.yourReviewName), + style: theme.textTheme.bodyMedium, + decoration: InputDecoration( + label: Text( + context.l10n.yourReviewName, + style: theme.textTheme.bodyMedium, + ), + ), ), const SizedBox( height: kYaruPagePadding, ), TextField( + maxLength: _kMaxTitleLength, controller: _reviewTitleController, onChanged: widget.onReviewTitleChanged, - decoration: InputDecoration(hintText: context.l10n.yourReviewTitle), + style: theme.textTheme.bodyMedium, + decoration: InputDecoration( + label: Text( + context.l10n.summary, + style: theme.textTheme.bodyMedium, + ), + hintText: context.l10n.summeryHint, + ), ), const SizedBox( height: kYaruPagePadding, ), SizedBox( - width: 500, + width: 600, child: TextField( + maxLength: _kMaxReviewLength, controller: _reviewController, onChanged: widget.onReviewChanged, keyboardType: TextInputType.multiline, minLines: 10, maxLines: 10, - decoration: InputDecoration(hintText: context.l10n.yourReview), + style: theme.textTheme.bodyMedium, + decoration: InputDecoration( + label: Text( + context.l10n.yourReview, + style: theme.textTheme.bodyMedium, + ), + hintText: context.l10n.whatDoYouThink, + floatingLabelAlignment: FloatingLabelAlignment.start, + alignLabelWithHint: true, + ), ), ), ], ), actions: [ - ElevatedButton( - onPressed: () { - if (widget.onReviewSend != null) { - widget.onReviewSend!(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.reviewSent), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + context.l10n.whatDataIsSend + + context.l10n.privacyPolicy, // https://odrs.gnome.org/privacy + style: theme.textTheme.bodyMedium, + ), + Row( + children: [ + OutlinedButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(context.l10n.cancel), ), - ); - } - Navigator.of(context).pop(); - }, - child: Text(context.l10n.send), - ) + const SizedBox(width: 10), + AnimatedBuilder( + animation: Listenable.merge([ + _reviewController, + _reviewTitleController, + _reviewUserController, + ]), + builder: (context, child) { + return ElevatedButton( + onPressed: _isReviewValid + ? () { + if (widget.onReviewSend != null) { + widget.onReviewSend!(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.reviewSent), + ), + ); + } + Navigator.of(context).pop(); + } + : null, + child: Text(context.l10n.submit), + ); + }, + ), + ], + ), + ], + ), ], ); } } -class _ReviewsCarousel extends StatelessWidget { - const _ReviewsCarousel({ +class _ReviewsTrailer extends StatelessWidget { + const _ReviewsTrailer({ // ignore: unused_element super.key, this.userReviews, @@ -463,153 +498,244 @@ class _ReviewsCarousel extends StatelessWidget { @override Widget build(BuildContext context) { - return YaruCarousel( - height: 200, - width: 1000, - placeIndicator: false, - controller: controller, + return Column( children: [ if (userReviews != null) - for (final userReview in userReviews!) - Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - _RatingHeader( - userReview: userReview, - onFlag: onFlag, - onVote: onVote, - ), - const SizedBox( - height: 10, - ), - Expanded( - child: InkWell( - borderRadius: BorderRadius.circular(kYaruButtonRadius), - onTap: () => showDialog( - context: context, - builder: (c) => - _ReviewDetailsDialog(userReviews: userReviews), - ), - child: Text( - userReview.review ?? '', - overflow: TextOverflow.ellipsis, - maxLines: 8, - ), - ), - ), - const SizedBox( - height: kYaruPagePadding, - ) - ], + for (var i = 0; + i < (userReviews!.length > 3 ? 3 : userReviews!.length); + i++) + _Review( + onFlag: onFlag, + onVote: onVote, + userReview: userReviews![i], ) ], ); } } -class _RatingHeader extends StatelessWidget { - const _RatingHeader({ +class _Review extends StatelessWidget { + const _Review({ required this.userReview, - this.onVote, - this.onFlag, + required this.onFlag, + required this.onVote, }); final AppReview userReview; - final Function(AppReview, bool)? onVote; - final Function(AppReview)? onFlag; + final Function(AppReview p1)? onFlag; + final Function(AppReview p1, bool p2)? onVote; @override Widget build(BuildContext context) { - final theme = Theme.of(context); - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + return Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, children: [ + Text( + userReview.title ?? '', + overflow: TextOverflow.ellipsis, + maxLines: 1, + style: const TextStyle(fontWeight: FontWeight.w500), + ), + const SizedBox( + height: 5, + ), + RatingBar.builder( + initialRating: userReview.rating ?? 0, + minRating: 1, + direction: Axis.horizontal, + allowHalfRating: true, + itemCount: 5, + itemPadding: EdgeInsets.zero, + itemSize: 15, + itemBuilder: (context, _) => const Icon( + YaruIcons.star_filled, + color: kStarColor, + size: 2, + ), + unratedColor: + Theme.of(context).colorScheme.onSurface.withOpacity(0.2), + onRatingUpdate: (rating) {}, + ignoreGestures: true, + ), + const SizedBox( + width: 10, + ), + const SizedBox( + height: kYaruPagePadding, + ), + Text( + userReview.review ?? '', + overflow: TextOverflow.ellipsis, + maxLines: 8, + ), + const SizedBox( + height: kYaruPagePadding, + ), Row( + mainAxisSize: MainAxisSize.min, children: [ - RatingBar.builder( - initialRating: userReview.rating ?? 0, - minRating: 1, - direction: Axis.horizontal, - allowHalfRating: true, - itemCount: 5, - itemPadding: EdgeInsets.zero, - itemSize: 15, - itemBuilder: (context, _) => const Icon( - YaruIcons.star_filled, - color: kStarColor, - size: 2, - ), - unratedColor: theme.colorScheme.onSurface.withOpacity(0.2), - onRatingUpdate: (rating) {}, - ignoreGestures: true, - ), - const SizedBox( - width: 10, - ), Text( DateFormat.yMd(Platform.localeName).format( userReview.dateTime ?? DateTime.now(), ), - style: Theme.of(context).textTheme.bodySmall, + style: Theme.of(context).textTheme.bodySmall!.copyWith( + color: Theme.of(context).hintColor, + ), ), const SizedBox( - width: 10, + width: 5, ), Text( userReview.username ?? context.l10n.unknown, - style: Theme.of(context).textTheme.bodySmall, - overflow: TextOverflow.ellipsis, - ), - const SizedBox( - width: 10, - ), - IconButton( - icon: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - '${userReview.positiveVote ?? 1}', - style: Theme.of(context).textTheme.bodySmall, - ), - Icon( - Icons.arrow_upward, - color: Theme.of(context).disabledColor, - size: 16, - ) - ], - ), - onPressed: - onVote == null ? null : () => onVote!(userReview, false), - ), - IconButton( - icon: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - '${userReview.negativeVote ?? 1}', - style: Theme.of(context).textTheme.bodySmall, + style: Theme.of(context).textTheme.bodySmall!.copyWith( + color: Theme.of(context).hintColor, + overflow: TextOverflow.ellipsis, ), - Icon( - Icons.arrow_downward, - color: Theme.of(context).disabledColor, - size: 16, - ) - ], - ), - onPressed: - onVote == null ? null : () => onVote!(userReview, true), ), ], ), - IconButton( - icon: Icon( - Icons.flag_rounded, - size: 16, - color: Theme.of(context).disabledColor, + const SizedBox( + height: kYaruPagePadding, + ), + _ReviewRatingBar( + userReview: userReview, + onFlag: onFlag, + onVote: onVote, + ), + const Padding( + padding: EdgeInsets.only(top: 20, bottom: 20), + child: Divider( + height: 0, ), - onPressed: onFlag == null ? null : () => onFlag!(userReview), + ), + ], + ); + } +} + +class _ReviewRatingBar extends StatelessWidget { + const _ReviewRatingBar({ + required this.userReview, + this.onVote, + this.onFlag, + }); + + final AppReview userReview; + final Function(AppReview, bool)? onVote; + final Function(AppReview)? onFlag; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Wrap( + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 10, + runSpacing: 20, + children: [ + RawChip( + onPressed: onVote == null ? null : () => onVote!(userReview, false), + label: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.thumb_up_outlined, + color: theme.hintColor, + size: 16, + ), + const SizedBox( + width: 5, + ), + Text( + '${userReview.positiveVote ?? 1} ${context.l10n.helpful}', + style: theme.textTheme.bodySmall, + ), + ], + ), + ), + RawChip( + onPressed: onVote == null ? null : () => onVote!(userReview, true), + label: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.thumb_down_outlined, + color: theme.hintColor, + size: 16, + ), + const SizedBox( + width: 5, + ), + Text( + '${userReview.negativeVote ?? 1} ${context.l10n.notHelpful}', + style: theme.textTheme.bodySmall, + ), + const SizedBox( + width: 10, + ), + ], + ), + ), + RawChip( + onPressed: onFlag == null + ? null + : () => showDialog( + context: context, + builder: (context) => _ReportReviewDialog( + onFlag: () => onFlag!(userReview), + ), + ), + label: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.flag_rounded, + size: 16, + color: Theme.of(context).hintColor, + ), + const SizedBox( + width: 5, + ), + Text( + context.l10n.reportAbuse, + style: theme.textTheme.bodySmall, + ), + ], + ), + ) + ], + ); + } +} + +class _ReportReviewDialog extends StatelessWidget { + const _ReportReviewDialog({required this.onFlag}); + + final void Function() onFlag; + + @override + Widget build(BuildContext context) { + return AlertDialog( + titlePadding: EdgeInsets.zero, + title: + YaruDialogTitleBar(title: Text(context.l10n.reportReviewDialogTitle)), + content: SizedBox( + width: 400, + child: Text(context.l10n.reportReviewDialogBody), + ), + actions: [ + OutlinedButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(context.l10n.cancel), + ), + ElevatedButton( + onPressed: () { + onFlag(); + Navigator.of(context).pop(); + }, + child: Text(context.l10n.report), ) ], ); diff --git a/lib/app/common/app_page/app_swipe_gesture.dart b/lib/app/common/app_page/app_swipe_gesture.dart index ca3a5d378..1c2308894 100644 --- a/lib/app/common/app_page/app_swipe_gesture.dart +++ b/lib/app/common/app_page/app_swipe_gesture.dart @@ -66,6 +66,7 @@ class _BackGestureState extends State } void onPanStart(DragStartDetails details, BoxConstraints constraints) { + swipeBackController.reset(); currentExtent = 0; xPosition = 0 - _kButtonSize; yPosition = (constraints.maxHeight - _kButtonSize) / 2; diff --git a/lib/app/common/app_page/publisher_name.dart b/lib/app/common/app_page/publisher_name.dart index d4f916ce3..1e33e2401 100644 --- a/lib/app/common/app_page/publisher_name.dart +++ b/lib/app/common/app_page/publisher_name.dart @@ -17,7 +17,7 @@ import 'package:flutter/material.dart'; import 'package:software/app/common/constants.dart'; -import 'package:url_launcher/url_launcher.dart'; +import 'package:software/l10n/l10n.dart'; import 'package:yaru_icons/yaru_icons.dart'; class PublisherName extends StatelessWidget { @@ -30,6 +30,7 @@ class PublisherName extends StatelessWidget { this.limitChildWidth = true, this.height = 14, this.enhanceChildText = false, + required this.onPublisherSearch, }); final bool verified; @@ -39,6 +40,7 @@ class PublisherName extends StatelessWidget { final bool limitChildWidth; final double height; final bool enhanceChildText; + final void Function()? onPublisherSearch; @override Widget build(BuildContext context) { @@ -48,15 +50,12 @@ class PublisherName extends StatelessWidget { publisherName, style: Theme.of(context).textTheme.bodyMedium!.copyWith( fontSize: height, - fontStyle: enhanceChildText ? FontStyle.italic : FontStyle.normal, - color: enhanceChildText - ? theme.colorScheme.onSurface.withOpacity(0.7) - : null, + color: enhanceChildText ? theme.hintColor : null, ), overflow: TextOverflow.ellipsis, ); return InkWell( - onTap: () => launchUrl(Uri.parse(website)), + onTap: onPublisherSearch, child: SizedBox( child: Row( mainAxisSize: MainAxisSize.min, @@ -72,10 +71,13 @@ class PublisherName extends StatelessWidget { if (verified) Padding( padding: EdgeInsets.only(left: height * 0.2), - child: Icon( - Icons.verified, - color: light ? kGreenLight : kGreenDark, - size: height * 0.85, + child: Tooltip( + message: context.l10n.verified, + child: Icon( + Icons.verified, + color: light ? kGreenLight : kGreenDark, + size: height * 0.85, + ), ), ) else if (starDev) @@ -107,10 +109,13 @@ class _StarDeveloper extends StatelessWidget { borderRadius: BorderRadius.circular(20), ), child: Center( - child: Icon( - YaruIcons.star_filled, - color: Colors.white, - size: height, + child: Tooltip( + message: context.l10n.starDeveloper, + child: Icon( + YaruIcons.star_filled, + color: Colors.white, + size: height, + ), ), ), ); diff --git a/lib/app/common/app_rating.dart b/lib/app/common/app_rating.dart index d2f6aab1d..3cdea1cb3 100644 --- a/lib/app/common/app_rating.dart +++ b/lib/app/common/app_rating.dart @@ -7,9 +7,22 @@ class AppRating { final double? average; final int? total; + final int? star0; + final int? star1; + final int? star2; + final int? star3; + final int? star4; + final int? star5; + const AppRating({ this.average, this.total, + this.star0, + this.star1, + this.star2, + this.star3, + this.star4, + this.star5, }); @override diff --git a/lib/app/common/close_confirmation_dialog.dart b/lib/app/common/close_confirmation_dialog.dart index c02e56257..6e9e6ba53 100644 --- a/lib/app/common/close_confirmation_dialog.dart +++ b/lib/app/common/close_confirmation_dialog.dart @@ -72,7 +72,7 @@ class CloseWindowConfirmDialog extends StatelessWidget { Expanded( child: DangerousDelayedButton( duration: const Duration(seconds: 3), - onPressed: onConfirm, + onPressed: () => Navigator.of(context).pop(true), child: Text( context.l10n.quit, ), diff --git a/lib/app/common/constants.dart b/lib/app/common/constants.dart index a8a2d1cee..e06bcfbab 100644 --- a/lib/app/common/constants.dart +++ b/lib/app/common/constants.dart @@ -59,3 +59,5 @@ const kShimmerBaseLight = Color.fromARGB(120, 228, 228, 228); const kShimmerBaseDark = Color.fromARGB(255, 51, 51, 51); const kShimmerHighLightLight = Color.fromARGB(200, 247, 247, 247); const kShimmerHighLightDark = Color.fromARGB(255, 57, 57, 57); + +const kLeadingGap = 40.0; diff --git a/lib/app/common/expandable_title.dart b/lib/app/common/expandable_title.dart new file mode 100644 index 000000000..00eab4c18 --- /dev/null +++ b/lib/app/common/expandable_title.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +class ExpandableContainerTitle extends StatelessWidget { + final String title; + + const ExpandableContainerTitle(this.title, {super.key}); + + @override + Widget build(BuildContext context) { + return Text( + title, + style: Theme.of(context).textTheme.titleLarge!.copyWith(fontSize: 17), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ); + } +} diff --git a/lib/app/common/link.dart b/lib/app/common/link.dart index 29a10fb1c..6811961fd 100644 --- a/lib/app/common/link.dart +++ b/lib/app/common/link.dart @@ -18,6 +18,7 @@ import 'package:flutter/material.dart'; import 'package:software/l10n/l10n.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:yaru_colors/yaru_colors.dart'; class Link extends StatelessWidget { const Link({ @@ -44,8 +45,14 @@ class Link extends StatelessWidget { Theme.of(context) .textTheme .bodyMedium - ?.copyWith(color: Theme.of(context).colorScheme.primary), + ?.copyWith(color: context.linkColor), ), ); } } + +extension LinkColor on BuildContext { + Color get linkColor => Theme.of(this).brightness == Brightness.light + ? YaruColors.blue[700]! + : YaruColors.blue[500]!; +} diff --git a/lib/app/common/loading_banner_grid.dart b/lib/app/common/loading_banner_grid.dart index 9ba2b6393..4b6434d44 100644 --- a/lib/app/common/loading_banner_grid.dart +++ b/lib/app/common/loading_banner_grid.dart @@ -77,8 +77,8 @@ class LoadingExploreHeader extends StatelessWidget { return Shimmer.fromColors( baseColor: shimmerBase, highlightColor: shimmerHighLight, - child: Padding( - padding: const EdgeInsets.only( + child: const Padding( + padding: EdgeInsets.only( top: kPagePadding, left: kPagePadding, bottom: kPagePadding - 5, @@ -88,7 +88,7 @@ class LoadingExploreHeader extends StatelessWidget { runAlignment: WrapAlignment.start, crossAxisAlignment: WrapCrossAlignment.start, spacing: 10, - children: const [_LoadingButton(), _LoadingButton()], + children: [_LoadingButton(), _LoadingButton()], ), ), ); diff --git a/lib/app/common/packagekit/dependency_dialogs.dart b/lib/app/common/packagekit/dependency_dialogs.dart new file mode 100644 index 000000000..4d9c37005 --- /dev/null +++ b/lib/app/common/packagekit/dependency_dialogs.dart @@ -0,0 +1,198 @@ +import 'package:collection/collection.dart'; +import 'package:data_size/data_size.dart'; +import 'package:flutter/material.dart'; +import 'package:software/app/common/border_container.dart'; +import 'package:software/l10n/l10n.dart'; +import 'package:yaru_icons/yaru_icons.dart'; +import 'package:yaru_widgets/yaru_widgets.dart'; + +import 'package_model.dart'; + +class InstallDepsDialog extends StatelessWidget { + final VoidCallback onInstall; + final String packageName; + final List dependencies; + + const InstallDepsDialog({ + super.key, + required this.onInstall, + required this.packageName, + required this.dependencies, + }); + + @override + Widget build(BuildContext context) { + return _DepsDialog( + onConfirm: onInstall, + dependencies: dependencies, + body: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.l10n.dependenciesInstallListing( + dependencies.length, + dependencies.map((d) => d.size).sum.formatByteSize(), + packageName, + ), + style: Theme.of(context).textTheme.bodyLarge, + ), + Text( + context.l10n.dependenciesQuestion, + style: Theme.of(context) + .textTheme + .bodyLarge! + .copyWith(fontWeight: FontWeight.w500), + ), + ], + ), + confirmLabel: context.l10n.install, + packageName: packageName, + ); + } +} + +class RemoveDepsDialog extends StatefulWidget { + final void Function(bool) onRemove; + final String packageName; + final List dependencies; + + const RemoveDepsDialog({ + super.key, + required this.onRemove, + required this.packageName, + required this.dependencies, + }); + + @override + State createState() => _RemoveDepsDialogState(); +} + +class _RemoveDepsDialogState extends State { + bool autoremove = true; + @override + Widget build(BuildContext context) { + return _DepsDialog( + onConfirm: () => widget.onRemove(autoremove), + dependencies: widget.dependencies, + body: Text( + context.l10n.dependenciesRemoveListing( + widget.dependencies.length, + widget.dependencies.map((d) => d.size).sum.formatByteSize(), + widget.packageName, + ), + style: Theme.of(context).textTheme.bodyLarge, + ), + footer: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + YaruCheckbox( + value: autoremove, + onChanged: (value) => setState(() => autoremove = value ?? true), + ), + Text( + context.l10n.dependenciesAutoremove, + style: Theme.of(context).textTheme.bodyLarge, + ), + ], + ), + ), + confirmLabel: autoremove ? context.l10n.removeAll : context.l10n.remove, + packageName: widget.packageName, + ); + } +} + +class _DepsDialog extends StatelessWidget { + final VoidCallback onConfirm; + final String packageName; + final Widget body; + final Widget? footer; + final String confirmLabel; + final List dependencies; + + const _DepsDialog({ + required this.onConfirm, + required this.dependencies, + required this.body, + this.footer, + required this.confirmLabel, + required this.packageName, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return AlertDialog( + title: SizedBox( + width: 500, + child: YaruDialogTitleBar( + title: Text(context.l10n.dependencies), + ), + ), + titlePadding: EdgeInsets.zero, + content: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: kYaruPagePadding / 2), + child: body, + ), + Flexible( + child: SingleChildScrollView( + child: YaruExpandable( + expandButtonPosition: YaruExpandableButtonPosition.start, + header: MouseRegion( + cursor: SystemMouseCursors.click, + child: Text( + context.l10n.dependencies, + style: const TextStyle( + fontWeight: FontWeight.w500, + ), + ), + ), + child: BorderContainer( + child: Column( + children: [ + for (var d in dependencies) + ListTile( + title: Text(d.id.name), + subtitle: Text( + d.summary ?? context.l10n.unknown, + style: TextStyle( + color: Theme.of(context).hintColor, + ), + ), + leading: Icon( + YaruIcons.package_deb, + color: theme.colorScheme.onSurface, + ), + trailing: Text(d.size.formatByteSize()), + ) + ], + ), + ), + ), + ), + ), + if (footer != null) footer!, + ], + ), + actions: [ + OutlinedButton( + onPressed: () => Navigator.pop(context), + child: Text(context.l10n.cancel), + ), + ElevatedButton( + onPressed: () { + onConfirm(); + Navigator.of(context).pop(); + }, + child: Text(confirmLabel), + ) + ], + ); + } +} diff --git a/lib/app/common/packagekit/package_controls.dart b/lib/app/common/packagekit/package_controls.dart index 0af908400..92508ceaf 100644 --- a/lib/app/common/packagekit/package_controls.dart +++ b/lib/app/common/packagekit/package_controls.dart @@ -16,6 +16,9 @@ */ import 'package:flutter/material.dart'; +import 'package:packagekit/packagekit.dart'; +import 'package:provider/provider.dart'; +import 'package:software/app/common/packagekit/package_model.dart'; import 'package:software/l10n/l10n.dart'; import 'package:software/services/packagekit/package_state.dart'; import 'package:yaru_widgets/yaru_widgets.dart'; @@ -23,61 +26,66 @@ import 'package:yaru_widgets/yaru_widgets.dart'; class PackageControls extends StatelessWidget { const PackageControls({ super.key, - required this.isInstalled, - required this.install, - required this.remove, - required this.packageState, - required this.versionChanged, - this.hasDependencies, - this.showDeps, + this.showInstallDeps, + this.showRemoveDeps, }); - final bool? isInstalled; - final VoidCallback install; - final VoidCallback remove; - final PackageState packageState; - final bool? versionChanged; - final bool? hasDependencies; - final VoidCallback? showDeps; + final VoidCallback? showInstallDeps; + final VoidCallback? showRemoveDeps; @override Widget build(BuildContext context) { + final model = context.watch(); return Wrap( crossAxisAlignment: WrapCrossAlignment.center, alignment: WrapAlignment.start, runAlignment: WrapAlignment.start, spacing: 10, runSpacing: 10, - children: packageState == PackageState.processing + children: model.packageState != PackageState.ready ? [ - const SizedBox( + SizedBox( height: 40, child: Padding( - padding: EdgeInsets.all(8.0), + padding: const EdgeInsets.all(8.0), child: YaruCircularProgressIndicator( strokeWidth: 3, + value: model.percentage / 100.0, ), ), ), - Text(context.l10n.processing), + Text(model.packageState.localize(context.l10n)), + if (model.status == PackageKitStatus.download) + Text( + '(${context.l10n.downloadRemaining( + model.getFormattedDownloadSizeRemaining(), + )})', + ), ] : [ - if (isInstalled == true) + if (model.isInstalled == true) OutlinedButton( - onPressed: packageState != PackageState.ready ? null : remove, + onPressed: model.packageState != PackageState.ready + ? null + : model.dependencies.isNotEmpty + ? showRemoveDeps + : model.remove, child: Text(context.l10n.remove), ), - if (isInstalled == false) + if (model.isInstalled == false) ElevatedButton( - onPressed: packageState != PackageState.ready + onPressed: model.packageState != PackageState.ready ? null - : (hasDependencies == true ? showDeps : install), + : model.dependencies.isNotEmpty + ? showInstallDeps + : model.install, child: Text(context.l10n.install), ), - if (isInstalled == true && versionChanged == true) + if (model.isInstalled == true && model.versionChanged == true) ElevatedButton( - onPressed: - packageState != PackageState.ready ? null : install, + onPressed: model.packageState != PackageState.ready + ? null + : model.install, child: Text(context.l10n.update), ), const SizedBox.shrink() diff --git a/lib/app/common/packagekit/package_model.dart b/lib/app/common/packagekit/package_model.dart index 4c8ed9f57..b8d4c04a5 100644 --- a/lib/app/common/packagekit/package_model.dart +++ b/lib/app/common/packagekit/package_model.dart @@ -16,7 +16,6 @@ */ import 'dart:async'; -import 'dart:io'; import 'package:appstream/appstream.dart'; import 'package:collection/collection.dart'; @@ -65,9 +64,12 @@ class PackageModel extends SafeChangeNotifier { String? get title => appstream?.localizedName() ?? packageId?.name; - String? get developerName { - final devName = appstream?.developerName[ - WidgetsBinding.instance.window.locale.countryCode?.toLowerCase()] ?? + String? getDeveloperName(BuildContext context) { + final devName = appstream?.developerName[View.of(context) + .platformDispatcher + .locale + .countryCode + ?.toLowerCase()] ?? appstream?.developerName['C']; return devName ?? appstream?.localizedName(); @@ -79,9 +81,7 @@ class PackageModel extends SafeChangeNotifier { return null; } - return DateFormat.yMd(Platform.localeName) - .add_jms() - .format(appstream!.releases.first.date!.toLocal()); + return DateFormat.yMd().format(appstream!.releases.first.date!.toLocal()); } List get screenshotUrls => @@ -93,20 +93,25 @@ class PackageModel extends SafeChangeNotifier { String? get iconUrl => appstream?.icon; - Future init({bool getUpdateDetail = false}) async { + Future init({ + bool getUpdateDetail = false, + bool getDependencies = true, + }) async { await _service.cancelCurrentUpdatesRefresh(); if (_packageId != null) { + await _service.isInstalled(model: this); await _updateDetails(); if (getUpdateDetail) { await _service.getUpdateDetail(model: this); } } else if (_path != null) { + isInstalled = false; await _service.getDetailsAboutLocalPackage(model: this); } _info = null; - await checkDependencies(); - - return _service.isInstalled(model: this).then(_updatePercentage); + if (getDependencies) { + await checkDependencies(); + } } PackageKitPackageId? _packageId; @@ -133,6 +138,14 @@ class PackageModel extends SafeChangeNotifier { notifyListeners(); } + PackageKitStatus _status = PackageKitStatus.unknown; + PackageKitStatus get status => _status; + set status(PackageKitStatus status) { + if (status == _status) return; + _status = status; + notifyListeners(); + } + // The group this package belongs to. PackageKitGroup? _group; PackageKitGroup? get group => _group; @@ -197,6 +210,16 @@ class PackageModel extends SafeChangeNotifier { notifyListeners(); } + String getFormattedDownloadSizeRemaining() => + _downloadSizeRemaining.formatByteSize(); + int _downloadSizeRemaining = 0; + int get downloadSizeRemaining => _downloadSizeRemaining; + set downloadSizeRemaining(int value) { + if (value == _downloadSizeRemaining) return; + _downloadSizeRemaining = value; + notifyListeners(); + } + String _changelog = ''; String get changelog => _changelog; set changelog(String value) { @@ -234,56 +257,73 @@ class PackageModel extends SafeChangeNotifier { return _service.getDetails(model: this); } - void _updatePercentage([void _]) { - if (isInstalled != null) { - percentage = isInstalled! ? 100 : 0; - } - } - Future install() async { if (_path != null) { return _service .installLocalFile(model: this) .then(_updateDetails) - .then(_updatePercentage); + .then((_) => checkDependencies()); } else if (_packageId != null) { return _service .install(model: this) .then(_updateDetails) - .then(_updatePercentage); + .then((_) => checkDependencies()); } } - Future remove() async { + Future remove({bool autoremove = false}) async { return _service - .remove(model: this) + .remove(model: this, autoremove: autoremove) .then(_updateDetails) - .then(_updatePercentage); + .then((_) => checkDependencies()); } - Map? _dependencies; - Map? get dependencies => _dependencies; - set dependencies(Map? value) { - if (value == null) return; - _dependencies = value; + List _dependencies = []; + UnmodifiableListView get dependencies => + UnmodifiableListView(_dependencies); + set dependencies(List value) { + if (listEquals(_dependencies, value)) return; + _dependencies = value.toList(); notifyListeners(); } - List get uninstalledDependencyNames => dependencies != null - ? dependencies!.entries - .where( - (element) => element.value == PackageKitInfo.available, - ) - .map((e) => e.key.name) - .toList() - : []; - Future checkDependencies() async { - if (_packageId == null) return; - await _service.getDependencies(model: this); + if (_packageId == null || isInstalled == null) return; + if (isInstalled!) { + await _service.getInstalledDependencies(model: this); + } else { + await _service.getMissingDependencies(model: this); + } } @override String toString() => 'PackageModel($_packageId, $_path, ${describeEnum(_packageState)})'; } + +@immutable +class PackageDependecy { + const PackageDependecy({ + required this.id, + required this.info, + required this.size, + this.summary, + }); + final PackageKitPackageId id; + final PackageKitInfo info; + final int size; + final String? summary; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is PackageDependecy && + other.id == id && + other.info == info && + other.size == size && + other.summary == summary; + } + + @override + int get hashCode => Object.hash(id, info, size, summary); +} diff --git a/lib/app/common/packagekit/package_page.dart b/lib/app/common/packagekit/package_page.dart index 4679022a1..caae57afe 100644 --- a/lib/app/common/packagekit/package_page.dart +++ b/lib/app/common/packagekit/package_page.dart @@ -16,6 +16,8 @@ */ import 'package:appstream/appstream.dart'; +import 'package:collection/collection.dart'; +import 'package:data_size/data_size.dart'; import 'package:flutter/material.dart'; import 'package:packagekit/packagekit.dart'; import 'package:provider/provider.dart'; @@ -27,6 +29,7 @@ import 'package:software/app/common/app_page/app_format_toggle_buttons.dart'; import 'package:software/app/common/app_page/app_page.dart'; import 'package:software/app/common/app_rating.dart'; import 'package:software/app/common/border_container.dart'; +import 'package:software/app/common/packagekit/dependency_dialogs.dart'; import 'package:software/app/common/packagekit/package_controls.dart'; import 'package:software/app/common/packagekit/package_model.dart'; import 'package:software/app/common/rating_model.dart'; @@ -39,16 +42,19 @@ import 'package:software/services/packagekit/package_service.dart'; import 'package:ubuntu_service/ubuntu_service.dart'; import 'package:yaru_icons/yaru_icons.dart'; import 'package:yaru_widgets/yaru_widgets.dart'; +import '../expandable_title.dart'; class PackagePage extends StatefulWidget { const PackagePage({ super.key, this.appstream, this.snap, + this.enableSearch = true, }); final AppstreamComponent? appstream; final Snap? snap; + final bool enableSearch; static Widget create({ String? path, @@ -56,6 +62,7 @@ class PackagePage extends StatefulWidget { PackageKitPackageId? packageId, AppstreamComponent? appstream, Snap? snap, + bool enableSearch = true, }) { return MultiProvider( providers: [ @@ -74,6 +81,7 @@ class PackagePage extends StatefulWidget { child: PackagePage( appstream: appstream, snap: snap, + enableSearch: enableSearch, ), ); } @@ -84,6 +92,7 @@ class PackagePage extends StatefulWidget { AppstreamComponent? appstream, Snap? snap, bool replace = false, + bool enableSearch = true, }) { assert(id != null || appstream != null); return (id == null ? appstream!.packageKitId : Future.value(id)).then( @@ -97,6 +106,7 @@ class PackagePage extends StatefulWidget { packageId: id, appstream: appstream, snap: snap, + enableSearch: enableSearch, ); }, ), @@ -110,6 +120,7 @@ class PackagePage extends StatefulWidget { packageId: id, appstream: appstream, snap: snap, + enableSearch: enableSearch, ); }, ), @@ -133,7 +144,10 @@ class _PackagePageState extends State { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; - context.read().init().then((value) => initialized = true); + context + .read() + .init() + .then((_) => setState(() => initialized = true)); context.read().load(_ratingId, _ratingVersion); }); } @@ -146,7 +160,9 @@ class _PackagePageState extends State { final userReviews = context.select((ReviewModel m) => m.userReviews); final appData = AppData( - publisherName: model.developerName ?? context.l10n.unknown, + publisherName: model.getDeveloperName(context) ?? context.l10n.unknown, + publisherUsername: + model.getDeveloperName(context) ?? context.l10n.unknown, releasedAt: model.releasedAt ?? context.l10n.unknown, appSize: model.getFormattedSize() ?? context.l10n.unknown, confinementName: context.l10n.classic, @@ -162,7 +178,7 @@ class _PackagePageState extends State { screenShotUrls: model.screenshotUrls, description: model.description, userReviews: userReviews ?? [], - averageRating: rating?.average ?? 0.0, + appRating: rating, appFormat: AppFormat.packageKit, versionChanged: model.versionChanged ?? false, contact: context.l10n.unknown, @@ -171,10 +187,17 @@ class _PackagePageState extends State { ); final preControls = widget.snap == null - ? const BorderContainer( - padding: EdgeInsets.symmetric(horizontal: 5), + ? BorderContainer( + color: theme.dividerColor, + padding: const EdgeInsets.symmetric(horizontal: 5), borderRadius: 6, - child: SizedBox(height: 40, child: DebianLabel()), + child: const SizedBox( + height: 40, + child: AppFormatLabel( + appFormat: AppFormat.packageKit, + isSelected: true, + ), + ), ) : AppFormatToggleButtons( isSelected: const [ @@ -188,24 +211,27 @@ class _PackagePageState extends State { appstream: widget.appstream, snap: widget.snap!, replace: true, + enableSearch: widget.enableSearch, ); } }, ); var controls = PackageControls( - isInstalled: model.isInstalled, - versionChanged: model.versionChanged, - packageState: model.packageState, - remove: () => model.remove(), - install: model.install, - hasDependencies: model.uninstalledDependencyNames.isNotEmpty, - showDeps: () => showDialog( + showInstallDeps: () => showDialog( context: context, - builder: (context) => _ShowDepsDialog( + builder: (context) => InstallDepsDialog( packageName: model.title ?? context.l10n.unknown, onInstall: model.install, - dependencies: model.uninstalledDependencyNames, + dependencies: model.dependencies, + ), + ), + showRemoveDeps: () => showDialog( + context: context, + builder: (context) => RemoveDepsDialog( + packageName: model.title ?? context.l10n.unknown, + onRemove: (autoremove) => model.remove(autoremove: autoremove), + dependencies: model.dependencies, ), ), ); @@ -213,21 +239,32 @@ class _PackagePageState extends State { final dependencies = BorderContainer( initialized: initialized, child: YaruExpandable( - header: Text( - '${context.l10n.dependencies} (${model.uninstalledDependencyNames.length})', - style: Theme.of(context).textTheme.titleLarge, + header: ExpandableContainerTitle( + '${context.l10n.dependencies} (${model.dependencies.length}) - ' + '${model.dependencies.map((d) => d.size).sum.formatByteSize()}', ), child: Padding( padding: const EdgeInsets.only(top: 10), child: Column( - children: model.uninstalledDependencyNames - .map( + children: model.dependencies + .map( (e) => ListTile( - title: Text(e), + title: Text(e.id.name), + subtitle: e.summary != null + ? Text( + e.summary!, + style: TextStyle( + color: Theme.of(context).hintColor, + ), + ) + : null, leading: Icon( YaruIcons.package_deb, color: theme.colorScheme.onSurface, ), + trailing: Text( + e.size.formatByteSize(), + ), ), ) .toList(), @@ -238,16 +275,17 @@ class _PackagePageState extends State { final review = context.read(); return AppPage( + enableSearch: widget.enableSearch, initialized: initialized, appData: appData, + appIsInstalled: model.isInstalled ?? false, icon: AppIcon( iconUrl: model.iconUrl, size: 150, ), preControls: preControls, controls: controls, - subDescription: - model.uninstalledDependencyNames.isEmpty ? null : dependencies, + subDescription: model.dependencies.isEmpty ? null : dependencies, onReviewSend: () => review.submit(_ratingId, _ratingVersion), onRatingUpdate: (v) => review.rating = v, onReviewTitleChanged: (v) => review.title = v, @@ -260,100 +298,3 @@ class _PackagePageState extends State { ); } } - -class _ShowDepsDialog extends StatefulWidget { - final void Function() onInstall; - final String packageName; - final List dependencies; - - const _ShowDepsDialog({ - required this.onInstall, - required this.dependencies, - required this.packageName, - }); - - @override - State<_ShowDepsDialog> createState() => _ShowDepsDialogState(); -} - -class _ShowDepsDialogState extends State<_ShowDepsDialog> { - bool _isExpanded = false; - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - return AlertDialog( - title: SizedBox( - width: 500, - child: YaruDialogTitleBar( - title: Text(context.l10n.dependencies), - ), - ), - titlePadding: EdgeInsets.zero, - scrollable: true, - content: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(bottom: kYaruPagePadding / 2), - child: Text( - context.l10n.dependenciesListing( - widget.dependencies.length, - widget.packageName, - ), - style: theme.textTheme.bodyLarge, - ), - ), - Padding( - padding: const EdgeInsets.only(bottom: kYaruPagePadding), - child: Text( - context.l10n.dependenciesQuestion, - style: theme.textTheme.bodyLarge! - .copyWith(fontWeight: FontWeight.w500), - ), - ), - YaruExpandable( - expandButtonPosition: YaruExpandableButtonPosition.start, - onChange: (isExpanded) => setState(() => _isExpanded = isExpanded), - header: MouseRegion( - cursor: SystemMouseCursors.click, - child: Text( - context.l10n.dependencies, - style: TextStyle( - color: _isExpanded ? null : theme.primaryColor, - fontWeight: FontWeight.w500, - ), - ), - ), - child: BorderContainer( - child: Column( - children: [ - for (var d in widget.dependencies) - ListTile( - title: Text(d), - leading: const Icon( - YaruIcons.package_deb, - ), - ) - ], - ), - ), - ), - ], - ), - actions: [ - OutlinedButton( - onPressed: () => Navigator.pop(context), - child: Text(context.l10n.cancel), - ), - ElevatedButton( - onPressed: () { - widget.onInstall(); - Navigator.of(context).pop(); - }, - child: Text(context.l10n.install), - ) - ], - ); - } -} diff --git a/lib/app/common/packagekit/packagekit_filter_button.dart b/lib/app/common/packagekit/packagekit_filter_button.dart index 1ec3e0d52..0eb7f7b43 100644 --- a/lib/app/common/packagekit/packagekit_filter_button.dart +++ b/lib/app/common/packagekit/packagekit_filter_button.dart @@ -36,7 +36,7 @@ class PackageKitFilterButton extends StatelessWidget { ), ]; }, - child: Text(context.l10n.packageKitFilter), + child: Text(context.l10n.packageType), ); } } diff --git a/lib/app/common/rating_chart.dart b/lib/app/common/rating_chart.dart new file mode 100644 index 000000000..4d5c83270 --- /dev/null +++ b/lib/app/common/rating_chart.dart @@ -0,0 +1,140 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_rating_bar/flutter_rating_bar.dart'; +import 'package:software/app/common/app_rating.dart'; +import 'package:software/app/common/constants.dart'; +import 'package:yaru_icons/yaru_icons.dart'; +import 'package:yaru_widgets/yaru_widgets.dart'; + +class RatingChart extends StatelessWidget { + const RatingChart({super.key, required this.appRating}); + + final AppRating appRating; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return SizedBox( + height: 100, + width: 350, + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + appRating.average?.toStringAsFixed(1) ?? '', + style: const TextStyle( + height: 0.85, + fontSize: 50, + fontWeight: FontWeight.w200, + ), + ), + RatingBar.builder( + initialRating: appRating.average ?? 0, + minRating: 1, + direction: Axis.horizontal, + allowHalfRating: true, + itemCount: 5, + itemPadding: const EdgeInsets.only(right: 1), + itemSize: 15, + itemBuilder: (context, _) => const Icon( + YaruIcons.star_filled, + color: kStarColor, + size: 2, + ), + unratedColor: + Theme.of(context).colorScheme.onSurface.withOpacity(0.2), + onRatingUpdate: (rating) {}, + ignoreGestures: true, + ), + Text( + '${appRating.total} ratings', + style: theme.textTheme.bodySmall, + ) + ], + ), + const SizedBox( + width: kYaruPagePadding, + ), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (appRating.star5 != null) + _RatingBar( + starXAmount: appRating.star5!, + total: appRating.total!, + label: '5', + color: const Color.fromARGB(255, 54, 177, 52), + ), + if (appRating.star4 != null) + _RatingBar( + starXAmount: appRating.star4!, + total: appRating.total!, + label: '4', + color: const Color(0xFFb1cf00), + ), + if (appRating.star3 != null) + _RatingBar( + starXAmount: appRating.star3!, + total: appRating.total!, + label: '3', + color: const Color(0xFFd49e00), + ), + if (appRating.star2 != null) + _RatingBar( + starXAmount: appRating.star2!, + total: appRating.total!, + label: '2', + color: const Color(0xFFe56500), + ), + if (appRating.star1 != null) + _RatingBar( + starXAmount: appRating.star1!, + total: appRating.total!, + label: '1', + color: const Color(0xFFe21033), + ), + ], + ), + ), + ], + ), + ); + } +} + +class _RatingBar extends StatelessWidget { + const _RatingBar({ + required this.starXAmount, + required this.total, + required this.label, + required this.color, + }); + + final int starXAmount; + final int total; + final String label; + final Color color; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Text( + label, + ), + const SizedBox( + width: 5, + ), + Expanded( + child: YaruLinearProgressIndicator( + color: color, + value: (starXAmount / total).toDouble(), + ), + ), + ], + ); + } +} diff --git a/lib/app/common/rating_model.dart b/lib/app/common/rating_model.dart index 3e99810ab..40d8d15ec 100644 --- a/lib/app/common/rating_model.dart +++ b/lib/app/common/rating_model.dart @@ -24,5 +24,14 @@ class RatingModel extends SafeChangeNotifier { } extension on OdrsRating { - AppRating toAppRating() => AppRating(average: average, total: total); + AppRating toAppRating() => AppRating( + average: average, + total: total, + star0: star0, + star1: star1, + star2: star2, + star3: star3, + star4: star4, + star5: star5, + ); } diff --git a/lib/app/common/search_field.dart b/lib/app/common/search_field.dart index 9d6a77952..567870de3 100644 --- a/lib/app/common/search_field.dart +++ b/lib/app/common/search_field.dart @@ -91,6 +91,12 @@ class _SearchFieldState extends State { width: 280, height: 34, child: TextField( + style: theme.textTheme.bodyMedium, + strutStyle: const StrutStyle( + leading: 0.2, + ), + textAlignVertical: TextAlignVertical.center, + cursorWidth: 1, autofocus: widget.autofocus, controller: _controller, onChanged: onChanged, @@ -100,10 +106,10 @@ class _SearchFieldState extends State { hintText: widget.hintText, prefixIcon: const Icon( YaruIcons.search, - size: 15, + size: 16, ), prefixIconConstraints: - const BoxConstraints(minWidth: 40, minHeight: 0), + const BoxConstraints(minWidth: 34, minHeight: 30), suffixIcon: widget.searchQuery != null && widget.searchQuery!.isNotEmpty ? SizedBox( @@ -113,10 +119,9 @@ class _SearchFieldState extends State { color: Colors.transparent, child: InkWell( onTap: _clear, - child: Center( + child: const Center( child: Icon( YaruIcons.edit_clear, - color: Theme.of(context).hintColor, ), ), ), @@ -126,7 +131,7 @@ class _SearchFieldState extends State { suffixIconConstraints: const BoxConstraints(maxWidth: 30, minHeight: 0), isDense: true, - contentPadding: const EdgeInsets.all(8), + contentPadding: const EdgeInsets.fromLTRB(12, 12, 12, 18), fillColor: light ? Colors.white : Theme.of(context).dividerColor, enabledBorder: OutlineInputBorder( diff --git a/lib/app/common/snap/snap_channel_button.dart b/lib/app/common/snap/snap_channel_button.dart index e9b56ee63..65cb05f11 100644 --- a/lib/app/common/snap/snap_channel_button.dart +++ b/lib/app/common/snap/snap_channel_button.dart @@ -37,6 +37,7 @@ class SnapChannelPopupButton extends StatelessWidget { final light = theme.brightness == Brightness.light; return YaruPopupMenuButton( + padding: const EdgeInsets.only(left: 15, right: 5), initialValue: model.channelToBeInstalled, tooltip: context.l10n.channel, itemBuilder: (v) => [ @@ -83,18 +84,18 @@ class _Item extends StatelessWidget { Widget build(BuildContext context) { final theme = Theme.of(context); final labelStyle = TextStyle( - color: theme.disabledColor, - fontSize: 14, + fontWeight: FontWeight.normal, + color: theme.hintColor, ); const infoStyle = TextStyle( overflow: TextOverflow.ellipsis, - fontSize: 14, + fontWeight: FontWeight.normal, ); return Column( mainAxisSize: MainAxisSize.min, children: [ Padding( - padding: const EdgeInsets.all(8.0), + padding: const EdgeInsets.all(10.0), child: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -119,7 +120,7 @@ class _Item extends StatelessWidget { textAlign: TextAlign.end, ), Text( - context.l10n.releasedAt, + context.l10n.lastUpdated, style: labelStyle, maxLines: 1, textAlign: TextAlign.end, diff --git a/lib/app/common/snap/snap_controls.dart b/lib/app/common/snap/snap_controls.dart index 4e338af67..d1a482490 100644 --- a/lib/app/common/snap/snap_controls.dart +++ b/lib/app/common/snap/snap_controls.dart @@ -54,17 +54,24 @@ class SnapControls extends StatelessWidget { value: model.change?.progress, ), ), - if (model.change != null) + if (model.change != null) ...[ Text( getChangeMessage( context: context, changeKind: model.change!.kind, ), ), + if (model.change!.kind != 'remove-snap') + OutlinedButton( + onPressed: model.abortChange, + child: Text(context.l10n.cancel), + ), + ] ] : [ if (model.selectableChannels.isNotEmpty && - model.selectableChannels.length > 1) + model.selectableChannels.length > 1 && + appstream != null) const SnapChannelPopupButton(), if (model.snapIsInstalled) OutlinedButton( @@ -95,20 +102,13 @@ class SnapControls extends StatelessWidget { String getChangeMessage({ required BuildContext context, required String? changeKind, - }) { - switch (changeKind) { - case 'install-snap': - return context.l10n.installing; - case 'remove-snap': - return context.l10n.removing; - case 'refresh-snap': - return context.l10n.refreshing; - case 'connect-snap': - return context.l10n.changingPermissions; - case 'disconnect-snap': - return context.l10n.changingPermissions; - default: - return ''; - } - } + }) => + switch (changeKind) { + 'install-snap' => context.l10n.installing, + 'remove-snap' => context.l10n.removing, + 'refresh-snap' => context.l10n.refreshing, + 'connect-snap' => context.l10n.changingPermissions, + 'disconnect-snap' => context.l10n.changingPermissions, + _ => '' + }; } diff --git a/lib/app/common/snap/snap_model.dart b/lib/app/common/snap/snap_model.dart index 25d78c765..456945617 100644 --- a/lib/app/common/snap/snap_model.dart +++ b/lib/app/common/snap/snap_model.dart @@ -38,6 +38,7 @@ class SnapModel extends SafeChangeNotifier { await _snapService.authorize(); await _loadSnapChangeInProgress(); await _loadChange(); + await _snapService.loadSnapsWithUpdate(); _localSnap = await _findLocalSnap(huskSnapName); if (online) { @@ -144,7 +145,7 @@ class SnapModel extends SafeChangeNotifier { String get selectedChannelReleasedAt => selectableChannels[channelToBeInstalled] != null - ? DateFormat.yMd(Platform.localeName) + ? DateFormat.yMMMd(Platform.localeName) .format(selectableChannels[channelToBeInstalled]!.releasedAt) : ''; @@ -297,6 +298,11 @@ class SnapModel extends SafeChangeNotifier { Future _loadChange() async => change = (await _snapService.getSnapChanges(name: huskSnapName)); + Future abortChange() async { + await _snapService.abortChange(_storeSnap!); + return _loadChange(); + } + Future _findLocalSnap(String huskSnapName) async => _snapService.findLocalSnap(huskSnapName); @@ -401,4 +407,9 @@ class SnapModel extends SafeChangeNotifier { } return ''; } + + bool isUpdateAvailable() => + _snapService.snapsWithUpdate + .indexWhere((snap) => snap.name == huskSnapName) >= + 0; } diff --git a/lib/app/common/snap/snap_page.dart b/lib/app/common/snap/snap_page.dart index 874539273..521b72a26 100644 --- a/lib/app/common/snap/snap_page.dart +++ b/lib/app/common/snap/snap_page.dart @@ -26,10 +26,10 @@ import 'package:software/app/common/app_icon.dart'; import 'package:software/app/common/app_page/app_format_toggle_buttons.dart'; import 'package:software/app/common/app_page/app_page.dart'; import 'package:software/app/common/app_rating.dart'; -import 'package:software/app/common/border_container.dart'; import 'package:software/app/common/packagekit/package_page.dart'; import 'package:software/app/common/rating_model.dart'; import 'package:software/app/common/review_model.dart'; +import 'package:software/app/common/snap/snap_channel_button.dart'; import 'package:software/app/common/snap/snap_connections_button.dart'; import 'package:software/app/common/snap/snap_connections_dialog.dart'; import 'package:software/app/common/snap/snap_controls.dart'; @@ -38,19 +38,27 @@ import 'package:software/l10n/l10n.dart'; import 'package:software/services/odrs_service.dart'; import 'package:software/services/snap_service.dart'; import 'package:ubuntu_service/ubuntu_service.dart'; +import 'package:yaru_widgets/yaru_widgets.dart'; class SnapPage extends StatefulWidget { - const SnapPage({super.key, this.appstream, required this.snap}); + const SnapPage({ + super.key, + this.appstream, + required this.snap, + this.enableSearch = true, + }); /// Optional AppstreamComponent if found final AppstreamComponent? appstream; final Snap snap; + final bool enableSearch; static Widget create({ required BuildContext context, required Snap snap, PackageKitPackageId? packageId, AppstreamComponent? appstream, + bool enableSearch = true, }) => MultiProvider( providers: [ @@ -68,6 +76,7 @@ class SnapPage extends StatefulWidget { child: SnapPage( appstream: appstream, snap: snap, + enableSearch: enableSearch, ), ); @@ -76,6 +85,7 @@ class SnapPage extends StatefulWidget { required Snap snap, AppstreamComponent? appstream, bool replace = false, + bool enableSearch = true, }) { final route = MaterialPageRoute( builder: (BuildContext context) { @@ -83,6 +93,7 @@ class SnapPage extends StatefulWidget { context: context, snap: snap, appstream: appstream, + enableSearch: enableSearch, ); }, ); @@ -119,6 +130,7 @@ class _SnapPageState extends State { final model = context.watch(); final rating = context.select((RatingModel m) => m.getRating(_ratingId)); final userReviews = context.select((ReviewModel m) => m.userReviews); + final theme = Theme.of(context); final appData = AppData( releasedAt: model.selectedChannelReleasedAt, @@ -131,6 +143,7 @@ class _SnapPageState extends State { verified: model.verified, starredDeveloper: model.starredDeveloper, publisherName: model.publisher?.displayName ?? '', + publisherUsername: model.publisher?.username ?? '', website: model.storeUrl ?? '', summary: model.summary ?? '', title: model.title ?? '', @@ -139,11 +152,9 @@ class _SnapPageState extends State { model.version, screenShotUrls: model.screenshotUrls ?? [], description: model.description ?? '', - versionChanged: - model.selectableChannels[model.channelToBeInstalled]?.version != - model.version, + versionChanged: model.isUpdateAvailable(), userReviews: userReviews ?? [], - averageRating: rating?.average ?? 0.0, + appRating: rating, appFormat: AppFormat.snap, contact: model.contact ?? context.l10n.unknown, ); @@ -152,6 +163,46 @@ class _SnapPageState extends State { appstream: widget.appstream, ); + const snapLabel = SizedBox( + height: 39, + child: AppFormatLabel( + appFormat: AppFormat.snap, + isSelected: true, + ), + ); + + final snapLabelContainerCut = YaruBorderContainer( + color: theme.colorScheme.outline, + padding: const EdgeInsets.symmetric(horizontal: 5), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(kYaruButtonRadius), + bottomLeft: Radius.circular(kYaruButtonRadius), + ), + child: snapLabel, + ); + + final snapLabelWithChannelButton = Row( + mainAxisSize: MainAxisSize.min, + children: [ + snapLabelContainerCut, + OutlinedButtonTheme( + data: OutlinedButtonThemeData( + style: OutlinedButtonTheme.of(context).style?.copyWith( + shape: MaterialStateProperty.resolveWith( + (states) => const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topRight: Radius.circular(kYaruButtonRadius), + bottomRight: Radius.circular(kYaruButtonRadius), + ), + ), + ), + ), + ), + child: const SnapChannelPopupButton(), + ) + ], + ); + final preControls = Wrap( spacing: 10, children: [ @@ -173,11 +224,7 @@ class _SnapPageState extends State { }, ) else - const BorderContainer( - padding: EdgeInsets.symmetric(horizontal: 5), - borderRadius: 6, - child: SizedBox(height: 39, child: SnapLabel()), - ), + snapLabelWithChannelButton, if (model.snapIsInstalled && model.strict) SnapConnectionsButton( onPressed: () => showDialog( @@ -193,6 +240,7 @@ class _SnapPageState extends State { final review = context.read(); return AppPage( + enableSearch: widget.enableSearch, initialized: initialized, appData: appData, appIsInstalled: model.snapIsInstalled, diff --git a/lib/app/common/snap/snap_section.dart b/lib/app/common/snap/snap_section.dart index ed8025e1e..4eb20eff9 100644 --- a/lib/app/common/snap/snap_section.dart +++ b/lib/app/common/snap/snap_section.dart @@ -47,206 +47,161 @@ enum SnapSection { String get title => name.replaceAll('_', '-'); - String localize(AppLocalizations l10n) { - switch (this) { - case SnapSection.art_and_design: - return l10n.artAndDesign; - case SnapSection.books_and_reference: - return l10n.booksAndReference; - case SnapSection.development: - return l10n.development; - case SnapSection.devices_and_iot: - return l10n.devicesAndIot; - case SnapSection.education: - return l10n.education; - case SnapSection.entertainment: - return l10n.entertainment; - case SnapSection.featured: - return l10n.featured; - case SnapSection.finance: - return l10n.finance; - case SnapSection.games: - return l10n.games; - case SnapSection.health_and_fitness: - return l10n.healthAndFitness; - case SnapSection.music_and_audio: - return l10n.musicAndAudio; - case SnapSection.news_and_weather: - return l10n.newsAndWeather; - case SnapSection.personalisation: - return l10n.personalisation; - case SnapSection.photo_and_video: - return l10n.photoAndVideo; - case SnapSection.productivity: - return l10n.productivity; - case SnapSection.science: - return l10n.science; - case SnapSection.security: - return l10n.security; - case SnapSection.server_and_cloud: - return l10n.serverAndCloud; - case SnapSection.social: - return l10n.social; - case SnapSection.utilities: - return l10n.utilities; - case SnapSection.all: - return l10n.all; - default: - return title; - } - } + String localize(AppLocalizations l10n) => switch (this) { + SnapSection.art_and_design => l10n.artAndDesign, + SnapSection.books_and_reference => l10n.booksAndReference, + SnapSection.development => l10n.development, + SnapSection.devices_and_iot => l10n.devicesAndIot, + SnapSection.education => l10n.education, + SnapSection.entertainment => l10n.entertainment, + SnapSection.featured => l10n.featured, + SnapSection.finance => l10n.finance, + SnapSection.games => l10n.games, + SnapSection.health_and_fitness => l10n.healthAndFitness, + SnapSection.music_and_audio => l10n.musicAndAudio, + SnapSection.news_and_weather => l10n.newsAndWeather, + SnapSection.personalisation => l10n.personalisation, + SnapSection.photo_and_video => l10n.photoAndVideo, + SnapSection.productivity => l10n.productivity, + SnapSection.science => l10n.science, + SnapSection.security => l10n.security, + SnapSection.server_and_cloud => l10n.serverAndCloud, + SnapSection.social => l10n.social, + SnapSection.utilities => l10n.utilities, + SnapSection.all => l10n.all, + }; + + String slogan(AppLocalizations l10n) => switch (this) { + SnapSection.art_and_design => l10n.artAndDesignSlogan, + SnapSection.books_and_reference => l10n.booksAndReferenceSlogan, + SnapSection.development => l10n.developmentSlogan, + SnapSection.devices_and_iot => l10n.devicesAndIotSlogan, + SnapSection.education => l10n.educationSlogan, + SnapSection.entertainment => l10n.entertainmentSlogan, + SnapSection.featured => l10n.featuredSlogan, + SnapSection.finance => l10n.financeSlogan, + SnapSection.games => l10n.gamesSlogan, + SnapSection.health_and_fitness => l10n.healthAndFitnessSlogan, + SnapSection.music_and_audio => l10n.musicAndAudioSlogan, + SnapSection.news_and_weather => l10n.newsAndWeatherSlogan, + SnapSection.personalisation => l10n.personalisationSlogan, + SnapSection.photo_and_video => l10n.photoAndVideoSlogan, + SnapSection.productivity => l10n.productivitySlogan, + SnapSection.science => l10n.scienceSlogan, + SnapSection.security => l10n.securitySlogan, + SnapSection.server_and_cloud => l10n.serverAndCloudSlogan, + SnapSection.social => l10n.socialSlogan, + SnapSection.utilities => l10n.utilitiesSlogan, + SnapSection.all => l10n.featuredSlogan + }; - String slogan(AppLocalizations l10n) { + List get colors => switch (this) { + SnapSection.art_and_design => [ + const Color.fromARGB(255, 0, 5, 148).value, + const Color.fromARGB(255, 255, 155, 179).value + ], + SnapSection.books_and_reference => [ + const Color.fromARGB(255, 59, 54, 54).value, + const Color.fromARGB(108, 0, 114, 229).value + ], + SnapSection.development => [ + const Color.fromARGB(255, 54, 0, 80).value, + const Color.fromARGB(255, 225, 59, 149).value + ], + SnapSection.devices_and_iot => [ + const Color.fromARGB(255, 71, 71, 71).value, + YaruColors.red.value + ], + SnapSection.education => [ + const Color.fromARGB(255, 71, 71, 71).value, + YaruColors.magenta.value + ], + SnapSection.entertainment => [ + const Color.fromARGB(255, 163, 98, 12).value, + const Color.fromARGB(255, 255, 137, 26).value + ], + SnapSection.featured => [ + const Color.fromARGB(255, 167, 92, 22).value, + const Color.fromARGB(255, 133, 1, 122).value + ], + SnapSection.finance => [ + const Color.fromARGB(255, 71, 71, 71).value, + YaruColors.purple.value + ], + SnapSection.games => [ + const Color.fromARGB(255, 180, 22, 1).value, + const Color.fromARGB(255, 254, 172, 12).value + ], + SnapSection.health_and_fitness => [ + const Color.fromARGB(255, 86, 23, 122).value, + YaruColors.warning.value + ], + SnapSection.music_and_audio => [ + const Color.fromARGB(255, 119, 10, 43).value, + const Color.fromARGB(157, 233, 0, 58).value + ], + SnapSection.news_and_weather => [ + const Color.fromARGB(255, 165, 115, 44).value, + const Color.fromARGB(255, 255, 219, 101).value + ], + SnapSection.personalisation => [ + const Color.fromARGB(255, 35, 12, 139).value, + const Color.fromARGB(255, 25, 173, 166).value + ], + SnapSection.photo_and_video => [ + const Color.fromARGB(255, 71, 71, 71).value, + const Color.fromARGB(255, 133, 133, 133).value + ], + SnapSection.productivity => [ + const Color.fromARGB(255, 8, 36, 53).value, + const Color.fromARGB(255, 41, 112, 104).value + ], + SnapSection.science => [ + const Color.fromARGB(255, 71, 71, 71).value, + YaruColors.orange.value + ], + SnapSection.security => [ + const Color.fromARGB(255, 16, 40, 49).value, + const Color.fromARGB(255, 19, 131, 112).value + ], + SnapSection.server_and_cloud => [ + const Color.fromARGB(255, 71, 28, 10).value, + YaruColors.orange.value + ], + SnapSection.social => [ + const Color.fromARGB(255, 11, 73, 59).value, + const Color.fromARGB(255, 15, 122, 87).value + ], + SnapSection.utilities => [ + const Color.fromARGB(136, 82, 74, 40).value, + const Color.fromARGB(155, 233, 203, 34).value + ], + SnapSection.all => [ + const Color.fromARGB(255, 112, 0, 69).value, + const Color.fromARGB(255, 233, 84, 32).value + ] + }; + + IconData getIcon(bool selected) { switch (this) { - case SnapSection.art_and_design: - return l10n.artAndDesignSlogan; - case SnapSection.books_and_reference: - return l10n.booksAndReferenceSlogan; + case SnapSection.all: + return selected ? YaruIcons.compass_filled : YaruIcons.compass; case SnapSection.development: - return l10n.developmentSlogan; - case SnapSection.devices_and_iot: - return l10n.devicesAndIotSlogan; - case SnapSection.education: - return l10n.educationSlogan; - case SnapSection.entertainment: - return l10n.entertainmentSlogan; - case SnapSection.featured: - return l10n.featuredSlogan; - case SnapSection.finance: - return l10n.financeSlogan; + return YaruIcons.wrench; case SnapSection.games: - return l10n.gamesSlogan; - case SnapSection.health_and_fitness: - return l10n.healthAndFitnessSlogan; - case SnapSection.music_and_audio: - return l10n.musicAndAudioSlogan; - case SnapSection.news_and_weather: - return l10n.newsAndWeatherSlogan; - case SnapSection.personalisation: - return l10n.personalisationSlogan; - case SnapSection.photo_and_video: - return l10n.photoAndVideoSlogan; - case SnapSection.productivity: - return l10n.productivitySlogan; - case SnapSection.science: - return l10n.scienceSlogan; - case SnapSection.security: - return l10n.securitySlogan; - case SnapSection.server_and_cloud: - return l10n.serverAndCloudSlogan; - case SnapSection.social: - return l10n.socialSlogan; - case SnapSection.utilities: - return l10n.utilitiesSlogan; - case SnapSection.all: - return l10n.featuredSlogan; - } - } - - // TODO: @madsrh please add colors - // Those are normal hex plus the leading FF for alpha, just leave FF - // or take colors from YaruColors - List get colors { - switch (this) { + return selected ? YaruIcons.games_filled : YaruIcons.games; case SnapSection.art_and_design: - return [0xFF12c2e9, 0xFFf64f59]; - case SnapSection.books_and_reference: - return [ - const Color.fromARGB(255, 59, 54, 54).value, - const Color.fromARGB(108, 0, 114, 229).value - ]; - case SnapSection.development: - return [ - const Color.fromARGB(255, 113, 80, 151).value, - const Color.fromARGB(255, 165, 26, 146).value - ]; + return selected + ? YaruIcons.rule_and_pen_filled + : YaruIcons.rule_and_pen; case SnapSection.devices_and_iot: - return [ - const Color.fromARGB(255, 71, 71, 71).value, - YaruColors.red.value - ]; - case SnapSection.education: - return [ - const Color.fromARGB(255, 71, 71, 71).value, - YaruColors.magenta.value - ]; - case SnapSection.entertainment: - return [ - const Color.fromARGB(255, 163, 98, 12).value, - const Color.fromARGB(255, 255, 137, 26).value - ]; - case SnapSection.featured: - return [ - const Color.fromARGB(255, 167, 92, 22).value, - const Color.fromARGB(255, 133, 1, 122).value - ]; - case SnapSection.finance: - return [ - const Color.fromARGB(255, 71, 71, 71).value, - YaruColors.purple.value - ]; - case SnapSection.games: - return [ - const Color.fromARGB(255, 25, 119, 96).value, - const Color.fromARGB(255, 135, 3, 124).value - ]; - case SnapSection.health_and_fitness: - return [ - const Color.fromARGB(255, 86, 23, 122).value, - YaruColors.warning.value - ]; - case SnapSection.music_and_audio: - return [ - const Color.fromARGB(255, 119, 10, 43).value, - const Color.fromARGB(157, 233, 0, 58).value - ]; - case SnapSection.news_and_weather: - return [ - const Color.fromARGB(255, 165, 115, 44).value, - const Color.fromARGB(255, 255, 219, 101).value - ]; - case SnapSection.personalisation: - return [ - const Color.fromARGB(255, 35, 12, 139).value, - const Color.fromARGB(255, 25, 173, 166).value - ]; - case SnapSection.photo_and_video: - return [ - const Color.fromARGB(255, 71, 71, 71).value, - const Color.fromARGB(255, 133, 133, 133).value - ]; - case SnapSection.productivity: - return [const Color(0xFF712290).value, const Color(0xFFff5733).value]; - case SnapSection.science: - return [ - const Color.fromARGB(255, 71, 71, 71).value, - YaruColors.orange.value - ]; - case SnapSection.security: - return [ - const Color.fromARGB(255, 16, 40, 49).value, - const Color.fromARGB(255, 19, 131, 112).value - ]; + return selected ? YaruIcons.chip_filled : YaruIcons.chip; case SnapSection.server_and_cloud: - return [ - const Color.fromARGB(255, 71, 28, 10).value, - YaruColors.orange.value - ]; - case SnapSection.social: - return [ - const Color.fromARGB(255, 11, 73, 59).value, - const Color.fromARGB(255, 15, 122, 87).value - ]; - case SnapSection.utilities: - return [ - const Color.fromARGB(136, 82, 74, 40).value, - const Color.fromARGB(155, 233, 203, 34).value - ]; - case SnapSection.all: - return [ - const Color.fromARGB(255, 167, 92, 22).value, - const Color.fromARGB(255, 133, 1, 122).value - ]; + return selected ? YaruIcons.cloud_filled : YaruIcons.cloud; + case SnapSection.productivity: + return selected ? YaruIcons.send_filled : YaruIcons.send; + default: + return selected ? YaruIcons.compass_filled : YaruIcons.compass; } } } @@ -272,5 +227,5 @@ Map snapSectionToIcon = { SnapSection.server_and_cloud: YaruIcons.cloud, SnapSection.social: YaruIcons.subtitles, SnapSection.utilities: YaruIcons.swiss_knife, - SnapSection.all: YaruIcons.app_grid + SnapSection.all: YaruIcons.application }; diff --git a/lib/app/common/snap/snap_sort.dart b/lib/app/common/snap/snap_sort.dart index 60e013836..9fa26d02e 100644 --- a/lib/app/common/snap/snap_sort.dart +++ b/lib/app/common/snap/snap_sort.dart @@ -22,14 +22,9 @@ enum SnapSort { installDate, size; - String localize(AppLocalizations l10n) { - switch (this) { - case SnapSort.name: - return l10n.name; - case SnapSort.installDate: - return l10n.installDate; - case SnapSort.size: - return l10n.size; - } - } + String localize(AppLocalizations l10n) => switch (this) { + SnapSort.name => l10n.name, + SnapSort.installDate => l10n.installDate, + SnapSort.size => l10n.size + }; } diff --git a/lib/app/common/snap/snap_utils.dart b/lib/app/common/snap/snap_utils.dart index 4a1b94866..aa6b3a346 100644 --- a/lib/app/common/snap/snap_utils.dart +++ b/lib/app/common/snap/snap_utils.dart @@ -1,20 +1,6 @@ import 'package:snapd/snapd.dart'; import 'package:software/app/common/snap/snap_sort.dart'; -bool isSnapUpdateAvailable({required Snap storeSnap, required Snap localSnap}) { - if (storeSnap.name == 'snapcraft') return false; - final version = localSnap.version; - - final selectAbleChannels = getSelectableChannels(storeSnap: storeSnap); - final tracking = getTrackingChannel( - trackingChannel: localSnap.trackingChannel, - selectableChannels: selectAbleChannels, - ); - final trackingVersion = selectAbleChannels[tracking]?.version; - - return trackingVersion != version; -} - Map getSelectableChannels({required Snap? storeSnap}) { Map selectableChannels = {}; if (storeSnap != null && storeSnap.tracks.isNotEmpty) { diff --git a/lib/app/explore/explore_model.dart b/lib/app/explore/explore_model.dart index f616c8391..b78c9e35b 100644 --- a/lib/app/explore/explore_model.dart +++ b/lib/app/explore/explore_model.dart @@ -19,6 +19,7 @@ import 'dart:async'; import 'package:appstream/appstream.dart'; import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; import 'package:safe_change_notifier/safe_change_notifier.dart'; import 'package:snapd/snapd.dart'; import 'package:software/app/common/app_finding.dart'; @@ -33,13 +34,18 @@ class ExploreModel extends SafeChangeNotifier { final AppstreamService _appstreamService; final SnapService _snapService; final PackageService _packageService; - StreamSubscription? _sectionsChangedSub; + StreamSubscription? _sectionsChangedSub; Future init() async { _enabledAppFormats.add(AppFormat.snap); _selectedAppFormats.add(AppFormat.snap); - _sectionsChangedSub = - _snapService.sectionsChanged.listen((_) => notifyListeners()); + _loadStartPageSnaps(SnapSection.all); + _sectionsChangedSub = _snapService.sectionsChanged.listen( + (section) { + _loadStartPageSnaps(section); + notifyListeners(); + }, + ); await _packageService.initialized; if (_packageService.isAvailable) { @@ -47,7 +53,12 @@ class ExploreModel extends SafeChangeNotifier { _enabledAppFormats.add(AppFormat.packageKit); _selectedAppFormats.add(AppFormat.packageKit); notifyListeners(); - }); + }).then( + (_) => Future.forEach( + startPageApps.keys, + _loadStartPageAppstreamComponents, + ), + ); } } @@ -93,8 +104,8 @@ class ExploreModel extends SafeChangeNotifier { notifyListeners(); } - Map> get sectionNameToSnapsMap => - _snapService.sectionNameToSnapsMap; + final startPageApps = >{}; + var startPageAppsChanged = 0; final Set _selectedAppFormats = {}; Set get selectedAppFormats => Set.from(_selectedAppFormats); @@ -125,6 +136,15 @@ class ExploreModel extends SafeChangeNotifier { } } + Future _findSnapByName(String name) async { + try { + return await _snapService.findSnapByName(name); + } on SnapdException catch (e) { + errorMessage = e.message.toString(); + return null; + } + } + Future> _findAppstreamComponents( String searchQuery, ) async => @@ -137,57 +157,116 @@ class ExploreModel extends SafeChangeNotifier { notifyListeners(); } + Map? get filteredSearchResult => _searchResult == null + ? null + : (Map.from(_searchResult!) + ..removeWhere( + (_, appFinding) { + if (setEquals(_selectedAppFormats, {AppFormat.snap})) { + return appFinding.snap == null; + } else if (setEquals(_selectedAppFormats, {AppFormat.packageKit})) { + return appFinding.appstream == null; + } else { + return false; + } + }, + )); + + void _loadStartPageSnaps(SnapSection section) { + if (!_snapService.sectionNameToSnapsMap.containsKey(section)) return; + startPageApps[section] = _snapService.sectionNameToSnapsMap[section]! + .map((s) => AppFinding(snap: s)) + .toList(); + startPageAppsChanged++; + } + + Future _getAppstreamComponentFromSnap(Snap snap) => + _findAppstreamComponents(snap.name).then( + (components) => + components.firstWhereOrNull( + (e) => + e.package == snap.name && + e.type == AppstreamComponentType.desktopApplication, + ) ?? + components.firstWhereOrNull((e) => e.package == snap.name), + ); + + Future _loadStartPageAppstreamComponents( + SnapSection section, + ) async { + if (!startPageApps.containsKey(section)) return; + for (var i = 0; i < startPageApps[section]!.length; i++) { + final appstreamComponent = await _getAppstreamComponentFromSnap( + startPageApps[section]![i].snap!, + ); + await Future.delayed(const Duration(milliseconds: 2)); + if (appstreamComponent != null) { + startPageApps[section]![i] = AppFinding( + snap: startPageApps[section]![i].snap, + appstream: appstreamComponent, + ); + } + } + startPageAppsChanged++; + notifyListeners(); + } + + Future searchByPublisher(String username) async { + setSearchQuery(username); + + searchResult = null; + + final Map appFindings = {}; + if (searchQuery != null && searchQuery != '') { + final snaps = await _findSnapsByQuery(searchQuery!); + final publishersSnaps = + snaps.where((snap) => snap.publisher?.username == username); + + for (final snap in publishersSnaps) { + appFindings.putIfAbsent( + snap.name, + () => AppFinding(snap: snap), + ); + } + + searchResult = appFindings; + } + } + Future search() async { searchResult = null; final Map appFindings = {}; if (searchQuery != null && searchQuery != '') { - if (selectedAppFormats - .containsAll([AppFormat.snap, AppFormat.packageKit])) { - final snaps = await _findSnapsByQuery(searchQuery!); - for (final snap in snaps) { - appFindings.putIfAbsent( - snap.name, - () => AppFinding(snap: snap), - ); - } + final snaps = await _findSnapsByQuery(searchQuery!); + final exactMatch = await _findSnapByName(searchQuery!); + if (exactMatch != null) { + snaps.insert(0, exactMatch); + } + for (final snap in snaps) { + appFindings.putIfAbsent( + snap.name, + () => AppFinding(snap: snap), + ); + } - final components = await _findAppstreamComponents(searchQuery!); - for (final component in components) { - final snap = - snaps.firstWhereOrNull((snap) => snap.name == component.package); - if (snap == null) { - appFindings.putIfAbsent( - component.localizedName(), - () => AppFinding(appstream: component), - ); - } else { - appFindings.update( - snap.name, - (value) => AppFinding( - snap: snap, - appstream: component, - ), - ); - } - } - } else if (selectedAppFormats.contains(AppFormat.snap) && - !(selectedAppFormats.contains(AppFormat.packageKit))) { - final snaps = await _findSnapsByQuery(searchQuery!); - for (final snap in snaps) { - appFindings.putIfAbsent( - snap.name, - () => AppFinding(snap: snap), - ); - } - } else if (!selectedAppFormats.contains(AppFormat.snap) && - (selectedAppFormats.contains(AppFormat.packageKit))) { - final components = await _findAppstreamComponents(searchQuery!); - for (final component in components) { + final components = await _findAppstreamComponents(searchQuery!); + for (final component in components) { + final snap = + snaps.firstWhereOrNull((snap) => snap.name == component.package); + if (snap == null) { appFindings.putIfAbsent( component.localizedName(), () => AppFinding(appstream: component), ); + } else { + appFindings.update( + snap.name, + (value) => AppFinding( + snap: snap, + appstream: component, + ), + ); } } diff --git a/lib/app/explore/explore_page.dart b/lib/app/explore/explore_page.dart index 32b5d292b..bdc5c28bc 100644 --- a/lib/app/explore/explore_page.dart +++ b/lib/app/explore/explore_page.dart @@ -22,6 +22,7 @@ import 'package:provider/provider.dart'; import 'package:software/app/app_model.dart'; import 'package:software/app/common/app_format.dart'; import 'package:software/app/common/connectivity_notifier.dart'; +import 'package:software/app/common/constants.dart'; import 'package:software/app/common/search_field.dart'; import 'package:software/app/common/snap/snap_section.dart'; import 'package:software/app/explore/explore_error_page.dart'; @@ -31,41 +32,27 @@ import 'package:software/app/explore/offline_page.dart'; import 'package:software/app/explore/search_page.dart'; import 'package:software/app/explore/start_page.dart'; import 'package:software/l10n/l10n.dart'; -import 'package:software/services/appstream/appstream_service.dart'; -import 'package:software/services/packagekit/package_service.dart'; -import 'package:software/services/snap_service.dart'; -import 'package:ubuntu_service/ubuntu_service.dart'; -import 'package:yaru_icons/yaru_icons.dart'; import 'package:yaru_widgets/yaru_widgets.dart'; class ExplorePage extends StatefulWidget { - const ExplorePage({super.key}); + const ExplorePage({super.key, required this.section}); - static Widget create( - BuildContext context, [ - String? errorMessage, - ]) { - return ChangeNotifierProvider( - create: (_) => ExploreModel( - getService(), - getService(), - getService(), - errorMessage, - )..init(), - child: const ExplorePage(), - ); - } + final SnapSection section; - static Widget createTitle(BuildContext context) => - Text(context.l10n.explorePageTitle); + static Widget createTitle(BuildContext context, SnapSection snapSection) => + Text( + snapSection == SnapSection.all + ? context.l10n.explorePageTitle + : snapSection.localize(context.l10n), + ); - static Widget createIcon( - BuildContext context, - bool selected, - ) => - selected - ? const Icon(YaruIcons.compass_filled) - : const Icon(YaruIcons.compass); + static Widget createIcon({ + required BuildContext context, + required bool selected, + required SnapSection snapSection, + }) { + return Icon(snapSection.getIcon(selected)); + } @override State createState() => _ExplorePageState(); @@ -77,12 +64,18 @@ class _ExplorePageState extends State { void initState() { super.initState(); final model = context.read(); - _sidebarEventListener = context - .read() - .sidebarEvents - .listen((_) => model.setSearchQuery('')); + _sidebarEventListener = context.read().sidebarEvents.listen((_) { + model.setSearchQuery(''); + model.setSelectedSection(widget.section); + }); final connectivity = context.read(); connectivity.init(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + model.setSelectedSection(widget.section); + model.setSearchQuery(''); + }); } @override @@ -94,13 +87,11 @@ class _ExplorePageState extends State { @override Widget build(BuildContext context) { final connectivity = context.watch(); - final showErrorPage = context.select((ExploreModel m) => m.showErrorPage); final showSearchPage = context.select((ExploreModel m) => m.showSearchPage); final searchQuery = context.select((ExploreModel m) => m.searchQuery); final setSearchQuery = context.read().setSearchQuery; - final sectionSnapsAll = context.select((ExploreModel m) { - return m.sectionNameToSnapsMap[SnapSection.all]; - }); + final startPageApps = context.read().startPageApps; + context.select((ExploreModel m) => m.startPageAppsChanged); final selectedAppFormats = context.select((ExploreModel m) => m.selectedAppFormats); final enabledAppFormats = @@ -112,37 +103,45 @@ class _ExplorePageState extends State { final handleAppFormat = context.select((ExploreModel m) => m.handleAppFormat); - final showSnap = context.select( - (ExploreModel m) => m.selectedAppFormats.contains(AppFormat.snap), - ); - final showPackageKit = context.select( - (ExploreModel m) => m.selectedAppFormats.contains(AppFormat.packageKit), - ); - - final searchResult = context.select((ExploreModel m) => m.searchResult); + final filteredSearchResult = + context.select((ExploreModel m) => m.filteredSearchResult); final search = context.select((ExploreModel m) => m.search); + final errorMessage = context.select((AppModel m) => m.errorMessage); + + Widget page = switch (widget.section) { + SnapSection.games => const GamesStartPage(), + SnapSection.all => const ExploreAllPage(), + _ => GenericStartPage( + snapSection: widget.section, + apps: startPageApps[widget.section], + ) + }; return Scaffold( appBar: YaruWindowTitleBar( + leading: const SizedBox(width: kLeadingGap), title: SearchField( - key: ValueKey(showSearchPage), + key: ValueKey( + '$showSearchPage${ModalRoute.of(context)?.isCurrent ?? searchQuery}', + ), searchQuery: searchQuery, onChanged: (value) { setSearchQuery(value); search(); }, - hintText: context.l10n.searchHintAppStore, + hintText: widget.section == SnapSection.all + ? context.l10n.searchHintAppStore + : '${context.l10n.searchHint}: ${widget.section.localize(context.l10n)}', ), ), body: !connectivity.isOnline ? const OfflinePage() - : showErrorPage + : errorMessage != null && errorMessage.isNotEmpty ? const ExploreErrorPage() : (showSearchPage ? SearchPage( - searchResult: searchResult, - showPackageKit: showPackageKit, - showSnap: showSnap, + searchResult: filteredSearchResult, + preferSnap: selectedAppFormats.contains(AppFormat.snap), header: ExploreHeader( selectedSection: selectedSection, enabledAppFormats: enabledAppFormats, @@ -157,10 +156,7 @@ class _ExplorePageState extends State { }, ), ) - : StartPage( - snaps: sectionSnapsAll, - snapSection: SnapSection.all, - )), + : page), ); } } diff --git a/lib/app/explore/offline_page.dart b/lib/app/explore/offline_page.dart index b83509bff..b220e87a3 100644 --- a/lib/app/explore/offline_page.dart +++ b/lib/app/explore/offline_page.dart @@ -34,7 +34,9 @@ class OfflinePage extends StatelessWidget { ), Text( context.l10n.offline, - style: Theme.of(context).textTheme.displaySmall, + style: Theme.of(context).textTheme.displaySmall?.copyWith( + color: Theme.of(context).disabledColor, + ), ) ], ), diff --git a/lib/app/explore/search_page.dart b/lib/app/explore/search_page.dart index 078ccf6a9..f2d1e2831 100644 --- a/lib/app/explore/search_page.dart +++ b/lib/app/explore/search_page.dart @@ -22,26 +22,53 @@ import 'package:software/app/common/constants.dart'; import 'package:software/app/common/loading_banner_grid.dart'; import 'package:software/l10n/l10n.dart'; -class SearchPage extends StatelessWidget { +class SearchPage extends StatefulWidget { const SearchPage({ super.key, required this.header, this.searchResult, - required this.showSnap, - required this.showPackageKit, + this.preferSnap = true, }); final Widget header; final Map? searchResult; - final bool showSnap; - final bool showPackageKit; + final bool preferSnap; + + @override + State createState() => _SearchPageState(); +} + +class _SearchPageState extends State { + late ScrollController _controller; + late int _searchResultAmount; + + @override + void initState() { + super.initState(); + _searchResultAmount = 30; + + _controller = ScrollController(); + _controller.addListener(() { + if (_controller.position.maxScrollExtent == _controller.offset) { + setState(() { + _searchResultAmount = _searchResultAmount + 5; + }); + } + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { - if (searchResult == null) { - return Column( + if (widget.searchResult == null) { + return const Column( crossAxisAlignment: CrossAxisAlignment.start, - children: const [ + children: [ LoadingExploreHeader(), Expanded(child: LoadingBannerGrid()), ], @@ -50,10 +77,11 @@ class SearchPage extends StatelessWidget { return Column( children: [ - header, - if (searchResult!.isNotEmpty) + widget.header, + if (widget.searchResult!.isNotEmpty) Expanded( child: GridView.builder( + controller: _controller, padding: const EdgeInsets.only( bottom: 15, right: 15, @@ -61,13 +89,16 @@ class SearchPage extends StatelessWidget { ), gridDelegate: kGridDelegate, shrinkWrap: true, - itemCount: searchResult!.length, + itemCount: + widget.searchResult!.entries.take(_searchResultAmount).length, itemBuilder: (context, index) { - final appFinding = searchResult!.entries.elementAt(index); + final appFinding = + widget.searchResult!.entries.elementAt(index); return AppBanner( appFinding: appFinding, - showSnap: showSnap, - showPackageKit: showPackageKit, + showPackageKit: true, + showSnap: true, + preferSnap: widget.preferSnap, ); }, ), diff --git a/lib/app/explore/section_banner.dart b/lib/app/explore/section_banner.dart index bdb931e8f..32257ab90 100644 --- a/lib/app/explore/section_banner.dart +++ b/lib/app/explore/section_banner.dart @@ -1,11 +1,13 @@ import 'package:flutter/material.dart'; -import 'package:snapd/snapd.dart'; -import 'package:software/app/common/base_plate.dart'; -import 'package:software/l10n/l10n.dart'; -import 'package:software/snapx.dart'; +import 'package:shimmer/shimmer.dart'; +import 'package:software/app/common/app_finding.dart'; import 'package:software/app/common/app_icon.dart'; +import 'package:software/app/common/base_plate.dart'; import 'package:software/app/common/snap/snap_page.dart'; import 'package:software/app/common/snap/snap_section.dart'; +import 'package:software/l10n/l10n.dart'; +import 'package:software/snapx.dart'; +import 'package:yaru_colors/yaru_colors.dart'; import 'package:yaru_widgets/yaru_widgets.dart'; import '../common/constants.dart'; @@ -13,76 +15,121 @@ import '../common/constants.dart'; class SectionBanner extends StatelessWidget { const SectionBanner({ super.key, - required this.snaps, + required this.apps, required this.section, required this.gradientColors, }); - final List snaps; + final List? apps; final SnapSection section; final List gradientColors; @override Widget build(BuildContext context) { - return ConstrainedBox( - constraints: const BoxConstraints(minHeight: 230), - child: Padding( - padding: const EdgeInsets.only( - top: 5, - left: kPagePadding, - right: kPagePadding, - bottom: kPagePadding - 5, - ), - child: Container( - padding: const EdgeInsets.all(kYaruPagePadding), - width: 20000, - alignment: Alignment.center, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - gradient: LinearGradient( - colors: gradientColors, - ), + if (apps == null || apps!.isEmpty || apps!.any((app) => app == null)) { + return const LoadingSectionBanner(); + } + + final firstGradientColorIsBright = ThemeData.estimateBrightnessForColor( + gradientColors.first, + ) == + Brightness.light; + + final title = Text( + section.localize(context.l10n), + style: Theme.of(context).textTheme.headlineSmall!.copyWith( + color: firstGradientColorIsBright ? YaruColors.inkstone : Colors.white, + fontWeight: FontWeight.w500, + shadows: [ + if (!firstGradientColorIsBright) + Shadow( + offset: const Offset(0, 1), + blurRadius: 1.0, + color: Colors.black.withOpacity( + 0.4, + ), //color of shadow with opacity + ) + else + Shadow( + offset: const Offset(0, 1), + blurRadius: 1.0, + color: Colors.white.withOpacity( + 0.9, + ), + ) + ], + ), + ); + + final subSlogan = Text( + section.slogan(context.l10n), + style: Theme.of(context).textTheme.headlineSmall!.copyWith( + color: firstGradientColorIsBright ? YaruColors.inkstone : Colors.white, + fontWeight: FontWeight.w100, + shadows: [ + if (!firstGradientColorIsBright) + Shadow( + offset: const Offset(0, 1), + blurRadius: 1.0, + color: Colors.black.withOpacity( + 0.4, + ), //color of shadow with opacity + ) + else + Shadow( + offset: const Offset(0, 1), + blurRadius: 1.0, + color: Colors.white.withOpacity( + 0.9, + ), + ) + ], + ), + ); + + return Padding( + padding: const EdgeInsets.only( + top: 5, + left: kPagePadding, + right: kPagePadding, + bottom: kPagePadding - 5, + ), + child: Container( + padding: const EdgeInsets.all(30), + height: 220, + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + gradient: LinearGradient( + colors: gradientColors, ), + ), + child: SizedBox( + width: 800, child: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - alignment: WrapAlignment.spaceBetween, + runSpacing: kYaruPagePadding, runAlignment: WrapAlignment.start, - runSpacing: 20, + crossAxisAlignment: WrapCrossAlignment.start, + alignment: WrapAlignment.spaceBetween, children: [ - ConstrainedBox( - constraints: BoxConstraints.loose(const Size(250, 1000)), - child: Text( - section.slogan(context.l10n), - style: Theme.of(context).textTheme.headlineSmall!.copyWith( - color: Colors.white, - shadows: [ - Shadow( - offset: const Offset(0, 1), //position of shadow - blurRadius: 1.0, //blur intensity of shadow - color: Colors.black - .withOpacity(0.4), //color of shadow with opacity - ), - ], - ), - ), - ), - const SizedBox( - width: 80, + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + title, + subSlogan, + ], ), Wrap( - alignment: WrapAlignment.spaceBetween, - spacing: kYaruPagePadding, - children: snaps + spacing: 10, + children: apps! .map( (e) => _PlatedIcon( - snap: e, + app: e!, ), ) .toList(), ), - const SizedBox( - height: kYaruPagePadding, - ), ], ), ), @@ -95,10 +142,10 @@ class _PlatedIcon extends StatefulWidget { const _PlatedIcon({ // ignore: unused_element super.key, - required this.snap, + required this.app, }); - final Snap snap; + final AppFinding app; @override State<_PlatedIcon> createState() => _PlatedIconState(); @@ -111,22 +158,26 @@ class _PlatedIconState extends State<_PlatedIcon> { Widget build(BuildContext context) { final dark = Theme.of(context).brightness == Brightness.dark; return Tooltip( - message: widget.snap.name, + message: widget.app.snap!.name, verticalOffset: 45.0, child: Material( color: Colors.transparent, child: InkWell( - onTap: () => SnapPage.push(context: context, snap: widget.snap), + onTap: () => SnapPage.push( + context: context, + snap: widget.app.snap!, + appstream: widget.app.appstream, + ), onHover: (value) => setState(() => hovered = value), child: BasePlate( hovered: hovered, child: AppIcon( - iconUrl: widget.snap.iconUrl, + iconUrl: widget.app.snap!.iconUrl, loadingBaseColor: dark ? const Color.fromARGB(255, 236, 236, 236) : null, loadingHighlight: dark ? const Color.fromARGB(255, 211, 211, 211) : null, - size: 65, + size: 50, ), ), ), @@ -134,3 +185,36 @@ class _PlatedIconState extends State<_PlatedIcon> { ); } } + +class LoadingSectionBanner extends StatelessWidget { + const LoadingSectionBanner({super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + var light = theme.brightness == Brightness.light; + final shimmerBase = + light ? const Color.fromARGB(120, 228, 228, 228) : YaruColors.jet; + final shimmerHighLight = + light ? const Color.fromARGB(200, 247, 247, 247) : YaruColors.coolGrey; + return Shimmer.fromColors( + baseColor: shimmerBase, + highlightColor: shimmerHighLight, + child: Container( + margin: const EdgeInsets.only( + top: 5, + left: kPagePadding, + right: kPagePadding, + bottom: kPagePadding - 5, + ), + + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(kYaruContainerRadius), + color: Theme.of(context).colorScheme.surface, + ), + height: 220, + // width: 800, + ), + ); + } +} diff --git a/lib/app/explore/section_grid.dart b/lib/app/explore/section_grid.dart index f7b3639e8..032595b17 100644 --- a/lib/app/explore/section_grid.dart +++ b/lib/app/explore/section_grid.dart @@ -16,15 +16,15 @@ */ import 'package:flutter/material.dart'; -import 'package:snapd/snapd.dart'; import 'package:software/app/common/app_banner.dart'; import 'package:software/app/common/app_finding.dart'; import 'package:software/app/common/constants.dart'; +import 'package:software/app/common/loading_banner_grid.dart'; class SectionGrid extends StatelessWidget { const SectionGrid({ super.key, - required this.snaps, + required this.apps, this.animateBanners = false, this.padding, this.initSection = true, @@ -33,7 +33,7 @@ class SectionGrid extends StatelessWidget { this.skip = 0, }); - final List snaps; + final List? apps; final int take; final int skip; final bool animateBanners; @@ -43,9 +43,12 @@ class SectionGrid extends StatelessWidget { @override Widget build(BuildContext context) { - if (snaps.isEmpty) return const SizedBox(); + if (apps == null || + apps!.isEmpty || + apps!.any((app) => app == null) || + apps!.any((app) => app!.snap == null)) return const LoadingBannerGrid(); - final snapsMod = snaps.take(take).toList().skip(skip); + final appsMod = apps!.take(take).toList().skip(skip); return GridView.builder( physics: ignoreScrolling ? const NeverScrollableScrollPhysics() : null, @@ -57,17 +60,17 @@ class SectionGrid extends StatelessWidget { ), shrinkWrap: true, gridDelegate: kGridDelegate, - itemCount: snapsMod.length, + itemCount: appsMod.length, itemBuilder: (context, index) { - final snap = snapsMod.elementAt(index); + final app = appsMod.elementAt(index); return AppBanner( appFinding: MapEntry( - snap.name, - AppFinding(snap: snap), + app!.snap!.title ?? '', + app, ), showSnap: true, - showPackageKit: false, + showPackageKit: true, ); }, ); diff --git a/lib/app/explore/start_page.dart b/lib/app/explore/start_page.dart index 493089f35..a381c8ad4 100644 --- a/lib/app/explore/start_page.dart +++ b/lib/app/explore/start_page.dart @@ -16,30 +16,31 @@ */ import 'package:flutter/material.dart'; -import 'package:shimmer/shimmer.dart'; -import 'package:snapd/snapd.dart'; -import 'package:software/app/common/loading_banner_grid.dart'; +import 'package:provider/provider.dart'; +import 'package:software/app/common/app_banner.dart'; +import 'package:software/app/common/app_finding.dart'; +import 'package:software/app/common/constants.dart'; import 'package:software/app/common/snap/snap_section.dart'; +import 'package:software/app/explore/explore_model.dart'; import 'package:software/app/explore/section_banner.dart'; import 'package:software/app/explore/section_grid.dart'; import 'package:software/snapx.dart'; -import 'package:yaru_colors/yaru_colors.dart'; -class StartPage extends StatefulWidget { - const StartPage({ +class GenericStartPage extends StatefulWidget { + const GenericStartPage({ super.key, - this.snaps, required this.snapSection, + this.apps, }); - final List? snaps; final SnapSection snapSection; + final List? apps; @override - State createState() => _StartPageState(); + State createState() => _GenericStartPageState(); } -class _StartPageState extends State { +class _GenericStartPageState extends State { late ScrollController _controller; late int _amount; @@ -61,14 +62,31 @@ class _StartPageState extends State { @override Widget build(BuildContext context) { + final appsWithIcons = + widget.apps?.where((app) => app.snap?.iconUrl != null).toList(); + AppFinding? bannerApp; + AppFinding? bannerApp2; + AppFinding? bannerApp3; + + bannerApp = appsWithIcons?.elementAt(0); + bannerApp2 = appsWithIcons?.elementAt(1); + bannerApp3 = appsWithIcons?.elementAt(2); + return SingleChildScrollView( padding: const EdgeInsets.only(top: 15), controller: _controller, child: Column( children: [ - _TeaserPage( - snapSection: widget.snapSection, - snaps: widget.snaps, + SectionBanner( + gradientColors: + widget.snapSection.colors.map((e) => Color(e)).toList(), + apps: [bannerApp, bannerApp2, bannerApp3], + section: widget.snapSection, + ), + SectionGrid( + apps: widget.apps, + take: 20, + skip: 3, ), ], ), @@ -76,73 +94,137 @@ class _StartPageState extends State { } } -class _TeaserPage extends StatelessWidget { - const _TeaserPage({ - required this.snapSection, - this.snaps, - }); +class ExploreAllPage extends StatefulWidget { + const ExploreAllPage({super.key}); - final SnapSection snapSection; - final List? snaps; + @override + State createState() => _ExploreAllPageState(); +} + +class _ExploreAllPageState extends State { + late ScrollController _controller; + late int _amount; + + @override + void initState() { + super.initState(); + + _amount = 60; + _controller = ScrollController(); + + _controller.addListener(() { + if (_controller.position.maxScrollExtent == _controller.offset) { + setState(() { + _amount = _amount + 5; + }); + } + }); + } @override Widget build(BuildContext context) { - final snapsWithIcons = - snaps?.where((snap) => snap.iconUrl != null).toList(); - Snap? bannerSnap; - Snap? bannerSnap2; - Snap? bannerSnap3; - - if (snapsWithIcons != null && snapsWithIcons.isNotEmpty) { - bannerSnap = snapsWithIcons.elementAt(0); - bannerSnap2 = snapsWithIcons.elementAt(1); - bannerSnap3 = snapsWithIcons.elementAt(2); - } - if (bannerSnap == null || bannerSnap2 == null || bannerSnap3 == null) { - return Column( - children: const [ - _LoadingSectionBanner(), - LoadingBannerGrid(), + final apps = context.read().startPageApps[SnapSection.all]; + context.select((ExploreModel m) => m.startPageAppsChanged); + + final appsWithIcons = + apps?.where((app) => app.snap?.iconUrl != null).toList(); + AppFinding? bannerApp; + AppFinding? bannerApp2; + AppFinding? bannerApp3; + + bannerApp = appsWithIcons?.elementAt(0); + bannerApp2 = appsWithIcons?.elementAt(1); + bannerApp3 = appsWithIcons?.elementAt(2); + + return SingleChildScrollView( + padding: const EdgeInsets.only(top: 15), + controller: _controller, + child: Column( + children: [ + SectionBanner( + gradientColors: + SnapSection.all.colors.map((e) => Color(e)).toList(), + apps: [bannerApp, bannerApp2, bannerApp3], + section: SnapSection.all, + ), + SectionGrid( + apps: apps, + take: 20, + skip: 3, + ), ], - ); - } - - return Column( - children: [ - SectionBanner( - gradientColors: snapSection.colors.map((e) => Color(e)).toList(), - snaps: [bannerSnap, bannerSnap2, bannerSnap3], - section: snapSection, - ), - SectionGrid( - snaps: snaps ?? [], - take: 20, - skip: 3, - ), - ], + ), ); } } -class _LoadingSectionBanner extends StatelessWidget { - // ignore: unused_element - const _LoadingSectionBanner({super.key}); +class GamesStartPage extends StatefulWidget { + const GamesStartPage({super.key}); + + @override + State createState() => _GamesStartPageState(); +} + +class _GamesStartPageState extends State { + late ScrollController _controller; + late int _amount; + + @override + void initState() { + super.initState(); + + _amount = 30; + _controller = ScrollController(); + + _controller.addListener(() { + if (_controller.position.maxScrollExtent == _controller.offset) { + setState(() { + _amount = _amount + 5; + }); + } + }); + } @override Widget build(BuildContext context) { - final theme = Theme.of(context); - var light = theme.brightness == Brightness.light; - final shimmerBase = - light ? const Color.fromARGB(120, 228, 228, 228) : YaruColors.jet; - final shimmerHighLight = - light ? const Color.fromARGB(200, 247, 247, 247) : YaruColors.coolGrey; - return Shimmer.fromColors( - baseColor: shimmerBase, - highlightColor: shimmerHighLight, - child: SectionBanner( - snaps: const [], - section: SnapSection.all, - gradientColors: SnapSection.all.colors.map((e) => Color(e)).toList(), + final apps = context.read().startPageApps[SnapSection.games]; + + final appsWithIcons = + apps?.where((app) => app.snap?.iconUrl != null).toList(); + AppFinding? bannerApp; + AppFinding? bannerApp2; + AppFinding? bannerApp3; + + bannerApp = appsWithIcons?.elementAt(0); + bannerApp2 = appsWithIcons?.elementAt(1); + bannerApp3 = appsWithIcons?.elementAt(2); + + return SingleChildScrollView( + controller: _controller, + padding: const EdgeInsets.only(top: 15), + child: Column( + children: [ + SectionBanner( + gradientColors: + SnapSection.games.colors.map((e) => Color(e)).toList(), + apps: [bannerApp, bannerApp2, bannerApp3], + section: SnapSection.games, + ), + GridView( + shrinkWrap: true, + padding: kGridPadding, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: kImageGridDelegate, + children: [ + for (final app in apps + ?.where((a) => a.snap!.bannerUrl != null) + .toList() + .skip(3) ?? + []) + AppImageBanner(snap: app.snap!), + ], + ), + ], ), ); } diff --git a/lib/app/installed/installed_header.dart b/lib/app/installed/installed_header.dart deleted file mode 100644 index e196daf3d..000000000 --- a/lib/app/installed/installed_header.dart +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (C) 2022 Canonical Ltd - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 as - * published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -import 'package:flutter/material.dart'; -import 'package:packagekit/packagekit.dart'; -import 'package:software/app/common/app_format.dart'; -import 'package:software/app/common/app_format_popup.dart'; -import 'package:software/app/common/constants.dart'; -import 'package:software/app/common/packagekit/packagekit_filter_button.dart'; -import 'package:software/app/common/snap/snap_sort.dart'; -import 'package:software/app/common/snap/snap_sort_popup.dart'; -import 'package:software/l10n/l10n.dart'; -import 'package:yaru_widgets/yaru_widgets.dart'; - -class InstalledHeader extends StatelessWidget { - const InstalledHeader({ - super.key, - required this.appFormat, - required this.enabledAppFormats, - required this.setAppFormat, - required this.handleFilter, - required this.packageKitFilters, - required this.snapSort, - required this.setSnapSort, - required this.setLoadSnapsWithUpdates, - required this.loadSnapsWithUpdates, - }); - - final AppFormat appFormat; - final Set enabledAppFormats; - final void Function(AppFormat) setAppFormat; - final void Function(bool, PackageKitFilter) handleFilter; - final Set packageKitFilters; - final SnapSort snapSort; - final void Function(SnapSort) setSnapSort; - final void Function(bool) setLoadSnapsWithUpdates; - final bool loadSnapsWithUpdates; - - @override - Widget build(BuildContext context) { - return Padding( - padding: kHeaderPadding, - child: Align( - alignment: Alignment.centerLeft, - child: Wrap( - alignment: WrapAlignment.start, - crossAxisAlignment: WrapCrossAlignment.start, - runAlignment: WrapAlignment.start, - spacing: 10, - children: [ - AppFormatPopup( - appFormat: appFormat, - enabledAppFormats: enabledAppFormats, - onSelected: setAppFormat, - ), - if (appFormat == AppFormat.packageKit) - PackageKitFilterButton( - onTap: (value, filter) => handleFilter(value, filter), - filters: packageKitFilters, - lockInstalled: true, - ), - if (appFormat == AppFormat.snap) - SnapSortPopup( - value: snapSort, - onSelected: (value) => setSnapSort(value), - ), - if (appFormat == AppFormat.snap) - YaruIconButton( - onPressed: () => setLoadSnapsWithUpdates(!loadSnapsWithUpdates), - isSelected: loadSnapsWithUpdates, - icon: const Icon(Icons.upgrade_rounded), - tooltip: context.l10n.updateAvailable, - ), - ], - ), - ), - ); - } -} diff --git a/lib/app/installed/installed_model.dart b/lib/app/installed/installed_model.dart deleted file mode 100644 index 2219d4230..000000000 --- a/lib/app/installed/installed_model.dart +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright (C) 2022 Canonical Ltd - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 as - * published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -import 'dart:async'; - -import 'package:packagekit/packagekit.dart'; -import 'package:safe_change_notifier/safe_change_notifier.dart'; -import 'package:snapd/snapd.dart'; -import 'package:software/services/packagekit/package_service.dart'; -import 'package:software/services/snap_service.dart'; -import 'package:software/app/common/app_format.dart'; -import 'package:software/app/common/snap/snap_sort.dart'; - -class InstalledModel extends SafeChangeNotifier { - final PackageService _packageService; - InstalledModel( - this._packageService, - this._snapService, - ); - - StreamSubscription? _installedSub; - - List get installedPackages => - _packageService.isAvailable ? _packageService.installedPackages : []; - - final SnapService _snapService; - StreamSubscription? _snapChangesSub; - - // Local snaps - bool _isLoadingSnapsCompleted = false; - bool get isLoadingSnapsCompleted => _isLoadingSnapsCompleted; - List get localSnaps => _snapService.localSnaps; - Future loadLocalSnaps() async { - _snapService.loadLocalSnaps().whenComplete(() { - _isLoadingSnapsCompleted = true; - notifyListeners(); - }); - } - - // Local snaps with update - Future> get localSnapsWithUpdate async => - await _snapService.loadSnapsWithUpdate(); - - Future init() async { - _snapChangesSub = _snapService.snapChangesInserted.listen((_) { - if (_snapService.snapChanges.isEmpty) { - loadLocalSnaps().then((value) => notifyListeners()); - } - }); - _enabledAppFormats.add(AppFormat.snap); - if (_packageService.isAvailable) { - _enabledAppFormats.add(AppFormat.packageKit); - _installedSub = _packageService.installedPackagesChanged.listen((event) { - notifyListeners(); - }); - await _packageService.getInstalledPackages(filters: packageKitFilters); - } - - await loadLocalSnaps(); - notifyListeners(); - } - - @override - Future dispose() async { - await _snapChangesSub?.cancel(); - _installedSub?.cancel(); - - super.dispose(); - } - - String? _searchQuery; - String? get searchQuery => _searchQuery; - void setSearchQuery(String? value) { - if (value == _searchQuery) return; - _searchQuery = value; - notifyListeners(); - } - - final Set _enabledAppFormats = {}; - Set get enabledAppFormats => _enabledAppFormats; - AppFormat _appFormat = AppFormat.snap; - AppFormat get appFormat => _appFormat; - void setAppFormat(AppFormat value) { - if (value == _appFormat) return; - _appFormat = value; - _loadSnapsWithUpdates = false; - if (_appFormat == AppFormat.packageKit && _packageService.isAvailable) { - _packageService.getInstalledPackages().then((_) => notifyListeners()); - } else { - notifyListeners(); - } - } - - final Set _packageKitFilters = { - PackageKitFilter.installed, - PackageKitFilter.gui, - PackageKitFilter.newest, - PackageKitFilter.application, - PackageKitFilter.notSource, - }; - Set get packageKitFilters => _packageKitFilters; - Future handleFilter(bool value, PackageKitFilter filter) async { - if (!_packageService.isAvailable) return; - if (value) { - _packageKitFilters.add(filter); - } else { - _packageKitFilters.remove(filter); - } - await _packageService.getInstalledPackages(filters: packageKitFilters); - notifyListeners(); - } - - bool _busy = false; - bool get busy => _busy; - set busy(bool value) { - if (value == _busy) return; - _busy = value; - notifyListeners(); - } - - SnapSort _snapSort = SnapSort.name; - SnapSort get snapSort => _snapSort; - void setSnapSort(SnapSort value) { - if (value == _snapSort) return; - _snapSort = value; - notifyListeners(); - } - - bool _loadSnapsWithUpdates = false; - bool get loadSnapsWithUpdates => _loadSnapsWithUpdates; - void setLoadSnapsWithUpdates(bool value) { - if (value == _loadSnapsWithUpdates) return; - _loadSnapsWithUpdates = value; - notifyListeners(); - } -} diff --git a/lib/app/installed/installed_packages_page.dart b/lib/app/installed/installed_packages_page.dart deleted file mode 100644 index c97b90e49..000000000 --- a/lib/app/installed/installed_packages_page.dart +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (C) 2022 Canonical Ltd - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 as - * published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:software/app/common/animated_scroll_view_item.dart'; -import 'package:software/app/common/app_icon.dart'; -import 'package:software/app/common/constants.dart'; -import 'package:software/app/common/packagekit/package_page.dart'; -import 'package:software/app/installed/installed_model.dart'; -import 'package:yaru_widgets/yaru_widgets.dart'; - -class InstalledPackagesPage extends StatefulWidget { - const InstalledPackagesPage({super.key}); - - @override - State createState() => _InstalledPackagesPageState(); -} - -class _InstalledPackagesPageState extends State { - late ScrollController _controller; - - @override - void initState() { - super.initState(); - _controller = ScrollController(); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final model = context.watch(); - final installedApps = model.searchQuery == null - ? model.installedPackages - : model.installedPackages - .where((element) => element.name.startsWith(model.searchQuery!)) - .toList(); - - return model.installedPackages.isNotEmpty - ? GridView.builder( - controller: _controller, - padding: kGridPadding, - gridDelegate: kGridDelegate, - shrinkWrap: true, - itemCount: installedApps.length, - itemBuilder: (context, index) { - final package = installedApps[index]; - return AnimatedScrollViewItem( - child: YaruBanner.tile( - title: Text(package.name), - subtitle: Text(package.version), - onTap: () => PackagePage.push(context, id: package), - icon: const Padding( - padding: EdgeInsets.only(left: 10, right: 5), - child: AppIcon( - iconUrl: null, - ), - ), - ), - ); - }, - ) - : const SizedBox(); - } -} diff --git a/lib/app/installed/installed_page.dart b/lib/app/installed/installed_page.dart deleted file mode 100644 index 8882ad078..000000000 --- a/lib/app/installed/installed_page.dart +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright (C) 2022 Canonical Ltd - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 as - * published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -import 'package:badges/badges.dart' as badges; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:software/app/common/app_format.dart'; -import 'package:software/app/common/constants.dart'; -import 'package:software/app/common/indeterminate_circular_progress_icon.dart'; -import 'package:software/app/common/search_field.dart'; -import 'package:software/app/installed/installed_header.dart'; -import 'package:software/app/installed/installed_model.dart'; -import 'package:software/app/installed/installed_packages_page.dart'; -import 'package:software/app/installed/installed_snaps_page.dart'; -import 'package:software/l10n/l10n.dart'; -import 'package:software/services/packagekit/package_service.dart'; -import 'package:software/services/snap_service.dart'; -import 'package:ubuntu_service/ubuntu_service.dart'; -import 'package:yaru_icons/yaru_icons.dart'; -import 'package:yaru_widgets/yaru_widgets.dart'; - -class InstalledPage extends StatelessWidget { - const InstalledPage({super.key}); - - static Widget create( - BuildContext context, - ) { - return ChangeNotifierProvider( - create: (context) => InstalledModel( - getService(), - getService(), - )..init(), - child: const InstalledPage(), - ); - } - - static Widget createTitle(BuildContext context) => - Text(context.l10n.installed); - - static Widget createIcon({ - required BuildContext context, - required bool selected, - int? badgeCount, - bool? processing, - }) { - if (badgeCount != null && badgeCount > 0) { - return _InstalledPageIcon(count: badgeCount); - } - return selected - ? const Icon(YaruIcons.ok_filled) - : const Icon(YaruIcons.ok); - } - - @override - Widget build(BuildContext context) { - final searchQuery = context.select((InstalledModel m) => m.searchQuery); - final appFormat = context.select((InstalledModel m) => m.appFormat); - final setAppFormat = context.select((InstalledModel m) => m.setAppFormat); - final setSearchQuery = - context.select((InstalledModel m) => m.setSearchQuery); - final enabledAppFormats = - context.select((InstalledModel m) => m.enabledAppFormats); - final loadSnapsWithUpdates = - context.select((InstalledModel m) => m.loadSnapsWithUpdates); - final setLoadSnapsWithUpdates = - context.select((InstalledModel m) => m.setLoadSnapsWithUpdates); - final handleFilter = context.select((InstalledModel m) => m.handleFilter); - final packageKitFilters = - context.select((InstalledModel m) => m.packageKitFilters); - final snapSort = context.select((InstalledModel m) => m.snapSort); - final setSnapSort = context.select((InstalledModel m) => m.setSnapSort); - - final page = Column( - children: [ - InstalledHeader( - appFormat: appFormat, - enabledAppFormats: enabledAppFormats, - handleFilter: handleFilter, - loadSnapsWithUpdates: loadSnapsWithUpdates, - packageKitFilters: packageKitFilters, - setAppFormat: setAppFormat, - setLoadSnapsWithUpdates: setLoadSnapsWithUpdates, - setSnapSort: setSnapSort, - snapSort: snapSort, - ), - if (appFormat == AppFormat.snap) - const Expanded(child: InstalledSnapsPage()) - else if (appFormat == AppFormat.packageKit) - const Expanded(child: InstalledPackagesPage()), - ], - ); - - return Scaffold( - appBar: YaruWindowTitleBar( - title: SearchField( - searchQuery: searchQuery ?? '', - onChanged: setSearchQuery, - hintText: context.l10n.searchHintInstalled, - ), - ), - body: page, - ); - } -} - -class _InstalledPageIcon extends StatelessWidget { - // ignore: unused_element - const _InstalledPageIcon({super.key, required this.count}); - - final int count; - - @override - Widget build(BuildContext context) { - return badges.Badge( - badgeColor: Theme.of(context).primaryColor, - badgeContent: Text( - count.toString(), - style: badgeTextStyle, - ), - child: const IndeterminateCircularProgressIcon(), - ); - } -} diff --git a/lib/app/installed/installed_snaps_page.dart b/lib/app/installed/installed_snaps_page.dart deleted file mode 100644 index f66d6ef8c..000000000 --- a/lib/app/installed/installed_snaps_page.dart +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright (C) 2022 Canonical Ltd - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 as - * published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:snapd/snapd.dart'; -import 'package:software/app/common/loading_banner_grid.dart'; -import 'package:software/app/common/snap/snap_grid.dart'; -import 'package:software/app/common/snap/snap_utils.dart'; -import 'package:software/app/common/updates_splash_screen.dart'; -import 'package:software/app/common/snap/no_snaps_installed_page.dart'; -import 'package:software/app/installed/installed_model.dart'; -import 'package:software/app/updates/no_updates_page.dart'; -import 'package:software/l10n/l10n.dart'; -import 'package:yaru_icons/yaru_icons.dart'; - -class InstalledSnapsPage extends StatefulWidget { - const InstalledSnapsPage({super.key}); - @override - State createState() => _InstalledSnapsPageState(); -} - -class _InstalledSnapsPageState extends State { - @override - Widget build(BuildContext context) { - final model = context.watch(); - - if (model.loadSnapsWithUpdates) { - return FutureBuilder>( - future: model.localSnapsWithUpdate, - builder: (context, snapshot) { - if (snapshot.connectionState != ConnectionState.done) { - return const Center( - child: SingleChildScrollView( - child: UpdatesSplashScreen( - icon: YaruIcons.snapcraft, - ), - ), - ); - } else { - if ((snapshot.hasData && snapshot.data!.isEmpty) || - !snapshot.hasData) { - return const Center( - child: SingleChildScrollView(child: NoUpdatesPage()), - ); - } else { - return SnapGrid( - snaps: sortSnaps( - snapSort: model.snapSort, - snaps: snapshot.data!, - ), - ); - } - } - }, - ); - } else { - if (model.localSnaps.isEmpty && !model.isLoadingSnapsCompleted) { - return const LoadingBannerGrid(); - } else if (model.localSnaps.isEmpty && model.isLoadingSnapsCompleted) { - return NoSnapsInstalledPage( - message: context.l10n.noSnapsInstalled, - icon: YaruIcons.no_package_snap, - ); - } else { - final snaps = model.searchQuery == null - ? model.localSnaps - : model.localSnaps - .where((snap) => snap.name.startsWith(model.searchQuery!)) - .toList(); - return SnapGrid( - snaps: sortSnaps( - snapSort: model.snapSort, - snaps: snaps, - ), - ); - } - } - } -} diff --git a/lib/app/package_installer/package_installer_page.dart b/lib/app/package_installer/package_installer_page.dart index 1be1604eb..b8981e550 100644 --- a/lib/app/package_installer/package_installer_page.dart +++ b/lib/app/package_installer/package_installer_page.dart @@ -42,6 +42,7 @@ class PackageInstallerPage { path: debPath, appstream: appstream, packageId: packageId, + enableSearch: false, ); } return FutureBuilder( diff --git a/lib/app/settings/repo_dialog.dart b/lib/app/settings/repo_dialog.dart index a89e128c4..052551fcf 100644 --- a/lib/app/settings/repo_dialog.dart +++ b/lib/app/settings/repo_dialog.dart @@ -1,15 +1,28 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:software/app/common/message_bar.dart'; import 'package:software/l10n/l10n.dart'; -import 'package:software/app/updates/package_updates_model.dart'; +import 'package:software/app/collection/package_updates_model.dart'; +import 'package:software/services/packagekit/package_service.dart'; import 'package:software/services/packagekit/updates_state.dart'; +import 'package:ubuntu_service/ubuntu_service.dart'; +import 'package:ubuntu_session/ubuntu_session.dart'; import 'package:yaru_icons/yaru_icons.dart'; import 'package:yaru_widgets/yaru_widgets.dart'; class RepoDialog extends StatefulWidget { - // ignore: unused_element const RepoDialog({super.key}); + static Widget create(BuildContext context) { + return ChangeNotifierProvider( + create: (context) => PackageUpdatesModel( + getService(), + getService(), + ), + child: const RepoDialog(), + ); + } + @override State createState() => _RepoDialogState(); } @@ -17,10 +30,33 @@ class RepoDialog extends StatefulWidget { class _RepoDialogState extends State { late TextEditingController controller; + void showSnackBar() { + if (!mounted) return; + final model = context.read(); + if (model.errorMessage.isNotEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + duration: const Duration(minutes: 1), + padding: EdgeInsets.zero, + content: MessageBar( + message: model.errorMessage, + copyMessage: context.l10n.copyErrorMessage, + ), + ), + ); + } + } + + bool _initialized = false; @override void initState() { super.initState(); controller = TextEditingController(); + + context + .read() + .init(handleError: () => showSnackBar(), loadRepoList: true) + .then((_) => _initialized = true); } @override @@ -33,56 +69,81 @@ class _RepoDialogState extends State { Widget build(BuildContext context) { final model = context.watch(); - return SimpleDialog( - title: YaruDialogTitleBar( - title: model.updatesState != UpdatesState.updating && - model.updatesState != UpdatesState.checkingForUpdates - ? Row( - children: [ - IconButton( - onPressed: - controller.text.isEmpty ? null : () => model.addRepo(), - icon: const Icon(YaruIcons.plus), - ), - const SizedBox( - width: 10, - ), - SizedBox( - width: 300, - child: TextField( - onChanged: (value) => model.manualRepoName = value, - controller: controller, - decoration: InputDecoration( - isDense: false, - hintText: context.l10n.enterRepoName, - border: const UnderlineInputBorder(), - enabledBorder: const UnderlineInputBorder( - borderSide: BorderSide(color: Colors.transparent), - ), + final ready = (model.updatesState != UpdatesState.updating && + model.updatesState != UpdatesState.checkingForUpdates) && + _initialized; + + return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: BorderRadius.circular(10), + ), + child: Column( + children: [ + YaruDialogTitleBar( + leading: YaruBackButton( + style: YaruBackButtonStyle.rounded, + onPressed: () => Navigator.of(context).pop(), + ), + title: ready + ? Row( + children: [ + IconButton( + onPressed: controller.text.isEmpty + ? null + : () => model.addRepo(), + icon: const Icon(YaruIcons.plus), + ), + const SizedBox( + width: 10, ), - ), + SizedBox( + width: 300, + height: 35, + child: TextField( + onChanged: (value) => model.manualRepoName = value, + controller: controller, + decoration: InputDecoration( + isDense: false, + hintText: context.l10n.enterRepoName, + ), + ), + ) + ], ) + : const SizedBox.shrink(), + ), + if (!ready) + const Expanded( + child: Center(child: YaruCircularProgressIndicator()), + ) + else + Expanded( + child: ListView( + padding: const EdgeInsets.only( + top: kYaruPagePadding, + bottom: kYaruPagePadding, + ), + children: [ + for (final e in model.repos) + ListTile( + enabled: model.updatesState != UpdatesState.updating && + model.updatesState != UpdatesState.checkingForUpdates, + trailing: YaruCheckbox( + value: e.enabled, + onChanged: (v) => + model.toggleRepo(id: e.repoId, value: v!), + ), + title: ListTile( + title: Text(e.repoId), + subtitle: Text(e.description), + ), + ) ], - ) - : const SizedBox(), - ), - titlePadding: EdgeInsets.zero, - children: model.repos - .map( - (e) => ListTile( - enabled: model.updatesState != UpdatesState.updating && - model.updatesState != UpdatesState.checkingForUpdates, - trailing: YaruCheckbox( - value: e.enabled, - onChanged: (v) => model.toggleRepo(id: e.repoId, value: v!), - ), - title: ListTile( - title: Text(e.repoId), - subtitle: Text(e.description), ), - ), - ) - .toList(), + ) + ], + ), ); } } diff --git a/lib/app/settings/settings_model.dart b/lib/app/settings/settings_model.dart index 8b1bf39c8..9ba1d10b7 100644 --- a/lib/app/settings/settings_model.dart +++ b/lib/app/settings/settings_model.dart @@ -19,26 +19,48 @@ import 'package:package_info_plus/package_info_plus.dart'; import 'package:safe_change_notifier/safe_change_notifier.dart'; import 'package:software/services/packagekit/package_service.dart'; -const repoUrl = 'https://github.com/ubuntu-flutter-community/software'; +const _repoUrl = 'https://github.com/ubuntu-flutter-community/software'; class SettingsModel extends SafeChangeNotifier { - String appName; + final PackageService _packageService; + SettingsModel(this._packageService); - String packageName; + String? _appName; + String? get appName => _appName; + set appName(String? value) { + if (value == null || value == _appName) return; + _appName = value; + notifyListeners(); + } - String version; + String? _packageName; + String? get packageName => _packageName; + set packageName(String? value) { + if (value == null || value == _packageName) return; + _packageName = value; + notifyListeners(); + } - String buildNumber; + String? _version; + String? get version => _version; + set version(String? value) { + if (value == null || value == _version) return; + _version = value; + notifyListeners(); + } - final PackageService _packageService; - SettingsModel(this._packageService) - : appName = '', - packageName = '', - version = '', - buildNumber = ''; + String? _buildNumber; + String? get buildNumber => _buildNumber; + set buildNumber(String? value) { + if (value == null || value == _buildNumber) return; + _buildNumber = value; + notifyListeners(); + } bool get packageKitAvailable => _packageService.isAvailable; + String get repoUrl => _repoUrl; + Future init() async { final packageInfo = await PackageInfo.fromPlatform(); appName = packageInfo.appName; diff --git a/lib/app/settings/settings_page.dart b/lib/app/settings/settings_page.dart index eb1bcf5f6..cebdbd322 100644 --- a/lib/app/settings/settings_page.dart +++ b/lib/app/settings/settings_page.dart @@ -19,15 +19,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:provider/provider.dart'; import 'package:software/app/app.dart'; -import 'package:software/app/common/message_bar.dart'; +import 'package:software/app/common/link.dart'; import 'package:software/app/settings/repo_dialog.dart'; import 'package:software/app/settings/settings_model.dart'; -import 'package:software/app/updates/package_updates_model.dart'; +import 'package:software/app/settings/theme_tile.dart'; import 'package:software/l10n/l10n.dart'; import 'package:software/services/packagekit/package_service.dart'; -import 'package:software/services/packagekit/updates_state.dart'; +import 'package:software/theme_mode_x.dart'; import 'package:ubuntu_service/ubuntu_service.dart'; -import 'package:ubuntu_session/ubuntu_session.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:yaru_icons/yaru_icons.dart'; import 'package:yaru_widgets/yaru_widgets.dart'; @@ -61,22 +60,76 @@ class _SettingsPageState extends State { @override Widget build(BuildContext context) { - return Scaffold( - appBar: YaruWindowTitleBar( - title: Text(context.l10n.settingsPageTitle), - ), - body: ListView( - children: [ - const ThemeSection(), - YaruSection( - margin: const EdgeInsets.all(kYaruPagePadding), - //width: kMinSectionWidth, - child: Column( - children: [_RepoTile.create(context), const _AboutTile()], - ), - ) - ], - ), + final nav = Navigator( + onPopPage: (route, result) => route.didPop(result), + key: Utils.settingsNav, + initialRoute: '/settings', + onGenerateRoute: (settings) { + Widget page = switch (settings.name) { + '/settings' => const _SettingsPage(), + '/repoDialog' => RepoDialog.create(context), + '/about' => const _AboutDialog(), + '/licenses' => const _LicensePage(), + _ => const _SettingsPage() + }; + + return PageRouteBuilder( + pageBuilder: (_, __, ___) => page, + transitionDuration: const Duration(milliseconds: 500), + ); + }, + ); + + return AlertDialog( + backgroundColor: Theme.of(context).colorScheme.background, + titlePadding: EdgeInsets.zero, + contentPadding: EdgeInsets.zero, + content: SizedBox(height: 800, width: 600, child: nav), + ); + } + + Future loadAsset(BuildContext context) async { + return await DefaultAssetBundle.of(context) + .loadString('assets/contributors.md'); + } +} + +class _SettingsPage extends StatelessWidget { + const _SettingsPage(); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + YaruDialogTitleBar( + onClose: (p0) => Navigator.of(rootNavigator: true, context).pop(), + title: SettingsPage.createTitle(context), + ), + Expanded( + child: ListView( + children: [ + const ThemeSection(), + YaruSection( + headline: Text(context.l10n.sources), + margin: const EdgeInsets.all(kYaruPagePadding), + child: const Column( + children: [ + _RepoTile(), + ], + ), + ), + YaruSection( + headline: Text(context.l10n.about), + margin: + const EdgeInsets.symmetric(horizontal: kYaruPagePadding), + child: const Column( + children: [_AboutTile(), _LicenseTile()], + ), + ), + ], + ), + ) + ], ); } } @@ -89,44 +142,8 @@ class ThemeSection extends StatefulWidget { } class _ThemeSectionState extends State { - int _listTileValue = 0; - - void onChanged(index) { - setState(() { - _listTileValue = index; - switch (index) { - case 0: - { - App.themeNotifier.value = ThemeMode.system; - } - break; - - case 1: - { - App.themeNotifier.value = ThemeMode.light; - } - break; - - case 2: - { - App.themeNotifier.value = ThemeMode.dark; - } - break; - } - }); - } - - @override - void initState() { - super.initState(); - if (App.themeNotifier.value == ThemeMode.system) { - _listTileValue = 0; - } else if (App.themeNotifier.value == ThemeMode.light) { - _listTileValue = 1; - } else if (App.themeNotifier.value == ThemeMode.dark) { - _listTileValue = 2; - } - } + void _onChanged(int index) => + setState(() => App.themeNotifier.value = ThemeMode.values[index]); @override Widget build(BuildContext context) { @@ -138,29 +155,32 @@ class _ThemeSectionState extends State { right: kYaruPagePadding, ), headline: Text(context.l10n.theme), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - for (var i = 0; i < themes.length; ++i) - YaruRadioListTile( - title: Text( - themes[i], - style: const TextStyle(fontSize: 14), - ), - dense: true, - contentPadding: const EdgeInsets.symmetric( - horizontal: 8, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(kYaruContainerRadius), - ), - controlAffinity: ListTileControlAffinity.trailing, - value: i, - groupValue: _listTileValue, - onChanged: onChanged, - toggleable: false, - ), - ], + child: Center( + child: Padding( + padding: const EdgeInsets.only(top: kYaruPagePadding), + child: Wrap( + spacing: kYaruPagePadding, + children: [ + for (var i = 0; i < themes.length; ++i) + Column( + mainAxisSize: MainAxisSize.min, + children: [ + YaruSelectableContainer( + padding: const EdgeInsets.all(1), + borderRadius: BorderRadius.circular(12), + selected: App.themeNotifier.value == ThemeMode.values[i], + onTap: () => _onChanged(i), + child: ThemeTile(ThemeMode.values[i]), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Text(ThemeMode.values[i].localize(context.l10n)), + ) + ], + ), + ], + ), + ), ), ); } @@ -171,70 +191,36 @@ class _RepoTile extends StatefulWidget { @override State<_RepoTile> createState() => _RepoTileState(); - - static Widget create(BuildContext context) { - return ChangeNotifierProvider( - create: (context) => PackageUpdatesModel( - getService(), - getService(), - ), - child: const _RepoTile(), - ); - } } class _RepoTileState extends State<_RepoTile> { - bool _initialized = false; - @override - void initState() { - super.initState(); - context - .read() - .init(handleError: () => showSnackBar(), loadRepoList: true) - .then((_) => _initialized = true); - } - - void showSnackBar() { - if (!mounted) return; - final model = context.read(); - if (model.errorMessage.isNotEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - duration: const Duration(minutes: 1), - padding: EdgeInsets.zero, - content: MessageBar( - message: model.errorMessage, - copyMessage: context.l10n.copyErrorMessage, - ), - ), - ); - } - } - @override Widget build(BuildContext context) { - final model = context.watch(); + // final model = context.watch(); return YaruTile( - title: Text(context.l10n.sources), subtitle: Text(context.l10n.sourcesDescription), trailing: OutlinedButton( - onPressed: model.updatesState == UpdatesState.updating - ? null - : () => showDialog( - context: context, - builder: (context) { - if (!_initialized) { - return const AlertDialog( - content: YaruCircularProgressIndicator(), - ); - } - return ChangeNotifierProvider.value( - value: model, - child: const RepoDialog(), - ); - }, - ), - child: Text(context.l10n.configure), + onPressed: () => + Utils.settingsNav.currentState!.pushNamed('/repoDialog'), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox( + width: 10, + ), + const Icon( + YaruIcons.settings, + size: 18, + ), + const SizedBox( + width: 5, + ), + Text(context.l10n.configure), + const SizedBox( + width: 10, + ), + ], + ), ), ); } @@ -246,76 +232,128 @@ class _AboutTile extends StatelessWidget { @override Widget build(BuildContext context) { final model = context.watch(); + return YaruTile( title: Text( - '${model.appName} ${model.version} ${model.buildNumber}', + '${context.l10n.version}: ${model.version} ${model.buildNumber}', + ), + trailing: OutlinedButton( + onPressed: () => Utils.settingsNav.currentState!.pushNamed('/about'), + child: Text(context.l10n.contributors), + ), + ); + } +} + +class _AboutDialog extends StatelessWidget { + const _AboutDialog(); + + @override + Widget build(BuildContext context) { + final model = context.watch(); + + return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: BorderRadius.circular(10), ), - trailing: TextButton( - onPressed: () { - showAboutDialog( - applicationVersion: model.version, - applicationIcon: Image.asset( - 'assets/software.png', - width: 60, - filterQuality: FilterQuality.medium, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + YaruDialogTitleBar( + leading: YaruBackButton( + style: YaruBackButtonStyle.rounded, + onPressed: () => Navigator.of(context).pop(), ), - children: [ - Align( - alignment: Alignment.centerLeft, - child: InkWell( - borderRadius: BorderRadius.circular(5), - onTap: () async => await launchUrl(Uri.parse(repoUrl)), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - context.l10n.findOurRepository, - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: Theme.of(context).colorScheme.primary, + ), + Expanded( + child: ListView( + padding: const EdgeInsets.only( + top: kYaruPagePadding, + bottom: kYaruPagePadding, + left: 40, + right: 40, + ), + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Image.asset( + 'assets/software.png', + width: 100, + height: 100, + filterQuality: FilterQuality.medium, + ), + Padding( + padding: const EdgeInsets.all(10.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + model.appName ?? '', + style: Theme.of(context).textTheme.headlineSmall, + ), + Text( + '${context.l10n.version} ${model.version} ${model.buildNumber}', + ), + InkWell( + borderRadius: BorderRadius.circular(5), + onTap: () async => + await launchUrl(Uri.parse(model.repoUrl)), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + context.l10n.findOurRepository, + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith( + color: context.linkColor, + ), + ), + const SizedBox( + width: 5, + ), + Icon( + YaruIcons.external_link, + color: context.linkColor, + size: 18, + ) + ], ), + ), + ], ), - const SizedBox( - width: 5, - ), - Icon( - YaruIcons.external_link, - color: Theme.of(context).primaryColor, - size: 18, - ) - ], - ), + ), + ], ), - ), - const SizedBox( - height: 20, - ), - SizedBox( - width: 400, - height: 300, - child: FutureBuilder( + const SizedBox( + height: 20, + ), + FutureBuilder( future: loadAsset(context), builder: (context, snapshot) { if (snapshot.hasData) { - return Markdown( - padding: EdgeInsets.zero, + return MarkdownBody( data: '${context.l10n.madeBy}:\n ${snapshot.data!}', onTapLink: (text, href, title) => href != null ? launchUrl(Uri.parse(href)) : null, + styleSheet: MarkdownStyleSheet( + a: TextStyle(color: context.linkColor), + ), ); } else { return const SizedBox(); } }, ), - ) - ], - context: context, - useRootNavigator: false, - ); - }, - child: Text(context.l10n.about), + ], + ), + ) + ], ), - enabled: true, ); } @@ -324,3 +362,63 @@ class _AboutTile extends StatelessWidget { .loadString('assets/contributors.md'); } } + +class _LicenseTile extends StatelessWidget { + const _LicenseTile(); + + @override + Widget build(BuildContext context) { + return YaruTile( + title: Link( + linkText: '${context.l10n.license}: GPL3', + url: 'https://www.gnu.org/licenses/gpl-3.0.de.html', + textStyle: Theme.of(context).textTheme.bodyLarge, + ), + trailing: OutlinedButton( + onPressed: () => Utils.settingsNav.currentState!.pushNamed('/licenses'), + child: Text(context.l10n.packagesUsed), + ), + enabled: true, + ); + } +} + +class _LicensePage extends StatelessWidget { + const _LicensePage(); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: BorderRadius.circular(10), + ), + child: ClipRRect( + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(8.0), + bottomRight: Radius.circular(8.0), + ), + child: Column( + children: [ + const YaruDialogTitleBar(), + Expanded( + child: Theme( + data: Theme.of(context).copyWith( + pageTransitionsTheme: + YaruMasterDetailTheme.of(context).landscapeTransitions, + ), + child: const LicensePage(), + ), + ), + ], + ), + ), + ); + } +} + +class Utils { + static GlobalKey settingsNav = GlobalKey(); + static GlobalKey repoNav = GlobalKey(); + static GlobalKey aboutNav = GlobalKey(); +} diff --git a/lib/app/settings/theme_tile.dart b/lib/app/settings/theme_tile.dart new file mode 100644 index 000000000..7bc91db3b --- /dev/null +++ b/lib/app/settings/theme_tile.dart @@ -0,0 +1,144 @@ +import 'package:flutter/material.dart'; +import 'package:yaru_colors/yaru_colors.dart'; +import 'package:yaru_icons/yaru_icons.dart'; + +class ThemeTile extends StatelessWidget { + const ThemeTile(this.themeMode, {super.key}); + + final ThemeMode themeMode; + + @override + Widget build(BuildContext context) { + const height = 100.0; + const width = 150.0; + var borderRadius2 = BorderRadius.circular(12); + var lightContainer = Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: borderRadius2, + ), + ); + var darkContainer = Container( + decoration: BoxDecoration( + color: YaruColors.coolGrey, + borderRadius: borderRadius2, + ), + ); + var titleBar = Container( + height: 20, + decoration: BoxDecoration( + color: themeMode == ThemeMode.dark + ? Colors.white.withOpacity(0.05) + : Colors.black.withOpacity(0.1), + borderRadius: const BorderRadius.only( + topRight: Radius.circular(10), + topLeft: Radius.circular(10), + ), + ), + ); + return Stack( + alignment: Alignment.topRight, + children: [ + Card( + elevation: 5, + child: SizedBox( + height: height, + width: width, + child: themeMode == ThemeMode.system + ? Stack( + children: [ + ClipPath( + clipBehavior: Clip.antiAlias, + clipper: _CustomClipPathLight( + height: height, + width: width, + ), + child: lightContainer, + ), + ClipPath( + clipBehavior: Clip.antiAlias, + clipper: _CustomClipPathDark( + height: height, + width: width, + ), + child: darkContainer, + ), + titleBar + ], + ) + : (themeMode == ThemeMode.light + ? Stack( + children: [lightContainer, titleBar], + ) + : Stack( + children: [darkContainer, titleBar], + )), + ), + ), + Positioned( + right: 8, + top: 5, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + YaruIcons.window_minimize, + color: + themeMode == ThemeMode.dark ? Colors.white : Colors.black, + size: 15, + ), + Icon( + YaruIcons.window_maximize, + size: 15, + color: + themeMode == ThemeMode.dark ? Colors.white : Colors.black, + ), + Icon( + YaruIcons.window_close, + size: 15, + color: + themeMode == ThemeMode.dark ? Colors.white : Colors.black, + ), + ], + ), + ), + ], + ); + } +} + +class _CustomClipPathDark extends CustomClipper { + _CustomClipPathDark({required this.height, required this.width}); + + final double height; + final double width; + + @override + Path getClip(Size size) { + Path path = Path(); + path.lineTo(0, width); + path.lineTo(width, height); + return path; + } + + @override + bool shouldReclip(CustomClipper oldClipper) => false; +} + +class _CustomClipPathLight extends CustomClipper { + _CustomClipPathLight({required this.height, required this.width}); + + final double height; + final double width; + + @override + Path getClip(Size size) { + Path path = Path(); + path.lineTo(width, 0); + path.lineTo(width, height); + return path; + } + + @override + bool shouldReclip(CustomClipper oldClipper) => false; +} diff --git a/lib/app/updates/package_updates_page.dart b/lib/app/updates/package_updates_page.dart deleted file mode 100644 index 23cd8f93f..000000000 --- a/lib/app/updates/package_updates_page.dart +++ /dev/null @@ -1,396 +0,0 @@ -/* - * Copyright (C) 2022 Canonical Ltd - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 as - * published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -import 'dart:math'; - -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:software/l10n/l10n.dart'; -import 'package:software/services/packagekit/package_service.dart'; -import 'package:software/app/common/border_container.dart'; -import 'package:software/app/common/constants.dart'; -import 'package:software/app/common/message_bar.dart'; -import 'package:software/app/common/updates_splash_screen.dart'; -import 'package:software/app/updates/no_updates_page.dart'; -import 'package:software/app/updates/update_banner.dart'; -import 'package:software/app/updates/package_updates_model.dart'; -import 'package:software/services/packagekit/updates_state.dart'; -import 'package:ubuntu_service/ubuntu_service.dart'; -import 'package:ubuntu_session/ubuntu_session.dart'; -import 'package:ubuntu_widgets/ubuntu_widgets.dart'; -import 'package:xdg_icons/xdg_icons.dart'; -import 'package:yaru_icons/yaru_icons.dart'; -import 'package:yaru_widgets/yaru_widgets.dart'; - -class PackageUpdatesPage extends StatefulWidget { - const PackageUpdatesPage({super.key, required this.appFormatPopup}); - - final Widget appFormatPopup; - - static Widget create({ - required BuildContext context, - required Widget appFormatPopup, - }) { - return ChangeNotifierProvider( - create: (_) => PackageUpdatesModel( - getService(), - getService(), - ), - child: PackageUpdatesPage(appFormatPopup: appFormatPopup), - ); - } - - @override - State createState() => _PackageUpdatesPageState(); -} - -class _PackageUpdatesPageState extends State { - @override - void initState() { - super.initState(); - final model = context.read(); - model.init(handleError: () => showSnackBar()); - } - - void showSnackBar() { - if (!mounted) return; - final model = context.read(); - if (model.errorMessage.isNotEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - duration: const Duration(minutes: 1), - padding: EdgeInsets.zero, - content: MessageBar( - message: model.errorMessage, - copyMessage: context.l10n.copyErrorMessage, - ), - ), - ); - } - } - - @override - Widget build(BuildContext context) { - final model = context.watch(); - var hPadding = (0.00013 * pow(MediaQuery.of(context).size.width, 2)) - 20; - - hPadding = hPadding > 800 ? 800 : hPadding; - - return Column( - children: [ - _UpdatesHeader(appFormatsPopup: widget.appFormatPopup), - if (model.updatesState == UpdatesState.noUpdates) - const Expanded(child: Center(child: NoUpdatesPage())), - if (model.updatesState == UpdatesState.readyToUpdate) - _UpdatesListView(hPadding: hPadding), - if (model.updatesState == UpdatesState.updating) - _UpdatingPage(hPadding: hPadding), - if (model.updatesState == UpdatesState.checkingForUpdates) - Expanded( - child: Center( - child: SingleChildScrollView( - child: UpdatesSplashScreen( - icon: YaruIcons.debian, - percentage: model.percentage, - ), - ), - ), - ) - ], - ); - } -} - -class _UpdatingPage extends StatefulWidget { - const _UpdatingPage({ - required this.hPadding, - }); - - final double hPadding; - - @override - State<_UpdatingPage> createState() => _UpdatingPageState(); -} - -class _UpdatingPageState extends State<_UpdatingPage> { - //final terminalController = TerminalController(); - - @override - Widget build(BuildContext context) { - final model = context.watch(); - - final children = [ - Text( - model.info != null ? model.info!.name : '', - style: Theme.of(context).textTheme.headlineMedium, - ), - const SizedBox( - height: 20, - ), - Text( - model.processedId != null ? model.processedId!.name : '', - style: Theme.of(context).textTheme.titleLarge, - ), - const SizedBox( - height: 20, - ), - Padding( - padding: EdgeInsets.only( - left: widget.hPadding * 1.5, - right: widget.hPadding * 1.5, - ), - child: YaruLinearProgressIndicator( - value: model.percentage != null ? model.percentage! / 100 : 0, - ), - ), - const SizedBox( - height: 100, - ), - Padding( - padding: EdgeInsets.only(left: widget.hPadding, right: widget.hPadding), - child: BorderContainer( - color: Colors.transparent, - child: YaruExpandable( - header: Text( - 'Details', - style: Theme.of(context).textTheme.titleLarge, - ), - child: SizedBox( - height: 300, - width: 600, - child: LogView( - log: model.terminalOutput, - style: TextStyle( - inherit: false, - fontFamily: 'Ubuntu Mono', - fontSize: Theme.of(context).textTheme.bodyMedium!.fontSize, - textBaseline: TextBaseline.alphabetic, - ), - ), - ), - ), - ), - ), - ]; - - return Expanded( - child: Center( - child: ListView( - children: [ - for (final child in children) - Center( - child: child, - ) - ], - ), - ), - ); - } -} - -class _UpdatesHeader extends StatelessWidget { - const _UpdatesHeader({ - required this.appFormatsPopup, - }); - - final Widget appFormatsPopup; - - @override - Widget build(BuildContext context) { - final model = context.watch(); - - return Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.all(kPagePadding), - child: Wrap( - direction: Axis.horizontal, - alignment: WrapAlignment.start, - crossAxisAlignment: WrapCrossAlignment.start, - runAlignment: WrapAlignment.start, - textDirection: TextDirection.rtl, - spacing: 10, - runSpacing: 10, - children: [ - if (model.updates.isNotEmpty) - ElevatedButton( - onPressed: model.updatesState == UpdatesState.readyToUpdate && - !model.nothingSelected - ? () => model.updateAll( - updatesComplete: context.l10n.updatesComplete, - updatesAvailable: context.l10n.updateAvailable, - ) - : null, - child: Text(context.l10n.updateButton), - ), - OutlinedButton( - onPressed: model.updatesState == UpdatesState.updating || - model.updatesState == UpdatesState.checkingForUpdates - ? null - : () => model.refresh(), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon( - YaruIcons.refresh, - size: 18, - ), - const SizedBox( - width: 5, - ), - Text(context.l10n.refreshButton) - ], - ), - ), - if (model.updatesState == UpdatesState.noUpdates) - if (model.requireRestartApp) - ElevatedButton( - onPressed: () => model.exitApp(), - child: Text(context.l10n.requireRestartApp), - ) - else if (model.requireRestartSession) - ElevatedButton( - onPressed: () => model.logout(), - child: Text(context.l10n.requireRestartSession), - ) - else if (model.requireRestartSystem) - ElevatedButton( - onPressed: () => model.reboot(), - child: Text(context.l10n.requireRestartSystem), - ), - appFormatsPopup, - ], - ), - ), - ); - } -} - -class _UpdatesListView extends StatefulWidget { - // ignore: unused_element - const _UpdatesListView({super.key, required this.hPadding}); - - final double hPadding; - - @override - State<_UpdatesListView> createState() => _UpdatesListViewState(); -} - -class _UpdatesListViewState extends State<_UpdatesListView> { - bool _isExpanded = true; - - @override - Widget build(BuildContext context) { - final model = context.watch(); - - return Expanded( - child: ListView( - children: [ - const XdgIcon( - name: 'aptdaemon-upgrade', - theme: 'Yaru', - size: 100, - ), - const SizedBox( - height: 10, - ), - Center( - child: Text( - context.l10n.weHaveUpdates, - style: Theme.of(context).textTheme.headlineSmall, - textAlign: TextAlign.center, - ), - ), - const SizedBox( - height: 10, - ), - Padding( - padding: EdgeInsets.only( - top: 20, - bottom: 50, - left: widget.hPadding, - right: widget.hPadding, - ), - child: BorderContainer( - child: YaruExpandable( - isExpanded: _isExpanded, - onChange: (isExpanded) => - setState(() => _isExpanded = isExpanded), - header: MouseRegion( - cursor: SystemMouseCursors.click, - child: _isExpanded - ? Row( - mainAxisSize: MainAxisSize.min, - children: [ - YaruCheckbox( - value: model.allSelected - ? true - : model.nothingSelected - ? false - : null, - tristate: true, - onChanged: (v) => v != null - ? model.selectAll() - : model.deselectAll(), - ), - const SizedBox( - width: 10, - ), - Expanded( - child: Text( - '${model.selectedUpdatesLength}/${model.updates.length} ${context.l10n.xSelected}', - style: Theme.of(context).textTheme.titleLarge, - overflow: TextOverflow.ellipsis, - ), - ) - ], - ) - : Text( - '${model.selectedUpdatesLength}/${model.updates.length} ${context.l10n.xSelected}', - style: Theme.of(context).textTheme.titleLarge, - ), - ), - child: Padding( - padding: const EdgeInsets.only(top: kYaruPagePadding), - child: Column( - children: List.generate(model.updates.length, (index) { - final update = model.getUpdate(index); - return SizedBox( - height: 70, - child: UpdateBanner( - group: model.getGroup(update), - selected: model.isUpdateSelected(update), - updateId: update, - installedId: - model.getInstalledId(update.name) ?? update, - onChanged: model.updatesState == - UpdatesState.checkingForUpdates - ? null - : (v) => model.selectUpdate(update, v!), - ), - ); - }), - ), - ), - ), - ), - ), - ], - ), - ); - } -} diff --git a/lib/app/updates/snap_updates_model.dart b/lib/app/updates/snap_updates_model.dart deleted file mode 100644 index 72783fa3b..000000000 --- a/lib/app/updates/snap_updates_model.dart +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (C) 2022 Canonical Ltd - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 as - * published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -import 'dart:async'; - -import 'package:safe_change_notifier/safe_change_notifier.dart'; -import 'package:snapd/snapd.dart'; -import 'package:software/services/snap_service.dart'; - -class SnapUpdatesModel extends SafeChangeNotifier { - SnapUpdatesModel( - this._snapService, - ); - - final SnapService _snapService; - StreamSubscription? _snapChangesSub; - StreamSubscription? _refreshErrorSub; - List? _snapsWithUpdates; - List? get snapsWithUpdates => _snapsWithUpdates; - - Future init({Function(String)? onRefreshError}) async { - _snapChangesSub = _snapService.snapChangesInserted.listen((_) { - checkingForUpdates = true; - if (_snapService.snapChanges.isEmpty) { - _loadSnapsWithUpdate().then((_) => checkingForUpdates = false); - } - }); - _refreshErrorSub = _snapService.refreshError.listen((event) { - if (onRefreshError != null) { - onRefreshError(event); - } - }); - - await checkForUpdates(); - } - - @override - Future dispose() async { - await _snapChangesSub?.cancel(); - await _refreshErrorSub?.cancel(); - super.dispose(); - } - - bool _checkingForUpdates = false; - bool get checkingForUpdates => _checkingForUpdates; - set checkingForUpdates(bool value) { - if (value == _checkingForUpdates) return; - _checkingForUpdates = value; - notifyListeners(); - } - - Future checkForUpdates() async { - checkingForUpdates = true; - _snapsWithUpdates = await _loadSnapsWithUpdate(); - checkingForUpdates = false; - } - - Future> _loadSnapsWithUpdate() async => - await _snapService.loadSnapsWithUpdate(); - - Future refreshAll({ - required String doneMessage, - }) async { - await _snapService.authorize(); - if (_snapsWithUpdates == null) return; - for (var snap in _snapsWithUpdates!) { - _snapService.refresh( - snap: snap, - message: doneMessage, - confinement: snap.confinement, - channel: snap.channel, - ); - } - notifyListeners(); - } -} diff --git a/lib/app/updates/snap_updates_page.dart b/lib/app/updates/snap_updates_page.dart deleted file mode 100644 index a0b581f29..000000000 --- a/lib/app/updates/snap_updates_page.dart +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright (C) 2022 Canonical Ltd - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 as - * published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:software/app/common/constants.dart'; -import 'package:software/app/common/snap/snap_grid.dart'; -import 'package:software/app/common/updates_splash_screen.dart'; -import 'package:software/app/updates/no_updates_page.dart'; -import 'package:software/app/updates/snap_updates_model.dart'; -import 'package:software/l10n/l10n.dart'; -import 'package:software/services/snap_service.dart'; -import 'package:ubuntu_service/ubuntu_service.dart'; -import 'package:yaru_icons/yaru_icons.dart'; - -class SnapUpdatesPage extends StatelessWidget { - const SnapUpdatesPage({super.key, required this.appFormatPopup}); - - final Widget appFormatPopup; - - static Widget create({ - required BuildContext context, - required Widget appFormatPopup, - }) { - return ChangeNotifierProvider( - create: (context) => SnapUpdatesModel( - getService(), - )..init( - onRefreshError: (e) => ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text(e))), - ), - child: SnapUpdatesPage(appFormatPopup: appFormatPopup), - ); - } - - @override - Widget build(BuildContext context) { - final model = context.watch(); - final snaps = model.snapsWithUpdates ?? []; - - return Column( - children: [ - Padding( - padding: const EdgeInsets.all(kPagePadding), - child: _SnapUpdatesHeader(appFormatPopup: appFormatPopup), - ), - if (model.checkingForUpdates) - const Expanded( - child: Center( - child: UpdatesSplashScreen(icon: YaruIcons.snapcraft), - ), - ) - else if (snaps.isEmpty) - const Expanded(child: Center(child: NoUpdatesPage())) - else - Expanded( - child: SnapGrid( - snaps: snaps, - ), - ) - ], - ); - } -} - -class _SnapUpdatesHeader extends StatelessWidget { - const _SnapUpdatesHeader({ - required this.appFormatPopup, - }); - - final Widget appFormatPopup; - - @override - Widget build(BuildContext context) { - final model = context.watch(); - final snaps = model.snapsWithUpdates ?? []; - return Align( - alignment: Alignment.centerLeft, - child: Wrap( - spacing: 10, - children: [ - appFormatPopup, - OutlinedButton( - onPressed: model.checkingForUpdates ? null : model.checkForUpdates, - child: Text( - context.l10n.refreshButton, - ), - ), - if (snaps.isNotEmpty) - ElevatedButton( - onPressed: model.checkingForUpdates - ? null - : () => model.refreshAll( - doneMessage: context.l10n.done, - ), - child: Text(context.l10n.updateButton), - ), - ], - ), - ); - } -} diff --git a/lib/app/updates/updates_model.dart b/lib/app/updates/updates_model.dart deleted file mode 100644 index e559ce7ef..000000000 --- a/lib/app/updates/updates_model.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:safe_change_notifier/safe_change_notifier.dart'; -import 'package:software/app/common/app_format.dart'; -import 'package:software/services/packagekit/package_service.dart'; - -class UpdatesModel extends SafeChangeNotifier { - final PackageService _packageService; - - AppFormat? _appFormat; - - UpdatesModel(this._packageService); - - Future init() async { - _enabledAppFormats.add(AppFormat.snap); - if (_packageService.isAvailable) { - _enabledAppFormats.add(AppFormat.packageKit); - _appFormat = AppFormat.packageKit; - notifyListeners(); - } - } - - AppFormat? get appFormat => _appFormat; - set appFormat(AppFormat? value) { - if (value == null || value == _appFormat) return; - _appFormat = value; - notifyListeners(); - } - - final Set _enabledAppFormats = {}; - Set get enabledAppFormats => _enabledAppFormats; - - void setAppFormat(AppFormat value) { - if (value == _appFormat) return; - _appFormat = value; - if (_appFormat == AppFormat.packageKit && _packageService.isAvailable) { - _packageService.getInstalledPackages().then((_) => notifyListeners()); - } else { - notifyListeners(); - } - } -} diff --git a/lib/app/updates/updates_page.dart b/lib/app/updates/updates_page.dart deleted file mode 100644 index f32cacf36..000000000 --- a/lib/app/updates/updates_page.dart +++ /dev/null @@ -1,137 +0,0 @@ -import 'package:badges/badges.dart' as badges; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:software/app/common/app_format.dart'; -import 'package:software/app/common/app_format_popup.dart'; -import 'package:software/app/common/constants.dart'; -import 'package:software/app/common/indeterminate_circular_progress_icon.dart'; -import 'package:software/app/updates/package_updates_page.dart'; -import 'package:software/app/updates/snap_updates_page.dart'; -import 'package:software/app/updates/updates_model.dart'; -import 'package:software/l10n/l10n.dart'; -import 'package:software/services/packagekit/package_service.dart'; -import 'package:ubuntu_service/ubuntu_service.dart'; -import 'package:yaru_icons/yaru_icons.dart'; -import 'package:yaru_widgets/yaru_widgets.dart'; - -class UpdatesPage extends StatefulWidget { - const UpdatesPage({ - super.key, - required this.windowWidth, - }); - - final double windowWidth; - - static Widget create({ - required BuildContext context, - required double windowWidth, - }) { - return ChangeNotifierProvider( - create: (context) => UpdatesModel(getService())..init(), - child: UpdatesPage(windowWidth: windowWidth), - ); - } - - static Widget createTitle(BuildContext context) => Text(context.l10n.updates); - - static Widget createIcon({ - required BuildContext context, - required bool selected, - int? badgeCount, - bool? processing, - }) { - return _UpdatesIcon( - count: badgeCount ?? 0, - processing: processing ?? false, - ); - } - - @override - State createState() => _UpdatesPageState(); -} - -class _UpdatesPageState extends State - with TickerProviderStateMixin { - late TabController _tabController; - - @override - void initState() { - super.initState(); - _tabController = TabController(length: 2, vsync: this); - } - - @override - void dispose() { - _tabController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final model = context.watch(); - - final appFormatPopup = AppFormatPopup( - onSelected: (appFormat) => model.appFormat = appFormat, - appFormat: model.appFormat ?? AppFormat.snap, - enabledAppFormats: model.enabledAppFormats, - ); - - return Scaffold( - appBar: YaruWindowTitleBar( - titleSpacing: 0, - title: Text(context.l10n.updates), - ), - body: model.appFormat == AppFormat.packageKit - ? PackageUpdatesPage.create( - context: context, - appFormatPopup: appFormatPopup, - ) - : SnapUpdatesPage.create( - context: context, - appFormatPopup: appFormatPopup, - ), - ); - } -} - -class _UpdatesIcon extends StatelessWidget { - const _UpdatesIcon({ - // ignore: unused_element - super.key, - required this.count, - required this.processing, - }); - - final int count; - final bool processing; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - if (processing && count > 0) { - return badges.Badge( - position: badges.BadgePosition.topEnd(), - badgeColor: count > 0 ? theme.primaryColor : Colors.transparent, - badgeContent: count > 0 - ? Text( - count.toString(), - style: badgeTextStyle, - ) - : null, - child: const IndeterminateCircularProgressIcon(), - ); - } else if (processing && count == 0) { - return const IndeterminateCircularProgressIcon(); - } else if (!processing && count > 0) { - return badges.Badge( - badgeColor: theme.primaryColor, - badgeContent: Text( - count.toString(), - style: badgeTextStyle, - ), - child: const Icon(YaruIcons.sync), - ); - } - return const Icon(YaruIcons.sync); - } -} diff --git a/lib/l10n/app_cs.arb b/lib/l10n/app_cs.arb new file mode 100644 index 000000000..7d1e6d697 --- /dev/null +++ b/lib/l10n/app_cs.arb @@ -0,0 +1,430 @@ +{ + "explorePageTitle": "Procházet", + "@explorePageTitle": {}, + "myAppsPageTitle": "Moje aplikace", + "@myAppsPageTitle": {}, + "updatesPageTitle": "Aktualizace", + "@updatesPageTitle": {}, + "artAndDesign": "Umění a design", + "@artAndDesign": {}, + "development": "Vývoj", + "@development": {}, + "devicesAndIot": "Zařízení a IOT", + "@devicesAndIot": {}, + "education": "Výuka", + "@education": {}, + "entertainment": "Zábava", + "@entertainment": {}, + "featured": "Významné", + "@featured": {}, + "finance": "Finance", + "@finance": {}, + "healthAndFitness": "Zdraví a fitness", + "@healthAndFitness": {}, + "musicAndAudio": "Hudba a zvuk", + "@musicAndAudio": {}, + "newsAndWeather": "Zprávy a počasí", + "@newsAndWeather": {}, + "personalisation": "Přizpůsobení", + "@personalisation": {}, + "photoAndVideo": "Fotografie a video", + "@photoAndVideo": {}, + "productivity": "Kancelář", + "@productivity": {}, + "science": "Věda", + "@science": {}, + "social": "Sociální", + "@social": {}, + "utilities": "Nástroje", + "@utilities": {}, + "all": "Všechny kategorie snapů", + "@all": {}, + "artAndDesignSlogan": "Nástroje pro umělce", + "@artAndDesignSlogan": {}, + "booksAndReferenceSlogan": "Uspořádejte svou sbírku knih", + "@booksAndReferenceSlogan": {}, + "developmentSlogan": "Aplikace pro vývojáře", + "@developmentSlogan": {}, + "devicesAndIotSlogan": "Zařízení a IOT", + "@devicesAndIotSlogan": {}, + "featuredSlogan": "Naše doporučené aplikace", + "@featuredSlogan": {}, + "gamesSlogan": "Hry a hraní", + "@gamesSlogan": {}, + "musicAndAudioSlogan": "Hudba a zvuk", + "@musicAndAudioSlogan": {}, + "newsAndWeatherSlogan": "Zprávy a počasí", + "@newsAndWeatherSlogan": {}, + "personalisationSlogan": "Přizpůsobení", + "@personalisationSlogan": {}, + "scienceSlogan": "Vědecké nástroje", + "@scienceSlogan": {}, + "securitySlogan": "Chraňte svá data", + "@securitySlogan": {}, + "serverAndCloudSlogan": "Server a cloud", + "@serverAndCloudSlogan": {}, + "socialSlogan": "Pojďte spolu", + "@socialSlogan": {}, + "utilitiesSlogan": "Nástroje", + "@utilitiesSlogan": {}, + "lastUpdated": "Naposledy aktualizováno", + "@lastUpdated": {}, + "notInstalled": "Není nainstalováno", + "@notInstalled": {}, + "confinement": "Režim ohraničení", + "@confinement": {}, + "license": "Licence", + "@license": {}, + "version": "Verze", + "@version": {}, + "channel": "Kanál", + "@channel": {}, + "install": "Instalovat", + "@install": {}, + "removeAll": "Odebrat vše", + "@removeAll": {}, + "confirmRemove": "Opravdu chcete odstranit tento balíček?", + "@confirmRemove": {}, + "removePackage": "Odebrat {package}", + "@removePackage": { + "placeholders": { + "package": { + "type": "String" + } + } + }, + "website": "Webové stránky", + "@website": {}, + "description": "Popis", + "@description": {}, + "open": "Otevřít", + "@open": {}, + "contact": "Kontakt", + "@contact": {}, + "about": "O aplikaci", + "@about": {}, + "showMore": "Zobrazit více", + "@showMore": {}, + "showLess": "Zobrazit méně", + "@showLess": {}, + "snapPackages": "Balíčky Snap", + "@snapPackages": {}, + "snapPackage": "Snap", + "@snapPackage": {}, + "debianPackages": "Balíčky Debian", + "@debianPackages": {}, + "debianPackage": "Debian", + "@debianPackage": {}, + "updateAvailable": "Aktualizace k dispozici", + "@updateAvailable": {}, + "size": "Velikost", + "@size": {}, + "updates": "Aktualizace", + "@updates": {}, + "searchHint": "Hledat", + "@searchHint": {}, + "searchHintAppStore": "Hledat aplikace", + "@searchHintAppStore": {}, + "searchHintInstalled": "Hledat nainstalované aplikace", + "@searchHintInstalled": {}, + "updateSelected": "Aktualizace vybrána", + "@updateSelected": {}, + "xSelected": "vybrané aktualizace", + "@xSelected": {}, + "deselectAll": "Odznačit vše", + "@deselectAll": {}, + "update": "Aktualizace", + "@update": {}, + "updateButton": "Aktualizovat", + "@updateButton": {}, + "weHaveUpdates": "Máme pro vás aktualizace!", + "@weHaveUpdates": {}, + "justAMoment": "Ještě chvilku!", + "@justAMoment": {}, + "checkingForUpdates": "Vyhledáváme aktualizace", + "@checkingForUpdates": {}, + "updating": "Vydržte – aktualizujeme váš systém. Nezavírejte prosím tuto aplikaci ani nevypínejte počítač", + "@updating": {}, + "filterSnaps": "Nastavte filtr snapů", + "@filterSnaps": {}, + "enterRepoName": "Zadejte název repozitáře", + "@enterRepoName": {}, + "requireRestartSystem": "Pro dokončení aktualizací restartujte systém", + "@requireRestartSystem": {}, + "requireRestartSession": "Pro dokončení aktualizací se odhlaste", + "@requireRestartSession": {}, + "source": "Zdroj", + "@source": {}, + "confirm": "Potvrdit", + "@confirm": {}, + "quit": "Ukončit", + "@quit": {}, + "cancel": "Zrušit", + "@cancel": {}, + "updatesComplete": "Aktualizace dokončeny", + "@updatesComplete": {}, + "architecture": "Architektura", + "@architecture": {}, + "verified": "Ověřený vydavatel", + "@verified": {}, + "classic": "klasický", + "@classic": {}, + "findOurRepository": "Najděte si nás na GitHubu", + "@findOurRepository": {}, + "changelogTooLong": "Pro úplný seznam změn prosím navštivte:", + "@changelogTooLong": {}, + "noPackageFound": "Omlouváme se, ale s tímto vyhledávacím dotazem jsme nenašli žádný balíček", + "@noPackageFound": {}, + "noSnapFound": "Omlouváme se, ale s tímto vyhledávacím dotazem jsme nenašli žádný snap", + "@noSnapFound": {}, + "quitDanger": "Právě běží aktualizace systému. Ukončení aplikace nyní může způsobit poškození systému!", + "@quitDanger": {}, + "packageKitFilter": "Typ balíčku", + "@packageKitFilter": {}, + "packageType": "Typ balíčku", + "@packageType": {}, + "packageDetails": "Podrobnosti o balíčku", + "@packageDetails": {}, + "copyErrorMessage": "Zkopírovat chybovou zprávu", + "@copyErrorMessage": {}, + "madeBy": "Ubuntu Software je vyvinut a navržen", + "@madeBy": {}, + "reviewsAndRatings": "Recenze a hodnocení", + "@reviewsAndRatings": {}, + "ratingsAndReviews": "Hodnocení a recenze", + "@ratingsAndReviews": {}, + "ratings": "Hodnocení", + "@ratings": {}, + "yourReview": "Vaše recenze", + "@yourReview": {}, + "yourReviewTitle": "Název recenze", + "@yourReviewTitle": {}, + "summary": "Shrnutí", + "@summary": {}, + "yourReviewName": "Vaše jméno", + "@yourReviewName": {}, + "clickToRate": "Klikněte pro hodnocení", + "@clickToRate": {}, + "rate": "Hodnotit", + "@rate": {}, + "showAllReviews": "Zobrazit všechny recenze", + "@showAllReviews": {}, + "unknown": "Neznámé", + "@unknown": {}, + "reviewSent": "Recenze odeslána", + "@reviewSent": {}, + "helpful": "Užitečné", + "@helpful": {}, + "notHelpful": "Neužitečné", + "@notHelpful": {}, + "whatDataIsSend": "Zjistěte, jaká data se odesílají v našich ", + "@whatDataIsSend": {}, + "privacyPolicy": "zásadách ochrany osobních údajů.", + "@privacyPolicy": {}, + "multiAppFormatsFound": "Pro tuto aplikaci jsme našli několik formátů.", + "@multiAppFormatsFound": {}, + "changingPermissions": "Změna oprávnění", + "@changingPermissions": {}, + "attention": "Pozor!", + "@attention": {}, + "removing": "Odebírání", + "@removing": {}, + "refreshing": "Obnovování", + "@refreshing": {}, + "processing": "Zpracovávání", + "@processing": {}, + "ready": "Připraveno", + "@ready": {}, + "collection": "Sbírka", + "@collection": {}, + "packagesUsed": "Použité balíčky", + "@packagesUsed": {}, + "starDeveloper": "Hvězdný vývojář", + "@starDeveloper": {}, + "links": "Odkazy", + "@links": {}, + "additionalInformation": "Další informace", + "@additionalInformation": {}, + "dependenciesAutoremove": "Odebrat již nepotřebné závislosti", + "@dependenciesAutoremove": {}, + "dependenciesInstallListing": "{length} závislostí s celkovou velikostí {size} bude staženo při instalaci {packageName}", + "@dependenciesInstallListing": { + "placeholders": { + "length": { + "type": "int" + }, + "size": { + "type": "String" + }, + "packageName": { + "type": "String" + } + } + }, + "dependenciesFullList": "Zobrazit úplný seznam závislostí", + "@dependenciesFullList": {}, + "dependenciesQuestion": "Jste si jisti, že chcete pokračovat?", + "@dependenciesQuestion": {}, + "dependencies": "Závislosti", + "@dependencies": {}, + "permissions": "Oprávnění", + "@permissions": {}, + "downloading": "Stahování", + "@downloading": {}, + "downloadRemaining": "Stahování... {bytes} zbývá", + "@downloadRemaining": { + "placeholders": { + "bytes": { + "type": "String" + } + } + }, + "upgrading": "Povyšování", + "@upgrading": {}, + "theme": "Motiv", + "@theme": {}, + "system": "Systém", + "@system": {}, + "light": "Světlý", + "@light": {}, + "dark": "Tmavý", + "@dark": {}, + "noSnapsInstalled": "Ve vašem systému nejsou nainstalovány žádné Snap aplikace", + "@noSnapsInstalled": {}, + "releasedAt": "Vydáno v", + "@releasedAt": {}, + "configure": "Nastavit", + "@configure": {}, + "sourcesDescription": "Nastavte, odkud se aktualizuje váš systém a balíčky Debianu třetích stran.", + "@sourcesDescription": {}, + "reportReviewDialogTitle": "Nahlásit recenzi", + "@reportReviewDialogTitle": {}, + "reportAbuse": "Nahlásit zneužití", + "@reportAbuse": {}, + "report": "Nahlásit", + "@report": {}, + "share": "Sdílet", + "@share": {}, + "copiedToClipboard": "Zkopírováno do schránky", + "@copiedToClipboard": {}, + "manage": "Spravovat", + "@manage": {}, + "reportReviewDialogBody": "Můžete nahlásit recenzi za hrubé, neslušné nebo diskriminační chování. Po nahlášení bude recenze skryta, dokud nebude zkontrolována administrátorem.", + "@reportReviewDialogBody": {}, + "appTitle": "Ubuntu Software", + "@appTitle": {}, + "settingsPageTitle": "Nastavení", + "@settingsPageTitle": {}, + "booksAndReference": "Knihy a reference", + "@booksAndReference": {}, + "games": "Hry", + "@games": {}, + "security": "Bezpečnost", + "@security": {}, + "updatesAvailable": "aktualizace k dispozici", + "@updatesAvailable": {}, + "serverAndCloud": "Server a cloud", + "@serverAndCloud": {}, + "healthAndFitnessSlogan": "Zdraví a fitness", + "@healthAndFitnessSlogan": {}, + "entertainmentSlogan": "Nástroje pro domácí zábavu", + "@entertainmentSlogan": {}, + "educationSlogan": "Nástroje pro domácí vzdělávání", + "@educationSlogan": {}, + "productivitySlogan": "Buďte produktivní!", + "@productivitySlogan": {}, + "financeSlogan": "Finanční nástroje", + "@financeSlogan": {}, + "photoAndVideoSlogan": "Fotografie a video", + "@photoAndVideoSlogan": {}, + "installDate": "Datum instalace", + "@installDate": {}, + "refresh": "Obnovit", + "@refresh": {}, + "remove": "Odebrat", + "@remove": {}, + "offline": "Offline", + "@offline": {}, + "sortBy": "Řadit podle", + "@sortBy": {}, + "media": "Média", + "@media": {}, + "connections": "Spojení", + "@connections": {}, + "name": "Název", + "@name": {}, + "done": "Hotovo", + "@done": {}, + "selectAll": "Vybrat vše", + "@selectAll": {}, + "allSelected": "Všechny aktualizace vybrány", + "@allSelected": {}, + "multiUpdateButton": "Aktualizovat vše", + "@multiUpdateButton": {}, + "refreshButton": "Zkontrolovat aktualizace", + "@refreshButton": {}, + "noUpdates": "Vše je aktuální", + "@noUpdates": {}, + "installed": "Nainstalováno", + "@installed": {}, + "installing": "Instaluje se", + "@installing": {}, + "packageInstaller": "Instalátor balíčků", + "@packageInstaller": {}, + "readyToUpdate": "Připraveno k aktualizaci", + "@readyToUpdate": {}, + "publisher": "Vydavatel", + "@publisher": {}, + "apps": "aplikace", + "@apps": {}, + "sources": "Zdroje", + "@sources": {}, + "changelog": "Seznam změn", + "@changelog": {}, + "contributors": "Přispěvatelé", + "@contributors": {}, + "gallery": "Galerie", + "@gallery": {}, + "dependenciesRemoveListing": "{length} závislostí s celkovou velikostí {size} lze automaticky odstranit při odstranění {packageName}", + "@dependenciesRemoveListing": { + "placeholders": { + "length": { + "type": "int" + }, + "size": { + "type": "String" + }, + "packageName": { + "type": "String" + } + } + }, + "issued": "Vydáno", + "@issued": {}, + "runsInBackground": "Ubuntu Software zůstává běžet na pozadí pro kontrolu aktualizací systému.", + "@runsInBackground": {}, + "rating": "Hodnocení", + "@rating": {}, + "packageKitGroup": "Kategorie", + "@packageKitGroup": {}, + "requireRestartApp": "Pro dokončení aktualizací restartujte aplikaci", + "@requireRestartApp": {}, + "appFormat": "Formát balíčku aplikací", + "@appFormat": {}, + "allPackageTypes": "Všechny typy balíčků", + "@allPackageTypes": {}, + "writeAreview": "Napsat recenzi", + "@writeAreview": {}, + "whatDoYouThink": "Co si o aplikaci myslíte? Zkuste svůj pohled zdůvodnit.", + "@whatDoYouThink": {}, + "send": "Odeslat", + "@send": {}, + "summeryHint": "Uveďte krátké shrnutí své recenze, například: Skvělá aplikace, doporučuji.", + "@summeryHint": {}, + "submit": "Odeslat", + "@submit": {}, + "appstreamSearchGreylist": "aplikace;balíček;program;sada;nástroj", + "@appstreamSearchGreylist": { + "description": "List of 'grey-listed' words separated with ';'. Do not translate this list directly. Instead, provide a list of words in your language that people are likely to include in a search but that should normally be ignored in the search." + } +} diff --git a/lib/l10n/app_da.arb b/lib/l10n/app_da.arb index 0b257c423..984cc1f8c 100644 --- a/lib/l10n/app_da.arb +++ b/lib/l10n/app_da.arb @@ -1,5 +1,5 @@ { - "appTitle": "Ubuntu Software", + "appTitle": "Ubuntu Varehus", "@appTitle": {}, "explorePageTitle": "Udforsk", "@explorePageTitle": {}, @@ -17,7 +17,7 @@ "@development": {}, "devicesAndIot": "Enheder og IOT", "@devicesAndIot": {}, - "education": "Reference", + "education": "Uddannelse", "@education": {}, "entertainment": "Underholdning", "@entertainment": {}, @@ -27,13 +27,13 @@ "@finance": {}, "games": "Spil", "@games": {}, - "healthAndFitness": "Sundhed og Fitness", + "healthAndFitness": "Helse og Form", "@healthAndFitness": {}, "musicAndAudio": "Musik og Lyd", "@musicAndAudio": {}, - "newsAndWeather": "Nyheder og Vejret", + "newsAndWeather": "Nyheder og Vejr", "@newsAndWeather": {}, - "personalisation": "Personalisering", + "personalisation": "Tilpasning", "@personalisation": {}, "photoAndVideo": "Foto og Video", "@photoAndVideo": {}, @@ -45,11 +45,11 @@ "@security": {}, "serverAndCloud": "Server og Sky", "@serverAndCloud": {}, - "social": "Social", + "social": "Socialt", "@social": {}, - "utilities": "Hjælpeprogrammer", + "utilities": "Værktøjer", "@utilities": {}, - "all": "Alle", + "all": "Alle snap-kategorier", "@all": {}, "installDate": "Installationsdato", "@installDate": {}, @@ -57,21 +57,21 @@ "@lastUpdated": {}, "notInstalled": "Ikke installeret", "@notInstalled": {}, - "confinement": "Indespærring", + "confinement": "Forvaring", "@confinement": {}, "license": "Licens", "@license": {}, - "version": "Version", + "version": "Udgave", "@version": {}, "channel": "Kanal", "@channel": {}, - "install": "Installere", + "install": "Installér", "@install": {}, - "refresh": "Opdater", + "refresh": "Genopfrisk", "@refresh": {}, "remove": "Fjern", "@remove": {}, - "website": "Hjemmeside", + "website": "Netsted", "@website": {}, "description": "Beskrivelse", "@description": {}, @@ -87,9 +87,9 @@ "@showLess": {}, "offline": "Offline", "@offline": {}, - "snapPackages": "Snap pakker", + "snapPackages": "Snap-pakker", "@snapPackages": {}, - "debianPackages": "Debian pakker", + "debianPackages": "Debian-pakker", "@debianPackages": {}, "connections": "Forbindelser", "@connections": {}, @@ -99,9 +99,9 @@ "@size": {}, "name": "Navn", "@name": {}, - "sortBy": "Sorter efter", + "sortBy": "Sortér efter", "@sortBy": {}, - "media": "Media", + "media": "Medier", "@media": {}, "done": "Færdig", "@done": {}, @@ -115,21 +115,21 @@ "@selectAll": {}, "deselectAll": "Fravælg alle", "@deselectAll": {}, - "update": "Opdater", + "update": "Opdatering", "@update": {}, - "noUpdates": "Alt er opdateret", + "noUpdates": "Alt er ajour", "@noUpdates": {}, "apps": "apps", "@apps": {}, - "filterSnaps": "Set snap filteret", + "filterSnaps": "Indstil snap-filteret", "@filterSnaps": {}, - "enterRepoName": "Skriv arkiv navn", + "enterRepoName": "Indtast depot-navn", "@enterRepoName": {}, - "requireRestartSystem": "Genstart systemet for at afslutte opdatering", + "requireRestartSystem": "Genstart system, for at færdiggøre opdateringer", "@requireRestartSystem": {}, - "requireRestartSession": "Log ud for at afslutte opdatering", + "requireRestartSession": "Log ud, for at færdiggøre opdateringer", "@requireRestartSession": {}, - "requireRestartApp": "Genstart appen for at færdiggøre opdatering", + "requireRestartApp": "Genstart appen, for at færdiggøre opdateringer", "@requireRestartApp": {}, "issued": "Udstedt", "@issued": {}, @@ -141,23 +141,23 @@ "@source": {}, "sources": "Kilder", "@sources": {}, - "packageInstaller": "Pakkeinstalleringsprogram", + "packageInstaller": "Pakkeinstallatør", "@packageInstaller": {}, "classic": "klassisk", "@classic": {}, - "changelogTooLong": "Se hele ændringsloggen på:", + "changelogTooLong": "For den fulde ændringslog, besøg venligst:", "@changelogTooLong": {}, - "runsInBackground": "Ubuntu Software bliver ved med at køre i baggrunden for at se efter systemopdateringer.", + "runsInBackground": "Ubuntu Varehus vedbliver at køre i baggrunden, for at kontrollere for systemopdateringer.", "@runsInBackground": {}, - "updating": "Vent - vi opdaterer dit system. Luk ikke denne app og luk ikke din computer", + "updating": "Vent lige - vi opdaterer dit system. Luk venligst ikke denne app eller luk din computer", "@updating": {}, - "readyToUpdate": "Klar til at opdatere", + "readyToUpdate": "Klar til opdatering", "@readyToUpdate": {}, - "verified": "Verificeret udgiver", + "verified": "Godkendt Udgiver", "@verified": {}, - "quitDanger": "Systemopdateringer kører i øjeblikket. Hvis du afslutter appen nu, kan dit system blive beskadiget!", + "quitDanger": "Systemopdateringer kører i øjeblikket. Afslutning af Appen nu, vil kunne efterlade dit system i en beskadiget tilstand!", "@quitDanger": {}, - "checkingForUpdates": "Lige et øjeblik - vi tjekker efter opdateringer", + "checkingForUpdates": "Vi kontrollerer for opdateringer", "@checkingForUpdates": {}, "noPackageFound": "Beklager, vi kunne ikke finde nogen pakke med denne søgeforespørgsel", "@noPackageFound": {}, @@ -169,7 +169,7 @@ "@updatesComplete": {}, "findOurRepository": "Find os på GitHub", "@findOurRepository": {}, - "cancel": "Annullere", + "cancel": "Afbryd", "@cancel": {}, "confirm": "Bekræft", "@confirm": {}, @@ -177,12 +177,254 @@ "@packageKitGroup": {}, "packageKitFilter": "Pakketypen", "@packageKitFilter": {}, - "packageDetails": "Pakkedetaljer", + "packageDetails": "Pakkeoplysninger", "@packageDetails": {}, "copyErrorMessage": "Kopiér fejlmeddelelse", "@copyErrorMessage": {}, - "madeBy": "Ubuntu Software er udviklet og designet af", + "madeBy": "Ubuntu Varehus er udviklet og designet af", "@madeBy": {}, - "attention": "OBS!", - "@attention": {} + "attention": "Giv agt!", + "@attention": {}, + "searchHintAppStore": "Søg efter apps", + "@searchHintAppStore": {}, + "searchHintInstalled": "Søg i dine installerede apps", + "@searchHintInstalled": {}, + "multiAppFormatsFound": "Vi har fundet flere forskellige formater for denne applikation.", + "@multiAppFormatsFound": {}, + "installing": "Installerer", + "@installing": {}, + "installed": "Installeret", + "@installed": {}, + "appFormat": "Applikationers pakkeformat", + "@appFormat": {}, + "dependenciesInstallListing": "{length} afhængigheder med en samlet størrelse på {size} vil blive hentet, under installation af {packageName}", + "@dependenciesInstallListing": { + "placeholders": { + "length": { + "type": "int" + }, + "size": { + "type": "String" + }, + "packageName": { + "type": "String" + } + } + }, + "updatesAvailable": "opdateringer tilgængelige", + "@updatesAvailable": {}, + "gamesSlogan": "Spil og gaming", + "@gamesSlogan": {}, + "healthAndFitnessSlogan": "Helse og Form", + "@healthAndFitnessSlogan": {}, + "photoAndVideoSlogan": "Foto og Video", + "@photoAndVideoSlogan": {}, + "dependenciesRemoveListing": "{length} afhængigheder med en samlet størrelse på {size} kan fjernes automatisk, under fjernelse af {packageName}", + "@dependenciesRemoveListing": { + "placeholders": { + "length": { + "type": "int" + }, + "size": { + "type": "String" + }, + "packageName": { + "type": "String" + } + } + }, + "noSnapsInstalled": "Der er ingen Snap-applikationer installeret på dit system", + "@noSnapsInstalled": {}, + "manage": "Administrér", + "@manage": {}, + "contributors": "Bidragsydere", + "@contributors": {}, + "securitySlogan": "Beskyt dine data", + "@securitySlogan": {}, + "publisher": "Udgiver", + "@publisher": {}, + "sourcesDescription": "Indstil hvor dit system og tredjeparts Debian-pakker opdateres fra.", + "@sourcesDescription": {}, + "yourReviewTitle": "Anmeldelsestitel (valgfrit)", + "@yourReviewTitle": {}, + "appstreamSearchGreylist": "app;applikation;pakke;program;værktøj;tilbehør;plugin", + "@appstreamSearchGreylist": { + "description": "List of 'grey-listed' words separated with ';'. Do not translate this list directly. Instead, provide a list of words in your language that people are likely to include in a search but that should normally be ignored in the search." + }, + "yourReviewName": "Dit navn (valgfrit)", + "@yourReviewName": {}, + "financeSlogan": "Finansværktøjer", + "@financeSlogan": {}, + "yourReview": "Din anmeldelse", + "@yourReview": {}, + "developmentSlogan": "Apps til udviklere", + "@developmentSlogan": {}, + "links": "Link", + "@links": {}, + "reviewsAndRatings": "Anmeldelser og gradueringer", + "@reviewsAndRatings": {}, + "refreshButton": "Kontrollér for opdateringer", + "@refreshButton": {}, + "reportAbuse": "Rapportér misbrug", + "@reportAbuse": {}, + "changingPermissions": "Ændrer tilladelser", + "@changingPermissions": {}, + "dependenciesQuestion": "Er du sikker på, at du vil fortsætte?", + "@dependenciesQuestion": {}, + "additionalInformation": "Yderligere Information", + "@additionalInformation": {}, + "dependenciesAutoremove": "Fjern ikke længere nødvendige afhængigheder", + "@dependenciesAutoremove": {}, + "packageType": "Pakketype", + "@packageType": {}, + "newsAndWeatherSlogan": "Nyheder og Vejr", + "@newsAndWeatherSlogan": {}, + "ratingsAndReviews": "Gradueringer og anmeldelser", + "@ratingsAndReviews": {}, + "packagesUsed": "Pakker benyttet", + "@packagesUsed": {}, + "reportReviewDialogTitle": "Rapportér anmeldelse", + "@reportReviewDialogTitle": {}, + "dependencies": "Afhængigheder", + "@dependencies": {}, + "upgrading": "Opgraderer", + "@upgrading": {}, + "reviewSent": "Anmeldelse sendt", + "@reviewSent": {}, + "ratings": "Gradueringer", + "@ratings": {}, + "reportReviewDialogBody": "Du kan rapportere en anmeldelse for krænkende, flabet, eller diskriminerende adfærd. Når den er rapporteret, vil en anmeldelse blive skjult, indtil den er blevet kontrolleret af en administrator.", + "@reportReviewDialogBody": {}, + "share": "Del", + "@share": {}, + "configure": "Konfigurér", + "@configure": {}, + "artAndDesignSlogan": "Værktøjer til kunstnere", + "@artAndDesignSlogan": {}, + "devicesAndIotSlogan": "Enheder og IOT", + "@devicesAndIotSlogan": {}, + "educationSlogan": "Værktøjer til hjemmeskole", + "@educationSlogan": {}, + "musicAndAudioSlogan": "Musik og Lyd", + "@musicAndAudioSlogan": {}, + "personalisationSlogan": "Tilpasning", + "@personalisationSlogan": {}, + "productivitySlogan": "Vær produktiv!", + "@productivitySlogan": {}, + "scienceSlogan": "Videnskabelige værktøjer", + "@scienceSlogan": {}, + "socialSlogan": "Find sammen", + "@socialSlogan": {}, + "utilitiesSlogan": "Værktøjer", + "@utilitiesSlogan": {}, + "allSelected": "Alle opdateringer valgt", + "@allSelected": {}, + "xSelected": "opdateringer valgt", + "@xSelected": {}, + "weHaveUpdates": "Vis har opdateringer til dig!", + "@weHaveUpdates": {}, + "justAMoment": "Lige et øjeblik!", + "@justAMoment": {}, + "updateButton": "Opdatér", + "@updateButton": {}, + "clickToRate": "Klik for at graduere", + "@clickToRate": {}, + "allPackageTypes": "Alle pakketyper", + "@allPackageTypes": {}, + "rating": "Graduering", + "@rating": {}, + "showAllReviews": "Vis alle anmeldelser", + "@showAllReviews": {}, + "unknown": "Ukendt", + "@unknown": {}, + "gallery": "Galleri", + "@gallery": {}, + "releasedAt": "Udgivet den", + "@releasedAt": {}, + "removeAll": "Fjern alt", + "@removeAll": {}, + "confirmRemove": "Er du sikker på, at du vil fjerne denne pakke?", + "@confirmRemove": {}, + "removePackage": "Fjern {package}", + "@removePackage": { + "placeholders": { + "package": { + "type": "String" + } + } + }, + "snapPackage": "Snap", + "@snapPackage": {}, + "multiUpdateButton": "Opdatér alt", + "@multiUpdateButton": {}, + "writeAreview": "Skriv en anmeldelse", + "@writeAreview": {}, + "summary": "Opsummering", + "@summary": {}, + "rate": "Graduér", + "@rate": {}, + "whatDoYouThink": "Hvad synes du om appen? Prøv at give en grund til synspunktet.", + "@whatDoYouThink": {}, + "summeryHint": "Giv en kort opsummering af din anmeldelse, for eksempel: God app, klart anbefalet.", + "@summeryHint": {}, + "submit": "Indsend", + "@submit": {}, + "helpful": "Nyttigt", + "@helpful": {}, + "notHelpful": "Ikke nyttigt", + "@notHelpful": {}, + "whatDataIsSend": "Find ud af, hvilke data der sendes i vores ", + "@whatDataIsSend": {}, + "privacyPolicy": "privatlivspolitik.", + "@privacyPolicy": {}, + "downloading": "Henter", + "@downloading": {}, + "downloadRemaining": "Henter... {bytes} tilbage", + "@downloadRemaining": { + "placeholders": { + "bytes": { + "type": "String" + } + } + }, + "ready": "Klar", + "@ready": {}, + "removing": "Fjerner", + "@removing": {}, + "refreshing": "Genopfrisker", + "@refreshing": {}, + "theme": "Tema", + "@theme": {}, + "system": "System", + "@system": {}, + "light": "Lyst", + "@light": {}, + "dependenciesFullList": "Se fuld liste over afhængigheder", + "@dependenciesFullList": {}, + "starDeveloper": "Stjerneudvikler", + "@starDeveloper": {}, + "collection": "Samling", + "@collection": {}, + "copiedToClipboard": "Kopieret til udklipsholder", + "@copiedToClipboard": {}, + "report": "Rapportér", + "@report": {}, + "debianPackage": "Debian", + "@debianPackage": {}, + "entertainmentSlogan": "Værktøjer til hjemmeunderholdning", + "@entertainmentSlogan": {}, + "featuredSlogan": "Vores udvalgte apps", + "@featuredSlogan": {}, + "serverAndCloudSlogan": "Server og Sky", + "@serverAndCloudSlogan": {}, + "booksAndReferenceSlogan": "Organisér din bogsamling", + "@booksAndReferenceSlogan": {}, + "send": "Send", + "@send": {}, + "permissions": "Tilladelser", + "@permissions": {}, + "processing": "Behandler", + "@processing": {}, + "dark": "Mørkt", + "@dark": {} } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index caf4affe5..5ea7a0bf8 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -97,7 +97,7 @@ "@lastUpdated": {}, "notInstalled": "Nicht installiert", "@notInstalled": {}, - "confinement": "Confinement", + "confinement": "Einschränkung", "@confinement": {}, "license": "Lizenz", "@license": {}, @@ -169,11 +169,11 @@ "@update": {}, "updateButton": "Aktualisieren", "@updateButton": {}, - "noUpdates": "Alles auf dem neusten Stand!", + "noUpdates": "Alles ist auf dem neuesten Stand", "@noUpdates": {}, - "updating": "Bitte habe etwas Geduld - wir aktualisieren dein System", + "updating": "Bleiben Sie dran - wir aktualisieren gerade Ihr System. Bitte schließen Sie diese App nicht, und fahren Sie Ihren Computer nicht herunter", "@updating": {}, - "checkingForUpdates": "Wir suchen nach neuen Aktualisierungen", + "checkingForUpdates": "Wir prüfen auf Aktualisierungen", "@checkingForUpdates": {}, "readyToUpdate": "Bereit zur Aktualisierung", "@readyToUpdate": {}, @@ -239,17 +239,17 @@ "@copyErrorMessage": {}, "madeBy": "Ubuntu Software wird entwickelt und designed von", "@madeBy": {}, - "reviewsAndRatings": "Bewertungen und Rezensionen", + "reviewsAndRatings": "Rezensionen und Bewertungen", "@reviewsAndRatings": {}, "yourReview": "Ihre Rezension", "@yourReview": {}, - "yourReviewTitle": "Title (optional)", + "yourReviewTitle": "Titel der Rezension", "@yourReviewTitle": {}, - "yourReviewName": "Ihr Name (optional)", + "yourReviewName": "Ihr Name", "@yourReviewName": {}, "clickToRate": "Anklicken zum bewerten", "@clickToRate": {}, - "showAllReviews": "All Rezensionen anzeigen", + "showAllReviews": "Alle Rezensionen anzeigen", "@showAllReviews": {}, "send": "Senden", "@send": {}, @@ -259,13 +259,13 @@ "@reviewSent": {}, "multiAppFormatsFound": "Wir konnten verschiedene Formate für diese Anwendung finden.", "@multiAppFormatsFound": {}, - "changingPermissions": "Ändere Berechtigungen", + "changingPermissions": "Berechtigungen ändern", "@changingPermissions": {}, - "installing": "Installiere", + "installing": "Wird installiert", "@installing": {}, - "removing": "Entferne", + "removing": "Wird entfernt", "@removing": {}, - "refreshing": "Frische auf", + "refreshing": "Wird aufgefrischt", "@refreshing": {}, "attention": "Achtung!", "@attention": {}, @@ -293,17 +293,6 @@ "@theme": {}, "dependenciesFullList": "Siehe vollständige Liste der Abhängigkeiten", "@dependenciesFullList": {}, - "dependenciesListing": "{length} Abhängigkeiten werden bei der Installation von {packageName} heruntergeladen", - "@dependenciesListing": { - "placeholders": { - "length": { - "type": "int" - }, - "packageName": { - "type": "String" - } - } - }, "gallery": "Galerie", "@gallery": {}, "dependenciesQuestion": "Sind Sie sicher, dass Sie fortfahren möchten?", @@ -327,5 +316,115 @@ "additionalInformation": "Zusätzliche Informationen", "@additionalInformation": {}, "links": "Links", - "@links": {} + "@links": {}, + "searchHintAppStore": "Nach Apps suchen", + "@searchHintAppStore": {}, + "searchHintInstalled": "Durchsuchen Sie Ihre installierten Apps", + "@searchHintInstalled": {}, + "multiUpdateButton": "Alle aktualisieren", + "@multiUpdateButton": {}, + "downloading": "Wird heruntergeladen", + "@downloading": {}, + "downloadRemaining": "Wird heruntergeladen... {bytes} verbleiben", + "@downloadRemaining": { + "placeholders": { + "bytes": { + "type": "String" + } + } + }, + "ready": "Bereit", + "@ready": {}, + "packagesUsed": "Verwendete Pakete", + "@packagesUsed": {}, + "contributors": "Mitwirkende", + "@contributors": {}, + "collection": "Sammlung", + "@collection": {}, + "upgrading": "Wird hochgestuft", + "@upgrading": {}, + "removeAll": "Alle entfernen", + "@removeAll": {}, + "confirmRemove": "Sind Sie sicher, dass Sie dieses Paket entfernen möchten?", + "@confirmRemove": {}, + "rate": "Bewerten", + "@rate": {}, + "whatDoYouThink": "Was halten Sie von der App? Versuchen Sie, eine Begründung für Ihre Ansicht zu nennen.", + "@whatDoYouThink": {}, + "submit": "Absenden", + "@submit": {}, + "reportReviewDialogBody": "Sie können eine Bewertung wegen beleidigendem, unhöflichem oder diskriminierendem Verhalten melden. Nach der Meldung wird die Bewertung ausgeblendet, bis sie von einem Administrator überprüft wurde.", + "@reportReviewDialogBody": {}, + "dependenciesInstallListing": "{length} Abhängigkeiten mit einer Gesamtgröße von {size} werden bei der Installation von {packageName} heruntergeladen", + "@dependenciesInstallListing": { + "placeholders": { + "length": { + "type": "int" + }, + "size": { + "type": "String" + }, + "packageName": { + "type": "String" + } + } + }, + "removePackage": "{package} entfernen", + "@removePackage": { + "placeholders": { + "package": { + "type": "String" + } + } + }, + "packageType": "Pakettyp", + "@packageType": {}, + "ratingsAndReviews": "Bewertungen und Rezensionen", + "@ratingsAndReviews": {}, + "ratings": "Bewertungen", + "@ratings": {}, + "writeAreview": "Eine Rezension schreiben", + "@writeAreview": {}, + "summary": "Zusammenfassung", + "@summary": {}, + "summeryHint": "Schreiben Sie eine kurze Zusammenfassung Ihrer Bewertung, zum Beispiel: Tolle App, würde ich empfehlen.", + "@summeryHint": {}, + "helpful": "Hilfreich", + "@helpful": {}, + "notHelpful": "Nicht hilfreich", + "@notHelpful": {}, + "whatDataIsSend": "Finden Sie heraus, welche Daten gesendet werden in unserer ", + "@whatDataIsSend": {}, + "privacyPolicy": "Datenschutzerklärung.", + "@privacyPolicy": {}, + "dependenciesRemoveListing": "{length} Abhängigkeiten mit einer Gesamtgröße von {size} können beim Entfernen von {packageName} automatisch entfernt werden", + "@dependenciesRemoveListing": { + "placeholders": { + "length": { + "type": "int" + }, + "size": { + "type": "String" + }, + "packageName": { + "type": "String" + } + } + }, + "dependenciesAutoremove": "Nicht mehr benötigte Abhängigkeiten entfernen", + "@dependenciesAutoremove": {}, + "starDeveloper": "Star-Entwickler", + "@starDeveloper": {}, + "manage": "Verwalten", + "@manage": {}, + "copiedToClipboard": "In die Zwischenablage kopiert", + "@copiedToClipboard": {}, + "share": "Teilen", + "@share": {}, + "report": "Melden", + "@report": {}, + "reportAbuse": "Missbrauch melden", + "@reportAbuse": {}, + "reportReviewDialogTitle": "Bewertung melden", + "@reportReviewDialogTitle": {} } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index d7ef4af72..97315b98f 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -55,6 +55,16 @@ "install": "Install", "refresh": "Refresh", "remove": "Remove", + "removeAll": "Remove all", + "confirmRemove": "Are you sure you want to remove this package?", + "removePackage": "Remove {package}", + "@removePackage": { + "placeholders": { + "package": { + "type": "String" + } + } + }, "website": "Website", "description": "Description", "open": "Open", @@ -88,6 +98,7 @@ "deselectAll": "Deselect all", "update": "Update", "updateButton": "Update", + "multiUpdateButton": "Update all", "refreshButton": "Check for updates", "noUpdates": "Everything is up to date", "updating": "Hang on - we are updating your system. Please do not close this app, or shutdown your computer", @@ -122,26 +133,50 @@ "allPackageTypes": "All package types", "packageKitGroup": "The category", "packageKitFilter": "The type of package", + "packageType": "Package type", "packageDetails": "Package details", "copyErrorMessage": "Copy error message", "madeBy": "Ubuntu Software is developed and designed by", "reviewsAndRatings": "Reviews and ratings", + "ratingsAndReviews": "Ratings and reviews", "rating": "Rating", + "ratings": "Ratings", "yourReview": "Your review", - "yourReviewTitle": "Review Title (optional)", - "yourReviewName": "Your name (optional)", + "writeAreview": "Write a review", + "yourReviewTitle": "Review Title", + "summary": "Summary", + "yourReviewName": "Your name", "clickToRate": "Click to rate", + "rate": "Rate", "showAllReviews": "Show all reviews", + "whatDoYouThink": "What do you think of the app? Try to give a reason for the view.", + "summeryHint": "Give a short summary of your review, for example: Great app, would recommend.", "send": "Send", + "submit": "Submit", "unknown": "Unknown", "reviewSent": "Review sent", + "helpful": "Helpful", + "notHelpful": "Not helpful", + "whatDataIsSend": "Find out what data is send in our ", + "privacyPolicy": "privacy policy.", "multiAppFormatsFound": "We've found multiple formats for this application.", "changingPermissions": "Changing permissions", "permissions": "Permissions", + "downloading": "Downloading", + "downloadRemaining": "Downloading... {bytes} remaining", + "@downloadRemaining": { + "placeholders": { + "bytes": { + "type": "String" + } + } + }, "installing": "Installing", "processing": "Processing", + "ready": "Ready", "removing": "Removing", "refreshing": "Refreshing", + "upgrading": "Upgrading", "attention": "Attention!", "theme": "Theme", "system": "System", @@ -153,24 +188,54 @@ "description": "List of 'grey-listed' words separated with ';'. Do not translate this list directly. Instead, provide a list of words in your language that people are likely to include in a search but that should normally be ignored in the search." }, "releasedAt": "Released at", + "lastUpdated": "Last updated", "installed": "Installed", "configure": "Configure", "sourcesDescription": "Setup where your system and third-party Debian packages are updated from.", "dependencies": "Dependencies", "dependenciesQuestion": "Are you sure you want to proceed?", "dependenciesFullList": "See full list of dependencies", - "dependenciesListing": "{length} dependencies will be downloaded when installing {packageName}", - "@dependenciesListing" : { + "dependenciesInstallListing": "{length} dependencies with a total size of {size} will be downloaded when installing {packageName}", + "@dependenciesInstallListing" : { + "placeholders": { + "length": { + "type": "int" + }, + "size": { + "type": "String" + }, + "packageName": { + "type": "String" + } + } + }, + "dependenciesRemoveListing": "{length} dependencies with a total size of {size} can be automatically removed when removing {packageName}", + "@dependenciesRemoveListing" : { "placeholders": { "length": { "type": "int" }, + "size": { + "type": "String" + }, "packageName": { "type": "String" } } }, + "dependenciesAutoremove": "Remove no longer needed dependencies", "gallery": "Gallery", "additionalInformation": "Additional Information", - "links": "Links" -} \ No newline at end of file + "links": "Links", + "starDeveloper": "Star developer", + "packagesUsed": "Packages used", + "contributors": "Contributors", + "collection": "Collection", + "manage": "Manage", + "copiedToClipboard": "Copied to clipboard", + "share": "Share", + "report": "Report", + "reportAbuse": "Report abuse", + "reportReviewDialogTitle": "Report review", + "reportReviewDialogBody": "You can report a review for abusive, rude, or discriminatory behavior. Once reported, a review will be hidden until it has been checked by an administrator." +} diff --git a/lib/l10n/app_eo.arb b/lib/l10n/app_eo.arb index daa7a4a96..72cbd0e14 100644 --- a/lib/l10n/app_eo.arb +++ b/lib/l10n/app_eo.arb @@ -251,7 +251,7 @@ "@refreshButton": {}, "allPackageTypes": "Ĉiaj pakoj", "@allPackageTypes": {}, - "yourReviewTitle": "Titolo de recenzo (nedeviga)", + "yourReviewTitle": "Titolo de recenzo", "@yourReviewTitle": {}, "showAllReviews": "Montri ĉiujn recenzojn", "@showAllReviews": {}, @@ -277,7 +277,7 @@ "@booksAndReferenceSlogan": {}, "utilitiesSlogan": "Pliutiligu vian komputilon", "@utilitiesSlogan": {}, - "yourReviewName": "Via nomo (nedeviga)", + "yourReviewName": "Via nomo", "@yourReviewName": {}, "sourcesDescription": "Agordi la fontojn, laŭ kiuj ĝisdatiĝos viaj sistemaj kaj triapartiaj Debian-pakoj.", "@sourcesDescription": {}, @@ -311,21 +311,120 @@ "@publisher": {}, "noSnapsInstalled": "Ekzistas neniuj Snap-programoj instalitaj sur via sistemo", "@noSnapsInstalled": {}, - "dependenciesListing": "{length} dependaĵoj estas kuninstalotaj kune kun {packageName}", - "@dependenciesListing": { + "permissions": "Permesoj", + "@permissions": {}, + "additionalInformation": "Pliaj informoj", + "@additionalInformation": {}, + "links": "Ligoj", + "@links": {}, + "searchHintAppStore": "Serĉi programojn", + "@searchHintAppStore": {}, + "searchHintInstalled": "Serĉi viajn instalitajn programojn", + "@searchHintInstalled": {}, + "multiUpdateButton": "Ĝisdatigi ĉion", + "@multiUpdateButton": {}, + "packagesUsed": "Uzataj pakoj", + "@packagesUsed": {}, + "upgrading": "Ĝisdatigante", + "@upgrading": {}, + "downloading": "Elŝutante", + "@downloading": {}, + "ready": "Prete", + "@ready": {}, + "downloadRemaining": "Elŝutante… {bytes} restas", + "@downloadRemaining": { + "placeholders": { + "bytes": { + "type": "String" + } + } + }, + "contributors": "Kontribuintoj", + "@contributors": {}, + "collection": "Kolekto", + "@collection": {}, + "summary": "Resumo", + "@summary": {}, + "whatDoYouThink": "Kiel vi opinias pri la programo? Provu klarigi kialojn de via opinio.", + "@whatDoYouThink": {}, + "removeAll": "Malinstali ĉion", + "@removeAll": {}, + "removePackage": "Malinstali {package}", + "@removePackage": { + "placeholders": { + "package": { + "type": "String" + } + } + }, + "confirmRemove": "Ĉu vi certe volas malinstali ĉi tiun pakon?", + "@confirmRemove": {}, + "packageType": "Speco de pako", + "@packageType": {}, + "ratings": "Taksoj", + "@ratings": {}, + "writeAreview": "Verki recenzon", + "@writeAreview": {}, + "ratingsAndReviews": "Taksoj kaj recenzoj", + "@ratingsAndReviews": {}, + "submit": "Submeti", + "@submit": {}, + "rate": "Taksi", + "@rate": {}, + "summeryHint": "Koncize resumu vian recenzon, ekzemple: Bona programo, rekomendinda.", + "@summeryHint": {}, + "privacyPolicy": "reguloj pri privateco.", + "@privacyPolicy": {}, + "helpful": "Utila", + "@helpful": {}, + "notHelpful": "Malutila", + "@notHelpful": {}, + "whatDataIsSend": "Tiajn datenojn, kiaj sendiĝas, priskribas niaj ", + "@whatDataIsSend": {}, + "dependenciesRemoveListing": "{length} dependaĵoj de totala grando {size} estas aŭtomate malinstaleblaj kune kun {packageName}", + "@dependenciesRemoveListing": { "placeholders": { "length": { "type": "int" }, + "size": { + "type": "String" + }, "packageName": { "type": "String" } } }, - "permissions": "Permesoj", - "@permissions": {}, - "additionalInformation": "Pliaj informoj", - "@additionalInformation": {}, - "links": "Ligoj", - "@links": {} + "manage": "Administri", + "@manage": {}, + "starDeveloper": "Stelula programisto", + "@starDeveloper": {}, + "copiedToClipboard": "Kopiita en tondujon", + "@copiedToClipboard": {}, + "dependenciesInstallListing": "{length} dependaĵoj de totala grando {size} estas kuninstalotaj kune kun {packageName}", + "@dependenciesInstallListing": { + "placeholders": { + "length": { + "type": "int" + }, + "size": { + "type": "String" + }, + "packageName": { + "type": "String" + } + } + }, + "report": "Raporti", + "@report": {}, + "reportAbuse": "Raporti misuzon", + "@reportAbuse": {}, + "reportReviewDialogTitle": "Raporti recenzon", + "@reportReviewDialogTitle": {}, + "dependenciesAutoremove": "Malinstali ne plu necesajn dependaĵojn", + "@dependenciesAutoremove": {}, + "share": "Kunhavigi", + "@share": {}, + "reportReviewDialogBody": "Vi povas raporti recenzon pro misa, fia aŭ diskriminacia konduto. Raportita recenzo kaŝiĝos ĝis kontrolo de administranto.", + "@reportReviewDialogBody": {} } diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 28f7f94fd..d97f602d2 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -245,7 +245,7 @@ "@productivitySlogan": {}, "utilitiesSlogan": "Utilidades", "@utilitiesSlogan": {}, - "yourReviewTitle": "Título de la reseña (opcional)", + "yourReviewTitle": "Título de la reseña", "@yourReviewTitle": {}, "allPackageTypes": "Todos los tipos de paquetes", "@allPackageTypes": {}, @@ -255,7 +255,7 @@ "@removing": {}, "refreshing": "Actualizando", "@refreshing": {}, - "yourReviewName": "Tu nombre (opcional)", + "yourReviewName": "Tu nombre", "@yourReviewName": {}, "showAllReviews": "Mostrar todas las reseñas", "@showAllReviews": {}, @@ -305,17 +305,6 @@ "@gallery": {}, "dependenciesFullList": "Ver la lista completa de las dependencias", "@dependenciesFullList": {}, - "dependenciesListing": "{length} dependencias se descargarán al instalar {packageName}", - "@dependenciesListing": { - "placeholders": { - "length": { - "type": "int" - }, - "packageName": { - "type": "String" - } - } - }, "dependencies": "Dependencias", "@dependencies": {}, "dependenciesQuestion": "¿Estas seguro que deseas continuar?", @@ -331,5 +320,111 @@ "additionalInformation": "Información adicional", "@additionalInformation": {}, "links": "Enlaces", - "@links": {} + "@links": {}, + "packagesUsed": "Paquetes utilizados", + "@packagesUsed": {}, + "contributors": "Colaboradores", + "@contributors": {}, + "collection": "Colección", + "@collection": {}, + "multiUpdateButton": "Actualizar todo", + "@multiUpdateButton": {}, + "ready": "Listo", + "@ready": {}, + "upgrading": "Actualizando", + "@upgrading": {}, + "downloading": "Descargando", + "@downloading": {}, + "downloadRemaining": "Descargando... {bytes} restantes", + "@downloadRemaining": { + "placeholders": { + "bytes": { + "type": "String" + } + } + }, + "dependenciesInstallListing": "Las dependencias de {length} con un tamaño total de {size} se descargarán al instalar {packageName}", + "@dependenciesInstallListing": { + "placeholders": { + "length": { + "type": "int" + }, + "size": { + "type": "String" + }, + "packageName": { + "type": "String" + } + } + }, + "reportReviewDialogTitle": "Informe de revisión", + "@reportReviewDialogTitle": {}, + "reportReviewDialogBody": "Puedes denunciar una revisión por comportamiento abusivo, grosero o discriminatorio. Una vez informada, la reseña se ocultará hasta que un administrador la verifique.", + "@reportReviewDialogBody": {}, + "removeAll": "Eliminar todo", + "@removeAll": {}, + "confirmRemove": "¿Estás seguro de que quieres eliminar este paquete?", + "@confirmRemove": {}, + "removePackage": "Eliminar {package}", + "@removePackage": { + "placeholders": { + "package": { + "type": "String" + } + } + }, + "packageType": "Tipo de paquete", + "@packageType": {}, + "ratingsAndReviews": "Valoraciones y reseñas", + "@ratingsAndReviews": {}, + "ratings": "Valoraciones", + "@ratings": {}, + "writeAreview": "Escribe una reseña", + "@writeAreview": {}, + "summary": "Resumen", + "@summary": {}, + "rate": "Tasa", + "@rate": {}, + "whatDoYouThink": "¿Qué opinas de la aplicación? Trata de dar una razón desde tu punto de vista.", + "@whatDoYouThink": {}, + "summeryHint": "Proporciona un breve resumen de tu reseña, por ejemplo: Excelente aplicación, la recomendaría.", + "@summeryHint": {}, + "submit": "Entregar", + "@submit": {}, + "helpful": "Útil", + "@helpful": {}, + "notHelpful": "Inútil", + "@notHelpful": {}, + "whatDataIsSend": "Averigua qué datos se están enviando en nuestro ", + "@whatDataIsSend": {}, + "privacyPolicy": "política de privacidad.", + "@privacyPolicy": {}, + "dependenciesRemoveListing": "Las dependencias de {length} con un tamaño total de {size} se pueden eliminar automáticamente al eliminar {packageName}", + "@dependenciesRemoveListing": { + "placeholders": { + "length": { + "type": "int" + }, + "size": { + "type": "String" + }, + "packageName": { + "type": "String" + } + } + }, + "dependenciesAutoremove": "Eliminar las dependencias que ya no se necesitan", + "@dependenciesAutoremove": {}, + "starDeveloper": "Desarrollador estrella", + "@starDeveloper": {}, + "manage": "Administrar", + "@manage": {}, + "copiedToClipboard": "Copiado al portapapeles", + "@copiedToClipboard": {}, + "share": "Compartir", + "@share": {}, + "report": "Informe", + "@report": {}, + "reportAbuse": "Informar de un abuso", + "@reportAbuse": {} } diff --git a/lib/l10n/app_fa.arb b/lib/l10n/app_fa.arb index 9569bbf15..a568e6f57 100644 --- a/lib/l10n/app_fa.arb +++ b/lib/l10n/app_fa.arb @@ -1,76 +1,430 @@ { "appTitle": "نرم‌افزارهای اوبونتو", + "@appTitle": {}, "explorePageTitle": "کاوش", + "@explorePageTitle": {}, "myAppsPageTitle": "کاره‌های من", + "@myAppsPageTitle": {}, "updatesPageTitle": "به‌روز رسانی‌ها", + "@updatesPageTitle": {}, "settingsPageTitle": "تنظیمات", + "@settingsPageTitle": {}, "artAndDesign": "هنر و طراحی", + "@artAndDesign": {}, "booksAndReference": "کتاب‌ها و ارجاع", + "@booksAndReference": {}, "development": "توسعه", + "@development": {}, "devicesAndIot": "افزاره‌ها و اینترنت اشیاء", + "@devicesAndIot": {}, "education": "تحصیلات", + "@education": {}, "entertainment": "سرگرمی", + "@entertainment": {}, "featured": "ویژه", + "@featured": {}, "finance": "مالی", + "@finance": {}, "games": "بازی‌ها", + "@games": {}, "healthAndFitness": "سلامتی و تندرستی", + "@healthAndFitness": {}, "musicAndAudio": "آهنگ و صدا", + "@musicAndAudio": {}, "newsAndWeather": "اخبار و هواشناسی", + "@newsAndWeather": {}, "personalisation": "شخصی‌سازی", + "@personalisation": {}, "photoAndVideo": "تصویر و ویدیو", + "@photoAndVideo": {}, "productivity": "بهره‌وری", + "@productivity": {}, "science": "علمی", + "@science": {}, "security": "امنیت", + "@security": {}, "serverAndCloud": "کارساز و ابر", + "@serverAndCloud": {}, "social": "اجتماعی", + "@social": {}, "utilities": "ابزارها", + "@utilities": {}, "all": "همه", + "@all": {}, "installDate": "تاریخ نصب", + "@installDate": {}, "lastUpdated": "آخرین به‌روز رسانی", + "@lastUpdated": {}, "notInstalled": "نصب نشده", + "@notInstalled": {}, "confinement": "محدودیت", + "@confinement": {}, "license": "پروانه", + "@license": {}, "version": "نگارش", + "@version": {}, "channel": "کانال", + "@channel": {}, "install": "نصب", + "@install": {}, "refresh": "نوسازی", + "@refresh": {}, "remove": "برداشتن", + "@remove": {}, "website": "پایگاه", + "@website": {}, "description": "توضیحات", + "@description": {}, "open": "گشودن", + "@open": {}, "contact": "ارتباط", + "@contact": {}, "about": "درباره", + "@about": {}, "showMore": "نمایش بیشتر", + "@showMore": {}, "showLess": "نمایش کمتر", + "@showLess": {}, "offline": "برون‌خط", + "@offline": {}, "snapPackages": "بسته‌های اسنب", + "@snapPackages": {}, "debianPackages": "بسته‌های دبیان", + "@debianPackages": {}, "connections": "اتّصالات", + "@connections": {}, "updateAvailable": "به‌روز رسانی موجود است", + "@updateAvailable": {}, "size": "اندازه", + "@size": {}, "name": "نام", + "@name": {}, "sortBy": "مرتب‌سازی بر اساس", + "@sortBy": {}, "media": "رسانه", + "@media": {}, "done": "انجام شد", + "@done": {}, "updates": "به‌روز رسانی‌ها", + "@updates": {}, "searchHint": "جست‌وجو", + "@searchHint": {}, "updateSelected": "به‌روز رسانی گزیده شد", + "@updateSelected": {}, "selectAll": "گزینش همه", + "@selectAll": {}, "deselectAll": "لغو گزینش همه", + "@deselectAll": {}, "update": "به‌روز رسانی", + "@update": {}, "noUpdates": "همه چیز به‌روز است", + "@noUpdates": {}, "apps": "کاره‌ها", + "@apps": {}, "filterSnaps": "تنظیم پالایه‌های اسنپ", + "@filterSnaps": {}, "enterRepoName": "نام مخزن را وارد کنید", + "@enterRepoName": {}, "requireRestartSystem": "برای تکمیل به‌روز رسانی‌ها، سامانه را مجدداً راه‌اندازی کنید", + "@requireRestartSystem": {}, "requireRestartSession": "برای تکمیل به‌روز رسانی‌ها از حساب خود خارج شوید", + "@requireRestartSession": {}, "requireRestartApp": "برای تکمیل به‌روز رسانی‌ها، کاره‌ها را مجدداً راه‌اندازی کنید", + "@requireRestartApp": {}, "issued": "Issued", + "@issued": {}, "changelog": "تغییرات", + "@changelog": {}, "architecture": "معماری", + "@architecture": {}, "source": "منبع", + "@source": {}, "sources": "منابع", + "@sources": {}, "packageInstaller": "نصب‌کنندهٔ بسته", + "@packageInstaller": {}, "classic": "کلاسیک", - "verified": "ناشر تأیید شده" -} + "@classic": {}, + "verified": "ناشر تأیید شده", + "@verified": {}, + "noSnapFound": "", + "@noSnapFound": {}, + "changelogTooLong": "", + "@changelogTooLong": {}, + "allSelected": "", + "@allSelected": {}, + "packageKitFilter": "", + "@packageKitFilter": {}, + "updating": "", + "@updating": {}, + "confirmRemove": "", + "@confirmRemove": {}, + "updatesAvailable": "", + "@updatesAvailable": {}, + "checkingForUpdates": "", + "@checkingForUpdates": {}, + "submit": "", + "@submit": {}, + "readyToUpdate": "", + "@readyToUpdate": {}, + "summary": "", + "@summary": {}, + "writeAreview": "", + "@writeAreview": {}, + "packageKitGroup": "", + "@packageKitGroup": {}, + "packageType": "", + "@packageType": {}, + "weHaveUpdates": "", + "@weHaveUpdates": {}, + "justAMoment": "", + "@justAMoment": {}, + "searchHintInstalled": "", + "@searchHintInstalled": {}, + "report": "", + "@report": {}, + "whatDataIsSend": "", + "@whatDataIsSend": {}, + "healthAndFitnessSlogan": "", + "@healthAndFitnessSlogan": {}, + "xSelected": "", + "@xSelected": {}, + "multiAppFormatsFound": "", + "@multiAppFormatsFound": {}, + "newsAndWeatherSlogan": "", + "@newsAndWeatherSlogan": {}, + "ratings": "", + "@ratings": {}, + "refreshButton": "", + "@refreshButton": {}, + "share": "", + "@share": {}, + "ratingsAndReviews": "", + "@ratingsAndReviews": {}, + "publisher": "", + "@publisher": {}, + "reportReviewDialogTitle": "", + "@reportReviewDialogTitle": {}, + "contributors": "", + "@contributors": {}, + "debianPackage": "", + "@debianPackage": {}, + "entertainmentSlogan": "", + "@entertainmentSlogan": {}, + "searchHintAppStore": "", + "@searchHintAppStore": {}, + "permissions": "", + "@permissions": {}, + "allPackageTypes": "", + "@allPackageTypes": {}, + "notHelpful": "", + "@notHelpful": {}, + "packagesUsed": "", + "@packagesUsed": {}, + "privacyPolicy": "", + "@privacyPolicy": {}, + "serverAndCloudSlogan": "", + "@serverAndCloudSlogan": {}, + "additionalInformation": "", + "@additionalInformation": {}, + "removePackage": "", + "@removePackage": { + "placeholders": { + "package": { + "type": "String" + } + } + }, + "helpful": "", + "@helpful": {}, + "rate": "", + "@rate": {}, + "noPackageFound": "", + "@noPackageFound": {}, + "manage": "", + "@manage": {}, + "collection": "", + "@collection": {}, + "cancel": "", + "@cancel": {}, + "whatDoYouThink": "", + "@whatDoYouThink": {}, + "findOurRepository": "", + "@findOurRepository": {}, + "installing": "", + "@installing": {}, + "gallery": "", + "@gallery": {}, + "reviewSent": "", + "@reviewSent": {}, + "reportReviewDialogBody": "", + "@reportReviewDialogBody": {}, + "multiUpdateButton": "", + "@multiUpdateButton": {}, + "appFormat": "", + "@appFormat": {}, + "upgrading": "", + "@upgrading": {}, + "summeryHint": "", + "@summeryHint": {}, + "artAndDesignSlogan": "", + "@artAndDesignSlogan": {}, + "booksAndReferenceSlogan": "", + "@booksAndReferenceSlogan": {}, + "developmentSlogan": "", + "@developmentSlogan": {}, + "featuredSlogan": "", + "@featuredSlogan": {}, + "personalisationSlogan": "", + "@personalisationSlogan": {}, + "photoAndVideoSlogan": "", + "@photoAndVideoSlogan": {}, + "productivitySlogan": "", + "@productivitySlogan": {}, + "updateButton": "", + "@updateButton": {}, + "updatesComplete": "", + "@updatesComplete": {}, + "confirm": "", + "@confirm": {}, + "runsInBackground": "", + "@runsInBackground": {}, + "packageDetails": "", + "@packageDetails": {}, + "copyErrorMessage": "", + "@copyErrorMessage": {}, + "madeBy": "", + "@madeBy": {}, + "reviewsAndRatings": "", + "@reviewsAndRatings": {}, + "rating": "", + "@rating": {}, + "yourReview": "", + "@yourReview": {}, + "yourReviewName": "", + "@yourReviewName": {}, + "clickToRate": "", + "@clickToRate": {}, + "unknown": "", + "@unknown": {}, + "dependencies": "", + "@dependencies": {}, + "releasedAt": "", + "@releasedAt": {}, + "removeAll": "", + "@removeAll": {}, + "downloading": "", + "@downloading": {}, + "downloadRemaining": "", + "@downloadRemaining": { + "placeholders": { + "bytes": { + "type": "String" + } + } + }, + "processing": "", + "@processing": {}, + "ready": "", + "@ready": {}, + "removing": "", + "@removing": {}, + "refreshing": "", + "@refreshing": {}, + "attention": "", + "@attention": {}, + "theme": "", + "@theme": {}, + "light": "", + "@light": {}, + "dark": "", + "@dark": {}, + "noSnapsInstalled": "", + "@noSnapsInstalled": {}, + "appstreamSearchGreylist": "", + "@appstreamSearchGreylist": { + "description": "List of 'grey-listed' words separated with ';'. Do not translate this list directly. Instead, provide a list of words in your language that people are likely to include in a search but that should normally be ignored in the search." + }, + "installed": "", + "@installed": {}, + "configure": "", + "@configure": {}, + "sourcesDescription": "", + "@sourcesDescription": {}, + "dependenciesQuestion": "", + "@dependenciesQuestion": {}, + "dependenciesFullList": "", + "@dependenciesFullList": {}, + "dependenciesInstallListing": "", + "@dependenciesInstallListing": { + "placeholders": { + "length": { + "type": "int" + }, + "size": { + "type": "String" + }, + "packageName": { + "type": "String" + } + } + }, + "dependenciesRemoveListing": "", + "@dependenciesRemoveListing": { + "placeholders": { + "length": { + "type": "int" + }, + "size": { + "type": "String" + }, + "packageName": { + "type": "String" + } + } + }, + "dependenciesAutoremove": "", + "@dependenciesAutoremove": {}, + "links": "", + "@links": {}, + "starDeveloper": "", + "@starDeveloper": {}, + "copiedToClipboard": "", + "@copiedToClipboard": {}, + "reportAbuse": "", + "@reportAbuse": {}, + "snapPackage": "", + "@snapPackage": {}, + "devicesAndIotSlogan": "", + "@devicesAndIotSlogan": {}, + "educationSlogan": "", + "@educationSlogan": {}, + "financeSlogan": "", + "@financeSlogan": {}, + "gamesSlogan": "", + "@gamesSlogan": {}, + "musicAndAudioSlogan": "", + "@musicAndAudioSlogan": {}, + "scienceSlogan": "", + "@scienceSlogan": {}, + "securitySlogan": "", + "@securitySlogan": {}, + "utilitiesSlogan": "", + "@utilitiesSlogan": {}, + "quit": "", + "@quit": {}, + "quitDanger": "", + "@quitDanger": {}, + "yourReviewTitle": "", + "@yourReviewTitle": {}, + "changingPermissions": "", + "@changingPermissions": {}, + "system": "", + "@system": {}, + "showAllReviews": "", + "@showAllReviews": {}, + "send": "", + "@send": {}, + "socialSlogan": "", + "@socialSlogan": {} +} diff --git a/lib/l10n/app_fi.arb b/lib/l10n/app_fi.arb index cd3ae48c6..d4c9af122 100644 --- a/lib/l10n/app_fi.arb +++ b/lib/l10n/app_fi.arb @@ -49,7 +49,7 @@ "@social": {}, "utilities": "Hyötyohjelmat", "@utilities": {}, - "all": "Kaikki", + "all": "Kaikki snap-luokat", "@all": {}, "installDate": "Asennuksen päivämäärä", "@installDate": {}, @@ -161,7 +161,7 @@ "@findOurRepository": {}, "changelogTooLong": "Lukeaksesi täydelliset muutoskirjaukset, suuntaa tänne:", "@changelogTooLong": {}, - "checkingForUpdates": "Hetkinen vain - tarkistamme päivitysten saatavuuden", + "checkingForUpdates": "Tarkistetaan päivitysten saatavuutta", "@checkingForUpdates": {}, "readyToUpdate": "Valmiina päivittämään", "@readyToUpdate": {}, @@ -198,5 +198,233 @@ "multiAppFormatsFound": "Löysimme tälle sovellukselle useita eri tiedostomuotoja.", "@multiAppFormatsFound": {}, "attention": "Huomio!", - "@attention": {} + "@attention": {}, + "dark": "Tumma", + "@dark": {}, + "configure": "Määritä", + "@configure": {}, + "links": "Linkit", + "@links": {}, + "clickToRate": "", + "@clickToRate": {}, + "showAllReviews": "", + "@showAllReviews": {}, + "scienceSlogan": "Tiedetyökalut", + "@scienceSlogan": {}, + "yourReviewName": "Nimesi (valinnainen)", + "@yourReviewName": {}, + "rating": "", + "@rating": {}, + "socialSlogan": "", + "@socialSlogan": {}, + "copiedToClipboard": "Kopioitu leikepöydälle", + "@copiedToClipboard": {}, + "reportReviewDialogBody": "", + "@reportReviewDialogBody": {}, + "reportReviewDialogTitle": "Ilmoita arvio", + "@reportReviewDialogTitle": {}, + "report": "Ilmoita", + "@report": {}, + "updateButton": "Päivitä", + "@updateButton": {}, + "reportAbuse": "Ilmoita väärinkäytöksestä", + "@reportAbuse": {}, + "light": "Vaalea", + "@light": {}, + "confirmRemove": "Haluatko varmasti poistaa tämän paketin?", + "@confirmRemove": {}, + "additionalInformation": "Lisätiedot", + "@additionalInformation": {}, + "searchHintInstalled": "Etsi asennettuja sovelluksia", + "@searchHintInstalled": {}, + "share": "Jaa", + "@share": {}, + "theme": "Teema", + "@theme": {}, + "packageType": "Pakettityyppi", + "@packageType": {}, + "contributors": "Avustajat", + "@contributors": {}, + "allSelected": "Kaikki päivitykset valittu", + "@allSelected": {}, + "ratings": "", + "@ratings": {}, + "packagesUsed": "Käytetyt paketit", + "@packagesUsed": {}, + "searchHintAppStore": "Etsi sovelluksia", + "@searchHintAppStore": {}, + "ratingsAndReviews": "Arviot ja arvostelut", + "@ratingsAndReviews": {}, + "xSelected": "päivitystä valittu", + "@xSelected": {}, + "starDeveloper": "", + "@starDeveloper": {}, + "justAMoment": "Pieni hetki!", + "@justAMoment": {}, + "refreshButton": "Tarkista päivitykset", + "@refreshButton": {}, + "summary": "Yhteenveto", + "@summary": {}, + "removePackage": "Poista {package}", + "@removePackage": { + "placeholders": { + "package": { + "type": "String" + } + } + }, + "securitySlogan": "Suojaa tietojasi", + "@securitySlogan": {}, + "utilitiesSlogan": "Apuohjelmat", + "@utilitiesSlogan": {}, + "serverAndCloudSlogan": "Palvelin ja pilvi", + "@serverAndCloudSlogan": {}, + "dependencies": "Riippuvuudet", + "@dependencies": {}, + "updatesAvailable": "päivitystä saatavilla", + "@updatesAvailable": {}, + "educationSlogan": "Kotiopetuksen työkaluja", + "@educationSlogan": {}, + "multiUpdateButton": "Päivitä kaikki", + "@multiUpdateButton": {}, + "allPackageTypes": "Kaikki pakettityypit", + "@allPackageTypes": {}, + "releasedAt": "Julkaistu", + "@releasedAt": {}, + "removeAll": "Poista kaikki", + "@removeAll": {}, + "weHaveUpdates": "Päivityksiä sinulle!", + "@weHaveUpdates": {}, + "publisher": "Julkaisija", + "@publisher": {}, + "writeAreview": "", + "@writeAreview": {}, + "yourReviewTitle": "", + "@yourReviewTitle": {}, + "rate": "", + "@rate": {}, + "whatDoYouThink": "", + "@whatDoYouThink": {}, + "summeryHint": "", + "@summeryHint": {}, + "submit": "Lähetä", + "@submit": {}, + "helpful": "Hyödyllinen", + "@helpful": {}, + "notHelpful": "Ei hyödyllinen", + "@notHelpful": {}, + "whatDataIsSend": "", + "@whatDataIsSend": {}, + "privacyPolicy": "", + "@privacyPolicy": {}, + "changingPermissions": "", + "@changingPermissions": {}, + "permissions": "Oikeudet", + "@permissions": {}, + "downloading": "Ladataan", + "@downloading": {}, + "downloadRemaining": "Ladataan... {bytes} jäljellä", + "@downloadRemaining": { + "placeholders": { + "bytes": { + "type": "String" + } + } + }, + "installing": "Asennetaan", + "@installing": {}, + "ready": "Valmis", + "@ready": {}, + "removing": "Poistetaan", + "@removing": {}, + "refreshing": "", + "@refreshing": {}, + "upgrading": "", + "@upgrading": {}, + "system": "Järjestelmä", + "@system": {}, + "appstreamSearchGreylist": "app;application;package;program;programme;suite;tool;sovellus;ohjelma;paketti;työkalu;äppi", + "@appstreamSearchGreylist": { + "description": "List of 'grey-listed' words separated with ';'. Do not translate this list directly. Instead, provide a list of words in your language that people are likely to include in a search but that should normally be ignored in the search." + }, + "sourcesDescription": "Määritä mistä järjestelmän ja kolmansien osapuolten Debian-paketit päivitetään.", + "@sourcesDescription": {}, + "dependenciesQuestion": "Haluatko varmasti jatkaa?", + "@dependenciesQuestion": {}, + "dependenciesFullList": "Katso täysi riippuvuusluettelo", + "@dependenciesFullList": {}, + "dependenciesInstallListing": "{length} riippuvuutta, joiden koko on {size}, ladataan kun {packageName} asennetaan", + "@dependenciesInstallListing": { + "placeholders": { + "length": { + "type": "int" + }, + "size": { + "type": "String" + }, + "packageName": { + "type": "String" + } + } + }, + "gallery": "Galleria", + "@gallery": {}, + "dependenciesRemoveListing": "{length} riippuvuutta, joiden koko on yhteensä {size}, poistetaan automaattisesti kun {packageName} poistetaan", + "@dependenciesRemoveListing": { + "placeholders": { + "length": { + "type": "int" + }, + "size": { + "type": "String" + }, + "packageName": { + "type": "String" + } + } + }, + "dependenciesAutoremove": "Poista tarpeettomat riippuvuudet", + "@dependenciesAutoremove": {}, + "collection": "Kokoelma", + "@collection": {}, + "manage": "Hallitse", + "@manage": {}, + "snapPackage": "Snap", + "@snapPackage": {}, + "debianPackage": "Debian", + "@debianPackage": {}, + "booksAndReferenceSlogan": "Järjestä kirjakokoelmasi", + "@booksAndReferenceSlogan": {}, + "developmentSlogan": "Sovelluksia kehittäjille", + "@developmentSlogan": {}, + "entertainmentSlogan": "Kotiviihteen työkaluja", + "@entertainmentSlogan": {}, + "featuredSlogan": "", + "@featuredSlogan": {}, + "financeSlogan": "", + "@financeSlogan": {}, + "gamesSlogan": "Pelit ja pelaaminen", + "@gamesSlogan": {}, + "healthAndFitnessSlogan": "Terveys ja kuntoilu", + "@healthAndFitnessSlogan": {}, + "musicAndAudioSlogan": "Musiikki ja ääni", + "@musicAndAudioSlogan": {}, + "newsAndWeatherSlogan": "Uutiset ja sää", + "@newsAndWeatherSlogan": {}, + "personalisationSlogan": "Mukauttaminen", + "@personalisationSlogan": {}, + "photoAndVideoSlogan": "Kuvat ja video", + "@photoAndVideoSlogan": {}, + "productivitySlogan": "Ole tuottelias!", + "@productivitySlogan": {}, + "processing": "Käsitellään", + "@processing": {}, + "installed": "Asennettu", + "@installed": {}, + "artAndDesignSlogan": "Työkaluja taiteilijoille", + "@artAndDesignSlogan": {}, + "devicesAndIotSlogan": "Laitteet ja IOT", + "@devicesAndIotSlogan": {}, + "noSnapsInstalled": "Järjestelmään ei ole asennettu Snap-sovelluksia", + "@noSnapsInstalled": {} } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 638af277a..7f899c7c9 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -239,15 +239,15 @@ "@socialSlogan": {}, "allPackageTypes": "Tous types de paquets", "@allPackageTypes": {}, - "yourReviewTitle": "Titre de l'avis (optionnel)", + "yourReviewTitle": "Titre de l’avis", "@yourReviewTitle": {}, - "yourReviewName": "Votre nom (optionnel)", + "yourReviewName": "Votre nom", "@yourReviewName": {}, "clickToRate": "Cliquer pour noter", "@clickToRate": {}, "showAllReviews": "Montrer tous les avis", "@showAllReviews": {}, - "installing": "En cours d'installation", + "installing": "En cours d’installation", "@installing": {}, "removing": "En cours de suppression", "@removing": {}, @@ -265,7 +265,7 @@ "@appstreamSearchGreylist": { "description": "List of 'grey-listed' words separated with ';'. Do not translate this list directly. Instead, provide a list of words in your language that people are likely to include in a search but that should normally be ignored in the search." }, - "educationSlogan": "Utilitaires pour l'enseignement", + "educationSlogan": "Utilitaires pour l’enseignement", "@educationSlogan": {}, "photoAndVideoSlogan": "Photo et vidéo", "@photoAndVideoSlogan": {}, @@ -297,24 +297,13 @@ "@debianPackage": {}, "dependencies": "Dépendances", "@dependencies": {}, - "dependenciesQuestion": "Êtes-vous sûr de vouloir continuer ?", + "dependenciesQuestion": "Êtes-vous sûr de vouloir continuer ?", "@dependenciesQuestion": {}, "dependenciesFullList": "Voir la liste complète des dépendances", "@dependenciesFullList": {}, "gallery": "Galerie", "@gallery": {}, - "dependenciesListing": "{length} dépendances seront téléchargées lors de l'installation de {packageName}", - "@dependenciesListing": { - "placeholders": { - "length": { - "type": "int" - }, - "packageName": { - "type": "String" - } - } - }, - "noSnapsInstalled": "Aucune application Snap n'est installée sur votre système", + "noSnapsInstalled": "Aucune application Snap n’est installée sur votre système", "@noSnapsInstalled": {}, "rating": "Note", "@rating": {}, @@ -327,5 +316,115 @@ "links": "Liens", "@links": {}, "additionalInformation": "Informations complémentaires", - "@additionalInformation": {} + "@additionalInformation": {}, + "searchHintAppStore": "Rechercher des applications", + "@searchHintAppStore": {}, + "searchHintInstalled": "Rechercher vos applications installées", + "@searchHintInstalled": {}, + "multiUpdateButton": "Tout mettre à jour", + "@multiUpdateButton": {}, + "packagesUsed": "Paquets utilisés", + "@packagesUsed": {}, + "downloadRemaining": "En course de téléchargement… {bytes} restant", + "@downloadRemaining": { + "placeholders": { + "bytes": { + "type": "String" + } + } + }, + "collection": "Collection", + "@collection": {}, + "ready": "Prêt", + "@ready": {}, + "upgrading": "En cours de mise à jour", + "@upgrading": {}, + "contributors": "Contributeurs", + "@contributors": {}, + "downloading": "En cours de téléchargement", + "@downloading": {}, + "report": "Signaler", + "@report": {}, + "manage": "Gérer", + "@manage": {}, + "reportReviewDialogBody": "Vous pouvez signaler un avis pour comportement abusif, impoli ou discriminatoire. Une fois signalé, un avis sera masqué jusqu’à ce qu’il ait été vérifié par un administrateur.", + "@reportReviewDialogBody": {}, + "whatDataIsSend": "Découvrez quelles données sont envoyées dans notre ", + "@whatDataIsSend": {}, + "summeryHint": "Donnez un bref résumé de votre avis, par exemple : Super application, je la recommande.", + "@summeryHint": {}, + "dependenciesRemoveListing": "{length} dépendances d'une taille totale de {size} peuvent être automatiquement supprimées lors de la suppression de {packageName}", + "@dependenciesRemoveListing": { + "placeholders": { + "length": { + "type": "int" + }, + "size": { + "type": "String" + }, + "packageName": { + "type": "String" + } + } + }, + "removeAll": "Supprimer tout", + "@removeAll": {}, + "removePackage": "Supprimer {package}", + "@removePackage": { + "placeholders": { + "package": { + "type": "String" + } + } + }, + "confirmRemove": "Voulez-vous vraiment supprimer ce package ?", + "@confirmRemove": {}, + "ratingsAndReviews": "Notes et avis", + "@ratingsAndReviews": {}, + "ratings": "Notes", + "@ratings": {}, + "summary": "Résumé", + "@summary": {}, + "packageType": "Type de paquet", + "@packageType": {}, + "rate": "Noter", + "@rate": {}, + "writeAreview": "Écrire un avis", + "@writeAreview": {}, + "whatDoYouThink": "Que pensez-vous de l’application ? Essayez de justifier votre opinion.", + "@whatDoYouThink": {}, + "submit": "Soumettre", + "@submit": {}, + "helpful": "Utile", + "@helpful": {}, + "notHelpful": "Inutile", + "@notHelpful": {}, + "copiedToClipboard": "Copié dans le presse-papiers", + "@copiedToClipboard": {}, + "share": "Partager", + "@share": {}, + "reportAbuse": "Signaler un abus", + "@reportAbuse": {}, + "reportReviewDialogTitle": "Signaler un avis", + "@reportReviewDialogTitle": {}, + "privacyPolicy": "politique de confidentialité.", + "@privacyPolicy": {}, + "starDeveloper": "Développeur vedette", + "@starDeveloper": {}, + "dependenciesAutoremove": "Supprimer les dépendances qui ne sont plus nécessaires", + "@dependenciesAutoremove": {}, + "dependenciesInstallListing": "{length} dépendances d'une taille totale de {size} seront téléchargées lors de l’installation de {packageName}", + "@dependenciesInstallListing": { + "placeholders": { + "length": { + "type": "int" + }, + "size": { + "type": "String" + }, + "packageName": { + "type": "String" + } + } + } } diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index 49f4af1b2..109faec33 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -1,74 +1,430 @@ { - "appTitle": "Perangkat Lunak Ubuntu", - "explorePageTitle": "Jelajahi", - "myAppsPageTitle": "Aplikasi Saya", - "updatesPageTitle": "Pembaruan", - "settingsPageTitle": "Pengaturan", - "artAndDesign": "Desain dan Seni", - "booksAndReference": "Buku dan Referensi", - "development": "Pengembangan", - "devicesAndIot": "Gawai and IOT", - "education": "Pendidikan", - "entertainment": "Hiburan", - "featured": "Unggulan", - "finance": "Keuangan", - "games": "Permainan", - "healthAndFitness": "Fitnes dan Kesehatan", - "musicAndAudio": "Audio dan Musik", - "newsAndWeather": "Cuaca dan Berita", - "personalisation": "Personalisasi", - "photoAndVideo": "Vidio dan Foto", - "productivity": "Produktivitas", - "science": "Sains", - "security": "Keamanan", - "serverAndCloud": "Cloud dan Server", - "social": "Sosial", - "utilities": "Utilitas", - "all": "Semua", - "installDate": "Tanggal Terpasang", - "lastUpdated": "Pembaruan Terakhir", - "notInstalled": "Belum Terpasang", - "confinement": "Aturan Pakai", - "license": "Lisensi", - "version": "Versi", - "channel": "Kanal", - "install": "Pasangkan", - "refresh": "Perbarui", - "remove": "Hapus", - "website": "Situs web", - "description": "Deskripsi", - "open": "Buka", - "contact": "Kontak", - "about": "Tentang", - "showMore": "Tampilkan lebih banyak", - "showLess": "Kurangi yang di tampilkan", - "offline": "Terputus", - "snapPackages": "Paket Snap", - "debianPackages": "Paket Debian", - "connections": "Koneksi", - "updateAvailable": "Terdapat Pembaruan", - "size": "Ukuran", - "name": "Nama", - "sortBy": "Urut Berdasarkan", - "media": "Media", - "done": "Selesai", - "updates": "Pembaruan", - "searchHint": "Cari", - "updateSelected": "Perbaruan terpilih", - "selectAll": "Pilih semua", - "deselectAll": "Batalkan semua pilihan", - "update": "Perbarui", - "noUpdates": "Semua terlihat baik, tidak terdapat pembaruan", - "apps": "aplikasi", - "filterSnaps": "Atur filter snap", - "enterRepoName": "Ketik nama repositori", - "requireRestartSystem": "Muat ulang sistem untuk menyelesaikan pembaruan", - "requireRestartSession": "Logout untuk menyelesaikan pembaruan", - "requireRestartApp": "Buka ulang app untuk menyelesaikan pembaruan", - "issued": "Telah di Terbitkan", - "changelog": "Daftar Perubahan", - "architecture": "Arsitektur", - "source": "Sumber", - "sources": "Beberapa Sumber", - "packageInstaller": "Pemasangan Paket" + "appTitle": "Perangkat Lunak Ubuntu", + "@appTitle": {}, + "explorePageTitle": "Jelajahi", + "@explorePageTitle": {}, + "myAppsPageTitle": "Aplikasi Saya", + "@myAppsPageTitle": {}, + "updatesPageTitle": "Pembaruan", + "@updatesPageTitle": {}, + "settingsPageTitle": "Pengaturan", + "@settingsPageTitle": {}, + "artAndDesign": "Desain dan Seni", + "@artAndDesign": {}, + "booksAndReference": "Buku dan Referensi", + "@booksAndReference": {}, + "development": "Pengembangan", + "@development": {}, + "devicesAndIot": "Gawai and IOT", + "@devicesAndIot": {}, + "education": "Pendidikan", + "@education": {}, + "entertainment": "Hiburan", + "@entertainment": {}, + "featured": "Unggulan", + "@featured": {}, + "finance": "Keuangan", + "@finance": {}, + "games": "Permainan", + "@games": {}, + "healthAndFitness": "Fitnes dan Kesehatan", + "@healthAndFitness": {}, + "musicAndAudio": "Audio dan Musik", + "@musicAndAudio": {}, + "newsAndWeather": "Cuaca dan Berita", + "@newsAndWeather": {}, + "personalisation": "Personalisasi", + "@personalisation": {}, + "photoAndVideo": "Vidio dan Foto", + "@photoAndVideo": {}, + "productivity": "Produktivitas", + "@productivity": {}, + "science": "Sains", + "@science": {}, + "security": "Keamanan", + "@security": {}, + "serverAndCloud": "Cloud dan Server", + "@serverAndCloud": {}, + "social": "Sosial", + "@social": {}, + "utilities": "Utilitas", + "@utilities": {}, + "all": "Semua", + "@all": {}, + "installDate": "Tanggal Terpasang", + "@installDate": {}, + "lastUpdated": "Pembaruan Terakhir", + "@lastUpdated": {}, + "notInstalled": "Belum Terpasang", + "@notInstalled": {}, + "confinement": "Aturan Pakai", + "@confinement": {}, + "license": "Lisensi", + "@license": {}, + "version": "Versi", + "@version": {}, + "channel": "Kanal", + "@channel": {}, + "install": "Pasangkan", + "@install": {}, + "refresh": "Perbarui", + "@refresh": {}, + "remove": "Hapus", + "@remove": {}, + "website": "Situs web", + "@website": {}, + "description": "Deskripsi", + "@description": {}, + "open": "Buka", + "@open": {}, + "contact": "Kontak", + "@contact": {}, + "about": "Tentang", + "@about": {}, + "showMore": "Tampilkan lebih banyak", + "@showMore": {}, + "showLess": "Kurangi yang di tampilkan", + "@showLess": {}, + "offline": "Terputus", + "@offline": {}, + "snapPackages": "Paket Snap", + "@snapPackages": {}, + "debianPackages": "Paket Debian", + "@debianPackages": {}, + "connections": "Koneksi", + "@connections": {}, + "updateAvailable": "Terdapat Pembaruan", + "@updateAvailable": {}, + "size": "Ukuran", + "@size": {}, + "name": "Nama", + "@name": {}, + "sortBy": "Urut Berdasarkan", + "@sortBy": {}, + "media": "Media", + "@media": {}, + "done": "Selesai", + "@done": {}, + "updates": "Pembaruan", + "@updates": {}, + "searchHint": "Cari", + "@searchHint": {}, + "updateSelected": "Perbaruan terpilih", + "@updateSelected": {}, + "selectAll": "Pilih semua", + "@selectAll": {}, + "deselectAll": "Batalkan semua pilihan", + "@deselectAll": {}, + "update": "Perbarui", + "@update": {}, + "noUpdates": "Semua terlihat baik, tidak terdapat pembaruan", + "@noUpdates": {}, + "apps": "aplikasi", + "@apps": {}, + "filterSnaps": "Atur filter snap", + "@filterSnaps": {}, + "enterRepoName": "Ketik nama repositori", + "@enterRepoName": {}, + "requireRestartSystem": "Muat ulang sistem untuk menyelesaikan pembaruan", + "@requireRestartSystem": {}, + "requireRestartSession": "Logout untuk menyelesaikan pembaruan", + "@requireRestartSession": {}, + "requireRestartApp": "Buka ulang app untuk menyelesaikan pembaruan", + "@requireRestartApp": {}, + "issued": "Telah di Terbitkan", + "@issued": {}, + "changelog": "Daftar Perubahan", + "@changelog": {}, + "architecture": "Arsitektur", + "@architecture": {}, + "source": "Sumber", + "@source": {}, + "sources": "Beberapa Sumber", + "@sources": {}, + "packageInstaller": "Pemasangan Paket", + "@packageInstaller": {}, + "allPackageTypes": "", + "@allPackageTypes": {}, + "searchHintAppStore": "", + "@searchHintAppStore": {}, + "allSelected": "", + "@allSelected": {}, + "send": "", + "@send": {}, + "cancel": "", + "@cancel": {}, + "madeBy": "", + "@madeBy": {}, + "dark": "", + "@dark": {}, + "reviewsAndRatings": "", + "@reviewsAndRatings": {}, + "reportReviewDialogTitle": "", + "@reportReviewDialogTitle": {}, + "removing": "", + "@removing": {}, + "dependencies": "", + "@dependencies": {}, + "unknown": "", + "@unknown": {}, + "searchHintInstalled": "", + "@searchHintInstalled": {}, + "ratingsAndReviews": "", + "@ratingsAndReviews": {}, + "light": "", + "@light": {}, + "refreshing": "", + "@refreshing": {}, + "installing": "", + "@installing": {}, + "summeryHint": "", + "@summeryHint": {}, + "report": "", + "@report": {}, + "appstreamSearchGreylist": "", + "@appstreamSearchGreylist": { + "description": "List of 'grey-listed' words separated with ';'. Do not translate this list directly. Instead, provide a list of words in your language that people are likely to include in a search but that should normally be ignored in the search." + }, + "noSnapFound": "", + "@noSnapFound": {}, + "reportAbuse": "", + "@reportAbuse": {}, + "showAllReviews": "", + "@showAllReviews": {}, + "collection": "", + "@collection": {}, + "reportReviewDialogBody": "", + "@reportReviewDialogBody": {}, + "confirmRemove": "", + "@confirmRemove": {}, + "photoAndVideoSlogan": "", + "@photoAndVideoSlogan": {}, + "removePackage": "", + "@removePackage": { + "placeholders": { + "package": { + "type": "String" + } + } + }, + "rating": "", + "@rating": {}, + "helpful": "", + "@helpful": {}, + "gamesSlogan": "", + "@gamesSlogan": {}, + "manage": "", + "@manage": {}, + "contributors": "", + "@contributors": {}, + "serverAndCloudSlogan": "", + "@serverAndCloudSlogan": {}, + "installed": "", + "@installed": {}, + "theme": "", + "@theme": {}, + "share": "", + "@share": {}, + "releasedAt": "", + "@releasedAt": {}, + "yourReviewTitle": "", + "@yourReviewTitle": {}, + "whatDataIsSend": "", + "@whatDataIsSend": {}, + "socialSlogan": "", + "@socialSlogan": {}, + "dependenciesQuestion": "", + "@dependenciesQuestion": {}, + "submit": "", + "@submit": {}, + "updatesComplete": "", + "@updatesComplete": {}, + "noSnapsInstalled": "", + "@noSnapsInstalled": {}, + "dependenciesInstallListing": "", + "@dependenciesInstallListing": { + "placeholders": { + "length": { + "type": "int" + }, + "size": { + "type": "String" + }, + "packageName": { + "type": "String" + } + } + }, + "privacyPolicy": "", + "@privacyPolicy": {}, + "downloading": "", + "@downloading": {}, + "clickToRate": "", + "@clickToRate": {}, + "writeAreview": "", + "@writeAreview": {}, + "attention": "", + "@attention": {}, + "findOurRepository": "", + "@findOurRepository": {}, + "appFormat": "", + "@appFormat": {}, + "notHelpful": "", + "@notHelpful": {}, + "summary": "", + "@summary": {}, + "downloadRemaining": "", + "@downloadRemaining": { + "placeholders": { + "bytes": { + "type": "String" + } + } + }, + "sourcesDescription": "", + "@sourcesDescription": {}, + "packageType": "", + "@packageType": {}, + "processing": "", + "@processing": {}, + "system": "", + "@system": {}, + "copyErrorMessage": "", + "@copyErrorMessage": {}, + "yourReviewName": "", + "@yourReviewName": {}, + "devicesAndIotSlogan": "", + "@devicesAndIotSlogan": {}, + "whatDoYouThink": "", + "@whatDoYouThink": {}, + "packageKitGroup": "", + "@packageKitGroup": {}, + "configure": "", + "@configure": {}, + "ratings": "", + "@ratings": {}, + "utilitiesSlogan": "", + "@utilitiesSlogan": {}, + "copiedToClipboard": "", + "@copiedToClipboard": {}, + "debianPackage": "", + "@debianPackage": {}, + "noPackageFound": "", + "@noPackageFound": {}, + "quitDanger": "", + "@quitDanger": {}, + "ready": "", + "@ready": {}, + "artAndDesignSlogan": "", + "@artAndDesignSlogan": {}, + "booksAndReferenceSlogan": "", + "@booksAndReferenceSlogan": {}, + "educationSlogan": "", + "@educationSlogan": {}, + "entertainmentSlogan": "", + "@entertainmentSlogan": {}, + "healthAndFitnessSlogan": "", + "@healthAndFitnessSlogan": {}, + "newsAndWeatherSlogan": "", + "@newsAndWeatherSlogan": {}, + "personalisationSlogan": "", + "@personalisationSlogan": {}, + "productivitySlogan": "", + "@productivitySlogan": {}, + "scienceSlogan": "", + "@scienceSlogan": {}, + "updateButton": "", + "@updateButton": {}, + "refreshButton": "", + "@refreshButton": {}, + "multiUpdateButton": "", + "@multiUpdateButton": {}, + "updating": "", + "@updating": {}, + "checkingForUpdates": "", + "@checkingForUpdates": {}, + "readyToUpdate": "", + "@readyToUpdate": {}, + "confirm": "", + "@confirm": {}, + "runsInBackground": "", + "@runsInBackground": {}, + "quit": "", + "@quit": {}, + "packageKitFilter": "", + "@packageKitFilter": {}, + "packageDetails": "", + "@packageDetails": {}, + "reviewSent": "", + "@reviewSent": {}, + "multiAppFormatsFound": "", + "@multiAppFormatsFound": {}, + "changingPermissions": "", + "@changingPermissions": {}, + "permissions": "", + "@permissions": {}, + "removeAll": "", + "@removeAll": {}, + "xSelected": "", + "@xSelected": {}, + "classic": "", + "@classic": {}, + "verified": "", + "@verified": {}, + "publisher": "", + "@publisher": {}, + "changelogTooLong": "", + "@changelogTooLong": {}, + "yourReview": "", + "@yourReview": {}, + "rate": "", + "@rate": {}, + "upgrading": "", + "@upgrading": {}, + "dependenciesFullList": "", + "@dependenciesFullList": {}, + "dependenciesRemoveListing": "", + "@dependenciesRemoveListing": { + "placeholders": { + "length": { + "type": "int" + }, + "size": { + "type": "String" + }, + "packageName": { + "type": "String" + } + } + }, + "dependenciesAutoremove": "", + "@dependenciesAutoremove": {}, + "gallery": "", + "@gallery": {}, + "starDeveloper": "", + "@starDeveloper": {}, + "packagesUsed": "", + "@packagesUsed": {}, + "snapPackage": "", + "@snapPackage": {}, + "updatesAvailable": "", + "@updatesAvailable": {}, + "developmentSlogan": "", + "@developmentSlogan": {}, + "financeSlogan": "", + "@financeSlogan": {}, + "musicAndAudioSlogan": "", + "@musicAndAudioSlogan": {}, + "featuredSlogan": "", + "@featuredSlogan": {}, + "justAMoment": "", + "@justAMoment": {}, + "securitySlogan": "", + "@securitySlogan": {}, + "weHaveUpdates": "", + "@weHaveUpdates": {}, + "additionalInformation": "", + "@additionalInformation": {}, + "links": "", + "@links": {} } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 1b1d12d5d..e859f3ef9 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -237,9 +237,9 @@ "@utilitiesSlogan": {}, "allPackageTypes": "Tutti i tipi di pacchetti", "@allPackageTypes": {}, - "yourReviewTitle": "Titolo recensione (facoltativo)", + "yourReviewTitle": "Titolo recensione", "@yourReviewTitle": {}, - "yourReviewName": "Il tuo nome (facoltativo)", + "yourReviewName": "Il tuo nome", "@yourReviewName": {}, "clickToRate": "Fai clic per valutare", "@clickToRate": {}, @@ -311,21 +311,120 @@ "@dependenciesQuestion": {}, "dependenciesFullList": "Visualizza l'elenco completo delle dipendenze", "@dependenciesFullList": {}, - "dependenciesListing": "{length} le dipendenze verranno scaricate durante l'installazione {packageName}", - "@dependenciesListing": { + "gallery": "Galleria", + "@gallery": {}, + "additionalInformation": "Informazioni aggiuntive", + "@additionalInformation": {}, + "links": "Link", + "@links": {}, + "searchHintAppStore": "Cerca app", + "@searchHintAppStore": {}, + "copiedToClipboard": "Copiato negli appunti", + "@copiedToClipboard": {}, + "confirmRemove": "Sei sicuro di voler rimuovere questo pacchetto?", + "@confirmRemove": {}, + "share": "Condividi", + "@share": {}, + "whatDoYouThink": "Cosa ne pensi dell'app? Prova a dare una ragione per la vederla.", + "@whatDoYouThink": {}, + "dependenciesRemoveListing": "Le dipendenze {length} con una dimensione totale di {size} possono essere rimosse automaticamente durante la rimozione di {packageName}", + "@dependenciesRemoveListing": { "placeholders": { "length": { "type": "int" }, + "size": { + "type": "String" + }, "packageName": { "type": "String" } } }, - "gallery": "Galleria", - "@gallery": {}, - "additionalInformation": "Informazioni aggiuntive", - "@additionalInformation": {}, - "links": "Link", - "@links": {} + "multiUpdateButton": "Aggiorna tutto", + "@multiUpdateButton": {}, + "removeAll": "Rimuovi tutto", + "@removeAll": {}, + "removePackage": "Rimuovere {package}", + "@removePackage": { + "placeholders": { + "package": { + "type": "String" + } + } + }, + "searchHintInstalled": "Cerca app installate", + "@searchHintInstalled": {}, + "packageType": "Tipo di pacchetto", + "@packageType": {}, + "ratingsAndReviews": "Valutazioni e recensioni", + "@ratingsAndReviews": {}, + "ratings": "Valutazioni", + "@ratings": {}, + "writeAreview": "Scrivi una recensione", + "@writeAreview": {}, + "summary": "Riepilogo", + "@summary": {}, + "rate": "Voto", + "@rate": {}, + "summeryHint": "Fornisci un breve riassunto della tua recensione, ad esempio: Ottima app, la consiglierei.", + "@summeryHint": {}, + "dependenciesAutoremove": "Rimuovi le dipendenze non più necessarie", + "@dependenciesAutoremove": {}, + "packagesUsed": "Pacchetti utilizzati", + "@packagesUsed": {}, + "contributors": "Contributori", + "@contributors": {}, + "submit": "Invia", + "@submit": {}, + "helpful": "Utile", + "@helpful": {}, + "notHelpful": "Non utile", + "@notHelpful": {}, + "whatDataIsSend": "Scopri quali dati vengono inviati nel nostro ", + "@whatDataIsSend": {}, + "privacyPolicy": "informativa sulla privacy.", + "@privacyPolicy": {}, + "downloading": "Scaricamento in corso", + "@downloading": {}, + "downloadRemaining": "Download in corso... {bytes} rimanenti", + "@downloadRemaining": { + "placeholders": { + "bytes": { + "type": "String" + } + } + }, + "ready": "Pronto", + "@ready": {}, + "upgrading": "Aggiornamento in corso", + "@upgrading": {}, + "dependenciesInstallListing": "Le dipendenze {length} con una dimensione totale di {size} verranno scaricate durante l'installazione di {packageName}", + "@dependenciesInstallListing": { + "placeholders": { + "length": { + "type": "int" + }, + "size": { + "type": "String" + }, + "packageName": { + "type": "String" + } + } + }, + "collection": "Collezione", + "@collection": {}, + "manage": "Gestisci", + "@manage": {}, + "starDeveloper": "Stelle sviluppatore", + "@starDeveloper": {}, + "report": "Report", + "@report": {}, + "reportAbuse": "Segnala un abuso", + "@reportAbuse": {}, + "reportReviewDialogTitle": "Segnala revisione", + "@reportReviewDialogTitle": {}, + "reportReviewDialogBody": "Puoi segnalare una recensione per comportamento offensivo, maleducato o discriminatorio. Una volta segnalata, una recensione verrà nascosta fino a quando non sarà verificata da un amministratore.", + "@reportReviewDialogBody": {} } diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index c8cc94bbb..8193cc445 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -219,7 +219,7 @@ "@reviewSent": {}, "changingPermissions": "権限を変更中", "@changingPermissions": {}, - "yourReviewName": "お名前(任意)", + "yourReviewName": "お名前", "@yourReviewName": {}, "scienceSlogan": "科学ツール", "@scienceSlogan": {}, @@ -257,7 +257,7 @@ "@findOurRepository": {}, "madeBy": "Ubuntu Softwareは、以下の人々によって開発・設計されました。", "@madeBy": {}, - "yourReviewTitle": "タイトル(任意)", + "yourReviewTitle": "タイトル", "@yourReviewTitle": {}, "serverAndCloudSlogan": "サーバー・クラウド", "@serverAndCloudSlogan": {}, @@ -299,17 +299,6 @@ "@dependenciesFullList": {}, "noSnapsInstalled": "システムにSnapアプリがインストールされていません", "@noSnapsInstalled": {}, - "dependenciesListing": "{packageName}をインストールすると、{length}個の依存パッケージがダウンロードされます", - "@dependenciesListing": { - "placeholders": { - "length": { - "type": "int" - }, - "packageName": { - "type": "String" - } - } - }, "socialSlogan": "一緒にソーシャルしましょう。", "@socialSlogan": {}, "noPackageFound": "検索クエリーに該当するパッケージがありません", @@ -327,5 +316,115 @@ "links": "リンク", "@links": {}, "additionalInformation": "追加情報", - "@additionalInformation": {} + "@additionalInformation": {}, + "searchHintInstalled": "", + "@searchHintInstalled": {}, + "downloading": "", + "@downloading": {}, + "contributors": "", + "@contributors": {}, + "starDeveloper": "", + "@starDeveloper": {}, + "report": "", + "@report": {}, + "summary": "", + "@summary": {}, + "ratingsAndReviews": "", + "@ratingsAndReviews": {}, + "privacyPolicy": "", + "@privacyPolicy": {}, + "collection": "", + "@collection": {}, + "upgrading": "", + "@upgrading": {}, + "submit": "", + "@submit": {}, + "manage": "", + "@manage": {}, + "share": "", + "@share": {}, + "ready": "", + "@ready": {}, + "packagesUsed": "", + "@packagesUsed": {}, + "packageType": "", + "@packageType": {}, + "writeAreview": "", + "@writeAreview": {}, + "searchHintAppStore": "", + "@searchHintAppStore": {}, + "reportReviewDialogBody": "", + "@reportReviewDialogBody": {}, + "rate": "", + "@rate": {}, + "dependenciesAutoremove": "", + "@dependenciesAutoremove": {}, + "dependenciesRemoveListing": "", + "@dependenciesRemoveListing": { + "placeholders": { + "length": { + "type": "int" + }, + "size": { + "type": "String" + }, + "packageName": { + "type": "String" + } + } + }, + "ratings": "", + "@ratings": {}, + "whatDataIsSend": "", + "@whatDataIsSend": {}, + "whatDoYouThink": "", + "@whatDoYouThink": {}, + "dependenciesInstallListing": "", + "@dependenciesInstallListing": { + "placeholders": { + "length": { + "type": "int" + }, + "size": { + "type": "String" + }, + "packageName": { + "type": "String" + } + } + }, + "downloadRemaining": "", + "@downloadRemaining": { + "placeholders": { + "bytes": { + "type": "String" + } + } + }, + "summeryHint": "", + "@summeryHint": {}, + "notHelpful": "", + "@notHelpful": {}, + "confirmRemove": "", + "@confirmRemove": {}, + "multiUpdateButton": "", + "@multiUpdateButton": {}, + "removeAll": "", + "@removeAll": {}, + "removePackage": "", + "@removePackage": { + "placeholders": { + "package": { + "type": "String" + } + } + }, + "helpful": "", + "@helpful": {}, + "copiedToClipboard": "", + "@copiedToClipboard": {}, + "reportAbuse": "", + "@reportAbuse": {}, + "reportReviewDialogTitle": "", + "@reportReviewDialogTitle": {} } diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index b6401bc10..a3a86f1bc 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -174,5 +174,257 @@ "quitDanger": "시스템 업데이트가 현재 실행 중입니다. 지금 앱을 종료하면 시스템이 손상된 상태로 남을 수 있습니다!", "@quitDanger": {}, "attention": "주목!", - "@attention": {} + "@attention": {}, + "educationSlogan": "학습 도구", + "@educationSlogan": {}, + "entertainmentSlogan": "엔터테인먼트 도구", + "@entertainmentSlogan": {}, + "featuredSlogan": "추천 앱", + "@featuredSlogan": {}, + "searchHintAppStore": "", + "@searchHintAppStore": {}, + "allSelected": "", + "@allSelected": {}, + "xSelected": "", + "@xSelected": {}, + "appFormat": "", + "@appFormat": {}, + "allPackageTypes": "", + "@allPackageTypes": {}, + "packageKitGroup": "", + "@packageKitGroup": {}, + "reviewsAndRatings": "", + "@reviewsAndRatings": {}, + "rating": "", + "@rating": {}, + "configure": "", + "@configure": {}, + "links": "", + "@links": {}, + "dark": "", + "@dark": {}, + "noSnapsInstalled": "", + "@noSnapsInstalled": {}, + "dependencies": "", + "@dependencies": {}, + "notHelpful": "", + "@notHelpful": {}, + "reportReviewDialogBody": "", + "@reportReviewDialogBody": {}, + "dependenciesAutoremove": "", + "@dependenciesAutoremove": {}, + "downloading": "", + "@downloading": {}, + "removePackage": "", + "@removePackage": { + "placeholders": { + "package": { + "type": "String" + } + } + }, + "removeAll": "", + "@removeAll": {}, + "sourcesDescription": "", + "@sourcesDescription": {}, + "refreshing": "", + "@refreshing": {}, + "dependenciesFullList": "", + "@dependenciesFullList": {}, + "whatDoYouThink": "", + "@whatDoYouThink": {}, + "theme": "", + "@theme": {}, + "rate": "", + "@rate": {}, + "gallery": "", + "@gallery": {}, + "downloadRemaining": "", + "@downloadRemaining": { + "placeholders": { + "bytes": { + "type": "String" + } + } + }, + "multiAppFormatsFound": "", + "@multiAppFormatsFound": {}, + "appstreamSearchGreylist": "", + "@appstreamSearchGreylist": { + "description": "List of 'grey-listed' words separated with ';'. Do not translate this list directly. Instead, provide a list of words in your language that people are likely to include in a search but that should normally be ignored in the search." + }, + "upgrading": "", + "@upgrading": {}, + "dependenciesRemoveListing": "", + "@dependenciesRemoveListing": { + "placeholders": { + "length": { + "type": "int" + }, + "size": { + "type": "String" + }, + "packageName": { + "type": "String" + } + } + }, + "manage": "", + "@manage": {}, + "reportReviewDialogTitle": "", + "@reportReviewDialogTitle": {}, + "additionalInformation": "", + "@additionalInformation": {}, + "newsAndWeatherSlogan": "", + "@newsAndWeatherSlogan": {}, + "permissions": "", + "@permissions": {}, + "processing": "", + "@processing": {}, + "packageType": "", + "@packageType": {}, + "gamesSlogan": "", + "@gamesSlogan": {}, + "dependenciesQuestion": "", + "@dependenciesQuestion": {}, + "confirmRemove": "", + "@confirmRemove": {}, + "installed": "", + "@installed": {}, + "removing": "", + "@removing": {}, + "report": "", + "@report": {}, + "ready": "", + "@ready": {}, + "scienceSlogan": "", + "@scienceSlogan": {}, + "packagesUsed": "", + "@packagesUsed": {}, + "socialSlogan": "", + "@socialSlogan": {}, + "packageKitFilter": "", + "@packageKitFilter": {}, + "privacyPolicy": "", + "@privacyPolicy": {}, + "summary": "", + "@summary": {}, + "weHaveUpdates": "", + "@weHaveUpdates": {}, + "copiedToClipboard": "", + "@copiedToClipboard": {}, + "justAMoment": "", + "@justAMoment": {}, + "helpful": "", + "@helpful": {}, + "securitySlogan": "", + "@securitySlogan": {}, + "submit": "", + "@submit": {}, + "searchHintInstalled": "", + "@searchHintInstalled": {}, + "snapPackage": "", + "@snapPackage": {}, + "changingPermissions": "", + "@changingPermissions": {}, + "dependenciesInstallListing": "", + "@dependenciesInstallListing": { + "placeholders": { + "length": { + "type": "int" + }, + "size": { + "type": "String" + }, + "packageName": { + "type": "String" + } + } + }, + "reportAbuse": "", + "@reportAbuse": {}, + "collection": "", + "@collection": {}, + "system": "", + "@system": {}, + "musicAndAudioSlogan": "", + "@musicAndAudioSlogan": {}, + "copyErrorMessage": "", + "@copyErrorMessage": {}, + "summeryHint": "", + "@summeryHint": {}, + "light": "", + "@light": {}, + "contributors": "", + "@contributors": {}, + "share": "", + "@share": {}, + "releasedAt": "", + "@releasedAt": {}, + "artAndDesignSlogan": "", + "@artAndDesignSlogan": {}, + "booksAndReferenceSlogan": "", + "@booksAndReferenceSlogan": {}, + "developmentSlogan": "", + "@developmentSlogan": {}, + "devicesAndIotSlogan": "", + "@devicesAndIotSlogan": {}, + "utilitiesSlogan": "", + "@utilitiesSlogan": {}, + "updateButton": "", + "@updateButton": {}, + "yourReview": "", + "@yourReview": {}, + "yourReviewName": "", + "@yourReviewName": {}, + "clickToRate": "", + "@clickToRate": {}, + "showAllReviews": "", + "@showAllReviews": {}, + "send": "", + "@send": {}, + "unknown": "", + "@unknown": {}, + "reviewSent": "", + "@reviewSent": {}, + "multiUpdateButton": "", + "@multiUpdateButton": {}, + "refreshButton": "", + "@refreshButton": {}, + "publisher": "", + "@publisher": {}, + "packageDetails": "", + "@packageDetails": {}, + "madeBy": "", + "@madeBy": {}, + "ratingsAndReviews": "", + "@ratingsAndReviews": {}, + "ratings": "", + "@ratings": {}, + "writeAreview": "", + "@writeAreview": {}, + "yourReviewTitle": "", + "@yourReviewTitle": {}, + "whatDataIsSend": "", + "@whatDataIsSend": {}, + "starDeveloper": "", + "@starDeveloper": {}, + "debianPackage": "", + "@debianPackage": {}, + "financeSlogan": "", + "@financeSlogan": {}, + "healthAndFitnessSlogan": "", + "@healthAndFitnessSlogan": {}, + "personalisationSlogan": "", + "@personalisationSlogan": {}, + "photoAndVideoSlogan": "", + "@photoAndVideoSlogan": {}, + "productivitySlogan": "", + "@productivitySlogan": {}, + "serverAndCloudSlogan": "", + "@serverAndCloudSlogan": {}, + "updatesAvailable": "", + "@updatesAvailable": {}, + "installing": "", + "@installing": {} } diff --git a/lib/l10n/app_oc.arb b/lib/l10n/app_oc.arb index 9d3ede7c2..c84169ae1 100644 --- a/lib/l10n/app_oc.arb +++ b/lib/l10n/app_oc.arb @@ -311,21 +311,120 @@ "@dependenciesQuestion": {}, "dependenciesFullList": "Veire la lista complèta de las dependéncias", "@dependenciesFullList": {}, - "dependenciesListing": "{length} dependéncias seràn telecargadas en installant {packageName}", - "@dependenciesListing": { + "gallery": "Galariá", + "@gallery": {}, + "additionalInformation": "Informacions complementàrias", + "@additionalInformation": {}, + "links": "Ligams", + "@links": {}, + "searchHintAppStore": "Cercar d’aplicacions", + "@searchHintAppStore": {}, + "searchHintInstalled": "Cercar dins las aplicacions installadas", + "@searchHintInstalled": {}, + "removePackage": "", + "@removePackage": { + "placeholders": { + "package": { + "type": "String" + } + } + }, + "packageType": "", + "@packageType": {}, + "confirmRemove": "", + "@confirmRemove": {}, + "reportReviewDialogBody": "", + "@reportReviewDialogBody": {}, + "summary": "", + "@summary": {}, + "notHelpful": "", + "@notHelpful": {}, + "ratings": "", + "@ratings": {}, + "summeryHint": "", + "@summeryHint": {}, + "multiUpdateButton": "", + "@multiUpdateButton": {}, + "reportReviewDialogTitle": "", + "@reportReviewDialogTitle": {}, + "whatDoYouThink": "", + "@whatDoYouThink": {}, + "removeAll": "", + "@removeAll": {}, + "contributors": "", + "@contributors": {}, + "submit": "", + "@submit": {}, + "ratingsAndReviews": "", + "@ratingsAndReviews": {}, + "copiedToClipboard": "", + "@copiedToClipboard": {}, + "writeAreview": "", + "@writeAreview": {}, + "rate": "", + "@rate": {}, + "helpful": "", + "@helpful": {}, + "whatDataIsSend": "", + "@whatDataIsSend": {}, + "privacyPolicy": "", + "@privacyPolicy": {}, + "upgrading": "", + "@upgrading": {}, + "dependenciesInstallListing": "", + "@dependenciesInstallListing": { "placeholders": { "length": { "type": "int" }, + "size": { + "type": "String" + }, "packageName": { "type": "String" } } }, - "gallery": "Galariá", - "@gallery": {}, - "additionalInformation": "Informacions complementàrias", - "@additionalInformation": {}, - "links": "Ligams", - "@links": {} + "dependenciesRemoveListing": "", + "@dependenciesRemoveListing": { + "placeholders": { + "length": { + "type": "int" + }, + "size": { + "type": "String" + }, + "packageName": { + "type": "String" + } + } + }, + "dependenciesAutoremove": "", + "@dependenciesAutoremove": {}, + "starDeveloper": "", + "@starDeveloper": {}, + "packagesUsed": "", + "@packagesUsed": {}, + "collection": "", + "@collection": {}, + "manage": "", + "@manage": {}, + "share": "", + "@share": {}, + "report": "", + "@report": {}, + "reportAbuse": "", + "@reportAbuse": {}, + "downloading": "", + "@downloading": {}, + "downloadRemaining": "", + "@downloadRemaining": { + "placeholders": { + "bytes": { + "type": "String" + } + } + }, + "ready": "", + "@ready": {} } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index e45be77b8..67a0356fd 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -39,7 +39,7 @@ "@social": {}, "utilities": "Narzędzia", "@utilities": {}, - "all": "Wyszukaj wszystkie", + "all": "Wszystkie kategorie snap", "@all": {}, "installDate": "Data instalacji", "@installDate": {}, @@ -208,5 +208,223 @@ "multiAppFormatsFound": "Znaleźliśmy wiele formatów dla tej aplikacji.", "@multiAppFormatsFound": {}, "attention": "Uwaga!", - "@attention": {} + "@attention": {}, + "searchHintAppStore": "Wyszukiwanie aplikacji", + "@searchHintAppStore": {}, + "searchHintInstalled": "Wyszukiwanie zainstalowanych aplikacji", + "@searchHintInstalled": {}, + "permissions": "Uprawnienia", + "@permissions": {}, + "installing": "Instalacja", + "@installing": {}, + "releasedAt": "", + "@releasedAt": {}, + "noSnapsInstalled": "W twoim systemie nie ma zainstalowanej aplikacji Snap", + "@noSnapsInstalled": {}, + "dependenciesFullList": "Zobacz pełną listę zależności", + "@dependenciesFullList": {}, + "gallery": "Galeria", + "@gallery": {}, + "removePackage": "Usuń {package}", + "@removePackage": { + "placeholders": { + "package": { + "type": "String" + } + } + }, + "confirmRemove": "Czy na pewno chcesz usunąć ten pakiet?", + "@confirmRemove": {}, + "share": "Udostępnij", + "@share": {}, + "report": "Raport", + "@report": {}, + "links": "Linki", + "@links": {}, + "sourcesDescription": "Konfiguracja, z której aktualizowany jest system i pakiety Debiana firm trzecich.", + "@sourcesDescription": {}, + "dependencies": "Zależności", + "@dependencies": {}, + "contributors": "Współtwórcy", + "@contributors": {}, + "educationSlogan": "Narzędzia do nauki w domu", + "@educationSlogan": {}, + "entertainmentSlogan": "Narzędzia domowej rozrywki", + "@entertainmentSlogan": {}, + "privacyPolicy": "polityka prywatności.", + "@privacyPolicy": {}, + "appstreamSearchGreylist": "", + "@appstreamSearchGreylist": { + "description": "List of 'grey-listed' words separated with ';'. Do not translate this list directly. Instead, provide a list of words in your language that people are likely to include in a search but that should normally be ignored in the search." + }, + "healthAndFitnessSlogan": "Zdrowie i fitness", + "@healthAndFitnessSlogan": {}, + "manage": "Zarządzanie", + "@manage": {}, + "dependenciesRemoveListing": "Zależności {length} o łącznym rozmiarze {size} mogą zostać automatycznie usunięte podczas usuwania {packageName}", + "@dependenciesRemoveListing": { + "placeholders": { + "length": { + "type": "int" + }, + "size": { + "type": "String" + }, + "packageName": { + "type": "String" + } + } + }, + "dark": "Ciemny", + "@dark": {}, + "downloading": "Pobieranie", + "@downloading": {}, + "featuredSlogan": "Nasze polecane aplikacje", + "@featuredSlogan": {}, + "helpful": "Pomocne", + "@helpful": {}, + "productivitySlogan": "Bądź produktywny!", + "@productivitySlogan": {}, + "theme": "Motyw", + "@theme": {}, + "notHelpful": "Nieprzydatne", + "@notHelpful": {}, + "offline": "", + "@offline": {}, + "dependenciesAutoremove": "Usuń niepotrzebne już zależności", + "@dependenciesAutoremove": {}, + "refreshing": "Odświeżanie", + "@refreshing": {}, + "removing": "Usuwanie", + "@removing": {}, + "additionalInformation": "Dodatkowe informacje", + "@additionalInformation": {}, + "downloadRemaining": "Pobieranie... Pozostało {bytes}", + "@downloadRemaining": { + "placeholders": { + "bytes": { + "type": "String" + } + } + }, + "whatDataIsSend": "", + "@whatDataIsSend": {}, + "dependenciesQuestion": "Czy chcesz kontynuować?", + "@dependenciesQuestion": {}, + "configure": "Konfiguracja", + "@configure": {}, + "dependenciesInstallListing": "Zależności {length} o łącznym rozmiarze {size} zostaną pobrane podczas instalacji {packageName}", + "@dependenciesInstallListing": { + "placeholders": { + "length": { + "type": "int" + }, + "size": { + "type": "String" + }, + "packageName": { + "type": "String" + } + } + }, + "artAndDesignSlogan": "Narzędzia dla artystów", + "@artAndDesignSlogan": {}, + "socialSlogan": "Spotkajmy się", + "@socialSlogan": {}, + "upgrading": "Aktualizacja", + "@upgrading": {}, + "copiedToClipboard": "Skopiowano do schowka", + "@copiedToClipboard": {}, + "debianPackage": "Debian", + "@debianPackage": {}, + "reportAbuse": "Zgłoś nadużycie", + "@reportAbuse": {}, + "system": "System", + "@system": {}, + "financeSlogan": "Narzędzia finansowe", + "@financeSlogan": {}, + "changingPermissions": "", + "@changingPermissions": {}, + "yourReviewName": "Twoje imię i nazwisko (opcjonalnie)", + "@yourReviewName": {}, + "reportReviewDialogBody": "Możesz zgłosić opinię dotyczącą obraźliwego, niegrzecznego lub dyskryminującego zachowania. Zgłoszona recenzja będzie ukryta do czasu sprawdzenia jej przez administratora.", + "@reportReviewDialogBody": {}, + "removeAll": "Usuń wszystko", + "@removeAll": {}, + "light": "Jasny", + "@light": {}, + "rate": "Wskaźnik", + "@rate": {}, + "serverAndCloudSlogan": "Serwer i chmura", + "@serverAndCloudSlogan": {}, + "booksAndReferenceSlogan": "Organizuje twoje książki", + "@booksAndReferenceSlogan": {}, + "installed": "Zainstalowany", + "@installed": {}, + "snapPackage": "", + "@snapPackage": {}, + "developmentSlogan": "Aplikacje dla programistów", + "@developmentSlogan": {}, + "gamesSlogan": "Gry i granie", + "@gamesSlogan": {}, + "utilitiesSlogan": "Narzędzia", + "@utilitiesSlogan": {}, + "refreshButton": "Sprawdź dostępność aktualizacji", + "@refreshButton": {}, + "yourReviewTitle": "Tytuł recenzji (opcjonalnie)", + "@yourReviewTitle": {}, + "clickToRate": "Kliknij, aby ocenić", + "@clickToRate": {}, + "multiUpdateButton": "Zaktualizuj wszystko", + "@multiUpdateButton": {}, + "publisher": "Wydawca", + "@publisher": {}, + "collection": "Kolekcja", + "@collection": {}, + "allPackageTypes": "Wszystkie typy pakietów", + "@allPackageTypes": {}, + "packageType": "", + "@packageType": {}, + "ratingsAndReviews": "Oceny i recenzje", + "@ratingsAndReviews": {}, + "rating": "Ocena", + "@rating": {}, + "ratings": "Oceny", + "@ratings": {}, + "writeAreview": "Napisz recenzję", + "@writeAreview": {}, + "summary": "Streszczenie", + "@summary": {}, + "showAllReviews": "Pokaż wszystkie recenzje", + "@showAllReviews": {}, + "whatDoYouThink": "Co sądzisz o aplikacji? Spróbuj uzasadnić swoją opinię.", + "@whatDoYouThink": {}, + "summeryHint": "Krótkie podsumowanie recenzji, na przykład: Świetna aplikacja, polecam.", + "@summeryHint": {}, + "submit": "Prześlij", + "@submit": {}, + "starDeveloper": "", + "@starDeveloper": {}, + "packagesUsed": "Używane pakiety", + "@packagesUsed": {}, + "reportReviewDialogTitle": "Przegląd raportu", + "@reportReviewDialogTitle": {}, + "devicesAndIotSlogan": "Urządzenia i IOT", + "@devicesAndIotSlogan": {}, + "musicAndAudioSlogan": "Muzyka i dźwięk", + "@musicAndAudioSlogan": {}, + "newsAndWeatherSlogan": "Wiadomości i pogoda", + "@newsAndWeatherSlogan": {}, + "personalisationSlogan": "Personalizacja", + "@personalisationSlogan": {}, + "scienceSlogan": "Narzędzia naukowe", + "@scienceSlogan": {}, + "securitySlogan": "Chroń swoje dane", + "@securitySlogan": {}, + "photoAndVideoSlogan": "Zdjęcia i wideo", + "@photoAndVideoSlogan": {}, + "processing": "Przetwarzanie", + "@processing": {}, + "ready": "Gotowe", + "@ready": {} } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 4218f6274..6e8d74aa7 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -289,17 +289,6 @@ "@debianPackage": {}, "theme": "Tema", "@theme": {}, - "dependenciesListing": "{length} as dependências serão descarregadas ao instalar {packageName}", - "@dependenciesListing": { - "placeholders": { - "length": { - "type": "int" - }, - "packageName": { - "type": "String" - } - } - }, "publisher": "Editor", "@publisher": {}, "rating": "Avaliação", @@ -327,5 +316,115 @@ "additionalInformation": "Informação adicional", "@additionalInformation": {}, "links": "Ligações", - "@links": {} + "@links": {}, + "dependenciesInstallListing": "", + "@dependenciesInstallListing": { + "placeholders": { + "length": { + "type": "int" + }, + "size": { + "type": "String" + }, + "packageName": { + "type": "String" + } + } + }, + "ratingsAndReviews": "", + "@ratingsAndReviews": {}, + "collection": "", + "@collection": {}, + "ratings": "", + "@ratings": {}, + "writeAreview": "", + "@writeAreview": {}, + "packageType": "", + "@packageType": {}, + "searchHintAppStore": "", + "@searchHintAppStore": {}, + "removePackage": "", + "@removePackage": { + "placeholders": { + "package": { + "type": "String" + } + } + }, + "rate": "", + "@rate": {}, + "confirmRemove": "", + "@confirmRemove": {}, + "starDeveloper": "", + "@starDeveloper": {}, + "manage": "", + "@manage": {}, + "dependenciesAutoremove": "", + "@dependenciesAutoremove": {}, + "removeAll": "", + "@removeAll": {}, + "share": "", + "@share": {}, + "reportAbuse": "", + "@reportAbuse": {}, + "summary": "", + "@summary": {}, + "notHelpful": "", + "@notHelpful": {}, + "helpful": "", + "@helpful": {}, + "contributors": "", + "@contributors": {}, + "upgrading": "", + "@upgrading": {}, + "multiUpdateButton": "", + "@multiUpdateButton": {}, + "searchHintInstalled": "", + "@searchHintInstalled": {}, + "whatDoYouThink": "", + "@whatDoYouThink": {}, + "summeryHint": "", + "@summeryHint": {}, + "submit": "", + "@submit": {}, + "whatDataIsSend": "", + "@whatDataIsSend": {}, + "privacyPolicy": "", + "@privacyPolicy": {}, + "downloading": "", + "@downloading": {}, + "downloadRemaining": "", + "@downloadRemaining": { + "placeholders": { + "bytes": { + "type": "String" + } + } + }, + "ready": "", + "@ready": {}, + "dependenciesRemoveListing": "", + "@dependenciesRemoveListing": { + "placeholders": { + "length": { + "type": "int" + }, + "size": { + "type": "String" + }, + "packageName": { + "type": "String" + } + } + }, + "packagesUsed": "", + "@packagesUsed": {}, + "copiedToClipboard": "", + "@copiedToClipboard": {}, + "report": "", + "@report": {}, + "reportReviewDialogTitle": "", + "@reportReviewDialogTitle": {}, + "reportReviewDialogBody": "", + "@reportReviewDialogBody": {} } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 59e5f2b4b..be058d561 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -271,7 +271,7 @@ "@yourReviewTitle": {}, "installing": "Установка", "@installing": {}, - "appstreamSearchGreylist": "app;application;package;program;programme;suite;tool", + "appstreamSearchGreylist": "приложение;програма;программа;программы;приложения;инструмент", "@appstreamSearchGreylist": { "description": "List of 'grey-listed' words separated with ';'. Do not translate this list directly. Instead, provide a list of words in your language that people are likely to include in a search but that should normally be ignored in the search." }, @@ -301,17 +301,6 @@ "@dependenciesQuestion": {}, "dependenciesFullList": "Посмотреть полный список зависимостей", "@dependenciesFullList": {}, - "dependenciesListing": "{length} зависимостей будет загружено в процессе установки {packageName}", - "@dependenciesListing": { - "placeholders": { - "length": { - "type": "int" - }, - "packageName": { - "type": "String" - } - } - }, "publisher": "Издатель", "@publisher": {}, "rating": "Рейтинг", @@ -327,5 +316,115 @@ "additionalInformation": "Дополнительная Информация", "@additionalInformation": {}, "links": "Ссылки", - "@links": {} + "@links": {}, + "searchHintInstalled": "Поиск установленных приложений", + "@searchHintInstalled": {}, + "searchHintAppStore": "Поиск приложений", + "@searchHintAppStore": {}, + "multiUpdateButton": "Обновить всё", + "@multiUpdateButton": {}, + "downloading": "Загрузка", + "@downloading": {}, + "downloadRemaining": "Загрузка... {bytes} осталось", + "@downloadRemaining": { + "placeholders": { + "bytes": { + "type": "String" + } + } + }, + "ready": "Готово", + "@ready": {}, + "upgrading": "Обновление", + "@upgrading": {}, + "packagesUsed": "Использованных пакетов", + "@packagesUsed": {}, + "contributors": "Участники", + "@contributors": {}, + "collection": "Коллекция", + "@collection": {}, + "reportReviewDialogBody": "", + "@reportReviewDialogBody": {}, + "removePackage": "", + "@removePackage": { + "placeholders": { + "package": { + "type": "String" + } + } + }, + "removeAll": "", + "@removeAll": {}, + "confirmRemove": "", + "@confirmRemove": {}, + "packageType": "", + "@packageType": {}, + "ratingsAndReviews": "", + "@ratingsAndReviews": {}, + "ratings": "", + "@ratings": {}, + "writeAreview": "", + "@writeAreview": {}, + "summary": "", + "@summary": {}, + "rate": "", + "@rate": {}, + "whatDoYouThink": "", + "@whatDoYouThink": {}, + "summeryHint": "", + "@summeryHint": {}, + "submit": "", + "@submit": {}, + "helpful": "", + "@helpful": {}, + "notHelpful": "", + "@notHelpful": {}, + "whatDataIsSend": "", + "@whatDataIsSend": {}, + "privacyPolicy": "", + "@privacyPolicy": {}, + "dependenciesInstallListing": "", + "@dependenciesInstallListing": { + "placeholders": { + "length": { + "type": "int" + }, + "size": { + "type": "String" + }, + "packageName": { + "type": "String" + } + } + }, + "dependenciesRemoveListing": "", + "@dependenciesRemoveListing": { + "placeholders": { + "length": { + "type": "int" + }, + "size": { + "type": "String" + }, + "packageName": { + "type": "String" + } + } + }, + "dependenciesAutoremove": "", + "@dependenciesAutoremove": {}, + "starDeveloper": "", + "@starDeveloper": {}, + "manage": "", + "@manage": {}, + "copiedToClipboard": "", + "@copiedToClipboard": {}, + "share": "", + "@share": {}, + "report": "", + "@report": {}, + "reportAbuse": "", + "@reportAbuse": {}, + "reportReviewDialogTitle": "", + "@reportReviewDialogTitle": {} } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb new file mode 100644 index 000000000..a347acbb7 --- /dev/null +++ b/lib/l10n/app_sk.arb @@ -0,0 +1,430 @@ +{ + "requireRestartSystem": "Reštartujte systém pre dokončenie aktualizácií", + "@requireRestartSystem": {}, + "newsAndWeather": "Správy a počasie", + "@newsAndWeather": {}, + "booksAndReference": "Knihy a referencie", + "@booksAndReference": {}, + "explorePageTitle": "Preskúmať", + "@explorePageTitle": {}, + "myAppsPageTitle": "Moje aplikácie", + "@myAppsPageTitle": {}, + "artAndDesign": "Umenie a dizajn", + "@artAndDesign": {}, + "settingsPageTitle": "Nastavenia", + "@settingsPageTitle": {}, + "updatesPageTitle": "Aktualizácie", + "@updatesPageTitle": {}, + "appTitle": "Softvér Ubuntu", + "@appTitle": {}, + "development": "Vývoj", + "@development": {}, + "devicesAndIot": "Zariadenia a IoT", + "@devicesAndIot": {}, + "entertainment": "Zábava", + "@entertainment": {}, + "finance": "Financie", + "@finance": {}, + "games": "Hry", + "@games": {}, + "healthAndFitness": "Zdravie a fitnes", + "@healthAndFitness": {}, + "musicAndAudio": "Hudba a zvuk", + "@musicAndAudio": {}, + "photoAndVideo": "Foto a video", + "@photoAndVideo": {}, + "productivity": "Produktivita", + "@productivity": {}, + "science": "Veda", + "@science": {}, + "education": "Vzdelávanie", + "@education": {}, + "personalisation": "Personalizácia", + "@personalisation": {}, + "featured": "Odporúčané", + "@featured": {}, + "serverAndCloud": "Server a cloud", + "@serverAndCloud": {}, + "utilities": "Nástroje", + "@utilities": {}, + "developmentSlogan": "Aplikácie pre vývojárov", + "@developmentSlogan": {}, + "devicesAndIotSlogan": "Zariadenia a IoT", + "@devicesAndIotSlogan": {}, + "educationSlogan": "Nástroje domáceho vzdelávania", + "@educationSlogan": {}, + "entertainmentSlogan": "Nástroje domácej zábavy", + "@entertainmentSlogan": {}, + "featuredSlogan": "Naše odporúčané aplikácie", + "@featuredSlogan": {}, + "gamesSlogan": "Hry a hranie", + "@gamesSlogan": {}, + "healthAndFitnessSlogan": "Zdravie a fitnes", + "@healthAndFitnessSlogan": {}, + "musicAndAudioSlogan": "Hudba a zvuk", + "@musicAndAudioSlogan": {}, + "newsAndWeatherSlogan": "Správy a počasie", + "@newsAndWeatherSlogan": {}, + "photoAndVideoSlogan": "Foto a video", + "@photoAndVideoSlogan": {}, + "productivitySlogan": "Buďte produktívni!", + "@productivitySlogan": {}, + "scienceSlogan": "Vedecké nástroje", + "@scienceSlogan": {}, + "securitySlogan": "Chráňte svoje údaje", + "@securitySlogan": {}, + "utilitiesSlogan": "Nástroje", + "@utilitiesSlogan": {}, + "all": "Všetky kategórie snapov", + "@all": {}, + "social": "Sociálne", + "@social": {}, + "personalisationSlogan": "Personalizácia", + "@personalisationSlogan": {}, + "socialSlogan": "Poďte spolu", + "@socialSlogan": {}, + "booksAndReferenceSlogan": "Usporiadajte si zbierku kníh", + "@booksAndReferenceSlogan": {}, + "lastUpdated": "Aktualizované", + "@lastUpdated": {}, + "license": "Licencia", + "@license": {}, + "version": "Verzia", + "@version": {}, + "install": "Inštalovať", + "@install": {}, + "confirmRemove": "Naozaj chcete odstrániť tento balík?", + "@confirmRemove": {}, + "website": "Webová stránka", + "@website": {}, + "description": "Popis", + "@description": {}, + "about": "O aplikácii", + "@about": {}, + "showMore": "Zobraziť viac", + "@showMore": {}, + "showLess": "Zobraziť menej", + "@showLess": {}, + "snapPackage": "Snap", + "@snapPackage": {}, + "remove": "Odstrániť", + "@remove": {}, + "removeAll": "Odstrániť všetko", + "@removeAll": {}, + "refresh": "Obnoviť", + "@refresh": {}, + "open": "Otvoriť", + "@open": {}, + "contact": "Kontakt", + "@contact": {}, + "channel": "Kanál", + "@channel": {}, + "offline": "Bez pripojenia", + "@offline": {}, + "snapPackages": "Balíky Snap", + "@snapPackages": {}, + "confinement": "Obmedzenie", + "@confinement": {}, + "connections": "Spojenia", + "@connections": {}, + "debianPackages": "Balíky Debian", + "@debianPackages": {}, + "weHaveUpdates": "Máme pre vás aktualizácie!", + "@weHaveUpdates": {}, + "justAMoment": "Len chvíľku!", + "@justAMoment": {}, + "name": "Názov", + "@name": {}, + "sortBy": "Zoradenie", + "@sortBy": {}, + "searchHint": "Hľadať", + "@searchHint": {}, + "searchHintInstalled": "Hľadať nainštalované aplikácie", + "@searchHintInstalled": {}, + "selectAll": "Vybrať všetko", + "@selectAll": {}, + "xSelected": "vybraté aktualizácie", + "@xSelected": {}, + "updates": "Aktualizácie", + "@updates": {}, + "deselectAll": "Odznačiť všetko", + "@deselectAll": {}, + "updatesAvailable": "dostupné aktualizácie", + "@updatesAvailable": {}, + "media": "Médiá", + "@media": {}, + "done": "Hotovo", + "@done": {}, + "allSelected": "Všetky aktualizácie vybraté", + "@allSelected": {}, + "updateSelected": "Aktualizovať vybraté", + "@updateSelected": {}, + "checkingForUpdates": "Kontrolujeme dostupnosť aktualizácií", + "@checkingForUpdates": {}, + "refreshButton": "Kontrola aktualizácií", + "@refreshButton": {}, + "readyToUpdate": "Pripravené na aktualizáciu", + "@readyToUpdate": {}, + "apps": "aplikácie", + "@apps": {}, + "multiUpdateButton": "Aktualizovať všetko", + "@multiUpdateButton": {}, + "updateButton": "Aktualizovať", + "@updateButton": {}, + "update": "Aktualizácia", + "@update": {}, + "filterSnaps": "Nastavte filter snapov", + "@filterSnaps": {}, + "requireRestartSession": "Pre dokončenie aktualizácií sa odhláste", + "@requireRestartSession": {}, + "requireRestartApp": "Pre dokončenie aktualizácií reštartujte aplikáciu", + "@requireRestartApp": {}, + "verified": "Overený vydavateľ", + "@verified": {}, + "source": "Zdroj", + "@source": {}, + "sources": "Zdroje", + "@sources": {}, + "cancel": "Zrušiť", + "@cancel": {}, + "findOurRepository": "Nájdete nás na GitHube", + "@findOurRepository": {}, + "noPackageFound": "Ľutujeme, s týmto vyhľadávacím dopytom sa nám nepodarilo nájsť žiadny balík", + "@noPackageFound": {}, + "changelogTooLong": "Pre úplný zoznam zmien, prosím navštívte:", + "@changelogTooLong": {}, + "updatesComplete": "Aktualizácie dokončené", + "@updatesComplete": {}, + "packageInstaller": "Inštalátor balíkov", + "@packageInstaller": {}, + "architecture": "Architektúra", + "@architecture": {}, + "changelog": "Zoznam zmien", + "@changelog": {}, + "classic": "klasické", + "@classic": {}, + "quit": "Ukončiť", + "@quit": {}, + "quitDanger": "Aktuálne sú spustené aktualizácie systému. Ak teraz aplikáciu ukončíte, váš systém môže zostať v poškodenom stave!", + "@quitDanger": {}, + "appFormat": "Formát balíka aplikácií", + "@appFormat": {}, + "packageKitGroup": "Kategória", + "@packageKitGroup": {}, + "allPackageTypes": "Všetky typy balíkov", + "@allPackageTypes": {}, + "runsInBackground": "Softvér Ubuntu stále beží na pozadí, aby kontroloval aktualizácie systému.", + "@runsInBackground": {}, + "ratingsAndReviews": "Hodnotenia a recenzie", + "@ratingsAndReviews": {}, + "ratings": "Hodnotenia", + "@ratings": {}, + "yourReviewName": "Vaše meno", + "@yourReviewName": {}, + "summary": "Zhrnutie", + "@summary": {}, + "yourReview": "Vaša recenzia", + "@yourReview": {}, + "yourReviewTitle": "Názov recenzie", + "@yourReviewTitle": {}, + "writeAreview": "Napísať recenziu", + "@writeAreview": {}, + "packageKitFilter": "Typ balíka", + "@packageKitFilter": {}, + "packageType": "Typ balíka", + "@packageType": {}, + "packageDetails": "Podrobnosti o balíku", + "@packageDetails": {}, + "reviewsAndRatings": "Recenzie a hodnotenia", + "@reviewsAndRatings": {}, + "rating": "Hodnotenie", + "@rating": {}, + "clickToRate": "Kliknutím ohodnoťte", + "@clickToRate": {}, + "copyErrorMessage": "Kopírovať chybovú správu", + "@copyErrorMessage": {}, + "madeBy": "Softvér Ubuntu je vyvinutý a navrhnutý", + "@madeBy": {}, + "helpful": "Užitočné", + "@helpful": {}, + "submit": "Odoslať", + "@submit": {}, + "rate": "Ohodnotiť", + "@rate": {}, + "whatDoYouThink": "Čo si myslíte o aplikácii? Skúste zdôvodniť svoj pohľad.", + "@whatDoYouThink": {}, + "send": "Odoslať", + "@send": {}, + "reviewSent": "Recenzia odoslaná", + "@reviewSent": {}, + "unknown": "Neznáme", + "@unknown": {}, + "theme": "Motív", + "@theme": {}, + "system": "Systémový", + "@system": {}, + "light": "Svetlý", + "@light": {}, + "dark": "Tmavý", + "@dark": {}, + "permissions": "Povolenia", + "@permissions": {}, + "removing": "Odstraňuje sa", + "@removing": {}, + "refreshing": "Obnovuje sa", + "@refreshing": {}, + "attention": "Pozor!", + "@attention": {}, + "installing": "Inštaluje sa", + "@installing": {}, + "changingPermissions": "Zmena povolení", + "@changingPermissions": {}, + "privacyPolicy": "zásadách ochrany súkromia.", + "@privacyPolicy": {}, + "downloading": "Sťahovanie", + "@downloading": {}, + "processing": "Spracováva sa", + "@processing": {}, + "downloadRemaining": "Sťahovanie... {bytes} zostáva", + "@downloadRemaining": { + "placeholders": { + "bytes": { + "type": "String" + } + } + }, + "ready": "Pripravené", + "@ready": {}, + "upgrading": "Inovuje sa", + "@upgrading": {}, + "releasedAt": "Vydané v", + "@releasedAt": {}, + "dependenciesQuestion": "Naozaj chcete pokračovať?", + "@dependenciesQuestion": {}, + "gallery": "Galéria", + "@gallery": {}, + "additionalInformation": "Ďalšie informácie", + "@additionalInformation": {}, + "links": "Odkazy", + "@links": {}, + "configure": "Nastaviť", + "@configure": {}, + "packagesUsed": "Použité balíky", + "@packagesUsed": {}, + "starDeveloper": "Hviezdny vývojár", + "@starDeveloper": {}, + "installed": "Nainštalované", + "@installed": {}, + "dependencies": "Súčasti", + "@dependencies": {}, + "dependenciesFullList": "Zobraziť úplný zoznam súčastí", + "@dependenciesFullList": {}, + "dependenciesRemoveListing": "{length} súčastí s celkovou veľkosťou {size} môžu byť automaticky odstránené pri odstraňovaní {packageName}", + "@dependenciesRemoveListing": { + "placeholders": { + "length": { + "type": "int" + }, + "size": { + "type": "String" + }, + "packageName": { + "type": "String" + } + } + }, + "dependenciesAutoremove": "Odstránenie nepotrebných súčastí", + "@dependenciesAutoremove": {}, + "manage": "Spravovať", + "@manage": {}, + "share": "Zdieľať", + "@share": {}, + "reportAbuse": "Nahlásiť zneužitie", + "@reportAbuse": {}, + "report": "Nahlásiť", + "@report": {}, + "reportReviewDialogTitle": "Nahlásiť recenziu", + "@reportReviewDialogTitle": {}, + "contributors": "Prispievatelia", + "@contributors": {}, + "copiedToClipboard": "Skopírované do schránky", + "@copiedToClipboard": {}, + "collection": "Zbierka", + "@collection": {}, + "reportReviewDialogBody": "Môžete nahlásiť recenziu za urážlivé, hrubé alebo diskriminačné správanie. Po nahlásení bude recenzia skrytá, kým ju neskontroluje administrátor.", + "@reportReviewDialogBody": {}, + "appstreamSearchGreylist": "aplikačný program;používateľské programy;aplikácia;aplikácie;aplikačné programy;aplikačný softvér;výpočtový program;výpočtové programy;počítačový program;počítačové programy;balík;balíky;program;programy;softvér;sada;sady;nástroj;nástroje", + "@appstreamSearchGreylist": { + "description": "List of 'grey-listed' words separated with ';'. Do not translate this list directly. Instead, provide a list of words in your language that people are likely to include in a search but that should normally be ignored in the search." + }, + "security": "Bezpečnosť", + "@security": {}, + "artAndDesignSlogan": "Nástroje pre umelcov", + "@artAndDesignSlogan": {}, + "financeSlogan": "Finančné nástroje", + "@financeSlogan": {}, + "serverAndCloudSlogan": "Server a cloud", + "@serverAndCloudSlogan": {}, + "installDate": "Nainštalované", + "@installDate": {}, + "notInstalled": "Nie je", + "@notInstalled": {}, + "removePackage": "Odstrániť {package}", + "@removePackage": { + "placeholders": { + "package": { + "type": "String" + } + } + }, + "updating": "Počkajte – aktualizujeme váš systém. Prosím, nezatvárajte túto aplikáciu a nevypínajte počítač", + "@updating": {}, + "publisher": "Vydavateľ", + "@publisher": {}, + "showAllReviews": "Zobraziť všetky recenzie", + "@showAllReviews": {}, + "notHelpful": "Nie je užitočné", + "@notHelpful": {}, + "size": "Veľkosť", + "@size": {}, + "noUpdates": "Všetko je aktuálne", + "@noUpdates": {}, + "multiAppFormatsFound": "Pre túto aplikáciu sme našli viac formátov.", + "@multiAppFormatsFound": {}, + "debianPackage": "Debian", + "@debianPackage": {}, + "searchHintAppStore": "Hľadať aplikácie", + "@searchHintAppStore": {}, + "confirm": "Potvrdiť", + "@confirm": {}, + "summeryHint": "Uveďte krátke zhrnutie svojej recenzie, napríklad: Odporúčam skvelú aplikáciu.", + "@summeryHint": {}, + "sourcesDescription": "Nastavte, odkiaľ sa aktualizuje váš systém a balíky Debianu tretích strán.", + "@sourcesDescription": {}, + "issued": "Vydané", + "@issued": {}, + "noSnapFound": "Ľutujeme, s týmto vyhľadávacím dopytom sa nám nepodarilo nájsť žiadny snap", + "@noSnapFound": {}, + "noSnapsInstalled": "Vo vašom systéme nie sú nainštalované žiadne Snap aplikácie", + "@noSnapsInstalled": {}, + "dependenciesInstallListing": "{length} súčastí s celkovou veľkosťou {size} sa stiahne pri inštalácii {packageName}", + "@dependenciesInstallListing": { + "placeholders": { + "length": { + "type": "int" + }, + "size": { + "type": "String" + }, + "packageName": { + "type": "String" + } + } + }, + "whatDataIsSend": "Zistite, aké údaje sa odosielajú v našich ", + "@whatDataIsSend": {}, + "updateAvailable": "Dostupná aktualizácia", + "@updateAvailable": {}, + "enterRepoName": "Zadajte názov úložiska", + "@enterRepoName": {} +} diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 6dd78c947..a1e83b351 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -218,5 +218,213 @@ "developmentSlogan": "Program för utvecklare", "@developmentSlogan": {}, "devicesAndIotSlogan": "Enheter och IOT", - "@devicesAndIotSlogan": {} + "@devicesAndIotSlogan": {}, + "debianPackage": "Debian", + "@debianPackage": {}, + "scienceSlogan": "Vetenskapsverktyg", + "@scienceSlogan": {}, + "socialSlogan": "Förena er", + "@socialSlogan": {}, + "personalisationSlogan": "Anpassning", + "@personalisationSlogan": {}, + "utilitiesSlogan": "Verktyg", + "@utilitiesSlogan": {}, + "publisher": "Utgivare", + "@publisher": {}, + "photoAndVideoSlogan": "Foto och video", + "@photoAndVideoSlogan": {}, + "sourcesDescription": "Ställ in var ditt system och tredjeparts Debian-paket uppdateras ifrån.", + "@sourcesDescription": {}, + "dependencies": "Beroenden", + "@dependencies": {}, + "newsAndWeatherSlogan": "Nyheter och väder", + "@newsAndWeatherSlogan": {}, + "securitySlogan": "Skydda din data", + "@securitySlogan": {}, + "snapPackage": "Snap-paket", + "@snapPackage": {}, + "releasedAt": "Släpptes", + "@releasedAt": {}, + "installed": "Installerat", + "@installed": {}, + "productivitySlogan": "Var produktiv!", + "@productivitySlogan": {}, + "serverAndCloudSlogan": "Server och moln", + "@serverAndCloudSlogan": {}, + "refreshButton": "Sök efter uppdateringar", + "@refreshButton": {}, + "allPackageTypes": "Alla pakettyper", + "@allPackageTypes": {}, + "refreshing": "Uppdaterar", + "@refreshing": {}, + "theme": "Tema", + "@theme": {}, + "light": "Ljust", + "@light": {}, + "appstreamSearchGreylist": "app;program;paket;svit;verktyg", + "@appstreamSearchGreylist": { + "description": "List of 'grey-listed' words separated with ';'. Do not translate this list directly. Instead, provide a list of words in your language that people are likely to include in a search but that should normally be ignored in the search." + }, + "rating": "Betyg", + "@rating": {}, + "showAllReviews": "Visa alla recensioner", + "@showAllReviews": {}, + "installing": "Installerar", + "@installing": {}, + "processing": "Behandlar", + "@processing": {}, + "removing": "Tar bort", + "@removing": {}, + "system": "System", + "@system": {}, + "dark": "Mörkt", + "@dark": {}, + "noSnapsInstalled": "Inga Snap-program är installerade på ditt system", + "@noSnapsInstalled": {}, + "dependenciesQuestion": "Är du säker på att du vill fortsätta?", + "@dependenciesQuestion": {}, + "dependenciesFullList": "Se fullständig lista över beroenden", + "@dependenciesFullList": {}, + "gallery": "Galleri", + "@gallery": {}, + "configure": "Konfigurera", + "@configure": {}, + "educationSlogan": "Verktyg för hemutbildning", + "@educationSlogan": {}, + "entertainmentSlogan": "Verktyg för hemmaunderhållning", + "@entertainmentSlogan": {}, + "featuredSlogan": "Våra utvalda program", + "@featuredSlogan": {}, + "financeSlogan": "Finansverktyg", + "@financeSlogan": {}, + "gamesSlogan": "Spel och gaming", + "@gamesSlogan": {}, + "healthAndFitnessSlogan": "Hälsa och träning", + "@healthAndFitnessSlogan": {}, + "yourReviewTitle": "Recensionens titel", + "@yourReviewTitle": {}, + "yourReviewName": "Ditt namn", + "@yourReviewName": {}, + "clickToRate": "Klicka för att sätta betyg", + "@clickToRate": {}, + "additionalInformation": "Ytterligare information", + "@additionalInformation": {}, + "links": "Länkar", + "@links": {}, + "searchHintAppStore": "Sök efter program", + "@searchHintAppStore": {}, + "searchHintInstalled": "Sök bland dina installerade program", + "@searchHintInstalled": {}, + "musicAndAudioSlogan": "Musik och ljud", + "@musicAndAudioSlogan": {}, + "ready": "Redo", + "@ready": {}, + "multiUpdateButton": "Uppdatera alla", + "@multiUpdateButton": {}, + "changingPermissions": "Ändra behörigheter", + "@changingPermissions": {}, + "permissions": "Behörigheter", + "@permissions": {}, + "downloading": "Laddar ner", + "@downloading": {}, + "downloadRemaining": "Laddar ner... {bytes} kvar", + "@downloadRemaining": { + "placeholders": { + "bytes": { + "type": "String" + } + } + }, + "upgrading": "Uppgraderar", + "@upgrading": {}, + "contributors": "Bidragsgivare", + "@contributors": {}, + "collection": "Samling", + "@collection": {}, + "packagesUsed": "Paket som används", + "@packagesUsed": {}, + "removeAll": "Ta bort alla", + "@removeAll": {}, + "removePackage": "Ta bort {package}", + "@removePackage": { + "placeholders": { + "package": { + "type": "String" + } + } + }, + "submit": "Skicka in", + "@submit": {}, + "manage": "Hantera", + "@manage": {}, + "share": "Dela", + "@share": {}, + "reportReviewDialogBody": "Du kan rapportera en recension för kränkande, oförskämt eller diskriminerande beteende. När den har rapporterats döljs en recension tills den har kontrollerats av en administratör.", + "@reportReviewDialogBody": {}, + "confirmRemove": "Är du säker på att du vill ta bort detta paket?", + "@confirmRemove": {}, + "packageType": "Pakettyp", + "@packageType": {}, + "ratingsAndReviews": "Betyg och recensioner", + "@ratingsAndReviews": {}, + "ratings": "Betyg", + "@ratings": {}, + "writeAreview": "Skriv en recension", + "@writeAreview": {}, + "summary": "Sammanfattning", + "@summary": {}, + "rate": "Betygsätt", + "@rate": {}, + "whatDoYouThink": "Vad tycker du detta program? Försök ge oss en bra anledning till varför du har din åsikt.", + "@whatDoYouThink": {}, + "summeryHint": "Ge en kort sammanfattning av din recension, till exempel: Bra program, skulle rekommendera.", + "@summeryHint": {}, + "helpful": "Hjälpsam", + "@helpful": {}, + "notHelpful": "Inte hjälpsam", + "@notHelpful": {}, + "whatDataIsSend": "Ta reda på vilken data som skickas i vår ", + "@whatDataIsSend": {}, + "privacyPolicy": "integritetspolicy.", + "@privacyPolicy": {}, + "dependenciesInstallListing": "{length} beroenden med en total storlek på {size} kommer att laddas ner då du installerar {packageName}", + "@dependenciesInstallListing": { + "placeholders": { + "length": { + "type": "int" + }, + "size": { + "type": "String" + }, + "packageName": { + "type": "String" + } + } + }, + "dependenciesRemoveListing": "{length} beroenden med en total storlek på {size} kan automatiskt tas bort när du tar bort {packageName}", + "@dependenciesRemoveListing": { + "placeholders": { + "length": { + "type": "int" + }, + "size": { + "type": "String" + }, + "packageName": { + "type": "String" + } + } + }, + "dependenciesAutoremove": "Ta bort beroenden som inte längre behövs", + "@dependenciesAutoremove": {}, + "copiedToClipboard": "Kopierades till urklipp", + "@copiedToClipboard": {}, + "report": "Rapportera", + "@report": {}, + "reportAbuse": "Rapportera missbruk", + "@reportAbuse": {}, + "reportReviewDialogTitle": "Rapportera recension", + "@reportReviewDialogTitle": {}, + "starDeveloper": "Stjärnutvecklare", + "@starDeveloper": {} } diff --git a/lib/l10n/app_tr.arb b/lib/l10n/app_tr.arb index eb9ecbd99..be0c7ed66 100644 --- a/lib/l10n/app_tr.arb +++ b/lib/l10n/app_tr.arb @@ -144,5 +144,287 @@ "updating": "Dişinizi sıkın - sisteminizi güncelliyoruz. Lütfen bu uygulamayı veya bilgisayarınızı kapatmayın", "@updating": {}, "requireRestartApp": "Güncellemeleri tamamlamak için uygulamayı yeniden başlatın", - "@requireRestartApp": {} + "@requireRestartApp": {}, + "packageInstaller": "", + "@packageInstaller": {}, + "appFormat": "", + "@appFormat": {}, + "system": "", + "@system": {}, + "light": "", + "@light": {}, + "snapPackage": "", + "@snapPackage": {}, + "changelog": "", + "@changelog": {}, + "packageKitFilter": "", + "@packageKitFilter": {}, + "reviewsAndRatings": "", + "@reviewsAndRatings": {}, + "yourReview": "", + "@yourReview": {}, + "yourReviewTitle": "", + "@yourReviewTitle": {}, + "refreshing": "", + "@refreshing": {}, + "installed": "", + "@installed": {}, + "searchHintAppStore": "", + "@searchHintAppStore": {}, + "multiAppFormatsFound": "", + "@multiAppFormatsFound": {}, + "verified": "", + "@verified": {}, + "refreshButton": "", + "@refreshButton": {}, + "utilitiesSlogan": "", + "@utilitiesSlogan": {}, + "copiedToClipboard": "", + "@copiedToClipboard": {}, + "reviewSent": "", + "@reviewSent": {}, + "reportReviewDialogBody": "", + "@reportReviewDialogBody": {}, + "gamesSlogan": "", + "@gamesSlogan": {}, + "classic": "", + "@classic": {}, + "report": "", + "@report": {}, + "whatDoYouThink": "", + "@whatDoYouThink": {}, + "healthAndFitnessSlogan": "", + "@healthAndFitnessSlogan": {}, + "share": "", + "@share": {}, + "debianPackage": "", + "@debianPackage": {}, + "changingPermissions": "", + "@changingPermissions": {}, + "installing": "", + "@installing": {}, + "packageDetails": "", + "@packageDetails": {}, + "devicesAndIotSlogan": "", + "@devicesAndIotSlogan": {}, + "appstreamSearchGreylist": "", + "@appstreamSearchGreylist": { + "description": "List of 'grey-listed' words separated with ';'. Do not translate this list directly. Instead, provide a list of words in your language that people are likely to include in a search but that should normally be ignored in the search." + }, + "ready": "", + "@ready": {}, + "privacyPolicy": "", + "@privacyPolicy": {}, + "artAndDesignSlogan": "", + "@artAndDesignSlogan": {}, + "photoAndVideoSlogan": "", + "@photoAndVideoSlogan": {}, + "madeBy": "", + "@madeBy": {}, + "collection": "", + "@collection": {}, + "source": "", + "@source": {}, + "musicAndAudioSlogan": "", + "@musicAndAudioSlogan": {}, + "dependenciesInstallListing": "", + "@dependenciesInstallListing": { + "placeholders": { + "length": { + "type": "int" + }, + "size": { + "type": "String" + }, + "packageName": { + "type": "String" + } + } + }, + "sources": "", + "@sources": {}, + "showAllReviews": "", + "@showAllReviews": {}, + "permissions": "", + "@permissions": {}, + "theme": "", + "@theme": {}, + "architecture": "", + "@architecture": {}, + "financeSlogan": "", + "@financeSlogan": {}, + "securitySlogan": "", + "@securitySlogan": {}, + "publisher": "", + "@publisher": {}, + "searchHintInstalled": "", + "@searchHintInstalled": {}, + "reportReviewDialogTitle": "", + "@reportReviewDialogTitle": {}, + "educationSlogan": "", + "@educationSlogan": {}, + "clickToRate": "", + "@clickToRate": {}, + "send": "", + "@send": {}, + "ratings": "", + "@ratings": {}, + "reportAbuse": "", + "@reportAbuse": {}, + "runsInBackground": "", + "@runsInBackground": {}, + "copyErrorMessage": "", + "@copyErrorMessage": {}, + "confinement": "", + "@confinement": {}, + "weHaveUpdates": "", + "@weHaveUpdates": {}, + "multiUpdateButton": "", + "@multiUpdateButton": {}, + "updatesComplete": "", + "@updatesComplete": {}, + "findOurRepository": "", + "@findOurRepository": {}, + "changelogTooLong": "", + "@changelogTooLong": {}, + "noPackageFound": "", + "@noPackageFound": {}, + "noSnapFound": "", + "@noSnapFound": {}, + "cancel": "", + "@cancel": {}, + "confirm": "", + "@confirm": {}, + "removeAll": "", + "@removeAll": {}, + "confirmRemove": "", + "@confirmRemove": {}, + "removePackage": "", + "@removePackage": { + "placeholders": { + "package": { + "type": "String" + } + } + }, + "quit": "", + "@quit": {}, + "quitDanger": "", + "@quitDanger": {}, + "allPackageTypes": "", + "@allPackageTypes": {}, + "packageKitGroup": "", + "@packageKitGroup": {}, + "packageType": "", + "@packageType": {}, + "ratingsAndReviews": "", + "@ratingsAndReviews": {}, + "rating": "", + "@rating": {}, + "writeAreview": "", + "@writeAreview": {}, + "summary": "", + "@summary": {}, + "yourReviewName": "", + "@yourReviewName": {}, + "rate": "", + "@rate": {}, + "submit": "", + "@submit": {}, + "summeryHint": "", + "@summeryHint": {}, + "unknown": "", + "@unknown": {}, + "helpful": "", + "@helpful": {}, + "notHelpful": "", + "@notHelpful": {}, + "whatDataIsSend": "", + "@whatDataIsSend": {}, + "downloading": "", + "@downloading": {}, + "downloadRemaining": "", + "@downloadRemaining": { + "placeholders": { + "bytes": { + "type": "String" + } + } + }, + "processing": "", + "@processing": {}, + "removing": "", + "@removing": {}, + "upgrading": "", + "@upgrading": {}, + "attention": "", + "@attention": {}, + "dark": "", + "@dark": {}, + "releasedAt": "", + "@releasedAt": {}, + "noSnapsInstalled": "", + "@noSnapsInstalled": {}, + "configure": "", + "@configure": {}, + "sourcesDescription": "", + "@sourcesDescription": {}, + "dependencies": "", + "@dependencies": {}, + "dependenciesQuestion": "", + "@dependenciesQuestion": {}, + "dependenciesFullList": "", + "@dependenciesFullList": {}, + "dependenciesRemoveListing": "", + "@dependenciesRemoveListing": { + "placeholders": { + "length": { + "type": "int" + }, + "size": { + "type": "String" + }, + "packageName": { + "type": "String" + } + } + }, + "dependenciesAutoremove": "", + "@dependenciesAutoremove": {}, + "gallery": "", + "@gallery": {}, + "additionalInformation": "", + "@additionalInformation": {}, + "links": "", + "@links": {}, + "starDeveloper": "", + "@starDeveloper": {}, + "packagesUsed": "", + "@packagesUsed": {}, + "contributors": "", + "@contributors": {}, + "manage": "", + "@manage": {}, + "issued": "", + "@issued": {}, + "booksAndReferenceSlogan": "", + "@booksAndReferenceSlogan": {}, + "developmentSlogan": "", + "@developmentSlogan": {}, + "entertainmentSlogan": "", + "@entertainmentSlogan": {}, + "featuredSlogan": "", + "@featuredSlogan": {}, + "scienceSlogan": "", + "@scienceSlogan": {}, + "personalisationSlogan": "", + "@personalisationSlogan": {}, + "serverAndCloudSlogan": "", + "@serverAndCloudSlogan": {}, + "productivitySlogan": "", + "@productivitySlogan": {}, + "socialSlogan": "", + "@socialSlogan": {}, + "newsAndWeatherSlogan": "", + "@newsAndWeatherSlogan": {} } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb new file mode 100644 index 000000000..999bc08de --- /dev/null +++ b/lib/l10n/app_uk.arb @@ -0,0 +1,430 @@ +{ + "packageKitFilter": "Тип категорії", + "@packageKitFilter": {}, + "removing": "Видалення", + "@removing": {}, + "quit": "Вийти", + "@quit": {}, + "quitDanger": "Зараз оновлюється система. Якщо ви вийдете з програми, то система може стати пошкодженою!", + "@quitDanger": {}, + "additionalInformation": "Додаткова інформація", + "@additionalInformation": {}, + "healthAndFitness": "Здоров'я та спорт", + "@healthAndFitness": {}, + "security": "Безпека", + "@security": {}, + "personalisation": "Персоналізація", + "@personalisation": {}, + "newsAndWeather": "Новини й погода", + "@newsAndWeather": {}, + "musicAndAudio": "Музика й звуки", + "@musicAndAudio": {}, + "explorePageTitle": "Цікаве", + "@explorePageTitle": {}, + "myAppsPageTitle": "Мої програми", + "@myAppsPageTitle": {}, + "settingsPageTitle": "Налаштування", + "@settingsPageTitle": {}, + "artAndDesign": "Дизайн і мистецтво", + "@artAndDesign": {}, + "booksAndReference": "Книги та довідники", + "@booksAndReference": {}, + "development": "Розробка", + "@development": {}, + "devicesAndIot": "Пристрої та IOT", + "@devicesAndIot": {}, + "education": "Освіта й навчання", + "@education": {}, + "finance": "Фінанси", + "@finance": {}, + "games": "Ігри", + "@games": {}, + "photoAndVideo": "Фотографії й відео", + "@photoAndVideo": {}, + "productivity": "Продуктивність", + "@productivity": {}, + "science": "Наука", + "@science": {}, + "serverAndCloud": "Сервера та хмари", + "@serverAndCloud": {}, + "social": "Соціальні мережі", + "@social": {}, + "all": "Усі категорії snap", + "@all": {}, + "artAndDesignSlogan": "Інструменти для художників", + "@artAndDesignSlogan": {}, + "entertainmentSlogan": "Засоби домашніх розваг", + "@entertainmentSlogan": {}, + "gamesSlogan": "Ігри та розваги", + "@gamesSlogan": {}, + "healthAndFitnessSlogan": "Здоров'я та спорт", + "@healthAndFitnessSlogan": {}, + "musicAndAudioSlogan": "Музика та аудіо", + "@musicAndAudioSlogan": {}, + "socialSlogan": "Спілкуйтесь та будьте на зв'язку", + "@socialSlogan": {}, + "notInstalled": "Не встановлено", + "@notInstalled": {}, + "confinement": "Обмеження", + "@confinement": {}, + "install": "Встановити", + "@install": {}, + "refresh": "Оновити", + "@refresh": {}, + "remove": "Видалити", + "@remove": {}, + "website": "Сайт", + "@website": {}, + "debianPackages": "Debian пакети", + "@debianPackages": {}, + "debianPackage": "Debian", + "@debianPackage": {}, + "connections": "Підключення", + "@connections": {}, + "name": "Назва", + "@name": {}, + "sortBy": "Сортувати за", + "@sortBy": {}, + "media": "Медіа", + "@media": {}, + "searchHintAppStore": "Шукати програми", + "@searchHintAppStore": {}, + "updateSelected": "Обране оновлення", + "@updateSelected": {}, + "xSelected": "оновлення обрані", + "@xSelected": {}, + "update": "Оновити", + "@update": {}, + "updateButton": "Оновити", + "@updateButton": {}, + "multiUpdateButton": "Оновити все", + "@multiUpdateButton": {}, + "noUpdates": "Всі програми останньої версії", + "@noUpdates": {}, + "updating": "Постривайте - ваша система оновлюється! Будь ласка, не зачиняйте це вікно і також не вимикайте комп'ютер", + "@updating": {}, + "checkingForUpdates": "Перевіряємо на наявність оновлень", + "@checkingForUpdates": {}, + "apps": "програми", + "@apps": {}, + "issued": "Виданий", + "@issued": {}, + "verified": "Провірений видавець", + "@verified": {}, + "noPackageFound": "Прикро, але програми з таким запитом не знайдено", + "@noPackageFound": {}, + "cancel": "Скасувати", + "@cancel": {}, + "runsInBackground": "Каталог програм Ubuntu буде працювати у фоновому режимі задля перевірок оновлень системи.", + "@runsInBackground": {}, + "appFormat": "Формат пакетів програм", + "@appFormat": {}, + "allPackageTypes": "Всі типи пакетів", + "@allPackageTypes": {}, + "madeBy": "Каталог програм Ubuntu Software розроблений та створений", + "@madeBy": {}, + "yourReviewTitle": "Коротко (не обов'язково)", + "@yourReviewTitle": {}, + "yourReviewName": "Ваше ім'я (не обов'язково)", + "@yourReviewName": {}, + "clickToRate": "Нажміть для оцінювання", + "@clickToRate": {}, + "reviewSent": "Відгук від", + "@reviewSent": {}, + "multiAppFormatsFound": "Знайдено декілька форматів для цієї програми.", + "@multiAppFormatsFound": {}, + "downloadRemaining": "Завантаження... {bytes} залишилось", + "@downloadRemaining": { + "placeholders": { + "bytes": { + "type": "String" + } + } + }, + "installing": "Встановлення", + "@installing": {}, + "processing": "Оброблення", + "@processing": {}, + "ready": "Готово", + "@ready": {}, + "upgrading": "Оновлення", + "@upgrading": {}, + "attention": "Увага!", + "@attention": {}, + "dark": "Темна", + "@dark": {}, + "light": "Світла", + "@light": {}, + "releasedAt": "Опубліковано", + "@releasedAt": {}, + "installed": "Встановлено", + "@installed": {}, + "configure": "Налаштувати", + "@configure": {}, + "sourcesDescription": "Налаштувати звідки ваша система та сторонні Debian пакети будуть оновлюватися.", + "@sourcesDescription": {}, + "gallery": "Галерея", + "@gallery": {}, + "appTitle": "Каталог програм Ubuntu", + "@appTitle": {}, + "links": "Посилання", + "@links": {}, + "packagesUsed": "Використані пакети", + "@packagesUsed": {}, + "contributors": "Вкладники", + "@contributors": {}, + "collection": "Коллекція", + "@collection": {}, + "updatesPageTitle": "Оновлення", + "@updatesPageTitle": {}, + "entertainment": "Розваги", + "@entertainment": {}, + "featured": "Обране", + "@featured": {}, + "packageDetails": "Подробиці пакета", + "@packageDetails": {}, + "source": "Джерело", + "@source": {}, + "unknown": "Невідомо", + "@unknown": {}, + "readyToUpdate": "Готовий оновити", + "@readyToUpdate": {}, + "enterRepoName": "Введіть назву репозиторію", + "@enterRepoName": {}, + "installDate": "Дата встановлення", + "@installDate": {}, + "version": "Версія", + "@version": {}, + "showAllReviews": "Показати всі огляди", + "@showAllReviews": {}, + "yourReview": "Ваш відгук", + "@yourReview": {}, + "rating": "Оцінка", + "@rating": {}, + "lastUpdated": "Останнє оновлено", + "@lastUpdated": {}, + "requireRestartSystem": "Перезавантажте систему для завершення оновлень", + "@requireRestartSystem": {}, + "utilities": "Утиліти", + "@utilities": {}, + "educationSlogan": "Засоби домашнього навчання", + "@educationSlogan": {}, + "newsAndWeatherSlogan": "Новини й погода", + "@newsAndWeatherSlogan": {}, + "productivitySlogan": "Будь продуктивним!", + "@productivitySlogan": {}, + "scienceSlogan": "Наукові засоби", + "@scienceSlogan": {}, + "updates": "Оновлення", + "@updates": {}, + "weHaveUpdates": "Ми маємо для вас оновлення!", + "@weHaveUpdates": {}, + "requireRestartApp": "Перезапустіть програму для завершення оновлень", + "@requireRestartApp": {}, + "send": "Відправити", + "@send": {}, + "reviewsAndRatings": "Відгуки та оцінки", + "@reviewsAndRatings": {}, + "channel": "Джерело", + "@channel": {}, + "devicesAndIotSlogan": "Прилади та IOT", + "@devicesAndIotSlogan": {}, + "changingPermissions": "Зміна прав", + "@changingPermissions": {}, + "searchHintInstalled": "Шукати встановлені програми", + "@searchHintInstalled": {}, + "deselectAll": "Скасувати вибір усіх", + "@deselectAll": {}, + "personalisationSlogan": "Персоналізація", + "@personalisationSlogan": {}, + "packageInstaller": "Інсталлятор програм", + "@packageInstaller": {}, + "architecture": "Архітектура", + "@architecture": {}, + "changelogTooLong": "Для повного списку змін відвідайте:", + "@changelogTooLong": {}, + "changelog": "Список змін", + "@changelog": {}, + "offline": "Не в мережі", + "@offline": {}, + "description": "Опис", + "@description": {}, + "allSelected": "Всі оновлення обрані", + "@allSelected": {}, + "sources": "Джерела", + "@sources": {}, + "dependencies": "Залежності", + "@dependencies": {}, + "noSnapFound": "Прикро, але snap-програми з таким запитом не знайдено", + "@noSnapFound": {}, + "refreshing": "Оновлення", + "@refreshing": {}, + "contact": "Контакт", + "@contact": {}, + "noSnapsInstalled": "Не виявлено встановлених застосунків Snap в вашій системі", + "@noSnapsInstalled": {}, + "snapPackages": "Snap пакети", + "@snapPackages": {}, + "updateAvailable": "Доступне оновлення", + "@updateAvailable": {}, + "updatesAvailable": "доступні оновлення", + "@updatesAvailable": {}, + "classic": "класичний", + "@classic": {}, + "photoAndVideoSlogan": "Фотографії та відео", + "@photoAndVideoSlogan": {}, + "utilitiesSlogan": "Утиліти", + "@utilitiesSlogan": {}, + "about": "Про", + "@about": {}, + "showLess": "Менше подробиць", + "@showLess": {}, + "snapPackage": "Snap", + "@snapPackage": {}, + "done": "Виконано", + "@done": {}, + "searchHint": "Пошук", + "@searchHint": {}, + "publisher": "Видавець", + "@publisher": {}, + "updatesComplete": "Оновлення завершено", + "@updatesComplete": {}, + "refreshButton": "Перевірити наявність оновлень", + "@refreshButton": {}, + "serverAndCloudSlogan": "Сервера та мережі", + "@serverAndCloudSlogan": {}, + "findOurRepository": "Знаходьте нас на GitHub", + "@findOurRepository": {}, + "securitySlogan": "Захищайте свої дані", + "@securitySlogan": {}, + "filterSnaps": "Встановити snap фільтр", + "@filterSnaps": {}, + "booksAndReferenceSlogan": "Збери свою колекцію книжок", + "@booksAndReferenceSlogan": {}, + "developmentSlogan": "Програми для розробників", + "@developmentSlogan": {}, + "featuredSlogan": "Обрані нами програми", + "@featuredSlogan": {}, + "financeSlogan": "Фінансові засоби", + "@financeSlogan": {}, + "license": "Ліцензія", + "@license": {}, + "open": "Відкрито", + "@open": {}, + "size": "Розмір", + "@size": {}, + "showMore": "Більше подробиць", + "@showMore": {}, + "justAMoment": "Хвилинку!", + "@justAMoment": {}, + "selectAll": "Вибрати все", + "@selectAll": {}, + "requireRestartSession": "Вийдіть з сеансу для завершення оновлення", + "@requireRestartSession": {}, + "copyErrorMessage": "Скопіювати повідомлення про помилку", + "@copyErrorMessage": {}, + "packageKitGroup": "Категорія", + "@packageKitGroup": {}, + "confirm": "Підтвердити", + "@confirm": {}, + "permissions": "Права", + "@permissions": {}, + "downloading": "Завантаження", + "@downloading": {}, + "theme": "Тема", + "@theme": {}, + "system": "Система", + "@system": {}, + "appstreamSearchGreylist": "програма;додаток;застосунок;пакет;інструмент;засіб;засоби", + "@appstreamSearchGreylist": { + "description": "List of 'grey-listed' words separated with ';'. Do not translate this list directly. Instead, provide a list of words in your language that people are likely to include in a search but that should normally be ignored in the search." + }, + "dependenciesQuestion": "Ви впевнені, що хочете продовжити?", + "@dependenciesQuestion": {}, + "dependenciesFullList": "Показати всі залежності", + "@dependenciesFullList": {}, + "ratingsAndReviews": "", + "@ratingsAndReviews": {}, + "reportReviewDialogBody": "", + "@reportReviewDialogBody": {}, + "dependenciesAutoremove": "", + "@dependenciesAutoremove": {}, + "summary": "", + "@summary": {}, + "dependenciesInstallListing": "", + "@dependenciesInstallListing": { + "placeholders": { + "length": { + "type": "int" + }, + "size": { + "type": "String" + }, + "packageName": { + "type": "String" + } + } + }, + "confirmRemove": "", + "@confirmRemove": {}, + "helpful": "", + "@helpful": {}, + "manage": "", + "@manage": {}, + "reportReviewDialogTitle": "", + "@reportReviewDialogTitle": {}, + "report": "", + "@report": {}, + "privacyPolicy": "", + "@privacyPolicy": {}, + "reportAbuse": "", + "@reportAbuse": {}, + "ratings": "", + "@ratings": {}, + "copiedToClipboard": "", + "@copiedToClipboard": {}, + "removeAll": "", + "@removeAll": {}, + "removePackage": "", + "@removePackage": { + "placeholders": { + "package": { + "type": "String" + } + } + }, + "packageType": "", + "@packageType": {}, + "writeAreview": "", + "@writeAreview": {}, + "rate": "", + "@rate": {}, + "whatDoYouThink": "", + "@whatDoYouThink": {}, + "summeryHint": "", + "@summeryHint": {}, + "submit": "", + "@submit": {}, + "notHelpful": "", + "@notHelpful": {}, + "whatDataIsSend": "", + "@whatDataIsSend": {}, + "dependenciesRemoveListing": "", + "@dependenciesRemoveListing": { + "placeholders": { + "length": { + "type": "int" + }, + "size": { + "type": "String" + }, + "packageName": { + "type": "String" + } + } + }, + "starDeveloper": "", + "@starDeveloper": {}, + "share": "", + "@share": {} +} diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 0967ef424..7b3a01cd2 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1 +1,430 @@ -{} +{ + "explorePageTitle": "探索", + "@explorePageTitle": {}, + "myAppsPageTitle": "我的应用", + "@myAppsPageTitle": {}, + "updatesPageTitle": "更新", + "@updatesPageTitle": {}, + "settingsPageTitle": "设置", + "@settingsPageTitle": {}, + "games": "游戏", + "@games": {}, + "searchHintAppStore": "搜索应用", + "@searchHintAppStore": {}, + "searchHintInstalled": "搜索已安装的应用", + "@searchHintInstalled": {}, + "xSelected": "更新所选内容", + "@xSelected": {}, + "updating": "稍等 - 我们正在更新您的系统。请不要关闭此应用程序,或关闭您的计算机", + "@updating": {}, + "readyToUpdate": "准备更新", + "@readyToUpdate": {}, + "checkingForUpdates": "我们正在检查更新", + "@checkingForUpdates": {}, + "filterSnaps": "设置snap过滤器", + "@filterSnaps": {}, + "enterRepoName": "输入存储库名称", + "@enterRepoName": {}, + "requireRestartSystem": "重新启动系统以完成更新", + "@requireRestartSystem": {}, + "issued": "已发布", + "@issued": {}, + "gallery": "Gallery", + "@gallery": {}, + "snapPackage": "Snap", + "@snapPackage": {}, + "debianPackage": "Debian", + "@debianPackage": {}, + "theme": "主题", + "@theme": {}, + "light": "浅色", + "@light": {}, + "social": "社会", + "@social": {}, + "utilities": "公用事业", + "@utilities": {}, + "all": "所有snap类别", + "@all": {}, + "offline": "离线", + "@offline": {}, + "updateSelected": "更新所选内容", + "@updateSelected": {}, + "selectAll": "全选", + "@selectAll": {}, + "deselectAll": "取消全选", + "@deselectAll": {}, + "update": "更新", + "@update": {}, + "updateButton": "更新", + "@updateButton": {}, + "apps": "应用", + "@apps": {}, + "copyErrorMessage": "复制错误信息", + "@copyErrorMessage": {}, + "unknown": "未知", + "@unknown": {}, + "configure": "配置", + "@configure": {}, + "about": "关于", + "@about": {}, + "showMore": "显示更多", + "@showMore": {}, + "showLess": "显示更少", + "@showLess": {}, + "connections": "连接", + "@connections": {}, + "updateAvailable": "可用更新", + "@updateAvailable": {}, + "updatesAvailable": "可用更新", + "@updatesAvailable": {}, + "size": "大小", + "@size": {}, + "updates": "更新", + "@updates": {}, + "weHaveUpdates": "我们为您提供了更新!", + "@weHaveUpdates": {}, + "justAMoment": "请稍等!", + "@justAMoment": {}, + "refreshButton": "检查更新", + "@refreshButton": {}, + "noUpdates": "已经是最新的", + "@noUpdates": {}, + "changelogTooLong": "完整的更新日志,请访问:", + "@changelogTooLong": {}, + "findOurRepository": "在GitHub上找到我们", + "@findOurRepository": {}, + "runsInBackground": "Ubuntu Software 一直在后台运行以检查系统更新。", + "@runsInBackground": {}, + "allPackageTypes": "所有包类型", + "@allPackageTypes": {}, + "confirm": "确认", + "@confirm": {}, + "yourReviewTitle": "评论标题", + "@yourReviewTitle": {}, + "send": "发送", + "@send": {}, + "showAllReviews": "显示所有评论", + "@showAllReviews": {}, + "appFormat": "应用程序的包格式", + "@appFormat": {}, + "packageKitGroup": "类别", + "@packageKitGroup": {}, + "packageKitFilter": "包类型", + "@packageKitFilter": {}, + "publisher": "发布者", + "@publisher": {}, + "noPackageFound": "抱歉,我们找不到包含此搜索查询的任何包", + "@noPackageFound": {}, + "noSnapFound": "抱歉,我们找不到包含此搜索查询的任何snap包", + "@noSnapFound": {}, + "cancel": "取消", + "@cancel": {}, + "quit": "退出", + "@quit": {}, + "quitDanger": "系统更新当前正在运行。现在退出应用程序可能会损坏您的系统!", + "@quitDanger": {}, + "clickToRate": "点击评分", + "@clickToRate": {}, + "reviewSent": "发出的评论", + "@reviewSent": {}, + "multiAppFormatsFound": "我们找到了该应用的多种格式的包。", + "@multiAppFormatsFound": {}, + "changingPermissions": "更改权限", + "@changingPermissions": {}, + "permissions": "权限", + "@permissions": {}, + "installing": "安装中", + "@installing": {}, + "system": "系统", + "@system": {}, + "processing": "进程", + "@processing": {}, + "removing": "删除", + "@removing": {}, + "refreshing": "刷新中", + "@refreshing": {}, + "attention": "注意!", + "@attention": {}, + "packagesUsed": "使用的包", + "@packagesUsed": {}, + "removeAll": "移除所有", + "@removeAll": {}, + "confirmRemove": "你确定要删除这个软件包吗?", + "@confirmRemove": {}, + "removePackage": "删除 {package}", + "@removePackage": { + "placeholders": { + "package": { + "type": "String" + } + } + }, + "multiUpdateButton": "全部更新", + "@multiUpdateButton": {}, + "requireRestartSession": "注销以完成更新", + "@requireRestartSession": {}, + "requireRestartApp": "重新启动应用以完成更新", + "@requireRestartApp": {}, + "changelog": "更新日志", + "@changelog": {}, + "architecture": "建筑", + "@architecture": {}, + "source": "源", + "@source": {}, + "sources": "源", + "@sources": {}, + "packageInstaller": "包安装程序", + "@packageInstaller": {}, + "classic": "经典的", + "@classic": {}, + "verified": "已验证的发布者", + "@verified": {}, + "packageType": "包类型", + "@packageType": {}, + "packageDetails": "包详情", + "@packageDetails": {}, + "madeBy": "Ubuntu Software的开发和设计者是", + "@madeBy": {}, + "reviewsAndRatings": "评论和评级", + "@reviewsAndRatings": {}, + "ratingsAndReviews": "评级和评论", + "@ratingsAndReviews": {}, + "rating": "评分", + "@rating": {}, + "ratings": "评分", + "@ratings": {}, + "yourReview": "您的评论", + "@yourReview": {}, + "writeAreview": "撰写评论", + "@writeAreview": {}, + "summary": "摘要", + "@summary": {}, + "rate": "评分", + "@rate": {}, + "whatDoYouThink": "你觉得这个应用程序怎么样?尝试给出这种观点的理由。", + "@whatDoYouThink": {}, + "summeryHint": "对您的评论进行简短总结,例如:很棒的应用程序,会推荐。", + "@summeryHint": {}, + "submit": "提交", + "@submit": {}, + "helpful": "有帮助", + "@helpful": {}, + "notHelpful": "无帮助", + "@notHelpful": {}, + "whatDataIsSend": "找出我们发送的数据 ", + "@whatDataIsSend": {}, + "privacyPolicy": "隐私策略。", + "@privacyPolicy": {}, + "downloading": "下载中", + "@downloading": {}, + "downloadRemaining": "正在下载...剩余 {bytes}", + "@downloadRemaining": { + "placeholders": { + "bytes": { + "type": "String" + } + } + }, + "ready": "准备就绪", + "@ready": {}, + "upgrading": "升级中", + "@upgrading": {}, + "dark": "暗黑", + "@dark": {}, + "noSnapsInstalled": "您的系统上未安装 Snap 应用程序", + "@noSnapsInstalled": {}, + "appstreamSearchGreylist": "应用程序;程序包;程序;程序;套件;工具", + "@appstreamSearchGreylist": { + "description": "List of 'grey-listed' words separated with ';'. Do not translate this list directly. Instead, provide a list of words in your language that people are likely to include in a search but that should normally be ignored in the search." + }, + "releasedAt": "发布于", + "@releasedAt": {}, + "installed": "已安装", + "@installed": {}, + "sourcesDescription": "设置您的系统和第三方Debian软件包的更新位置。", + "@sourcesDescription": {}, + "dependencies": "依赖项", + "@dependencies": {}, + "dependenciesQuestion": "您确定要继续吗?", + "@dependenciesQuestion": {}, + "dependenciesFullList": "查看完整的依赖性列表", + "@dependenciesFullList": {}, + "dependenciesInstallListing": "安装 {packageName} 时将下载总大小为 {size} 的 {length} 依赖项", + "@dependenciesInstallListing": { + "placeholders": { + "length": { + "type": "int" + }, + "size": { + "type": "String" + }, + "packageName": { + "type": "String" + } + } + }, + "dependenciesRemoveListing": "删除 {packageName} 时,可以自动删除总大小为 {size} 的 {length} 依赖项", + "@dependenciesRemoveListing": { + "placeholders": { + "length": { + "type": "int" + }, + "size": { + "type": "String" + }, + "packageName": { + "type": "String" + } + } + }, + "dependenciesAutoremove": "删除不再需要的依赖项", + "@dependenciesAutoremove": {}, + "additionalInformation": "附加信息", + "@additionalInformation": {}, + "links": "链接", + "@links": {}, + "starDeveloper": "明星开发者", + "@starDeveloper": {}, + "contributors": "贡献者", + "@contributors": {}, + "collection": "收藏", + "@collection": {}, + "manage": "管理", + "@manage": {}, + "copiedToClipboard": "复制到剪贴板", + "@copiedToClipboard": {}, + "share": "分享", + "@share": {}, + "report": "报告", + "@report": {}, + "reportAbuse": "举报滥用行为", + "@reportAbuse": {}, + "reportReviewDialogTitle": "报告审核", + "@reportReviewDialogTitle": {}, + "reportReviewDialogBody": "您可以举报辱骂、粗鲁或歧视行为的评论。一旦报告,评论将被隐藏,直到它被管理员检查。", + "@reportReviewDialogBody": {}, + "newsAndWeather": "新闻和天气", + "@newsAndWeather": {}, + "personalisation": "个性化", + "@personalisation": {}, + "serverAndCloud": "服务器和云", + "@serverAndCloud": {}, + "development": "开发", + "@development": {}, + "education": "教育", + "@education": {}, + "artAndDesignSlogan": "艺术家工具", + "@artAndDesignSlogan": {}, + "snapPackages": "Snap 包", + "@snapPackages": {}, + "musicAndAudio": "音乐与音频", + "@musicAndAudio": {}, + "debianPackages": "Debian 包", + "@debianPackages": {}, + "done": "已完成", + "@done": {}, + "searchHint": "搜索", + "@searchHint": {}, + "appTitle": "Ubuntu Software", + "@appTitle": {}, + "booksAndReference": "书籍和参考资料", + "@booksAndReference": {}, + "entertainment": "娱乐", + "@entertainment": {}, + "featured": "特色", + "@featured": {}, + "healthAndFitness": "健康与健身", + "@healthAndFitness": {}, + "educationSlogan": "家庭教育工具", + "@educationSlogan": {}, + "entertainmentSlogan": "家庭娱乐工具", + "@entertainmentSlogan": {}, + "featuredSlogan": "我们的特色应用", + "@featuredSlogan": {}, + "financeSlogan": "金融工具", + "@financeSlogan": {}, + "gamesSlogan": "游戏", + "@gamesSlogan": {}, + "healthAndFitnessSlogan": "健康与健身", + "@healthAndFitnessSlogan": {}, + "newsAndWeatherSlogan": "新闻和天气", + "@newsAndWeatherSlogan": {}, + "photoAndVideoSlogan": "照片和视频", + "@photoAndVideoSlogan": {}, + "productivitySlogan": "提高工作效率!", + "@productivitySlogan": {}, + "securitySlogan": "保护您的数据", + "@securitySlogan": {}, + "socialSlogan": "一起来吧", + "@socialSlogan": {}, + "lastUpdated": "更新时间", + "@lastUpdated": {}, + "notInstalled": "未安装", + "@notInstalled": {}, + "version": "版本", + "@version": {}, + "channel": "频道", + "@channel": {}, + "install": "安装", + "@install": {}, + "refresh": "刷新", + "@refresh": {}, + "sortBy": "排序方式", + "@sortBy": {}, + "allSelected": "选择的所有更新", + "@allSelected": {}, + "remove": "移除", + "@remove": {}, + "website": "网站", + "@website": {}, + "contact": "联系", + "@contact": {}, + "name": "名字", + "@name": {}, + "artAndDesign": "艺术与设计", + "@artAndDesign": {}, + "devicesAndIot": "设备和物联网", + "@devicesAndIot": {}, + "finance": "金融", + "@finance": {}, + "photoAndVideo": "照片和视频", + "@photoAndVideo": {}, + "productivity": "生产力", + "@productivity": {}, + "science": "科学", + "@science": {}, + "security": "安全", + "@security": {}, + "booksAndReferenceSlogan": "整理你的藏书", + "@booksAndReferenceSlogan": {}, + "developmentSlogan": "开发者应用", + "@developmentSlogan": {}, + "devicesAndIotSlogan": "设备和物联网", + "@devicesAndIotSlogan": {}, + "musicAndAudioSlogan": "音乐与音频", + "@musicAndAudioSlogan": {}, + "personalisationSlogan": "个性化", + "@personalisationSlogan": {}, + "scienceSlogan": "科学工具", + "@scienceSlogan": {}, + "serverAndCloudSlogan": "服务器和云", + "@serverAndCloudSlogan": {}, + "utilitiesSlogan": "公用事业", + "@utilitiesSlogan": {}, + "installDate": "安装日期", + "@installDate": {}, + "license": "许可证", + "@license": {}, + "description": "描述", + "@description": {}, + "open": "打开", + "@open": {}, + "media": "媒体", + "@media": {}, + "updatesComplete": "更新完毕", + "@updatesComplete": {}, + "yourReviewName": "您的姓名", + "@yourReviewName": {}, + "confinement": "", + "@confinement": {} +} diff --git a/lib/l10n/app_zh_Hant.arb b/lib/l10n/app_zh_Hant.arb index f35f418da..884076fed 100644 --- a/lib/l10n/app_zh_Hant.arb +++ b/lib/l10n/app_zh_Hant.arb @@ -57,7 +57,7 @@ "@personalisation": {}, "productivity": "生產力工具", "@productivity": {}, - "all": "搜尋全部", + "all": "全部Snap分類", "@all": {}, "installDate": "安裝日期", "@installDate": {}, @@ -80,5 +80,351 @@ "open": "開啟", "@open": {}, "contact": "聯繫", - "@contact": {} + "@contact": {}, + "dark": "深色", + "@dark": {}, + "requireRestartSession": "請登出以完成更新", + "@requireRestartSession": {}, + "requireRestartApp": "請重新啟動應用程式以完成更新", + "@requireRestartApp": {}, + "issued": "發行日期", + "@issued": {}, + "reviewSent": "評論送出", + "@reviewSent": {}, + "installed": "已安裝", + "@installed": {}, + "snapPackage": "Snap", + "@snapPackage": {}, + "debianPackage": "Debian", + "@debianPackage": {}, + "done": "完成", + "@done": {}, + "allPackageTypes": "所有套件種類", + "@allPackageTypes": {}, + "publisher": "發行者", + "@publisher": {}, + "reviewsAndRatings": "評論與評分", + "@reviewsAndRatings": {}, + "light": "淺色", + "@light": {}, + "noSnapsInstalled": "你的系統上未安裝任何 Snap 應用程式", + "@noSnapsInstalled": {}, + "releasedAt": "發行日", + "@releasedAt": {}, + "yourReview": "你的評論", + "@yourReview": {}, + "send": "送出", + "@send": {}, + "unknown": "不明", + "@unknown": {}, + "artAndDesignSlogan": "藝術工具", + "@artAndDesignSlogan": {}, + "booksAndReferenceSlogan": "整理你的藏書", + "@booksAndReferenceSlogan": {}, + "photoAndVideoSlogan": "相片與影片", + "@photoAndVideoSlogan": {}, + "featuredSlogan": "精選程式", + "@featuredSlogan": {}, + "developmentSlogan": "開發用程式", + "@developmentSlogan": {}, + "entertainmentSlogan": "居家娛樂工具", + "@entertainmentSlogan": {}, + "financeSlogan": "金融工具", + "@financeSlogan": {}, + "gamesSlogan": "遊戲.遊樂", + "@gamesSlogan": {}, + "musicAndAudioSlogan": "音樂與音訊", + "@musicAndAudioSlogan": {}, + "newsAndWeatherSlogan": "新聞與天氣", + "@newsAndWeatherSlogan": {}, + "showMore": "顯示更多", + "@showMore": {}, + "snapPackages": "Snap 套件", + "@snapPackages": {}, + "debianPackages": "Debian 套件", + "@debianPackages": {}, + "justAMoment": "請稍待!", + "@justAMoment": {}, + "updateAvailable": "可更新", + "@updateAvailable": {}, + "size": "大小", + "@size": {}, + "name": "名稱", + "@name": {}, + "connections": "權限", + "@connections": {}, + "updatesAvailable": "可更新", + "@updatesAvailable": {}, + "updateSelected": "已選擇更新", + "@updateSelected": {}, + "filterSnaps": "限定 Snap 類別", + "@filterSnaps": {}, + "enterRepoName": "輸入軟體套件庫名稱", + "@enterRepoName": {}, + "requireRestartSystem": "請重新開機以完成更新", + "@requireRestartSystem": {}, + "changelog": "更新日誌", + "@changelog": {}, + "architecture": "架構", + "@architecture": {}, + "source": "來源", + "@source": {}, + "changelogTooLong": "若要詳閱更新日誌,請見:", + "@changelogTooLong": {}, + "sources": "來源", + "@sources": {}, + "updatesComplete": "更新完成", + "@updatesComplete": {}, + "findOurRepository": "看看我們的 GitHub", + "@findOurRepository": {}, + "verified": "已通過驗證的發行者", + "@verified": {}, + "copyErrorMessage": "複製錯誤訊息", + "@copyErrorMessage": {}, + "quit": "關閉", + "@quit": {}, + "packageKitGroup": "分類", + "@packageKitGroup": {}, + "rating": "評分", + "@rating": {}, + "yourReviewTitle": "評論標題", + "@yourReviewTitle": {}, + "multiAppFormatsFound": "此應用程式有多種格式。", + "@multiAppFormatsFound": {}, + "packageInstaller": "套件安裝器", + "@packageInstaller": {}, + "madeBy": "《Ubuntu 軟體》由以下人士開發、設計", + "@madeBy": {}, + "showAllReviews": "顯示所有評論", + "@showAllReviews": {}, + "appstreamSearchGreylist": "程式;套件;軟體", + "@appstreamSearchGreylist": { + "description": "List of 'grey-listed' words separated with ';'. Do not translate this list directly. Instead, provide a list of words in your language that people are likely to include in a search but that should normally be ignored in the search." + }, + "dependenciesQuestion": "確定要繼續嗎?", + "@dependenciesQuestion": {}, + "permissions": "權限", + "@permissions": {}, + "processing": "處理中", + "@processing": {}, + "system": "系統", + "@system": {}, + "theme": "佈景主題", + "@theme": {}, + "dependencies": "依賴的套件", + "@dependencies": {}, + "dependenciesFullList": "顯示依賴套件的完整清單", + "@dependenciesFullList": {}, + "searchHintAppStore": "尋找應用程式", + "@searchHintAppStore": {}, + "searchHintInstalled": "搜尋已安裝的應用程式", + "@searchHintInstalled": {}, + "runsInBackground": "《Ubuntu 軟體》會繼續在背景運行,以檢查系統更新。", + "@runsInBackground": {}, + "configure": "設定", + "@configure": {}, + "sourcesDescription": "設定系統和第三方 Debian 套件的來源。", + "@sourcesDescription": {}, + "devicesAndIotSlogan": "設備與 IoT 裝置", + "@devicesAndIotSlogan": {}, + "educationSlogan": "居家學習工具", + "@educationSlogan": {}, + "healthAndFitnessSlogan": "健康與健身", + "@healthAndFitnessSlogan": {}, + "personalisationSlogan": "個人化", + "@personalisationSlogan": {}, + "productivitySlogan": "提昇生產力!", + "@productivitySlogan": {}, + "scienceSlogan": "科學工具", + "@scienceSlogan": {}, + "securitySlogan": "保護你的個資", + "@securitySlogan": {}, + "serverAndCloudSlogan": "伺服器與雲端", + "@serverAndCloudSlogan": {}, + "socialSlogan": "當我們同在一起", + "@socialSlogan": {}, + "utilitiesSlogan": "附屬工具", + "@utilitiesSlogan": {}, + "showLess": "減少顯示", + "@showLess": {}, + "offline": "離線", + "@offline": {}, + "sortBy": "排序", + "@sortBy": {}, + "media": "媒體", + "@media": {}, + "updates": "更新", + "@updates": {}, + "searchHint": "搜尋", + "@searchHint": {}, + "selectAll": "選擇全部", + "@selectAll": {}, + "allSelected": "選擇了所有更新", + "@allSelected": {}, + "xSelected": "更新已選項目", + "@xSelected": {}, + "weHaveUpdates": "有更新喔!", + "@weHaveUpdates": {}, + "deselectAll": "取消選擇全部", + "@deselectAll": {}, + "update": "更新", + "@update": {}, + "updateButton": "更新", + "@updateButton": {}, + "refreshButton": "檢查更新", + "@refreshButton": {}, + "noUpdates": "程式皆為最新狀態", + "@noUpdates": {}, + "updating": "請稍等。目前正在更新你的系統,請勿關閉此軟體或關機", + "@updating": {}, + "checkingForUpdates": "正在檢查更新", + "@checkingForUpdates": {}, + "readyToUpdate": "準備好安裝更新", + "@readyToUpdate": {}, + "apps": "應用程式", + "@apps": {}, + "classic": "傳統", + "@classic": {}, + "noPackageFound": "抱歉,找不到和此搜尋關鍵字相關的套件", + "@noPackageFound": {}, + "noSnapFound": "抱歉,找不到和此搜尋關鍵字相關的 Snap 套件", + "@noSnapFound": {}, + "cancel": "取消", + "@cancel": {}, + "confirm": "確認", + "@confirm": {}, + "quitDanger": "正在進行系統更新。現在關閉應用程式可能會導致電腦系統毀損!", + "@quitDanger": {}, + "appFormat": "應用程式套件的格式", + "@appFormat": {}, + "packageKitFilter": "套件的種類", + "@packageKitFilter": {}, + "packageDetails": "套件細節", + "@packageDetails": {}, + "yourReviewName": "姓名", + "@yourReviewName": {}, + "clickToRate": "點此處評分", + "@clickToRate": {}, + "changingPermissions": "變更權限", + "@changingPermissions": {}, + "installing": "安裝中", + "@installing": {}, + "removing": "移除中", + "@removing": {}, + "refreshing": "重新整理中", + "@refreshing": {}, + "attention": "注意!", + "@attention": {}, + "gallery": "軟體圖像一覽", + "@gallery": {}, + "additionalInformation": "額外資訊", + "@additionalInformation": {}, + "links": "連結", + "@links": {}, + "downloading": "下載中", + "@downloading": {}, + "multiUpdateButton": "更新全部", + "@multiUpdateButton": {}, + "downloadRemaining": "下載中…尚餘 {bytes}", + "@downloadRemaining": { + "placeholders": { + "bytes": { + "type": "String" + } + } + }, + "ready": "準備完成", + "@ready": {}, + "upgrading": "更新中", + "@upgrading": {}, + "packagesUsed": "使用的套件", + "@packagesUsed": {}, + "collection": "收藏庫", + "@collection": {}, + "contributors": "協力者", + "@contributors": {}, + "submit": "送出", + "@submit": {}, + "manage": "管理", + "@manage": {}, + "reportReviewDialogTitle": "回報評論", + "@reportReviewDialogTitle": {}, + "dependenciesRemoveListing": "移除 {packageName} 之後可以順便移除 {length} 個依賴套件,共清出 {size} 的空間", + "@dependenciesRemoveListing": { + "placeholders": { + "length": { + "type": "int" + }, + "size": { + "type": "String" + }, + "packageName": { + "type": "String" + } + } + }, + "confirmRemove": "確定要刪除這個套件嗎?", + "@confirmRemove": {}, + "dependenciesInstallListing": "安裝 {packageName} 需要下載 {length} 個依賴套件,共佔用 {size} 的空間", + "@dependenciesInstallListing": { + "placeholders": { + "length": { + "type": "int" + }, + "size": { + "type": "String" + }, + "packageName": { + "type": "String" + } + } + }, + "copiedToClipboard": "複製到剪貼簿", + "@copiedToClipboard": {}, + "starDeveloper": "明星開發者", + "@starDeveloper": {}, + "privacyPolicy": "隱私政策。", + "@privacyPolicy": {}, + "removeAll": "移除全部", + "@removeAll": {}, + "removePackage": "移除 {package}", + "@removePackage": { + "placeholders": { + "package": { + "type": "String" + } + } + }, + "packageType": "套件類型", + "@packageType": {}, + "ratingsAndReviews": "評分與評論", + "@ratingsAndReviews": {}, + "ratings": "評分", + "@ratings": {}, + "writeAreview": "撰寫評論", + "@writeAreview": {}, + "summary": "總結", + "@summary": {}, + "rate": "評分", + "@rate": {}, + "whatDoYouThink": "你對這個應用程式有什麼看法?請替你的看法寫下理由。", + "@whatDoYouThink": {}, + "summeryHint": "對你的評論作一個簡短的總結,例如:很棒的 app,非常推薦。", + "@summeryHint": {}, + "helpful": "很有用", + "@helpful": {}, + "notHelpful": "不太有用", + "@notHelpful": {}, + "whatDataIsSend": "檢查你送出了哪些資料從我們的 ", + "@whatDataIsSend": {}, + "dependenciesAutoremove": "移除不再被需要的依賴套件", + "@dependenciesAutoremove": {}, + "share": "分享", + "@share": {}, + "report": "回報", + "@report": {}, + "reportAbuse": "回報謾罵", + "@reportAbuse": {}, + "reportReviewDialogBody": "你可以回報謾罵、粗俗、或歧視性的評論。回報之後,在管理員審查完成之前該篇評論都會保持隱藏狀態。", + "@reportReviewDialogBody": {} } diff --git a/lib/main.dart b/lib/main.dart index ad17374a9..1c175c4d7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -21,6 +21,7 @@ import 'package:flutter/material.dart'; import 'package:gtk_application/gtk_application.dart'; import 'package:launcher_entry/launcher_entry.dart'; import 'package:packagekit/packagekit.dart'; +import 'package:snapcraft_launcher/snapcraft_launcher.dart'; import 'package:snapd/snapd.dart'; import 'package:software/app/app.dart'; import 'package:software/services/appstream/appstream_service.dart'; @@ -29,17 +30,11 @@ import 'package:software/services/packagekit/package_service.dart'; import 'package:software/services/snap_service.dart'; import 'package:ubuntu_service/ubuntu_service.dart'; import 'package:ubuntu_session/ubuntu_session.dart'; -import 'package:window_manager/window_manager.dart'; import 'package:yaru_widgets/yaru_widgets.dart'; Future main(List args) async { - WidgetsFlutterBinding.ensureInitialized(); - await windowManager.ensureInitialized(); - await YaruWindowTitleBar.ensureInitialized(); - windowManager.setPreventClose(false); - registerService(AppstreamService.new); registerService( NotificationsClient.new, @@ -74,5 +69,10 @@ Future main(List args) async { dispose: (s) => s.close(), ); + registerService( + () => PrivilegedDesktopLauncher(), + dispose: (s) => s.close(), + ); + runApp(App.create()); } diff --git a/lib/services/packagekit/package_service.dart b/lib/services/packagekit/package_service.dart index 8e7575985..4fd371ffb 100644 --- a/lib/services/packagekit/package_service.dart +++ b/lib/services/packagekit/package_service.dart @@ -23,6 +23,7 @@ import 'package:desktop_notifications/desktop_notifications.dart'; import 'package:file/file.dart'; import 'package:file/local.dart'; import 'package:intl/intl.dart'; +import 'package:meta/meta.dart'; import 'package:packagekit/packagekit.dart'; import 'package:software/services/packagekit/package_state.dart'; import 'package:software/app/common/packagekit/package_model.dart'; @@ -38,19 +39,42 @@ class MissingPackageIDException implements Exception { class PackageService { final PackageKitClient _client; + final DBusClient _dBusClient; final NotificationsClient _notificationsClient; bool _serviceAvailable = false; - PackageService() + PackageService([@visibleForTesting DBusClient? dbusClient]) : _client = getService(), - _notificationsClient = getService() { - _initialized = _client.connect().then((_) { - _serviceAvailable = true; - }).onError( - (_, __) {}, - test: (error) => error is DBusServiceUnknownException, + _notificationsClient = getService(), + _dBusClient = dbusClient ?? DBusClient.system() { + _initialized = _activatePackageKit(_dBusClient).then( + (_) => _client.connect().then((_) { + _serviceAvailable = true; + }).onError( + (_, __) {}, + test: (error) => error is DBusServiceUnknownException, + ), ); } + /// Explicitly activates the PackageKit service in case it is not running. + /// Prevents AppArmor denials when trying to call a well-known method while + /// the daemon is inactive. + /// See https://github.com/ubuntu-flutter-community/software/issues/1215 + /// and https://forum.snapcraft.io/t/apparmor-denial-in-new-snap-store-despite-connected-packagekit-control-interface/35290 + static Future _activatePackageKit(DBusClient dBusClient) async { + final object = DBusRemoteObject( + dBusClient, + name: 'org.freedesktop.DBus', + path: DBusObjectPath('/org/freedesktop/DBus'), + ); + await object.callMethod( + 'org.freedesktop.DBus', + 'StartServiceByName', + const [DBusString('org.freedesktop.PackageKit'), DBusUint32(0)], + ); + await dBusClient.close(); + } + late final Future _initialized; Future get initialized => _initialized; bool get isAvailable => _serviceAvailable; @@ -77,7 +101,6 @@ class PackageService { final Map _installedPackages = {}; List get installedPackages => _installedPackages.entries.map((e) => e.value).toList(); - PackageKitPackageId? getInstalledId(String name) => _installedPackages[name]; final _installedPackagesController = StreamController.broadcast(); Stream get installedPackagesChanged => _installedPackagesController.stream; @@ -85,6 +108,12 @@ class PackageService { _installedPackagesController.add(value); } + final Map _installedPackagesForUpdates = {}; + List get installedPackagesForUpdates => + _installedPackagesForUpdates.entries.map((e) => e.value).toList(); + PackageKitPackageId? getInstalledId(String name) => + _installedPackagesForUpdates[name]; + final Map _idsToGroups = {}; final _groupsController = StreamController.broadcast(); Stream get groupsChanged => _groupsController.stream; @@ -288,7 +317,7 @@ class PackageService { _updates.putIfAbsent(id, () => true); setUpdatesChanged(true); } else if (event is PackageKitItemProgressEvent) { - setUpdatePercentage(event.percentage); + setUpdatePercentage(_pendingUpdatesCheckTransaction?.percentage); } else if (event is PackageKitErrorCodeEvent) { if (isRefreshErrorToReport(event.code)) { final error = '${event.code}: ${event.details}'; @@ -331,7 +360,7 @@ class PackageService { setTerminalOutput(event.info.toString()); } else if (event is PackageKitItemProgressEvent) { setUpdatesState(UpdatesState.updating); - setUpdatePercentage(event.percentage); + setUpdatePercentage(updatePackagesTransaction.percentage); setProcessedId(event.packageId); setStatus(event.status); @@ -389,17 +418,20 @@ class PackageService { Set filters = const { PackageKitFilter.installed, }, + bool? forUpdates, }) async { - _installedPackages.clear(); + final list = + forUpdates == true ? _installedPackagesForUpdates : _installedPackages; + + list.clear(); final transaction = await _client.createTransaction(); final completer = Completer(); final subscription = transaction.events.listen((event) { if (event is PackageKitPackageEvent) { - _installedPackages.putIfAbsent( + list.putIfAbsent( event.packageId.name, () => event.packageId, ); - setInstalledPackagesChanged(true); } else if (event is PackageKitErrorCodeEvent) { setErrorMessage('${event.code}: ${event.details}'); } else if (event is PackageKitFinishedEvent) { @@ -409,7 +441,9 @@ class PackageService { await transaction.getPackages( filter: filters, ); - return completer.future.whenComplete(subscription.cancel); + await completer.future; + await subscription.cancel(); + setInstalledPackagesChanged(true); } Future _updateGroups(Iterable ids) async { @@ -426,44 +460,66 @@ class PackageService { return completer.future.whenComplete(subscription.cancel); } - Future remove({required PackageModel model}) async { + Future remove({ + required PackageModel model, + bool autoremove = false, + }) async { if (model.packageId == null) throw const MissingPackageIDException(); - model.packageState = PackageState.processing; + model.packageState = PackageState.removing; + model.percentage = 0; final transaction = await _client.createTransaction(); final completer = Completer(); final subscription = transaction.events.listen((event) { if (event is PackageKitPackageEvent) { model.info = event.info; } else if (event is PackageKitItemProgressEvent) { - model.percentage = 100 - event.percentage; + model.percentage = event.percentage; + model.status = event.status; } else if (event is PackageKitFinishedEvent) { model.isInstalled = (event.exit != PackageKitExit.success); model.packageState = PackageState.ready; completer.complete(); } }); - transaction.removePackages([model.packageId!]); - return completer.future.whenComplete(subscription.cancel); + await transaction.removePackages( + [model.packageId!], + allowDeps: autoremove, + autoremove: autoremove, + ); + await completer.future; + await subscription.cancel(); + _installedPackages.remove(model.packageId!.name); + setInstalledPackagesChanged(true); } Future install({required PackageModel model}) async { if (model.packageId == null) throw const MissingPackageIDException(); - model.packageState = PackageState.processing; + model.packageState = PackageState.installing; + model.percentage = 0; final transaction = await _client.createTransaction(); final completer = Completer(); final subscription = transaction.events.listen((event) { if (event is PackageKitPackageEvent) { model.info = event.info; } else if (event is PackageKitItemProgressEvent) { - model.percentage = event.percentage; + model.percentage = transaction.percentage; + model.downloadSizeRemaining = transaction.downloadSizeRemaining; + model.status = event.status; } else if (event is PackageKitFinishedEvent) { model.packageState = PackageState.ready; model.isInstalled = (event.exit == PackageKitExit.success); + model.status = PackageKitStatus.unknown; completer.complete(); } }); - transaction.installPackages([model.packageId!]); - return completer.future.whenComplete(subscription.cancel); + await transaction.installPackages([model.packageId!]); + await completer.future; + await subscription.cancel(); + _installedPackages.putIfAbsent( + model.packageId!.name, + () => model.packageId!, + ); + setInstalledPackagesChanged(true); } Future isInstalled({required PackageModel model}) async { @@ -472,8 +528,10 @@ class PackageService { final transaction = await _client.createTransaction(); final completer = Completer(); final subscription = transaction.events.listen((event) { - if (event is PackageKitPackageEvent) { - model.isInstalled = event.info == PackageKitInfo.installed; + if (event is PackageKitPackageEvent && + model.packageId!.name == event.packageId.name && + event.info == PackageKitInfo.installed) { + model.isInstalled = true; model.versionChanged = event.packageId.version != model.packageId?.version; } else if (event is PackageKitFinishedEvent) { @@ -484,7 +542,6 @@ class PackageService { }); transaction.searchNames( [model.packageId!.name], - filter: {PackageKitFilter.installed}, ); return completer.future.whenComplete(subscription.cancel); } @@ -644,7 +701,8 @@ class PackageService { !fileSystem.file(model.path!).existsSync()) { throw FileSystemException('', model.path); } - model.packageState = PackageState.processing; + model.packageState = PackageState.installing; + model.percentage = 0; final transaction = await _client.createTransaction(); final completer = Completer(); final subscription = transaction.events.listen((event) { @@ -652,15 +710,24 @@ class PackageService { model.info = event.info; model.packageId = event.packageId; } else if (event is PackageKitItemProgressEvent) { - model.percentage = event.percentage; + model.percentage = transaction.percentage; + model.downloadSizeRemaining = transaction.downloadSizeRemaining; + model.status = event.status; } else if (event is PackageKitFinishedEvent) { model.isInstalled = (event.exit == PackageKitExit.success); model.packageState = PackageState.ready; + model.status = PackageKitStatus.unknown; completer.complete(); } }); - transaction.installFiles([model.path!]); - return completer.future.whenComplete(subscription.cancel); + await transaction.installFiles([model.path!]); + await completer.future; + await subscription.cancel(); + _installedPackages.putIfAbsent( + model.packageId!.name, + () => model.packageId!, + ); + setInstalledPackagesChanged(true); } bool isRefreshErrorToReport(PackageKitError code) { @@ -675,16 +742,70 @@ class PackageService { }.contains(code); } - Future getDependencies({ + Future getInstalledDependencies({ required PackageModel model, }) async { - Map dependencies = {}; + if (model.packageId == null) throw const MissingPackageIDException(); + final removeTransaction = await _client.createTransaction(); + final removeCompleter = Completer(); + Map dependencyInfos = {}; + final removeSubscription = removeTransaction.events.listen((event) { + if (event is PackageKitPackageEvent && + event.packageId != model.packageId) { + dependencyInfos.putIfAbsent(event.packageId, () => event.info); + } else if (event is PackageKitFinishedEvent) { + removeCompleter.complete(); + } + }); + await removeTransaction.removePackages( + [model.packageId!], + allowDeps: true, + autoremove: true, + transactionFlags: {PackageKitTransactionFlag.simulate}, + ); + await removeCompleter.future; + await removeSubscription.cancel(); + + if (dependencyInfos.isEmpty) { + model.dependencies = []; + return; + } + + final detailsTransaction = await _client.createTransaction(); + final detailsCompleter = Completer(); + final dependencies = []; + final detailsSubscription = detailsTransaction.events.listen((event) { + if (event is PackageKitDetailsEvent) { + dependencies.add( + PackageDependecy( + id: event.packageId, + info: PackageKitInfo.installed, + size: event.size, + summary: event.summary, + ), + ); + } else if (event is PackageKitFinishedEvent) { + detailsCompleter.complete(); + } + }); + await detailsTransaction.getDetails(dependencyInfos.keys); + await detailsCompleter.future; + await detailsSubscription.cancel(); + + model.dependencies = dependencies; + } + + Future getMissingDependencies({ + required PackageModel model, + }) async { + Map dependencyInfos = {}; if (model.packageId == null) return; final dependsOnTransaction = await _client.createTransaction(); final dependsOnCompleter = Completer(); - dependsOnTransaction.events.listen((event) { - if (event is PackageKitPackageEvent) { - dependencies.putIfAbsent( + final dependsOnSubscription = dependsOnTransaction.events.listen((event) { + if (event is PackageKitPackageEvent && + event.info == PackageKitInfo.available) { + dependencyInfos.putIfAbsent( event.packageId, () => event.info, ); @@ -694,8 +815,35 @@ class PackageService { dependsOnCompleter.complete(); } }); - await dependsOnTransaction.dependsOn([model.packageId!]); - await dependsOnCompleter.future; + await dependsOnTransaction.dependsOn([model.packageId!], recursive: true); + await dependsOnCompleter.future.whenComplete(dependsOnSubscription.cancel); + + if (dependencyInfos.isEmpty) { + model.dependencies = []; + return; + } + + final dependencies = []; + + final getDetailsTransaction = await _client.createTransaction(); + final getDetailsCompleter = Completer(); + final getDetailsSubscription = getDetailsTransaction.events.listen((event) { + if (event is PackageKitDetailsEvent) { + dependencies.add( + PackageDependecy( + id: event.packageId, + info: dependencyInfos[event.packageId] ?? PackageKitInfo.unknown, + size: event.size, + summary: event.summary, + ), + ); + } else if (event is PackageKitFinishedEvent) { + getDetailsCompleter.complete(); + } + }); + await getDetailsTransaction.getDetails(dependencyInfos.keys); + await getDetailsCompleter.future + .whenComplete(getDetailsSubscription.cancel); model.dependencies = dependencies; } diff --git a/lib/services/packagekit/package_state.dart b/lib/services/packagekit/package_state.dart index d2fafc016..c4ffd1893 100644 --- a/lib/services/packagekit/package_state.dart +++ b/lib/services/packagekit/package_state.dart @@ -15,4 +15,22 @@ * */ -enum PackageState { processing, ready } +import 'package:software/l10n/l10n.dart'; + +enum PackageState { + installing, + removing, + upgrading, + downloading, + processing, + ready; + + String localize(AppLocalizations l10n) => switch (this) { + PackageState.installing => l10n.installing, + PackageState.removing => l10n.removing, + PackageState.upgrading => l10n.upgrading, + PackageState.downloading => l10n.downloading, + PackageState.processing => l10n.processing, + PackageState.ready => l10n.ready + }; +} diff --git a/lib/services/packagekit/updates_state.dart b/lib/services/packagekit/updates_state.dart index 7a289fe40..8cf86d197 100644 --- a/lib/services/packagekit/updates_state.dart +++ b/lib/services/packagekit/updates_state.dart @@ -23,18 +23,10 @@ enum UpdatesState { checkingForUpdates, readyToUpdate; - String localize(AppLocalizations l10n) { - switch (this) { - case noUpdates: - return l10n.noUpdates; - case updating: - return l10n.updating; - case checkingForUpdates: - return l10n.checkingForUpdates; - case readyToUpdate: - return l10n.readyToUpdate; - default: - return ''; - } - } + String localize(AppLocalizations l10n) => switch (this) { + noUpdates => l10n.noUpdates, + updating => l10n.updating, + checkingForUpdates => l10n.checkingForUpdates, + readyToUpdate => l10n.readyToUpdate, + }; } diff --git a/lib/services/snap_service.dart b/lib/services/snap_service.dart index fd7881176..c972d3e5f 100644 --- a/lib/services/snap_service.dart +++ b/lib/services/snap_service.dart @@ -21,7 +21,6 @@ import 'package:collection/collection.dart'; import 'package:desktop_notifications/desktop_notifications.dart'; import 'package:snapd/snapd.dart'; import 'package:software/app/common/snap/snap_section.dart'; -import 'package:software/app/common/snap/snap_utils.dart'; import 'package:software/snapd_change_x.dart'; import 'package:ubuntu_service/ubuntu_service.dart'; @@ -48,16 +47,18 @@ class SnapService { } if (newChange.ready) { removeChange(snap); - _notificationsClient.notify( - 'Software', - body: '$doneString: ${newChange.summary}', - appName: snap.name, - appIcon: 'snap-store', - hints: [ - NotificationHint.desktopEntry('software'), - NotificationHint.urgency(NotificationUrgency.normal) - ], - ); + if (newChange.status == 'Done') { + _notificationsClient.notify( + 'Software', + body: '$doneString: ${newChange.summary}', + appName: snap.name, + appIcon: 'snap-store', + hints: [ + NotificationHint.desktopEntry('software'), + NotificationHint.urgency(NotificationUrgency.normal) + ], + ); + } break; } await Future.delayed( @@ -77,6 +78,12 @@ class SnapService { return _snapChanges[snap]; } + Future abortChange(Snap snap) async { + final change = getChange(snap); + if (change == null) return; + await _snapDClient.abortChange(change.id); + } + final _snapChangesController = StreamController.broadcast(); Stream get snapChangesInserted => _snapChangesController.stream; @@ -121,15 +128,10 @@ class SnapService { } } - final List _localSnaps = []; - List get localSnaps => _localSnaps; - Future> loadLocalSnaps() async { - final snaps = (await _snapDClient.getSnaps()); - if (snaps.length != _localSnaps.length) { - _localSnaps.clear(); - _localSnaps.addAll(snaps); - } - return _localSnaps; + List? _localSnaps; + List? get localSnaps => _localSnaps; + Future loadLocalSnaps() async { + _localSnaps = (await _snapDClient.getSnaps()); } Future> findSnapsByQuery({ @@ -217,6 +219,8 @@ class SnapService { _refreshErrorController.add( '${snap.name} has running apps, close ${snap.name} to update.', ); + } else if (e.kind == 'auth-cancelled') { + rethrow; } } } @@ -287,8 +291,8 @@ class SnapService { Future getSnapChanges({required String name}) async => (await _snapDClient.getChanges(name: name)).firstOrNull; - final _sectionsChangedController = StreamController.broadcast(); - Stream get sectionsChanged => _sectionsChangedController.stream; + final _sectionsChangedController = StreamController.broadcast(); + Stream get sectionsChanged => _sectionsChangedController.stream; final Map> _sectionNameToSnapsMap = {}; Map> get sectionNameToSnapsMap => @@ -303,42 +307,36 @@ class SnapService { sectionList.add(snap); } _sectionNameToSnapsMap.putIfAbsent(section, () => sectionList); - _sectionsChangedController.add(true); + _sectionsChangedController.add(section); } - final List _snapsWithUpdate = []; - List get snapsWithUpdate => _snapsWithUpdate; - Future> loadSnapsWithUpdate() async { - List localSnaps = await _snapDClient.getSnaps(); - - Map localSnapsToStoreSnaps = {}; - for (var snap in localSnaps) { - final storeSnap = await findSnapByName(snap.name) ?? snap; - localSnapsToStoreSnaps.putIfAbsent(snap, () => storeSnap); - } - - final snapsWithUpdates = localSnaps.where((snap) { - if (localSnapsToStoreSnaps[snap] == null) return false; - return isSnapUpdateAvailable( - storeSnap: localSnapsToStoreSnaps[snap]!, - localSnap: snap, - ); - }).toList(); - - if (_snapsWithUpdate.length != snapsWithUpdates.length) { - _snapsWithUpdate.clear(); - _snapsWithUpdate.addAll(snapsWithUpdates); - } - - return _snapsWithUpdate; + List _snapsWithUpdate = []; + UnmodifiableListView get snapsWithUpdate => + UnmodifiableListView(_snapsWithUpdate); + Future loadSnapsWithUpdate() async { + _snapsWithUpdate = await _snapDClient.find(filter: SnapFindFilter.refresh); } Future refreshAll({ required String doneMessage, - required List snaps, }) async { await authorize(); - for (var snap in snaps) { + if (snapsWithUpdate.isEmpty) return; + + final firstSnap = snapsWithUpdate.first; + try { + await refresh( + snap: firstSnap, + message: doneMessage, + channel: firstSnap.channel, + confinement: firstSnap.confinement, + ); + } on SnapdException catch (e) { + if (e.kind == 'auth-cancelled') { + return; + } + } + for (var snap in snapsWithUpdate.skip(1)) { await refresh( snap: snap, message: doneMessage, diff --git a/lib/snapx.dart b/lib/snapx.dart index 219e38985..c3db0b9c5 100644 --- a/lib/snapx.dart +++ b/lib/snapx.dart @@ -15,7 +15,10 @@ * */ +import 'dart:io'; + import 'package:collection/collection.dart'; +import 'package:intl/intl.dart'; import 'package:snapd/snapd.dart'; extension SnapX on Snap { @@ -26,4 +29,20 @@ extension SnapX on Snap { media.where((m) => m.type == 'screenshot').map((m) => m.url).toList(); bool get verified => publisher?.validation == 'verified'; bool get starredDeveloper => publisher?.validation == 'starred'; + bool get strict => confinement == SnapConfinement.strict; + + /// The localizedDate this snap was installed. + String get localizedDate { + if (installDate == null) return ''; + return DateFormat.yMMMd(Platform.localeName).format(installDate!); + } + + /// ISO normed [localizedDate] + String get installDateIsoNorm { + if (installDate == null) return ''; + + return DateFormat.yMd(Platform.localeName) + .add_jms() + .format(installDate!.toLocal()); + } } diff --git a/lib/theme_mode_x.dart b/lib/theme_mode_x.dart new file mode 100644 index 000000000..84dfa9583 --- /dev/null +++ b/lib/theme_mode_x.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; +import 'package:software/l10n/l10n.dart'; + +extension ThemeModeX on ThemeMode { + String localize(AppLocalizations l10n) => switch (this) { + ThemeMode.system => l10n.system, + ThemeMode.dark => l10n.dark, + ThemeMode.light => l10n.light + }; +} diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index c0c4a0c9a..2bf1139ae 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -6,6 +6,8 @@ set(APPLICATION_ID "io.snapcraft.Store") cmake_policy(SET CMP0063 NEW) +set(USE_LIBHANDY ON) + set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") # Configure build options. diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 77d364ef6..bd6b2f2ec 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -14,6 +14,7 @@ #include #include #include +#include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = @@ -40,4 +41,7 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) xdg_icons_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "XdgIconsPlugin"); xdg_icons_plugin_register_with_registrar(xdg_icons_registrar); + g_autoptr(FlPluginRegistrar) yaru_window_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "YaruWindowLinuxPlugin"); + yaru_window_linux_plugin_register_with_registrar(yaru_window_linux_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index f0a584bc8..0348d3fa6 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -11,6 +11,7 @@ list(APPEND FLUTTER_PLUGIN_LIST url_launcher_linux window_manager xdg_icons + yaru_window_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/linux/my_application.cc b/linux/my_application.cc index e04849d21..9302d5877 100644 --- a/linux/my_application.cc +++ b/linux/my_application.cc @@ -5,6 +5,8 @@ #include #endif +#include + #include "flutter/generated_plugin_registrant.h" struct _MyApplication { @@ -29,54 +31,26 @@ static void my_application_activate(GApplication* application) { } #endif - 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, "Ubuntu Software"); - 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, "Ubuntu Software Store"); - } + GtkWindow* window = GTK_WINDOW(hdy_application_window_new()); + gtk_window_set_application(window, GTK_APPLICATION(application)); GdkGeometry geometry_min; geometry_min.min_width = 660; geometry_min.min_height = 600; gtk_window_set_geometry_hints(window, nullptr, &geometry_min, GDK_HINT_MIN_SIZE); - gtk_window_set_default_size(window, 860, 860); + 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_show(GTK_WIDGET(window)); - gtk_widget_show(GTK_WIDGET(view)); + gtk_widget_grab_focus(GTK_WIDGET(view)); } @@ -90,6 +64,9 @@ static gint my_application_command_line(GApplication *application, GApplicationC g_warning("Failed to register: %s", error->message); return 1; } + + hdy_init(); + g_application_activate(application); return 0; } diff --git a/pubspec.yaml b/pubspec.yaml index 3328a4d39..585fbe7d7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,16 +4,17 @@ description: The Ubuntu Software Store made with Flutter. publish_to: "none" -version: 0.2.7-alpha +version: 0.3.0-alpha environment: - sdk: ">=2.17.0 <3.0.0" - flutter: ">=3.0.0" + sdk: '>=3.0.0 <4.0.0' + flutter: '>=3.10.0' dependencies: appstream: ^0.2.8 async: ^2.10.0 badges: ^2.0.3 + cached_network_image: ^3.2.3 collection: ^1.16.0 connectivity_plus: ^2.3.1 crypto: ^3.0.2 @@ -32,38 +33,36 @@ dependencies: flutter_svg: ^1.1.0 glib: 0.0.1 gtk_application: ^0.0.3 - handy_window: ^0.2.0 - intl: ^0.17.0 + handy_window: ^0.3.0 + intl: ^0.18.0 launcher_entry: ^0.1.0 - liquid_progress_indicator: ^0.4.0 - meta: ^1.7.0 - odrs: + liquid_progress_indicator: git: - url: https://github.com/ubuntu-flutter-community/odrs.dart - package_info_plus: ^1.4.2 - packagekit: ^0.2.4 + url: https://github.com/ubuntu-flutter-community/liquid_progress_indicator + ref: fdeb8de + meta: ^1.7.0 + odrs: ^0.0.1 + package_info_plus: ^4.0.0 + packagekit: ^0.2.6 palette_generator: ^0.3.3 + path: ^1.8.2 provider: ^6.0.2 quiver: ^3.1.0 safe_change_notifier: ^0.2.0 shimmer: ^2.0.0 - snapd: ^0.4.6 + snapcraft_launcher: ^0.1.0 + snapd: 0.4.8 snowball_stemmer: ^0.1.0 synchronized: ^3.0.0+3 ubuntu_service: ^0.2.0 ubuntu_session: ^0.0.2 - ubuntu_widgets: - git: - url: https://github.com/canonical/ubuntu-flutter-plugins - path: packages/ubuntu_widgets url_launcher: ^6.1.2 version: ^3.0.2 - window_manager: 0.3.0 xdg_icons: ^0.0.1 - yaru: ^0.5.1 - yaru_colors: ^0.1.1 - yaru_icons: ^1.0.2 - yaru_widgets: ^2.0.1 + yaru: ^0.5.5 + yaru_colors: ^0.1.6 + yaru_icons: ^1.0.3 + yaru_widgets: ^2.1.1 dev_dependencies: build_runner: ^2.2.0 @@ -73,7 +72,6 @@ dev_dependencies: integration_test: sdk: flutter mocktail: ^0.3.0 - path: ^1.8.0 flutter: uses-material-design: true diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 12bd66fb5..7beac39f0 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -19,7 +19,7 @@ assumes: parts: flutter-git: source: https://github.com/flutter/flutter.git - source-tag: 3.3.10 + source-tag: 3.10.0 plugin: nil override-build: | set -eux @@ -29,8 +29,6 @@ parts: ln -sf $CRAFT_PART_INSTALL/usr/libexec/flutter/bin/flutter $CRAFT_PART_INSTALL/usr/bin/flutter export PATH="$CRAFT_PART_INSTALL/usr/bin:$PATH" flutter doctor - flutter channel stable - flutter upgrade build-packages: - clang - cmake @@ -63,6 +61,7 @@ apps: plugs: - appstream-metadata - desktop + - desktop-launch - desktop-legacy - network - network-manager diff --git a/test/services/package_model_test.dart b/test/services/package_model_test.dart index b45fddc83..9f03ee9ec 100644 --- a/test/services/package_model_test.dart +++ b/test/services/package_model_test.dart @@ -22,7 +22,7 @@ void main() { when(service.cancelCurrentUpdatesRefresh).thenAnswer((_) async {}); when(() => service.getDetails(model: any(named: 'model'))) .thenAnswer((_) async {}); - when(() => service.getDependencies(model: any(named: 'model'))) + when(() => service.getMissingDependencies(model: any(named: 'model'))) .thenAnswer((_) async {}); when(() => service.getUpdateDetail(model: any(named: 'model'))) .thenAnswer((_) async {}); @@ -57,11 +57,12 @@ void main() { test('init model', () async { var model = PackageModel(service: service, packageId: firefoxPackageId); + model.isInstalled = false; await model.init(); verify(() => service.cancelCurrentUpdatesRefresh()).called(1); verify(() => service.getDetails(model: model)).called(1); verify(() => service.isInstalled(model: model)).called(1); - verify(() => service.getDependencies(model: model)).called(1); + verify(() => service.getMissingDependencies(model: model)).called(1); verifyNever(() => service.getUpdateDetail(model: model)); verifyNever(() => service.getDetailsAboutLocalPackage(model: model)); @@ -78,23 +79,10 @@ void main() { await model.init(); verify(() => service.cancelCurrentUpdatesRefresh()).called(1); verify(() => service.getDetailsAboutLocalPackage(model: model)).called(1); - verify(() => service.isInstalled(model: model)).called(1); verifyNever(() => service.getDetails(model: model)); verifyNever(() => service.getUpdateDetail(model: model)); }); - test('update percentage', () async { - final model = PackageModel(service: service, packageId: firefoxPackageId); - - model.isInstalled = true; - await model.init(); - expect(model.percentage, 100); - - model.isInstalled = false; - await model.init(); - expect(model.percentage, 0); - }); - test('install', () async { var model = PackageModel(service: service, packageId: firefoxPackageId); await model.install(); diff --git a/test/services/package_service_test.dart b/test/services/package_service_test.dart index 4e8727f2b..2955fdc27 100644 --- a/test/services/package_service_test.dart +++ b/test/services/package_service_test.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; +import 'package:dbus/dbus.dart'; import 'package:desktop_notifications/desktop_notifications.dart'; import 'package:file/memory.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -20,9 +21,12 @@ class MockPackageKitClient extends Mock implements PackageKitClient {} class MockPackageKitTransaction extends Mock implements PackageKitTransaction {} +class MockDBusClient extends Mock implements DBusClient {} + void main() { late MockPackageKitClient mockPKClient; late MockNotificationsClient mockNotificationsClient; + late MockDBusClient mockDBusClient; late MemoryFileSystem testFS; @@ -133,8 +137,7 @@ void main() { }); when( - () => transaction - .searchNames(['firefox'], filter: {PackageKitFilter.installed}), + () => transaction.searchNames(['firefox']), ).thenAnswer((_) { controller.add( const PackageKitPackageEvent( @@ -190,14 +193,14 @@ void main() { const PackageKitItemProgressEvent( packageId: firefoxPackageId, status: PackageKitStatus.remove, - percentage: 27, + percentage: 33, ), ); controller.add( const PackageKitItemProgressEvent( packageId: firefoxPackageId, status: PackageKitStatus.remove, - percentage: 72, + percentage: 67, ), ); controller.add( @@ -237,6 +240,11 @@ void main() { return emitFinishedEvent(controller); }); + var percentages = [33, 67, 100]; + when(() => transaction.percentage) + .thenAnswer((_) => percentages.removeAt(0)); + when(() => transaction.downloadSizeRemaining).thenReturn(0); + return transaction; } @@ -284,6 +292,23 @@ void main() { hints: any(named: 'hints'), ), ).thenAnswer((_) async => MockNotification()); + + mockDBusClient = MockDBusClient(); + when( + () => mockDBusClient.callMethod( + path: DBusObjectPath('/org/freedesktop/DBus'), + name: 'StartServiceByName', + destination: 'org.freedesktop.DBus', + interface: 'org.freedesktop.DBus', + values: const [DBusString('org.freedesktop.PackageKit'), DBusUint32(0)], + allowInteractiveAuthorization: + any(named: 'allowInteractiveAuthorization'), + noReplyExpected: false, + noAutoStart: false, + replySignature: null, + ), + ).thenAnswer((_) async => DBusMethodSuccessResponse()); + when(mockDBusClient.close).thenAnswer((_) async {}); }); tearDown(() { @@ -291,14 +316,15 @@ void main() { unregisterMockService(); }); - test('instantiate service', () { - PackageService(); + test('instantiate service', () async { + final service = PackageService(mockDBusClient); + await service.initialized; verify(mockPKClient.connect).called(1); }); test('init', () async { - final service = PackageService(); + final service = PackageService(mockDBusClient); expect(service.isAvailable, isFalse); @@ -310,7 +336,7 @@ void main() { }); test('no updates', () async { - final service = PackageService(); + final service = PackageService(mockDBusClient); expect(service.updates, isEmpty); expectLater( @@ -326,7 +352,7 @@ void main() { }); test('get details', () async { - final service = PackageService(); + final service = PackageService(mockDBusClient); final model = createPackageModel( service: service, packageId: firefoxPackageId, @@ -342,7 +368,7 @@ void main() { }); test('is installed', () async { - final service = PackageService(); + final service = PackageService(mockDBusClient); final model = createPackageModel( service: service, packageId: firefoxPackageId, @@ -366,7 +392,7 @@ void main() { }); test('toggle repo', () async { - final service = PackageService(); + final service = PackageService(mockDBusClient); expectLater(service.reposChanged, emitsInOrder([true, true])); @@ -375,7 +401,7 @@ void main() { }); test('send update notification', () async { - final service = PackageService(); + final service = PackageService(mockDBusClient); const body = 'ho ho ho'; expect(service.lastUpdatesState, isNull); @@ -404,12 +430,12 @@ void main() { }); test('resolve package id', () async { - final service = PackageService(); + final service = PackageService(mockDBusClient); expect(await service.resolve('firefox'), firefoxPackageId); }); test('get details about local package', () async { - final service = PackageService(); + final service = PackageService(mockDBusClient); final tempFile = await createTempFile(); final model = createPackageModel(service: service, path: tempFile.path); @@ -427,7 +453,7 @@ void main() { }); test('install package', () async { - final service = PackageService(); + final service = PackageService(mockDBusClient); final model = createPackageModel( service: service, packageId: firefoxPackageId, @@ -443,26 +469,27 @@ void main() { } }); + expectLater(service.installedPackagesChanged, emits(true)); await service.install(model: model); expect(model.info, PackageKitInfo.installing); expect(model.isInstalled, isTrue); expect(packageStates, [ PackageState.ready, - PackageState.processing, + PackageState.installing, PackageState.ready, ]); expect(percentages, [0, 33, 67, 100]); + expect(service.installedPackages.contains(model.packageId), isTrue); }); test('remove package', () async { - final service = PackageService(); + final service = PackageService(mockDBusClient); final model = createPackageModel( service: service, packageId: firefoxPackageId, ); model.isInstalled = true; - model.percentage = 100; final packageStates = [model.packageState]; final percentages = [model.percentage]; @@ -474,20 +501,22 @@ void main() { } }); + expectLater(service.installedPackagesChanged, emits(true)); await service.remove(model: model); expect(model.info, PackageKitInfo.removing); expect(model.isInstalled, isFalse); expect(packageStates, [ PackageState.ready, - PackageState.processing, + PackageState.removing, PackageState.ready, ]); - expect(percentages, [100, 73, 28, 0]); + expect(percentages, [0, 33, 67, 100]); + expect(service.installedPackages.contains(model.packageId), isFalse); }); test('install local file', () async { - final service = PackageService(); + final service = PackageService(mockDBusClient); final tempFile = await createTempFile(); final model = createPackageModel(service: service, path: tempFile.path); @@ -498,6 +527,7 @@ void main() { } }); + expectLater(service.installedPackagesChanged, emits(true)); await service.installLocalFile(model: model, fileSystem: testFS); expect(model.packageId, firefoxPackageId); @@ -505,9 +535,10 @@ void main() { expect(model.isInstalled, isTrue); expect(packageStates, [ PackageState.ready, - PackageState.processing, + PackageState.installing, PackageState.ready, ]); + expect(service.installedPackages.contains(model.packageId), isTrue); }); MockPackageKitTransaction createMockUpdateTransaction() { @@ -564,6 +595,10 @@ void main() { when( () => updateTransaction.getDetails(any(that: contains(firefoxPackageId))), ).thenAnswer((_) => emitFinishedEvent(controller)); + var percentages = [13, 37]; + when(() => updateTransaction.percentage) + .thenAnswer((_) => percentages.removeAt(0)); + when(() => updateTransaction.downloadSizeRemaining).thenReturn(0); return updateTransaction; } @@ -572,7 +607,7 @@ void main() { when(mockPKClient.createTransaction) .thenAnswer((_) async => updateTransaction); - final service = PackageService(); + final service = PackageService(mockDBusClient); await service.refreshUpdates(); expect(service.updates.length, 1); @@ -588,7 +623,7 @@ void main() { when(mockPKClient.createTransaction) .thenAnswer((_) async => updateTransaction); - final service = PackageService(); + final service = PackageService(mockDBusClient); await service.refreshUpdates(); expect(service.isUpdateSelected(firefoxPackageId), isTrue); service.selectionChanged.listen(expectAsync1((_) {}, count: 3)); diff --git a/test/services/snap_service_test.dart b/test/services/snap_service_test.dart index 99237f4bf..b5a627837 100644 --- a/test/services/snap_service_test.dart +++ b/test/services/snap_service_test.dart @@ -200,8 +200,12 @@ void main() { ), ).thenAnswer((_) async => changeId); when(() => mockSnapdClient.getChange(changeId)).thenAnswer( - (_) async => - SnapdChange(id: changeId, spawnTime: DateTime.now(), ready: true), + (_) async => SnapdChange( + id: changeId, + spawnTime: DateTime.now(), + ready: true, + status: 'Done', + ), ); when(() => mockSnapdClient.getSnap(snap1.name)) .thenAnswer((_) async => snap1); @@ -235,8 +239,12 @@ void main() { when(() => mockSnapdClient.remove(snap1.name)) .thenAnswer((_) async => changeId); when(() => mockSnapdClient.getChange(changeId)).thenAnswer( - (_) async => - SnapdChange(id: changeId, spawnTime: DateTime.now(), ready: true), + (_) async => SnapdChange( + id: changeId, + spawnTime: DateTime.now(), + ready: true, + status: 'Done', + ), ); when(() => mockSnapdClient.getSnap(snap1.name)) .thenAnswer((_) async => snap1); @@ -286,8 +294,12 @@ void main() { ), ).thenAnswer((_) async => changeId); when(() => mockSnapdClient.getChange(changeId)).thenAnswer( - (_) async => - SnapdChange(id: changeId, spawnTime: DateTime.now(), ready: true), + (_) async => SnapdChange( + id: changeId, + spawnTime: DateTime.now(), + ready: true, + status: 'Done', + ), ); when(() => mockSnapdClient.getSnap(snap1.name)) .thenAnswer((_) async => snap1); @@ -472,15 +484,52 @@ void main() { when(mockSnapdClient.getSnaps).thenAnswer( (_) async => [snapWithUpdateOld, snapWithoutUpdate], ); - when(() => mockSnapdClient.find(section: any(named: 'name'))).thenAnswer( - (i) async => - i.namedArguments[const Symbol('name')] == snapWithUpdateOld.name - ? [snapWithUpdateNew] - : i.namedArguments[const Symbol('name')] == snapWithoutUpdate.name - ? [snapWithoutUpdate] - : [], - ); + when(() => mockSnapdClient.find(filter: SnapFindFilter.refresh)) + .thenAnswer((_) async => [snapWithUpdateNew]); await service.loadSnapsWithUpdate(); - expect(service.snapsWithUpdate, [snapWithUpdateOld]); + expect(service.snapsWithUpdate, [snapWithUpdateNew]); + }); + + test('abort change', () async { + const changeId = '42'; + const channel = 'latest/stable'; + var ready = false; + when( + () => mockSnapdClient.install( + snap1.name, + channel: any(named: 'channel'), + classic: any(named: 'classic'), + ), + ).thenAnswer((_) async => changeId); + when(() => mockSnapdClient.getChange(changeId)).thenAnswer( + (_) async { + return SnapdChange( + id: changeId, + spawnTime: DateTime.now(), + ready: ready, + status: ready ? 'Error' : '', + ); + }, + ); + when(() => mockSnapdClient.getSnap(snap1.name)) + .thenAnswer((_) async => snap1); + when(() => mockSnapdClient.abortChange(changeId)).thenAnswer((_) async { + ready = true; + return SnapdChange(spawnTime: DateTime.now()); + }); + + service.snapChangesInserted.listen((event) { + service.abortChange(snap1); + }); + await service.install(snap1, channel, ''); + verifyNever( + () => mockNotificationsClient.notify( + any(), + body: any(named: 'body'), + appName: snap1.name, + appIcon: 'snap-store', + hints: any(named: 'hints'), + ), + ); }); } diff --git a/test/widget_test.dart b/test/widget_test.dart index 133c4dc0d..875d92c81 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -1,16 +1,21 @@ import 'dart:async'; +import 'package:collection/collection.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:gtk_application/gtk_application.dart'; import 'package:launcher_entry/launcher_entry.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:packagekit/packagekit.dart'; +import 'package:snapd/snapd.dart'; import 'package:software/app/app.dart'; +import 'package:software/app/common/snap/snap_section.dart'; import 'package:software/services/appstream/appstream_service.dart'; import 'package:software/services/packagekit/package_service.dart'; import 'package:software/services/packagekit/updates_state.dart'; import 'package:software/services/snap_service.dart'; import 'package:ubuntu_service/ubuntu_service.dart'; +import 'package:badges/badges.dart' as badges; class MockAppstreamService extends Mock implements AppstreamService {} @@ -43,7 +48,10 @@ void main() { final packageServiceMock = MockPackageService(); registerMockService(packageServiceMock); when(packageServiceMock.init).thenAnswer((_) async {}); - when(() => packageServiceMock.updates).thenReturn([]); + when(() => packageServiceMock.initialized).thenAnswer((_) async {}); + when(() => packageServiceMock.isAvailable).thenReturn(true); + when(() => packageServiceMock.updates) + .thenReturn([const PackageKitPackageId(name: 'foo', version: '1')]); final updatesChangedController = StreamController.broadcast(); when(() => packageServiceMock.updatesChanged) .thenAnswer((_) => updatesChangedController.stream); @@ -59,7 +67,8 @@ void main() { when(snapServiceMock.init).thenAnswer((_) async {}); when(() => snapServiceMock.sectionNameToSnapsMap).thenReturn({}); - final snapSectionsChangedController = StreamController.broadcast(); + final snapSectionsChangedController = + StreamController.broadcast(); when(() => snapServiceMock.sectionsChanged) .thenAnswer((_) => snapSectionsChangedController.stream); @@ -72,6 +81,9 @@ void main() { sectionName: any(named: 'sectionName'), ), ).thenAnswer((_) async => []); + when(() => snapServiceMock.snapsWithUpdate).thenReturn( + UnmodifiableListView([const Snap(name: 'bar'), const Snap(name: 'baz')]), + ); final gtkApplicationNotifierMock = MockGtkApplicationNotifier(); registerMockService(gtkApplicationNotifierMock); @@ -82,5 +94,9 @@ void main() { when(launcherEntryServiceMock.update).thenAnswer((_) async {}); await tester.pumpWidget(App.create()); + await tester.pumpAndSettle(); + + final badge = find.widgetWithText(badges.Badge, '3'); + expect(badge, findsOneWidget); }); }