better structure
This commit is contained in:
526
config/quickshell/Modules/Panel/CalendarPanel.qml
Normal file
526
config/quickshell/Modules/Panel/CalendarPanel.qml
Normal file
@@ -0,0 +1,526 @@
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Constants
|
||||
import qs.Noctalia
|
||||
import qs.Services
|
||||
import qs.Utils
|
||||
|
||||
NPanel {
|
||||
id: root
|
||||
|
||||
preferredWidth: 400
|
||||
preferredHeight: 520
|
||||
|
||||
panelContent: ColumnLayout {
|
||||
id: content
|
||||
|
||||
readonly property int firstDayOfWeek: Qt.locale().firstDayOfWeek
|
||||
property bool isCurrentMonth: checkIsCurrentMonth()
|
||||
readonly property bool weatherReady: (LocationService.data.weather !== null)
|
||||
|
||||
function checkIsCurrentMonth() {
|
||||
return (Time.date.getMonth() === grid.month) && (Time.date.getFullYear() === grid.year);
|
||||
}
|
||||
|
||||
function getISOWeekNumber(date) {
|
||||
const target = new Date(date.getTime());
|
||||
target.setHours(0, 0, 0, 0);
|
||||
const dayOfWeek = target.getDay() || 7;
|
||||
target.setDate(target.getDate() + 4 - dayOfWeek);
|
||||
const yearStart = new Date(target.getFullYear(), 0, 1);
|
||||
const weekNumber = Math.ceil(((target - yearStart) / 8.64e+07 + 1) / 7);
|
||||
return weekNumber;
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginL
|
||||
spacing: Style.marginM
|
||||
|
||||
Connections {
|
||||
function onDateChanged() {
|
||||
isCurrentMonth = checkIsCurrentMonth();
|
||||
}
|
||||
|
||||
target: Time
|
||||
}
|
||||
|
||||
// Combined blue banner with date/time and weather summary
|
||||
NBox {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: blueColumn.implicitHeight + Style.marginM * 2
|
||||
|
||||
ColumnLayout {
|
||||
id: blueColumn
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM
|
||||
spacing: 0
|
||||
|
||||
// Combined layout for weather icon, date, and weather text
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 60
|
||||
spacing: Style.marginS
|
||||
|
||||
// Weather icon and temperature
|
||||
ColumnLayout {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
spacing: Style.marginXXS
|
||||
|
||||
NIcon {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
icon: weatherReady ? LocationService.weatherSymbolFromCode(LocationService.data.weather.current_weather.weathercode) : "cloud"
|
||||
pointSize: Style.fontSizeXXL
|
||||
color: Colors.text
|
||||
}
|
||||
|
||||
NText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: {
|
||||
if (!weatherReady)
|
||||
return "";
|
||||
|
||||
var temp = LocationService.data.weather.current_weather.temperature;
|
||||
var suffix = "C";
|
||||
temp = Math.round(temp);
|
||||
return `${temp}°${suffix}`;
|
||||
}
|
||||
pointSize: Style.fontSizeM
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Colors.text
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Today day number
|
||||
NText {
|
||||
visible: content.isCurrentMonth
|
||||
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
|
||||
text: Time.date.getDate()
|
||||
pointSize: Style.fontSizeXXXL * 1.5
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Colors.text
|
||||
}
|
||||
|
||||
Item {
|
||||
visible: !content.isCurrentMonth
|
||||
}
|
||||
|
||||
// Month, year, location
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: false
|
||||
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
|
||||
spacing: -Style.marginXS
|
||||
|
||||
RowLayout {
|
||||
spacing: 0
|
||||
|
||||
NText {
|
||||
text: Qt.locale().monthName(grid.month, Locale.LongFormat).toUpperCase()
|
||||
pointSize: Style.fontSizeXL * 1.2
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Colors.text
|
||||
Layout.alignment: Qt.AlignBaseline
|
||||
Layout.maximumWidth: 150
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
NText {
|
||||
text: ` ${grid.year}`
|
||||
pointSize: Style.fontSizeL
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Qt.alpha(Colors.text, 0.7)
|
||||
Layout.alignment: Qt.AlignBaseline
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: 0
|
||||
|
||||
NText {
|
||||
text: {
|
||||
if (!weatherReady)
|
||||
return "Weather unavailable";
|
||||
|
||||
const chunks = LocationService.data.name.split(",");
|
||||
return chunks[0];
|
||||
}
|
||||
pointSize: Style.fontSizeM
|
||||
font.weight: Style.fontWeightMedium
|
||||
color: Colors.text
|
||||
Layout.maximumWidth: 150
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
NText {
|
||||
text: weatherReady ? ` (${LocationService.data.weather.timezone_abbreviation})` : ""
|
||||
pointSize: Style.fontSizeXS
|
||||
font.weight: Style.fontWeightMedium
|
||||
color: Qt.alpha(Colors.text, 0.7)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Spacer between date and clock
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Digital clock with circular progress
|
||||
Item {
|
||||
width: Style.fontSizeXXXL * 1.9
|
||||
height: Style.fontSizeXXXL * 1.9
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
// Seconds circular progress
|
||||
Canvas {
|
||||
id: secondsProgress
|
||||
|
||||
property real progress: Time.date.getSeconds() / 60
|
||||
|
||||
anchors.fill: parent
|
||||
onProgressChanged: requestPaint()
|
||||
onPaint: {
|
||||
var ctx = getContext("2d");
|
||||
var centerX = width / 2;
|
||||
var centerY = height / 2;
|
||||
var radius = Math.min(width, height) / 2 - 3;
|
||||
ctx.reset();
|
||||
// Background circle
|
||||
ctx.beginPath();
|
||||
ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI);
|
||||
ctx.lineWidth = 2.5;
|
||||
ctx.strokeStyle = Qt.alpha(Colors.text, 0.15);
|
||||
ctx.stroke();
|
||||
// Progress arc
|
||||
ctx.beginPath();
|
||||
ctx.arc(centerX, centerY, radius, -Math.PI / 2, -Math.PI / 2 + progress * 2 * Math.PI);
|
||||
ctx.lineWidth = 2.5;
|
||||
ctx.strokeStyle = Colors.text;
|
||||
ctx.lineCap = "round";
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onDateChanged() {
|
||||
secondsProgress.progress = Time.date.getSeconds() / 60;
|
||||
}
|
||||
|
||||
target: Time
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Digital clock
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: -Style.marginXXS
|
||||
|
||||
NText {
|
||||
text: {
|
||||
var t = Qt.locale().toString(new Date(), "HH");
|
||||
return t.split(" ")[0];
|
||||
}
|
||||
pointSize: Style.fontSizeXS
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Colors.text
|
||||
family: Fonts.sans
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: Qt.formatTime(Time.date, "mm")
|
||||
pointSize: Style.fontSizeXXS
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Colors.text
|
||||
family: Fonts.sans
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 6-day forecast (outside blue banner)
|
||||
RowLayout {
|
||||
visible: weatherReady
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: Style.marginL
|
||||
|
||||
Repeater {
|
||||
model: weatherReady ? Math.min(6, LocationService.data.weather.daily.time.length) : 0
|
||||
|
||||
delegate: ColumnLayout {
|
||||
Layout.preferredWidth: 0
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: Style.marginS
|
||||
|
||||
NText {
|
||||
text: {
|
||||
var weatherDate = new Date(LocationService.data.weather.daily.time[index].replace(/-/g, "/"));
|
||||
return Qt.locale().toString(weatherDate, "ddd");
|
||||
}
|
||||
color: Color.mOnSurfaceVariant
|
||||
pointSize: Style.fontSizeM
|
||||
font.weight: Style.fontWeightMedium
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NIcon {
|
||||
Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
|
||||
icon: LocationService.weatherSymbolFromCode(LocationService.data.weather.daily.weathercode[index])
|
||||
pointSize: Style.fontSizeXXL * 1.5
|
||||
color: LocationService.weatherColorFromCode(LocationService.data.weather.daily.weathercode[index])
|
||||
}
|
||||
|
||||
NText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: {
|
||||
var max = LocationService.data.weather.daily.temperature_2m_max[index];
|
||||
var min = LocationService.data.weather.daily.temperature_2m_min[index];
|
||||
max = Math.round(max);
|
||||
min = Math.round(min);
|
||||
return `${max}°/${min}°`;
|
||||
}
|
||||
pointSize: Style.fontSizeXS
|
||||
color: Colors.text
|
||||
font.weight: Style.fontWeightMedium
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Loading indicator for weather
|
||||
RowLayout {
|
||||
visible: !weatherReady
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
NBusyIndicator {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Spacer
|
||||
Item {
|
||||
}
|
||||
|
||||
// Navigation and divider
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "chevron-left"
|
||||
colorBg: Color.transparent
|
||||
colorBorder: Color.transparent
|
||||
colorBorderHover: Color.transparent
|
||||
onClicked: {
|
||||
let newDate = new Date(grid.year, grid.month - 1, 1);
|
||||
grid.year = newDate.getFullYear();
|
||||
grid.month = newDate.getMonth();
|
||||
content.isCurrentMonth = content.checkIsCurrentMonth();
|
||||
}
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "calendar"
|
||||
colorBg: Color.transparent
|
||||
colorBorder: Color.transparent
|
||||
colorBorderHover: Color.transparent
|
||||
onClicked: {
|
||||
grid.month = Time.date.getMonth();
|
||||
grid.year = Time.date.getFullYear();
|
||||
content.isCurrentMonth = true;
|
||||
}
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "chevron-right"
|
||||
colorBg: Color.transparent
|
||||
colorBorder: Color.transparent
|
||||
colorBorderHover: Color.transparent
|
||||
onClicked: {
|
||||
let newDate = new Date(grid.year, grid.month + 1, 1);
|
||||
grid.year = newDate.getFullYear();
|
||||
grid.month = newDate.getMonth();
|
||||
content.isCurrentMonth = content.checkIsCurrentMonth();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Names of days of the week
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
Item {
|
||||
Layout.preferredWidth: visible ? Style.baseWidgetSize * 0.7 : 0
|
||||
}
|
||||
|
||||
GridLayout {
|
||||
Layout.fillWidth: true
|
||||
columns: 7
|
||||
rows: 1
|
||||
columnSpacing: 0
|
||||
rowSpacing: 0
|
||||
|
||||
Repeater {
|
||||
model: 7
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Style.baseWidgetSize * 0.6
|
||||
|
||||
NText {
|
||||
anchors.centerIn: parent
|
||||
text: {
|
||||
let dayIndex = (content.firstDayOfWeek + index) % 7;
|
||||
const dayNames = ["S", "M", "T", "W", "T", "F", "S"];
|
||||
return dayNames[dayIndex];
|
||||
}
|
||||
color: Color.mPrimary
|
||||
pointSize: Style.fontSizeS
|
||||
font.weight: Style.fontWeightBold
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Grid with weeks and days
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
spacing: 0
|
||||
|
||||
// Column of week numbers
|
||||
ColumnLayout {
|
||||
Layout.preferredWidth: visible ? Style.baseWidgetSize * 0.7 : 0
|
||||
Layout.fillHeight: true
|
||||
spacing: 0
|
||||
|
||||
Repeater {
|
||||
model: 6
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
NText {
|
||||
anchors.centerIn: parent
|
||||
color: Color.mOutline
|
||||
pointSize: Style.fontSizeXXS
|
||||
font.weight: Style.fontWeightMedium
|
||||
text: {
|
||||
let firstOfMonth = new Date(grid.year, grid.month, 1);
|
||||
let firstDayOfWeek = content.firstDayOfWeek;
|
||||
let firstOfMonthDayOfWeek = firstOfMonth.getDay();
|
||||
let daysBeforeFirst = (firstOfMonthDayOfWeek - firstDayOfWeek + 7) % 7;
|
||||
if (daysBeforeFirst === 0)
|
||||
daysBeforeFirst = 7;
|
||||
|
||||
let gridStartDate = new Date(grid.year, grid.month, 1 - daysBeforeFirst);
|
||||
let rowStartDate = new Date(gridStartDate);
|
||||
rowStartDate.setDate(gridStartDate.getDate() + (index * 7));
|
||||
let thursday = new Date(rowStartDate);
|
||||
if (firstDayOfWeek === 0) {
|
||||
thursday.setDate(rowStartDate.getDate() + 4);
|
||||
} else if (firstDayOfWeek === 1) {
|
||||
thursday.setDate(rowStartDate.getDate() + 3);
|
||||
} else {
|
||||
let daysToThursday = (4 - firstDayOfWeek + 7) % 7;
|
||||
thursday.setDate(rowStartDate.getDate() + daysToThursday);
|
||||
}
|
||||
return `${getISOWeekNumber(thursday)}`;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Days Grid
|
||||
MonthGrid {
|
||||
id: grid
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
spacing: Style.marginXXS
|
||||
month: Time.date.getMonth()
|
||||
year: Time.date.getFullYear()
|
||||
locale: Qt.locale()
|
||||
|
||||
delegate: Item {
|
||||
Rectangle {
|
||||
width: Style.baseWidgetSize * 0.9
|
||||
height: Style.baseWidgetSize * 0.9
|
||||
anchors.centerIn: parent
|
||||
radius: Style.radiusM
|
||||
color: model.today ? Color.mSecondary : Color.transparent
|
||||
|
||||
NText {
|
||||
anchors.centerIn: parent
|
||||
text: model.day
|
||||
color: {
|
||||
if (model.today)
|
||||
return Color.mOnSecondary;
|
||||
|
||||
if (model.month === grid.month)
|
||||
return Color.mOnSurface;
|
||||
|
||||
return Color.mOnSurfaceVariant;
|
||||
}
|
||||
opacity: model.month === grid.month ? 1 : 0.4
|
||||
pointSize: Style.fontSizeM
|
||||
font.weight: model.today ? Style.fontWeightBold : Style.fontWeightMedium
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user