Clock and calendar

This commit is contained in:
quadbyte 2025-08-09 18:00:57 -04:00
parent 0e037561f3
commit bce57c101a
12 changed files with 530 additions and 4 deletions

81
Helpers/Holidays.js Normal file
View file

@ -0,0 +1,81 @@
var _countryCode = null;
var _regionCode = null;
var _regionName = null;
var _holidaysCache = {};
function getCountryCode(callback) {
if (_countryCode) {
callback(_countryCode);
return;
}
var xhr = new XMLHttpRequest();
xhr.open("GET", "https://nominatim.openstreetmap.org/search?city="+ Settings.settings.weatherCity+"&country=&format=json&addressdetails=1&extratags=1", true);
xhr.onreadystatechange = function() {
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
var response = JSON.parse(xhr.responseText);
_countryCode = response?.[0]?.address?.country_code ?? "US";
_regionCode = response?.[0]?.address?.["ISO3166-2-lvl4"] ?? "";
_regionName = response?.[0]?.address?.state ?? "";
callback(_countryCode);
}
}
xhr.send();
}
function getHolidays(year, countryCode, callback) {
var cacheKey = year + "-" + countryCode;
if (_holidaysCache[cacheKey]) {
callback(_holidaysCache[cacheKey]);
return;
}
var url = "https://date.nager.at/api/v3/PublicHolidays/" + year + "/" + countryCode;
var xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.onreadystatechange = function() {
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
var holidays = JSON.parse(xhr.responseText);
var augmentedHolidays = filterHolidaysByRegion(holidays);
_holidaysCache[cacheKey] = augmentedHolidays;
callback(augmentedHolidays);
}
}
xhr.send();
}
function filterHolidaysByRegion(holidays) {
if (!_regionCode) {
return holidays;
}
const retHolidays = [];
holidays.forEach(function(holiday) {
if (holiday.counties?.length > 0) {
let found = false;
holiday.counties.forEach(function(county) {
if (county.toLowerCase() === _regionCode.toLowerCase()) {
found = true;
}
});
if (found) {
var regionText = " (" + _regionName + ")";
holiday.name = holiday.name + regionText;
holiday.localName = holiday.localName + regionText;
retHolidays.push(holiday);
}
} else {
retHolidays.push(holiday);
}
});
return retHolidays;
}
function getHolidaysForMonth(year, month, callback) {
getCountryCode(function(countryCode) {
getHolidays(year, countryCode, function(holidays) {
var filtered = holidays.filter(function(h) {
var date = new Date(h.date);
return date.getFullYear() === year && date.getMonth() === month;
});
callback(filtered);
});
});
}

View file

@ -61,7 +61,7 @@ PanelWindow {
NSlider {}
Clock {}
}
}
}

56
Modules/Bar/Clock.qml Normal file
View file

@ -0,0 +1,56 @@
import QtQuick
import qs.Services
import qs.Theme
import qs.Widgets
Rectangle {
id: root
readonly property real scaling: Scaling.scale(screen)
width: textItem.paintedWidth
height: textItem.paintedHeight
color: "transparent"
Text {
id: textItem
text: Time.time
font.family: Theme.fontFamily
font.weight: Font.Bold
font.pointSize: Style.fontSmall * scaling
color: Theme.textPrimary
anchors.centerIn: parent
}
MouseArea {
id: clockMouseArea
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onEntered: {
if (!calendar.visible) {
tooltip.show()
}
}
onExited: {
tooltip.hide()
}
onClicked: function () {
calendar.visible = !calendar.visible
if (calendar.visible) {
tooltip.hide();
}
}
}
NCalendar {
id: calendar
visible: false
}
NTooltip {
id: tooltip
text: Time.dateString
target: root
}
}

View file

View file

@ -41,6 +41,6 @@ Singleton {
}
// 3) Safe default
return 1.0
return 2.0
}
}

View file

