Widget Vikunja

pull/878/head
Mord0reK 2025-11-16 15:21:29 +07:00 committed by GitHub
parent 096b6f4a46
commit c96b88e246
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 321 additions and 0 deletions

@ -0,0 +1,113 @@
.vikunja-table {
width: 100%;
border-collapse: collapse;
}
.vikunja-table thead {
border-bottom: 1px solid var(--color-separator);
}
.vikunja-table th {
text-align: left;
padding: 0.8rem 0.5rem;
color: var(--color-text-subdue);
font-weight: 500;
text-transform: uppercase;
/* font-size: 0.75rem; */
letter-spacing: 0.05em;
}
.vikunja-table tbody tr {
border-bottom: 1px solid var(--color-separator);
transition: background-color 0.2s;
}
.vikunja-table tbody tr:hover {
background-color: var(--color-widget-background-highlight);
}
.vikunja-table tbody tr:last-child {
border-bottom: none;
}
.vikunja-table td {
padding: 0.8rem 0.5rem;
vertical-align: middle;
}
.vikunja-time-left {
color: var(--color-text-base);
font-weight: 500;
white-space: nowrap;
}
.vikunja-title {
color: var(--color-primary);
font-weight: 400;
}
.vikunja-progress {
padding: 0.8rem 1rem;
}
.progress-container {
position: relative;
width: 100%;
height: 1.5rem;
background-color: var(--color-widget-background-highlight);
border-radius: 0.5rem;
overflow: hidden;
}
.progress-bar {
position: absolute;
left: 0;
top: 0;
height: 100%;
transition: width 0.3s ease;
border-radius: 0.5rem;
}
.progress-bar.progress-low {
background-color: #ef4444;
}
.progress-bar.progress-medium {
background-color: #f59e0b;
}
.progress-bar.progress-high {
background-color: #10b981;
}
.progress-text {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
font-size: 0.75rem;
font-weight: 600;
color: var(--color-text-base);
z-index: 1;
}
.vikunja-labels {
padding: 0.8rem 0.5rem;
}
.label-container {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
}
.vikunja-table .label {
display: inline-block;
padding: 0.25rem 0.6rem;
border-radius: 1rem;
font-size: 1.3rem;
font-weight: 500;
background-color: transparent;
border: 2px solid;
white-space: nowrap;
}

@ -16,6 +16,7 @@
@import "widget-videos.css";
@import "widget-weather.css";
@import "widget-todo.css";
@import "widget-vikunja.css";
@import "forum-posts.css";

@ -0,0 +1,40 @@
{{ template "widget-base.html" . }}
{{ define "widget-content" }}
{{ if .Tasks }}
<table class="vikunja-table">
<thead>
<tr>
<th>Koniec za</th>
<th>Treść zadania</th>
<th>Etykiety</th>
</tr>
</thead>
<tbody>
{{ range .Tasks }}
<tr>
<td class="vikunja-time-left">
{{ if .TimeLeft }}{{ .TimeLeft }}{{ else }}-{{ end }}
</td>
<td class="vikunja-title">{{ .Title }}</td>
<td class="vikunja-labels">
{{ if .Labels }}
<div class="label-container">
{{ range .Labels }}
<span class="label" style="border-color: {{ .Color }}; color: {{ .Color }};">{{ .Title }}</span>
{{ end }}
</div>
{{ else }}
-
{{ end }}
</td>
</tr>
{{ end }}
</tbody>
</table>
{{ else }}
<div class="flex items-center justify-center padding-block-5">
<p>Brak zadań do wykonania</p>
</div>
{{ end }}
{{ end }}

