From c2a43e6bbaf087978b44ac41b142c1a413670c9e Mon Sep 17 00:00:00 2001 From: Frederik Feichtmeier Date: Sun, 4 Jun 2023 12:13:56 +0200 Subject: [PATCH] Release 0.3.0 to candidate (#1253) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * snap: use flutter 3.7.0 (#942) * Installed page - update icon/button styling (#946) Fixes #913 * Release_0.2.8-alpha (#947) * Show deb dependency sizes (#950) * add PackageDependecy class * show size of missing dependencies * Translations update from Hosted Weblate (#932) * Translated using Weblate (Spanish) Currently translated at 98.7% (154 of 156 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/es/ * Translated using Weblate (Spanish) Currently translated at 100.0% (156 of 156 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/es/ * Translated using Weblate (Russian) Currently translated at 99.3% (160 of 161 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/ru/ * Translated using Weblate (Swedish) Currently translated at 98.7% (159 of 161 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/sv/ * Translated using Weblate (Swedish) Currently translated at 98.7% (159 of 161 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/sv/ * Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (161 of 161 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/zh_Hant/ --------- Co-authored-by: gallegonovato Co-authored-by: phlostically Co-authored-by: DefaultX-od Co-authored-by: Arve Eriksson <031299870@telia.com> Co-authored-by: Luna Jernberg Co-authored-by: sashimi * make search available from App downwards (#945) Fixes #921 * PackageModel: make checkDependencies optional in init (#952) * Don't use full width on Settings page (#949) * Don't use full width on Settings page Fixes #904 * Add fake titlebar to theme tile * fix scroll behavior in dependency dialog (#955) * Settings: move to trailing & open in dialog (#953) * Settings: move to trailing & open in dialog * Navigate to repo and about in navigator * add padding * Better layout * background color * Added gear icon (#957) * SettingsPage: add license button and route, improve layout (#964) * Changed font size for app_header (#963) * Changed font size * Only use theme * Added text color (#967) * SettingsModel: clean up (#968) - make properties nullable - make const private to not show up in code completion elsewhere * LicensePage tweaks (#965) * Licensepage tweaks Small tweaks for license page: - Removed duplicate back button - Use full window height (removed SizedBox) * Added const * Wrapped in ClipRRect * AppPage Gallery dialog should resize on window size change (#972) Bonus: use YaruDialogTitlebar Fixes #971 * AppPage Gallery Dialog: use appData.name as title (#973) * SnapUpdates: refresh all should not ask for password before every update (#970) * SnapUpdates: refresh all should not ask for password before every update Fixes #969 * check if list is empty * if (_snapsWithUpdates?.isEmpty ?? true) return; * Merge updates and installed pages (#974) * merge updates and installed Fixes #960 * make appformat popup useable * use context select and valuekeys * Select snap list * Add package updates * Improve package updates design * l10n, and add filter popup for packagekit * HIde searchbar for deb updates * specify when to hide searchbar * Remove collection item * remove import * Move name filtering to model * filter packages by searchQuery * fix NPE * init the packagekit filter * if (searchQuery?.isEmpty ?? true) :) * Remove unused code * Add remove button deb tiles, select packagekit first if available * Translations update from Hosted Weblate (#961) * Translated using Weblate (Spanish) Currently translated at 100.0% (161 of 161 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/es/ * Translated using Weblate (Occitan) Currently translated at 100.0% (161 of 161 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/oc/ * Translated using Weblate (Spanish) Currently translated at 100.0% (163 of 163 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/es/ --------- Co-authored-by: gallegonovato Co-authored-by: Quentin PAGÈS * Shortcuts (#956) * Add shortcuts to sections in the sidebar * Special case games page * Fix package installer index * AppImageBanner: add icon * SectionBanner: switch between dark and light text * set selection section on page change, adjust section banner height to avoid jumping * change art and design gradient * Collection: sort snaps by update and SnapSort (#975) * Collection: sort snaps by update and SnapSort * No need for maps * Header: 20 pixels runspacing at line overflow * PackageService: remove filter in isInstalled (#979) uninstalled packages need to be included in the search to correctly determine whether a package is installed * PackageService: fix isInstalled (#981) * Update list of installed packages upon installation/removal (#983) * PackageService: update _installedPackages * PackageService: emit signal after installing/removing * update package service tests * Collection: fix tile shape and divider (#985) based on list length and index * CollectionPage: re-add deb controls and improve layout (#989) - re-add all debian package related controls (refresh, update all) - adapt the deb updates section to snap updates - adapt deb updates list to installed deb list - move PackageUpdatesModel higher in the tree to a multiprovider around collectionpage - do not always ask packageService for a fresh list of updates on page refresh, stream subs should update the page here - do not show all debian packages but only PackageKitFilter.installed, PackageKitFilter.gui, PackageKitFilter.newest, PackageKitFilter.application, PackageKitFilter.notSource, PackageKitFilter.notDevelopment, * Hide disabled Open button for snaps (#990) * Hide open disabled - Hide open disabled - Changed order so Remove is always right * Hide Open for snaps when disabled * Translations update from Hosted Weblate (#984) * Translated using Weblate (Spanish) Currently translated at 100.0% (165 of 165 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/es/ --------- Co-authored-by: gallegonovato * make sure isInstalled doesn't get reset if another version is found (#992) * fixing a mess (#995) Co-authored-by: MadsRH * UpdateBanner: rename to PackageUpdateBanner (#993) * Packagekit progress indicator (#944) * PackageControls: access PackageModel via Provider * update packagekit * add new localization strings * add more PackageStates * PackageModel: expose transaction progress * PackageControls: show progress, remaining DL size * update tests * show transaction perentage in updateAll * CollectionPage: remove PackageControls parameters * CollectionPage: set isInstalled in model * CollectionModel: make _installedSnaps null at start (#997) * CollectionModel: make _installedSnaps null at start Fixes #994 * bool get snapUpdatesAvailable => installedSnapsWithUpdates?.isNotEmpty ?? false; * remove loadSnaps duplicate * remove duplicate getter * CollectionModel: remove unecessary list clearing (#999) * rebuild package tiles when installed packages change (#1001) * rebuild package tiles when installed packages change * remove unused key * PackageService: reduce signals in getInstalledPackages (#1002) only send one packages changed signal after the transaction completes, instead of sending one for each packagekit event Co-authored-by: Frederik Feichtmeier * CollectionPage: improve performance (#1004) * CollectionPage: improve performance - service: use a second list of IDs only for checking the installedId vs updateId versions so the installed list does get overwritten when checking for updates - collectionpage: replace columns with many elements with listview builders, this needs a second change with scroll controllers and lazy loading in the future * physics: const NeverScrollableScrollPhysics(), * add optional parameter to getInstalledPackages * App: add more shortcut pageItems (#1000) * App: add more shortcut pageItems - iot - server and cloud * fix _commandLineListener * Add productivty, remove server + iot * helper method and paperplane icon * `StartPage`: show appstream data as soon as possible (#1006) * SnapService: pass section name in sectionsChanged * ExploreModel: add unified startPageApps property * CollectionPage: improve icon and design and fix bugs (#1010) * CollectionPage: improve icon and design * Sort children and fix busy bug * keep header with checkbox * fix package tile padding * enable snap list after check, disable color for update button, remove snap updates from normal snap list * Changed Offline text color to grey (#1013) * Changed text color to grey * Update offline_page.dart * Fix overflow error in review/ratings (#1017) * fix overflow error in review/ratings * remove unused sizedBox * [Reviews/Rating] expand inkwell to include blank space (#1018) * [Reviews/Rating] expand inkwell to include blank space * empty commit for test failure? * show all items in Additional Information section (#1020) * remove extra bottom padding in review dialog (#1021) * remove awkward review carousel cutoff (#1022) * CollectionPage: show to top button (#1024) * CollectionPage: show to top button * dispose controller * only show the button for packagekit for now * animate * Collection page: show badge on app format selector (#1027) Fixes #1026 * Translations update from Hosted Weblate (#1028) * Added translation using Weblate (Ukrainian) * Translated using Weblate (German) Currently translated at 100.0% (169 of 169 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/de/ * Translated using Weblate (Spanish) Currently translated at 100.0% (169 of 169 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/es/ * Translated using Weblate (French) Currently translated at 100.0% (169 of 169 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/fr/ * Translated using Weblate (Russian) Currently translated at 100.0% (169 of 169 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/ru/ * Translated using Weblate (Swedish) Currently translated at 100.0% (169 of 169 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/sv/ * Translated using Weblate (Esperanto) Currently translated at 100.0% (169 of 169 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/eo/ * Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (169 of 169 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/zh_Hant/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (169 of 169 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/uk/ --------- Co-authored-by: Макс Лисенко Co-authored-by: Ettore Atalan Co-authored-by: gallegonovato Co-authored-by: phlostically Co-authored-by: Luna Jernberg Co-authored-by: sashimi * Pubspec: Upgrade handy_window (#1029) Fixes #1023 * CollectionPage: organize (#1031) * CollectionPage: organize Fixes #1025 * re-wrap snap collection in the scrollview to show the FAB * Align FAB bottom right without stack * software_test: increase timeout window * Release 0.2.9-alpha (#1034) * Package updating page: add padding (#1039) * Remove unnecessary toolips (#1038) * Remove unnecessary toolips - removed unnecessary toolips - added a length threshold for Version and Publisher * Added length to license tooltip * Package type visualization improvements (#1040) Fixes #1036 * Added padding (#1045) * Only show date for debs (#1046) * Bring section banner closer to design (#1044) * Bring section banner closer to design * Improve spacing * PackagePage: set appIsInstalled in AppPage (#1048) * ExploreModel: enable packageKit before loading StartPage components (#1050) * Correctly look for snap updates (#1051) Fixes #1042 * Remove the package updating details (#1052) Fixes #1041 Co-authored-by: Dennis Loose * exempt common bots from CLA (#1053) * Revert "exempt common bots from CLA (#1053)" (#1055) This reverts commit fe060388bc8bced4d9432cabb4f65d595ca4021c. * SnapPage: add a share button (#1056) Fixes #1047 * Tooltips for publisher icons (#1059) * Added tooltip for icons * Added string for Star developer * Add tooltip for share icon (#1057) * Switch to yaru_window (#1063) * AppBanner: better publishername guessing (#1064) Fixes #986 * Fix Title alignment (#1065) Fixes #895 * Use HdyWindow directly from libhandy (#978) Probably fixes: #868 * Fix: Can't switch snap tracking channel without reinstalling (#1071) Fixes #1069 * Use snapd.dart to find snaps with updates (#1070) * SnapService: fetch refreshable snaps from snapd * SnapService: make localSnaps read-only * snap_utils: remove isSnapUpdateAvailable * update snap service test * Find unlisted snaps by exact name (#1077) * fix null check (#1084) * Remove pubspec_overrides: no longer needed with yaru_window (#1085) * Make publisher name click search for publisher (#1078) * Make publisher name click search for publisher Fixes #1016 * Update lib/app/explore/explore_page.dart Co-authored-by: Dennis Loose * Update lib/app/common/app_page/app_page.dart Co-authored-by: Dennis Loose --------- Co-authored-by: Dennis Loose * added ellipsis and overflow (#1087) * Smaller fontsize for YaruExpanders (#1089) * Removed scaledFontSize (#1091) Removed scaledFontSize and set titleLarge at size 23 * Snap channel dropdown text styling (#1093) * fix snapd.dart git ref until PR is merged (#1097) * AppModel: return sum of snap and deb updates (#1102) * AppModel: return sum of snap and deb updates * test update badge * ExplorePage: reset searchQuery in initState (#1101) Co-authored-by: Frederik Feichtmeier * Changed sizebox height (#1100) * Changed sizebox height * use localization string --------- Co-authored-by: Dennis Loose Co-authored-by: Frederik Feichtmeier * update snapd.dart (#1104) * Removed isExpanded for text color (#1095) * Use primary color for links (#1094) * Use primary color for links * Use primary color for links * Removed blue contributor links * Alert dialog for removing snap (#1110) * Add dialog * Added translation strings * trailing comma * typo * UpdateDialog: Added width to container (#1115) fixes expand sections to full width #1114 * SectionBanner Font tweaks (#1112) removed transparency, changed fontweight * @Zoospora gradients for sectionbanners (#1111) * SnapPage: use outline for snapchannel button (#1117) * Update from yaru (#1108) * Update from yaru - stronger borders - stronger button outlines * update yaru to 0.5.5 * Implement Ratings and Reviews redesign (#1119) * Review styling (#1121) * New translations strings * Styling fixes * ReviewRatingBar: simplify alignment and detachment (#1122) * Replace Collection with Manage (#1124) * Added Manage string * Replaced collection with manage * Text aligned left (#1127) * Shorter translation for Package type (#1123) * New translation for package type * New translation string * appformat toggle button fontweight (#1129) * Add isSelected styling * add isSelected * Cancel `refreshAll` if first PolKit authentication request is cancelled (#1130) * SnapService: rethrow auth-cancelled exception * SnapService: cancel refreshAll on first auth-cancelled * CollectionModel: use refreshAll from SnapService * Update cla-check.yaml * Attempt to fix CLA (#1132) * Attempt to fix CLA * PackagePage: show dependency sizes (#1133) * PackagePage: show dependency sizes * add dependency package summary * fix integration test * Fix the CLA check - attempt 7 (#1134) Co-authored-by: Frederik Feichtmeier * Add summary to dialog (#1137) * pass AppFindings to SectionBanner (#1140) * Allow cancelling ongoing snap changes (#1139) * SnapService: add abortChange * SnapService: notify only if change is 'Done' * add abortChange to snap models * add cancel button to snap controls * update snap service test * remove unneeded removeChange in abortChange * Dependency removal (#1138) * bump packagekit version * update localization strings * add autoremove parameter to remove() * ensure isInstalled is set after model is initialized * get deps for installed packages that can be removed * add dependency dialog for package removal * update test * check dependencies after install/remove * add missing trailing comma * set isInstalled to false for local debs * UpdatePage: switch appformat dropdown to toggle button (#1146) Fixes #1080 * use desktop launch interface (#1141) * add snapcraft_launcher to pubspec * use PrivilegedDesktopLauncher to launch snaps * add desktop-launch plug to snapcraft.yaml * ExplorePage: detangle and prepare for more pages (#1149) * ExplorePage: detangle and prepare for more pages - split startpage into GenericStartPage, ExploreAllPage and GamesStartPage - remove parameters of GamesStartPage * Move loading into banner and grid * Linkcolor blue - again (#1150) * More readable publisherName for app_banners (#1151) * Changed textstyle for publisherName * Removed italic * bodyMedium font in app format dropdown (#1152) * removed unnecessary sized box (#1157) * Remove themeTile artifacts (#1160) * remove theme tiel artifacts * revert border radius change * fix swipe animation not resetting (#1162) * Use yMMMd format for ReleasedAt (#1163) * fix(UI): explore page snaps have lower case letters (#1167) * Additional Information fixes (#1159) * Additional Information fixes - use horizontal 20 pixel spacing - move the icon to the start - publisherName.replaceAll(' ', '\u00A0') * Icon, right, tooltip on hover * Issue1173 - review divider (#1180) * Thinner divider for reviews * Revert height * Use same height as master * Fixed more dividers * Use divider with padding * Changes searchfield font size (#1155) * Changes searchfield font size * Revert searchfield height --------- Co-authored-by: Frederik Feichtmeier * show installed apps when installing snap (#1166) Co-authored-by: Frederik Feichtmeier * feat(UI): add total ratings along with reviews (#1187) * fix(UI): explore page snaps have lower case letters * feat(UI): add total ratings along with reviews * Make collection page 📄 lazy 🦥 load its packages (#1188) Fixes #1009 * Review improvements (#1189) * Text fixes for review dialog * New string for review dialog Related to issue 1186 * Removed unused package * Make search page 📃 lazy 🦥 loading its list (#1191) Fixes #1007 * feat(UI): allow only full star rating (#1205) * Translation strings (#1206) * fix(integration_test): set netplan renderer (#1226) somehow `netplan set renderer=NetworkManager` doesn't work any longer. using a configuration file with the renderer set to NM seems to do the trick for now. * Translations update from Hosted Weblate (#1220) * Added translation using Weblate (Ukrainian) * Translated using Weblate (German) Currently translated at 100.0% (169 of 169 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/de/ * Translated using Weblate (Spanish) Currently translated at 100.0% (169 of 169 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/es/ * Translated using Weblate (French) Currently translated at 100.0% (169 of 169 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/fr/ * Translated using Weblate (Russian) Currently translated at 100.0% (169 of 169 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/ru/ * Translated using Weblate (Swedish) Currently translated at 100.0% (169 of 169 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/sv/ * Translated using Weblate (Esperanto) Currently translated at 100.0% (169 of 169 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/eo/ * Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (169 of 169 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/zh_Hant/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (169 of 169 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/uk/ * Update translation files Updated by "Cleanup translation files" hook in Weblate. Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/ * Translated using Weblate (German) Currently translated at 99.4% (194 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/de/ * Translated using Weblate (Spanish) Currently translated at 100.0% (195 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/es/ * Translated using Weblate (Swedish) Currently translated at 99.4% (194 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/sv/ * Translated using Weblate (Esperanto) Currently translated at 98.4% (192 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/eo/ * Translated using Weblate (German) Currently translated at 100.0% (195 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/de/ * Translated using Weblate (French) Currently translated at 95.3% (186 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/fr/ * Translated using Weblate (Italian) Currently translated at 100.0% (195 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/it/ * Translated using Weblate (Esperanto) Currently translated at 100.0% (195 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/eo/ * Translated using Weblate (Swedish) Currently translated at 100.0% (195 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/sv/ * Translated using Weblate (Chinese (Simplified)) Currently translated at 2.5% (5 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/zh_Hans/ * Added translation using Weblate (Czech) * Translated using Weblate (Czech) Currently translated at 100.0% (195 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/cs/ --------- Co-authored-by: Макс Лисенко Co-authored-by: Ettore Atalan Co-authored-by: gallegonovato Co-authored-by: phlostically Co-authored-by: Luna Jernberg Co-authored-by: sashimi Co-authored-by: bittin1ddc447d824349b2 Co-authored-by: albanobattistella Co-authored-by: Lattefang <370358679@qq.com> Co-authored-by: AsciiWolf * ci: pin flutter version to 3.7 (#1229) * Translations update from Hosted Weblate (#1227) * Added translation using Weblate (Ukrainian) * Translated using Weblate (German) Currently translated at 100.0% (169 of 169 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/de/ * Translated using Weblate (Spanish) Currently translated at 100.0% (169 of 169 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/es/ * Translated using Weblate (French) Currently translated at 100.0% (169 of 169 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/fr/ * Translated using Weblate (Russian) Currently translated at 100.0% (169 of 169 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/ru/ * Translated using Weblate (Swedish) Currently translated at 100.0% (169 of 169 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/sv/ * Translated using Weblate (Esperanto) Currently translated at 100.0% (169 of 169 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/eo/ * Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (169 of 169 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/zh_Hant/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (169 of 169 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/uk/ * Update translation files Updated by "Cleanup translation files" hook in Weblate. Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/ * Translated using Weblate (German) Currently translated at 99.4% (194 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/de/ * Translated using Weblate (Spanish) Currently translated at 100.0% (195 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/es/ * Translated using Weblate (Swedish) Currently translated at 99.4% (194 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/sv/ * Translated using Weblate (Esperanto) Currently translated at 98.4% (192 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/eo/ * Translated using Weblate (German) Currently translated at 100.0% (195 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/de/ * Translated using Weblate (French) Currently translated at 95.3% (186 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/fr/ * Translated using Weblate (Italian) Currently translated at 100.0% (195 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/it/ * Translated using Weblate (Esperanto) Currently translated at 100.0% (195 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/eo/ * Translated using Weblate (Swedish) Currently translated at 100.0% (195 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/sv/ * Translated using Weblate (Chinese (Simplified)) Currently translated at 2.5% (5 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/zh_Hans/ * Added translation using Weblate (Czech) * Translated using Weblate (Czech) Currently translated at 100.0% (195 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/cs/ * Added translation using Weblate (Slovak) * Translated using Weblate (Chinese (Simplified)) Currently translated at 99.4% (194 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/zh_Hans/ * Translated using Weblate (Slovak) Currently translated at 100.0% (195 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/sk/ * Translated using Weblate (Korean) Currently translated at 45.6% (89 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/ko/ * Translated using Weblate (Slovak) Currently translated at 100.0% (195 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/sk/ * Translated using Weblate (Slovak) Currently translated at 100.0% (195 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/sk/ --------- Co-authored-by: Макс Лисенко Co-authored-by: Ettore Atalan Co-authored-by: gallegonovato Co-authored-by: phlostically Co-authored-by: Luna Jernberg Co-authored-by: sashimi Co-authored-by: bittin1ddc447d824349b2 Co-authored-by: albanobattistella Co-authored-by: Lattefang <370358679@qq.com> Co-authored-by: AsciiWolf Co-authored-by: Peter Vančo Co-authored-by: Fan Chou Co-authored-by: El * Upgrade to dart 3 and flutter 3.10 (#1232) * Upgrade to dart 3 and flutter 3.10 * Translations update from Hosted Weblate (#1230) * Added translation using Weblate (Ukrainian) * Translated using Weblate (German) Currently translated at 100.0% (169 of 169 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/de/ * Translated using Weblate (Spanish) Currently translated at 100.0% (169 of 169 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/es/ * Translated using Weblate (French) Currently translated at 100.0% (169 of 169 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/fr/ * Translated using Weblate (Russian) Currently translated at 100.0% (169 of 169 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/ru/ * Translated using Weblate (Swedish) Currently translated at 100.0% (169 of 169 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/sv/ * Translated using Weblate (Esperanto) Currently translated at 100.0% (169 of 169 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/eo/ * Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (169 of 169 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/zh_Hant/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (169 of 169 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/uk/ * Update translation files Updated by "Cleanup translation files" hook in Weblate. Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/ * Translated using Weblate (German) Currently translated at 99.4% (194 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/de/ * Translated using Weblate (Spanish) Currently translated at 100.0% (195 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/es/ * Translated using Weblate (Swedish) Currently translated at 99.4% (194 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/sv/ * Translated using Weblate (Esperanto) Currently translated at 98.4% (192 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/eo/ * Translated using Weblate (German) Currently translated at 100.0% (195 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/de/ * Translated using Weblate (French) Currently translated at 95.3% (186 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/fr/ * Translated using Weblate (Italian) Currently translated at 100.0% (195 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/it/ * Translated using Weblate (Esperanto) Currently translated at 100.0% (195 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/eo/ * Translated using Weblate (Swedish) Currently translated at 100.0% (195 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/sv/ * Translated using Weblate (Chinese (Simplified)) Currently translated at 2.5% (5 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/zh_Hans/ * Added translation using Weblate (Czech) * Translated using Weblate (Czech) Currently translated at 100.0% (195 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/cs/ * Added translation using Weblate (Slovak) * Translated using Weblate (Chinese (Simplified)) Currently translated at 99.4% (194 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/zh_Hans/ * Translated using Weblate (Slovak) Currently translated at 100.0% (195 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/sk/ * Translated using Weblate (Korean) Currently translated at 45.6% (89 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/ko/ * Translated using Weblate (Slovak) Currently translated at 100.0% (195 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/sk/ * Translated using Weblate (Slovak) Currently translated at 100.0% (195 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/sk/ * Translated using Weblate (Danish) Currently translated at 46.6% (91 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/da/ * Translated using Weblate (Persian) Currently translated at 37.4% (73 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/fa/ * Translated using Weblate (Finnish) Currently translated at 50.2% (98 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/fi/ * Translated using Weblate (Indonesian) Currently translated at 36.4% (71 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/id/ * Translated using Weblate (Japanese) Currently translated at 81.0% (158 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/ja/ * Translated using Weblate (Portuguese) Currently translated at 81.0% (158 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/pt/ * Translated using Weblate (Russian) Currently translated at 86.1% (168 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/ru/ * Translated using Weblate (Occitan) Currently translated at 82.0% (160 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/oc/ * Translated using Weblate (Korean) Currently translated at 45.6% (89 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/ko/ * Translated using Weblate (Polish) Currently translated at 52.8% (103 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/pl/ * Translated using Weblate (Chinese (Traditional)) Currently translated at 86.1% (168 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/zh_Hant/ * Translated using Weblate (Chinese (Simplified)) Currently translated at 99.4% (194 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/zh_Hans/ * Translated using Weblate (Turkish) Currently translated at 27.6% (54 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/tr/ * Translated using Weblate (Ukrainian) Currently translated at 86.1% (168 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/uk/ * Translated using Weblate (Slovak) Currently translated at 100.0% (195 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/sk/ * Translated using Weblate (Finnish) Currently translated at 90.2% (176 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/fi/ * Translated using Weblate (Slovak) Currently translated at 100.0% (195 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/sk/ --------- Co-authored-by: Макс Лисенко Co-authored-by: Ettore Atalan Co-authored-by: gallegonovato Co-authored-by: phlostically Co-authored-by: Luna Jernberg Co-authored-by: sashimi Co-authored-by: bittin1ddc447d824349b2 Co-authored-by: albanobattistella Co-authored-by: Lattefang <370358679@qq.com> Co-authored-by: AsciiWolf Co-authored-by: Peter Vančo Co-authored-by: Fan Chou Co-authored-by: El Co-authored-by: Jiri Grönroos * Use the new switch expression (#1234) * SettingsPage: simplify theme switch logic (#1235) as suggested in https://github.com/ubuntu-flutter-community/software/pull/1234#discussion_r1197691654 by @jpnurmi * fix(packagekit): ensure packagekit daemon is active (#1238) * fix(packagekit): ensure packagekit daemon is active * test(packagekit): mock DBusClient * Translations update from Hosted Weblate (#1233) * Added translation using Weblate (Ukrainian) * Translated using Weblate (German) Currently translated at 100.0% (169 of 169 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/de/ * Translated using Weblate (Spanish) Currently translated at 100.0% (169 of 169 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/es/ * Translated using Weblate (French) Currently translated at 100.0% (169 of 169 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/fr/ * Translated using Weblate (Russian) Currently translated at 100.0% (169 of 169 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/ru/ * Translated using Weblate (Swedish) Currently translated at 100.0% (169 of 169 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/sv/ * Translated using Weblate (Esperanto) Currently translated at 100.0% (169 of 169 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/eo/ * Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (169 of 169 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/zh_Hant/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (169 of 169 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/uk/ * Update translation files Updated by "Cleanup translation files" hook in Weblate. Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/ * Translated using Weblate (German) Currently translated at 99.4% (194 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/de/ * Translated using Weblate (Spanish) Currently translated at 100.0% (195 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/es/ * Translated using Weblate (Swedish) Currently translated at 99.4% (194 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/sv/ * Translated using Weblate (Esperanto) Currently translated at 98.4% (192 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/eo/ * Translated using Weblate (German) Currently translated at 100.0% (195 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/de/ * Translated using Weblate (French) Currently translated at 95.3% (186 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/fr/ * Translated using Weblate (Italian) Currently translated at 100.0% (195 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/it/ * Translated using Weblate (Esperanto) Currently translated at 100.0% (195 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/eo/ * Translated using Weblate (Swedish) Currently translated at 100.0% (195 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/sv/ * Translated using Weblate (Chinese (Simplified)) Currently translated at 2.5% (5 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/zh_Hans/ * Added translation using Weblate (Czech) * Translated using Weblate (Czech) Currently translated at 100.0% (195 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/cs/ * Added translation using Weblate (Slovak) * Translated using Weblate (Chinese (Simplified)) Currently translated at 99.4% (194 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/zh_Hans/ * Translated using Weblate (Slovak) Currently translated at 100.0% (195 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/sk/ * Translated using Weblate (Korean) Currently translated at 45.6% (89 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/ko/ * Translated using Weblate (Slovak) Currently translated at 100.0% (195 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/sk/ * Translated using Weblate (Slovak) Currently translated at 100.0% (195 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/sk/ * Translated using Weblate (Danish) Currently translated at 46.6% (91 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/da/ * Translated using Weblate (Persian) Currently translated at 37.4% (73 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/fa/ * Translated using Weblate (Finnish) Currently translated at 50.2% (98 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/fi/ * Translated using Weblate (Indonesian) Currently translated at 36.4% (71 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/id/ * Translated using Weblate (Japanese) Currently translated at 81.0% (158 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/ja/ * Translated using Weblate (Portuguese) Currently translated at 81.0% (158 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/pt/ * Translated using Weblate (Russian) Currently translated at 86.1% (168 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/ru/ * Translated using Weblate (Occitan) Currently translated at 82.0% (160 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/oc/ * Translated using Weblate (Korean) Currently translated at 45.6% (89 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/ko/ * Translated using Weblate (Polish) Currently translated at 52.8% (103 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/pl/ * Translated using Weblate (Chinese (Traditional)) Currently translated at 86.1% (168 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/zh_Hant/ * Translated using Weblate (Chinese (Simplified)) Currently translated at 99.4% (194 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/zh_Hans/ * Translated using Weblate (Turkish) Currently translated at 27.6% (54 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/tr/ * Translated using Weblate (Ukrainian) Currently translated at 86.1% (168 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/uk/ * Translated using Weblate (Slovak) Currently translated at 100.0% (195 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/sk/ * Translated using Weblate (Finnish) Currently translated at 90.2% (176 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/fi/ * Translated using Weblate (Slovak) Currently translated at 100.0% (195 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/sk/ * Translated using Weblate (French) Currently translated at 100.0% (195 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/fr/ * Translated using Weblate (Polish) Currently translated at 70.2% (137 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/pl/ * Translated using Weblate (Chinese (Traditional)) Currently translated at 99.4% (194 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/zh_Hant/ --------- Co-authored-by: Макс Лисенко Co-authored-by: Ettore Atalan Co-authored-by: gallegonovato Co-authored-by: phlostically Co-authored-by: Luna Jernberg Co-authored-by: sashimi Co-authored-by: bittin1ddc447d824349b2 Co-authored-by: albanobattistella Co-authored-by: Lattefang <370358679@qq.com> Co-authored-by: AsciiWolf Co-authored-by: Peter Vančo Co-authored-by: Fan Chou Co-authored-by: El Co-authored-by: Jiri Grönroos Co-authored-by: EGuillemot Co-authored-by: WaldiS * chore(deps): odrs ^0.0.1 (#1240) * fix(explore): filter search results after search (#1241) Rather than performing a search with a scope limited by the selected package type, always search for all package types and filter the results afterwards. This way searching for only deb packages, will still include the snap versions in the results, if available and vice-versa. * fix(explore): prefer desktop appstream components (#1242) If multiple appstream components belong to the same package, prefer the one that has type 'desktop-application' in `_getAppstreamComponentFromSnap`. Co-authored-by: Frederik Feichtmeier * Translations update from Hosted Weblate (#1243) * Translated using Weblate (Polish) Currently translated at 89.2% (174 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/pl/ * Translated using Weblate (Slovak) Currently translated at 100.0% (195 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/sk/ --------- Co-authored-by: WaldiS Co-authored-by: Peter Vančo * chore: pin snapd.dart 0.4.8 (#1245) * fix: Installed snap list taking long to load (#1248) Fixes #1219 * Use cached_netowork_image for app banners and icons (#1249) * Add cached_network_image * feat: Cache app banners * feat: Cache app icons * feat(review): validate input (#1250) - name (user_display) is not optional - title (summary) must be 2-70 characters - review (description) must be 2-3000 characters https://github.com/GNOME/odrs-web/blob/master/odrs/views_api.py Co-authored-by: Frederik Feichtmeier * Translations update from Hosted Weblate (#1244) * Translated using Weblate (Polish) Currently translated at 89.2% (174 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/pl/ * Translated using Weblate (Slovak) Currently translated at 100.0% (195 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/sk/ * Translated using Weblate (Danish) Currently translated at 46.6% (91 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/da/ * Translated using Weblate (Danish) Currently translated at 99.4% (194 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/da/ * Translated using Weblate (German) Currently translated at 100.0% (195 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/de/ * Translated using Weblate (Spanish) Currently translated at 100.0% (195 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/es/ * Translated using Weblate (French) Currently translated at 100.0% (195 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/fr/ * Translated using Weblate (Japanese) Currently translated at 81.0% (158 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/ja/ * Translated using Weblate (Italian) Currently translated at 100.0% (195 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/it/ * Translated using Weblate (Swedish) Currently translated at 100.0% (195 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/sv/ * Translated using Weblate (Esperanto) Currently translated at 100.0% (195 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/eo/ * Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (195 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/zh_Hant/ * Translated using Weblate (Chinese (Simplified)) Currently translated at 99.4% (194 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/zh_Hans/ * Translated using Weblate (Czech) Currently translated at 100.0% (195 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/cs/ * Translated using Weblate (Slovak) Currently translated at 100.0% (195 of 195 strings) Translation: Ubuntu Software Store/ubuntu-software Translate-URL: https://hosted.weblate.org/projects/ubuntu-software/ubuntu-software/sk/ --------- Co-authored-by: WaldiS Co-authored-by: Peter Vančo Co-authored-by: Michael Millet Co-authored-by: phlostically Co-authored-by: EGuillemot Co-authored-by: AsciiWolf * Raise version to 0.3.0 --------- Co-authored-by: J-P Nurmi Co-authored-by: Dennis Loose Co-authored-by: Weblate (bot) Co-authored-by: gallegonovato Co-authored-by: phlostically Co-authored-by: DefaultX-od Co-authored-by: Arve Eriksson <031299870@telia.com> Co-authored-by: Luna Jernberg Co-authored-by: sashimi Co-authored-by: MadsRH Co-authored-by: Quentin PAGÈS Co-authored-by: Henry Riehl <73116038+whiskeyPeak@users.noreply.github.com> Co-authored-by: Paul Kepinski Co-authored-by: Макс Лисенко Co-authored-by: Ettore Atalan Co-authored-by: Shan Shaji Co-authored-by: Shan Shaji Co-authored-by: bittin1ddc447d824349b2 Co-authored-by: albanobattistella Co-authored-by: Lattefang <370358679@qq.com> Co-authored-by: AsciiWolf Co-authored-by: J-P Nurmi Co-authored-by: Peter Vančo Co-authored-by: Fan Chou Co-authored-by: El Co-authored-by: Jiri Grönroos Co-authored-by: EGuillemot Co-authored-by: WaldiS Co-authored-by: dejvizelo <59970707+dejvizelo@users.noreply.github.com> Co-authored-by: Michael Millet --- .github/workflows/analyze.yml | 7 +- .github/workflows/build.yml | 3 +- .github/workflows/cla-check.yaml | 4 +- .github/workflows/format.yml | 3 +- .github/workflows/test.yml | 5 +- .../assets/10-network-manager.yaml | 3 + integration_test/software_test.dart | 4 +- lib/app/app.dart | 110 +-- lib/app/app_model.dart | 69 +- lib/app/collection/collection_model.dart | 167 +++++ lib/app/collection/collection_page.dart | 322 ++++++++ lib/app/collection/collection_tile.dart | 88 +++ lib/app/collection/collection_toggle.dart | 82 +++ .../no_updates_page.dart | 0 lib/app/collection/package_collection.dart | 135 ++++ .../package_update_banner.dart} | 46 +- .../package_update_dialog.dart} | 20 +- .../package_updates_model.dart | 2 +- lib/app/collection/package_updates_page.dart | 286 ++++++++ lib/app/collection/simple_snap_controls.dart | 174 +++++ lib/app/collection/simple_snap_model.dart | 115 +++ lib/app/collection/snap_collection.dart | 146 ++++ lib/app/common/app_banner.dart | 80 +- lib/app/common/app_data.dart | 7 +- lib/app/common/app_format.dart | 12 +- lib/app/common/app_format_popup.dart | 84 ++- lib/app/common/app_icon.dart | 23 +- lib/app/common/app_page/app_description.dart | 11 +- .../app_page/app_format_toggle_buttons.dart | 85 ++- lib/app/common/app_page/app_header.dart | 44 +- .../app_infos/additional_information.dart | 80 +- .../common/app_page/app_infos/app_infos.dart | 31 +- .../app_page/app_infos/app_size_fragment.dart | 1 - .../app_infos/confinment_info_fragment.dart | 1 - .../app_infos/install_date_info_fragment.dart | 1 - .../app_infos/license_info_fragment.dart | 2 +- .../app_infos/publisher_info_fragment.dart | 17 +- .../app_infos/released_at_info_fragment.dart | 3 +- .../app_infos/version_info_fragment.dart | 2 +- lib/app/common/app_page/app_page.dart | 85 ++- lib/app/common/app_page/app_reviews.dart | 688 +++++++++++------- .../common/app_page/app_swipe_gesture.dart | 1 + lib/app/common/app_page/publisher_name.dart | 33 +- lib/app/common/app_rating.dart | 13 + lib/app/common/close_confirmation_dialog.dart | 2 +- lib/app/common/constants.dart | 2 + lib/app/common/expandable_title.dart | 17 + lib/app/common/link.dart | 9 +- lib/app/common/loading_banner_grid.dart | 6 +- .../common/packagekit/dependency_dialogs.dart | 198 +++++ .../common/packagekit/package_controls.dart | 60 +- lib/app/common/packagekit/package_model.dart | 116 ++- lib/app/common/packagekit/package_page.dart | 181 ++--- .../packagekit/packagekit_filter_button.dart | 2 +- lib/app/common/rating_chart.dart | 140 ++++ lib/app/common/rating_model.dart | 11 +- lib/app/common/search_field.dart | 15 +- lib/app/common/snap/snap_channel_button.dart | 11 +- lib/app/common/snap/snap_controls.dart | 36 +- lib/app/common/snap/snap_model.dart | 13 +- lib/app/common/snap/snap_page.dart | 70 +- lib/app/common/snap/snap_section.dart | 343 ++++----- lib/app/common/snap/snap_sort.dart | 15 +- lib/app/common/snap/snap_utils.dart | 14 - lib/app/explore/explore_model.dart | 173 +++-- lib/app/explore/explore_page.dart | 106 ++- lib/app/explore/offline_page.dart | 4 +- lib/app/explore/search_page.dart | 59 +- lib/app/explore/section_banner.dart | 202 +++-- lib/app/explore/section_grid.dart | 23 +- lib/app/explore/start_page.dart | 218 ++++-- lib/app/installed/installed_header.dart | 93 --- lib/app/installed/installed_model.dart | 150 ---- .../installed/installed_packages_page.dart | 84 --- lib/app/installed/installed_page.dart | 137 ---- lib/app/installed/installed_snaps_page.dart | 94 --- .../package_installer_page.dart | 1 + lib/app/settings/repo_dialog.dart | 157 ++-- lib/app/settings/settings_model.dart | 44 +- lib/app/settings/settings_page.dart | 472 +++++++----- lib/app/settings/theme_tile.dart | 144 ++++ lib/app/updates/package_updates_page.dart | 396 ---------- lib/app/updates/snap_updates_model.dart | 90 --- lib/app/updates/snap_updates_page.dart | 116 --- lib/app/updates/updates_model.dart | 40 - lib/app/updates/updates_page.dart | 137 ---- lib/l10n/app_cs.arb | 430 +++++++++++ lib/l10n/app_da.arb | 316 +++++++- lib/l10n/app_de.arb | 147 +++- lib/l10n/app_en.arb | 77 +- lib/l10n/app_eo.arb | 119 ++- lib/l10n/app_es.arb | 123 +++- lib/l10n/app_fa.arb | 358 ++++++++- lib/l10n/app_fi.arb | 234 +++++- lib/l10n/app_fr.arb | 135 +++- lib/l10n/app_id.arb | 500 +++++++++++-- lib/l10n/app_it.arb | 119 ++- lib/l10n/app_ja.arb | 127 +++- lib/l10n/app_ko.arb | 254 ++++++- lib/l10n/app_oc.arb | 115 ++- lib/l10n/app_pl.arb | 222 +++++- lib/l10n/app_pt.arb | 123 +++- lib/l10n/app_ru.arb | 125 +++- lib/l10n/app_sk.arb | 430 +++++++++++ lib/l10n/app_sv.arb | 210 +++++- lib/l10n/app_tr.arb | 284 +++++++- lib/l10n/app_uk.arb | 430 +++++++++++ lib/l10n/app_zh.arb | 431 ++++++++++- lib/l10n/app_zh_Hant.arb | 350 ++++++++- lib/main.dart | 12 +- lib/services/packagekit/package_service.dart | 222 +++++- lib/services/packagekit/package_state.dart | 20 +- lib/services/packagekit/updates_state.dart | 20 +- lib/services/snap_service.dart | 98 ++- lib/snapx.dart | 19 + lib/theme_mode_x.dart | 10 + linux/CMakeLists.txt | 2 + linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + linux/my_application.cc | 43 +- pubspec.yaml | 42 +- snap/snapcraft.yaml | 5 +- test/services/package_model_test.dart | 18 +- test/services/package_service_test.dart | 83 ++- test/services/snap_service_test.dart | 79 +- test/widget_test.dart | 20 +- 126 files changed, 10223 insertions(+), 3430 deletions(-) create mode 100644 integration_test/assets/10-network-manager.yaml create mode 100644 lib/app/collection/collection_model.dart create mode 100644 lib/app/collection/collection_page.dart create mode 100644 lib/app/collection/collection_tile.dart create mode 100644 lib/app/collection/collection_toggle.dart rename lib/app/{updates => collection}/no_updates_page.dart (100%) create mode 100644 lib/app/collection/package_collection.dart rename lib/app/{updates/update_banner.dart => collection/package_update_banner.dart} (81%) rename lib/app/{updates/update_dialog.dart => collection/package_update_dialog.dart} (92%) rename lib/app/{updates => collection}/package_updates_model.dart (99%) create mode 100644 lib/app/collection/package_updates_page.dart create mode 100644 lib/app/collection/simple_snap_controls.dart create mode 100644 lib/app/collection/simple_snap_model.dart create mode 100644 lib/app/collection/snap_collection.dart create mode 100644 lib/app/common/expandable_title.dart create mode 100644 lib/app/common/packagekit/dependency_dialogs.dart create mode 100644 lib/app/common/rating_chart.dart delete mode 100644 lib/app/installed/installed_header.dart delete mode 100644 lib/app/installed/installed_model.dart delete mode 100644 lib/app/installed/installed_packages_page.dart delete mode 100644 lib/app/installed/installed_page.dart delete mode 100644 lib/app/installed/installed_snaps_page.dart create mode 100644 lib/app/settings/theme_tile.dart delete mode 100644 lib/app/updates/package_updates_page.dart delete mode 100644 lib/app/updates/snap_updates_model.dart delete mode 100644 lib/app/updates/snap_updates_page.dart delete mode 100644 lib/app/updates/updates_model.dart delete mode 100644 lib/app/updates/updates_page.dart create mode 100644 lib/l10n/app_cs.arb create mode 100644 lib/l10n/app_sk.arb create mode 100644 lib/l10n/app_uk.arb create mode 100644 lib/theme_mode_x.dart 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); }); }