diff --git a/src/components/InteractiveEditor/EditWidget.vue b/src/components/InteractiveEditor/EditWidget.vue
index 2b9a62a7e7..3d747850e3 100644
--- a/src/components/InteractiveEditor/EditWidget.vue
+++ b/src/components/InteractiveEditor/EditWidget.vue
@@ -45,9 +45,36 @@
:options="boolRadioOptions"
:initialOption="form.ignoreErrors"
/>
-
({
timeout: '',
useProxy: '',
ignoreErrors: '',
+ options: {},
});
/* Coerce a string from a free-form options input back to its likely native type. */
@@ -103,6 +131,55 @@ const coerceValue = (raw) => {
return raw;
};
+const widgetOptionDefinitions = {
+ 'nextcloud-calendar': [
+ {
+ name: 'icsUrl',
+ label: 'Calendar URL',
+ description: 'Public ICS export URL or shared calendar export URL.',
+ type: 'text',
+ },
+ {
+ name: 'calendarId',
+ label: 'Calendar ID',
+ description: 'Nextcloud calendar ID for authenticated calendars.',
+ type: 'text',
+ },
+ {
+ name: 'username',
+ label: 'Username',
+ description: 'Optional. Only required for private Nextcloud calendars that need authentication.',
+ type: 'text',
+ },
+ {
+ name: 'password',
+ label: 'Password',
+ description: 'Optional. Use only with authenticated Nextcloud calendar access (app password or account password).',
+ type: 'password',
+ },
+ {
+ name: 'pastDays',
+ label: 'Include recent past days',
+ description: 'Number of past days to show. Set 0 to show only future events.',
+ type: 'number',
+ },
+ {
+ name: 'limit',
+ label: 'Event count',
+ description: 'Maximum upcoming events to display.',
+ type: 'number',
+ },
+ {
+ name: 'taskLimit',
+ label: 'Task count',
+ description: 'Maximum tasks to display.',
+ type: 'number',
+ },
+ ],
+};
+
+const getWidgetOptionDefinitions = (widgetType) => widgetOptionDefinitions[(widgetType || '').toLowerCase()] || [];
+
export default {
name: 'EditWidget',
components: {
@@ -127,6 +204,9 @@ export default {
},
computed: {
allowViewConfig() { return this.$store.getters.permissions.allowViewConfig; },
+ currentOptionDefinitions() {
+ return getWidgetOptionDefinitions(this.form.type);
+ },
},
/* Populate form before children render so Radio's `initialOption` is set on its first creation. */
created() {
@@ -140,11 +220,20 @@ export default {
timeout: widget.timeout ?? '',
useProxy: widget.useProxy === undefined ? '' : String(widget.useProxy),
ignoreErrors: widget.ignoreErrors === undefined ? '' : String(widget.ignoreErrors),
+ options: Object.fromEntries(
+ Object.entries(widget.options || {}).map(([key, value]) => [
+ key,
+ typeof value === 'object' ? JSON.stringify(value) : String(value),
+ ]),
+ ),
};
- this.optionRows = Object.entries(widget.options || {}).map(([key, value]) => ({
- key,
- value: typeof value === 'object' ? JSON.stringify(value) : String(value),
- }));
+ const reservedKeys = getWidgetOptionDefinitions(widget.type || this.form.type).map((field) => field.name);
+ this.optionRows = Object.entries(widget.options || {})
+ .filter(([key]) => !reservedKeys.includes(key))
+ .map(([key, value]) => ({
+ key,
+ value: typeof value === 'object' ? JSON.stringify(value) : String(value),
+ }));
},
mounted() {
this.$modal.show(this.modalName);
@@ -171,6 +260,11 @@ export default {
if (this.form.ignoreErrors === 'true') widget.ignoreErrors = true;
else if (this.form.ignoreErrors === 'false') widget.ignoreErrors = false;
const options = {};
+ Object.entries(this.form.options || {}).forEach(([key, value]) => {
+ if (value !== '' && value !== null && value !== undefined) {
+ options[key] = coerceValue(value);
+ }
+ });
this.optionRows.forEach(({ key, value }) => {
const k = (key || '').trim();
if (k) options[k] = coerceValue(value);
@@ -202,6 +296,11 @@ export default {
this.$toast.error('Error saving widget. See Logs.');
}
},
+ boolToStr(value) {
+ if (value === true || value === 'true') return 'true';
+ if (value === false || value === 'false') return 'false';
+ return '';
+ },
closeModal() {
this.$modal.hide(this.modalName);
},
diff --git a/src/components/Widgets/NextcloudCalendar.vue b/src/components/Widgets/NextcloudCalendar.vue
new file mode 100644
index 0000000000..dbc8d60d34
--- /dev/null
+++ b/src/components/Widgets/NextcloudCalendar.vue
@@ -0,0 +1,449 @@
+
+
+
+
+
+
+
diff --git a/src/components/Widgets/WidgetBase.vue b/src/components/Widgets/WidgetBase.vue
index 34144e68b8..0c1fcb186e 100644
--- a/src/components/Widgets/WidgetBase.vue
+++ b/src/components/Widgets/WidgetBase.vue
@@ -113,6 +113,7 @@ const COMPAT = {
'nd-ram-history': 'NdRamHistory',
'news-headlines': 'NewsHeadlines',
'nextcloud-notifications': 'NextcloudNotifications',
+ 'nextcloud-calendar': 'NextcloudCalendar',
'nextcloud-php-opcache': 'NextcloudPhpOpcache',
'nextcloud-stats': 'NextcloudStats',
'nextcloud-system': 'NextcloudSystem',
diff --git a/user-data/conf.yml b/user-data/conf.yml
index 01cf86ddd4..b9f12ea550 100644
--- a/user-data/conf.yml
+++ b/user-data/conf.yml
@@ -56,3 +56,16 @@ sections:
options:
rssUrl: https://github.com/lissy93/dashy/releases.atom
limit: 5
+
+ - name: Nextcloud
+ icon: fas fa-calendar-alt
+ widgets:
+ - type: nextcloud-calendar
+ label: Nextcloud Calendar (public)
+ options:
+ icsUrl: https://example.com/your-calendar.ics
+ useProxy: true
+ pastDays: 0
+ showTasks: true
+ limit: 5
+ taskLimit: 20