@ -0,0 +1,165 @@
package glance
import (
"context"
"fmt"
"html/template"
"net/http"
"time"
)
var vikunjaWidgetTemplate = mustParseTemplate("vikunja.html", "widget-base.html")
type vikunjaWidget struct {
widgetBase `yaml:",inline"`
URL string `yaml:"url"`
Token string `yaml:"token"`
Tasks []vikunjaTask
}
type vikunjaTask struct {
Title string
DueDate time.Time
Done bool
PercentDone int
Labels []vikunjaLabel
TimeLeft string
DueDateStr string
}
type vikunjaLabel struct {
Title string
Color string
}
type vikunjaAPITask struct {
ID int `json:"id"`
Title string `json:"title"`
Done bool `json:"done"`
DueDate string `json:"due_date"`
PercentDone float64 `json:"percent_done"`
Labels []vikunjaAPILabel `json:"labels"`
}
type vikunjaAPILabel struct {
Title string `json:"title"`
HexColor string `json:"hex_color"`
}
func (widget *vikunjaWidget) initialize() error {
widget.withTitle("Vikunja").withCacheDuration(5 * time.Minute)
if widget.URL == "" {
return fmt.Errorf("URL is required")
}
if widget.Token == "" {
return fmt.Errorf("token is required")
}
return nil
}
func (widget *vikunjaWidget) update(ctx context.Context) {
tasks, err := widget.fetchTasks()
if !widget.canContinueUpdateAfterHandlingErr(err) {
return
}
widget.Tasks = tasks
}
func (widget *vikunjaWidget) fetchTasks() ([]vikunjaTask, error) {
url := widget.URL + "/api/v1/tasks/all"
request, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
request.Header.Set("Authorization", "Bearer "+widget.Token)
apiTasks, err := decodeJsonFromRequest[[]vikunjaAPITask](defaultHTTPClient, request)
if err != nil {
return nil, err
}
tasks := make([]vikunjaTask, 0)
now := time.Now()
for _, apiTask := range apiTasks {
if apiTask.Done {
continue
}
task := vikunjaTask{
Title: apiTask.Title,
Done: apiTask.Done,
PercentDone: int(apiTask.PercentDone),
}
if apiTask.DueDate != "" {
dueDate, err := time.Parse(time.RFC3339, apiTask.DueDate)
if err == nil {
task.DueDate = dueDate
task.DueDateStr = dueDate.Format("2006-01-02 15:04")
task.TimeLeft = formatTimeLeft(now, dueDate)
}
}
task.Labels = make([]vikunjaLabel, len(apiTask.Labels))
for i, label := range apiTask.Labels {
color := label.HexColor
// Ensure the color has # prefix
if color != "" && color[0] != '#' {
color = "#" + color
}
task.Labels[i] = vikunjaLabel{
Title: label.Title,
Color: color,
}
}
tasks = append(tasks, task)
}
return tasks, nil
}
func formatTimeLeft(now, dueDate time.Time) string {
if dueDate.IsZero() {
return "-"
}
duration := dueDate.Sub(now)
if duration < 0 {
duration = -duration
days := int(duration.Hours() / 24)
hours := int(duration.Hours()) % 24
if days > 0 {
return fmt.Sprintf("-%d dni %d godz.", days, hours)
}
if hours > 0 {
return fmt.Sprintf("-%d godz.", hours)
}
return fmt.Sprintf("-%d min.", int(duration.Minutes()))
}
days := int(duration.Hours() / 24)
hours := int(duration.Hours()) % 24
if days > 0 {
return fmt.Sprintf("%d dni %d godz.", days, hours)
}
if hours > 0 {
return fmt.Sprintf("%d godz.", hours)
}
return fmt.Sprintf("%d min.", int(duration.Minutes()))
}
func (widget *vikunjaWidget) Render() template.HTML {
return widget.renderTemplate(widget, vikunjaWidgetTemplate)
}

@ -83,6 +83,8 @@ func newWidget(widgetType string) (widget, error) {
w = &todoWidget{}
case "radyjko":
w = &radyjkoWidget{}
case "vikunja":
w = &vikunjaWidget{}
default:
return nil, fmt.Errorf("unknown widget type: %s", widgetType)
}