Merge remote-tracking branch 'origin/main' into addon-i18n

This commit is contained in:
月月
2025-07-12 21:03:24 +08:00
254 changed files with 2744 additions and 955 deletions
@@ -0,0 +1,238 @@
import "root:/modules/common"
import "root:/modules/common/widgets"
import "root:/services"
import "./calendar"
import "./todo"
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
Rectangle {
id: root
radius: Appearance.rounding.normal
color: Appearance.colors.colLayer1
clip: true
implicitHeight: collapsed ? collapsedBottomWidgetGroupRow.implicitHeight : bottomWidgetGroupRow.implicitHeight
property int selectedTab: 0
property bool collapsed: Persistent.states.sidebar.bottomGroup.collapsed
property var tabs: [
{"type": "calendar", "name": "Calendar", "icon": "calendar_month", "widget": calendarWidget},
{"type": "todo", "name": "To Do", "icon": "done_outline", "widget": todoWidget}
]
Behavior on implicitHeight {
NumberAnimation {
duration: Appearance.animation.elementMove.duration
easing.type: Appearance.animation.elementMove.type
easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
}
}
function setCollapsed(state) {
Persistent.states.sidebar.bottomGroup.collapsed = state
if (collapsed) {
bottomWidgetGroupRow.opacity = 0
}
else {
collapsedBottomWidgetGroupRow.opacity = 0
}
collapseCleanFadeTimer.start()
}
Timer {
id: collapseCleanFadeTimer
interval: Appearance.animation.elementMove.duration / 2
repeat: false
onTriggered: {
if(collapsed) collapsedBottomWidgetGroupRow.opacity = 1
else bottomWidgetGroupRow.opacity = 1
}
}
Keys.onPressed: (event) => {
if ((event.key === Qt.Key_PageDown || event.key === Qt.Key_PageUp)
&& event.modifiers === Qt.ControlModifier) {
if (event.key === Qt.Key_PageDown) {
root.selectedTab = Math.min(root.selectedTab + 1, root.tabs.length - 1)
} else if (event.key === Qt.Key_PageUp) {
root.selectedTab = Math.max(root.selectedTab - 1, 0)
}
event.accepted = true;
}
}
// The thing when collapsed
RowLayout {
id: collapsedBottomWidgetGroupRow
opacity: collapsed ? 1 : 0
visible: opacity > 0
Behavior on opacity {
NumberAnimation {
id: collapsedBottomWidgetGroupRowFade
duration: Appearance.animation.elementMove.duration / 2
easing.type: Appearance.animation.elementMove.type
easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
}
}
spacing: 15
CalendarHeaderButton {
Layout.margins: 10
Layout.rightMargin: 0
forceCircle: true
onClicked: {
root.setCollapsed(false)
}
contentItem: MaterialSymbol {
text: "keyboard_arrow_up"
iconSize: Appearance.font.pixelSize.larger
horizontalAlignment: Text.AlignHCenter
color: Appearance.colors.colOnLayer1
}
}
StyledText {
property int remainingTasks: Todo.list.filter(task => !task.done).length;
Layout.margins: 10
Layout.leftMargin: 0
text: `${DateTime.collapsedCalendarFormat} ${remainingTasks} task${remainingTasks > 1 ? "s" : ""}`
font.pixelSize: Appearance.font.pixelSize.large
color: Appearance.colors.colOnLayer1
}
}
// The thing when expanded
RowLayout {
id: bottomWidgetGroupRow
opacity: collapsed ? 0 : 1
visible: opacity > 0
Behavior on opacity {
NumberAnimation {
id: bottomWidgetGroupRowFade
duration: Appearance.animation.elementMove.duration / 2
easing.type: Appearance.animation.elementMove.type
easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
}
}
anchors.fill: parent
height: tabStack.height
spacing: 10
// Navigation rail
Item {
Layout.fillHeight: true
Layout.fillWidth: false
Layout.leftMargin: 10
Layout.topMargin: 10
width: tabBar.width
// Navigation rail buttons
NavigationRailTabArray {
id: tabBar
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 5
currentIndex: root.selectedTab
expanded: false
Repeater {
model: root.tabs
NavigationRailButton {
showToggledHighlight: false
toggled: root.selectedTab == index
buttonText: modelData.name
buttonIcon: modelData.icon
onClicked: {
root.selectedTab = index
}
}
}
}
// Collapse button
CalendarHeaderButton {
anchors.left: parent.left
anchors.top: parent.top
forceCircle: true
onClicked: {
root.setCollapsed(true)
}
contentItem: MaterialSymbol {
text: "keyboard_arrow_down"
iconSize: Appearance.font.pixelSize.larger
horizontalAlignment: Text.AlignHCenter
color: Appearance.colors.colOnLayer1
}
}
}
// Content area
StackLayout {
id: tabStack
Layout.fillWidth: true
height: tabStack.children[0]?.tabLoader?.implicitHeight // TODO: make this less stupid
Layout.alignment: Qt.AlignVCenter
property int realIndex: 0
property int animationDuration: Appearance.animation.elementMoveFast.duration * 1.5
// Switch the tab on halfway of the anim duration
Connections {
target: root
function onSelectedTabChanged() {
delayedStackSwitch.start()
tabStack.realIndex = root.selectedTab
}
}
Timer {
id: delayedStackSwitch
interval: tabStack.animationDuration / 2
repeat: false
onTriggered: {
tabStack.currentIndex = root.selectedTab
}
}
Repeater {
model: tabs
Item { // TODO: make behavior on y also act for the item that's switched to
id: tabItem
property int tabIndex: index
property string tabType: modelData.type
property int animDistance: 5
property var tabLoader: tabLoader
// Opacity: show up only when being animated to
opacity: (tabStack.currentIndex === tabItem.tabIndex && tabStack.realIndex === tabItem.tabIndex) ? 1 : 0
// Y: starts animating when user selects a different tab
y: (tabStack.realIndex === tabItem.tabIndex) ? 0 : (tabStack.realIndex < tabItem.tabIndex) ? animDistance : -animDistance
Behavior on opacity { NumberAnimation { duration: tabStack.animationDuration / 2; easing.type: Easing.OutCubic } }
Behavior on y { NumberAnimation { duration: tabStack.animationDuration; easing.type: Easing.OutExpo } }
Loader {
id: tabLoader
anchors.fill: parent
sourceComponent: modelData.widget
focus: root.selectedTab === tabItem.tabIndex
}
}
}
}
}
// Calendar component
Component {
id: calendarWidget
CalendarWidget {
anchors.centerIn: parent
}
}
// To Do component
Component {
id: todoWidget
TodoWidget {
anchors.fill: parent
anchors.margins: 5
}
}
}
@@ -0,0 +1,83 @@
import "root:/modules/common"
import "root:/modules/common/widgets"
import "root:/services"
import "./calendar"
import "./notifications"
import "./todo"
import "./volumeMixer"
import "root:/services/"
import Qt5Compat.GraphicalEffects
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
Rectangle {
id: root
radius: Appearance.rounding.normal
color: Appearance.colors.colLayer1
property int selectedTab: 0
property var tabButtonList: [{"icon": "notifications", "name": Translation.tr("Notifications")}, {"icon": "volume_up", "name": Translation.tr("Volume mixer")}]
Keys.onPressed: (event) => {
if (event.key === Qt.Key_PageDown || event.key === Qt.Key_PageUp) {
if (event.key === Qt.Key_PageDown) {
root.selectedTab = Math.min(root.selectedTab + 1, root.tabButtonList.length - 1)
} else if (event.key === Qt.Key_PageUp) {
root.selectedTab = Math.max(root.selectedTab - 1, 0)
}
event.accepted = true;
}
if (event.modifiers === Qt.ControlModifier) {
if (event.key === Qt.Key_Tab) {
root.selectedTab = (root.selectedTab + 1) % root.tabButtonList.length
} else if (event.key === Qt.Key_Backtab) {
root.selectedTab = (root.selectedTab - 1 + root.tabButtonList.length) % root.tabButtonList.length
}
event.accepted = true;
}
}
ColumnLayout {
anchors.margins: 5
anchors.fill: parent
spacing: 0
PrimaryTabBar {
id: tabBar
tabButtonList: root.tabButtonList
externalTrackedTab: root.selectedTab
function onCurrentIndexChanged(currentIndex) {
root.selectedTab = currentIndex
}
}
SwipeView {
id: swipeView
Layout.topMargin: 5
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 10
currentIndex: root.selectedTab
onCurrentIndexChanged: {
tabBar.enableIndicatorAnimation = true
root.selectedTab = currentIndex
}
clip: true
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: swipeView.width
height: swipeView.height
radius: Appearance.rounding.small
}
}
NotificationList {}
VolumeMixer {}
}
}
}
@@ -0,0 +1,253 @@
import "root:/"
import "root:/services"
import "root:/modules/common"
import "root:/modules/common/widgets"
import "root:/modules/common/functions/string_utils.js" as StringUtils
import "root:/modules/common/functions/file_utils.js" as FileUtils
import "./quickToggles/"
import "root:/services/"
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Effects
import Qt5Compat.GraphicalEffects
import Quickshell.Io
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Hyprland
Scope {
id: root
property int sidebarWidth: Appearance.sizes.sidebarWidth
property int sidebarPadding: 15
property string settingsQmlPath: Quickshell.configPath("settings.qml")
PanelWindow {
id: sidebarRoot
visible: GlobalStates.sidebarRightOpen
function hide() {
GlobalStates.sidebarRightOpen = false
}
exclusiveZone: 0
implicitWidth: sidebarWidth
WlrLayershell.namespace: "quickshell:sidebarRight"
// Hyprland 0.49: Focus is always exclusive and setting this breaks mouse focus grab
// WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
color: "transparent"
anchors {
top: true
right: true
bottom: true
}
HyprlandFocusGrab {
id: grab
windows: [ sidebarRoot ]
active: GlobalStates.sidebarRightOpen
onCleared: () => {
if (!active) sidebarRoot.hide()
}
}
Loader {
id: sidebarContentLoader
active: GlobalStates.sidebarRightOpen
anchors {
top: parent.top
bottom: parent.bottom
right: parent.right
left: parent.left
topMargin: Appearance.sizes.hyprlandGapsOut
rightMargin: Appearance.sizes.hyprlandGapsOut
bottomMargin: Appearance.sizes.hyprlandGapsOut
leftMargin: Appearance.sizes.elevationMargin
}
width: sidebarWidth - Appearance.sizes.hyprlandGapsOut - Appearance.sizes.elevationMargin
height: parent.height - Appearance.sizes.hyprlandGapsOut * 2
focus: GlobalStates.sidebarRightOpen
Keys.onPressed: (event) => {
if (event.key === Qt.Key_Escape) {
sidebarRoot.hide();
}
}
sourceComponent: Item {
implicitHeight: sidebarRightBackground.implicitHeight
implicitWidth: sidebarRightBackground.implicitWidth
StyledRectangularShadow {
target: sidebarRightBackground
}
Rectangle {
id: sidebarRightBackground
anchors.fill: parent
implicitHeight: parent.height - Appearance.sizes.hyprlandGapsOut * 2
implicitWidth: sidebarWidth - Appearance.sizes.hyprlandGapsOut * 2
color: Appearance.colors.colLayer0
border.width: 1
border.color: Appearance.m3colors.m3outlineVariant
radius: Appearance.rounding.screenRounding - Appearance.sizes.hyprlandGapsOut + 1
ColumnLayout {
spacing: sidebarPadding
anchors.fill: parent
anchors.margins: sidebarPadding
RowLayout {
Layout.fillHeight: false
spacing: 10
Layout.margins: 10
Layout.topMargin: 5
Layout.bottomMargin: 0
Item {
implicitWidth: distroIcon.width
implicitHeight: distroIcon.height
CustomIcon {
id: distroIcon
width: 25
height: 25
source: SystemInfo.distroIcon
}
ColorOverlay {
anchors.fill: distroIcon
source: distroIcon
color: Appearance.colors.colOnLayer0
}
}
StyledText {
font.pixelSize: Appearance.font.pixelSize.normal
color: Appearance.colors.colOnLayer0
text: StringUtils.format(Translation.tr("Uptime: {0}"), DateTime.uptime)
textFormat: Text.MarkdownText
}
Item {
Layout.fillWidth: true
}
ButtonGroup {
QuickToggleButton {
toggled: false
buttonIcon: "restart_alt"
onClicked: {
Hyprland.dispatch("reload")
Quickshell.reload(true)
}
StyledToolTip {
content: Translation.tr("Reload Hyprland & Quickshell")
}
}
QuickToggleButton {
toggled: false
buttonIcon: "settings"
onClicked: {
Hyprland.dispatch("global quickshell:sidebarRightClose")
Quickshell.execDetached(["qs", "-p", root.settingsQmlPath])
}
StyledToolTip {
content: Translation.tr("Settings")
}
}
QuickToggleButton {
toggled: false
buttonIcon: "power_settings_new"
onClicked: {
Hyprland.dispatch("global quickshell:sessionOpen")
}
StyledToolTip {
content: Translation.tr("Session")
}
}
}
}
ButtonGroup {
Layout.alignment: Qt.AlignHCenter
spacing: 5
padding: 5
color: Appearance.colors.colLayer1
NetworkToggle {}
BluetoothToggle {}
NightLight {}
GameMode {}
IdleInhibitor {}
CloudflareWarp {}
}
// Center widget group
CenterWidgetGroup {
focus: sidebarRoot.visible
Layout.alignment: Qt.AlignHCenter
Layout.fillHeight: true
Layout.fillWidth: true
}
BottomWidgetGroup {
Layout.alignment: Qt.AlignHCenter
Layout.fillHeight: false
Layout.fillWidth: true
Layout.preferredHeight: implicitHeight
}
}
}
}
}
}
IpcHandler {
target: "sidebarRight"
function toggle(): void {
GlobalStates.sidebarRightOpen = !GlobalStates.sidebarRightOpen;
if(GlobalStates.sidebarRightOpen) Notifications.timeoutAll();
}
function close(): void {
GlobalStates.sidebarRightOpen = false;
}
function open(): void {
GlobalStates.sidebarRightOpen = true;
Notifications.timeoutAll();
}
}
GlobalShortcut {
name: "sidebarRightToggle"
description: Translation.tr("Toggles right sidebar on press")
onPressed: {
GlobalStates.sidebarRightOpen = !GlobalStates.sidebarRightOpen;
if(GlobalStates.sidebarRightOpen) Notifications.timeoutAll();
}
}
GlobalShortcut {
name: "sidebarRightOpen"
description: Translation.tr("Opens right sidebar on press")
onPressed: {
GlobalStates.sidebarRightOpen = true;
Notifications.timeoutAll();
}
}
GlobalShortcut {
name: "sidebarRightClose"
description: Translation.tr("Closes right sidebar on press")
onPressed: {
GlobalStates.sidebarRightOpen = false;
}
}
}
@@ -0,0 +1,36 @@
import "root:/modules/common"
import "root:/modules/common/widgets"
import "root:/modules/common/functions/color_utils.js" as ColorUtils
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
RippleButton {
id: button
property string day
property int isToday
property bool bold
Layout.fillWidth: false
Layout.fillHeight: false
implicitWidth: 38;
implicitHeight: 38;
toggled: (isToday == 1)
buttonRadius: Appearance.rounding.small
contentItem: StyledText {
anchors.fill: parent
text: day
horizontalAlignment: Text.AlignHCenter
font.weight: bold ? Font.DemiBold : Font.Normal
color: (isToday == 1) ? Appearance.m3colors.m3onPrimary :
(isToday == 0) ? Appearance.colors.colOnLayer1 :
Appearance.colors.colOutlineVariant
Behavior on color {
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
}
}
}
@@ -0,0 +1,38 @@
import "root:/modules/common"
import "root:/modules/common/widgets"
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
RippleButton {
id: button
property string buttonText: ""
property string tooltipText: ""
property bool forceCircle: false
implicitHeight: 30
implicitWidth: forceCircle ? implicitHeight : (contentItem.implicitWidth + 10 * 2)
Behavior on implicitWidth {
SmoothedAnimation {
velocity: Appearance.animation.elementMove.velocity
}
}
background.anchors.fill: button
buttonRadius: Appearance.rounding.full
colBackground: Appearance.colors.colLayer2
colBackgroundHover: Appearance.colors.colLayer2Hover
colRipple: Appearance.colors.colLayer2Active
contentItem: StyledText {
text: buttonText
horizontalAlignment: Text.AlignHCenter
font.pixelSize: Appearance.font.pixelSize.larger
color: Appearance.colors.colOnLayer1
}
StyledToolTip {
content: tooltipText
extraVisibleCondition: tooltipText.length > 0
}
}
@@ -0,0 +1,122 @@
import "root:/modules/common"
import "root:/modules/common/widgets"
import "./calendar_layout.js" as CalendarLayout
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Item {
// Layout.topMargin: 10
anchors.topMargin: 10
property int monthShift: 0
property var viewingDate: CalendarLayout.getDateInXMonthsTime(monthShift)
property var calendarLayout: CalendarLayout.getCalendarLayout(viewingDate, monthShift === 0)
width: calendarColumn.width
implicitHeight: calendarColumn.height + 10 * 2
Keys.onPressed: (event) => {
if ((event.key === Qt.Key_PageDown || event.key === Qt.Key_PageUp)
&& event.modifiers === Qt.NoModifier) {
if (event.key === Qt.Key_PageDown) {
monthShift++;
} else if (event.key === Qt.Key_PageUp) {
monthShift--;
}
event.accepted = true;
}
}
MouseArea {
anchors.fill: parent
onWheel: (event) => {
if (event.angleDelta.y > 0) {
monthShift--;
} else if (event.angleDelta.y < 0) {
monthShift++;
}
}
}
ColumnLayout {
id: calendarColumn
anchors.centerIn: parent
spacing: 5
// Calendar header
RowLayout {
Layout.fillWidth: true
spacing: 5
CalendarHeaderButton {
clip: true
buttonText: `${monthShift != 0 ? "• " : ""}${viewingDate.toLocaleDateString(Qt.locale(), "MMMM yyyy")}`
tooltipText: (monthShift === 0) ? "" : Translation.tr("Jump to current month")
onClicked: {
monthShift = 0;
}
}
Item {
Layout.fillWidth: true
Layout.fillHeight: false
}
CalendarHeaderButton {
forceCircle: true
onClicked: {
monthShift--;
}
contentItem: MaterialSymbol {
text: "chevron_left"
iconSize: Appearance.font.pixelSize.larger
horizontalAlignment: Text.AlignHCenter
color: Appearance.colors.colOnLayer1
}
}
CalendarHeaderButton {
forceCircle: true
onClicked: {
monthShift++;
}
contentItem: MaterialSymbol {
text: "chevron_right"
iconSize: Appearance.font.pixelSize.larger
horizontalAlignment: Text.AlignHCenter
color: Appearance.colors.colOnLayer1
}
}
}
// Week days row
RowLayout {
id: weekDaysRow
Layout.alignment: Qt.AlignHCenter
Layout.fillHeight: false
spacing: 5
Repeater {
model: CalendarLayout.weekDays
delegate: CalendarDayButton {
day: modelData.day
isToday: modelData.today
bold: true
enabled: false
}
}
}
// Real week rows
Repeater {
id: calendarRows
// model: calendarLayout
model: 6
delegate: RowLayout {
Layout.alignment: Qt.AlignHCenter
Layout.fillHeight: false
spacing: 5
Repeater {
model: Array(7).fill(modelData)
delegate: CalendarDayButton {
day: calendarLayout[modelData][index].day
isToday: calendarLayout[modelData][index].today
}
}
}
}
}
}
@@ -0,0 +1,115 @@
const weekDays = [ // MONDAY IS THE FIRST DAY OF THE WEEK :HESRIGHTYOUKNOW:
{ day: 'Mo', today: 0 },
{ day: 'Tu', today: 0 },
{ day: 'We', today: 0 },
{ day: 'Th', today: 0 },
{ day: 'Fr', today: 0 },
{ day: 'Sa', today: 0 },
{ day: 'Su', today: 0 },
]
function checkLeapYear(year) {
return (
year % 400 == 0 ||
(year % 4 == 0 && year % 100 != 0));
}
function getMonthDays(month, year) {
const leapYear = checkLeapYear(year);
if ((month <= 7 && month % 2 == 1) || (month >= 8 && month % 2 == 0)) return 31;
if (month == 2 && leapYear) return 29;
if (month == 2 && !leapYear) return 28;
return 30;
}
function getNextMonthDays(month, year) {
const leapYear = checkLeapYear(year);
if (month == 1 && leapYear) return 29;
if (month == 1 && !leapYear) return 28;
if (month == 12) return 31;
if ((month <= 7 && month % 2 == 1) || (month >= 8 && month % 2 == 0)) return 30;
return 31;
}
function getPrevMonthDays(month, year) {
const leapYear = checkLeapYear(year);
if (month == 3 && leapYear) return 29;
if (month == 3 && !leapYear) return 28;
if (month == 1) return 31;
if ((month <= 7 && month % 2 == 1) || (month >= 8 && month % 2 == 0)) return 30;
return 31;
}
function getDateInXMonthsTime(x) {
var currentDate = new Date(); // Get the current date
if (x == 0) return currentDate; // If x is 0, return the current date
var targetMonth = currentDate.getMonth() + x; // Calculate the target month
var targetYear = currentDate.getFullYear(); // Get the current year
// Adjust the year and month if necessary
targetYear += Math.floor(targetMonth / 12);
targetMonth = (targetMonth % 12 + 12) % 12;
// Create a new date object with the target year and month
var targetDate = new Date(targetYear, targetMonth, 1);
// Set the day to the last day of the month to get the desired date
// targetDate.setDate(0);
return targetDate;
}
function getCalendarLayout(dateObject, highlight) {
if (!dateObject) dateObject = new Date();
const weekday = (dateObject.getDay() + 6) % 7; // MONDAY IS THE FIRST DAY OF THE WEEK
const day = dateObject.getDate();
const month = dateObject.getMonth() + 1;
const year = dateObject.getFullYear();
const weekdayOfMonthFirst = (weekday + 35 - (day - 1)) % 7;
const daysInMonth = getMonthDays(month, year);
const daysInNextMonth = getNextMonthDays(month, year);
const daysInPrevMonth = getPrevMonthDays(month, year);
// Fill
var monthDiff = (weekdayOfMonthFirst == 0 ? 0 : -1);
var toFill, dim;
if(weekdayOfMonthFirst == 0) {
toFill = 1;
dim = daysInMonth;
}
else {
toFill = (daysInPrevMonth - (weekdayOfMonthFirst - 1));
dim = daysInPrevMonth;
}
var calendar = [...Array(6)].map(() => Array(7));
var i = 0, j = 0;
while (i < 6 && j < 7) {
calendar[i][j] = {
"day": toFill,
"today": ((toFill == day && monthDiff == 0 && highlight) ? 1 : (
monthDiff == 0 ? 0 :
-1
))
};
// Increment
toFill++;
if (toFill > dim) { // Next month?
monthDiff++;
if (monthDiff == 0)
dim = daysInMonth;
else if (monthDiff == 1)
dim = daysInNextMonth;
toFill = 1;
}
// Next tile
j++;
if (j == 7) {
j = 0;
i++;
}
}
return calendar;
}
@@ -0,0 +1,120 @@
import "root:/modules/common"
import "root:/modules/common/widgets"
import "root:/services"
import "root:/services/"
import Qt5Compat.GraphicalEffects
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
Item {
id: root
NotificationListView { // Scrollable window
id: listview
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: statusRow.top
anchors.bottomMargin: 5
clip: true
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: listview.width
height: listview.height
radius: Appearance.rounding.normal
}
}
popup: false
}
// Placeholder when list is empty
Item {
anchors.fill: listview
visible: opacity > 0
opacity: (Notifications.list.length === 0) ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: Appearance.animation.menuDecel.duration
easing.type: Appearance.animation.menuDecel.type
}
}
ColumnLayout {
anchors.centerIn: parent
spacing: 5
MaterialSymbol {
Layout.alignment: Qt.AlignHCenter
iconSize: 55
color: Appearance.m3colors.m3outline
text: "notifications_active"
}
StyledText {
Layout.alignment: Qt.AlignHCenter
font.pixelSize: Appearance.font.pixelSize.normal
color: Appearance.m3colors.m3outline
horizontalAlignment: Text.AlignHCenter
text: Translation.tr("No notifications")
}
}
}
Item {
id: statusRow
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
Layout.fillWidth: true
implicitHeight: Math.max(
controls.implicitHeight,
statusText.implicitHeight
)
StyledText {
id: statusText
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 10
horizontalAlignment: Text.AlignHCenter
text: `${Notifications.list.length} notifications`
opacity: Notifications.list.length > 0 ? 1 : 0
visible: opacity > 0
Behavior on opacity {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
}
ButtonGroup {
id: controls
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.rightMargin: 5
NotificationStatusButton {
buttonIcon: "notifications_paused"
buttonText: Translation.tr("Silent")
toggled: Notifications.silent
onClicked: () => {
Notifications.silent = !Notifications.silent;
}
}
NotificationStatusButton {
buttonIcon: "clear_all"
buttonText: Translation.tr("Clear")
onClicked: () => {
Notifications.discardAllNotifications()
}
}
}
}
}
@@ -0,0 +1,44 @@
import "root:/modules/common"
import "root:/modules/common/widgets"
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
GroupButton {
id: button
property string buttonText: ""
property string buttonIcon: ""
baseWidth: content.implicitWidth + 10 * 2
baseHeight: 30
buttonRadius: baseHeight / 2
buttonRadiusPressed: Appearance.rounding.small
colBackground: Appearance.colors.colLayer2
colBackgroundHover: Appearance.colors.colLayer2Hover
colBackgroundActive: Appearance.colors.colLayer2Active
property color colText: toggled ? Appearance.m3colors.m3onPrimary : Appearance.colors.colOnLayer1
contentItem: Item {
id: content
anchors.fill: parent
implicitWidth: contentRowLayout.implicitWidth
implicitHeight: contentRowLayout.implicitHeight
RowLayout {
id: contentRowLayout
anchors.centerIn: parent
spacing: 5
MaterialSymbol {
text: buttonIcon
iconSize: Appearance.font.pixelSize.large
color: button.colText
}
StyledText {
text: buttonText
font.pixelSize: Appearance.font.pixelSize.small
color: button.colText
}
}
}
}
@@ -0,0 +1,36 @@
import "../"
import "root:/services"
import "root:/modules/common"
import "root:/modules/common/widgets"
import "root:/modules/common/functions/string_utils.js" as StringUtils
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Hyprland
QuickToggleButton {
toggled: Bluetooth.bluetoothEnabled
buttonIcon: Bluetooth.bluetoothConnected ? "bluetooth_connected" : Bluetooth.bluetoothEnabled ? "bluetooth" : "bluetooth_disabled"
onClicked: {
toggleBluetooth.running = true
}
altAction: () => {
Quickshell.execDetached(["bash", "-c", `${Config.options.apps.bluetooth}`])
Hyprland.dispatch("global quickshell:sidebarRightClose")
}
Process {
id: toggleBluetooth
command: ["bash", "-c", `bluetoothctl power ${Bluetooth.bluetoothEnabled ? "off" : "on"}`]
onRunningChanged: {
if(!running) {
Bluetooth.update()
}
}
}
StyledToolTip {
content: StringUtils.format(Translation.tr("{0} | Right-click to configure"),
(Bluetooth.bluetoothEnabled && Bluetooth.bluetoothDeviceName.length > 0) ?
Bluetooth.bluetoothDeviceName : Translation.tr("Bluetooth"))
}
}
@@ -0,0 +1,85 @@
import "root:/modules/common"
import "root:/modules/common/widgets"
import "../"
import QtQuick
import Quickshell.Io
import Quickshell
import Quickshell.Hyprland
QuickToggleButton {
id: root
toggled: false
visible: false
contentItem: CustomIcon {
id: distroIcon
source: 'cloudflare-dns-symbolic'
anchors.centerIn: parent
height: 16
colorize: true
color: root.toggled ? Appearance.m3colors.m3onPrimary : Appearance.colors.colOnLayer1
Behavior on color {
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
}
}
onClicked: {
if (toggled) {
root.toggled = false
Quickshell.execDetached(["warp-cli", "disconnect"])
} else {
root.toggled = true
Quickshell.execDetached(["warp-cli", "connect"])
}
}
Process {
id: connectProc
command: ["warp-cli", "connect"]
onExited: (exitCode, exitStatus) => {
if (exitCode !== 0) {
Quickshell.execDetached(["notify-send", "Cloudflare WARP", "Connection failed. Please inspect manually with the <tt>warp-cli</tt> command", "-a", "Shell"])
}
}
}
Process {
id: registrationProc
command: ["warp-cli", "registration", "new"]
onExited: (exitCode, exitStatus) => {
console.log("Warp registration exited with code and status:", exitCode, exitStatus)
if (exitCode === 0) {
connectProc.running = true
} else {
Quickshell.execDetached(["notify-send", "Cloudflare WARP", "Registration failed. Please inspect manually with the <tt>warp-cli</tt> command", "-a", "Shell"])
}
}
}
Process {
id: fetchActiveState
running: true
command: ["bash", "-c", "warp-cli status"]
stdout: StdioCollector {
id: warpStatusCollector
onStreamFinished: {
if (warpStatusCollector.text.length > 0) {
console.log("Showing warp")
root.visible = true
}
if (warpStatusCollector.text.includes("Unable")) {
registrationProc.running = true
} else if (warpStatusCollector.text.includes("Connected")) {
root.toggled = true
} else if (warpStatusCollector.text.includes("Disconnected")) {
root.toggled = false
}
}
}
}
StyledToolTip {
content: qsTr("Cloudflare WARP (1.1.1.1)")
}
}
@@ -0,0 +1,33 @@
import "root:/modules/common"
import "root:/modules/common/widgets"
import "../"
import "root:/services/"
import Quickshell
import Quickshell.Io
import Quickshell.Hyprland
QuickToggleButton {
id: root
buttonIcon: "gamepad"
toggled: toggled
onClicked: {
root.toggled = !root.toggled
if (root.toggled) {
Quickshell.execDetached(["bash", "-c", `hyprctl --batch "keyword animations:enabled 0; keyword decoration:shadow:enabled 0; keyword decoration:blur:enabled 0; keyword general:gaps_in 0; keyword general:gaps_out 0; keyword general:border_size 1; keyword decoration:rounding 0; keyword general:allow_tearing 1"`])
} else {
Quickshell.execDetached(["hyprctl", "reload"])
}
}
Process {
id: fetchActiveState
running: true
command: ["bash", "-c", `test "$(hyprctl getoption animations:enabled -j | jq ".int")" -ne 0`]
onExited: (exitCode, exitStatus) => {
root.toggled = exitCode !== 0 // Inverted because enabled = nonzero exit
}
}
StyledToolTip {
content: Translation.tr("Game mode")
}
}
@@ -0,0 +1,33 @@
import "root:/modules/common"
import "root:/modules/common/widgets"
import "../"
import "root:/services/"
import Quickshell.Io
import Quickshell
import Quickshell.Hyprland
QuickToggleButton {
id: root
toggled: false
buttonIcon: "coffee"
onClicked: {
if (toggled) {
root.toggled = false
Quickshell.execDetached(["pkill", "wayland-idle"]) // pkill doesn't accept too long names
} else {
root.toggled = true
Quickshell.execDetached([`${Directories.scriptPath}/wayland-idle-inhibitor.py`])
}
}
Process {
id: fetchActiveState
running: true
command: ["bash", "-c", "pidof wayland-idle-inhibitor.py"]
onExited: (exitCode, exitStatus) => {
root.toggled = exitCode === 0
}
}
StyledToolTip {
content: Translation.tr("Keep system awake")
}
}
@@ -0,0 +1,34 @@
import "root:/services"
import "root:/modules/common"
import "root:/modules/common/widgets"
import "root:/modules/common/functions/string_utils.js" as StringUtils
import "../"
import "root:/services/"
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Hyprland
QuickToggleButton {
toggled: Network.networkName.length > 0 && Network.networkName != "lo"
buttonIcon: Network.materialSymbol
onClicked: {
toggleNetwork.running = true
}
altAction: () => {
Quickshell.execDetached(["bash", "-c", `${Network.ethernet ? Config.options.apps.networkEthernet : Config.options.apps.network}`])
Hyprland.dispatch("global quickshell:sidebarRightClose")
}
Process {
id: toggleNetwork
command: ["bash", "-c", "nmcli radio wifi | grep -q enabled && nmcli radio wifi off || nmcli radio wifi on"]
onRunningChanged: {
if(!running) {
Network.update()
}
}
}
StyledToolTip {
content: StringUtils.format(Translation.tr("{0} | Right-click to configure"), Network.networkName)
}
}
@@ -0,0 +1,43 @@
import "root:/modules/common"
import "root:/modules/common/widgets"
import "../"
import "root:/services/"
import Quickshell.Io
import Quickshell
QuickToggleButton {
id: nightLightButton
property bool enabled: false
toggled: enabled
buttonIcon: "nightlight"
onClicked: {
nightLightButton.enabled = !nightLightButton.enabled
if (enabled) {
nightLightOn.startDetached()
}
else {
nightLightOff.startDetached()
}
}
Process {
id: nightLightOn
command: ["gammastep"]
}
Process {
id: nightLightOff
command: ["pkill", "gammastep"]
}
Process {
id: updateNightLightState
running: true
command: ["pidof", "gammastep"]
stdout: SplitParser {
onRead: (data) => { // if not empty then set toggled to true
nightLightButton.enabled = data.length > 0
}
}
}
StyledToolTip {
content: Translation.tr("Night Light")
}
}
@@ -0,0 +1,33 @@
import "root:/modules/common"
import "root:/modules/common/widgets"
import "root:/modules/common/functions/color_utils.js" as ColorUtils
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell.Io
GroupButton {
id: button
property string buttonIcon
baseWidth: altAction ? 60 : 40
baseHeight: 40
clickedWidth: baseWidth + 20
toggled: false
buttonRadius: (altAction && toggled) ? Appearance?.rounding.normal : Math.min(baseHeight, baseWidth) / 2
buttonRadiusPressed: Appearance?.rounding?.small
contentItem: MaterialSymbol {
anchors.centerIn: parent
iconSize: Appearance.font.pixelSize.larger
fill: toggled ? 1 : 0
color: toggled ? Appearance.m3colors.m3onPrimary : Appearance.colors.colOnLayer1
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
text: buttonIcon
Behavior on color {
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
}
}
}
@@ -0,0 +1,180 @@
import "root:/modules/common"
import "root:/modules/common/widgets"
import "root:/services"
import Qt5Compat.GraphicalEffects
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
Item {
id: root
required property var taskList;
property string emptyPlaceholderIcon
property string emptyPlaceholderText
property int todoListItemSpacing: 5
property int todoListItemPadding: 8
property int listBottomPadding: 80
Flickable {
id: flickable
anchors.fill: parent
contentHeight: columnLayout.height
clip: true
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: flickable.width
height: flickable.height
radius: Appearance.rounding.small
}
}
ColumnLayout {
id: columnLayout
width: parent.width
spacing: 0
Repeater {
model: ScriptModel {
values: taskList
}
delegate: Item {
id: todoItem
property bool pendingDoneToggle: false
property bool pendingDelete: false
property bool enableHeightAnimation: false
Layout.fillWidth: true
implicitHeight: todoItemRectangle.implicitHeight + todoListItemSpacing
height: implicitHeight
clip: true
Behavior on implicitHeight {
enabled: enableHeightAnimation
NumberAnimation {
duration: Appearance.animation.elementMoveFast.duration
easing.type: Appearance.animation.elementMoveFast.type
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
}
}
function startAction() {
enableHeightAnimation = true
todoItem.implicitHeight = 0
actionTimer.start()
}
Timer {
id: actionTimer
interval: Appearance.animation.elementMoveFast.duration
repeat: false
onTriggered: {
if (todoItem.pendingDelete) {
Todo.deleteItem(modelData.originalIndex)
} else if (todoItem.pendingDoneToggle) {
if (!modelData.done) Todo.markDone(modelData.originalIndex)
else Todo.markUnfinished(modelData.originalIndex)
}
}
}
Rectangle {
id: todoItemRectangle
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
implicitHeight: todoContentRowLayout.implicitHeight
color: Appearance.colors.colLayer2
radius: Appearance.rounding.small
ColumnLayout {
id: todoContentRowLayout
anchors.left: parent.left
anchors.right: parent.right
StyledText {
Layout.fillWidth: true // Needed for wrapping
Layout.leftMargin: 10
Layout.rightMargin: 10
Layout.topMargin: todoListItemPadding
id: todoContentText
text: modelData.content
wrapMode: Text.Wrap
}
RowLayout {
Layout.leftMargin: 10
Layout.rightMargin: 10
Layout.bottomMargin: todoListItemPadding
Item {
Layout.fillWidth: true
}
TodoItemActionButton {
Layout.fillWidth: false
onClicked: {
todoItem.pendingDoneToggle = true
todoItem.startAction()
}
contentItem: MaterialSymbol {
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
text: modelData.done ? "remove_done" : "check"
iconSize: Appearance.font.pixelSize.larger
color: Appearance.colors.colOnLayer1
}
}
TodoItemActionButton {
Layout.fillWidth: false
onClicked: {
todoItem.pendingDelete = true
todoItem.startAction()
}
contentItem: MaterialSymbol {
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
text: "delete_forever"
iconSize: Appearance.font.pixelSize.larger
color: Appearance.colors.colOnLayer1
}
}
}
}
}
}
}
// Bottom padding
Item {
implicitHeight: listBottomPadding
}
}
}
Item { // Placeholder when list is empty
visible: opacity > 0
opacity: taskList.length === 0 ? 1 : 0
anchors.fill: parent
Behavior on opacity {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
ColumnLayout {
anchors.centerIn: parent
spacing: 5
MaterialSymbol {
Layout.alignment: Qt.AlignHCenter
iconSize: 55
color: Appearance.m3colors.m3outline
text: emptyPlaceholderIcon
}
StyledText {
Layout.alignment: Qt.AlignHCenter
font.pixelSize: Appearance.font.pixelSize.normal
color: Appearance.m3colors.m3outline
horizontalAlignment: Text.AlignHCenter
text: emptyPlaceholderText
}
}
}
}
@@ -0,0 +1,35 @@
import "root:/modules/common"
import "root:/modules/common/widgets"
import "root:/modules/common/functions/color_utils.js" as ColorUtils
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
RippleButton {
id: button
property string buttonText: ""
property string tooltipText: ""
implicitHeight: 30
implicitWidth: implicitHeight
Behavior on implicitWidth {
SmoothedAnimation {
velocity: Appearance.animation.elementMove.velocity
}
}
buttonRadius: Appearance.rounding.small
contentItem: StyledText {
text: buttonText
horizontalAlignment: Text.AlignHCenter
font.pixelSize: Appearance.font.pixelSize.larger
color: Appearance.colors.colOnLayer1
}
StyledToolTip {
content: tooltipText
extraVisibleCondition: tooltipText.length > 0
}
}
@@ -0,0 +1,295 @@
import "root:/modules/common"
import "root:/modules/common/widgets"
import "root:/services"
import "root:/modules/common/functions/color_utils.js" as ColorUtils
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
Item {
id: root
property int currentTab: 0
property var tabButtonList: [{"icon": "checklist", "name": Translation.tr("Unfinished")}, {"name": Translation.tr("Done"), "icon": "check_circle"}]
property bool showAddDialog: false
property int dialogMargins: 20
property int fabSize: 48
property int fabMargins: 14
Keys.onPressed: (event) => {
if ((event.key === Qt.Key_PageDown || event.key === Qt.Key_PageUp) && event.modifiers === Qt.NoModifier) {
if (event.key === Qt.Key_PageDown) {
currentTab = Math.min(currentTab + 1, root.tabButtonList.length - 1)
} else if (event.key === Qt.Key_PageUp) {
currentTab = Math.max(currentTab - 1, 0)
}
event.accepted = true;
}
// Open add dialog on "N" (any modifiers)
else if (event.key === Qt.Key_N) {
root.showAddDialog = true
event.accepted = true;
}
// Close dialog on Esc if open
else if (event.key === Qt.Key_Escape && root.showAddDialog) {
root.showAddDialog = false
event.accepted = true;
}
}
ColumnLayout {
anchors.fill: parent
spacing: 0
TabBar {
id: tabBar
Layout.fillWidth: true
currentIndex: currentTab
onCurrentIndexChanged: currentTab = currentIndex
background: Item {
WheelHandler {
onWheel: (event) => {
if (event.angleDelta.y < 0)
tabBar.currentIndex = Math.min(tabBar.currentIndex + 1, root.tabButtonList.length - 1)
else if (event.angleDelta.y > 0)
tabBar.currentIndex = Math.max(tabBar.currentIndex - 1, 0)
}
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
}
}
Repeater {
model: root.tabButtonList
delegate: SecondaryTabButton {
selected: (index == currentTab)
buttonText: modelData.name
buttonIcon: modelData.icon
}
}
}
Item { // Tab indicator
id: tabIndicator
Layout.fillWidth: true
height: 3
property bool enableIndicatorAnimation: false
Connections {
target: root
function onCurrentTabChanged() {
tabIndicator.enableIndicatorAnimation = true
}
}
Rectangle {
id: indicator
property int tabCount: root.tabButtonList.length
property real fullTabSize: root.width / tabCount;
property real targetWidth: tabBar.contentItem.children[0].children[tabBar.currentIndex].tabContentWidth
implicitWidth: targetWidth
anchors {
top: parent.top
bottom: parent.bottom
}
x: tabBar.currentIndex * fullTabSize + (fullTabSize - targetWidth) / 2
color: Appearance.colors.colPrimary
radius: Appearance.rounding.full
Behavior on x {
enabled: tabIndicator.enableIndicatorAnimation
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
Behavior on implicitWidth {
enabled: tabIndicator.enableIndicatorAnimation
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
}
}
Rectangle { // Tabbar bottom border
id: tabBarBottomBorder
Layout.fillWidth: true
height: 1
color: Appearance.colors.colOutlineVariant
}
SwipeView {
id: swipeView
Layout.topMargin: 10
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 10
clip: true
currentIndex: currentTab
onCurrentIndexChanged: {
tabIndicator.enableIndicatorAnimation = true
currentTab = currentIndex
}
// To Do tab
TaskList {
listBottomPadding: root.fabSize + root.fabMargins * 2
emptyPlaceholderIcon: "check_circle"
emptyPlaceholderText: Translation.tr("Nothing here!")
taskList: Todo.list
.map(function(item, i) { return Object.assign({}, item, {originalIndex: i}); })
.filter(function(item) { return !item.done; })
}
TaskList {
listBottomPadding: root.fabSize + root.fabMargins * 2
emptyPlaceholderIcon: "checklist"
emptyPlaceholderText: Translation.tr("Finished tasks will go here")
taskList: Todo.list
.map(function(item, i) { return Object.assign({}, item, {originalIndex: i}); })
.filter(function(item) { return item.done; })
}
}
}
// + FAB
StyledRectangularShadow {
target: fabButton
radius: fabButton.buttonRadius
blur: 0.6 * Appearance.sizes.elevationMargin
}
FloatingActionButton {
id: fabButton
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.rightMargin: root.fabMargins
anchors.bottomMargin: root.fabMargins
onClicked: root.showAddDialog = true
contentItem: MaterialSymbol {
text: "add"
horizontalAlignment: Text.AlignHCenter
iconSize: Appearance.font.pixelSize.huge
color: Appearance.m3colors.m3onPrimaryContainer
}
}
Item {
anchors.fill: parent
z: 9999
visible: opacity > 0
opacity: root.showAddDialog ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: Appearance.animation.elementMoveFast.duration
easing.type: Appearance.animation.elementMoveFast.type
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
}
}
onVisibleChanged: {
if (!visible) {
todoInput.text = ""
fabButton.focus = true
}
}
Rectangle { // Scrim
anchors.fill: parent
radius: Appearance.rounding.small
color: Appearance.colors.colScrim
MouseArea {
hoverEnabled: true
anchors.fill: parent
preventStealing: true
propagateComposedEvents: false
}
}
Rectangle { // The dialog
id: dialog
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: root.dialogMargins
implicitHeight: dialogColumnLayout.implicitHeight
color: Appearance.colors.colSurfaceContainerHigh
radius: Appearance.rounding.normal
function addTask() {
if (todoInput.text.length > 0) {
Todo.addTask(todoInput.text)
todoInput.text = ""
root.showAddDialog = false
root.currentTab = 0 // Show unfinished tasks
}
}
ColumnLayout {
id: dialogColumnLayout
anchors.fill: parent
spacing: 16
StyledText {
Layout.topMargin: 16
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.alignment: Qt.AlignLeft
color: Appearance.m3colors.m3onSurface
font.pixelSize: Appearance.font.pixelSize.larger
text: Translation.tr("Add task")
}
TextField {
id: todoInput
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
padding: 10
color: activeFocus ? Appearance.m3colors.m3onSurface : Appearance.m3colors.m3onSurfaceVariant
renderType: Text.NativeRendering
selectedTextColor: Appearance.m3colors.m3onSecondaryContainer
selectionColor: Appearance.colors.colSecondaryContainer
placeholderText: Translation.tr("Task description")
placeholderTextColor: Appearance.m3colors.m3outline
focus: root.showAddDialog
onAccepted: dialog.addTask()
background: Rectangle {
anchors.fill: parent
radius: Appearance.rounding.verysmall
border.width: 2
border.color: todoInput.activeFocus ? Appearance.colors.colPrimary : Appearance.m3colors.m3outline
color: "transparent"
}
cursorDelegate: Rectangle {
width: 1
color: todoInput.activeFocus ? Appearance.colors.colPrimary : "transparent"
radius: 1
}
}
RowLayout {
Layout.bottomMargin: 16
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.alignment: Qt.AlignRight
spacing: 5
DialogButton {
buttonText: Translation.tr("Cancel")
onClicked: root.showAddDialog = false
}
DialogButton {
buttonText: Translation.tr("Add")
enabled: todoInput.text.length > 0
onClicked: dialog.addTask()
}
}
}
}
}
}
@@ -0,0 +1,56 @@
import "root:/modules/common"
import "root:/modules/common/widgets"
import "root:/services"
import "root:/services/"
import Qt5Compat.GraphicalEffects
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell.Widgets
import Quickshell.Services.Pipewire
GroupButton {
id: button
required property bool input
buttonRadius: Appearance.rounding.small
colBackground: Appearance.colors.colLayer2
colBackgroundHover: Appearance.colors.colLayer2Hover
colBackgroundActive: Appearance.colors.colLayer2Active
clickedWidth: baseWidth + 30
contentItem: RowLayout {
anchors.fill: parent
anchors.margins: 5
spacing: 5
MaterialSymbol {
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: false
Layout.leftMargin: 5
color: Appearance.colors.colOnLayer2
iconSize: Appearance.font.pixelSize.hugeass
text: input ? "mic_external_on" : "media_output"
}
ColumnLayout {
Layout.fillWidth: true
Layout.rightMargin: 5
spacing: 0
StyledText {
Layout.fillWidth: true
elide: Text.ElideRight
font.pixelSize: Appearance.font.pixelSize.normal
text: input ? Translation.tr("Input") : Translation.tr("Output")
color: Appearance.colors.colOnLayer2
}
StyledText {
Layout.fillWidth: true
elide: Text.ElideRight
font.pixelSize: Appearance.font.pixelSize.smaller
text: (input ? Pipewire.defaultAudioSource?.description : Pipewire.defaultAudioSink?.description) ?? Translation.tr("Unknown")
color: Appearance.m3colors.m3outline
}
}
}
}
@@ -0,0 +1,285 @@
import "root:/modules/common"
import "root:/modules/common/widgets"
import "root:/services"
import "root:/services/"
import Qt5Compat.GraphicalEffects
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import Quickshell.Services.Pipewire
Item {
id: root
property bool showDeviceSelector: false
property bool deviceSelectorInput
property int dialogMargins: 16
property PwNode selectedDevice
readonly property list<PwNode> appPwNodes: Pipewire.nodes.values.filter((node) => {
// return node.type == "21" // Alternative, not as clean
return node.isSink && node.isStream
})
function showDeviceSelectorDialog(input: bool) {
root.selectedDevice = null
root.showDeviceSelector = true
root.deviceSelectorInput = input
}
Keys.onPressed: (event) => {
// Close dialog on pressing Esc if open
if (event.key === Qt.Key_Escape && root.showDeviceSelector) {
root.showDeviceSelector = false
event.accepted = true;
}
}
ColumnLayout {
anchors.fill: parent
Item {
Layout.fillWidth: true
Layout.fillHeight: true
Flickable {
id: flickable
anchors.fill: parent
contentHeight: volumeMixerColumnLayout.height
clip: true
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: flickable.width
height: flickable.height
radius: Appearance.rounding.normal
}
}
ColumnLayout {
id: volumeMixerColumnLayout
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: 10
spacing: 10
Repeater {
model: root.appPwNodes
VolumeMixerEntry {
Layout.fillWidth: true
required property var modelData
node: modelData
}
}
}
}
// Placeholder when list is empty
Item {
anchors.fill: flickable
visible: opacity > 0
opacity: (root.appPwNodes.length === 0) ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: Appearance.animation.menuDecel.duration
easing.type: Appearance.animation.menuDecel.type
}
}
ColumnLayout {
anchors.centerIn: parent
spacing: 5
MaterialSymbol {
Layout.alignment: Qt.AlignHCenter
iconSize: 55
color: Appearance.m3colors.m3outline
text: "brand_awareness"
}
StyledText {
Layout.alignment: Qt.AlignHCenter
font.pixelSize: Appearance.font.pixelSize.normal
color: Appearance.m3colors.m3outline
horizontalAlignment: Text.AlignHCenter
text: Translation.tr("No audio source")
}
}
}
}
// Device selector
ButtonGroup {
id: deviceSelectorRowLayout
Layout.fillWidth: true
Layout.fillHeight: false
AudioDeviceSelectorButton {
Layout.fillWidth: true
input: false
onClicked: root.showDeviceSelectorDialog(input)
}
AudioDeviceSelectorButton {
Layout.fillWidth: true
input: true
onClicked: root.showDeviceSelectorDialog(input)
}
}
}
// Device selector dialog
Item {
anchors.fill: parent
z: 9999
visible: opacity > 0
opacity: root.showDeviceSelector ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: Appearance.animation.elementMoveFast.duration
easing.type: Appearance.animation.elementMoveFast.type
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
}
}
Rectangle { // Scrim
id: scrimOverlay
anchors.fill: parent
radius: Appearance.rounding.small
color: Appearance.colors.colScrim
MouseArea {
hoverEnabled: true
anchors.fill: parent
preventStealing: true
propagateComposedEvents: false
}
}
Rectangle { // The dialog
id: dialog
color: Appearance.colors.colSurfaceContainerHigh
radius: Appearance.rounding.normal
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: 30
implicitHeight: dialogColumnLayout.implicitHeight
ColumnLayout {
id: dialogColumnLayout
anchors.fill: parent
spacing: 16
StyledText {
id: dialogTitle
Layout.topMargin: dialogMargins
Layout.leftMargin: dialogMargins
Layout.rightMargin: dialogMargins
Layout.alignment: Qt.AlignLeft
color: Appearance.m3colors.m3onSurface
font.pixelSize: Appearance.font.pixelSize.larger
text: `Select ${root.deviceSelectorInput ? "input" : "output"} device`
}
Rectangle {
color: Appearance.m3colors.m3outline
implicitHeight: 1
Layout.fillWidth: true
Layout.leftMargin: dialogMargins
Layout.rightMargin: dialogMargins
}
Flickable {
id: dialogFlickable
Layout.fillWidth: true
clip: true
implicitHeight: Math.min(scrimOverlay.height - dialogMargins * 8 - dialogTitle.height - dialogButtonsRowLayout.height, devicesColumnLayout.implicitHeight)
contentHeight: devicesColumnLayout.implicitHeight
ColumnLayout {
id: devicesColumnLayout
anchors.fill: parent
Layout.fillWidth: true
spacing: 0
Repeater {
model: ScriptModel {
values: Pipewire.nodes.values.filter(node => {
return !node.isStream && node.isSink !== root.deviceSelectorInput && node.audio
})
}
// This could and should be refractored, but all data becomes null when passed wtf
delegate: StyledRadioButton {
id: radioButton
required property var modelData
Layout.leftMargin: root.dialogMargins
Layout.rightMargin: root.dialogMargins
Layout.fillWidth: true
description: modelData.description
checked: modelData.id === Pipewire.defaultAudioSink?.id
Connections {
target: root
function onShowDeviceSelectorChanged() {
if(!root.showDeviceSelector) return;
radioButton.checked = (modelData.id === Pipewire.defaultAudioSink?.id)
}
}
onCheckedChanged: {
if (checked) {
root.selectedDevice = modelData
}
}
}
}
Item {
implicitHeight: dialogMargins
}
}
}
Rectangle {
color: Appearance.m3colors.m3outline
implicitHeight: 1
Layout.fillWidth: true
Layout.leftMargin: dialogMargins
Layout.rightMargin: dialogMargins
}
RowLayout {
id: dialogButtonsRowLayout
Layout.bottomMargin: dialogMargins
Layout.leftMargin: dialogMargins
Layout.rightMargin: dialogMargins
Layout.alignment: Qt.AlignRight
DialogButton {
buttonText: Translation.tr("Cancel")
onClicked: {
root.showDeviceSelector = false
}
}
DialogButton {
buttonText: Translation.tr("OK")
onClicked: {
root.showDeviceSelector = false
if (root.selectedDevice) {
if (root.deviceSelectorInput) {
Pipewire.preferredDefaultAudioSource = root.selectedDevice
} else {
Pipewire.preferredDefaultAudioSink = root.selectedDevice
}
}
}
}
}
}
}
}
}
@@ -0,0 +1,65 @@
import "root:/modules/common"
import "root:/modules/common/widgets"
import "root:/services"
import Qt5Compat.GraphicalEffects
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import Quickshell.Services.Pipewire
Item {
id: root
required property PwNode node;
PwObjectTracker { objects: [ node ] }
implicitHeight: rowLayout.implicitHeight
RowLayout {
id: rowLayout
anchors.fill: parent
spacing: 10
ColumnLayout {
Layout.fillWidth: true
spacing: 0
RowLayout {
StyledText {
Layout.fillWidth: true
font.pixelSize: Appearance.font.pixelSize.normal
elide: Text.ElideRight
text: {
// application.name -> description -> name
const app = root.node.properties["application.name"] ?? (root.node.description != "" ? root.node.description : root.node.name);
const media = root.node.properties["media.name"];
return media != undefined ? `${app} ${media}` : app;
}
}
}
RowLayout {
Image {
property real size: slider.trackHeight * 1.3
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
visible: source != ""
sourceSize.width: size
sourceSize.height: size
source: {
let icon;
icon = AppSearch.guessIcon(root.node.properties["application.icon-name"]);
if (AppSearch.iconExists(icon)) return Quickshell.iconPath(icon, "image-missing");
icon = AppSearch.guessIcon(root.node.properties["node.name"]);
return Quickshell.iconPath(icon, "image-missing");
}
}
StyledSlider {
id: slider
value: root.node.audio.volume
onValueChanged: root.node.audio.volume = value
}
}
}
}
}