Merge lp:~verzegnassi-stefano/openstore/redesign into lp:openstore

Proposed by Stefano Verzegnassi
Status: Merged
Merged at revision: 23
Proposed branch: lp:~verzegnassi-stefano/openstore/redesign
Merge into: lp:openstore
Diff against target: 2042 lines (+1377/-433)
13 files modified
manifest.json.in (+1/-1)
openstore/AppDetailsPage.qml (+542/-387)
openstore/CategoriesPage.qml (+60/-0)
openstore/DiscoverTab.qml (+165/-0)
openstore/EmptyState.qml (+74/-0)
openstore/FilteredAppView.qml (+108/-0)
openstore/Main.qml (+188/-44)
openstore/SearchPage.qml (+87/-0)
openstore/SectionDivider.qml (+52/-0)
openstore/TextualButtonStyle.qml (+67/-0)
openstore/appmodel.cpp (+15/-0)
openstore/appmodel.h (+11/-1)
openstore/openstore.qrc (+7/-0)
To merge this branch: bzr merge lp:~verzegnassi-stefano/openstore/redesign
Reviewer Review Type Date Requested Status
Michael Zanetti Needs Information
Review via email: mp+323619@code.launchpad.net

Commit message

Full UI redesign of the application:
* Added a 'Discover' tab
* Added a 'My Apps' tab
* Search and categories
* Redesigned AppDetails page

Description of the change

Full UI redesign of the application:
* Added a 'Discover' tab
* Added a 'My Apps' tab
* Search and categories
* Redesigned AppDetails page

Please note in DiscoverTab.qml that data are fetched from:
https://gist.githubusercontent.com/sverzegnassi/e6cdcfc44785ce90e5904c5fa1f9441f/raw/ddb35eb91186d36ea131ab8b99604b8a40890939/DiscoverData.json

To post a comment you must log in.
29. By Stefano Verzegnassi

Bump SDK version, we're using now 'automaticHeight' property in UITK Headers

30. By Stefano Verzegnassi

DiscoverTab app tiles: Show label if application is already installed or an update is available

31. By Stefano Verzegnassi

Fixed a few visual issues in AppDetails page, and be sure we don't get false positives for the 'unconfined' alert

Revision history for this message
Michael Zanetti (mzanetti) wrote :

The CategoryModel.qml has high potential to drift apart from the server categories. I'd vote for adding an API to the server that allows grabbing the categories.

====================

+ var dataUrl = "https://gist.githubusercontent.com/sverzegnassi/e6cdcfc44785ce90e5904c5fa1f9441f/raw/ddb35eb91186d36ea131ab8b99604b8a40890939/DiscoverData.json"

This looks very odd... I guess the intention is to have a file defining the "featured items" etc, that contributors can modify/update. That's fair, but please keep it somewhere on the infrastructure. E.g. put this Json file into the app repository here, or host it somewhere on the openstore server.

==================

1974 + // TODO: Is this really necessary?
1975 + // buildInstalledClickList();

what's up with this?

Apart from that, code looks ok to me!

review: Needs Information
Revision history for this message
Stefano Verzegnassi (verzegnassi-stefano) wrote :

Ok, first 2 questions are up to you guys. I just kept the file I uploaded on my gist since I got no other input.

Answering to the third question, well, that's something I forgot to remove when I've been testing the app on the desktop. I'm on xenial and click tools are a bit broken: I did a few changes to the code in order to make OpenStore believe I have some package installed. Forgot to remove those few bits. :P

32. By Stefano Verzegnassi

Remove i18n from printSize

33. By Stefano Verzegnassi

Remove dirty bits

34. By Stefano Verzegnassi

Remove 'Packager/Publisher' entry from AppDetailsPage

35. By Stefano Verzegnassi

Use the brand new Discover API from the OpenStore server

36. By Stefano Verzegnassi

Use the brand new Categories API from the OpenStore server

37. By Stefano Verzegnassi

Move categories GET request in Main.qml, since it makes no sense to download definitions every time we open the hamburger menu. Also, there's the risk that categories are not in sync with the repo manifest

38. By Stefano Verzegnassi

The icon is now called 'find', not 'search'...

39. By Stefano Verzegnassi

