diff --git a/src/taskflower/db/model/task.py b/src/taskflower/db/model/task.py index 8fc3be4..866959e 100644 --- a/src/taskflower/db/model/task.py +++ b/src/taskflower/db/model/task.py @@ -14,6 +14,6 @@ class Task(db.Model): due: Mapped[datetime] = mapped_column(DateTime(timezone=False)) created: Mapped[datetime] = mapped_column(DateTime(timezone=False), default=now) complete: Mapped[bool] = mapped_column(Boolean, default=False) - completed: Mapped[int] = mapped_column(DateTime(timezone=False), nullable=True) + completed: Mapped[datetime|None] = mapped_column(DateTime(timezone=False), nullable=True) namespace: Mapped[int] = mapped_column(Integer, ForeignKey('namespace.id', ondelete='CASCADE')) owner: Mapped[int] = mapped_column(Integer, ForeignKey('user.id', ondelete='CASCADE')) \ No newline at end of file diff --git a/src/taskflower/sanitize/task.py b/src/taskflower/sanitize/task.py index 6e17afb..6ae04ca 100644 --- a/src/taskflower/sanitize/task.py +++ b/src/taskflower/sanitize/task.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from datetime import datetime, timezone +from datetime import datetime, timedelta, timezone from typing import Self import humanize @@ -34,6 +34,8 @@ class TaskForUser: due_rel: str created: datetime complete: bool + completed: datetime|None + overdue: bool namespace_id: int can_edit: bool @@ -41,6 +43,33 @@ class TaskForUser: can_complete: bool can_uncomplete: bool + @property + def delay_style(self) -> str: + ''' Returns an appropriate 'animation-delay' property for + recently-completed tasks; otherwise an empty string. + Intended for use in jinja templating + ''' + if self.just_complete and self.completed is not None: + delta = now() - self.completed + return f'animation-delay: {-1*delta.seconds}s;' + else: + return '' + + @property + def just_complete(self) -> bool: + ''' Returns whether the task was recently finished (within 5 minutes). + + Used for jinja templates. + ''' + return ( + self.complete + and (self.completed is not None) + and ( + (now() - self.completed) + < timedelta(minutes=5) + ) + ) + @classmethod def from_task( cls, @@ -71,6 +100,12 @@ class TaskForUser: _due_str(tsk.due), tsk.created, tsk.complete, + ( + tsk.completed.replace(tzinfo=timezone.utc) + if tsk.completed is not None + else None + ), + tsk.due.replace(tzinfo=timezone.utc) < now(), tsk.namespace, NPT.EDIT_ALL_TASKS in perms, NPT.DELETE_ALL_TASKS in perms, diff --git a/src/taskflower/static/list-view.css b/src/taskflower/static/list-view.css index 63c9baa..a663391 100644 --- a/src/taskflower/static/list-view.css +++ b/src/taskflower/static/list-view.css @@ -1,31 +1,160 @@ - .list{ - width: 100%; +@keyframes task-reward-fade-even { + 0% { + background-color : var(--block-grey-bg); + color : var(--block-grey-text); + fill : var(--block-grey-text); + stroke : var(--block-grey-text); + } + 0.5% { + background-color : var(--block-good-bg); + color : var(--block-good-text); + fill : var(--block-good-text); + stroke : var(--block-good-text); + } + 80% { + background-color : var(--block-good-bg); + color : var(--block-good-text); + fill : var(--block-good-text); + stroke : var(--block-good-text); + } + 100% { + background-color : var(--block-grey-bg); + color : var(--block-grey-text); + fill : var(--block-grey-text); + stroke : var(--block-grey-text); + } +} + +@keyframes task-reward-fade-odd { + 0% { + background-color : var(--block-grey-2-bg); + color : var(--block-grey-2-text); + fill : var(--block-grey-2-text); + stroke : var(--block-grey-2-text); + } + 0.5% { + background-color : var(--block-good-2-bg); + color : var(--block-good-2-text); + fill : var(--block-good-2-text); + stroke : var(--block-good-2-text); + } + 80% { + background-color : var(--block-good-2-bg); + color : var(--block-good-2-text); + fill : var(--block-good-2-text); + stroke : var(--block-good-2-text); + } + 100% { + background-color : var(--block-grey-2-bg); + color : var(--block-grey-2-text); + fill : var(--block-grey-2-text); + stroke : var(--block-grey-2-text); + } +} + + +.list{ + width: 100%; + + --table-row-even-bg : var(--block-2-bg); + --table-row-even-text : var(--block-2-text); + --table-row-even-border-width: var(--block-2-border-width); + --table-row-even-border-color: var(--block-2-border-color); + --table-row-odd-bg : var(--block-3-bg); + --table-row-odd-text : var(--block-3-text); + --table-row-odd-border-width: var(--block-3-border-width); + --table-row-odd-border-color: var(--block-3-border-color); + + tr { + + --table-row-bg : var(--table-row-even-bg); + --table-row-text : var(--table-row-even-text); + --table-row-border-width : var(--table-row-even-border-width); + --table-row-border-color : var(--table-row-even-border-color); + + + background-color: var(--table-row-bg); + color: var(--table-row-text); + border: var(--table-row-border-width) solid var(--table-row-border-color); + transition: background-color 2s; + transition: color 2s; + transition: border 2s; + transition: all 2s; + + .icon svg { + fill: var(--table-row-text); + stroke: var(--table-row-text); + transition: fill 2s; + transition: stroke 2s; + } + + &:nth-child(odd) { + --table-row-bg : var(--table-row-odd-bg); + --table-row-text : var(--table-row-odd-text); + --table-row-border-width : var(--table-row-odd-border-width); + --table-row-border-color : var(--table-row-odd-border-color); + } + + &.overdue { + --table-row-even-bg : var(--block-bad-bg); + --table-row-even-text : var(--block-bad-text); + --table-row-even-border-width: var(--block-bad-border-width); + --table-row-even-border-color: var(--block-bad-border-color); + --table-row-odd-bg : var(--block-bad-2-bg); + --table-row-odd-text : var(--block-bad-2-text); + --table-row-odd-border-width: var(--block-bad-2-border-width); + --table-row-odd-border-color: var(--block-bad-2-border-color); + } + + &.complete { + --table-row-even-bg : var(--block-grey-bg); + --table-row-even-text : var(--block-grey-text); + --table-row-even-border-width : var(--block-grey-border-width); + --table-row-even-border-color : var(--block-grey-border-color); + --table-row-odd-bg : var(--block-grey-2-bg); + --table-row-odd-text : var(--block-grey-2-text); + --table-row-odd-border-width : var(--block-grey-2-border-width); + --table-row-odd-border-color : var(--block-grey-2-border-color); + + &.just-complete{ + animation-name: task-reward-fade-even; + animation-duration: 300s; + + .checkbox .icon svg { + animation-name: task-reward-fade-even; + animation-duration: 300s; + } + + &:nth-child(odd) { + animation-name: task-reward-fade-odd; + + .checkbox .icon svg { + animation-name: task-reward-fade-odd; + } + } + } + } } - table.list{ - border-collapse: collapse; + .task-table-row { + &.complete { + font-style: italic; + text-decoration: line-through; + } } - .list tr { - background-color: var(--table-row-bg-1); - color: var(--on-table-row); - } - - .list tr:nth-child(odd) { - background-color: var(--table-row-bg-2); - color: var(--on-table-row); - } - - .list .task-header-row { + .task-header-row { border-bottom: 8px solid var(--bg); } - .list td, .list th{ + td, th{ border-right: 4px solid var(--bg); border-left: 4px solid var(--bg); + border-top: var(--table-row-border-width) solid var(--table-row-border-color); + border-bottom: var(--table-row-border-width) solid var(--table-row-border-color); } - .list td { + td { padding-left: 1rem; &.nopad { @@ -37,77 +166,82 @@ } } - .list p { + p { margin: 0; } - .list .detail-view { + .detail-view { display: none; border-bottom: 4px solid var(--bg); border-top: 1px solid var(--bg); + + &.shown { + display: table-row; + } } - .list .detail-view-elem { + .detail-view-elem { padding: 1rem; + + .small-details { + font-size: small; + font-style: italic; + } + + hr { + color: var(--table-row-text); + } + + .detail-view-header { + display: flex; + + h1 { + margin: 0 1rem; + font-size: larger; + } + + &::before { + content: ""; + flex: 1 1; + background: repeating-linear-gradient( + 60deg, + + var(--table-row-text) 0, + var(--table-row-text) 0.25rem, + + transparent 0.35rem, + transparent 1.0rem, + + var(--table-row-text) 1.1rem + ); + width: 100%; + max-width: 2rem; + } + + &::after{ + content: ""; + flex: 1 1; + background: repeating-linear-gradient( + 60deg, + + var(--table-row-text) 0, + var(--table-row-text) 0.25rem, + + transparent 0.35rem, + transparent 1.0rem, + + var(--table-row-text) 1.1rem + ); + width: 100%; + } + } + + .main-description { + margin-top: 0.5rem; + } } - - .list .detail-view.shown { - display: table-row; - } +} - .list .detail-view-elem .small-details { - font-size: small; - font-style: italic; - } - - .list .detail-view-elem hr { - color: var(--on-table-row); - } - - .list .detail-view-elem .detail-view-header h1 { - margin: 0 1rem; - font-size: larger; - } - - .list .detail-view-elem .detail-view-header { - display: flex; - } - - .list .detail-view-elem .detail-view-header::before{ - content: ""; - flex: 1 1; - background: repeating-linear-gradient( - 60deg, - - var(--on-table-row) 0, - var(--on-table-row) 0.25rem, - - transparent 0.35rem, - transparent 1.0rem, - - var(--on-table-row) 1.1rem - ); - width: 100%; - max-width: 2rem; - } - - .list .detail-view-elem .detail-view-header::after{ - content: ""; - flex: 1 1; - background: repeating-linear-gradient( - 60deg, - - var(--on-table-row) 0, - var(--on-table-row) 0.25rem, - - transparent 0.35rem, - transparent 1.0rem, - - var(--on-table-row) 1.1rem - ); - width: 100%; - } - - .list .detail-view-elem .main-description { - margin-top: 0.5rem; - } \ No newline at end of file +table.list{ + border-collapse: collapse; +} \ No newline at end of file diff --git a/src/taskflower/static/style.css b/src/taskflower/static/style.css index 3763669..3e7e384 100644 --- a/src/taskflower/static/style.css +++ b/src/taskflower/static/style.css @@ -15,6 +15,52 @@ --body-font: sans-serif; --footer-font: sans-serif; + --block-2-bg: #331826; + --block-2-border-color: var(--block-2-bg); + --block-2-text: #ffc4d1; + --block-2-border-width: 0; + + --block-3-bg: #462837; + --block-3-border-color: var(--block-3-bg); + --block-3-text: #ffc4d1; + --block-3-border-width: 0; + + --block-neutral-border-width : 0; + --block-neutral-2-border-width : 0; + --block-neutral-bg : #182633; + --block-neutral-border-color : var(--block-neutral-bg); + --block-neutral-text : #c4ecff; + --block-neutral-2-bg : #283e46; + --block-neutral-2-border-color : var(--block-neutral-2-bg); + --block-neutral-2-text : #c4e0ff; + + --block-grey-border-width : 0; + --block-grey-2-border-width : 0; + --block-grey-bg : #262726; + --block-grey-border-color : var(--block-grey-bg); + --block-grey-text : #b6b6b6; + --block-grey-2-bg : #484948; + --block-grey-2-border-color : var(--block-grey-2-bg); + --block-grey-2-text : #a8a8a8; + + --block-good-border-width : 0; + --block-good-2-border-width : 0; + --block-good-bg : #003f1c; + --block-good-border-color : var(--block-good-bg); + --block-good-text : #c4ffce; + --block-good-2-bg : #005333; + --block-good-2-border-color : var(--block-good-2-bg); + --block-good-2-text : #c4ffce; + + --block-bad-border-width : 1px; + --block-bad-2-border-width : 1px; + --block-bad-bg : #360a0a; + --block-bad-border-color : #fd1c42; + --block-bad-text : #ff798f; + --block-bad-2-bg : #4b0d0d; + --block-bad-2-border-color : #fd1c42; + --block-bad-2-text : #ffa7b5; + --table-row-bg-1: #331826; --table-row-bg-2: #462837; --on-table-row: #ffc4d1; diff --git a/src/taskflower/static/time-utils.js b/src/taskflower/static/time-utils.js index ff4eb86..7df5794 100644 --- a/src/taskflower/static/time-utils.js +++ b/src/taskflower/static/time-utils.js @@ -153,6 +153,10 @@ function update_reltimes(){ ).forEach((el) => { const time = new Date(Number(el.dataset.timestamp)*1000) const delta = time - Date.now() + const tr = el.parentElement.parentElement.parentElement + if(delta < 0 && !tr.classList.contains('overdue')){ + tr.classList.add('overdue') + } el.textContent = sophontize(delta, time) }) } diff --git a/src/taskflower/templates/task/_shorttask.html b/src/taskflower/templates/task/_shorttask.html index 3efea56..d94df58 100644 --- a/src/taskflower/templates/task/_shorttask.html +++ b/src/taskflower/templates/task/_shorttask.html @@ -11,7 +11,7 @@ {% endmacro %} {% macro inline_task(task, list_id=0) %} -