├── .gitignore
├── .meta
├── screenshot.jpg
└── screenshot-platform.jpg
├── assets
├── fonts
│ ├── Acre.otf
│ └── BebasNeue.otf
├── backgrounds
│ ├── gb.webp
│ ├── pc.webp
│ ├── amiga.webp
│ ├── c64.webp
│ ├── fds.webp
│ ├── gba.webp
│ ├── gbc.webp
│ ├── n64.webp
│ ├── naomi.webp
│ ├── nes.webp
│ ├── ngpc.webp
│ ├── ps2.webp
│ ├── ps3.webp
│ ├── psx.webp
│ ├── sfc.webp
│ ├── sgb.webp
│ ├── snes.webp
│ ├── arcade.webp
│ ├── famicom.webp
│ ├── neogeo.webp
│ ├── saturn.webp
│ ├── scummvm.webp
│ ├── sega32x.webp
│ ├── segacd.webp
│ ├── sg-1000.webp
│ ├── switch.webp
│ ├── vectrex.webp
│ ├── x68000.webp
│ ├── amigacd32.webp
│ ├── amstradcpc.webp
│ ├── atari2600.webp
│ ├── atari7800.webp
│ ├── atarilynx.webp
│ ├── dreamcast.webp
│ ├── gamegear.webp
│ ├── megadrive.webp
│ ├── pcengine.webp
│ ├── pcenginecd.webp
│ └── mastersystem.webp
├── background-noise.webp
├── search-solid.svg
├── circle-notch-solid.svg
└── sort-alpha-down-solid.svg
├── .prettierrc.json
├── theme.cfg
├── .editorconfig
├── TODO.txt
├── Components
├── Options
│ ├── Caption.qml
│ ├── SearchTextInput.qml
│ ├── Button.qml
│ ├── Choice.qml
│ └── Options.qml
├── SearchFilter
│ ├── GamelistSortFilterProxyModel.qml
│ └── CollectionSortFilterProxyModel.qml
├── GamelistScreen
│ ├── GameDetails
│ │ ├── utils.js
│ │ ├── MetaBox.qml
│ │ ├── GameDetails.qml
│ │ └── GamePreviewItem.qml
│ ├── GamelistScreen.qml
│ └── GameGrid
│ │ ├── GameGrid.qml
│ │ └── GameGridItem.qml
├── CollectionsCarousel
│ ├── CollectionsCarousel.qml
│ ├── CollectionScreen.qml
│ └── CollectionDetails.qml
├── utils.js
├── MainSwitcher.qml
└── Main.qml
├── theme.qml
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | *.qmlc
2 | *.xcf
3 | .DS_Store
4 |
--------------------------------------------------------------------------------
/.meta/screenshot.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buzz/pegasus-theme-slick/main/.meta/screenshot.jpg
--------------------------------------------------------------------------------
/assets/fonts/Acre.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buzz/pegasus-theme-slick/main/assets/fonts/Acre.otf
--------------------------------------------------------------------------------
/assets/backgrounds/gb.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buzz/pegasus-theme-slick/main/assets/backgrounds/gb.webp
--------------------------------------------------------------------------------
/assets/backgrounds/pc.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buzz/pegasus-theme-slick/main/assets/backgrounds/pc.webp
--------------------------------------------------------------------------------
/assets/fonts/BebasNeue.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buzz/pegasus-theme-slick/main/assets/fonts/BebasNeue.otf
--------------------------------------------------------------------------------
/.meta/screenshot-platform.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buzz/pegasus-theme-slick/main/.meta/screenshot-platform.jpg
--------------------------------------------------------------------------------
/assets/background-noise.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buzz/pegasus-theme-slick/main/assets/background-noise.webp
--------------------------------------------------------------------------------
/assets/backgrounds/amiga.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buzz/pegasus-theme-slick/main/assets/backgrounds/amiga.webp
--------------------------------------------------------------------------------
/assets/backgrounds/c64.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buzz/pegasus-theme-slick/main/assets/backgrounds/c64.webp
--------------------------------------------------------------------------------
/assets/backgrounds/fds.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buzz/pegasus-theme-slick/main/assets/backgrounds/fds.webp
--------------------------------------------------------------------------------
/assets/backgrounds/gba.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buzz/pegasus-theme-slick/main/assets/backgrounds/gba.webp
--------------------------------------------------------------------------------
/assets/backgrounds/gbc.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buzz/pegasus-theme-slick/main/assets/backgrounds/gbc.webp
--------------------------------------------------------------------------------
/assets/backgrounds/n64.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buzz/pegasus-theme-slick/main/assets/backgrounds/n64.webp
--------------------------------------------------------------------------------
/assets/backgrounds/naomi.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buzz/pegasus-theme-slick/main/assets/backgrounds/naomi.webp
--------------------------------------------------------------------------------
/assets/backgrounds/nes.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buzz/pegasus-theme-slick/main/assets/backgrounds/nes.webp
--------------------------------------------------------------------------------
/assets/backgrounds/ngpc.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buzz/pegasus-theme-slick/main/assets/backgrounds/ngpc.webp
--------------------------------------------------------------------------------
/assets/backgrounds/ps2.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buzz/pegasus-theme-slick/main/assets/backgrounds/ps2.webp
--------------------------------------------------------------------------------
/assets/backgrounds/ps3.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buzz/pegasus-theme-slick/main/assets/backgrounds/ps3.webp
--------------------------------------------------------------------------------
/assets/backgrounds/psx.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buzz/pegasus-theme-slick/main/assets/backgrounds/psx.webp
--------------------------------------------------------------------------------
/assets/backgrounds/sfc.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buzz/pegasus-theme-slick/main/assets/backgrounds/sfc.webp
--------------------------------------------------------------------------------
/assets/backgrounds/sgb.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buzz/pegasus-theme-slick/main/assets/backgrounds/sgb.webp
--------------------------------------------------------------------------------
/assets/backgrounds/snes.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buzz/pegasus-theme-slick/main/assets/backgrounds/snes.webp
--------------------------------------------------------------------------------
/assets/backgrounds/arcade.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buzz/pegasus-theme-slick/main/assets/backgrounds/arcade.webp
--------------------------------------------------------------------------------
/assets/backgrounds/famicom.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buzz/pegasus-theme-slick/main/assets/backgrounds/famicom.webp
--------------------------------------------------------------------------------
/assets/backgrounds/neogeo.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buzz/pegasus-theme-slick/main/assets/backgrounds/neogeo.webp
--------------------------------------------------------------------------------
/assets/backgrounds/saturn.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buzz/pegasus-theme-slick/main/assets/backgrounds/saturn.webp
--------------------------------------------------------------------------------
/assets/backgrounds/scummvm.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buzz/pegasus-theme-slick/main/assets/backgrounds/scummvm.webp
--------------------------------------------------------------------------------
/assets/backgrounds/sega32x.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buzz/pegasus-theme-slick/main/assets/backgrounds/sega32x.webp
--------------------------------------------------------------------------------
/assets/backgrounds/segacd.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buzz/pegasus-theme-slick/main/assets/backgrounds/segacd.webp
--------------------------------------------------------------------------------
/assets/backgrounds/sg-1000.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buzz/pegasus-theme-slick/main/assets/backgrounds/sg-1000.webp
--------------------------------------------------------------------------------
/assets/backgrounds/switch.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buzz/pegasus-theme-slick/main/assets/backgrounds/switch.webp
--------------------------------------------------------------------------------
/assets/backgrounds/vectrex.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buzz/pegasus-theme-slick/main/assets/backgrounds/vectrex.webp
--------------------------------------------------------------------------------
/assets/backgrounds/x68000.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buzz/pegasus-theme-slick/main/assets/backgrounds/x68000.webp
--------------------------------------------------------------------------------
/assets/backgrounds/amigacd32.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buzz/pegasus-theme-slick/main/assets/backgrounds/amigacd32.webp
--------------------------------------------------------------------------------
/assets/backgrounds/amstradcpc.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buzz/pegasus-theme-slick/main/assets/backgrounds/amstradcpc.webp
--------------------------------------------------------------------------------
/assets/backgrounds/atari2600.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buzz/pegasus-theme-slick/main/assets/backgrounds/atari2600.webp
--------------------------------------------------------------------------------
/assets/backgrounds/atari7800.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buzz/pegasus-theme-slick/main/assets/backgrounds/atari7800.webp
--------------------------------------------------------------------------------
/assets/backgrounds/atarilynx.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buzz/pegasus-theme-slick/main/assets/backgrounds/atarilynx.webp
--------------------------------------------------------------------------------
/assets/backgrounds/dreamcast.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buzz/pegasus-theme-slick/main/assets/backgrounds/dreamcast.webp
--------------------------------------------------------------------------------
/assets/backgrounds/gamegear.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buzz/pegasus-theme-slick/main/assets/backgrounds/gamegear.webp
--------------------------------------------------------------------------------
/assets/backgrounds/megadrive.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buzz/pegasus-theme-slick/main/assets/backgrounds/megadrive.webp
--------------------------------------------------------------------------------
/assets/backgrounds/pcengine.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buzz/pegasus-theme-slick/main/assets/backgrounds/pcengine.webp
--------------------------------------------------------------------------------
/assets/backgrounds/pcenginecd.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buzz/pegasus-theme-slick/main/assets/backgrounds/pcenginecd.webp
--------------------------------------------------------------------------------
/assets/backgrounds/mastersystem.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/buzz/pegasus-theme-slick/main/assets/backgrounds/mastersystem.webp
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "always",
3 | "endOfLine": "lf",
4 | "printWidth": 100,
5 | "semi": true,
6 | "singleQuote": false
7 | }
8 |
--------------------------------------------------------------------------------
/theme.cfg:
--------------------------------------------------------------------------------
1 | name: Slick
2 | author: buzz
3 | version: 0.1
4 | summary: A slick theme focusing on simplicity and usability
5 | homepage: https://github.com/buzz/pegasus-theme-slick
6 | assets.screenshots:
7 | .meta/screenshot.jpg
8 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 | end_of_line = lf
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
13 |
--------------------------------------------------------------------------------
/assets/search-solid.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/TODO.txt:
--------------------------------------------------------------------------------
1 | - constants
2 | - font sizes
3 |
4 | - collections
5 | - mouse navigation?
6 | - add "virtual" collections
7 | - all games
8 | - favorite
9 | - genres
10 | - companies
11 | - little "breadcrumb" on the bottom?
12 | - split up info into cpu, memory, graphics... for better formatting
13 |
14 | - gamelist
15 | - views
16 | - fullscreen grid with toggleable details
17 | - text list instead of grid
18 | - a-z slider on the right side?
19 | - customize navigation
20 | - don't wrap at end of line
21 | - pageup/down: scroll one page?
22 | - fav item (keys details?)
23 | - Has fav support: https://github.com/plaidman/retromega-next
24 |
25 | - settings?
26 | - mute preview video
27 |
--------------------------------------------------------------------------------
/assets/circle-notch-solid.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assets/sort-alpha-down-solid.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Components/Options/Caption.qml:
--------------------------------------------------------------------------------
1 | import QtQuick 2.12
2 | import QtQuick.Layouts 1.11
3 | import QtGraphicalEffects 1.15
4 |
5 | Item {
6 | id: root
7 |
8 | required property string text
9 | required property string imageSource
10 |
11 | Image {
12 | id: icon
13 |
14 | height: root.height
15 | anchors {
16 | left: parent.left
17 | verticalCenter: root.verticalCenter
18 | }
19 |
20 | source: root.imageSource
21 | fillMode: Image.PreserveAspectFit
22 | smooth: true
23 |
24 | visible: false
25 | }
26 |
27 | ColorOverlay {
28 | anchors.fill: icon
29 | source: icon
30 | color: colorFontStrong
31 | }
32 |
33 | Text {
34 | id: text
35 |
36 | anchors {
37 | left: icon.right
38 | top: icon.top
39 | leftMargin: spacingHorizStd
40 | }
41 |
42 | text: root.text
43 | color: colorFontStrong
44 | font.family: headerFont.name
45 | font.pixelSize: fontSizeOptionsHeader
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Components/SearchFilter/GamelistSortFilterProxyModel.qml:
--------------------------------------------------------------------------------
1 | import SortFilterProxyModel 0.2
2 |
3 | SortFilterProxyModel {
4 | required property string searchText
5 | required property int sortIndex
6 |
7 | // Defer as both callbacks may fire
8 | onSortIndexChanged: Qt.callLater(changed)
9 | onSearchTextChanged: Qt.callLater(changed)
10 |
11 | signal changed
12 |
13 | filters: [
14 | RegExpFilter {
15 | enabled: !!searchText
16 | caseSensitivity: Qt.CaseInsensitive
17 | roleName: "title"
18 | pattern: searchText
19 | syntax: RegExpFilter.FixedString
20 | }
21 | ]
22 |
23 | sorters: [
24 | RoleSorter {
25 | roleName: listModelSortGamelist.get(sortIndex).roleName
26 | sortOrder: Qt.AscendingOrder
27 | // priority: 1
28 | }
29 | // RoleSorter {
30 | // enabled: listModelSortGamelist.get(sortIndex).roleName !== "sortBy"
31 | // enabled: listModelSortGamelist.get(sortIndex).roleName !== "sortBy"
32 | // roleName: "sortBy"
33 | // sortOrder: Qt.AscendingOrder
34 | // priority: 0
35 | // }
36 | ]
37 | }
38 |
--------------------------------------------------------------------------------
/Components/GamelistScreen/GameDetails/utils.js:
--------------------------------------------------------------------------------
1 | // Show dates in Y-M-D format
2 | function formatDate(date) {
3 | return Qt.formatDate(date, "yyyy");
4 | }
5 |
6 | // Show last played time as text. Based on the code of the default Pegasus theme.
7 | // Note to self: I should probably move this into the API.
8 | function formatLastPlayed(lastPlayed) {
9 | if (isNaN(lastPlayed)) return "never";
10 |
11 | var now = new Date();
12 |
13 | var elapsedHours = (now.getTime() - lastPlayed.getTime()) / 1000 / 60 / 60;
14 | if (elapsedHours < 24 && now.getDate() === lastPlayed.getDate()) return "today";
15 |
16 | var elapsedDays = Math.round(elapsedHours / 24);
17 | if (elapsedDays <= 1) return "yesterday";
18 |
19 | return `${elapsedDays} days ago`;
20 | }
21 |
22 | // Display the play time (provided in seconds) with text.
23 | // Based on the code of the default Pegasus theme.
24 | // Note to self: I should probably move this into the API.
25 | function formatPlayTime(playTime) {
26 | var minutes = Math.ceil(playTime / 60);
27 | if (minutes <= 90) return `${Math.round(minutes)} Mins`;
28 |
29 | return `${parseFloat((minutes / 60).toFixed(1))} Hours`;
30 | }
31 |
--------------------------------------------------------------------------------
/Components/Options/SearchTextInput.qml:
--------------------------------------------------------------------------------
1 | import QtQuick 2.12
2 | import QtQuick.Controls 2.15
3 |
4 | FocusScope {
5 | id: root
6 |
7 | property alias searchText: searchInput.text
8 |
9 | height: searchInput.height + spacingVertStd * 2
10 | clip: true
11 |
12 | signal accept()
13 |
14 | Rectangle {
15 | id: rect
16 |
17 | anchors.fill: parent
18 |
19 | color: root.focus ? colorControlBgHighlight : colorTextInputBg
20 | radius: radiusSmall
21 |
22 | Behavior on color {
23 | PropertyAnimation {
24 | duration: durationShort
25 | easing.type: Easing.OutQuad
26 | }
27 | }
28 |
29 | TextInput {
30 | id: searchInput
31 |
32 | anchors {
33 | top: parent.top
34 | left: parent.left
35 | right: parent.right
36 | topMargin: spacingVertStd
37 | rightMargin: spacingHorizStd
38 | leftMargin: spacingHorizStd
39 | }
40 | focus: true
41 |
42 | color: focus ? colorFontOptionsSelected : colorFontOptions
43 | font.family: generalFont.name
44 | font.pixelSize: fontSizeOptions
45 | maximumLength: 60
46 |
47 | onAccepted: root.accept()
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Components/Options/Button.qml:
--------------------------------------------------------------------------------
1 | import QtQuick 2.12
2 |
3 | FocusScope {
4 | id: root
5 |
6 | required property string text
7 |
8 | signal activate
9 |
10 | Rectangle {
11 | color: root.focus ? colorControlBgHighlight : "transparent"
12 | radius: radiusSmall
13 |
14 | anchors.centerIn: parent
15 | width: buttonText.implicitWidth + spacingHorizStd * 2
16 | height: buttonText.implicitHeight + spacingVertStd
17 |
18 | Behavior on color {
19 | PropertyAnimation {
20 | duration: durationShort
21 | easing.type: Easing.OutQuad
22 | }
23 | }
24 |
25 | Text {
26 | id: buttonText
27 |
28 | focus: true
29 | anchors.centerIn: parent
30 |
31 | text: root.text
32 | color: root.focus || mouseArea.containsMouse ? colorFontOptionsSelected : colorFontOptions
33 | font.family: generalFont.name
34 | font.pixelSize: fontSizeOptions
35 |
36 | Behavior on color {
37 | PropertyAnimation {
38 | duration: durationShort
39 | easing.type: Easing.OutQuad
40 | }
41 | }
42 |
43 | Keys.onPressed: {
44 | if (api.keys.isAccept(event)) {
45 | event.accepted = true;
46 | activate();
47 | }
48 | }
49 | }
50 | }
51 |
52 | MouseArea {
53 | id: mouseArea
54 |
55 | anchors.fill: parent
56 | hoverEnabled: true
57 |
58 | onClicked: activate()
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/Components/GamelistScreen/GamelistScreen.qml:
--------------------------------------------------------------------------------
1 | import QtQuick 2.12
2 |
3 | import "GameDetails"
4 | import "GameGrid"
5 |
6 | FocusScope {
7 | id: gamelistScreen
8 |
9 | property var collection
10 | property bool gamesFound: currentGameIndex !== -1
11 |
12 | signal itemSelected()
13 | signal back
14 |
15 | function jumpToCollection() {
16 | if (currentCollectionIndex >= 0) {
17 | collection = collectionSearchFilter.get(currentCollectionIndex)
18 | gameGrid.jumpToCollection();
19 | }
20 | }
21 |
22 | function jumpToGame() {
23 | const game = gamelistSearchFilter.get(currentGameIndex);
24 | gameDetails.game = game;
25 | Qt.callLater(gameGrid.jumpToGame);
26 | }
27 |
28 | GameDetails {
29 | id: gameDetails
30 |
31 | width: parent.width / 2.2
32 | anchors {
33 | top: parent.top
34 | left: parent.left
35 | bottom: parent.bottom
36 | }
37 | visible: gamesFound
38 | }
39 |
40 | GameGrid {
41 | id: gameGrid
42 |
43 | optionsView: mainSwitcher.optionsView
44 |
45 | width: parent.width / 2
46 | anchors {
47 | top: parent.top
48 | right: parent.right
49 | bottom: parent.bottom
50 | }
51 | visible: gamesFound
52 | }
53 |
54 | Text {
55 | anchors.centerIn: parent
56 |
57 | color: colorFontStrong
58 | font.family: generalFont.name
59 | font.pixelSize: fontSizeCollectionSubheader
60 | text: "No games found…"
61 | visible: !gamesFound
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Components/SearchFilter/CollectionSortFilterProxyModel.qml:
--------------------------------------------------------------------------------
1 | import SortFilterProxyModel 0.2
2 |
3 | SortFilterProxyModel {
4 | required property string searchText
5 | required property int sortIndex
6 |
7 | // Defer as both callbacks may fire
8 | onSortIndexChanged: Qt.callLater(changed)
9 | onSearchTextChanged: Qt.callLater(changed)
10 |
11 | signal changed
12 |
13 | proxyRoles: [
14 | ExpressionRole {
15 | name: "company"
16 | expression: model.extra ? model.extra.company : ""
17 | },
18 | ExpressionRole {
19 | name: "year"
20 | expression: model.extra ? model.extra.year : ""
21 | }
22 | ]
23 |
24 | filters: [
25 | AnyOf {
26 | enabled: !!searchText
27 |
28 | RegExpFilter {
29 | caseSensitivity: Qt.CaseInsensitive
30 | roleName: "name"
31 | pattern: searchText
32 | syntax: RegExpFilter.FixedString
33 | }
34 | RegExpFilter {
35 | caseSensitivity: Qt.CaseInsensitive
36 | roleName: "company"
37 | pattern: searchText
38 | syntax: RegExpFilter.FixedString
39 | }
40 | }
41 | ]
42 |
43 | sorters: [
44 | RoleSorter {
45 | roleName: listModelSortCollections.get(sortIndex).roleName
46 | sortOrder: Qt.AscendingOrder
47 | priority: 1
48 | },
49 | RoleSorter {
50 | enabled: listModelSortCollections.get(sortIndex).roleName !== "sortBy"
51 | roleName: "sortBy"
52 | sortOrder: Qt.AscendingOrder
53 | priority: 0
54 | }
55 | ]
56 | }
57 |
--------------------------------------------------------------------------------
/Components/CollectionsCarousel/CollectionsCarousel.qml:
--------------------------------------------------------------------------------
1 | import QtQuick 2.12
2 | import "../utils.js" as Utils
3 |
4 | ListView {
5 | id: collectionsCarousel
6 |
7 | signal select
8 |
9 | function jumpTo() {
10 | if (currentCollectionIndex >= 0) {
11 | positionViewAtIndex(currentCollectionIndex, ListView.SnapPosition);
12 | currentIndex = currentCollectionIndex;
13 | }
14 | }
15 |
16 | Keys.onPressed: {
17 | if (api.keys.isAccept(event)) {
18 | event.accepted = true;
19 | select();
20 | }
21 | }
22 | Keys.onRightPressed: {
23 | if (!horizontalVelocity) {
24 | event.accepted = true;
25 | incrementCurrentIndex();
26 | }
27 | }
28 | Keys.onLeftPressed: {
29 | if (!horizontalVelocity) {
30 | event.accepted = true;
31 | decrementCurrentIndex();
32 | }
33 | }
34 |
35 | onCurrentIndexChanged: currentCollectionIndex = currentIndex
36 |
37 | currentIndex: -1
38 | model: collectionSearchFilter
39 |
40 | orientation: ListView.Horizontal
41 | snapMode: PathView.SnapOneItem
42 | highlightRangeMode: ListView.StrictlyEnforceRange
43 | keyNavigationEnabled: false
44 | highlightFollowsCurrentItem: true
45 | spacing: vpx(100)
46 | cacheBuffer: 0 // Keep 1 delegate outside of screen in each direction
47 |
48 | highlightMoveDuration: -1
49 | highlightMoveVelocity: vpx(1280 * 4)
50 | maximumFlickVelocity: highlightMoveVelocity
51 |
52 | delegate: CollectionScreen {
53 | width: main.width
54 | height: main.height
55 | }
56 |
57 | Text {
58 | anchors.centerIn: parent
59 |
60 | color: colorFontStrong
61 | font.family: generalFont.name
62 | font.pixelSize: fontSizeCollectionSubheader
63 | text: "No collections found…"
64 | visible: currentCollectionIndex === -1
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/Components/GamelistScreen/GameDetails/MetaBox.qml:
--------------------------------------------------------------------------------
1 | import QtQuick 2.12
2 | import QtQuick.Layouts 1.11
3 |
4 | Rectangle {
5 | id: root
6 | property string metaTitle
7 | property string metaContent
8 | width: vpx(66)
9 | height: vpx(66)
10 | color: "transparent"
11 |
12 | RowLayout {
13 | id: rowcontent
14 | anchors.verticalCenter: root.verticalCenter
15 | anchors.horizontalCenter: root.horizontalCenter
16 | Rectangle {
17 | id: metaBox
18 | width: root.width
19 | height: root.height
20 | color: colorBgMetaBox
21 | clip: true
22 |
23 | Text {
24 | text: metaTitle
25 | color: colorFontBox
26 | width: parent.width
27 | font.family: generalFont.name
28 | fontSizeMode: Text.Fit
29 | font.pixelSize: spacingStd
30 | font.weight: Font.Bold
31 | font.capitalization: Font.AllUppercase
32 | horizontalAlignment: Text.AlignHCenter
33 | padding: vpx(4)
34 | anchors {
35 | bottom: parent.bottom;
36 | left: parent.left
37 | right: parent.right
38 | }
39 | }
40 |
41 | Text {
42 | id: metaValue
43 | text: metaContent
44 | color: colorFontBoxValue
45 | width: parent.width
46 | height: parent.height
47 | font.family: headerFont.name
48 | font.weight: Font.Bold
49 | font.capitalization: Font.AllUppercase
50 | fontSizeMode: Text.Fit
51 | font.pixelSize: vpx(44)
52 | horizontalAlignment: Text.AlignHCenter
53 | verticalAlignment: Text.AlignVCenter
54 | leftPadding: vpx(3)
55 | rightPadding: vpx(3)
56 | bottomPadding: vpx(12)
57 | topPadding: vpx(6)
58 | wrapMode: Text.WordWrap
59 | maximumLineCount: 3
60 | lineHeight: 0.8
61 | }
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/Components/utils.js:
--------------------------------------------------------------------------------
1 | function restoreOptions() {
2 | let sortIndexCollections = api.memory.get("sortIndexCollections");
3 | let sortIndexGamelist = api.memory.get("sortIndexGamelist");
4 |
5 | if (typeof sortIndexCollections !== "number" || sortIndexCollections < 0) {
6 | sortIndexCollections = 0;
7 | }
8 | if (typeof sortIndexGamelist !== "number" || sortIndexGamelist < 0) {
9 | sortIndexGamelist = 0;
10 | }
11 |
12 | return {
13 | sortIndexCollections,
14 | sortIndexGamelist,
15 | };
16 | }
17 |
18 | function saveOptions(opts) {
19 | api.memory.set("sortIndexCollections", opts.sortIndexCollections);
20 | api.memory.set("sortIndexGamelist", opts.sortIndexGamelist);
21 | }
22 |
23 | function restoreCollection() {
24 | const shortName = api.memory.get("collectionShortName");
25 | if (typeof shortName === "string" && shortName.length) return shortName;
26 | return "";
27 | }
28 |
29 | function saveCollection() {
30 | const collection = collectionSearchFilter.get(currentCollectionIndex);
31 | if (collection && collection.shortName)
32 | api.memory.set("collectionShortName", collection.shortName);
33 | }
34 |
35 | function restoreGame() {
36 | const collection = collectionSearchFilter.get(currentCollectionIndex);
37 | if (collection && collection.shortName) {
38 | const gamePath = api.memory.get(`${collection.shortName}GamePath`);
39 | if (typeof gamePath === "string" && gamePath.length > 0) return gamePath;
40 | }
41 | return "";
42 | }
43 |
44 | function saveGame() {
45 | const collection = collectionSearchFilter.get(currentCollectionIndex);
46 | if (
47 | collection &&
48 | typeof collection.shortName === "string" &&
49 | collection.shortName.length > 0 &&
50 | typeof currentGameIndex === "number" &&
51 | currentGameIndex >= 0
52 | ) {
53 | const game = gamelistSearchFilter.get(currentGameIndex);
54 | if (game && game.files && game.files.count)
55 | api.memory.set(`${collection.shortName}GamePath`, game.files.get(0).path);
56 | }
57 | }
58 |
59 | function isGameLaunch() {
60 | if (api.memory.has("gameLaunch")) {
61 | api.memory.unset("gameLaunch");
62 | return true;
63 | }
64 | return false;
65 | }
66 |
67 | function saveGameLaunch() {
68 | api.memory.set("gameLaunch", true);
69 | }
70 |
--------------------------------------------------------------------------------
/Components/CollectionsCarousel/CollectionScreen.qml:
--------------------------------------------------------------------------------
1 | import QtQuick 2.12
2 | import QtGraphicalEffects 1.12
3 |
4 | Item {
5 | id: collectionScreen
6 |
7 | // Collection image
8 | Item {
9 | id: collectionImage
10 |
11 | anchors {
12 | top: collectionScreen.top
13 | right: collectionScreen.right
14 | bottom: collectionScreen.bottom
15 | topMargin: marginVertCollectionScreen * 1.33
16 | rightMargin: marginHorizCollectionScreen / 2
17 | bottomMargin: marginVertCollectionScreen / 2
18 | leftMargin: marginHorizCollectionScreen
19 | }
20 |
21 | width: collectionScreen.width / 1.67
22 | height: collectionScreen.height
23 | scale: collectionScreen.ListView.isCurrentItem ? 1.0 : 0.5
24 | opacity: collectionScreen.ListView.isCurrentItem ? 1.0 : 0.0
25 |
26 | Behavior on scale {
27 | PropertyAnimation {
28 | duration: durationLong
29 | easing.type: Easing.OutCubic
30 | }
31 | }
32 | Behavior on opacity {
33 | PropertyAnimation {
34 | duration: durationLong
35 | easing.type: Easing.OutCubic
36 | }
37 | }
38 |
39 | // Colored glow
40 | GaussianBlur {
41 | anchors.fill: bgImg
42 |
43 | cached: true
44 | source: bgImg
45 | radius: vpx(32)
46 | samples: vpx(65)
47 |
48 | transparentBorder: true
49 | }
50 |
51 | Image {
52 | id: bgImg
53 |
54 | width: Math.min(parent.width, implicitWidth)
55 | height: Math.min(parent.height, implicitHeight)
56 |
57 | anchors {
58 | bottom: parent.bottom
59 | right: parent.right
60 | }
61 |
62 | visible: true
63 | asynchronous: true
64 | fillMode: Image.PreserveAspectFit
65 | source: `../../assets/backgrounds/${shortName}.webp`
66 | smooth: true
67 | }
68 | }
69 |
70 | // Collection info text
71 | CollectionDetails {
72 | anchors.fill: parent
73 |
74 | transform: Translate {
75 | y: collectionScreen.ListView.isCurrentItem ? 0 : vpx(-900)
76 |
77 | Behavior on y {
78 | PropertyAnimation {
79 | duration: 550
80 | easing.type: Easing.OutQuint
81 | }
82 | }
83 | }
84 | }
85 |
86 | MouseArea {
87 | anchors.fill: parent
88 | onDoubleClicked: collectionsCarousel.select()
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/Components/GamelistScreen/GameDetails/GameDetails.qml:
--------------------------------------------------------------------------------
1 | import QtQuick 2.12
2 | import QtQuick.Layouts 1.11
3 | import "utils.js" as Utils
4 |
5 | Item {
6 | id: gameDetails
7 |
8 | property var game
9 |
10 | ColumnLayout {
11 | anchors {
12 | fill: parent
13 | leftMargin: marginHorizGamelistScreen
14 | topMargin: marginVertGamelistScreen
15 | }
16 |
17 | // Collection name
18 | Text {
19 | id: collectionName
20 |
21 | Layout.alignment: Qt.AlignVBottom | Qt.AlignLeft
22 | text: collection && collection.name ? collection.name : ""
23 | color: colorFont
24 | font.pixelSize: fontSizeGameDetailsSubheader
25 | font.family: generalFont.name
26 | font.capitalization: Font.AllUppercase
27 | lineHeight: 0.9
28 | }
29 |
30 | // Game title
31 | Text {
32 | color: colorFontStrong
33 | text: game && game.title ? game.title : ""
34 | font.pixelSize: fontSizeGameDetailsHeader
35 | font.family: headerFont.name
36 | font.bold: true
37 | Layout.alignment: Qt.AlignVTop | Qt.AlignLeft
38 | Layout.fillWidth: true
39 | Layout.preferredWidth: parent.width
40 | elide: Text.ElideRight
41 | }
42 |
43 | // Meta boxes
44 | RowLayout {
45 | spacing: spacingStd
46 | MetaBox { metaTitle: 'PLAYERS'; metaContent: game && game.players ? game.players : "" }
47 | MetaBox { metaTitle: 'RATING'; metaContent: game && game.rating ? `${Math.round(game.rating * 100)}%` : "N/A"}
48 | MetaBox { metaTitle: 'RELEASED'; metaContent: game && game.releaseYear > 0 ? game.releaseYear : "N/A" }
49 | MetaBox { metaTitle: 'GENRE'; metaContent: game && game.genre ? game.genre : "N/A" }
50 | MetaBox { metaTitle: 'DEVELOPER'; metaContent: game && game.developer ? game.developer : "N/A" }
51 | MetaBox { metaTitle: 'PUBLISHER'; metaContent: game && game.publisher ? game.publisher : "N/A" }
52 | MetaBox { metaTitle: 'LAST PLAYED'; metaContent: game ? Utils.formatLastPlayed(game.lastPlayed) : "" }
53 | MetaBox { metaTitle: 'TIME PLAYED'; metaContent: game ? Utils.formatPlayTime(game.playTime) : "" }
54 | }
55 |
56 | // Game description
57 | Item {
58 | Layout.fillWidth: true
59 | Layout.fillHeight: true
60 | Layout.topMargin: spacingStd
61 |
62 | Text {
63 | font.pixelSize: fontSizeRegular
64 | font.family: generalFont.name
65 | text: game && game.description ? game.description : ""
66 | wrapMode: Text.WordWrap
67 | elide: Text.ElideRight
68 | color: colorFont
69 | anchors.fill: parent
70 | }
71 | }
72 |
73 | // Video
74 | Item {
75 | id: screenshotBox
76 | height: vpx(400)
77 | Layout.fillWidth: true
78 |
79 | Item {
80 | id: screenshot
81 |
82 | anchors {
83 | fill: parent
84 | horizontalCenter: parent.horizontalCenter
85 | verticalCenter: parent.verticalCenter
86 | bottomMargin: marginVertGamelistScreen
87 | }
88 |
89 | GamePreviewItem {
90 | anchors.fill: parent
91 |
92 | game: gameDetails.game
93 | optionsView: mainSwitcher.optionsView
94 | }
95 | }
96 | }
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/theme.qml:
--------------------------------------------------------------------------------
1 | import QtQuick 2.12
2 | import "Components"
3 |
4 | Main {
5 | // Colors
6 | readonly property color colorBgTop: "#aa000000"
7 | readonly property color colorBgCenter: "#77000000"
8 | readonly property color colorBgBottom: "#77010101"
9 |
10 | readonly property color colorBoxBorder: "#333333"
11 | readonly property color colorBgMetaBox: "#aaffffff"
12 | readonly property color colorBgBoxArt: "#888888"
13 |
14 | readonly property color colorTextInputBg: "#0cffffff"
15 | readonly property color colorControlBgHighlight: "#11ffffff"
16 |
17 | // Fonts
18 | FontLoader { id: headerFont; source: "assets/fonts/BebasNeue.otf" }
19 | FontLoader { id: generalFont; source: "assets/fonts/Acre.otf" }
20 |
21 | // Font colors
22 | readonly property color colorFont: "#888888"
23 | readonly property color colorFontLightlyTinted: "#616161"
24 | readonly property color colorFontTinted: "#3a3a3a"
25 | readonly property color colorFontStrong: "#ffffff"
26 | readonly property color colorFontRed: "#e1303a"
27 | readonly property color colorFontBox: "#222222"
28 | readonly property color colorFontBoxValue: "#111111"
29 |
30 | readonly property color colorFontOptions: "#aaaaaa"
31 | readonly property color colorFontOptionsSelected: "#ffffff"
32 |
33 | // Font sizes
34 | readonly property int fontSizeRegular: vpx(14)
35 | readonly property int fontSizeCollectionSubheader: vpx(21)
36 | readonly property int fontSizeCollectionHeader: vpx(120)
37 |
38 | readonly property int fontSizeGameDetailsSubheader: vpx(21)
39 | readonly property int fontSizeGameDetailsHeader: vpx(56)
40 |
41 | readonly property int fontSizeOptionsHeader: vpx(26)
42 | readonly property int fontSizeOptions: vpx(18)
43 |
44 | // Animations
45 | readonly property int durationVeryShort: 100
46 | readonly property int durationShort: 150
47 | readonly property int durationMedium: 300
48 | readonly property int durationLong: 600
49 | readonly property int durationPlayVideo: 1500 // time until video replaces image
50 | readonly property int durationLoadingSpinner: 1333
51 |
52 | // Margins
53 | readonly property int marginHorizCollectionScreen: vpx(96)
54 | readonly property int marginVertCollectionScreen: vpx(70)
55 | readonly property int marginHorizGamelistScreen: vpx(40)
56 | readonly property int marginVertGamelistScreen: vpx(40)
57 | readonly property int spacingStd: vpx(10)
58 | readonly property int spacingHorizStd: vpx(10)
59 | readonly property int spacingVertStd: vpx(6)
60 |
61 | // Misc
62 | readonly property real boxArtScaleFactor: 3
63 | readonly property int radiusSmall: vpx(4)
64 | readonly property int radiusLarge: vpx(12)
65 |
66 | // Sorting
67 | ListModel {
68 | id: listModelSortCollections
69 |
70 | ListElement { name: "Name"; roleName: "sortBy" }
71 | ListElement { name: "Year"; roleName: "year" }
72 | ListElement { name: "Company"; roleName: "company" }
73 | }
74 | ListModel {
75 | id: listModelSortGamelist
76 |
77 | ListElement { name: "Title"; roleName: "sortBy" }
78 | ListElement { name: "Year"; roleName: "releaseYear" }
79 | ListElement { name: "Developer"; roleName: "developer" }
80 | ListElement { name: "Publisher"; roleName: "publisher" }
81 | ListElement { name: "Genre"; roleName: "genre" }
82 | ListElement { name: "Play time"; roleName: "playTime" }
83 | ListElement { name: "Last played"; roleName: "lastPlayed" }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/Components/CollectionsCarousel/CollectionDetails.qml:
--------------------------------------------------------------------------------
1 | import QtQuick 2.12
2 |
3 | Item {
4 | Text {
5 | id: textSystemYear
6 | text: extra.year || ""
7 | color: colorFont
8 | font.family: generalFont.name
9 | font.pixelSize: fontSizeCollectionSubheader
10 |
11 | anchors {
12 | left: parent.left
13 | leftMargin: marginHorizCollectionScreen
14 | top: parent.top
15 | topMargin: marginVertCollectionScreen
16 | }
17 | }
18 |
19 | Text {
20 | id: textDot1
21 | text: extra.year ? " · " : ""
22 | color: colorFontLightlyTinted
23 | font.family: generalFont.name
24 | font.pixelSize: fontSizeCollectionSubheader
25 |
26 | anchors {
27 | left: textSystemYear.right
28 | top: parent.top
29 | topMargin: marginVertCollectionScreen
30 | }
31 | }
32 |
33 | Text {
34 | id: textSystemCompany
35 | text: extra.company || ""
36 | color: colorFont
37 | font.family: generalFont.name
38 | font.pixelSize: fontSizeCollectionSubheader
39 | font.capitalization: Font.AllUppercase
40 |
41 | anchors {
42 | left: textDot1.right
43 | top: parent.top
44 | topMargin: marginVertCollectionScreen
45 | }
46 | }
47 |
48 | Text {
49 | id: textDot2
50 | text: extra.company ? " · " : ""
51 | color: colorFontLightlyTinted
52 | font.family: generalFont.name
53 | font.pixelSize: fontSizeCollectionSubheader
54 |
55 | anchors {
56 | left: textSystemCompany.right
57 | top: parent.top
58 | topMargin: marginVertCollectionScreen
59 | }
60 | }
61 |
62 | Text {
63 | id: textSystemShortDescription
64 | text: summary || ""
65 | color: colorFontTinted
66 | font.family: generalFont.name
67 | font.pixelSize: fontSizeCollectionSubheader
68 | font.capitalization: Font.AllUppercase
69 |
70 | anchors {
71 | left: textDot2.right
72 | top: parent.top
73 | topMargin: marginVertCollectionScreen
74 | }
75 | }
76 |
77 | Text {
78 | id: textCollectionName
79 | text: name
80 | color: colorFontStrong
81 | font.bold: true
82 | font.family: headerFont.name
83 | font.pixelSize: fontSizeCollectionHeader
84 | font.capitalization: Font.AllUppercase
85 | font.letterSpacing: -vpx(1.25)
86 | renderType: Text.NativeRendering
87 | fontSizeMode: Text.Fit
88 | lineHeight: 0.85
89 | maximumLineCount: 2
90 | wrapMode: Text.Wrap
91 | width: vpx(640)
92 |
93 | anchors {
94 | left: parent.left
95 | leftMargin: marginHorizCollectionScreen
96 | top: textSystemShortDescription.bottom
97 | topMargin: vpx(25)
98 | }
99 | }
100 |
101 | Text {
102 | id: textCollectionGameCount
103 | text: `${games.count} AVAILABLE GAMES`
104 | color: colorFontRed
105 | font.family: generalFont.name
106 | font.pixelSize: fontSizeCollectionSubheader
107 |
108 | anchors {
109 | left: parent.left
110 | leftMargin: marginHorizCollectionScreen
111 | top: textCollectionName.bottom
112 | topMargin: vpx(22)
113 | }
114 | }
115 |
116 | Text {
117 | id: textCollectionDescription
118 | text: description
119 | color: colorFont
120 | font.family: generalFont.name
121 | font.pixelSize: fontSizeRegular
122 | width: vpx(420)
123 | wrapMode: Text.Wrap
124 |
125 | anchors {
126 | left: parent.left
127 | leftMargin: marginHorizCollectionScreen
128 | top: textCollectionGameCount.bottom
129 | topMargin: vpx(14)
130 | }
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/Components/GamelistScreen/GameDetails/GamePreviewItem.qml:
--------------------------------------------------------------------------------
1 | import QtQuick 2.12
2 | import QtMultimedia 5.12
3 | import QtGraphicalEffects 1.12
4 |
5 | Item {
6 | property var game
7 | required property bool optionsView
8 |
9 | property bool gamelistView: gamelistScreen.focus
10 |
11 | property bool steam: false
12 | property bool hasImage: getImageSource().length
13 |
14 | onOptionsViewChanged: {
15 | if (videoPreviewLoader.item) {
16 | if (optionsView)
17 | videoPreviewLoader.item.pause();
18 | else
19 | videoPreviewLoader.item.play();
20 | }
21 | }
22 |
23 | onGameChanged: {
24 | videoPreviewLoader.sourceComponent = undefined;
25 | if (gamelistView) {
26 | showImage();
27 | timerHideImage.stop();
28 | timerVideoDelay.restart();
29 | }
30 | }
31 |
32 | onGamelistViewChanged: {
33 | if (gamelistView) {
34 | videoPreviewLoader.sourceComponent = undefined;
35 | timerVideoDelay.restart();
36 | } else {
37 | videoPreviewLoader.sourceComponent = undefined;
38 | showImage();
39 | timerHideImage.stop();
40 | timerVideoDelay.stop();
41 | }
42 |
43 | if (collection && collection.shortName == "steam") {
44 | steam = true
45 | } else {
46 | steam = false
47 | }
48 | }
49 |
50 | // Small delay to avoid loading videos during scrolling
51 | Timer {
52 | id: timerVideoDelay
53 | interval: 500
54 | onTriggered: {
55 | if (game && game.assets && game.assets.videos && game.assets.videos.length) {
56 | videoPreviewLoader.sourceComponent = videoPreviewWrapper;
57 | if (hasImage) {
58 | timerHideImage.restart();
59 | } else {
60 | hideImage();
61 | }
62 | }
63 | }
64 | }
65 |
66 | Timer {
67 | id: timerHideImage
68 | interval: durationPlayVideo
69 | onTriggered: hideImage()
70 | }
71 |
72 | function showImage() {
73 | screenshot.opacity = 1;
74 | videoPreviewLoader.opacity = 0;
75 | }
76 |
77 | function hideImage() {
78 | screenshot.opacity = 0;
79 | videoPreviewLoader.opacity = 1;
80 | }
81 |
82 | function getImageSource() {
83 | if (game && game.assets) {
84 | if (steam && game.assets.logo)
85 | return game.assets.logo;
86 | if (game.assets.screenshots[0])
87 | return game.assets.screenshots[0];
88 | if (game.assets.poster)
89 | return game.assets.poster;
90 | }
91 | return "";
92 | }
93 |
94 | // Screenshot
95 | Image {
96 | id: screenshot
97 |
98 | z: 3
99 | anchors.fill: parent
100 |
101 | asynchronous: true
102 | visible: hasImage
103 |
104 | smooth: true
105 |
106 | source: getImageSource()
107 |
108 | sourceSize { width: 512; height: 512 }
109 | fillMode: Image.PreserveAspectCrop
110 |
111 | Behavior on opacity {
112 | PropertyAnimation {
113 | duration: durationLong
114 | easing.type: Easing.InOutCubic
115 | }
116 | }
117 | }
118 |
119 | // Video preview
120 | Component {
121 | id: videoPreviewWrapper
122 |
123 | Video {
124 | source: game.assets.videos.length ? game.assets.videos[0] : ""
125 | anchors.fill: parent
126 | fillMode: VideoOutput.PreserveAspectFit
127 | volume: 0.3
128 | loops: MediaPlayer.Infinite
129 | autoPlay: true
130 | }
131 | }
132 |
133 | Loader {
134 | id: videoPreviewLoader
135 | asynchronous: true
136 | anchors.fill: parent
137 | opacity: 0
138 |
139 | Behavior on opacity {
140 | PropertyAnimation {
141 | duration: durationLong
142 | easing.type: Easing.InOutCubic
143 | }
144 | }
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/Components/Options/Choice.qml:
--------------------------------------------------------------------------------
1 | import QtQuick 2.12
2 | import QtGraphicalEffects 1.12
3 |
4 | FocusScope {
5 | id: root
6 |
7 | clip: true
8 |
9 | required property var model
10 | readonly property int itemWidth: vpx(120)
11 |
12 | property alias currentIndex: listView.currentIndex
13 |
14 | signal accept
15 |
16 | function jumpTo(idx) {
17 | listView.positionViewAtIndex(idx, ListView.SnapPosition);
18 | listView.currentIndex = idx;
19 | }
20 |
21 | // Smoothly fade out grid at top and bottom
22 | LinearGradient {
23 | id: gradientMask
24 | anchors.fill: parent
25 |
26 | visible: false
27 | start: Qt.point(0, 0)
28 | end: Qt.point(width, 0)
29 | gradient: Gradient {
30 | GradientStop { position: 0.0; color: "transparent"}
31 | GradientStop { position: 0.1; color: "#ffffffff" }
32 | GradientStop { position: 0.5; color: "#ffffffff" }
33 | GradientStop { position: 0.9; color: "#ffffffff" }
34 | GradientStop { position: 1.0; color: "transparent"}
35 | }
36 | }
37 |
38 | ListView {
39 | id: listView
40 |
41 | anchors.fill: parent
42 |
43 | focus: true
44 |
45 | orientation: ListView.Horizontal
46 | snapMode: PathView.SnapOneItem
47 | highlightRangeMode: ListView.StrictlyEnforceRange
48 | highlightFollowsCurrentItem: true
49 | keyNavigationEnabled: false
50 | preferredHighlightBegin: (parent.width - itemWidth) / 2
51 | preferredHighlightEnd: (parent.width + itemWidth) / 2
52 | highlightMoveDuration: -1
53 | highlightMoveVelocity: vpx(200 * 4)
54 | maximumFlickVelocity: highlightMoveVelocity
55 | spacing: spacingStd / 2
56 |
57 | model: root.model
58 |
59 | layer.enabled: true
60 | layer.effect: OpacityMask {
61 | maskSource: gradientMask
62 | }
63 |
64 | delegate: Item {
65 | id: choiceItem
66 |
67 | width: itemWidth
68 | height: rect.height
69 |
70 | Rectangle {
71 | id: rect
72 |
73 | color: root.focus && choiceItem.ListView.isCurrentItem ? colorControlBgHighlight : "transparent"
74 | radius: radiusSmall
75 | width: itemWidth
76 | height: text.implicitHeight + spacingVertStd
77 | anchors.centerIn: parent
78 |
79 | Behavior on color {
80 | PropertyAnimation {
81 | duration: durationShort
82 | easing.type: Easing.OutQuad
83 | }
84 | }
85 |
86 | Text {
87 | id: text
88 |
89 | anchors.centerIn: parent
90 | font.family: generalFont.name
91 | font.pixelSize: fontSizeOptions
92 | text: name
93 | horizontalAlignment: Text.AlignHCenter
94 | color: choiceItem.ListView.isCurrentItem || mouseArea.containsMouse ? colorFontOptionsSelected : colorFontOptions
95 |
96 | Behavior on color {
97 | PropertyAnimation {
98 | duration: durationShort
99 | easing.type: Easing.OutQuad
100 | }
101 | }
102 | }
103 | }
104 |
105 | MouseArea {
106 | id: mouseArea
107 |
108 | anchors.fill: parent
109 | hoverEnabled: true
110 | onClicked: {
111 | listView.currentIndex = index;
112 | root.focus = true;
113 | }
114 | }
115 | }
116 |
117 | Keys.onPressed: {
118 | if (api.keys.isAccept(event)) {
119 | event.accepted = true;
120 | accept();
121 | }
122 | }
123 | Keys.onRightPressed: {
124 | event.accepted = true;
125 | listView.incrementCurrentIndex();
126 | }
127 | Keys.onLeftPressed: {
128 | event.accepted = true;
129 | listView.decrementCurrentIndex();
130 | }
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/Components/MainSwitcher.qml:
--------------------------------------------------------------------------------
1 | import QtQuick 2.12
2 | import QtGraphicalEffects 1.12
3 | import "CollectionsCarousel"
4 | import "GamelistScreen"
5 | import "utils.js" as Utils
6 |
7 | FocusScope {
8 | id: mainSwitcher
9 |
10 | property bool collectionsView: !gamelistScreen.focus
11 | required property bool optionsView
12 |
13 | function jumpToCollection() {
14 | collectionsCarousel.jumpTo();
15 | gamelistScreen.jumpToCollection();
16 | }
17 |
18 | function jumpToGame() { gamelistScreen.jumpToGame() }
19 |
20 | // Go to colleciton using short name
21 | function goToCollectionByShortName(shortName) {
22 | if (shortName) {
23 | let idx = 0;
24 | for (let i = 0; i < collectionSearchFilter.count; ++i) {
25 | const c = collectionSearchFilter.get(i);
26 | if (c.shortName === shortName) {
27 | idx = i;
28 | break;
29 | }
30 | }
31 | collectionsCarousel.positionViewAtIndex(idx, ListView.SnapPosition);
32 | collectionsCarousel.currentIndex = idx;
33 | }
34 | }
35 |
36 | // Go to game using ROM path
37 | function goToGameByPath(path) {
38 | let idx = 0;
39 | for (let i = 0; i < gamelistSearchFilter.count; ++i) {
40 | const g = gamelistSearchFilter.get(i);
41 | if (g.files.count && g.files.get(0).path === path) {
42 | idx = i;
43 | break;
44 | }
45 | }
46 | currentGameIndex = idx;
47 | }
48 |
49 | function showCollections() {
50 | collectionsCarousel.focus = true;
51 | }
52 |
53 | function showGamelist() {
54 | gamelistScreen.focus = true;
55 | }
56 |
57 | Keys.onPressed: {
58 | if (!collectionsCarousel.horizontalVelocity) {
59 | if (api.keys.isNextPage(event)) {
60 | event.accepted = true;
61 | Utils.saveGame();
62 | collectionsCarousel.incrementCurrentIndex();
63 | } else if (api.keys.isPrevPage(event)) {
64 | event.accepted = true;
65 | Utils.saveGame();
66 | collectionsCarousel.decrementCurrentIndex();
67 | }
68 | }
69 | }
70 |
71 | // Background
72 | Item {
73 | anchors.fill: parent
74 |
75 | Image {
76 | anchors.fill: parent
77 |
78 | source: "../assets/background-noise.webp"
79 | fillMode: Image.Tile
80 | }
81 |
82 | LinearGradient {
83 | anchors.fill: parent
84 |
85 | cached: true
86 | gradient: Gradient {
87 | GradientStop { position: 0.0; color: colorBgTop}
88 | GradientStop { position: 0.667; color: colorBgCenter}
89 | GradientStop { position: 1.0; color: colorBgBottom}
90 | }
91 | }
92 | }
93 |
94 | CollectionsCarousel {
95 | id: collectionsCarousel
96 |
97 | focus: true
98 |
99 | width: main.width
100 | height: main.height
101 | anchors.bottom: parent.bottom
102 |
103 | onSelect: {
104 | const collection = collectionSearchFilter.get(currentCollectionIndex);
105 | if (collection && collection.games && collection.games.count) {
106 | Utils.saveCollection();
107 | mainSwitcher.showGamelist();
108 | }
109 | }
110 | }
111 |
112 | GamelistScreen {
113 | id: gamelistScreen
114 |
115 | width: main.width
116 | height: main.height
117 | anchors.top: collectionsCarousel.bottom
118 |
119 | onBack: {
120 | Utils.saveCollection();
121 | Utils.saveGame();
122 | mainSwitcher.showCollections();
123 | }
124 |
125 | onItemSelected: main.launchGame()
126 | }
127 |
128 | states: [
129 | State {
130 | when: gamelistScreen.focus
131 | AnchorChanges {
132 | target: collectionsCarousel
133 | anchors.bottom: parent.top
134 | }
135 | }
136 | ]
137 |
138 | transitions: Transition {
139 | AnchorAnimation {
140 | duration: 666
141 | easing.type: Easing.OutExpo
142 | }
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/Components/Options/Options.qml:
--------------------------------------------------------------------------------
1 | import QtQuick 2.12
2 | import QtQuick.Layouts 1.11
3 | import QtGraphicalEffects 1.12
4 | import "../utils.js" as Utils
5 |
6 | FocusScope {
7 | id: options
8 |
9 | required property bool collectionsView
10 |
11 | signal setFilterSortCollections(string searchText, int sortIndex)
12 | signal setFilterSortGamelist(string searchText, int sortIndex)
13 | signal close
14 |
15 | function closeAndApply() {
16 | Utils.saveOptions({
17 | sortIndexCollections: sortCollections.currentIndex,
18 | sortIndexGamelist: sortGamelist.currentIndex,
19 | });
20 | close();
21 | if (collectionsView)
22 | setFilterSortCollections(searchInputCollections.searchText, sortCollections.currentIndex);
23 | else
24 | setFilterSortGamelist(searchInputGamelist.searchText, sortGamelist.currentIndex);
25 | }
26 |
27 | function resetSearchInputGamelist() {
28 | searchInputGamelist.searchText = "";
29 | }
30 |
31 | onFocusChanged: {
32 | if (focus) {
33 | if (collectionsView) {
34 | sortCollections.jumpTo(sortIndexCollections);
35 | searchInputCollections.focus = true;
36 | } else {
37 | sortGamelist.jumpTo(sortIndexGamelist);
38 | searchInputGamelist.focus = true;
39 | }
40 | }
41 | }
42 |
43 | Keys.onPressed: {
44 | if (api.keys.isFilters(event) || api.keys.isCancel(event)) {
45 | event.accepted = true;
46 | options.closeAndApply();
47 | }
48 | }
49 |
50 | // Drop shadow
51 | Rectangle {
52 | id: dropShadow
53 |
54 | width: background.width + vpx(6)
55 | height: background.height + vpx(6)
56 | anchors.centerIn: parent
57 |
58 | color: "#66000000"
59 | radius: radiusLarge
60 |
61 | layer.enabled: true
62 | layer.effect: GaussianBlur {
63 | radius: vpx(24)
64 | samples: vpx(49)
65 | transparentBorder: true
66 | }
67 | }
68 |
69 | // Transparent background
70 | Rectangle {
71 | id: background
72 |
73 | width: grid.width + vpx(40)
74 | height: grid.height + vpx(40)
75 | anchors.centerIn: parent
76 |
77 | color: "#c8000000"
78 | radius: radiusLarge
79 | }
80 |
81 | // Controls
82 | Item {
83 | anchors.fill: parent
84 |
85 | ColumnLayout {
86 | id: grid
87 |
88 | anchors.centerIn: parent
89 |
90 | spacing: spacingStd
91 | width: vpx(340)
92 |
93 | // Caption Search
94 | Caption {
95 | text: `Search ${collectionsView ? "collections" : "games"}`
96 | imageSource: "../../assets/search-solid.svg"
97 |
98 | Layout.fillWidth: true
99 | Layout.preferredHeight: vpx(20)
100 | }
101 |
102 | // Search input (collections)
103 | SearchTextInput {
104 | id: searchInputCollections
105 |
106 | Layout.fillWidth: true
107 | Layout.bottomMargin: spacingStd
108 |
109 | onAccept: options.closeAndApply()
110 |
111 | visible: collectionsView
112 |
113 | KeyNavigation.down: sortCollections
114 | }
115 |
116 | // Search input (games)
117 | SearchTextInput {
118 | id: searchInputGamelist
119 |
120 | Layout.fillWidth: true
121 | Layout.bottomMargin: spacingStd
122 |
123 | onAccept: options.closeAndApply()
124 |
125 | visible: !collectionsView
126 |
127 | KeyNavigation.down: sortGamelist
128 | }
129 |
130 | // Caption Sorting
131 | Caption {
132 | text: `Sort ${collectionsView ? "collections" : "games"} by`
133 | imageSource: "../../assets/sort-alpha-down-solid.svg"
134 |
135 | Layout.fillWidth: true
136 | Layout.preferredHeight: vpx(20)
137 | }
138 |
139 | // Sort (collections)
140 | Choice {
141 | id: sortCollections
142 |
143 | visible: collectionsView
144 |
145 | Layout.preferredHeight: vpx(28)
146 | Layout.fillWidth: true
147 | Layout.fillHeight: true
148 | Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
149 | Layout.bottomMargin: spacingStd
150 |
151 | model: listModelSortCollections
152 |
153 | KeyNavigation.up: searchInputCollections
154 | KeyNavigation.down: doneButton
155 |
156 | onAccept: options.closeAndApply()
157 | }
158 |
159 | // Sort (games)
160 | Choice {
161 | id: sortGamelist
162 |
163 | visible: !collectionsView
164 |
165 | Layout.preferredHeight: vpx(28)
166 | Layout.fillWidth: true
167 | Layout.fillHeight: true
168 | Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
169 |
170 | model: listModelSortGamelist
171 |
172 | KeyNavigation.up: searchInputGamelist
173 | KeyNavigation.down: doneButton
174 |
175 | onAccept: options.closeAndApply()
176 | }
177 |
178 | // Done button
179 | Button {
180 | id: doneButton
181 |
182 | text: "Done"
183 |
184 | Layout.preferredHeight: vpx(24)
185 | Layout.fillWidth: true
186 | Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
187 |
188 | KeyNavigation.up: collectionsView ? sortCollections : sortGamelist
189 |
190 | onActivate: close()
191 | }
192 | }
193 | }
194 | }
195 |
--------------------------------------------------------------------------------
/Components/GamelistScreen/GameGrid/GameGrid.qml:
--------------------------------------------------------------------------------
1 | import QtQuick 2.12
2 | import QtGraphicalEffects 1.12
3 |
4 | Item {
5 | id: gameGrid
6 |
7 | // Delay animation and scroll to item to make things work
8 | property bool enableSelectedItem: false
9 |
10 | required property bool optionsView
11 |
12 | onOptionsViewChanged: {
13 | if (!optionsView)
14 | resetSelectedItem();
15 | }
16 |
17 | function jumpToCollection() {
18 | gridView.cellsNeedRecalc();
19 | if (!collectionsView)
20 | resetSelectedItem();
21 | }
22 |
23 | function jumpToGame() {
24 | if (currentGameIndex >= 0 && currentGameIndex !== gridView.currentIndex)
25 | resetSelectedItem();
26 | }
27 |
28 | function resetSelectedItem() {
29 | enableSelectedItem = false;
30 | resetSelectedTimer.restart();
31 | }
32 |
33 | Timer {
34 | id: resetSelectedTimer
35 |
36 | interval: durationVeryShort
37 | onTriggered: {
38 | gridView.currentIndex = currentGameIndex;
39 | enableSelectedItem = true;
40 | }
41 | }
42 |
43 | // Smoothly fade out grid at top and bottom
44 | LinearGradient {
45 | id: gradientMask
46 | anchors.fill: parent
47 | visible: false
48 | gradient: Gradient {
49 | GradientStop { position: 0.01; color: "transparent"}
50 | GradientStop { position: 0.04; color: "#55ffffff"}
51 | GradientStop { position: 0.1; color: "#ffffffff" }
52 | GradientStop { position: 0.75; color: "#ffffffff" }
53 | GradientStop { position: 0.99; color: "transparent"}
54 | }
55 | }
56 |
57 | // Opacity mask needs to overlap grid as selected item may stick out of grid
58 | // and would be clipped.
59 | Item {
60 | anchors.fill: parent
61 |
62 | layer.enabled: true
63 | layer.effect: OpacityMask {
64 | maskSource: gradientMask
65 | }
66 |
67 | GridView {
68 | id: gridView
69 |
70 | property bool firstImageLoaded: false
71 | property real cellHeightRatio: 0.5
72 |
73 | focus: true
74 |
75 | anchors {
76 | fill: parent
77 | rightMargin: marginHorizGamelistScreen
78 | leftMargin: marginHorizGamelistScreen
79 | }
80 |
81 | model: gamelistSearchFilter
82 |
83 | keyNavigationEnabled: false
84 | cacheBuffer: 0
85 | snapMode: GridView.SnapToRow
86 |
87 | highlightRangeMode: GridView.StrictlyEnforceRange
88 | highlightFollowsCurrentItem: true
89 |
90 | cellWidth: width / columnCount
91 | cellHeight: cellWidth * cellHeightRatio;
92 |
93 | // Current item should stay in the middle (ensures smooth scrolling)
94 | preferredHighlightBegin: (height - cellHeight) / 2
95 | preferredHighlightEnd: (height + cellHeight) / 2
96 |
97 | property real columnCount: {
98 | if (cellHeightRatio > 1.2) return 5;
99 | if (cellHeightRatio > 0.6) return 4;
100 | return 3;
101 | }
102 |
103 | function cellsNeedRecalc() {
104 | firstImageLoaded = false;
105 | cellHeightRatio = 0.5;
106 | }
107 |
108 | function calcHeightRatio(imageW, imageH) {
109 | cellHeightRatio = 0.5;
110 |
111 | if (imageW > 0 && imageH > 0)
112 | cellHeightRatio = imageH / imageW;
113 | }
114 |
115 | onModelChanged: cellsNeedRecalc()
116 |
117 | onCurrentIndexChanged: {
118 | if (enableSelectedItem)
119 | currentGameIndex = currentIndex;
120 | }
121 |
122 | Keys.onUpPressed: {
123 | event.accepted = true;
124 | moveCurrentIndexUp();
125 | }
126 | Keys.onRightPressed: {
127 | event.accepted = true;
128 | moveCurrentIndexRight();
129 | }
130 | Keys.onDownPressed: {
131 | event.accepted = true;
132 | moveCurrentIndexDown();
133 | }
134 | Keys.onLeftPressed: {
135 | event.accepted = true;
136 | moveCurrentIndexLeft();
137 | }
138 |
139 | Keys.onPressed: {
140 | if (api.keys.isCancel(event)) {
141 | event.accepted = true;
142 | gamelistScreen.back();
143 | }
144 | else if (api.keys.isAccept(event)) {
145 | event.accepted = true;
146 | gamelistScreen.itemSelected();
147 | }
148 | }
149 |
150 | delegate: GameGridItem {
151 | width: GridView.view.cellWidth
152 | height: GridView.view.cellHeight
153 | state: !optionsView && gameGrid.enableSelectedItem && GridView.isCurrentItem ? "selected" : ""
154 | columnCount: GridView.view.columnCount
155 |
156 | imageHeightRatio: gridView.firstImageLoaded ? gridView.cellHeightRatio : 0.5
157 |
158 | onClicked: selectItem();
159 |
160 | onDoubleClicked: {
161 | selectItem();
162 | gamelistScreen.itemSelected()
163 | }
164 |
165 | onImageLoaded: {
166 | // NOTE: because images are loaded asynchronously,
167 | // firstImageLoaded may appear false multiple times!
168 | if (!gridView.firstImageLoaded) {
169 | gridView.firstImageLoaded = true;
170 | gridView.calcHeightRatio(imageWidth, imageHeight);
171 | }
172 | }
173 |
174 | function selectItem() {
175 | gridView.currentIndex = index;
176 | }
177 | }
178 | }
179 | }
180 | }
181 |
--------------------------------------------------------------------------------
/Components/GamelistScreen/GameGrid/GameGridItem.qml:
--------------------------------------------------------------------------------
1 | import QtQuick 2.12
2 | import QtGraphicalEffects 1.12
3 |
4 | Item {
5 | id: gridItem
6 | height: width * imageHeightRatio
7 | z: 1
8 |
9 | required property var modelData
10 | required property int index
11 | required property int columnCount
12 |
13 | property alias imageWidth: imageBoxArt.paintedWidth
14 | property alias imageHeight: imageBoxArt.paintedHeight
15 | property bool imageLoading: imageBoxArt.status === Image.Loading
16 | property real imageHeightRatio: 0.5
17 |
18 | signal clicked
19 | signal doubleClicked
20 | signal imageLoaded(int imgWidth, int imgHeight)
21 |
22 | readonly property int tilePadding: 4
23 |
24 | // Keep outmost left/right items within grid bounds
25 | property real translateX: {
26 | if (index % columnCount === 0) {
27 | // First column
28 | return (width - tilePadding) / 2 - 24;
29 | }
30 | if (index % columnCount === columnCount - 1) {
31 | // Last column
32 | return - (width - tilePadding) / 2 + 24;
33 | }
34 | return 0;
35 | }
36 |
37 | transform: Translate {
38 | id: translation
39 | x: 0
40 | }
41 |
42 | states: [
43 | State {
44 | name: "selected"
45 | PropertyChanges {
46 | target: gridItem
47 | z: 3
48 | }
49 | PropertyChanges {
50 | target: translation
51 | x: vpx(gridItem.translateX)
52 | }
53 | PropertyChanges {
54 | target: boxArt
55 | scale: 1
56 | layer.enabled: true
57 | }
58 | }
59 | ]
60 |
61 | transitions: Transition {
62 | PropertyAnimation {
63 | target: gridItem
64 | property: "z"
65 | duration: durationShort
66 | }
67 | PropertyAnimation {
68 | target: translation
69 | property: "x"
70 | duration: durationShort
71 | easing.type: Easing.InOutCubic
72 | }
73 | PropertyAnimation {
74 | target: boxArt
75 | property: "scale"
76 | duration: durationShort
77 | easing.type: Easing.InOutCubic
78 | }
79 | }
80 |
81 | // Box art image (non-selected are scaled down, so zoomed-in picture are crisp)
82 | Rectangle {
83 | id: boxArt
84 | width: parent.width * boxArtScaleFactor
85 | height: parent.height * boxArtScaleFactor
86 | transform: Translate {
87 | x: - width
88 | y: - height
89 | }
90 |
91 | color: "transparent"
92 |
93 | scale: 1 / boxArtScaleFactor
94 |
95 | layer.enabled: false
96 | layer.effect: DropShadow {
97 | spread: 0.15
98 | horizontalOffset: 0
99 | verticalOffset: 0
100 | radius: vpx(18)
101 | samples: 37
102 | color: "black"
103 | transparentBorder: true
104 | }
105 |
106 | Image {
107 | id: imageBoxArt
108 | anchors {
109 | fill: parent
110 | margins: vpx(tilePadding)
111 | }
112 |
113 | asynchronous: true
114 | visible: modelData && modelData.assets ? modelData.assets.boxFront : false
115 |
116 | source: modelData && modelData.assets ? modelData.assets.boxFront : ""
117 | sourceSize { width: 512; height: 512 }
118 | fillMode: Image.PreserveAspectFit
119 | smooth: true
120 |
121 | onStatusChanged: {
122 | if (status === Image.Ready) {
123 | imageHeightRatio = paintedHeight / paintedWidth;
124 | gridItem.imageLoaded(paintedWidth, paintedHeight);
125 | }
126 | }
127 | }
128 |
129 | // Missing box art fallback: Show dummy box art with game title
130 | Rectangle {
131 | anchors {
132 | fill: parent
133 | margins: vpx(tilePadding)
134 | }
135 | border {
136 | width: vpx(8)
137 | color: colorBoxBorder
138 | }
139 | color: colorBgBoxArt
140 | visible: modelData && modelData.assets ? !modelData.assets.boxFront : true
141 |
142 | Item {
143 | anchors.fill: parent
144 |
145 | Text {
146 | anchors.fill: parent
147 | text: "?"
148 | font {
149 | pixelSize: vpx(9999)
150 | family: generalFont.name
151 | }
152 | color: "#777"
153 | fontSizeMode: Text.Fit
154 | horizontalAlignment: Text.AlignHCenter
155 | verticalAlignment: Text.AlignVCenter
156 | }
157 |
158 | Text {
159 | anchors {
160 | fill: parent
161 | margins: vpx(16)
162 | }
163 | text: modelData && modelData.title ? modelData.title : ""
164 | wrapMode: Text.WrapAnywhere
165 | horizontalAlignment: Text.AlignHCenter
166 | verticalAlignment: Text.AlignVCenter
167 | color: colorFontBox
168 | font {
169 | pixelSize: vpx(48)
170 | family: headerFont.name
171 | }
172 | }
173 | }
174 | }
175 |
176 | // Loading spinner
177 | Item {
178 | visible: imageLoading
179 |
180 | anchors {
181 | fill: parent
182 | margins: vpx(tilePadding)
183 | }
184 |
185 | Rectangle {
186 | width: imageBoxArt.width
187 | height: imageBoxArt.height
188 | color: "black"
189 | anchors.fill: parent
190 | }
191 |
192 | Image {
193 | id: loadingSpinner
194 |
195 | height: vpx(32)
196 | anchors.centerIn: parent
197 |
198 | source: "../../../assets/circle-notch-solid.svg"
199 | fillMode: Image.PreserveAspectFit
200 | smooth: true
201 | visible: false
202 | }
203 |
204 | ColorOverlay {
205 | anchors.fill: loadingSpinner
206 | source: loadingSpinner
207 | color: colorFontStrong
208 |
209 | RotationAnimator on rotation {
210 | loops: Animator.Infinite;
211 | from: 0;
212 | to: 360;
213 | duration: durationLoadingSpinner
214 | }
215 | }
216 | }
217 | }
218 |
219 | MouseArea {
220 | anchors.fill: parent
221 | onClicked: gridItem.clicked()
222 | onDoubleClicked: gridItem.doubleClicked()
223 | }
224 | }
225 |
--------------------------------------------------------------------------------
/Components/Main.qml:
--------------------------------------------------------------------------------
1 | import QtQuick 2.12
2 | import QtGraphicalEffects 1.12
3 | import "SearchFilter"
4 | import "Options"
5 | import "utils.js" as Utils
6 |
7 | FocusScope {
8 | id: main
9 |
10 | // Prevent filter reset collection on initial load
11 | property bool initialLoad: true
12 |
13 | // Current collection/game index
14 | property int currentCollectionIndex: -1
15 | property int currentGameIndex: -1
16 |
17 | // Sorting/filtering
18 | property string searchTextCollections: ""
19 | property int sortIndexCollections: 0
20 | property string prevCollectionShortName: "" // collection to jump to jump
21 |
22 | property string searchTextGamelist: ""
23 | property int sortIndexGamelist: 0
24 | property string prevGamePath: "" // remember game to jump to jump
25 |
26 | Component.onCompleted: {
27 | prevCollectionShortName = Utils.restoreCollection();
28 |
29 | const options = Utils.restoreOptions();
30 | sortIndexCollections = options.sortIndexCollections;
31 | sortIndexGamelist = options.sortIndexGamelist;
32 |
33 | Qt.callLater(collectionSearchFilter.restoreCollection);
34 |
35 | // Go to gamelist if we're coming back from a game
36 | if (Utils.isGameLaunch())
37 | mainSwitcher.showGamelist();
38 | }
39 |
40 | onCurrentCollectionIndexChanged: {
41 | const collection = collectionSearchFilter.get(currentCollectionIndex);
42 | if (collection && collection.games)
43 | gamelistSearchFilter.sourceModel = collection.games;
44 | mainSwitcher.jumpToCollection();
45 |
46 | // Reset gamelist search text
47 | searchTextGamelist = "";
48 | options.resetSearchInputGamelist();
49 |
50 | Qt.callLater(mainSwitcher.jumpToGame);
51 | }
52 |
53 | onCurrentGameIndexChanged: {
54 | Qt.callLater(mainSwitcher.jumpToGame);
55 | }
56 |
57 | Component.onDestruction: {
58 | Utils.saveCollection();
59 | Utils.saveGame();
60 | }
61 |
62 | function launchGame() {
63 | Utils.saveCollection();
64 | Utils.saveGame();
65 | Utils.saveGameLaunch();
66 |
67 | const game = gamelistSearchFilter.get(currentGameIndex);
68 | if (game && game.modelData)
69 | game.modelData.launch();
70 | }
71 |
72 | // Search/filter collection
73 |
74 | CollectionSortFilterProxyModel {
75 | id: collectionSearchFilter
76 |
77 | sourceModel: api.collections
78 |
79 | searchText: main.searchTextCollections
80 | sortIndex: main.sortIndexCollections
81 |
82 | function restoreCollection() {
83 | main.initialLoad = false;
84 |
85 | if (count) {
86 | if (prevCollectionShortName) {
87 | // Restore collection from before
88 | mainSwitcher.goToCollectionByShortName(prevCollectionShortName);
89 | prevCollectionShortName = "";
90 | } else {
91 | currentCollectionIndex = 0;
92 | }
93 | }
94 | }
95 |
96 | // Restore collection after search/filter change
97 | onChanged: {
98 | if (!initialLoad)
99 | Qt.callLater(collectionSearchFilter.restoreCollection);
100 | }
101 | }
102 |
103 | GamelistSortFilterProxyModel {
104 | id: gamelistSearchFilter
105 |
106 | searchText: main.searchTextGamelist
107 | sortIndex: main.sortIndexGamelist
108 |
109 | function restoreGame() {
110 | // No games
111 | if (count === 0) {
112 | currentGameIndex = -1;
113 | return;
114 | }
115 |
116 | // Restore game from before
117 | if (prevGamePath) {
118 | mainSwitcher.goToGameByPath(prevGamePath);
119 | prevGamePath = "";
120 | return
121 | }
122 |
123 | // Otherwise show first game
124 | currentGameIndex = 0;
125 | }
126 |
127 | onSourceModelChanged: {
128 | if (count) {
129 | prevGamePath = Utils.restoreGame();
130 | Qt.callLater(gamelistSearchFilter.restoreGame);
131 | }
132 | }
133 |
134 | // Restore game after search/filter change
135 | onChanged: Qt.callLater(gamelistSearchFilter.restoreGame)
136 | }
137 |
138 | Keys.onPressed: {
139 | if (api.keys.isFilters(event)) {
140 | event.accepted = true;
141 | options.focus = true;
142 | }
143 | }
144 |
145 | MainSwitcher {
146 | id: mainSwitcher
147 |
148 | optionsView: options.focus
149 |
150 | anchors.fill: parent
151 | focus: true
152 | }
153 |
154 | // Options
155 | MouseArea { // Prevent mouse actions on lower elements
156 | anchors.fill: parent
157 | enabled: options.focus
158 | }
159 | GaussianBlur {
160 | anchors.fill: parent
161 |
162 | source: mainSwitcher
163 | radius: options.focus ? vpx(24) : 0
164 | samples: vpx(49)
165 |
166 | Behavior on radius {
167 | PropertyAnimation {
168 | duration: durationShort
169 | easing.type: Easing.OutQuad
170 | }
171 | }
172 | }
173 | Options {
174 | id: options
175 |
176 | collectionsView: mainSwitcher.collectionsView
177 |
178 | anchors.fill: parent
179 | opacity: 0
180 |
181 | onClose: {
182 | mainSwitcher.focus = true;
183 | // Remember previous collection/game to jump to after updating model
184 | if (mainSwitcher.collectionsView) {
185 | const prevCollection = collectionSearchFilter.get(currentCollectionIndex);
186 | main.prevCollectionShortName = prevCollection && prevCollection.shortName ? prevCollection.shortName : "";
187 | } else {
188 | const prevGame = gamelistSearchFilter.get(currentGameIndex);
189 | if (prevGame && prevGame.files && prevGame.files.count)
190 | main.prevGamePath = prevGame.files.get(0).path;
191 | }
192 | }
193 |
194 | onSetFilterSortCollections: {
195 | main.searchTextCollections = searchText;
196 | main.sortIndexCollections = sortIndex;
197 | }
198 |
199 | onSetFilterSortGamelist: {
200 | main.searchTextGamelist = searchText;
201 | main.sortIndexGamelist = sortIndex;
202 | }
203 |
204 | Behavior on opacity {
205 | PropertyAnimation {
206 | duration: durationVeryShort
207 | easing.type: Easing.OutQuad
208 | }
209 | }
210 | }
211 |
212 | states: [
213 | State {
214 | when: options.focus
215 | PropertyChanges {
216 | target: options
217 | opacity: 1.0
218 | }
219 | }
220 | ]
221 | }
222 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # Slick theme for Pegasus
4 |
5 | A slick theme focusing on simplicity and usability.
6 |
7 | ## Installation
8 |
9 | [Download](https://github.com/buzz/pegasus-theme-slick/archive/master.zip) and
10 | extract the theme to your [theme
11 | directory](http://pegasus-frontend.org/docs/user-guide/installing-themes). You
12 | can then select it in the settings menu of Pegasus.
13 |
14 | ## Platforms
15 |
16 | 
17 |
18 | The theme features technical details and a background images for the following platforms.
19 |
20 | `amigacd32`, `amiga`, `amstradcpc`, `arcade`, `atari2600`, `atari7800`,
21 | `atarilynx`, `c64`, `dreamcast`, `famicom`, `fds`, `gamegear`, `gba`, `gbc`,
22 | `gb`, `mastersystem`, `megadrive`, `n64`, `naomi`, `neogeo`, `nes`, `ngpc`,
23 | `pcenginecd`, `pcengine`, `pc`, `ps2`, `ps3`, `psx`, `saturn`, `scummvm`,
24 | `sega32x`, `segacd`, `sfc`, `sg-1000`, `sgb`, `snes`, `switch`, `vectrex`,
25 | `x68000`
26 |
27 | ## Supported metadata
28 |
29 | - `boxFront`
30 | - `logo`
31 | - `poster`
32 | - `screenshot`
33 | - `video`
34 |
35 | ## Credits
36 |
37 | Based on [Flixnet theme](https://github.com/mmatyas/pegasus-theme-flixnet) by
38 | [mmatyas](https://github.com/mmatyas).
39 |
40 | ### Images
41 |
42 | - Amstrad CPC, CC BY-SA 2.0 FR, Photograph by [Rama](https://commons.wikimedia.org/wiki/File:Amstrad_CPC_464-IMG_4849.JPG)
43 | - Arcade, CC BY 2.0 FR, Photograph by [Steven Miller](https://www.flickr.com/photos/aloha75/4906597504/)
44 | - Atari 2600, Public domain, Photograph by [Evan-Amos](https://commons.wikimedia.org/wiki/File:Atari-2600-Wood-4Sw-Set.png)
45 | - Atari 7800, Public domain, Photograph by [Evan-Amos](https://commons.wikimedia.org/wiki/File:Atari-7800-wControl-Pad-L.jpg)
46 | - Atari Lynx, Creative Commons Attribution-Share Alike 3.0 Unported, Photograph by [Evan-Amos](https://commons.wikimedia.org/wiki/File:Atari-Lynx-I-Handheld.jpg)
47 | - Amiga 500, CC BY 2.5, Photograph by [Bill Bertram](https://commons.wikimedia.org/wiki/File:Leander_Amiga500.jpg)
48 | - Amiga CD32, Public domain, Photograph by [Evan-Amos](https://commons.wikimedia.org/wiki/File:Amiga-CD32-wController-L.jpg)
49 | - Commodore 64, Public domain, Photograph by [Evan-Amos](https://commons.wikimedia.org/wiki/File:Commodore-64-Computer-FL.jpg)
50 | - Dreamcast, CC BY-SA 3.0, Photograph by [Evan-Amos](https://commons.wikimedia.org/wiki/File:Dreamcast-Console-Set.png)
51 | - Famicom, Public domain, Photograph by [Evan-Amos](https://commons.wikimedia.org/wiki/File:Nintendo-Famicom-Disk-System.jpg)
52 | - Famicom Disk System, Public domain, Photograph by [Evan-Amos](https://commons.wikimedia.org/wiki/File:Nintendo-Famicom-Disk-System.jpg)
53 | - Game Boy, Public domain, Photograph by [Evan-Amos](https://commons.wikimedia.org/wiki/File:Game-Boy-FL.jpg)
54 | - Game Boy Advance, Public domain, Photograph by [Evan-Amos](https://commons.wikimedia.org/wiki/File:Nintendo-Game-Boy-Advance-Milky-Blue-FL.png)
55 | - Game Boy Color, Public domain, Photograph by [Evan-Amos](https://commons.wikimedia.org/wiki/File:Nintendo-Game-Boy-Color-FL.png)
56 | - Game Gear, Public domain, Photograph by [Evan-Amos](https://commons.wikimedia.org/wiki/File:Game-Gear-Handheld.jpg)
57 | - IBM PC, CeCILL, Photograph by [Rama & Musée Bolo](https://en.wikipedia.org/wiki/File:IBM_PC-IMG_7271_(transparent).png)
58 | - NES, Public domain, Photograph by [Evan-Amos](https://commons.wikimedia.org/wiki/File:NES-Console-Set.jpg)
59 | - Nintendo 64, Public domain, Photograph by [Evan-Amos](https://commons.wikimedia.org/wiki/File:N64-Console-Set.jpg)
60 | - Nintendo Switch, Public domain, Photograph by [Evan-Amos](https://commons.wikimedia.org/wiki/File:Nintendo-Switch-Console-Docked-wJoyConRB.jpg)
61 | - Sega Saturn, Public domain, Photograph by [Evan-Amos](https://commons.wikimedia.org/wiki/File:Sega-Saturn-Console-Set-Mk1.png)
62 | - Sega CD, Public domain, Photograph by [Evan-Amos](https://commons.wikimedia.org/wiki/File:Sega-CD-Model1-Set.jpg)
63 | - Sega CD 32X, Public domain, Photograph by [Evan-Amos](https://commons.wikimedia.org/wiki/File:Sega-Genesis-Model2-32X.png)
64 | - Sega Master System, Public domain, Photograph by [Evan-Amos](https://commons.wikimedia.org/wiki/File:Sega-Master-System-Set.jpg)
65 | - Sega Naomi, Personal use only, https://www.kindpng.com/imgv/iiJibbT_sega-naomi-arcade-png-transparent-png/
66 | - Sega Mega Drive, Public domain, Photograph by [Evan-Amos](https://commons.wikimedia.org/wiki/File:Sega-Mega-Drive-EU-Mk1-wController-FL.jpg)
67 | - Sega SG-1000, CC BY-SA 3.0, Photograph by [Evan-Amos](https://en.wikipedia.org/wiki/File:Sega-SG-1000-Console-Set.jpg)
68 | - Super Famicom, Public domain, Photograph by [Evan-Amos](https://en.wikipedia.org/wiki/File:SNES-Mod1-Console-Set.jpg)
69 | - Super GameBoy, Public domain, Photograph by [Evan-Amos](https://commons.wikimedia.org/wiki/File:Nintendo-Super-Game-Boy.jpg)
70 | - Super Nintendo Entertainment System, Public domain, Photograph by [Evan-Amos](https://commons.wikimedia.org/wiki/File:Nintendo-Super-Famicom-Set-FL.jpg)
71 | - Neo Geo, Public domain, Photographs by Evan-Amos [1](https://commons.wikimedia.org/wiki/File:Neo-Geo-AES-Controller-FR.jpg) [2](https://commons.wikimedia.org/wiki/File:Neo-Geo-AES-FL.png)
72 | - Neo Geo Pocket, Public domain, Photographs by [Evan-Amos](https://commons.wikimedia.org/wiki/File:Neo-Geo-Pocket-Color-Blue-Left.png)
73 | - PlayStation, Public domain, Photograph by [Evan-Amos](https://commons.wikimedia.org/wiki/File:PlayStation-SCPH-1000-with-Controller.jpg)
74 | - PlayStation 2, Public domain, Photograph by [Evan-Amos](https://commons.wikimedia.org/wiki/File:Sony-PlayStation-2-30001-wController-L.png)
75 | - PlayStation 3, Public domain, Photograph by [Evan-Amos](https://commons.wikimedia.org/wiki/File:Sony-PlayStation-3-2001A-wController-L.jpg)
76 | - ScummVM logo, GPLv2, [The ScummVM Team](https://commons.wikimedia.org/wiki/File:ScummVM_%22Modern_Remastered%22_Logo.svg)
77 | - TurboGrafx 16, Public domain, Photograph by [Evan-Amos](https://en.wikipedia.org/wiki/File:TurboGrafx16-Console-Set.jpg)
78 | - TurboGrafx 16 CD, Public domain, Photograph by [Evan-Amos](https://commons.wikimedia.org/wiki/File:NEC-TurboGrafx-16-CD-FL.jpg)
79 | - Vectrex, CC BY-SA 3.0, Photograph by [Evan-Amos](https://commons.wikimedia.org/wiki/File:Vectrex-Console-Set.jpg)
80 | - X68000, Private image, Photograph by [HCKblog](https://hckblog.wordpress.com/2013/06/05/hck-vs-the-sharp-x68000/)
81 |
82 | ## License
83 |
84 | [](http://creativecommons.org/licenses/by-nc-sa/4.0/)
85 |
86 | Content licensed under [CC-BY-NC-SA](http://creativecommons.org/licenses/by-nc-sa/4.0/) unless otherwise noted.
87 |
88 | Font Awesome icons licensed under [Creative Commons Attribution 4.0 International](https://creativecommons.org/licenses/by/4.0/).
89 |
--------------------------------------------------------------------------------