Added Miniflux custom service

Co-authored-by: Moritz Kreutzer
Co-authored-by: Reiko Kaps 
Co-authored-by: igorkulman
This commit is contained in:
Bastien Wirtz
2025-11-16 11:49:01 +01:00
committed by GitHub
parent d1356c3e6a
commit 2a27bee30e
3 changed files with 213 additions and 0 deletions

View File

@@ -32,6 +32,7 @@ Available services are located in `src/components/`:
- [Matrix](#matrix)
- [Mealie](#mealie)
- [Medusa](#medusa)
- [Miniflux](#miniflux)
- [Nextcloud](#nextcloud)
- [OctoPrint / Moonraker](#octoprintmoonraker)
- [Olivetin](#olivetin)
@@ -389,6 +390,22 @@ The url must be the root url of Medusa application.
**API Key**: The Medusa API key can be found in General configuration > Interface. It is needed to access Medusa API.
## Miniflux
Displays the number of unread articles from your Miniflux RSS reader.
```yaml
- name: "Miniflux"
type: "Miniflux"
logo: "assets/tools/sample.png"
url: https://my-service.url
apikey: "<---insert-api-key-here--->"
style: "status" # Either "status" or "counter"
checkInterval: 60000 # Optional: Interval (in ms) for updating the unread count
```
**API Key**: Generate an API key in Miniflux web interface under **Settings > API Keys > Create a new API key**
## Nextcloud
Displays Nextcloud version and shows if Nextcloud is online, offline, or in [maintenance

View File

@@ -0,0 +1,36 @@
{
"total": 42,
"entries": [
{
"id": 888,
"user_id": 1,
"feed_id": 42,
"title": "Example Unread Entry",
"url": "http://example.org/article.html",
"comments_url": "",
"author": "John Doe",
"content": "<p>This is an unread RSS entry</p>",
"hash": "29f99e4074cdacca1766f47697d03c66070ef6a14770a1fd5a867483c207a1bb",
"published_at": "2025-11-11T16:15:19Z",
"created_at": "2025-11-11T16:15:19Z",
"status": "unread",
"share_code": "",
"starred": false,
"reading_time": 5,
"enclosures": null,
"feed": {
"id": 42,
"user_id": 1,
"title": "Tech Blog",
"site_url": "http://example.org",
"feed_url": "http://example.org/feed.atom",
"checked_at": "2025-11-11T21:06:03.133839Z",
"category": {
"id": 22,
"user_id": 1,
"title": "Technology"
}
}
}
]
}

View File

@@ -0,0 +1,160 @@
<template>
<Generic :item="item">
<template #content>
<p class="title is-4">{{ item.name }}</p>
<p class="subtitle is-6">
<template v-if="item.subtitle"> {{ item.subtitle }} </template>
<template v-else-if="unreadEntries">
<template v-if="unreadFeeds < 2">
{{ unreadEntries }} unread
</template>
<template v-else>
{{ unreadEntries }} unread in {{ unreadFeeds }} feeds
</template>
</template>
</p>
</template>
<template #indicator>
<i v-if="loading" class="fa fa-circle-notch fa-spin"></i>
<div v-else-if="style == 'status'" class="status" :class="statusClass">
{{ status }}
</div>
<div v-else class="notifs">
<strong v-if="unreadEntries > 0" class="notif unread" title="Unread">
{{ unreadEntries }}
</strong>
<strong
v-if="!isHealthy"
class="notif errors"
title="Connection error to Miniflux API, check url and apikey in config.yml"
>
?
</strong>
</div>
</template>
</Generic>
</template>
<script>
import service from "@/mixins/service.js";
export default {
name: "Miniflux",
mixins: [service],
props: {
item: Object,
},
data: () => ({
unreadEntries: 0,
unreadFeeds: 0,
isHealthy: false,
loading: true,
style: "status",
}),
computed: {
status: function () {
if (!this.isHealthy) {
return "Error";
}
return this.unreadEntries > 0 ? "Unread" : "Online";
},
statusClass: function () {
return this.status.toLowerCase();
},
},
created() {
const checkInterval = parseInt(this.item.checkInterval, 10) || 0;
if (checkInterval > 0) {
setInterval(() => this.fetchConfig(), checkInterval);
}
this.fetchStatus();
},
methods: {
fetchStatus: async function () {
const headers = {
"X-Auth-Token": this.item.apikey,
};
let counters;
try {
counters = await this.fetch("/v1/feeds/counters", { headers });
this.isHealthy = true;
} catch (e) {
console.log(e);
} finally {
this.loading = false;
}
if (!this.isHealthy) {
return;
}
const unreads = Object.values(counters.unreads || {});
this.unreadFeeds = unreads.length;
this.unreadEntries = unreads.reduce((accumulator, value) => {
return accumulator + value;
}, 0);
},
},
};
</script>
<style scoped lang="scss">
.status {
font-size: 0.8rem;
color: var(--text-title);
&.online:before {
background-color: #94e185;
border-color: #78d965;
box-shadow: 0 0 5px 1px #94e185;
}
&.unread:before {
background-color: #1774ff;
border-color: #1774ff;
box-shadow: 0 0 5px 1px #1774ff;
}
&.error:before {
background-color: #c9404d;
border-color: #c42c3b;
box-shadow: 0 0 5px 1px #c9404d;
}
&:before {
content: " ";
display: inline-block;
width: 7px;
height: 7px;
margin-right: 10px;
border: 1px solid #000;
border-radius: 7px;
}
}
.notifs {
position: absolute;
color: white;
font-family: sans-serif;
top: 0.3em;
right: 0.5em;
.notif {
display: inline-block;
padding: 0.2em 0.35em;
border-radius: 0.25em;
position: relative;
margin-left: 0.3em;
font-size: 0.8em;
&.unread {
background-color: #4fb5d6;
}
&.errors {
background-color: #e51111;
}
}
}
</style>