Re-add 'packager/publisher' field

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'manifest.json.in'
2--- manifest.json.in 2016-08-24 13:25:35 +0000
3+++ manifest.json.in 2017-05-08 18:42:06 +0000
4@@ -13,5 +13,5 @@
5 },
6 "version": "0.103",
7 "maintainer": "OpenStore Team <openstore-team@lists.launchpad.net>",
8- "framework": "ubuntu-sdk-15.04.3"
9+ "framework": "ubuntu-sdk-15.04.6"
10 }
11
12=== modified file 'openstore/AppDetailsPage.qml'
13--- openstore/AppDetailsPage.qml 2016-08-24 13:25:35 +0000
14+++ openstore/AppDetailsPage.qml 2017-05-08 18:42:06 +0000
15@@ -22,417 +22,572 @@
16
17
18 Page {
19+ id: appDetailsPage
20 header: PageHeader {
21 title: app ? app.name : "App details"
22+ automaticHeight: false
23 }
24
25 property var app: null
26
27
28- Flickable {
29+ ScrollView {
30+ id: scrollView
31 anchors.fill: parent
32 anchors.topMargin: parent.header.height
33- contentHeight: mainColumn.height + units.gu(2)
34- interactive: contentHeight > height - topMargin
35
36 Column {
37 id: mainColumn
38- anchors { left: parent.left; top: parent.top; right: parent.right }
39- anchors.margins: units.gu(2)
40- spacing: units.gu(1)
41- height: childrenRect.height
42-
43- RowLayout {
44- anchors { left: parent.left; right: parent.right }
45- height: units.gu(10)
46- spacing: units.gu(1)
47-
48- UbuntuShape {
49- Layout.fillHeight: true
50- Layout.preferredWidth: height
51-
52- image: Image {
53+ width: scrollView.width
54+ //spacing: units.gu(1)
55+
56+ ListItem {
57+ height: units.gu(16)
58+
59+ ListItemLayout {
60+ anchors.fill: parent
61+ title.text: app.name
62+ subtitle.text: app.author
63+ summary.text: printSize(app.fileSize)
64+
65+ UbuntuShape {
66+ SlotsLayout.position: SlotsLayout.Leading
67+ width: units.gu(12); height: width
68+ aspect: UbuntuShape.Flat
69+
70+ image: Image {
71+ height: parent.height
72+ width: parent.width
73+ source: app ? app.icon : ""
74+ }
75+ }
76+ }
77+ }
78+
79+ ListItem {
80+ height: units.gu(8)
81+
82+ RowLayout {
83+ id: buttonsRow
84+ anchors.fill: parent
85+ anchors.margins: units.gu(2)
86+ spacing: units.gu(2)
87+ visible: !appModel.installer.busy
88+
89+ Button {
90+ Layout.fillWidth: true
91+ text: app.installed ? i18n.tr("Upgrade") : i18n.tr("Install")
92+ visible: !app.installed || (app.installed && app.installedVersion < app.version)
93+ color: UbuntuColors.green
94+ onClicked: {
95+ appModel.installer.installPackage(app.packageUrl)
96+ }
97+ }
98+
99+ Button {
100+ Layout.fillWidth: true
101+ text: i18n.tr("Remove")
102+ visible: app.installed
103+ color: UbuntuColors.red
104+ onClicked: {
105+ appModel.installer.removePackage(app.appId, app.version)
106+ }
107+ }
108+ }
109+
110+ ProgressBar {
111+ id: installerProgressBar
112+ anchors {
113+ left: parent.left; leftMargin: units.gu(2)
114+ right: parent.right; rightMargin: units.gu(2)
115+ verticalCenter: parent.verticalCenter
116+ }
117+ maximumValue: app ? app.fileSize : 0
118+ value: appModel.installer.downloadProgress
119+ visible: appModel.installer.busy
120+ indeterminate: appModel.installer.downloadProgress == 0
121+ }
122+ }
123+
124+ ListItem {
125+ visible: {
126+ for (var i=0; i<app.hooksCount; ++i) {
127+ if (includesUnconfinedLocations(app.readPaths(i)))
128+ return true
129+ if (includesUnconfinedLocations(app.writePaths(i)))
130+ return true
131+ if (app.apparmorTemplate(i).indexOf("unconfined") >= 0)
132+ return true
133+ }
134+ return false
135+ }
136+ ListItemLayout {
137+ anchors.centerIn: parent
138+ subtitle.text: i18n.tr("This software requires extra privileges. See below for details.")
139+ subtitle.color: UbuntuColors.red
140+ subtitle.maximumLineCount: 2
141+ subtitle.wrapMode: Text.WordWrap
142+
143+ Icon {
144+ SlotsLayout.position: SlotsLayout.Leading
145+ width: units.gu(4); height: width
146+ name: "security-alert"
147+ }
148+ }
149+ }
150+
151+ ListItem {
152+ height: units.gu(32)
153+ visible: screenshotsView.count
154+
155+ ListView {
156+ id: screenshotsView
157+ anchors { left: parent.left; right: parent.right; verticalCenter: parent.verticalCenter }
158+ leftMargin: units.gu(2)
159+ rightMargin: units.gu(2)
160+ clip: true
161+ height: count > 0 ? units.gu(24) : 0
162+ visible: count > 0
163+ spacing: units.gu(1)
164+ orientation: ListView.Horizontal
165+ model: app.screenshots
166+ delegate: UbuntuShape {
167 height: parent.height
168- width: parent.width
169- source: app ? app.icon : ""
170- }
171- }
172-
173- ColumnLayout {
174- Layout.fillWidth: true
175- Layout.fillHeight: true
176- spacing: units.gu(1)
177- Label {
178- text: app.name
179- Layout.fillWidth: true
180- fontSize: "large"
181- }
182- Label {
183- text: app.author
184- Layout.fillWidth: true
185- }
186- }
187- }
188- ThinDivider { }
189- Label {
190- anchors { left: parent.left; right: parent.right }
191- text: app.tagline
192- wrapMode: Text.WordWrap
193- }
194-
195- ListView {
196- anchors { left: parent.left; right: parent.right; margins: -units.gu(2) }
197- leftMargin: units.gu(2)
198- rightMargin: units.gu(2)
199- clip: true
200- height: count > 0 ? units.gu(20) : 0
201- visible: count > 0
202- spacing: units.gu(1)
203- orientation: ListView.Horizontal
204- model: app.screenshots
205- delegate: UbuntuShape {
206- height: parent.height
207- // sh : lv.h = sw : x
208- width: screenshot.sourceSize.width * height / screenshot.sourceSize.height
209- sourceFillMode: UbuntuShape.PreserveAspectFit
210- source: Image {
211- id: screenshot
212- source: modelData
213- }
214-
215- AbstractButton {
216- id: screenShotButton
217- anchors.fill: parent
218- onClicked: {
219- print("opening at:", screenShotButton.mapToItem(root, 0, 0).x)
220- zoomIn.createObject(root, {x: screenShotButton.mapToItem(root, 0, 0).x, y: screenShotButton.mapToItem(root, 0, 0).y, itemScale: screenShotButton.height / root.height, imageSource: modelData});
221-// zoomIn.createObject(root, {x: 100, y: 100});
222- }
223- }
224-
225- Component {
226- id: zoomIn
227- Rectangle {
228- id: zI
229- width: parent.width
230- height: parent.height
231- color: "black"
232-
233- property real itemScale: 1
234- property string imageSource
235- transform: Scale {
236- origin.x: 0
237- origin.y: 0
238- xScale: zI.itemScale
239- yScale: zI.itemScale
240- }
241-
242- ParallelAnimation {
243- id: scaleInAnimation
244- onStarted: {
245- hideAnimation.initialScale = itemScale;
246- hideAnimation.initialX = x;
247- hideAnimation.initialY = y;
248- }
249-
250- UbuntuNumberAnimation { target: zI; property: "itemScale"; to: 1 }
251- UbuntuNumberAnimation { target: zI; properties: "x,y"; to: 0 }
252- }
253-
254-
255- Component.onCompleted: {
256- scaleInAnimation.start();
257- }
258-
259- Image {
260- anchors.fill: parent
261- source: zI.imageSource
262- fillMode: Image.PreserveAspectFit
263- }
264-
265- AbstractButton {
266- anchors.fill: parent
267- onClicked: {
268- hideAnimation.start()
269- }
270- }
271-
272- ParallelAnimation {
273- id: hideAnimation
274- property real initialScale: 1
275- property int initialX: 0
276- property int initialY: 0
277-
278-
279- UbuntuNumberAnimation { target: zI; property: "itemScale"; to: hideAnimation.initialScale }
280- UbuntuNumberAnimation { target: zI; property: "x"; to: hideAnimation.initialX }
281- UbuntuNumberAnimation { target: zI; property: "y"; to: hideAnimation.initialY }
282- onStopped: {
283- script: zI.destroy()
284- }
285-
286- }
287- }
288- }
289- }
290- }
291-
292- Label {
293- anchors { left: parent.left; right: parent.right }
294- text: app.description
295- wrapMode: Text.WordWrap
296- }
297-
298- ThinDivider { }
299-
300- Label {
301- text: "<b>Packager/Publisher:</b> " + (app.maintainer ? app.maintainer : "Openstore team")
302- }
303-
304- Label {
305- anchors { left: parent.left; right: parent.right }
306- text: "Installed version: " + (app.installedVersion ? app.installedVersion : "None")
307- wrapMode: Text.WordWrap
308- }
309-
310- Label {
311- anchors { left: parent.left; right: parent.right }
312- text: "Latest available version: " + app.version
313- wrapMode: Text.WordWrap
314- }
315-
316- Label {
317- anchors { left: parent.left; right: parent.right }
318- text: "Changelog:"
319- wrapMode: Text.WordWrap
320- visible: app.changelog
321- }
322-
323- Label {
324- anchors { left: parent.left; right: parent.right }
325- text: app.changelog
326- wrapMode: Text.WordWrap
327- visible: app.changelog
328- }
329-
330- ThinDivider {}
331-
332- ProgressBar {
333- Layout.fillWidth: true
334- maximumValue: app ? app.fileSize : 0
335- value: appModel.installer.downloadProgress
336- visible: appModel.installer.busy
337- indeterminate: appModel.installer.downloadProgress == 0
338- }
339-
340- RowLayout {
341- width: parent.width
342- spacing: units.gu(1)
343- visible: !appModel.installer.busy
344-
345- Button {
346- text: app.installed ? "Upgrade" : "Install"
347- visible: !app.installed || (app.installed && app.installedVersion < app.version)
348- color: "#DD4814"
349- onClicked: {
350- appModel.installer.installPackage(app.packageUrl)
351- }
352- }
353-
354- Button {
355- text: "Remove"
356- visible: app.installed
357- color: UbuntuColors.red
358- onClicked: {
359- appModel.installer.removePackage(app.appId, app.version)
360- }
361- }
362- }
363-
364- ThinDivider {}
365-
366- Label {
367- anchors { left: parent.left; right: parent.right }
368- text: "Package contents:"
369- font.bold: true
370+ // sh : lv.h = sw : x
371+ width: screenshot.sourceSize.width * height / screenshot.sourceSize.height
372+ aspect: UbuntuShape.Flat
373+ sourceFillMode: UbuntuShape.PreserveAspectFit
374+ source: Image {
375+ id: screenshot
376+ source: modelData
377+ smooth: true
378+ antialiasing: true
379+ }
380+
381+ AbstractButton {
382+ id: screenShotButton
383+ anchors.fill: parent
384+ onClicked: {
385+ print("opening at:", screenShotButton.mapToItem(root, 0, 0).x)
386+ zoomIn.createObject(root, {x: screenShotButton.mapToItem(root, 0, 0).x, y: screenShotButton.mapToItem(root, 0, 0).y, itemScale: screenShotButton.height / root.height, imageSource: modelData});
387+ // zoomIn.createObject(root, {x: 100, y: 100});
388+ }
389+ }
390+
391+ Component {
392+ id: zoomIn
393+ Rectangle {
394+ id: zI
395+ width: parent.width
396+ height: parent.height
397+ color: "black"
398+
399+ property real itemScale: 1
400+ property string imageSource
401+ transform: Scale {
402+ origin.x: 0
403+ origin.y: 0
404+ xScale: zI.itemScale
405+ yScale: zI.itemScale
406+ }
407+
408+ ParallelAnimation {
409+ id: scaleInAnimation
410+ onStarted: {
411+ hideAnimation.initialScale = itemScale;
412+ hideAnimation.initialX = x;
413+ hideAnimation.initialY = y;
414+ }
415+
416+ UbuntuNumberAnimation { target: zI; property: "itemScale"; to: 1 }
417+ UbuntuNumberAnimation { target: zI; properties: "x,y"; to: 0 }
418+ }
419+
420+
421+ Component.onCompleted: {
422+ scaleInAnimation.start();
423+ }
424+
425+ Image {
426+ anchors.fill: parent
427+ source: zI.imageSource
428+ fillMode: Image.PreserveAspectFit
429+ }
430+
431+ AbstractButton {
432+ anchors.fill: parent
433+ onClicked: {
434+ hideAnimation.start()
435+ }
436+ }
437+
438+ ParallelAnimation {
439+ id: hideAnimation
440+ property real initialScale: 1
441+ property int initialX: 0
442+ property int initialY: 0
443+
444+
445+ UbuntuNumberAnimation { target: zI; property: "itemScale"; to: hideAnimation.initialScale }
446+ UbuntuNumberAnimation { target: zI; property: "x"; to: hideAnimation.initialX }
447+ UbuntuNumberAnimation { target: zI; property: "y"; to: hideAnimation.initialY }
448+ onStopped: {
449+ script: zI.destroy()
450+ }
451+
452+ }
453+ }
454+ }
455+ }
456+ }
457+ }
458+
459+ ListItem {
460+ height: descLayout.height
461+ onClicked: descLayout.showAll = !descLayout.showAll
462+ ListItemLayout {
463+ id: descLayout
464+ property bool showAll: false
465+ title.text: app.tagline || i18n.tr("Description")
466+ subtitle.text: app.description
467+ subtitle.textSize: Label.Small
468+ subtitle.wrapMode: Text.WordWrap
469+ subtitle.maximumLineCount: showAll ? Number.MAX_VALUE : 5
470+
471+ Icon {
472+ width: units.gu(2); height: width
473+ SlotsLayout.position: SlotsLayout.Last
474+ name: descLayout.showAll ? "go-up" : "go-down"
475+ }
476+ }
477+ }
478+
479+ ListItem {
480+ height: changelogLayout.height
481+ visible: app.changelog
482+ onClicked: changelogLayout.showAll = !changelogLayout.showAll
483+ ListItemLayout {
484+ id: changelogLayout
485+ property bool showAll: false
486+ title.text: i18n.tr("What's New")
487+ subtitle.text: app.changelog
488+ subtitle.textSize: Label.Small
489+ subtitle.wrapMode: Text.WordWrap
490+ subtitle.maximumLineCount: showAll ? Number.MAX_VALUE : 5
491+
492+ Icon {
493+ width: units.gu(2); height: width
494+ SlotsLayout.position: SlotsLayout.Last
495+ name: changelogLayout.showAll ? "go-up" : "go-down"
496+ }
497+ }
498+ }
499+
500+ ListItem {
501+ divider.visible: false
502+ ListItemLayout {
503+ anchors.centerIn: parent
504+ title.text: i18n.tr("Packager/Publisher")
505+ subtitle.text: app.maintainer || i18n.tr("OpenStore team")
506+ }
507+ }
508+
509+ ListItem {
510+ divider.visible: false
511+ ListItemLayout {
512+ anchors.centerIn: parent
513+ title.text: i18n.tr("Installed version")
514+ subtitle.text: app.installedVersion || "None"
515+ }
516+ }
517+
518+ ListItem {
519+ divider.visible: false
520+ ListItemLayout {
521+ anchors.centerIn: parent
522+
523+ title.text: i18n.tr("Latest available version")
524+ subtitle.text: app.version
525+ }
526+ }
527+
528+ ListItem {
529+ divider.visible: false
530+ ListItemLayout {
531+ anchors.centerIn: parent
532+ title.text: i18n.tr("License") || i18n.tr("<i>N/A</i>")
533+ subtitle.text: app.license
534+ }
535+ }
536+
537+ ListItem {
538+ onClicked: Qt.openUrlExternally(app.source)
539+ ListItemLayout {
540+ anchors.centerIn: parent
541+ title.text: i18n.tr("Source Code") || i18n.tr("<i>N/A</i>")
542+ subtitle.text: app.source
543+ ProgressionSlot { visible: app.source }
544+ }
545+ }
546+
547+ ListItem {
548+ onClicked: {
549+ // FIXME: I don't like this heuristic, but there's no other way to get a reference
550+ // to the page that pushed 'appDetailsPage' into the stack.
551+ // The parent node is actually the PageWrapper that created this page, 'parentPage' is one of its properties.
552+ //var realParentPage = appDetailsPage.parentNode.parentPage
553+
554+ var pageProps = {
555+ title: app.author,
556+ filterPattern: new RegExp(app.author),
557+ filterProperty: "author"
558+ }
559+
560+ appDetailsPage.pageStack.addPageToCurrentColumn(/*realParentPage*/ appDetailsPage, filteredAppPageComponent, pageProps)
561+ }
562+ ListItemLayout {
563+ anchors.centerIn: parent
564+ title.text: i18n.tr("More from %1").arg(app.author)
565+ ProgressionSlot {}
566+ }
567+ }
568+
569+ ListItem {
570+ onClicked: {
571+ // FIXME: I don't like this heuristic, but there's no other way to get a reference
572+ // to the page that pushed 'appDetailsPage' into the stack.
573+ // The parent node is actually the PageWrapper that created this page, 'parentPage' is one of its properties.
574+ //var realParentPage = appDetailsPage.parentNode.parentPage
575+
576+ var pageProps = {
577+ title: app.category,
578+ filterPattern: new RegExp(app.category),
579+ filterProperty: "category"
580+ }
581+
582+ appDetailsPage.pageStack.addPageToCurrentColumn(/*realParentPage*/ appDetailsPage, filteredAppPageComponent, pageProps)
583+ }
584+ ListItemLayout {
585+ anchors.centerIn: parent
586+ // FIXME: app.category is not localized.
587+ title.text: i18n.tr("Other apps in %1").arg(app.category)
588+ ProgressionSlot {}
589+ }
590+ }
591+
592+ SectionDivider {
593+ text: i18n.tr("Package contents")
594 }
595
596 Repeater {
597 model: app.hooksCount
598
599- delegate: Column {
600- width: parent.width
601+ delegate: ListItem {
602+ height: hookDelLayout.height + units.gu(3)
603+
604 property var hooks: app.hooks(index)
605 property string permissions: app.permissions(index)
606 property string readpaths: app.readPaths(index)
607 property string writepaths: app.writePaths(index)
608 property string hookName: app.hookName(index)
609 property string apparmorTemplate: app.apparmorTemplate(index)
610- spacing: units.gu(1)
611-
612- RowLayout {
613- width: parent.width
614-
615- Label {
616- text: hookName
617- Layout.fillWidth: true
618- font.bold: true
619- }
620- HookIcon {
621- Layout.preferredHeight: units.gu(4)
622- Layout.preferredWidth: units.gu(4)
623- name: "stock_application"
624- visible: (hooks & ApplicationItem.HookDesktop)
625- }
626- HookIcon {
627- Layout.preferredHeight: units.gu(4)
628- Layout.preferredWidth: units.gu(4)
629- name: "search"
630- visible: (hooks & ApplicationItem.HookScope)
631- }
632- HookIcon {
633- Layout.preferredHeight: units.gu(4)
634- Layout.preferredWidth: units.gu(4)
635- name: "stock_website"
636- visible: (hooks & ApplicationItem.HookUrls)
637- }
638- HookIcon {
639- Layout.preferredHeight: units.gu(4)
640- Layout.preferredWidth: units.gu(4)
641- name: "share"
642- visible: (hooks & ApplicationItem.HookContentHub)
643- }
644- HookIcon {
645- Layout.preferredHeight: units.gu(4)
646- Layout.preferredWidth: units.gu(4)
647- name: "notification"
648- visible: (hooks & ApplicationItem.HookPushHelper)
649- }
650- HookIcon {
651- Layout.preferredHeight: units.gu(4)
652- Layout.preferredWidth: units.gu(4)
653- name: "contact-group"
654- visible: (hooks & ApplicationItem.HookAccountService)
655- }
656- }
657- RowLayout {
658- anchors { left: parent.left; right: parent.right }
659- spacing: units.gu(1)
660- Icon {
661- Layout.preferredHeight: units.gu(3)
662- Layout.preferredWidth: units.gu(3)
663- implicitHeight: height
664- implicitWidth: width
665- name: "security-alert"
666- visible: apparmorTemplate.indexOf("unconfined") >= 0
667- }
668-
669- Label {
670- id: templateLabel
671- Layout.fillWidth: true
672- text: "Apparmor profile: " + apparmorTemplate
673- visible: apparmorTemplate
674- color: apparmorTemplate.indexOf("unconfined") >= 0 ? UbuntuColors.red : permissionLabel.color
675- anchors.verticalCenter: parent.verticalCenter
676- }
677- }
678-
679-
680- Row {
681- anchors { left: parent.left; right: parent.right }
682- spacing: units.gu(1)
683- visible: permissions.length > 0
684-
685- Icon {
686- Layout.preferredHeight: units.gu(3)
687- Layout.preferredWidth: units.gu(3)
688- implicitHeight: height
689- implicitWidth: width
690- name: "security-alert"
691- }
692-
693- Label {
694- id: permissionLabel
695- text: "Permissions: " + (permissions ? permissions : "<i>none</i>")
696+
697+ Column {
698+ id: hookDelLayout
699+ anchors { left: parent.left; right: parent.right; margins: units.gu(2) }
700+ y: units.gu(1)
701+ spacing: units.gu(1)
702+
703+ RowLayout {
704 width: parent.width
705- wrapMode: Text.WordWrap
706- anchors.verticalCenter: parent.verticalCenter
707- }
708- }
709-
710- RowLayout {
711- anchors { left: parent.left; right: parent.right }
712- spacing: units.gu(1)
713- visible: readpaths.length > 0
714- Icon {
715- Layout.preferredHeight: units.gu(3)
716- Layout.preferredWidth: units.gu(3)
717- implicitHeight: height
718- implicitWidth: width
719- name: "security-alert"
720- }
721- Label {
722- text: "Read paths: " + readpaths
723- Layout.fillWidth: true
724- wrapMode: Text.WordWrap
725- anchors.verticalCenter: parent.verticalCenter
726- }
727- }
728- RowLayout {
729- anchors { left: parent.left; right: parent.right }
730- spacing: units.gu(1)
731- visible: writepaths.length > 0
732- Icon {
733- Layout.preferredHeight: units.gu(3)
734- Layout.preferredWidth: units.gu(3)
735- implicitHeight: height
736- implicitWidth: width
737- name: "security-alert"
738- }
739-
740- Label {
741- text: "Write paths: " + writepaths
742- Layout.fillWidth: true
743- wrapMode: Text.WordWrap
744- anchors.verticalCenter: parent.verticalCenter
745- }
746- }
747-
748- Button {
749- anchors { left: parent.left }
750- text: "Open"
751- color: UbuntuColors.green
752- visible: app.installed && (hooks & ApplicationItem.HookDesktop)
753- onClicked: Qt.openUrlExternally("appid://" + app.appId + "/" + hookName + "/" + app.installedVersion)
754- }
755- }
756- }
757- ThinDivider { }
758-
759- Label {
760- anchors { left: parent.left; right: parent.right }
761- text: "License: " + app.license
762- wrapMode: Text.WordWrap
763- }
764- Label {
765- anchors { left: parent.left; right: parent.right }
766- text: "Source code:"
767- wrapMode: Text.WordWrap
768- }
769- AbstractButton {
770- anchors { left: parent.left; right: parent.right }
771- height: linkLabel.implicitHeight
772- Label {
773- id: linkLabel
774- anchors { left: parent.left; right: parent.right }
775- text: app.source
776- wrapMode: Text.WordWrap
777- color: "blue"
778- }
779- onClicked: {
780- Qt.openUrlExternally(app.source)
781- }
782- }
783- }
784+ height: units.gu(4)
785+
786+ Label {
787+ text: hookName
788+ Layout.fillWidth: true
789+ }
790+
791+ HookIcon {
792+ Layout.preferredHeight: units.gu(4)
793+ Layout.preferredWidth: units.gu(4)
794+ name: "stock_application"
795+ visible: (hooks & ApplicationItem.HookDesktop)
796+ }
797+ HookIcon {
798+ Layout.preferredHeight: units.gu(4)
799+ Layout.preferredWidth: units.gu(4)
800+ name: "search"
801+ visible: (hooks & ApplicationItem.HookScope)
802+ }
803+ HookIcon {
804+ Layout.preferredHeight: units.gu(4)
805+ Layout.preferredWidth: units.gu(4)
806+ name: "stock_website"
807+ visible: (hooks & ApplicationItem.HookUrls)
808+ }
809+ HookIcon {
810+ Layout.preferredHeight: units.gu(4)
811+ Layout.preferredWidth: units.gu(4)
812+ name: "share"
813+ visible: (hooks & ApplicationItem.HookContentHub)
814+ }
815+ HookIcon {
816+ Layout.preferredHeight: units.gu(4)
817+ Layout.preferredWidth: units.gu(4)
818+ name: "notification"
819+ visible: (hooks & ApplicationItem.HookPushHelper)
820+ }
821+ HookIcon {
822+ Layout.preferredHeight: units.gu(4)
823+ Layout.preferredWidth: units.gu(4)
824+ name: "contact-group"
825+ visible: (hooks & ApplicationItem.HookAccountService)
826+ }
827+ }
828+
829+ ListItemLayout {
830+ anchors { left: parent.left; right: parent.right }
831+ anchors.leftMargin: units.gu(-2)
832+ height: units.gu(6)
833+ Icon {
834+ SlotsLayout.position: SlotsLayout.Leading
835+ width: units.gu(4); height: width
836+ name: "security-alert"
837+ visible: apparmorTemplate.indexOf("unconfined") >= 0
838+ }
839+
840+ title.text: i18n.tr("AppArmor profile")
841+ subtitle.text: apparmorTemplate || "Ubuntu confined app"
842+ subtitle.color: apparmorTemplate.indexOf("unconfined") >= 0 ? UbuntuColors.red : theme.palette.normal.backgroundSecondaryText
843+ subtitle.maximumLineCount: Number.MAX_VALUE
844+ }
845+
846+
847+ ListItemLayout {
848+ anchors { left: parent.left; right: parent.right }
849+ anchors.leftMargin: units.gu(-2)
850+ height: units.gu(6)
851+ visible: permissions.length > 0
852+
853+ Icon {
854+ property var restrictedPerms: ["bluetooth", "calendar", "contacts", "debug", "history", "music_files", "picture_files", "video_files"]
855+ SlotsLayout.position: SlotsLayout.Leading
856+ width: units.gu(4); height: width
857+ name: "security-alert"
858+ visible: {
859+ var length = restrictedPerms.length;
860+ while(length--) {
861+ if (permissions.indexOf(restrictedPerms[length]) > -1)
862+ return true
863+ }
864+ return false
865+ }
866+ }
867+
868+ title.text: i18n.tr("Permissions")
869+ subtitle.maximumLineCount: Number.MAX_VALUE
870+ subtitle.text: {
871+ if (permissions) {
872+ return permissions.replace("bluetooth", "<font color=\"#ED3146\">bluetooth</font>")
873+ .replace("calendar", "<font color=\"#ED3146\">calendar</font>")
874+ .replace("contacts", "<font color=\"#ED3146\">contacts</font>")
875+ .replace("debug", "<font color=\"#ED3146\">debug</font>")
876+ .replace("history", "<font color=\"#ED3146\">history</font>")
877+ .replace("music_files_read", "<font color=\"#ED3146\">music_files_read</font>")
878+ .replace("picture_files_read", "<font color=\"#ED3146\">music_files_read</font>")
879+ .replace("video_files_read", "<font color=\"#ED3146\">music_files_read</font>")
880+ .replace("music_files", "<font color=\"#ED3146\">music_files_read</font>")
881+ .replace("picture_files", "<font color=\"#ED3146\">music_files_read</font>")
882+ .replace("video_files", "<font color=\"#ED3146\">music_files_read</font>")
883+ }
884+
885+ return i18n.tr("<i>none required</i>")
886+ }
887+ }
888+
889+ ListItemLayout {
890+ anchors { left: parent.left; right: parent.right }
891+ anchors.leftMargin: units.gu(-2)
892+ height: units.gu(6)
893+ visible: readpaths.length > 0
894+
895+ Icon {
896+ SlotsLayout.position: SlotsLayout.Leading
897+ width: units.gu(4); height: width
898+ name: "security-alert"
899+ visible: includesUnconfinedLocations(readpaths)
900+ }
901+
902+ title.text: i18n.tr("Read paths")
903+ subtitle.text: readpaths || i18n.tr("<i>none</i>")
904+ subtitle.maximumLineCount: Number.MAX_VALUE
905+ }
906+
907+ ListItemLayout {
908+ anchors { left: parent.left; right: parent.right }
909+ anchors.leftMargin: units.gu(-2)
910+ height: units.gu(6)
911+ visible: writepaths.length > 0
912+ Icon {
913+ SlotsLayout.position: SlotsLayout.Leading
914+ width: units.gu(4); height: width
915+ name: "security-alert"
916+ visible: includesUnconfinedLocations(writepaths)
917+ }
918+
919+ title.text: i18n.tr("Write paths")
920+ subtitle.text: writepaths || i18n.tr("<i>none</i>")
921+ subtitle.maximumLineCount: Number.MAX_VALUE
922+ }
923+
924+ Button {
925+ anchors { right: parent.right }
926+ text: "Open"
927+ color: UbuntuColors.green
928+ visible: app.installed && (hooks & ApplicationItem.HookDesktop)
929+ onClicked: Qt.openUrlExternally("appid://" + app.appId + "/" + hookName + "/" + app.installedVersion)
930+ }
931+ }
932+ }
933+ }
934+ }
935+ }
936+
937+ function printSize(size) {
938+ var s
939+
940+ s = 1024 * 1024 * 1024
941+ if (size >= s)
942+ // TRANSLATORS: %1 is the size of a file, expressed in GB
943+ return i18n.tr("%1 GB").arg((size / s).toFixed(2));
944+
945+ s = 1024 * 1024
946+ if (size >= s)
947+ // TRANSLATORS: %1 is the size of a file, expressed in MB
948+ return i18n.tr("%1 MB").arg((size / s).toFixed(2));
949+
950+ s = 1024
951+ if (size >= s)
952+ // TRANSLATORS: %1 is the size of a file, expressed in kB
953+ return i18n.tr("%1 kB").arg(parseInt(size / s));
954+
955+ // TRANSLATORS: %1 is the size of a file, expressed in byte
956+ return i18n.tr("%1 byte").arg(size);
957+ }
958+
959+ function includesUnconfinedLocations(paths) {
960+ var p = paths.split(",")
961+ var j = 0
962+
963+ for (var i=0; i < p.length; ++i) {
964+ var x = p[i]
965+ if (x.match(/[^\w\s]/)) {
966+ if (x.indexOf("/home/phablet/.cache/" + app.appId) == -1 && x.indexOf("/home/phablet/.config/" + app.appId) == -1) {
967+ ++j
968+ }
969+ }
970+ }
971+
972+ return (j > 0)
973 }
974 }
975
976=== added file 'openstore/CategoriesPage.qml'
977--- openstore/CategoriesPage.qml 1970-01-01 00:00:00 +0000
978+++ openstore/CategoriesPage.qml 2017-05-08 18:42:06 +0000
979@@ -0,0 +1,60 @@
980+/*
981+ * Copyright (C) 2017 - Stefano Verzegnassi <verzegnassi.stefano@gmail.com>
982+ *
983+ * This program is free software; you can redistribute it and/or modify
984+ * it under the terms of the GNU General Public License as published by
985+ * the Free Software Foundation; version 3.
986+ *
987+ * This program is distributed in the hope that it will be useful,
988+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
989+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
990+ * GNU General Public License for more details.
991+ *
992+ * You should have received a copy of the GNU General Public License
993+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
994+ */
995+
996+import QtQuick 2.4
997+import Ubuntu.Components 1.3
998+
999+Page {
1000+ id: categoryPage
1001+
1002+ header: PageHeader {
1003+ title: i18n.tr("Categories")
1004+ leadingActionBar.actions: Action {
1005+ iconName: "close"
1006+ onTriggered: categoryPage.pageStack.removePages(categoryPage)
1007+ }
1008+ }
1009+
1010+ signal categoryClicked(var name, var code)
1011+
1012+ onCategoryClicked: {
1013+ var pageProps = {
1014+ title: name,
1015+ filterPattern: new RegExp(code.toString()),
1016+ filterProperty: "category"
1017+ }
1018+ categoryPage.pageStack.addPageToNextColumn(categoryPage, filteredAppPageComponent, pageProps)
1019+ }
1020+
1021+ ScrollView {
1022+ anchors.fill: parent
1023+ anchors.topMargin: categoryPage.header.height
1024+
1025+ ListView {
1026+ id: categoryView
1027+ anchors.fill: parent
1028+ model: categories.list
1029+ delegate: ListItem {
1030+ onClicked: categoryPage.categoryClicked(modelData, modelData)
1031+ ListItemLayout {
1032+ anchors.centerIn: parent
1033+ title.text: modelData
1034+ ProgressionSlot {}
1035+ }
1036+ }
1037+ }
1038+ }
1039+}
1040
1041=== added file 'openstore/DiscoverTab.qml'
1042--- openstore/DiscoverTab.qml 1970-01-01 00:00:00 +0000
1043+++ openstore/DiscoverTab.qml 2017-05-08 18:42:06 +0000
1044@@ -0,0 +1,165 @@
1045+/*
1046+ * Copyright (C) 2017 - Stefano Verzegnassi <verzegnassi.stefano@gmail.com>
1047+ *
1048+ * This program is free software; you can redistribute it and/or modify
1049+ * it under the terms of the GNU General Public License as published by
1050+ * the Free Software Foundation; version 3.
1051+ *
1052+ * This program is distributed in the hope that it will be useful,
1053+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1054+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1055+ * GNU General Public License for more details.
1056+ *
1057+ * You should have received a copy of the GNU General Public License
1058+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1059+ */
1060+
1061+import QtQuick 2.4
1062+import Ubuntu.Components 1.3
1063+import OpenStore 1.0
1064+
1065+ScrollView {
1066+ id: rootItem
1067+ anchors.fill: parent
1068+ anchors.topMargin: parent.header ? parent.header.height : 0
1069+
1070+ property var discoverData
1071+ property string discoverApiEndPoint: "https://open.uappexplorer.com/api/v1/apps/discover"
1072+ property AppModel storeModel
1073+
1074+ signal appDetailsRequired(var appId)
1075+ signal categoryViewRequired(var name, var categoryCode)
1076+
1077+ Component.onCompleted: {
1078+ var doc = new XMLHttpRequest();
1079+ doc.onreadystatechange = function() {
1080+ if (doc.readyState == 4 && doc.status == 200) {
1081+ var reply = JSON.parse(doc.responseText)
1082+
1083+ if (reply.success) {
1084+ rootItem.discoverData = reply.data
1085+ } else {
1086+ console.log("Unable to fetch discover data from server (success = false).")
1087+ }
1088+ }
1089+ }
1090+
1091+ doc.open("GET", discoverApiEndPoint, true);
1092+ doc.send();
1093+ }
1094+
1095+ ListView {
1096+ id: view
1097+ anchors.fill: parent
1098+
1099+ header: AbstractButton {
1100+ id: highlightAppControl
1101+ property var appItem: storeModel.app(storeModel.findApp(discoverData.highlighted_id))
1102+ width: parent.width
1103+ height: Math.min(units.gu(28), width * 9 / 16)
1104+
1105+ onClicked: rootItem.appDetailsRequired(discoverData.highlighted_id)
1106+
1107+ Image {
1108+ anchors.fill: parent
1109+ anchors.bottomMargin: units.gu(4)
1110+ source: highlightAppControl.appItem.icon
1111+ fillMode: Image.PreserveAspectCrop
1112+ }
1113+
1114+ ListItemLayout {
1115+ anchors.centerIn: parent
1116+
1117+ title.text: highlightAppControl.appItem.name
1118+ title.font.pixelSize: units.gu(3)
1119+ title.color: "white"
1120+
1121+ subtitle.text: highlightAppControl.appItem.tagline || highlightAppControl.appItem.description
1122+ subtitle.font.pixelSize: units.gu(1.5)
1123+ subtitle.color: "white"
1124+ }
1125+ }
1126+
1127+ model: discoverData ? rootItem.discoverData.categories : null
1128+ delegate: Column {
1129+ width: parent.width
1130+ spacing: units.gu(1)
1131+
1132+ ListItem {
1133+ divider.visible: false
1134+ onClicked: {
1135+ if (modelData.referral) {
1136+ rootItem.categoryViewRequired(modelData.name, modelData.referral)
1137+ }
1138+ }
1139+
1140+ ListItemLayout {
1141+ anchors.centerIn: parent
1142+ title.text: modelData.name
1143+ subtitle.text: modelData.tagline
1144+
1145+ ProgressionSlot {
1146+ visible: modelData.referral != ""
1147+ }
1148+ }
1149+ }
1150+
1151+ ListView {
1152+ anchors { left: parent.left; right: parent.right }
1153+ leftMargin: units.gu(2)
1154+ rightMargin: units.gu(2)
1155+ clip: true
1156+ height: count > 0 ? units.gu(24) : 0
1157+ visible: count > 0
1158+ spacing: units.gu(2)
1159+ orientation: ListView.Horizontal
1160+ model: modelData.ids
1161+ delegate: AbstractButton {
1162+ id: appDel
1163+ property var appItem: storeModel.app(storeModel.findApp(modelData))
1164+ height: parent.height
1165+ width: units.gu(16)
1166+
1167+ onClicked: rootItem.appDetailsRequired(modelData)
1168+
1169+ Column {
1170+ anchors.fill: parent
1171+
1172+ UbuntuShape {
1173+ width: parent.width
1174+ height: width
1175+ aspect: UbuntuShape.Flat
1176+ sourceFillMode: UbuntuShape.PreserveAspectFit
1177+ source: Image {
1178+ source: appDel.appItem.icon
1179+ }
1180+ }
1181+
1182+ ListItemLayout {
1183+ anchors {
1184+ left: parent.left; leftMargin: units.gu(-1)
1185+ right: parent.right
1186+ }
1187+
1188+ height: units.gu(4)
1189+ title {
1190+ text: appDel.appItem.name
1191+ textSize: Label.Small
1192+ wrapMode: Text.WrapAtWordBoundaryOrAnywhere
1193+ maximumLineCount: 2
1194+ }
1195+
1196+ subtitle {
1197+ text: appDel.appItem.author
1198+ textSize: Label.XSmall
1199+ }
1200+
1201+ summary.text: appDel.appItem.installed ? appDel.appItem.updateAvailable ? i18n.tr("Update available") : i18n.tr("Installed") : ""
1202+ summary.textSize: Label.XSmall
1203+ }
1204+ }
1205+ }
1206+ }
1207+ }
1208+ }
1209+}
1210
1211=== added file 'openstore/EmptyState.qml'
1212--- openstore/EmptyState.qml 1970-01-01 00:00:00 +0000
1213+++ openstore/EmptyState.qml 2017-05-08 18:42:06 +0000
1214@@ -0,0 +1,74 @@
1215+/*
1216+ * Copyright (C) 2014, 2015, 2016 Canonical Ltd
1217+ *
1218+ * This file is part of Ubuntu Clock App
1219+ *
1220+ * Ubuntu Clock App is free software: you can redistribute it and/or modify
1221+ * it under the terms of the GNU General Public License version 3 as
1222+ * published by the Free Software Foundation.
1223+ *
1224+ * Ubuntu Clock App is distributed in the hope that it will be useful,
1225+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1226+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1227+ * GNU General Public License for more details.
1228+ *
1229+ * You should have received a copy of the GNU General Public License
1230+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1231+ */
1232+
1233+import QtQuick 2.4
1234+import Ubuntu.Components 1.3
1235+
1236+/*
1237+ Component which displays an empty state (approved by design). It offers an
1238+ icon, title and subtitle to describe the empty state.
1239+*/
1240+
1241+Column {
1242+ id: emptyState
1243+ spacing: units.gu(2)
1244+ width: units.gu(36)
1245+
1246+ // Public APIs
1247+ default property alias iconPlaceholder: iconContainer.data
1248+ property alias iconName: emptyIcon.name
1249+ property alias title: emptyLabel.text
1250+ property alias subTitle: emptySublabel.text
1251+
1252+ Item {
1253+ width: childrenRect.width
1254+ height: childrenRect.height + units.gu(2)
1255+ Icon {
1256+ id: emptyIcon
1257+ height: visible ? units.gu(10) : 0
1258+ width: visible ? height : 0
1259+ color: "#BBBBBB"
1260+ visible: name || source
1261+ }
1262+ Row {
1263+ id: iconContainer
1264+ anchors.horizontalCenter: parent.horizontalCenter
1265+ }
1266+ }
1267+
1268+ Label {
1269+ id: emptyLabel
1270+ width: parent.width
1271+ horizontalAlignment: Text.AlignLeft
1272+ textSize: Label.XLarge
1273+
1274+ elide: Text.ElideRight
1275+ wrapMode: Text.WordWrap
1276+ maximumLineCount: 2
1277+ }
1278+
1279+ Label {
1280+ id: emptySublabel
1281+ width: parent.width
1282+ horizontalAlignment: Text.AlignLeft
1283+ textSize: Label.Medium
1284+
1285+ elide: Text.ElideRight
1286+ wrapMode: Text.WordWrap
1287+ }
1288+}
1289
1290=== added file 'openstore/FilteredAppView.qml'
1291--- openstore/FilteredAppView.qml 1970-01-01 00:00:00 +0000
1292+++ openstore/FilteredAppView.qml 2017-05-08 18:42:06 +0000
1293@@ -0,0 +1,108 @@
1294+/*
1295+ * Copyright (C) 2015 Michael Zanetti <michael.zanetti@ubuntu.com>
1296+ * Copyright (C) 2017 Stefano Verzegnassi <verzegnassi.stefano@gmail.com>
1297+ *
1298+ * This program is free software; you can redistribute it and/or modify
1299+ * it under the terms of the GNU General Public License as published by
1300+ * the Free Software Foundation; version 3.
1301+ *
1302+ * This program is distributed in the hope that it will be useful,
1303+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1304+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1305+ * GNU General Public License for more details.
1306+ *
1307+ * You should have received a copy of the GNU General Public License
1308+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1309+ */
1310+
1311+import QtQuick 2.4
1312+import Ubuntu.Components 1.3
1313+
1314+ScrollView {
1315+ id: rootItem
1316+ anchors.fill: parent
1317+ anchors.topMargin: parent.header ? parent.header.height : 0
1318+
1319+ property var filterPattern: new RegExp()
1320+ property string filterProperty
1321+
1322+ property string sortProperty
1323+ property int sortOrder
1324+
1325+ property alias model: sortedFilteredAppModel.model
1326+ property alias view: view
1327+
1328+ property bool showTicks: true
1329+
1330+ signal appDetailsRequired(var appId)
1331+
1332+ ListView {
1333+ id: view
1334+ model: SortFilterModel {
1335+ id: sortedFilteredAppModel
1336+
1337+ filter.pattern: rootItem.filterPattern
1338+ filter.property: rootItem.filterProperty
1339+
1340+ sort.property: rootItem.sortProperty
1341+ sort.order: rootItem.sortOrder
1342+ }
1343+
1344+ // TODO: Move it in Main.qml or elsewhere
1345+ onCountChanged: {
1346+ if (count > 0 && root.appIdToOpen != "") {
1347+ var index = appModel.findApp(root.appIdToOpen)
1348+ if (index >= 0) {
1349+ pageStack.addPageToNextColumn(mainPage, Qt.resolvedUrl("AppDetailsPage.qml"), {app: appModel.app(index)})
1350+ root.appIdToOpen = "";
1351+ }
1352+ }
1353+ }
1354+
1355+ delegate: ListItem {
1356+ height: layout.height + divider.height
1357+
1358+ ListItemLayout {
1359+ id: layout
1360+ title.text: model.name
1361+ summary.text: model.tagline
1362+
1363+ UbuntuShape {
1364+ SlotsLayout.position: SlotsLayout.Leading
1365+ aspect: UbuntuShape.Flat
1366+ image: Image {
1367+ source: model.icon
1368+ height: parent.height
1369+ width: parent.width
1370+ }
1371+ }
1372+ Icon {
1373+ SlotsLayout.position: SlotsLayout.Trailing
1374+ height: units.gu(2)
1375+ width: height
1376+ implicitHeight: height
1377+ implicitWidth: width
1378+ visible: model.installed && rootItem.showTicks
1379+ name: "tick"
1380+ color: model.updateAvailable ? UbuntuColors.orange : UbuntuColors.green
1381+ }
1382+
1383+ ProgressionSlot {}
1384+ }
1385+ onClicked: {
1386+ rootItem.appDetailsRequired(model.appId)
1387+ }
1388+ }
1389+
1390+ Loader {
1391+ anchors.centerIn: parent
1392+ active: view.count == 0
1393+ sourceComponent: EmptyState {
1394+ title: rootItem.filterProperty == "category" ? i18n.tr("Nothing here yet") : i18n.tr("No result found.").arg(rootItem.filterPattern)
1395+ subTitle: rootItem.filterProperty == "category" ? i18n.tr("No app has been released in this department yet.") : i18n.tr("Try with a different search.")
1396+ iconName: rootItem.filterProperty == "category" ? "ubuntu-store-symbolic" : "search"
1397+ anchors.centerIn: parent
1398+ }
1399+ }
1400+ }
1401+}
1402
1403=== modified file 'openstore/Main.qml'
1404--- openstore/Main.qml 2016-08-21 10:29:12 +0000
1405+++ openstore/Main.qml 2017-05-08 18:42:06 +0000
1406@@ -20,6 +20,7 @@
1407 import QtQuick.Layouts 1.1
1408 import Qt.labs.settings 1.0
1409 import Ubuntu.Content 1.3
1410+import QtQml.Models 2.1
1411
1412 MainView {
1413 id: root
1414@@ -82,6 +83,7 @@
1415 AppModel {
1416 id: appModel
1417 installer: installer
1418+ onRepositoryListFetched: repoFetchingIndicator.visible = false
1419 }
1420
1421 ServiceRegistry {
1422@@ -89,6 +91,31 @@
1423 clickInstaller: installer
1424 }
1425
1426+ QtObject {
1427+ id: categories
1428+
1429+ property string categoriesApiEndPoint: "https://open.uappexplorer.com/api/v1/categories"
1430+ property var list
1431+
1432+ Component.onCompleted: {
1433+ var doc = new XMLHttpRequest();
1434+ doc.onreadystatechange = function() {
1435+ if (doc.readyState == 4 && doc.status == 200) {
1436+ var reply = JSON.parse(doc.responseText)
1437+ if (reply.success) {
1438+ list = reply.data
1439+ } else {
1440+ console.log("Unable to fetch categories from server (success = false).")
1441+ }
1442+ }
1443+ }
1444+
1445+ doc.open("GET", categoriesApiEndPoint, true);
1446+ doc.send();
1447+ }
1448+
1449+ }
1450+
1451 property bool contentHubInstallInProgress: false
1452 Connections {
1453 target: ContentHub
1454@@ -113,54 +140,171 @@
1455 id: mainPage
1456 header: PageHeader {
1457 title: i18n.tr("Open Store")
1458+ automaticHeight: false
1459+
1460+ leadingActionBar.actions: Action {
1461+ iconName: "navigation-menu"
1462+ text: i18n.tr("Categories")
1463+ onTriggered: mainPage.pageStack.addPageToCurrentColumn(mainPage, Qt.resolvedUrl("CategoriesPage.qml"), {})
1464+ }
1465+
1466+ trailingActionBar.actions: Action {
1467+ iconName: "find"
1468+ text: i18n.tr("Search")
1469+ onTriggered: {
1470+ mainPage.pageStack.addPageToCurrentColumn(mainPage, searchPageComponent, {})
1471+ }
1472+ }
1473+
1474+ sections {
1475+ model: [ i18n.tr("Discover"), i18n.tr("My Apps") ]
1476+ selectedIndex: 0 // Should always match "Discover"
1477+ onSelectedIndexChanged: {
1478+ // Current section has changed, if there was an opened page
1479+ // in the second column, it is not anymore related to the
1480+ // new current section. Remove it.
1481+ mainPage.pageStack.removePages(mainPage)
1482+ }
1483+ }
1484 }
1485
1486-
1487 ListView {
1488- anchors.fill: parent
1489- anchors.topMargin: mainPage.header.height
1490+ id: view
1491+ anchors {
1492+ top: mainPage.header.bottom
1493+ bottom: parent.bottom
1494+ left: parent.left
1495+ right: parent.right
1496+ }
1497+
1498+ clip: true
1499+ orientation: ListView.Horizontal
1500+ interactive: false
1501+ snapMode: ListView.SnapOneItem
1502+ highlightMoveDuration: 0
1503+ currentIndex: mainPage.header.sections.selectedIndex
1504+
1505+ model: ObjectModel {
1506+ Loader {
1507+ id: discoverTabLoader
1508+ width: view.width
1509+ height: view.height
1510+ asynchronous: true
1511+ source: Qt.resolvedUrl("DiscoverTab.qml")
1512+
1513+ active: false
1514+ Connections {
1515+ target: appModel
1516+ onRepositoryListFetched: discoverTabLoader.active = true
1517+ }
1518+
1519+ onLoaded: {
1520+ item.storeModel = appModel
1521+
1522+ item.appDetailsRequired.connect(function(appId) {
1523+ var pageProps = {
1524+ app: appModel.app(appModel.findApp(appId))
1525+ }
1526+ mainPage.pageStack.addPageToNextColumn(mainPage, Qt.resolvedUrl("AppDetailsPage.qml"), pageProps)
1527+ })
1528+
1529+ item.categoryViewRequired.connect(function(name, code) {
1530+ var pageProps = {
1531+ title: name,
1532+ filterPattern: new RegExp(code.toString()),
1533+ filterProperty: "category"
1534+ }
1535+
1536+ mainPage.pageStack.removePages(mainPage)
1537+ mainPage.pageStack.addPageToCurrentColumn(mainPage, filteredAppPageComponent, pageProps)
1538+ })
1539+ }
1540+ }
1541+ Loader {
1542+ width: view.width
1543+ height: view.height
1544+ asynchronous: true
1545+ source: Qt.resolvedUrl("FilteredAppView.qml")
1546+
1547+ onLoaded: {
1548+ item.model = appModel
1549+
1550+ item.filterProperty = "installed"
1551+ item.filterPattern = new RegExp("true")
1552+
1553+ item.sortProperty = "updateAvailable"
1554+ item.sortOrder = Qt.DescendingOrder
1555+
1556+ item.view.section.property = "updateAvailable"
1557+ item.view.section.delegate = updateDivider
1558+
1559+ item.showTicks = false
1560+
1561+ item.appDetailsRequired.connect(function(appId) {
1562+ var pageProps = {
1563+ app: appModel.app(appModel.findApp(appId))
1564+ }
1565+ mainPage.pageStack.addPageToNextColumn(mainPage, Qt.resolvedUrl("AppDetailsPage.qml"), pageProps)
1566+ })
1567+ }
1568+
1569+ Component {
1570+ id: updateDivider
1571+ SectionDivider {
1572+ text: section == "true" ? i18n.tr("Available updates") : i18n.tr("Installed apps")
1573+ }
1574+ }
1575+ }
1576+ }
1577+ }
1578+
1579+ Column {
1580+ id: repoFetchingIndicator
1581+ anchors.centerIn: parent
1582+ anchors.verticalCenterOffset: mainPage.header.height * 0.4
1583+ spacing: units.gu(1)
1584+ ActivityIndicator {
1585+ anchors.horizontalCenter: parent.horizontalCenter
1586+ running: visible
1587+ }
1588+ Label {
1589+ textSize: Label.Small
1590+ text: i18n.tr("Fetching package list...")
1591+ }
1592+ }
1593+ }
1594+ }
1595+
1596+ Component {
1597+ id: searchPageComponent
1598+
1599+ SearchPage {
1600+ id: searchPage
1601+ model: appModel
1602+
1603+ onAppDetailsRequired: {
1604+ var pageProps = { app: appModel.app(appModel.findApp(appId)) }
1605+ searchPage.pageStack.addPageToNextColumn(searchPage, Qt.resolvedUrl("AppDetailsPage.qml"), pageProps)
1606+ }
1607+ }
1608+ }
1609+
1610+ Component {
1611+ id: filteredAppPageComponent
1612+ Page {
1613+ id: filteredAppPage
1614+ property alias filterPattern: filteredAppView.filterPattern
1615+ property alias filterProperty: filteredAppView.filterProperty
1616+ header: PageHeader {
1617+ title: filteredAppPage.title
1618+ automaticHeight: false
1619+ }
1620+ FilteredAppView {
1621+ id: filteredAppView
1622 model: appModel
1623-
1624- onCountChanged: {
1625- if (count > 0 && root.appIdToOpen != "") {
1626- var index = appModel.findApp(root.appIdToOpen)
1627- if (index >= 0) {
1628- pageStack.addPageToNextColumn(mainPage, Qt.resolvedUrl("AppDetailsPage.qml"), {app: appModel.app(index)})
1629- root.appIdToOpen = "";
1630- }
1631- }
1632- }
1633-
1634- delegate: ListItem {
1635- height: layout.height + divider.height
1636-
1637- ListItemLayout {
1638- id: layout
1639- title.text: model.name
1640- summary.text: model.tagline
1641-
1642- UbuntuShape {
1643- SlotsLayout.position: SlotsLayout.Leading
1644- image: Image {
1645- source: model.icon
1646- height: parent.height
1647- width: parent.width
1648- }
1649- }
1650- Icon {
1651- SlotsLayout.position: SlotsLayout.Trailing
1652- height: units.gu(2)
1653- width: height
1654- implicitHeight: height
1655- implicitWidth: width
1656- visible: model.installed
1657- name: "tick"
1658- color: model.installedVersion >= model.version ? UbuntuColors.green : UbuntuColors.orange
1659- }
1660- }
1661- onClicked: {
1662- pageStack.addPageToNextColumn(mainPage, Qt.resolvedUrl("AppDetailsPage.qml"), {app: appModel.app(index)})
1663- }
1664+ onAppDetailsRequired: {
1665+ var pageProps = { app: appModel.app(appModel.findApp(appId)) }
1666+ filteredAppPage.pageStack.addPageToNextColumn(filteredAppPage, Qt.resolvedUrl("AppDetailsPage.qml"), pageProps)
1667 }
1668 }
1669 }
1670
1671=== added file 'openstore/SearchPage.qml'
1672--- openstore/SearchPage.qml 1970-01-01 00:00:00 +0000
1673+++ openstore/SearchPage.qml 2017-05-08 18:42:06 +0000
1674@@ -0,0 +1,87 @@
1675+/*
1676+ * Copyright (C) 2017 - Stefano Verzegnassi <verzegnassi.stefano@gmail.com>
1677+ *
1678+ * This program is free software; you can redistribute it and/or modify
1679+ * it under the terms of the GNU General Public License as published by
1680+ * the Free Software Foundation; version 3.
1681+ *
1682+ * This program is distributed in the hope that it will be useful,
1683+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1684+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1685+ * GNU General Public License for more details.
1686+ *
1687+ * You should have received a copy of the GNU General Public License
1688+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1689+ */
1690+
1691+import QtQuick 2.4
1692+import Ubuntu.Components 1.3
1693+
1694+Page {
1695+ id: searchPage
1696+
1697+ property alias model: view.model
1698+ property alias query: searchField.text
1699+
1700+ signal appDetailsRequired(var appId)
1701+
1702+ header: PageHeader {
1703+ title: i18n.tr("Search")
1704+ automaticHeight: false
1705+ leadingActionBar.actions: null
1706+ trailingActionBar {
1707+ anchors.rightMargin: 0
1708+ delegate: TextualButtonStyle {}
1709+
1710+ actions: Action {
1711+ text: i18n.tr("Cancel")
1712+
1713+ onTriggered: {
1714+ // Clear the search
1715+ searchField.text = ""
1716+ searchPage.pageStack.removePages(searchPage)
1717+ }
1718+ }
1719+ }
1720+
1721+ contents: TextField {
1722+ id: searchField
1723+ anchors {
1724+ left: parent.left
1725+ right: parent.right
1726+ verticalCenter: parent.verticalCenter
1727+ }
1728+
1729+ primaryItem: Icon {
1730+ height: units.gu(2); width: height
1731+ name: "search"
1732+ }
1733+
1734+ placeholderText: i18n.tr("search in OpenStore...")
1735+ onTextChanged: view.search(text)
1736+ Component.onCompleted: view.search(text)
1737+
1738+ // Disable predictive text
1739+ inputMethodHints: Qt.ImhNoPredictiveText
1740+
1741+ onVisibleChanged: forceActiveFocus()
1742+ }
1743+ }
1744+
1745+ FilteredAppView {
1746+ id: view
1747+
1748+ // FIXME: TODO: Use "number of downloads" or "last updated" when they'll be available
1749+ sortOrder: Qt.AscendingOrder
1750+ sortProperty: "name"
1751+ filterProperty: "searchHackishString"
1752+ //filterPattern: new RegExp("$a") // A kind way to say SortFilterModel not to match anything until searchField is filled.
1753+
1754+ function search(text) {
1755+ //view.filterPattern = text ? new RegExp(text, 'i') : new RegExp("$a")
1756+ view.filterPattern = new RegExp(text, 'i')
1757+ }
1758+
1759+ onAppDetailsRequired: searchPage.appDetailsRequired(appId)
1760+ }
1761+}
1762
1763=== added file 'openstore/SectionDivider.qml'
1764--- openstore/SectionDivider.qml 1970-01-01 00:00:00 +0000
1765+++ openstore/SectionDivider.qml 2017-05-08 18:42:06 +0000
1766@@ -0,0 +1,52 @@
1767+import QtQuick 2.4
1768+import Ubuntu.Components 1.3
1769+
1770+Rectangle {
1771+ id: rootItem
1772+
1773+ property alias text: sectionLabel.text
1774+ property alias iconName: icon.name
1775+
1776+ anchors { left: parent.left; right: parent.right }
1777+ height: units.gu(4)
1778+
1779+ color: theme.palette.normal.foreground
1780+
1781+ Row {
1782+ anchors {
1783+ left: parent.left
1784+ right: parent.right
1785+ margins: units.gu(2)
1786+ verticalCenter: parent.verticalCenter
1787+ }
1788+ spacing: units.gu(1)
1789+
1790+ Icon {
1791+ id: icon
1792+ height: units.gu(2)
1793+ width: name ? units.gu(2) : 0
1794+ anchors.verticalCenter: parent.verticalCenter
1795+ }
1796+
1797+ Label {
1798+ id: sectionLabel
1799+ anchors.verticalCenter: parent.verticalCenter
1800+ }
1801+ }
1802+
1803+ Rectangle {
1804+ anchors {
1805+ left: parent.left
1806+ right: parent.right
1807+ bottom: parent.bottom
1808+ }
1809+
1810+ height: units.dp(2)
1811+ gradient: Gradient {
1812+ GradientStop { position: 0.0; color: Qt.rgba(0, 0, 0, 0.1) }
1813+ GradientStop { position: 0.49; color: Qt.rgba(0, 0, 0, 0.1) }
1814+ GradientStop { position: 0.5; color: Qt.rgba(1, 1, 1, 0.4) }
1815+ GradientStop { position: 1.0; color: Qt.rgba(1, 1, 1, 0.4) }
1816+ }
1817+ }
1818+}
1819
1820=== added file 'openstore/TextualButtonStyle.qml'
1821--- openstore/TextualButtonStyle.qml 1970-01-01 00:00:00 +0000
1822+++ openstore/TextualButtonStyle.qml 2017-05-08 18:42:06 +0000
1823@@ -0,0 +1,67 @@
1824+/*
1825+ * Copyright (C) 2016 Canonical, Ltd.
1826+ *
1827+ * This program is free software; you can redistribute it and/or modify
1828+ * it under the terms of the GNU General Public License as published by
1829+ * the Free Software Foundation; version 3.
1830+ *
1831+ * This program is distributed in the hope that it will be useful,
1832+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1833+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1834+ * GNU General Public License for more details.
1835+ *
1836+ * You should have received a copy of the GNU General Public License
1837+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1838+ */
1839+
1840+import QtQuick 2.4
1841+import Ubuntu.Components 1.3
1842+
1843+Component {
1844+ id: textualButton
1845+ AbstractButton {
1846+ id: button
1847+ action: modelData
1848+ width: layout.width + units.gu(4)
1849+ height: parent.height
1850+ Rectangle {
1851+ color: UbuntuColors.slate
1852+ opacity: 0.1
1853+ anchors.fill: parent
1854+ visible: button.pressed
1855+ }
1856+ Row {
1857+ id: layout
1858+ anchors.centerIn: parent
1859+ spacing: units.gu(1)
1860+ Icon {
1861+ anchors.verticalCenter: parent.verticalCenter
1862+ width: visible ? units.gu(2) : 0
1863+ height: width
1864+ name: action.iconName
1865+ source: action.iconSource
1866+ visible: (name != "") || (source != "")
1867+ color: {
1868+ if (button.enabled)
1869+ return text === i18n.tr("Pick") ? theme.palette.selected.backgroundText : theme.palette.normal.backgroundText
1870+
1871+ return theme.palette.disabled.backgroundText
1872+ }
1873+ }
1874+ Label {
1875+ anchors.verticalCenter: parent.verticalCenter
1876+ text: action.text
1877+ font.weight: text === i18n.tr("Pick") ? Font.Normal : Font.Light
1878+ // Hide text from overflow button of ActionBar
1879+ visible: text !== "More"
1880+ width: visible ? paintedWidth : 0
1881+ color: {
1882+ if (button.enabled)
1883+ return text === i18n.tr("Pick") ? theme.palette.selected.backgroundText : theme.palette.normal.backgroundText
1884+
1885+ return theme.palette.disabled.backgroundText
1886+ }
1887+ }
1888+ }
1889+ }
1890+}
1891
1892=== modified file 'openstore/appmodel.cpp'
1893--- openstore/appmodel.cpp 2016-08-21 10:29:12 +0000
1894+++ openstore/appmodel.cpp 2017-05-08 18:42:06 +0000
1895@@ -49,6 +49,8 @@
1896 switch (role) {
1897 case RoleName:
1898 return m_list.at(index.row())->name();
1899+ case RoleAppId:
1900+ return m_list.at(index.row())->appId();
1901 case RoleIcon:
1902 return m_list.at(index.row())->icon();
1903 case RoleAuthor:
1904@@ -57,6 +59,8 @@
1905 return m_list.at(index.row())->tagline();
1906 case RoleDescription:
1907 return m_list.at(index.row())->description();
1908+ case RoleCategory:
1909+ return m_list.at(index.row())->category();
1910 case RoleScreenshots:
1911 return m_list.at(index.row())->screenshots();
1912 case RoleChangelog:
1913@@ -69,8 +73,12 @@
1914 return m_list.at(index.row())->installed();
1915 case RoleInstalledVersion:
1916 return m_list.at(index.row())->installedVersion();
1917+ case RoleUpdateAvailable:
1918+ return bool(m_list.at(index.row())->installedVersion() < m_list.at(index.row())->version());
1919 case RoleMaintainer:
1920 return m_list.at(index.row())->maintainer();
1921+ case RoleSearchHackishString:
1922+ return QString(m_list.at(index.row())->name()) + QString(m_list.at(index.row())->appId()) + QString(m_list.at(index.row())->author());
1923 }
1924 return QVariant();
1925 }
1926@@ -79,17 +87,21 @@
1927 {
1928 QHash<int, QByteArray> roles;
1929 roles.insert(RoleName, "name");
1930+ roles.insert(RoleAppId, "appId");
1931 roles.insert(RoleIcon, "icon");
1932 roles.insert(RoleAuthor, "author");
1933 roles.insert(RoleTagline, "tagline");
1934 roles.insert(RoleDescription, "description");
1935+ roles.insert(RoleCategory, "category");
1936 roles.insert(RoleScreenshots, "screenshots");
1937 roles.insert(RoleChangelog, "changelog");
1938 roles.insert(RolePackageUrl, "packageUrl");
1939 roles.insert(RoleVersion, "version");
1940 roles.insert(RoleInstalled, "installed");
1941 roles.insert(RoleInstalledVersion, "installedVersion");
1942+ roles.insert(RoleUpdateAvailable, "updateAvailable");
1943 roles.insert(RoleMaintainer, "maintainer");
1944+ roles.insert(RoleSearchHackishString, "searchHackishString");
1945 return roles;
1946 }
1947
1948@@ -199,6 +211,7 @@
1949 item->setMaintainer(packageMap.value("maintainer_name").toString());
1950 item->setTagline(packageMap.value("tagline").toString());
1951 item->setDescription(packageMap.value("description").toString());
1952+ item->setCategory(packageMap.value("category").toString());
1953 item->setScreenshots(packageMap.value("screenshots").toStringList());
1954 item->setChangelog(packageMap.value("changelog").toString());
1955 item->setVersion(packageMap.value("version").toString());
1956@@ -257,6 +270,8 @@
1957 m_list.append(item);
1958 }
1959 endResetModel();
1960+
1961+ Q_EMIT repositoryListFetched();
1962 }
1963
1964 void AppModel::buildInstalledClickList()
1965
1966=== modified file 'openstore/appmodel.h'
1967--- openstore/appmodel.h 2016-08-21 10:29:12 +0000
1968+++ openstore/appmodel.h 2017-05-08 18:42:06 +0000
1969@@ -16,6 +16,7 @@
1970 Q_PROPERTY(QString author READ author CONSTANT)
1971 Q_PROPERTY(QString tagline READ tagline CONSTANT)
1972 Q_PROPERTY(QString description READ description CONSTANT)
1973+ Q_PROPERTY(QString category READ category CONSTANT)
1974 Q_PROPERTY(QStringList screenshots READ screenshots CONSTANT)
1975 Q_PROPERTY(QString changelog READ changelog CONSTANT)
1976 Q_PROPERTY(QString version READ version CONSTANT)
1977@@ -74,6 +75,9 @@
1978 QString description() const { return m_description; }
1979 void setDescription(const QString &description) { m_description = description; }
1980
1981+ QString category() const { return m_category; }
1982+ void setCategory(const QString &category) { m_category = category; }
1983+
1984 QStringList screenshots() const { return m_screenshots; }
1985 void setScreenshots(const QStringList &screenshots) { m_screenshots = screenshots; }
1986
1987@@ -123,6 +127,7 @@
1988 QString m_author;
1989 QString m_tagline;
1990 QString m_description;
1991+ QString m_category;
1992 QStringList m_screenshots;
1993 QString m_changelog;
1994 QString m_packageUrl;
1995@@ -144,17 +149,21 @@
1996 public:
1997 enum Roles {
1998 RoleName,
1999+ RoleAppId,
2000 RoleIcon,
2001 RoleAuthor,
2002 RoleTagline,
2003 RoleDescription,
2004+ RoleCategory,
2005 RoleScreenshots,
2006 RoleChangelog,
2007 RolePackageUrl,
2008 RoleVersion,
2009 RoleInstalled,
2010 RoleInstalledVersion,
2011- RoleMaintainer
2012+ RoleUpdateAvailable,
2013+ RoleMaintainer,
2014+ RoleSearchHackishString
2015 };
2016
2017 explicit AppModel(QObject *parent = 0);
2018@@ -182,6 +191,7 @@
2019
2020 Q_SIGNALS:
2021 void installerChanged();
2022+ void repositoryListFetched();
2023
2024 private:
2025 QList<ApplicationItem*> m_list;
2026
2027=== modified file 'openstore/openstore.qrc'
2028--- openstore/openstore.qrc 2015-09-08 21:41:14 +0000
2029+++ openstore/openstore.qrc 2017-05-08 18:42:06 +0000
2030@@ -3,5 +3,12 @@
2031 <file>Main.qml</file>
2032 <file>AppDetailsPage.qml</file>
2033 <file>HookIcon.qml</file>
2034+ <file>DiscoverTab.qml</file>
2035+ <file>CategoriesPage.qml</file>
2036+ <file>FilteredAppView.qml</file>
2037+ <file>EmptyState.qml</file>
2038+ <file>SectionDivider.qml</file>
2039+ <file>SearchPage.qml</file>
2040+ <file>TextualButtonStyle.qml</file>
2041 </qresource>
2042 </RCC>

Subscribers

People subscribed via source and target branches