@ -6,7 +6,6 @@ import Quickshell.Io
import qs.Services
Singleton {
id: root
property string shellName: "Noctalia"
property string settingsDir: Quickshell.env("NOCTALIA_SETTINGS_DIR")
@ -17,6 +16,7 @@ Singleton {
|| (settingsDir + "Settings.json")
property string themeFile: Quickshell.env("NOCTALIA_THEME_FILE")
|| (settingsDir + "Theme.json")
property var settings: settingAdapter
Item {
Component.onCompleted: {
@ -24,4 +24,88 @@ Singleton {
Quickshell.execDetached(["mkdir", "-p", settingsDir])
}
}
FileView {
id: settingFileView
path: settingsFile
watchChanges: true
onFileChanged: reload()
onAdapterUpdated: writeAdapter()
Component.onCompleted: function () {
reload()
}
onLoaded: function () {// Qt.callLater(function () {
// WallpaperManager.setCurrentWallpaper(settings.currentWallpaper, true);
// })
}
onLoadFailed: function (error) {
if (error.toString().includes("No such file") || error === 2) {
// File doesn't exist, create it with default values
writeAdapter()
}
}
JsonAdapter {
id: settingAdapter
property string weatherCity: "Dinslaken"
property string profileImage: Quickshell.env("HOME") + "/.face"
property bool useFahrenheit: false
property string wallpaperFolder: "/usr/share/wallpapers"
property string currentWallpaper: ""
property string videoPath: "~/Videos/"
property bool showActiveWindow: true
property bool showActiveWindowIcon: false
property bool showSystemInfoInBar: false
property bool showCorners: false
property bool showTaskbar: true
property bool showMediaInBar: false
property bool useSWWW: false
property bool randomWallpaper: false
property bool useWallpaperTheme: false
property int wallpaperInterval: 300
property string wallpaperResize: "crop"
property int transitionFps: 60
property string transitionType: "random"
property real transitionDuration: 1.1
property string visualizerType: "radial"
property bool reverseDayMonth: false
property bool use12HourClock: false
property bool dimPanels: true
property real fontSizeMultiplier: 1.0 // Font size multiplier (1.0 = normal, 1.2 = 20% larger, 0.8 = 20% smaller)
property int taskbarIconSize: 24 // Taskbar icon button size in pixels (default: 32, smaller: 24, larger: 40)
property var pinnedExecs: [] // Added for AppLauncher pinned apps
property bool showDock: true
property bool dockExclusive: false
property bool wifiEnabled: false
property bool bluetoothEnabled: false
property int recordingFrameRate: 60
property string recordingQuality: "very_high"
property string recordingCodec: "h264"
property string audioCodec: "opus"
property bool showCursor: true
property string colorRange: "limited"
// Monitor/Display Settings
property var barMonitors: [] // Array of monitor names to show the bar on
property var dockMonitors: [] // Array of monitor names to show the dock on
property var notificationMonitors: [] // Array of monitor names to show notifications on, "*" means all monitors
property var monitorScaleOverrides: {
} // Map of monitor name -> scale override (e.g., 0.8..2.0). When set, Theme.scale() returns this value
}
}
Connections {
target: settingAdapter
function onRandomWallpaperChanged() {
WallpaperManager.toggleRandomWallpaper()
}
function onWallpaperIntervalChanged() {
WallpaperManager.restartRandomWallpaperTimer()
}
function onWallpaperFolderChanged() {
WallpaperManager.loadWallpapers()
}
function onNotificationMonitorsChanged() {}
}
}

49
Services/Time.qml Normal file
View file

@ -0,0 +1,49 @@
pragma Singleton
import Quickshell
import QtQuick
import qs.Services
Singleton {
id: root
property var date: new Date()
property string time: Settings.settings.use12HourClock ? Qt.formatDateTime(
date,
"h:mm AP") : Qt.formatDateTime(
date, "HH:mm")
property string dateString: {
let now = date
let dayName = now.toLocaleDateString(Qt.locale(), "ddd")
dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1)
let day = now.getDate()
let suffix
if (day > 3 && day < 21)
suffix = 'th'
else
switch (day % 10) {
case 1:
suffix = "st"
break
case 2:
suffix = "nd"
break
case 3:
suffix = "rd"
break
default:
suffix = "th"
}
let month = now.toLocaleDateString(Qt.locale(), "MMMM")
let year = now.toLocaleDateString(Qt.locale(), "yyyy")
return `${dayName}, ` + (Settings.settings.reverseDayMonth ? `${month} ${day}${suffix} ${year}` : `${day}${suffix} ${month} ${year}`)
}
Timer {
interval: 1000
repeat: true
running: true
onTriggered: root.date = new Date()
}
}

View file

@ -47,4 +47,6 @@ Singleton {
property int barHeight: 36
property int baseWidgetHeight: 32
property int sliderWidth: 200
property int tooltipDelay: 300
}

204
Widgets/NCalendar.qml Normal file
View file

@ -0,0 +1,204 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import qs.Services
import qs.Theme
import "../Helpers/Holidays.js" as Holidays
NPanel {
id: calendarOverlay
readonly property real scaling: Scaling.scale(screen)
Rectangle {
color: Theme.backgroundPrimary
radius: 12
border.color: Theme.backgroundTertiary
border.width: 1
width: 340 * scaling
height: 380
anchors.top: parent.top
anchors.right: parent.right
anchors.topMargin: 4 * scaling
anchors.rightMargin: 4 * scaling
// Prevent closing when clicking in the panel bg
MouseArea {
anchors.fill: parent
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 12
// Month/Year header with navigation
RowLayout {
Layout.fillWidth: true
spacing: 8
NIconButton {
icon: "chevron_left"
onClicked: function () {
let newDate = new Date(calendar.year, calendar.month - 1, 1)
calendar.year = newDate.getFullYear()
calendar.month = newDate.getMonth()
}
}
Text {
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
text: calendar.title
color: Theme.textPrimary
opacity: 0.7
font.pointSize: Style.fontSmall * scaling
font.family: Theme.fontFamily
font.bold: true
}
NIconButton {
icon: "chevron_right"
onClicked: function () {
let newDate = new Date(calendar.year, calendar.month + 1, 1)
calendar.year = newDate.getFullYear()
calendar.month = newDate.getMonth()
}
}
}
DayOfWeekRow {
Layout.fillWidth: true
spacing: 0
Layout.leftMargin: 8 // Align with grid
Layout.rightMargin: 8
delegate: Text {
text: shortName
color: Theme.textPrimary
opacity: 0.8
font.pointSize: Style.fontSmall * scaling
font.family: Theme.fontFamily
font.bold: true
horizontalAlignment: Text.AlignHCenter
width: 32
}
}
MonthGrid {
id: calendar
property var holidays: []
// Fetch holidays when calendar is opened or month/year changes
function updateHolidays() {
Holidays.getHolidaysForMonth(calendar.year, calendar.month,
function (holidays) {
calendar.holidays = holidays
})
}
Layout.fillWidth: true
Layout.leftMargin: 8
Layout.rightMargin: 8
spacing: 0
month: Time.date.getMonth()
year: Time.date.getFullYear()
onMonthChanged: updateHolidays()
onYearChanged: updateHolidays()
Component.onCompleted: updateHolidays()
// Optionally, update when the panel becomes visible
Connections {
function onVisibleChanged() {
if (calendarOverlay.visible) {
calendar.month = Time.date.getMonth()
calendar.year = Time.date.getFullYear()
calendar.updateHolidays()
}
}
target: calendarOverlay
}
delegate: Rectangle {
property var holidayInfo: calendar.holidays.filter(function (h) {
var d = new Date(h.date)
return d.getDate() === model.day && d.getMonth() === model.month
&& d.getFullYear() === model.year
})
property bool isHoliday: holidayInfo.length > 0
width: 32
height: 32
radius: 8
color: {
if (model.today)
return Theme.accentPrimary
if (mouseArea2.containsMouse)
return Theme.backgroundTertiary
return "transparent"
}
// Holiday dot indicator
Rectangle {
visible: isHoliday
width: 4
height: 4
radius: 4
color: Theme.accentTertiary
anchors.top: parent.top
anchors.right: parent.right
anchors.topMargin: 4
anchors.rightMargin: 4
z: 2
}
Text {
anchors.centerIn: parent
text: model.day
color: model.today ? Theme.onAccent : Theme.textPrimary
opacity: model.month === calendar.month ? (mouseArea2.containsMouse ? 1 : 0.7) : 0.3
font.pointSize: Style.fontSmall * scaling
font.family: Theme.fontFamily
font.bold: model.today ? true : false
}
MouseArea {
id: mouseArea2
anchors.fill: parent
hoverEnabled: true
onEntered: {
if (isHoliday) {
holidayTooltip.text = holidayInfo.map(function (h) {
return h.localName + (h.name !== h.localName ? " (" + h.name + ")" : "")
+ (h.global ? " [Global]" : "")
}).join(", ")
holidayTooltip.target = parent;
holidayTooltip.show();
}
}
onExited: holidayTooltip.hide()
}
NTooltip {
id: holidayTooltip
text: ""
}
Behavior on color {
ColorAnimation {
duration: 150
}
}
}
}
}
}
}

View file

@ -14,6 +14,7 @@ Rectangle {
property bool hovering: false
property var onEntered: function () {}
property var onExited: function () {}
property var onClicked: function () {}
implicitWidth: size
implicitHeight: size
@ -45,5 +46,8 @@ Rectangle {
hovering = false
root.onExited()
}
onClicked: {
root.onClicked()
}
}
}

46
Widgets/NPanel.qml Normal file
View file

@ -0,0 +1,46 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Services
import qs.Theme
PanelWindow {
id: outerPanel
readonly property real scaling: Scaling.scale(screen)
property bool showOverlay: Settings.settings.dimPanels
property int topMargin: Style.barHeight * scaling
property color overlayColor: showOverlay ? Theme.overlay : "transparent"
function dismiss() {
visible = false
}
function show() {
visible = true
}
implicitWidth: screen.width
implicitHeight: screen.height
color: visible ? overlayColor : "transparent"
visible: false
WlrLayershell.exclusionMode: ExclusionMode.Ignore
screen: (typeof modelData !== 'undefined' ? modelData : null)
anchors.top: true
anchors.left: true
anchors.right: true
anchors.bottom: true
margins.top: topMargin
MouseArea {
anchors.fill: parent
onClicked: outerPanel.dismiss()
}
Behavior on color {
ColorAnimation {
duration: 350
easing.type: Easing.InOutCubic
}
}
}

View file

@ -9,7 +9,7 @@ Window {
property bool isVisible: false
property string text: "Placeholder"
property Item target: null
property int delay: 300
property int delay: Style.tooltipDelay
property bool positionAbove: false
flags: Qt.ToolTip | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint