major improvement to easylist for usability on mobile devices

Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
This commit is contained in:
2025-11-29 00:43:58 +01:00
parent 600b0f2cf4
commit 8f82ca87b4
11 changed files with 119 additions and 91 deletions

View File

@@ -20,11 +20,23 @@
let x = 0; let x = 0;
let y = 0; let y = 0;
function byName(a,b){ function byName(a,b){
return a.name.localeCompare(b.name); return a.name.localeCompare(b.name);
} }
function extend(e,task){
e.preventDefault();
e.stopPropagation();
highlight = task;
return false;
}
function getTask(evt){
var link = evt.target;
var id = link.getAttribute('task_id');
return tasks[id];
}
function goTag(e,newTag){ function goTag(e,newTag){
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@@ -33,6 +45,12 @@
load(); load();
} }
function ignore(evt){
evt.preventDefault();
evt.stopPropagation();
return false;
}
async function load(){ async function load(){
const url = api(`task/tagged/${tag}`); const url = api(`task/tagged/${tag}`);
const res = await get(url); const res = await get(url);
@@ -43,57 +61,17 @@
} else error(res); } else error(res);
} }
function noNoIndex(task){ function match(task){
return !task.no_index; if (!search) return true;
} if (task.name.toLowerCase().includes(search)) return true;
if (task.tags){
function ignore(evt){ for (let tag of task.tags){
evt.preventDefault(); if (tag.toLowerCase().includes(search)) return true;
evt.stopPropagation(); }
}
return false; return false;
} }
function extend(e,task){
e.preventDefault();
e.stopPropagation();
highlight = task;
return false;
}
function getTask(evt){
var link = evt.target;
var id = link.getAttribute('task_id');
return tasks[id];
}
function onclick(evt) {
let task = getTask(evt);
if (task.status <= 20) { // open
update(task,60);
} else update(task,20);
return ignore(evt);
}
function oncontextmenu(evt) {
highlight = getTask(evt);
return ignore(evt);
}
function ontouchstart(evt){
start = evt.timeStamp;
x = evt.touches[0].clientX;
y = evt.touches[0].clientY;
return ignore(evt);
}
function ontouchend(evt){
let d = Math.abs(x - evt.changedTouches[0].clientX) + Math.abs(y - evt.changedTouches[0].clientY);
measured(evt, evt.timeStamp - start, d);
return ignore(evt);
}
function measured(evt,duration,d){ function measured(evt,duration,d){
if (d > 100) return; if (d > 100) return;
if (duration < 500){ if (duration < 500){
@@ -103,6 +81,41 @@
} }
} }
function noNoIndex(task){
return !task.no_index;
}
function onclick(evt) {
ignore(evt);
let task = getTask(evt);
if (task.status <= 20) { // open
update(task,60);
} else update(task,20);
return false;
}
function oncontextmenu(evt) {
ignore(evt);
highlight = getTask(evt);
return false;
}
function ontouchstart(evt){
ignore(evt);
start = evt.timeStamp;
x = evt.touches[0].clientX;
y = evt.touches[0].clientY;
return false;
}
function ontouchend(evt){
ignore(evt);
let d = Math.abs(x - evt.changedTouches[0].clientX) + Math.abs(y - evt.changedTouches[0].clientY);
measured(evt, evt.timeStamp - start, d);
return false;
}
async function update(task,newState){ async function update(task,newState){
highlight = null; highlight = null;
const url = api(`task/${task.id}`); const url = api(`task/${task.id}`);
@@ -111,25 +124,23 @@
task.status = newState; task.status = newState;
yikes(); yikes();
// filter = null; // not sure what is better, resetting or keeping // filter = null; // not sure what is better, resetting or keeping
input.focus();
} else error(res); } else error(res);
} }
onMount(load); onMount(load);
</script> </script>
<h2>{tag}</h2> <h2>{t('tasks_for_tag',{tag:decodeURI(tag)})}</h2>
<div class="easylist"> <div class="easylist">
<fieldset class="open"> <fieldset class="open">
<legend>{t('state_open')}</legend> <legend>{t('state_open')}</legend>
{#if sorted} {#if sorted}
{#each sorted as task} {#each sorted as task}
{#if task.status == 20 && (!filter || task.name.toLowerCase().includes(search))} {#if task.status == 20 && match(task)}
<a href={`/task/${task.id}/view`} title={task.description.source} task_id={task.id} {onclick} {oncontextmenu} {ontouchstart} {ontouchend} > <div href={`/task/${task.id}/view`} title={task.description.source} task_id={task.id} {onclick} {oncontextmenu} {ontouchstart} {ontouchend} onmousedown={ontouchstart} onmouseup={ontouchend} >
{task.name} {task.name}
</a> </div>
{#if highlight == task} {#if highlight == task}
<Detail task={task} goTag={goTag} /> <Detail task={task} goTag={goTag} />
{/if} {/if}
@@ -142,10 +153,10 @@
<legend>{t('state_complete')}</legend> <legend>{t('state_complete')}</legend>
{#if sorted} {#if sorted}
{#each sorted as task} {#each sorted as task}
{#if task.status > 20 && (!filter || task.name.toLowerCase().includes(search))} {#if task.status > 20 && match(task)}
<a href={`/task/${task.id}/view`} title={task.description.source} task_id={task.id} {onclick} {oncontextmenu} {ontouchstart} {ontouchend} > <div href={`/task/${task.id}/view`} title={task.description.source} task_id={task.id} {onclick} {oncontextmenu} {ontouchstart} {ontouchend} onmousedown={ontouchstart} onmouseup={ontouchend} >
{task.name} {task.name}
</a> </div>
{#if highlight == task} {#if highlight == task}
<Detail task={task} goTag={goTag} /> <Detail task={task} goTag={goTag} />
{/if} {/if}

View File

@@ -1,26 +1,20 @@
<script> <script>
import { onMount } from 'svelte'; import { useTinyRouter } from 'svelte-tiny-router';
import { api, get, patch } from '../../urls.svelte';
import { error, yikes } from '../../warn.svelte';
import { t } from '../../translations.svelte'; import { t } from '../../translations.svelte';
let { goTag, task } = $props(); let { goTag, task } = $props();
let router = useTinyRouter();
async function load(){ function onclick(){
const url = api(`tags/task/${task.id}`); router.navigate(`/task/${task.id}/edit`);
const res = await get(url);
if (res.ok){
yikes();
task.tags = await res.json();
} else error(res);
} }
onMount(load);
</script> </script>
<button class="edit" {onclick}>{t('edit')}</button>
{@html task.description.rendered} {@html task.description.rendered}
{#if task.tags} {#if task.tags}
{t('other_tags')}: {t('other_tags')}:<br/>
{#each task.tags as tag} {#each task.tags as tag}
<button onclick={e => goTag(e,tag)}>{tag}</button> <button onclick={e => goTag(e,tag)}>{tag}</button>
{/each} {/each}

View File

@@ -14,6 +14,8 @@ import static de.srsoftware.umbrella.core.model.Permission.OWNER;
import static de.srsoftware.umbrella.project.Constants.PERMISSIONS; import static de.srsoftware.umbrella.project.Constants.PERMISSIONS;
import static de.srsoftware.umbrella.task.Constants.*; import static de.srsoftware.umbrella.task.Constants.*;
import static java.lang.System.Logger.Level.WARNING; import static java.lang.System.Logger.Level.WARNING;
import static java.net.URLDecoder.decode;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpExchange;
import de.srsoftware.configuration.Configuration; import de.srsoftware.configuration.Configuration;
@@ -180,11 +182,16 @@ public class TaskModule extends BaseHandler implements TaskService {
} }
private boolean getTaggedTasks(Path path, UmbrellaUser user, HttpExchange ex) throws IOException { private boolean getTaggedTasks(Path path, UmbrellaUser user, HttpExchange ex) throws IOException {
var tag = path.toString(); var tag = decode(path.toString(), UTF_8);
var tags = tagService().getTagUses(user,tag); var tags = tagService().getTagUses(user,tag);
var taskIds = nullable(tags.get(TASK)).orElseGet(ArrayList::new); var taskIds = nullable(tags.get(TASK)).orElseGet(ArrayList::new);
var tasks = taskDb.load(taskIds); var tasks = mapValues(taskDb.load(taskIds));
return sendContent(ex, mapValues(tasks)); var taskTags = tagService().getTags(TASK,taskIds,user);
for (var entry : tasks.entrySet()){
var list = taskTags.get(entry.getKey());
entry.getValue().put(TAGS,list==null?List.of():list);
}
return sendContent(ex, tasks);
} }
private boolean getTask(HttpExchange ex, long taskId, UmbrellaUser user) throws IOException { private boolean getTask(HttpExchange ex, long taskId, UmbrellaUser user) throws IOException {

View File

@@ -191,6 +191,7 @@
"oidc_Login" : "Anmeldung mit OIDC", "oidc_Login" : "Anmeldung mit OIDC",
"old_password": "altes Passwort", "old_password": "altes Passwort",
"organization": "Organisation", "organization": "Organisation",
"other_tags": "andere Tags",
"page": "Seite", "page": "Seite",
"parent_task": "übergeordnete Aufgabe", "parent_task": "übergeordnete Aufgabe",
@@ -278,6 +279,7 @@
"task": "Aufgabe", "task": "Aufgabe",
"task_list": "Aufgabenliste", "task_list": "Aufgabenliste",
"tasks": "Aufgaben", "tasks": "Aufgaben",
"tasks_for_tag": "Aufgaben mit Tag „{tag}“",
"tax_id": "Steuernummer", "tax_id": "Steuernummer",
"TAX-NUMBER": "Steuernummer", "TAX-NUMBER": "Steuernummer",
"tax_rate": "Steuersatz", "tax_rate": "Steuersatz",

View File

@@ -191,6 +191,7 @@
"oidc_Login" : "Login via OIDC", "oidc_Login" : "Login via OIDC",
"old_password": "old password", "old_password": "old password",
"organization": "organization", "organization": "organization",
"other_tags": "other tags",
"page": "page", "page": "page",
"parent_task": "parent task", "parent_task": "parent task",
@@ -278,6 +279,7 @@
"task": "task", "task": "task",
"task_list": "task list", "task_list": "task list",
"tasks": "tasks", "tasks": "tasks",
"tasks_for_tag": "tasks with tag „{tag}“",
"tax_id": "tax ID", "tax_id": "tax ID",
"TAX-NUMBER": "tax ID", "TAX-NUMBER": "tax ID",
"tax_rate": "tax rate", "tax_rate": "tax rate",

View File

@@ -296,8 +296,8 @@ tr:hover .taglist .tag button {
color: #222200; color: #222200;
} }
.easylist a { .easylist > fieldset > div {
border-color:orange; border-color: orange;
color: orange; color: orange;
} }
.easylist fieldset { .easylist fieldset {

View File

@@ -407,20 +407,25 @@ a.wikilink{
grid-column-end: span 2; grid-column-end: span 2;
} }
.easylist a { .easylist > fieldset > div {
display: block; display: block;
border: 1px solid; border: 1px solid;
margin: 7px; margin: 7px;
padding: 5px; padding: 5px;
border-radius: 5px; border-radius: 5px;
text-align: center; text-align: center;
user-select: none;
} }
.easylist .filter{ .easylist .filter{
position: sticky; position: sticky;
top: 40px; bottom: 22px;
z-index: 10; z-index: 10;
} }
.easylist .edit{
float: right;
}
@media screen and (max-width: 600px) { @media screen and (max-width: 600px) {
.grid2{ .grid2{
display: grid; display: grid;
@@ -443,15 +448,13 @@ a.wikilink{
min-height: 50px; min-height: 50px;
} }
.easylist a { .easylist > fieldset > div {
font-size: 25px; font-size: 25px;
padding: 10px;
} }
.easylist input{ .easylist input{
font-size: 20px; font-size: 20px;
} }
.easylist .filter{
top: 95px;
}
} }
fieldset.vcard{ fieldset.vcard{

View File

@@ -286,7 +286,7 @@ tr:hover .taglist .tag button {
color: #222200; color: #222200;
} }
.easylist a { .easylist > fieldset > div {
border-color: orange; border-color: orange;
color: orange; color: orange;
} }

View File

@@ -485,13 +485,14 @@ a.wikilink{
grid-column-end: span 2; grid-column-end: span 2;
} }
.easylist a { .easylist > fieldset > div {
display: block; display: block;
border: 1px solid; border: 1px solid;
margin: 7px; margin: 7px;
padding: 5px; padding: 5px;
border-radius: 5px; border-radius: 5px;
text-align: center; text-align: center;
user-select: none;
} }
.easylist .filter{ .easylist .filter{
position: sticky; position: sticky;
@@ -499,6 +500,10 @@ a.wikilink{
z-index: 10; z-index: 10;
} }
.easylist .edit{
float: right;
}
@media screen and (max-width: 600px) { @media screen and (max-width: 600px) {
.grid2{ .grid2{
display: grid; display: grid;
@@ -521,8 +526,9 @@ a.wikilink{
min-height: 50px; min-height: 50px;
} }
.easylist a { .easylist > fieldset > div {
font-size: 25px; font-size: 25px;
padding: 10px;
} }
.easylist input{ .easylist input{
font-size: 20px; font-size: 20px;

View File

@@ -274,7 +274,7 @@ tr:hover .taglist .tag button {
color: #bbb; color: #bbb;
} }
.easylist a { .easylist > fieldset > div {
border-color: blue; border-color: blue;
color: blue; color: blue;
background: #dfe4ff; background: #dfe4ff;

View File

@@ -407,20 +407,25 @@ a.wikilink{
grid-column-end: span 2; grid-column-end: span 2;
} }
.easylist a { .easylist > fieldset > div {
display: block; display: block;
border: 1px solid; border: 1px solid;
margin: 7px; margin: 7px;
padding: 5px; padding: 5px;
border-radius: 5px; border-radius: 5px;
text-align: center; text-align: center;
user-select: none;
} }
.easylist .filter{ .easylist .filter{
position: sticky; position: sticky;
top: 40px; bottom: 22px;
z-index: 10; z-index: 10;
} }
.easylist .edit{
float: right;
}
@media screen and (max-width: 600px) { @media screen and (max-width: 600px) {
.grid2{ .grid2{
display: grid; display: grid;
@@ -443,15 +448,13 @@ a.wikilink{
min-height: 50px; min-height: 50px;
} }
.easylist a { .easylist > fieldset > div {
font-size: 25px; font-size: 25px;
padding: 10px;
} }
.easylist input{ .easylist input{
font-size: 20px; font-size: 20px;
} }
.easylist .filter{
top: 95px;
}
} }
fieldset.vcard{ fieldset.vcard{