mirror of https://github.com/glanceapp/glance.git
Widget Vikunja
parent
096b6f4a46
commit
c96b88e246
@ -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;
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
Loading…
Reference in New Issue