├── .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 | ![](.meta/screenshot.jpg) 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 | ![](.meta/screenshot-platform.jpg) 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 | [![CC-BY-NC-SA](https://i.creativecommons.org/l/by-nc-sa/4.0/88x31.png)](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 | --------------------------------------------------------------------------------