KNewStuff

Page.qml
1 /*
2  SPDX-FileCopyrightText: 2019 Dan Leinir Turthra Jensen <[email protected]>
3 
4  SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
5 */
6 
7 /**
8  * @brief A Kirigami.Page component used for managing KNS entries
9  *
10  * This component is functionally equivalent to the old DownloadDialog
11  * @see KNewStuff::DownloadDialog
12  * @since 5.63
13  */
14 
15 import QtQuick 2.11
16 import QtQuick.Controls 2.11 as QtControls
17 import QtQuick.Layouts 1.11 as QtLayouts
18 import QtGraphicalEffects 1.11 as QtEffects
19 
20 import org.kde.kcm 1.2 as KCM
21 import org.kde.kirigami 2.19 as Kirigami
22 
23 import org.kde.newstuff 1.85 as NewStuff
24 
25 import "private" as Private
26 import "private/entrygriddelegates" as EntryGridDelegates
27 
28 KCM.GridViewKCM {
29  id: root;
30  /**
31  * @brief The configuration file which describes the application (knsrc)
32  *
33  * The format and location of this file is found in the documentation for
34  * KNS3::DownloadDialog
35  */
36  property alias configFile: newStuffEngine.configFile;
37  readonly property alias engine: newStuffEngine;
38 
39  /**
40  * Any generic message from the NewStuff.Engine
41  * @param message The message to be shown to the user
42  */
43  signal message(string message);
44  /**
45  * A message posted usually describing that whatever action a recent busy
46  * message said was happening has been completed
47  * @param message The message to be shown to the user
48  */
49  signal idleMessage(string message);
50  /**
51  * A message posted when the engine is busy doing something long duration
52  * (usually this will be when fetching installation data)
53  * @param message The message to be shown to the user
54  */
55  signal busyMessage(string message);
56  /**
57  * A message posted when something has gone wrong
58  * @param message The message to be shown to the user
59  */
60  signal errorMessage(string message);
61 
62  /**
63  * Whether or not to show the Upload... context action
64  * Usually this will be bound to the engine's property which usually defines
65  * this, but you can override it programmatically by setting it here.
66  * @since 5.85
67  * @see KNSCore::Engine::uploadEnabled
68  */
69  property alias showUploadAction: uploadAction.visible
70 
71 
72  /**
73  * Show the details page for a specific entry.
74  * If you call this function before the engine initialisation has been completed,
75  * the action itself will be postponed until that has happened.
76  * @param providerId The provider ID for the entry you wish to show details for
77  * @param entryId The unique ID for the entry you wish to show details for
78  * @since 5.79
79  */
80  function showEntryDetails(providerId, entryId) {
81  _showEntryDetailsThrottle.providerId = providerId;
82  _showEntryDetailsThrottle.entryId = entryId;
83  newStuffEngine.engine.storeSearch();
84 
85  //check if entry in question is perhaps a group, if so, load the new details.
86  var theIndex = newStuffModel.indexOfEntryId(providerId, entryId);
87  var type = newStuffModel.data(newStuffModel.index(theIndex, 0), NewStuff.ItemsModel.EntryTypeRole);
88 
89  if (type === NewStuff.ItemsModel.GroupEntry) {
90  newStuffEngine.searchTerm = newStuffModel.data(newStuffModel.index(theIndex, 0), NewStuff.ItemsModel.PayloadRole);
91  } else {
92  newStuffEngine.engine.fetchEntryById(entryId);
93  }
94 
95  if (newStuffEngine.isLoading) {
96  _showEntryDetailsThrottle.enabled = true;
97  } else {
98  _showEntryDetailsThrottle.onIsLoadingDataChanged();
99  }
100 
101  }
102  Connections {
103  id: _showEntryDetailsThrottle;
104  target: newStuffModel;
105  enabled: false;
106  property var entryId;
107  property var providerId;
108  function onIsLoadingDataChanged() {
109  if (newStuffModel.isLoadingData === false && root.view.count == 1) {
110  _showEntryDetailsThrottle.enabled = false;
111  var theIndex = newStuffModel.indexOfEntryId(_showEntryDetailsThrottle.providerId, _showEntryDetailsThrottle.entryId);
112  if (theIndex > -1) {
113  pageStack.push(detailsPage, {
114  newStuffModel: newStuffModel,
115  index: theIndex,
116  name: newStuffModel.data(newStuffModel.index(theIndex, 0), NewStuff.ItemsModel.NameRole),
117  author: newStuffModel.data(newStuffModel.index(theIndex, 0), NewStuff.ItemsModel.AuthorRole),
118  previews: newStuffModel.data(newStuffModel.index(theIndex, 0), NewStuff.ItemsModel.PreviewsRole),
119  shortSummary: newStuffModel.data(newStuffModel.index(theIndex, 0), NewStuff.ItemsModel.ShortSummaryRole),
120  summary: newStuffModel.data(newStuffModel.index(theIndex, 0), NewStuff.ItemsModel.SummaryRole),
121  homepage: newStuffModel.data(newStuffModel.index(theIndex, 0), NewStuff.ItemsModel.HomepageRole),
122  donationLink: newStuffModel.data(newStuffModel.index(theIndex, 0), NewStuff.ItemsModel.DonationLinkRole),
123  status: newStuffModel.data(newStuffModel.index(theIndex, 0), NewStuff.ItemsModel.StatusRole),
124  commentsCount: newStuffModel.data(newStuffModel.index(theIndex, 0), NewStuff.ItemsModel.NumberOfCommentsRole),
125  rating: newStuffModel.data(newStuffModel.index(theIndex, 0), NewStuff.ItemsModel.RatingRole),
126  downloadCount: newStuffModel.data(newStuffModel.index(theIndex, 0), NewStuff.ItemsModel.DownloadCountRole),
127  downloadLinks: newStuffModel.data(newStuffModel.index(theIndex, 0), NewStuff.ItemsModel.DownloadLinksRole),
128  providerId: _showEntryDetailsThrottle.providerId
129  });
130  _restoreSearchState.enabled = true;
131  } else {
132  root.message(i18ndc("knewstuff5", "A message which is shown when the user attempts to display a specific entry from a specific provider, but that entry isn't found", "The entry you attempted to display, identified by the unique ID %1, could not be found.", _showEntryDetailsThrottle.entryId));
133  newStuffEngine.engine.restoreSearch();
134  }
135  } else if (newStuffModel.isLoadingData === false && root.view.count > 1) {
136  // right now, this is only one level deep...
137  _showEntryDetailsThrottle.enabled = false;
138  _restoreSearchState.enabled = true;
139  }
140  }
141  }
142  Connections {
143  id: _restoreSearchState;
144  target: pageStack;
145  enabled: false;
146  function onCurrentIndexChanged() {
147  if (pageStack.currentIndex === 0) {
148  newStuffEngine.engine.restoreSearch();
149  _restoreSearchState.enabled = false;
150  }
151  }
152  }
153 
154  property string uninstallLabel: i18ndc("knewstuff5", "Request uninstallation of this item", "Uninstall");
155  property string useLabel: engine.engine.useLabel
156 
157  property int viewMode: Page.ViewMode.Tiles
158  enum ViewMode {
159  Tiles,
160  Icons,
161  Preview
162  }
163 
164  // Otherwise the first item will be focused, see BUG: 424894
165  Component.onCompleted: {
166  view.currentIndex = -1
167  }
168 
169  title: newStuffEngine.name
170 
171  view.header: Item {
172  implicitWidth: view.width - Kirigami.Units.gridUnit
173  implicitHeight: Kirigami.Units.gridUnit * 3
174  visible: !loadingOverlay.visible
175  Kirigami.InlineMessage {
176  anchors.fill: parent
177  anchors.margins: Kirigami.Units.smallSpacing
178  visible: true
179  text: i18nd("knewstuff5", "The content available here has been uploaded by users like you, and has not been reviewed by your distributor for functionality or stability.")
180  }
181  }
182 
183  NewStuff.Engine {
184  id: newStuffEngine;
185  property string statusMessage;
186  onMessage: {
187  root.message(message);
188  statusMessage = message;
189  }
190  onIdleMessage: {
191  root.idleMessage(message);
192  statusMessage = message;
193  }
194  onBusyMessage: {
195  root.busyMessage(message);
196  statusMessage = message;
197  }
198  onErrorMessage: {
199  root.errorMessage(message);
200  statusMessage = message;
201  }
202  }
203  NewStuff.QuestionAsker {}
204  Private.ErrorDisplayer { engine: newStuffEngine; active: root.isCurrentPage; }
205 
206  QtControls.ActionGroup { id: viewModeActionGroup }
207  QtControls.ActionGroup { id: viewFilterActionGroup }
208  QtControls.ActionGroup { id: viewSortingActionGroup }
209  actions {
210  contextualActions: [
211  Kirigami.Action {
212  text: {
213  if (root.viewMode == Page.ViewMode.Tiles) {
214  return i18nd("knewstuff5", "Tiles");
215  } else if (root.viewMode == Page.ViewMode.Icons) {
216  return i18nd("knewstuff5", "Icons");
217  } else {
218  return i18nd("knewstuff5", "Preview");
219  }
220  }
221  checkable: false
222  icon.name: {
223  if (root.viewMode == Page.ViewMode.Tiles) {
224  return "view-list-details";
225  } else if (root.viewMode == Page.ViewMode.Icons) {
226  return "view-list-icons";
227  } else {
228  return "view-preview";
229  }
230  }
231  Kirigami.Action {
232  icon.name: "view-list-details"
233  text: i18nd("knewstuff5", "Detailed Tiles View Mode")
234  onTriggered: { root.viewMode = Page.ViewMode.Tiles; }
235  checked: root.viewMode == Page.ViewMode.Tiles
236  checkable: true
237  QtControls.ActionGroup.group: viewModeActionGroup
238  }
239  Kirigami.Action {
240  icon.name: "view-list-icons"
241  text: i18nd("knewstuff5", "Icons Only View Mode")
242  onTriggered: { root.viewMode = Page.ViewMode.Icons; }
243  checked: root.viewMode == Page.ViewMode.Icons
244  checkable: true
245  QtControls.ActionGroup.group: viewModeActionGroup
246  }
247  Kirigami.Action {
248  icon.name: "view-preview"
249  text: i18nd("knewstuff5", "Large Preview View Mode")
250  onTriggered: { root.viewMode = Page.ViewMode.Preview; }
251  checked: root.viewMode == Page.ViewMode.Preview
252  checkable: true
253  QtControls.ActionGroup.group: viewModeActionGroup
254  }
255  },
256  Kirigami.Action {
257  text: {
258  if (newStuffEngine.filter === 0) {
259  return i18nd("knewstuff5", "Everything");
260  } else if (newStuffEngine.filter === 1) {
261  return i18nd("knewstuff5", "Installed");
262  } else if (newStuffEngine.filter === 2) {
263  return i18nd("knewstuff5", "Updateable");
264  } else {
265  // then it's ExactEntryId and we want to probably just ignore that
266  }
267  }
268  checkable: false
269  icon.name: {
270  if (newStuffEngine.filter === 0) {
271  return "package-available"
272  } else if (newStuffEngine.filter === 1) {
273  return "package-installed-updated"
274  } else if (newStuffEngine.filter === 2) {
275  return "package-installed-outdated"
276  } else {
277  // then it's ExactEntryId and we want to probably just ignore that
278  }
279  }
280  Kirigami.Action {
281  icon.name: "package-available"
282  text: i18ndc("knewstuff5", "List option which will set the filter to show everything", "Show All Entries")
283  checkable: true
284  checked: newStuffEngine.filter === 0
285  onTriggered: { newStuffEngine.filter = 0; }
286  QtControls.ActionGroup.group: viewFilterActionGroup
287  }
288  Kirigami.Action {
289  icon.name: "package-installed-updated"
290  text: i18ndc("knewstuff5", "List option which will set the filter so only installed items are shown", "Show Only Installed Entries")
291  checkable: true
292  checked: newStuffEngine.filter === 1
293  onTriggered: { newStuffEngine.filter = 1; }
294  QtControls.ActionGroup.group: viewFilterActionGroup
295  }
296  Kirigami.Action {
297  icon.name: "package-installed-outdated"
298  text: i18ndc("knewstuff5", "List option which will set the filter so only installed items with updates available are shown", "Show Only Updateable Entries")
299  checkable: true
300  checked: newStuffEngine.filter === 2
301  onTriggered: { newStuffEngine.filter = 2; }
302  QtControls.ActionGroup.group: viewFilterActionGroup
303  }
304  },
305  Kirigami.Action {
306  text: {
307  if (newStuffEngine.sortOrder === 0) {
308  return i18nd("knewstuff5", "Recent");
309  } else if (newStuffEngine.sortOrder === 1) {
310  return i18nd("knewstuff5", "Alphabetical");
311  } else if (newStuffEngine.sortOrder === 2) {
312  return i18nd("knewstuff5", "Rating");
313  } else if (newStuffEngine.sortOrder === 3) {
314  return i18nd("knewstuff5", "Downloads");
315  } else {
316  }
317  }
318  checkable: false
319  icon.name: {
320  if (newStuffEngine.sortOrder === 0) {
321  return "change-date-symbolic";
322  } else if (newStuffEngine.sortOrder === 1) {
323  return "sort-name";
324  } else if (newStuffEngine.sortOrder === 2) {
325  return "rating";
326  } else if (newStuffEngine.sortOrder === 3) {
327  return "download";
328  } else {
329  }
330  }
331  Kirigami.Action {
332  icon.name: "change-date-symbolic"
333  text: i18ndc("knewstuff5", "List option which will set the sort order to based on when items were most recently updated", "Show Most Recent First")
334  checkable: true
335  checked: newStuffEngine.sortOrder === 0
336  onTriggered: { newStuffEngine.sortOrder = 0; }
337  QtControls.ActionGroup.group: viewSortingActionGroup
338  }
339  Kirigami.Action {
340  icon.name: "sort-name"
341  text: i18ndc("knewstuff5", "List option which will set the sort order to be alphabetical based on the name", "Sort Alphabetically By Name")
342  checkable: true
343  checked: newStuffEngine.sortOrder === 1
344  onTriggered: { newStuffEngine.sortOrder = 1; }
345  QtControls.ActionGroup.group: viewSortingActionGroup
346  }
347  Kirigami.Action {
348  icon.name: "rating"
349  text: i18ndc("knewstuff5", "List option which will set the sort order to based on user ratings", "Show Highest Rated First")
350  checkable: true
351  checked: newStuffEngine.sortOrder === 2
352  onTriggered: { newStuffEngine.sortOrder = 2; }
353  QtControls.ActionGroup.group: viewSortingActionGroup
354  }
355  Kirigami.Action {
356  icon.name: "download"
357  text: i18ndc("knewstuff5", "List option which will set the sort order to based on number of downloads", "Show Most Downloaded First")
358  checkable: true
359  checked: newStuffEngine.sortOrder === 3
360  onTriggered: { newStuffEngine.sortOrder = 3; }
361  QtControls.ActionGroup.group: viewSortingActionGroup
362  }
363  },
364  Kirigami.Action {
365  id: uploadAction
366  text: i18nd("knewstuff5", "Upload...")
367  tooltip: i18nd("knewstuff5", "Learn how to add your own hot new stuff to this list")
368  iconName: "upload-media"
369  visible: newStuffEngine.engine.uploadEnabled
370  onTriggered: {
371  pageStack.push(uploadPage);
372  }
373  },
374  Kirigami.Action {
375  text: i18nd("knewstuff5", "Go to...")
376  iconName: "go-next";
377  id: searchModelActions;
378  visible: children.length > 0;
379  },
380  Kirigami.Action {
381  text: i18nd("knewstuff5", "Search...")
382  iconName: "system-search";
383  displayHint: Kirigami.DisplayHint.KeepVisible
384  displayComponent: Kirigami.SearchField {
385  enabled: engine.isValid
386  id: searchField
387  focusSequence: "Ctrl+F"
388  placeholderText: i18nd("knewstuff5", "Search...")
389  text: newStuffEngine.searchTerm
390  onAccepted: { newStuffEngine.searchTerm = searchField.text; }
391  Component.onCompleted: if (!Kirigami.InputMethod.willShowOnActive) {
392  forceActiveFocus();
393  }
394  }
395  }
396  ]
397  }
398 
399  Instantiator {
400  id: searchPresetInstatiator
401  model: newStuffEngine.searchPresetModel
402  Kirigami.Action {
403  text: model.displayName
404  iconName: model.iconName
405  property int indexEntry: index;
406  onTriggered: {
407  var curIndex = newStuffEngine.searchPresetModel.index(indexEntry, 0);
408  newStuffEngine.searchPresetModel.loadSearch(curIndex);
409  }
410  }
411  onObjectAdded: { searchModelActions.children.push(object); }
412  }
413 
414  Connections {
415  target: newStuffEngine.searchPresetModel
416  function onModelReset() { searchModelActions.children = []; }
417  }
418 
419  extraFooterTopPadding: false
420  footer: QtLayouts.RowLayout {
421  visible: visibleChildren.length > 0
422  height: visible ? implicitHeight : 0
423 
424  QtControls.Label {
425  visible: categoriesCombo.count > 2
426  text: i18nd("knewstuff5", "Category:")
427  }
428 
429  QtControls.ComboBox {
430  id: categoriesCombo
431  QtLayouts.Layout.fillWidth: true
432  visible: count > 2
433  model: newStuffEngine.categories
434  textRole: "displayName"
435  onCurrentIndexChanged: {
436  newStuffEngine.categoriesFilter = model.data(model.index(currentIndex, 0), NewStuff.CategoriesModel.NameRole);
437  }
438  }
439 
440  QtControls.Button {
441  QtLayouts.Layout.alignment: Qt.AlignRight
442  text: i18nd("knewstuff5", "Contribute your own…")
443  icon.name: "upload-media"
444  visible: newStuffEngine.engine.uploadEnabled && !uploadAction.visible
445  onClicked: {
446  pageStack.push(uploadPage);
447  }
448  }
449  }
450 
451  view.model: NewStuff.ItemsModel {
452  id: newStuffModel;
453  engine: newStuffEngine;
454  }
455  NewStuff.DownloadItemsSheet {
456  id: downloadItemsSheet
457  onItemPicked: {
458  newStuffModel.installItem(entryId, downloadItemId);
459  }
460  }
461 
462  view.implicitCellWidth: root.viewMode == Page.ViewMode.Tiles ? Kirigami.Units.gridUnit * 30 : (root.viewMode == Page.ViewMode.Preview ? Kirigami.Units.gridUnit * 25 : Kirigami.Units.gridUnit * 10)
463  view.implicitCellHeight: root.viewMode == Page.ViewMode.Tiles ? Math.round(view.implicitCellWidth / 3) : (root.viewMode == Page.ViewMode.Preview ? Kirigami.Units.gridUnit * 25 : Math.round(view.implicitCellWidth / 1.6) + Kirigami.Units.gridUnit*2)
464  view.delegate: root.viewMode == Page.ViewMode.Tiles ? tileDelegate : (root.viewMode == Page.ViewMode.Preview ? bigPreviewDelegate : thumbDelegate)
465 
466  Component {
467  id: bigPreviewDelegate
468  EntryGridDelegates.BigPreviewDelegate { }
469  }
470  Component {
471  id: tileDelegate
472  EntryGridDelegates.TileDelegate {
473  useLabel: root.useLabel
474  uninstallLabel: root.uninstallLabel
475  }
476  }
477  Component {
478  id: thumbDelegate
479  EntryGridDelegates.ThumbDelegate {
480  useLabel: root.useLabel
481  uninstallLabel: root.uninstallLabel
482  }
483  }
484 
485  Component {
486  id: detailsPage;
487  NewStuff.EntryDetails { }
488  }
489  Component {
490  id: uploadPage
491  NewStuff.UploadPage {
492  engine: newStuffEngine
493  }
494  }
495 
496  Item {
497  id: loadingOverlay
498  anchors.fill: parent
499  opacity: (newStuffEngine.isLoading || newStuffModel.isLoadingData) ? 1 : 0
500  Behavior on opacity { NumberAnimation { duration: Kirigami.Units.longDuration; } }
501  visible: opacity > 0
502  Rectangle {
503  anchors.fill: parent
504  color: Kirigami.Theme.backgroundColor
505  }
506  Kirigami.LoadingPlaceholder {
507  anchors.centerIn: parent
508  text: i18ndc("knewstuff5", "A text shown beside a busy indicator suggesting that data is being fetched", "Loading more…")
509  }
510  }
511 }
KCALUTILS_EXPORT QString errorMessage(const KCalendarCore::Exception &exception)
QString i18ndc(const char *domain, const char *context, const char *text, const TYPE &arg...)
QString i18nd(const char *domain, const char *text, const TYPE &arg...)
QChar * data()
QString message
This file is part of the KDE documentation.
Documentation copyright © 1996-2022 The KDE developers.
Generated on Sun Nov 27 2022 04:13:42 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.