mirror of
https://github.com/belsabbagh/dotfiles.git
synced 2026-04-11 17:47:09 +00:00
659 lines
25 KiB
QML
659 lines
25 KiB
QML
pragma ComponentBehavior: Bound
|
|
|
|
import ".."
|
|
import "../components"
|
|
import "../../launcher/services"
|
|
import qs.components
|
|
import qs.components.controls
|
|
import qs.components.effects
|
|
import qs.components.containers
|
|
import qs.services
|
|
import qs.config
|
|
import qs.utils
|
|
import Caelestia
|
|
import Quickshell
|
|
import Quickshell.Widgets
|
|
import QtQuick
|
|
import QtQuick.Layouts
|
|
import "../../../utils/scripts/fuzzysort.js" as Fuzzy
|
|
|
|
Item {
|
|
id: root
|
|
|
|
required property Session session
|
|
|
|
property var selectedApp: root.session.launcher.active
|
|
property bool hideFromLauncherChecked: false
|
|
property bool favouriteChecked: false
|
|
|
|
anchors.fill: parent
|
|
|
|
onSelectedAppChanged: {
|
|
root.session.launcher.active = root.selectedApp;
|
|
updateToggleState();
|
|
}
|
|
|
|
Connections {
|
|
target: root.session.launcher
|
|
function onActiveChanged() {
|
|
root.selectedApp = root.session.launcher.active;
|
|
updateToggleState();
|
|
}
|
|
}
|
|
|
|
function updateToggleState() {
|
|
if (!root.selectedApp) {
|
|
root.hideFromLauncherChecked = false;
|
|
root.favouriteChecked = false;
|
|
return;
|
|
}
|
|
|
|
const appId = root.selectedApp.id || root.selectedApp.entry?.id;
|
|
|
|
root.hideFromLauncherChecked = Config.launcher.hiddenApps && Config.launcher.hiddenApps.length > 0 && Strings.testRegexList(Config.launcher.hiddenApps, appId);
|
|
root.favouriteChecked = Config.launcher.favouriteApps && Config.launcher.favouriteApps.length > 0 && Strings.testRegexList(Config.launcher.favouriteApps, appId);
|
|
}
|
|
|
|
function saveHiddenApps(isHidden) {
|
|
if (!root.selectedApp) {
|
|
return;
|
|
}
|
|
|
|
const appId = root.selectedApp.id || root.selectedApp.entry?.id;
|
|
|
|
const hiddenApps = Config.launcher.hiddenApps ? [...Config.launcher.hiddenApps] : [];
|
|
|
|
if (isHidden) {
|
|
if (!hiddenApps.includes(appId)) {
|
|
hiddenApps.push(appId);
|
|
}
|
|
} else {
|
|
const index = hiddenApps.indexOf(appId);
|
|
if (index !== -1) {
|
|
hiddenApps.splice(index, 1);
|
|
}
|
|
}
|
|
|
|
Config.launcher.hiddenApps = hiddenApps;
|
|
Config.save();
|
|
}
|
|
|
|
AppDb {
|
|
id: allAppsDb
|
|
|
|
path: `${Paths.state}/apps.sqlite`
|
|
favouriteApps: Config.launcher.favouriteApps
|
|
entries: DesktopEntries.applications.values
|
|
}
|
|
|
|
property string searchText: ""
|
|
|
|
function filterApps(search: string): list<var> {
|
|
if (!search || search.trim() === "") {
|
|
const apps = [];
|
|
for (let i = 0; i < allAppsDb.apps.length; i++) {
|
|
apps.push(allAppsDb.apps[i]);
|
|
}
|
|
return apps;
|
|
}
|
|
|
|
if (!allAppsDb.apps || allAppsDb.apps.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
const preparedApps = [];
|
|
for (let i = 0; i < allAppsDb.apps.length; i++) {
|
|
const app = allAppsDb.apps[i];
|
|
const name = app.name || app.entry?.name || "";
|
|
preparedApps.push({
|
|
_item: app,
|
|
name: Fuzzy.prepare(name)
|
|
});
|
|
}
|
|
|
|
const results = Fuzzy.go(search, preparedApps, {
|
|
all: true,
|
|
keys: ["name"],
|
|
scoreFn: r => r[0].score
|
|
});
|
|
|
|
return results.sort((a, b) => b._score - a._score).map(r => r.obj._item);
|
|
}
|
|
|
|
property list<var> filteredApps: []
|
|
|
|
function updateFilteredApps() {
|
|
filteredApps = filterApps(searchText);
|
|
}
|
|
|
|
onSearchTextChanged: {
|
|
updateFilteredApps();
|
|
}
|
|
|
|
Component.onCompleted: {
|
|
updateFilteredApps();
|
|
}
|
|
|
|
Connections {
|
|
target: allAppsDb
|
|
function onAppsChanged() {
|
|
updateFilteredApps();
|
|
}
|
|
}
|
|
|
|
SplitPaneLayout {
|
|
anchors.fill: parent
|
|
|
|
leftContent: Component {
|
|
|
|
ColumnLayout {
|
|
id: leftLauncherLayout
|
|
anchors.fill: parent
|
|
|
|
spacing: Appearance.spacing.small
|
|
|
|
RowLayout {
|
|
spacing: Appearance.spacing.smaller
|
|
|
|
StyledText {
|
|
text: qsTr("Launcher")
|
|
font.pointSize: Appearance.font.size.large
|
|
font.weight: 500
|
|
}
|
|
|
|
Item {
|
|
Layout.fillWidth: true
|
|
}
|
|
|
|
ToggleButton {
|
|
toggled: !root.session.launcher.active
|
|
icon: "settings"
|
|
accent: "Primary"
|
|
iconSize: Appearance.font.size.normal
|
|
horizontalPadding: Appearance.padding.normal
|
|
verticalPadding: Appearance.padding.smaller
|
|
tooltip: qsTr("Launcher settings")
|
|
|
|
onClicked: {
|
|
if (root.session.launcher.active) {
|
|
root.session.launcher.active = null;
|
|
} else {
|
|
if (root.filteredApps.length > 0) {
|
|
root.session.launcher.active = root.filteredApps[0];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
StyledText {
|
|
Layout.topMargin: Appearance.spacing.large
|
|
text: qsTr("Applications (%1)").arg(root.searchText ? root.filteredApps.length : allAppsDb.apps.length)
|
|
font.pointSize: Appearance.font.size.normal
|
|
font.weight: 500
|
|
}
|
|
|
|
StyledText {
|
|
text: qsTr("All applications available in the launcher")
|
|
color: Colours.palette.m3outline
|
|
}
|
|
|
|
StyledRect {
|
|
Layout.fillWidth: true
|
|
Layout.topMargin: Appearance.spacing.normal
|
|
Layout.bottomMargin: Appearance.spacing.small
|
|
|
|
color: Colours.layer(Colours.palette.m3surfaceContainer, 2)
|
|
radius: Appearance.rounding.full
|
|
|
|
implicitHeight: Math.max(searchIcon.implicitHeight, searchField.implicitHeight, clearIcon.implicitHeight)
|
|
|
|
MaterialIcon {
|
|
id: searchIcon
|
|
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: Appearance.padding.normal
|
|
|
|
text: "search"
|
|
color: Colours.palette.m3onSurfaceVariant
|
|
}
|
|
|
|
StyledTextField {
|
|
id: searchField
|
|
|
|
anchors.left: searchIcon.right
|
|
anchors.right: clearIcon.left
|
|
anchors.leftMargin: Appearance.spacing.small
|
|
anchors.rightMargin: Appearance.spacing.small
|
|
|
|
topPadding: Appearance.padding.normal
|
|
bottomPadding: Appearance.padding.normal
|
|
|
|
placeholderText: qsTr("Search applications...")
|
|
|
|
onTextChanged: {
|
|
root.searchText = text;
|
|
}
|
|
}
|
|
|
|
MaterialIcon {
|
|
id: clearIcon
|
|
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: Appearance.padding.normal
|
|
|
|
width: searchField.text ? implicitWidth : implicitWidth / 2
|
|
opacity: {
|
|
if (!searchField.text)
|
|
return 0;
|
|
if (clearMouse.pressed)
|
|
return 0.7;
|
|
if (clearMouse.containsMouse)
|
|
return 0.8;
|
|
return 1;
|
|
}
|
|
|
|
text: "close"
|
|
color: Colours.palette.m3onSurfaceVariant
|
|
|
|
MouseArea {
|
|
id: clearMouse
|
|
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: searchField.text ? Qt.PointingHandCursor : undefined
|
|
|
|
onClicked: searchField.text = ""
|
|
}
|
|
|
|
Behavior on width {
|
|
Anim {
|
|
duration: Appearance.anim.durations.small
|
|
}
|
|
}
|
|
|
|
Behavior on opacity {
|
|
Anim {
|
|
duration: Appearance.anim.durations.small
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Loader {
|
|
id: appsListLoader
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
asynchronous: true
|
|
active: true
|
|
|
|
sourceComponent: StyledListView {
|
|
id: appsListView
|
|
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
|
|
model: root.filteredApps
|
|
spacing: Appearance.spacing.small / 2
|
|
clip: true
|
|
|
|
StyledScrollBar.vertical: StyledScrollBar {
|
|
flickable: parent
|
|
}
|
|
|
|
delegate: StyledRect {
|
|
required property var modelData
|
|
|
|
width: parent ? parent.width : 0
|
|
implicitHeight: 40
|
|
|
|
readonly property bool isSelected: root.selectedApp === modelData
|
|
|
|
color: isSelected ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent"
|
|
radius: Appearance.rounding.normal
|
|
|
|
opacity: 0
|
|
|
|
Behavior on opacity {
|
|
NumberAnimation {
|
|
duration: 1000
|
|
easing.type: Easing.OutCubic
|
|
}
|
|
}
|
|
|
|
Component.onCompleted: {
|
|
opacity = 1;
|
|
}
|
|
|
|
StateLayer {
|
|
function onClicked(): void {
|
|
root.session.launcher.active = modelData;
|
|
}
|
|
}
|
|
|
|
RowLayout {
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
anchors.margins: Appearance.padding.normal
|
|
|
|
spacing: Appearance.spacing.normal
|
|
|
|
IconImage {
|
|
Layout.alignment: Qt.AlignVCenter
|
|
implicitSize: 32
|
|
source: {
|
|
const entry = modelData.entry;
|
|
return entry ? Quickshell.iconPath(entry.icon, "image-missing") : "image-missing";
|
|
}
|
|
}
|
|
|
|
StyledText {
|
|
Layout.fillWidth: true
|
|
text: modelData.name || modelData.entry?.name || qsTr("Unknown")
|
|
font.pointSize: Appearance.font.size.normal
|
|
}
|
|
|
|
Loader {
|
|
Layout.alignment: Qt.AlignVCenter
|
|
readonly property bool isHidden: modelData ? Strings.testRegexList(Config.launcher.hiddenApps, modelData.id) : false
|
|
readonly property bool isFav: modelData ? Strings.testRegexList(Config.launcher.favouriteApps, modelData.id) : false
|
|
active: isHidden || isFav
|
|
|
|
sourceComponent: isHidden ? hiddenIcon : (isFav ? favouriteIcon : null)
|
|
}
|
|
|
|
Component {
|
|
id: hiddenIcon
|
|
MaterialIcon {
|
|
text: "visibility_off"
|
|
fill: 1
|
|
color: Colours.palette.m3primary
|
|
}
|
|
}
|
|
|
|
Component {
|
|
id: favouriteIcon
|
|
MaterialIcon {
|
|
text: "favorite"
|
|
fill: 1
|
|
color: Colours.palette.m3primary
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
rightContent: Component {
|
|
Item {
|
|
id: rightLauncherPane
|
|
|
|
property var pane: root.session.launcher.active
|
|
property string paneId: pane ? (pane.id || pane.entry?.id || "") : ""
|
|
property Component targetComponent: settings
|
|
property Component nextComponent: settings
|
|
property var displayedApp: null
|
|
|
|
function getComponentForPane() {
|
|
return pane ? appDetails : settings;
|
|
}
|
|
|
|
Component.onCompleted: {
|
|
displayedApp = pane;
|
|
targetComponent = getComponentForPane();
|
|
nextComponent = targetComponent;
|
|
}
|
|
|
|
Loader {
|
|
id: rightLauncherLoader
|
|
|
|
anchors.fill: parent
|
|
|
|
opacity: 1
|
|
scale: 1
|
|
transformOrigin: Item.Center
|
|
clip: false
|
|
|
|
sourceComponent: rightLauncherPane.targetComponent
|
|
active: true
|
|
|
|
property var displayedApp: rightLauncherPane.displayedApp
|
|
|
|
onItemChanged: {
|
|
if (item && rightLauncherPane.pane && rightLauncherPane.displayedApp !== rightLauncherPane.pane) {
|
|
rightLauncherPane.displayedApp = rightLauncherPane.pane;
|
|
}
|
|
}
|
|
}
|
|
|
|
Behavior on paneId {
|
|
PaneTransition {
|
|
target: rightLauncherLoader
|
|
propertyActions: [
|
|
PropertyAction {
|
|
target: rightLauncherPane
|
|
property: "displayedApp"
|
|
value: rightLauncherPane.pane
|
|
},
|
|
PropertyAction {
|
|
target: rightLauncherLoader
|
|
property: "active"
|
|
value: false
|
|
},
|
|
PropertyAction {
|
|
target: rightLauncherPane
|
|
property: "targetComponent"
|
|
value: rightLauncherPane.nextComponent
|
|
},
|
|
PropertyAction {
|
|
target: rightLauncherLoader
|
|
property: "active"
|
|
value: true
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
onPaneChanged: {
|
|
nextComponent = getComponentForPane();
|
|
paneId = pane ? (pane.id || pane.entry?.id || "") : "";
|
|
}
|
|
|
|
onDisplayedAppChanged: {
|
|
if (displayedApp) {
|
|
const appId = displayedApp.id || displayedApp.entry?.id;
|
|
root.hideFromLauncherChecked = Config.launcher.hiddenApps && Config.launcher.hiddenApps.length > 0 && Strings.testRegexList(Config.launcher.hiddenApps, appId);
|
|
root.favouriteChecked = Config.launcher.favouriteApps && Config.launcher.favouriteApps.length > 0 && Strings.testRegexList(Config.launcher.favouriteApps, appId);
|
|
} else {
|
|
root.hideFromLauncherChecked = false;
|
|
root.favouriteChecked = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Component {
|
|
id: settings
|
|
|
|
StyledFlickable {
|
|
id: settingsFlickable
|
|
flickableDirection: Flickable.VerticalFlick
|
|
contentHeight: settingsInner.height
|
|
|
|
StyledScrollBar.vertical: StyledScrollBar {
|
|
flickable: settingsFlickable
|
|
}
|
|
|
|
Settings {
|
|
id: settingsInner
|
|
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.top: parent.top
|
|
session: root.session
|
|
}
|
|
}
|
|
}
|
|
|
|
Component {
|
|
id: appDetails
|
|
|
|
ColumnLayout {
|
|
id: appDetailsLayout
|
|
anchors.fill: parent
|
|
|
|
readonly property var displayedApp: parent && parent.displayedApp !== undefined ? parent.displayedApp : null
|
|
|
|
spacing: Appearance.spacing.normal
|
|
|
|
SettingsHeader {
|
|
Layout.leftMargin: Appearance.padding.large * 2
|
|
Layout.rightMargin: Appearance.padding.large * 2
|
|
Layout.topMargin: Appearance.padding.large * 2
|
|
visible: displayedApp === null
|
|
icon: "apps"
|
|
title: qsTr("Launcher Applications")
|
|
}
|
|
|
|
Item {
|
|
Layout.alignment: Qt.AlignHCenter
|
|
Layout.leftMargin: Appearance.padding.large * 2
|
|
Layout.rightMargin: Appearance.padding.large * 2
|
|
Layout.topMargin: Appearance.padding.large * 2
|
|
visible: displayedApp !== null
|
|
implicitWidth: Math.max(appIconImage.implicitWidth, appTitleText.implicitWidth)
|
|
implicitHeight: appIconImage.implicitHeight + Appearance.spacing.normal + appTitleText.implicitHeight
|
|
|
|
ColumnLayout {
|
|
anchors.centerIn: parent
|
|
spacing: Appearance.spacing.normal
|
|
|
|
IconImage {
|
|
id: appIconImage
|
|
Layout.alignment: Qt.AlignHCenter
|
|
implicitSize: Appearance.font.size.extraLarge * 3 * 2
|
|
source: {
|
|
const app = appDetailsLayout.displayedApp;
|
|
if (!app)
|
|
return "image-missing";
|
|
const entry = app.entry;
|
|
if (entry && entry.icon) {
|
|
return Quickshell.iconPath(entry.icon, "image-missing");
|
|
}
|
|
return "image-missing";
|
|
}
|
|
}
|
|
|
|
StyledText {
|
|
id: appTitleText
|
|
Layout.alignment: Qt.AlignHCenter
|
|
text: displayedApp ? (displayedApp.name || displayedApp.entry?.name || qsTr("Application Details")) : ""
|
|
font.pointSize: Appearance.font.size.large
|
|
font.bold: true
|
|
}
|
|
}
|
|
}
|
|
|
|
Item {
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
Layout.topMargin: Appearance.spacing.large
|
|
Layout.leftMargin: Appearance.padding.large * 2
|
|
Layout.rightMargin: Appearance.padding.large * 2
|
|
|
|
StyledFlickable {
|
|
id: detailsFlickable
|
|
anchors.fill: parent
|
|
flickableDirection: Flickable.VerticalFlick
|
|
contentHeight: debugLayout.height
|
|
|
|
StyledScrollBar.vertical: StyledScrollBar {
|
|
flickable: parent
|
|
}
|
|
|
|
ColumnLayout {
|
|
id: debugLayout
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.top: parent.top
|
|
spacing: Appearance.spacing.normal
|
|
|
|
SwitchRow {
|
|
Layout.topMargin: Appearance.spacing.normal
|
|
visible: appDetailsLayout.displayedApp !== null
|
|
label: qsTr("Mark as favourite")
|
|
checked: root.favouriteChecked
|
|
// disabled if:
|
|
// * app is hidden
|
|
// * app isn't in favouriteApps array but marked as favourite anyway
|
|
// ^^^ This means that this app is favourited because of a regex check
|
|
// this button can not toggle regexed apps
|
|
enabled: appDetailsLayout.displayedApp !== null && !root.hideFromLauncherChecked && (Config.launcher.favouriteApps.indexOf(appDetailsLayout.displayedApp.id || appDetailsLayout.displayedApp.entry?.id) !== -1 || !root.favouriteChecked)
|
|
opacity: enabled ? 1 : 0.6
|
|
onToggled: checked => {
|
|
root.favouriteChecked = checked;
|
|
const app = appDetailsLayout.displayedApp;
|
|
if (app) {
|
|
const appId = app.id || app.entry?.id;
|
|
const favouriteApps = Config.launcher.favouriteApps ? [...Config.launcher.favouriteApps] : [];
|
|
if (checked) {
|
|
if (!favouriteApps.includes(appId)) {
|
|
favouriteApps.push(appId);
|
|
}
|
|
} else {
|
|
const index = favouriteApps.indexOf(appId);
|
|
if (index !== -1) {
|
|
favouriteApps.splice(index, 1);
|
|
}
|
|
}
|
|
Config.launcher.favouriteApps = favouriteApps;
|
|
Config.save();
|
|
}
|
|
}
|
|
}
|
|
SwitchRow {
|
|
Layout.topMargin: Appearance.spacing.normal
|
|
visible: appDetailsLayout.displayedApp !== null
|
|
label: qsTr("Hide from launcher")
|
|
checked: root.hideFromLauncherChecked
|
|
// disabled if:
|
|
// * app is favourited
|
|
// * app isn't in hiddenApps array but marked as hidden anyway
|
|
// ^^^ This means that this app is hidden because of a regex check
|
|
// this button can not toggle regexed apps
|
|
enabled: appDetailsLayout.displayedApp !== null && !root.favouriteChecked && (Config.launcher.hiddenApps.indexOf(appDetailsLayout.displayedApp.id || appDetailsLayout.displayedApp.entry?.id) !== -1 || !root.hideFromLauncherChecked)
|
|
opacity: enabled ? 1 : 0.6
|
|
onToggled: checked => {
|
|
root.hideFromLauncherChecked = checked;
|
|
const app = appDetailsLayout.displayedApp;
|
|
if (app) {
|
|
const appId = app.id || app.entry?.id;
|
|
const hiddenApps = Config.launcher.hiddenApps ? [...Config.launcher.hiddenApps] : [];
|
|
if (checked) {
|
|
if (!hiddenApps.includes(appId)) {
|
|
hiddenApps.push(appId);
|
|
}
|
|
} else {
|
|
const index = hiddenApps.indexOf(appId);
|
|
if (index !== -1) {
|
|
hiddenApps.splice(index, 1);
|
|
}
|
|
}
|
|
Config.launcher.hiddenApps = hiddenApps;
|
|
Config.save();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|