implement auto reconnect

This commit is contained in:
marlon-erler
2026-04-01 08:29:39 +00:00
parent c52d7b7685
commit 9d400a9929
82 changed files with 8340 additions and 7641 deletions
+81 -72
View File
@@ -1,8 +1,8 @@
import * as React from "bloatless-react";
import {
PropertyValueList,
createPropertyValueIndexState,
PropertyValueList,
createPropertyValueIndexState,
} from "../Components/propertyValueList";
import BoardViewModel from "../../ViewModel/Pages/boardViewModel";
@@ -14,84 +14,93 @@ import { allowDrop } from "../utility";
import { translations } from "../translations";
export function BoardKanbanPage(boardViewModel: BoardViewModel) {
return PropertyValueList(
"category",
(taskViewModel: TaskViewModel) => taskViewModel.task,
boardViewModel.taskViewModels,
(
categories: React.ListState<string>,
sortedCategories: React.State<string[]>
) => {
const categoryNameConverter: React.StateItemConverter<string> = (
categoryName: string
) => {
const index: React.State<number> = createPropertyValueIndexState(
sortedCategories,
categoryName
);
return KanbanBoard(categoryName, index, boardViewModel);
};
return PropertyValueList(
"category",
(taskViewModel: TaskViewModel) => taskViewModel.task,
boardViewModel.taskViewModels,
(
categories: React.ListState<string>,
sortedCategories: React.State<string[]>,
) => {
const categoryNameConverter: React.StateItemConverter<string> = (
categoryName: string,
) => {
const index: React.State<number> =
createPropertyValueIndexState(
sortedCategories,
categoryName,
);
return KanbanBoard(categoryName, index, boardViewModel);
};
return (
<div
class="kanban-board-wrapper"
children:append={[categories, categoryNameConverter]}
></div>
);
}
);
return (
<div
class="kanban-board-wrapper"
children:append={[categories, categoryNameConverter]}
></div>
);
},
);
}
function KanbanBoard(
categoryName: string,
index: React.State<number>,
boardViewModel: BoardViewModel
categoryName: string,
index: React.State<number>,
boardViewModel: BoardViewModel,
) {
return FilteredList(
{ category: categoryName },
(taskViewModel: TaskViewModel) => taskViewModel.task,
boardViewModel.filteredTaskViewModels,
(taskViewModels: React.ListState<TaskViewModel>) => {
const viewModel: TaskCategoryBulkChangeViewModel =
new TaskCategoryBulkChangeViewModel(taskViewModels, categoryName);
return FilteredList(
{ category: categoryName },
(taskViewModel: TaskViewModel) => taskViewModel.task,
boardViewModel.filteredTaskViewModels,
(taskViewModels: React.ListState<TaskViewModel>) => {
const viewModel: TaskCategoryBulkChangeViewModel =
new TaskCategoryBulkChangeViewModel(
taskViewModels,
categoryName,
);
function drop() {
boardViewModel.handleDropWithinBoard(categoryName);
}
function drop() {
boardViewModel.handleDropWithinBoard(categoryName);
}
const view = (
<div class="flex-column flex-no" on:dragover={allowDrop} on:drop={drop}>
<div class="flex-row width-input">
<input
placeholder={
translations.chatPage.task.renameCategoryInputPlaceholder
}
bind:value={viewModel.inputValue}
on:enter={viewModel.set}
></input>
<button
class="primary"
on:click={viewModel.set}
toggle:disabled={viewModel.cannotSet}
>
<span class="icon">check</span>
</button>
</div>
const view = (
<div
class="flex-column flex-no"
on:dragover={allowDrop}
on:drop={drop}
>
<div class="flex-row width-input">
<input
placeholder={
translations.chatPage.task
.renameCategoryInputPlaceholder
}
bind:value={viewModel.inputValue}
on:enter={viewModel.set}
></input>
<button
class="primary"
on:click={viewModel.set}
toggle:disabled={viewModel.cannotSet}
>
<span class="icon">check</span>
</button>
</div>
<hr></hr>
<hr></hr>
<div
class="kanban-column"
children:append={[taskViewModels, TaskViewModelToEntry]}
></div>
</div>
);
<div
class="kanban-column"
children:append={[taskViewModels, TaskViewModelToEntry]}
></div>
</div>
);
index.subscribe((newIndex) => {
view.style.order = newIndex;
});
return view;
}
);
index.subscribe((newIndex) => {
view.style.order = newIndex;
});
return view;
},
);
}
+135 -115
View File
@@ -1,7 +1,7 @@
import * as React from "bloatless-react";
import BoardViewModel, {
BoardPageType,
BoardPageType,
} from "../../ViewModel/Pages/boardViewModel";
import { BoardKanbanPage } from "./boardKanbanPage";
@@ -14,120 +14,140 @@ import { TaskViewModelToEntry } from "../Components/taskEntry";
import { translations } from "../translations";
export function BoardPage(boardViewModel: BoardViewModel) {
boardViewModel.loadData();
boardViewModel.loadData();
// main content
const mainContent = React.createProxyState(
[boardViewModel.selectedPage],
() => {
switch (boardViewModel.selectedPage.value) {
case BoardPageType.Kanban: {
return BoardKanbanPage(boardViewModel);
}
case BoardPageType.StatusGrid: {
return BoardStatusGridPage(boardViewModel);
}
default: {
return (
<div
class="task-grid"
children:append={[
boardViewModel.filteredTaskViewModels,
// main content
const mainContent = React.createProxyState(
[boardViewModel.selectedPage],
() => {
switch (boardViewModel.selectedPage.value) {
case BoardPageType.Kanban: {
return BoardKanbanPage(boardViewModel);
}
case BoardPageType.StatusGrid: {
return BoardStatusGridPage(boardViewModel);
}
default: {
return (
<div
class="task-grid"
children:append={[
boardViewModel.filteredTaskViewModels,
TaskViewModelToEntry,
]}
></div>
);
}
}
},
);
// task modal
const taskSettingsModal = React.createProxyState(
[boardViewModel.selectedTaskViewModel],
() => {
if (boardViewModel.selectedTaskViewModel.value == undefined) {
return <div></div>;
} else {
return TaskSettingsModal(
boardViewModel.selectedTaskViewModel.value,
);
}
},
);
return (
<div class="pane">
<div class="toolbar">
<span>
<button
class="ghost board-close-button"
aria-label={
translations.chatPage.task
.closeBoardButtonAudioLabel
}
on:click={boardViewModel.close}
>
<span class="icon">arrow_back</span>
</button>
<button
class="ghost board-toggle-button inset-outline"
aria-label={
translations.chatPage.task
.toggleBoardButtonAudioLabel
}
on:click={
boardViewModel.taskPageViewModel.toggleBoardList
}
toggle:selected={
boardViewModel.taskPageViewModel.isShowingBoadList
}
>
<span class="icon">dock_to_right</span>
</button>
<button
class="ghost"
aria-label={
translations.chatPage.task.boardSettingsHeadline
}
on:click={boardViewModel.showSettings}
>
<span class="icon">settings</span>
</button>
</span>
<span class="scroll-h ribbon">
{BoardViewToggleButton(
translations.chatPage.task.listViewButtonAudioLabel,
"view_list",
BoardPageType.List,
boardViewModel,
)}
{BoardViewToggleButton(
translations.chatPage.task.kanbanViewButtonAudioLabel,
"view_kanban",
BoardPageType.Kanban,
boardViewModel,
)}
{BoardViewToggleButton(
translations.chatPage.task.statusViewButtonAudioLabel,
"grid_view",
BoardPageType.StatusGrid,
boardViewModel,
)}
</span>
<span>
<button
class="ghost"
aria-label={
translations.chatPage.task
.filterTasksButtonAudioLabel
}
on:click={boardViewModel.showFilterModal}
>
<span class="icon">filter_alt</span>
</button>
<button
class="ghost"
aria-label={
translations.chatPage.task
.createTaskButtonAudioLabel
}
on:click={boardViewModel.createTask}
>
<span class="icon">add</span>
</button>
</span>
</div>
<div class="content main-content" children:set={mainContent}></div>
{BoardSettingsModal(boardViewModel)}
{SearchModal(
boardViewModel.searchViewModel,
translations.chatPage.task.filterTasksHeadline,
TaskViewModelToEntry,
]}
></div>
);
}
}
}
);
// task modal
const taskSettingsModal = React.createProxyState(
[boardViewModel.selectedTaskViewModel],
() => {
if (boardViewModel.selectedTaskViewModel.value == undefined) {
return <div></div>;
} else {
return TaskSettingsModal(boardViewModel.selectedTaskViewModel.value);
}
}
);
return (
<div class="pane">
<div class="toolbar">
<span>
<button
class="ghost board-close-button"
aria-label={translations.chatPage.task.closeBoardButtonAudioLabel}
on:click={boardViewModel.close}
>
<span class="icon">arrow_back</span>
</button>
<button
class="ghost board-toggle-button inset-outline"
aria-label={translations.chatPage.task.toggleBoardButtonAudioLabel}
on:click={boardViewModel.taskPageViewModel.toggleBoardList}
toggle:selected={boardViewModel.taskPageViewModel.isShowingBoadList}
>
<span class="icon">dock_to_right</span>
</button>
<button
class="ghost"
aria-label={translations.chatPage.task.boardSettingsHeadline}
on:click={boardViewModel.showSettings}
>
<span class="icon">settings</span>
</button>
</span>
<span class="scroll-h ribbon">
{BoardViewToggleButton(
translations.chatPage.task.listViewButtonAudioLabel,
"view_list",
BoardPageType.List,
boardViewModel
)}
{BoardViewToggleButton(
translations.chatPage.task.kanbanViewButtonAudioLabel,
"view_kanban",
BoardPageType.Kanban,
boardViewModel
)}
{BoardViewToggleButton(
translations.chatPage.task.statusViewButtonAudioLabel,
"grid_view",
BoardPageType.StatusGrid,
boardViewModel
)}
</span>
<span>
<button
class="ghost"
aria-label={translations.chatPage.task.filterTasksButtonAudioLabel}
on:click={boardViewModel.showFilterModal}
>
<span class="icon">filter_alt</span>
</button>
<button
class="ghost"
aria-label={translations.chatPage.task.createTaskButtonAudioLabel}
on:click={boardViewModel.createTask}
>
<span class="icon">add</span>
</button>
</span>
</div>
<div class="content main-content" children:set={mainContent}></div>
{BoardSettingsModal(boardViewModel)}
{SearchModal(
boardViewModel.searchViewModel,
translations.chatPage.task.filterTasksHeadline,
TaskViewModelToEntry,
boardViewModel.isPresentingFilterModal
)}
<div children:set={taskSettingsModal}></div>
</div>
);
boardViewModel.isPresentingFilterModal,
)}
<div children:set={taskSettingsModal}></div>
</div>
);
}
+203 -188
View File
@@ -1,14 +1,14 @@
import * as React from "bloatless-react";
import {
PropertyValueList,
collectPropertyValuesToState,
createPropertyValueIndexState,
createSortedPropertyValueState,
PropertyValueList,
collectPropertyValuesToState,
createPropertyValueIndexState,
createSortedPropertyValueState,
} from "../Components/propertyValueList";
import {
TaskCategoryBulkChangeViewModel,
TaskStatusBulkChangeViewModel,
TaskCategoryBulkChangeViewModel,
TaskStatusBulkChangeViewModel,
} from "../../ViewModel/Utility/taskPropertyBulkChangeViewModel";
import BoardViewModel from "../../ViewModel/Pages/boardViewModel";
@@ -19,221 +19,236 @@ import { allowDrop } from "../utility";
import { translations } from "../translations";
export function BoardStatusGridPage(boardViewModel: BoardViewModel) {
const statuses: React.ListState<string> = new React.ListState();
const sortedStatuses: React.State<string[]> =
createSortedPropertyValueState(statuses);
const statuses: React.ListState<string> = new React.ListState();
const sortedStatuses: React.State<string[]> =
createSortedPropertyValueState(statuses);
boardViewModel.taskViewModels.subscribe(() => {
collectPropertyValuesToState(
"status",
(taskViewModel: TaskViewModel) => taskViewModel.task,
boardViewModel.taskViewModels,
statuses
);
});
boardViewModel.taskViewModels.subscribe(() => {
collectPropertyValuesToState(
"status",
(taskViewModel: TaskViewModel) => taskViewModel.task,
boardViewModel.taskViewModels,
statuses,
);
});
const statusNameCellConverter: React.StateItemConverter<string> = (
statusName: string
) => {
const index = createPropertyValueIndexState(sortedStatuses, statusName);
return StatusNameCell(statusName, index, boardViewModel);
};
const statusNameCellConverter: React.StateItemConverter<string> = (
statusName: string,
) => {
const index = createPropertyValueIndexState(sortedStatuses, statusName);
return StatusNameCell(statusName, index, boardViewModel);
};
return (
<div class="status-page-content">
<div
class="status-name-row"
children:append={[statuses, statusNameCellConverter]}
></div>
{PropertyValueList(
"category",
(taskViewModel: TaskViewModel) => taskViewModel.task,
boardViewModel.filteredTaskViewModels,
(
categories: React.ListState<string>,
sortedCategories: React.State<string[]>
) => {
const categoryRowConverter: React.StateItemConverter<string> = (
categoryName: string
) => {
const index: React.State<number> = createPropertyValueIndexState(
sortedCategories,
categoryName
);
return CategoryRow(
categoryName,
index,
statuses,
sortedStatuses,
boardViewModel
);
};
return (
return (
<div class="status-page-content">
<div
class="status-grid-wrapper"
children:append={[categories, categoryRowConverter]}
class="status-name-row"
children:append={[statuses, statusNameCellConverter]}
></div>
);
}
)}
</div>
);
{PropertyValueList(
"category",
(taskViewModel: TaskViewModel) => taskViewModel.task,
boardViewModel.filteredTaskViewModels,
(
categories: React.ListState<string>,
sortedCategories: React.State<string[]>,
) => {
const categoryRowConverter: React.StateItemConverter<
string
> = (categoryName: string) => {
const index: React.State<number> =
createPropertyValueIndexState(
sortedCategories,
categoryName,
);
return CategoryRow(
categoryName,
index,
statuses,
sortedStatuses,
boardViewModel,
);
};
return (
<div
class="status-grid-wrapper"
children:append={[categories, categoryRowConverter]}
></div>
);
},
)}
</div>
);
}
function StatusNameCell(
statusName: string,
index: React.State<number>,
boardViewModel: BoardViewModel
statusName: string,
index: React.State<number>,
boardViewModel: BoardViewModel,
) {
const taskViewModelsWithMatchingStatus: React.ListState<TaskViewModel> =
new React.ListState();
const taskViewModelsWithMatchingStatus: React.ListState<TaskViewModel> =
new React.ListState();
boardViewModel.filteredTaskViewModels.handleAddition(
(taskViewModel: TaskViewModel) => {
const doesMatchStatus: boolean = taskViewModel.task.status == statusName;
if (doesMatchStatus == false) return;
boardViewModel.filteredTaskViewModels.handleAddition(
(taskViewModel: TaskViewModel) => {
const doesMatchStatus: boolean =
taskViewModel.task.status == statusName;
if (doesMatchStatus == false) return;
taskViewModelsWithMatchingStatus.add(taskViewModel);
boardViewModel.filteredTaskViewModels.handleRemoval(taskViewModel, () => {
taskViewModelsWithMatchingStatus.remove(taskViewModel);
});
}
);
const viewModel: TaskStatusBulkChangeViewModel =
new TaskStatusBulkChangeViewModel(
taskViewModelsWithMatchingStatus,
statusName
taskViewModelsWithMatchingStatus.add(taskViewModel);
boardViewModel.filteredTaskViewModels.handleRemoval(
taskViewModel,
() => {
taskViewModelsWithMatchingStatus.remove(taskViewModel);
},
);
},
);
const view = (
<div class="flex-row">
<div class="property-input-wrapper">
<input
placeholder={
translations.chatPage.task.renameCategoryInputPlaceholder
}
bind:value={viewModel.inputValue}
on:enter={viewModel.set}
></input>
<button
class="primary"
on:click={viewModel.set}
toggle:disabled={viewModel.cannotSet}
>
<span class="icon">check</span>
</button>
</div>
</div>
);
const viewModel: TaskStatusBulkChangeViewModel =
new TaskStatusBulkChangeViewModel(
taskViewModelsWithMatchingStatus,
statusName,
);
index.subscribe((newIndex) => {
view.style.order = newIndex;
});
const view = (
<div class="flex-row">
<div class="property-input-wrapper">
<input
placeholder={
translations.chatPage.task
.renameCategoryInputPlaceholder
}
bind:value={viewModel.inputValue}
on:enter={viewModel.set}
></input>
<button
class="primary"
on:click={viewModel.set}
toggle:disabled={viewModel.cannotSet}
>
<span class="icon">check</span>
</button>
</div>
</div>
);
return view;
index.subscribe((newIndex) => {
view.style.order = newIndex;
});
return view;
}
function CategoryRow(
categoryName: string,
index: React.State<number>,
allStatuses: React.ListState<string>,
sortedStatuses: React.State<string[]>,
boardViewModel: BoardViewModel
categoryName: string,
index: React.State<number>,
allStatuses: React.ListState<string>,
sortedStatuses: React.State<string[]>,
boardViewModel: BoardViewModel,
) {
return FilteredList(
{ category: categoryName },
(taskViewModel: TaskViewModel) => taskViewModel.task,
boardViewModel.filteredTaskViewModels,
(taskViewModels: React.ListState<TaskViewModel>) => {
const statusNameConverter: React.StateItemConverter<string> = (
statusName: string
) => {
const index = createPropertyValueIndexState(sortedStatuses, statusName);
return CategoryStatusColumn(
categoryName,
statusName,
index,
boardViewModel,
taskViewModels
);
};
return FilteredList(
{ category: categoryName },
(taskViewModel: TaskViewModel) => taskViewModel.task,
boardViewModel.filteredTaskViewModels,
(taskViewModels: React.ListState<TaskViewModel>) => {
const statusNameConverter: React.StateItemConverter<string> = (
statusName: string,
) => {
const index = createPropertyValueIndexState(
sortedStatuses,
statusName,
);
return CategoryStatusColumn(
categoryName,
statusName,
index,
boardViewModel,
taskViewModels,
);
};
const viewModel: TaskCategoryBulkChangeViewModel =
new TaskCategoryBulkChangeViewModel(taskViewModels, categoryName);
const viewModel: TaskCategoryBulkChangeViewModel =
new TaskCategoryBulkChangeViewModel(
taskViewModels,
categoryName,
);
const view = (
<div class="flex-row flex-no large-gap">
<div class="property-input-wrapper">
<input
placeholder={
translations.chatPage.task.renameCategoryInputPlaceholder
}
bind:value={viewModel.inputValue}
on:enter={viewModel.set}
></input>
<button
class="primary"
on:click={viewModel.set}
toggle:disabled={viewModel.cannotSet}
>
<span class="icon">check</span>
</button>
</div>
const view = (
<div class="flex-row flex-no large-gap">
<div class="property-input-wrapper">
<input
placeholder={
translations.chatPage.task
.renameCategoryInputPlaceholder
}
bind:value={viewModel.inputValue}
on:enter={viewModel.set}
></input>
<button
class="primary"
on:click={viewModel.set}
toggle:disabled={viewModel.cannotSet}
>
<span class="icon">check</span>
</button>
</div>
<div
class="flex-row large-gap padding-right"
children:append={[allStatuses, statusNameConverter]}
></div>
</div>
);
<div
class="flex-row large-gap padding-right"
children:append={[allStatuses, statusNameConverter]}
></div>
</div>
);
index.subscribe((newIndex) => {
view.style.order = newIndex;
});
index.subscribe((newIndex) => {
view.style.order = newIndex;
});
return view;
}
);
return view;
},
);
}
function CategoryStatusColumn(
categoryName: string,
statusName: string,
index: React.State<number>,
boardViewModel: BoardViewModel,
taskViewModelsWithMatchingCategory: React.ListState<TaskViewModel>
categoryName: string,
statusName: string,
index: React.State<number>,
boardViewModel: BoardViewModel,
taskViewModelsWithMatchingCategory: React.ListState<TaskViewModel>,
) {
const taskViewModels: React.ListState<TaskViewModel> = new React.ListState();
const taskViewModels: React.ListState<TaskViewModel> =
new React.ListState();
taskViewModelsWithMatchingCategory.handleAddition((taskViewModel) => {
const doesMatchStatus: boolean = taskViewModel.status.value == statusName;
if (doesMatchStatus == false) return;
taskViewModelsWithMatchingCategory.handleAddition((taskViewModel) => {
const doesMatchStatus: boolean =
taskViewModel.status.value == statusName;
if (doesMatchStatus == false) return;
taskViewModels.add(taskViewModel);
taskViewModelsWithMatchingCategory.handleRemoval(taskViewModel, () => {
taskViewModels.remove(taskViewModel);
taskViewModels.add(taskViewModel);
taskViewModelsWithMatchingCategory.handleRemoval(taskViewModel, () => {
taskViewModels.remove(taskViewModel);
});
});
});
function drop() {
boardViewModel.handleDropWithinBoard(categoryName, statusName);
}
function drop() {
boardViewModel.handleDropWithinBoard(categoryName, statusName);
}
const view = (
<div
class="status-column gap"
on:dragover={allowDrop}
on:drop={drop}
children:append={[taskViewModels, TaskViewModelToEntry]}
></div>
);
const view = (
<div
class="status-column gap"
on:dragover={allowDrop}
on:drop={drop}
children:append={[taskViewModels, TaskViewModelToEntry]}
></div>
);
index.subscribe((newIndex) => {
view.style.order = newIndex;
});
index.subscribe((newIndex) => {
view.style.order = newIndex;
});
return view;
return view;
}
+50 -51
View File
@@ -1,74 +1,73 @@
#chat-page
> div
> #main
> #calendar-page
> .pane-wrapper
> .pane
> .toolbar
.input-wrapper {
display: flex;
> div
> #main
> #calendar-page
> .pane-wrapper
> .pane
> .toolbar
.input-wrapper {
display: flex;
padding: var(--gap-between-tiles);
gap: var(--gap-between-tiles);
padding: var(--gap-between-tiles);
gap: var(--gap-between-tiles);
}
#chat-page
> div
> #main
> #calendar-page
> .pane-wrapper
> .pane
> .toolbar
.input-wrapper
> input {
min-height: unset;
height: calc(var(--control-height) - 2 * var(--gap-between-tiles));
> div
> #main
> #calendar-page
> .pane-wrapper
> .pane
> .toolbar
.input-wrapper
> input {
min-height: unset;
height: calc(var(--control-height) - 2 * var(--gap-between-tiles));
appearance: textfield;
-moz-appearance: textfield;
appearance: textfield;
-moz-appearance: textfield;
padding-right: 0px;
padding-right: 0px;
}
#calendar-page .input-wrapper > input::-webkit-outer-spin-button,
#calendar-page .input-wrapper > input::-webkit-innner-spin-button {
appearance: none;
-webkit-appearance: none;
appearance: none;
-webkit-appearance: none;
margin: 0px;
margin: 0px;
}
#chat-page
> div
> #main
> #calendar-page
> .pane-wrapper
> .pane
> .toolbar
.input-wrapper
> .year-input {
width: 4.4rem;
> div
> #main
> #calendar-page
> .pane-wrapper
> .pane
> .toolbar
.input-wrapper
> .year-input {
width: 4.4rem;
}
#chat-page
> div
> #main
> #calendar-page
> .pane-wrapper
> .pane
> .toolbar
.input-wrapper
> .month-input {
width: 3.1rem;
> div
> #main
> #calendar-page
> .pane-wrapper
> .pane
> .toolbar
.input-wrapper
> .month-input {
width: 3.1rem;
}
@media (max-width: 1024px) {
#chat-page > div > #main > #calendar-page {
flex-direction: column;
}
#chat-page > div > #main > #calendar-page {
flex-direction: column;
}
#chat-page > div > #main > #calendar-page .grid-pane-wrapper {
border-bottom: 1px solid var(--border);
}
#chat-page > div > #main > #calendar-page .grid-pane-wrapper {
border-bottom: 1px solid var(--border);
}
}
+195 -160
View File
@@ -10,168 +10,203 @@ import { TaskViewModelToEntry } from "../Components/taskEntry";
import { translations } from "../translations";
export function CalendarPage(calendarPageViewModel: CalendarPageViewModel) {
calendarPageViewModel.loadData();
calendarPageViewModel.loadData();
const mainContent = React.createProxyState(
[calendarPageViewModel.monthGrid],
() => {
const monthGrid = calendarPageViewModel.monthGrid.value;
const mainContent = React.createProxyState(
[calendarPageViewModel.monthGrid],
() => {
const monthGrid = calendarPageViewModel.monthGrid.value;
if (monthGrid == undefined) {
return <div></div>;
} else {
function drop(date: string) {
calendarPageViewModel.handleDrop(
monthGrid!.year.toString(),
monthGrid!.month.toString(),
date
);
}
return MonthGrid(monthGrid, calendarPageViewModel.selectedDate, drop);
}
}
);
const sidePaneContentWrapper = React.createProxyState(
[
calendarPageViewModel.selectedYear,
calendarPageViewModel.selectedMonth,
calendarPageViewModel.selectedDate,
],
() => {
const listState = calendarPageViewModel.getEventsForDate();
if (listState == undefined) {
return <div></div>;
} else {
const sidePaneContent = React.createProxyState([listState], () => {
if (listState.value.size == 0) {
return (
<div class="width-100 height-100 flex-column justify-center align-center">
<span class="secondary slide-up">
{translations.chatPage.calendar.noEvents}
</span>
</div>
);
} else {
return (
<div
class="flex-column gap slide-up padding-bottom"
children:append={[listState, TaskViewModelToEntry]}
></div>
);
}
});
return (
<div
class="width-100 height-100"
children:set={sidePaneContent}
></div>
);
}
}
);
const taskSettingsModal = React.createProxyState(
[calendarPageViewModel.selectedTaskViewModel],
() => {
if (calendarPageViewModel.selectedTaskViewModel.value == undefined) {
return <div></div>;
} else {
return TaskSettingsModal(
calendarPageViewModel.selectedTaskViewModel.value
);
}
}
);
return (
<div id="calendar-page">
<div class="pane-wrapper grid-pane-wrapper">
<div class="pane">
<div class="toolbar">
<span>
<button
class="ghost"
aria-label={
translations.chatPage.calendar.todayButtonAudioLabel
if (monthGrid == undefined) {
return <div></div>;
} else {
function drop(date: string) {
calendarPageViewModel.handleDrop(
monthGrid!.year.toString(),
monthGrid!.month.toString(),
date,
);
}
on:click={calendarPageViewModel.showToday}
>
<span class="icon">today</span>
</button>
</span>
<span>
<button
class="ghost"
aria-label={
translations.chatPage.calendar.previousMonthButtonAudioLabel
}
on:click={calendarPageViewModel.showPreviousMonth}
>
<span class="icon">arrow_back</span>
</button>
<span class="input-wrapper">
<input
class="year-input"
type="number"
aria-label={
translations.chatPage.calendar.yearInputAudioLabel
}
placeholder={
translations.chatPage.calendar.yearInputPlaceholder
}
bind:value={calendarPageViewModel.selectedYear}
></input>
<input
class="month-input"
type="number"
aria-label={
translations.chatPage.calendar.monthInputAudioLabel
}
placeholder={
translations.chatPage.calendar.monthInputPlaceholder
}
bind:value={calendarPageViewModel.selectedMonth}
></input>
</span>
<button
class="ghost"
aria-label={
translations.chatPage.calendar.nextMonthButtonAudioLabel
}
on:click={calendarPageViewModel.showNextMonth}
>
<span class="icon">arrow_forward</span>
</button>
</span>
<span>
<button
class="ghost"
aria-label={
translations.chatPage.task.createTaskButtonAudioLabel
}
on:click={calendarPageViewModel.createEvent}
>
<span class="icon">add</span>
</button>
</span>
</div>
<div class="content padding-0" children:set={mainContent}></div>
return MonthGrid(
monthGrid,
calendarPageViewModel.selectedDate,
drop,
);
}
},
);
const sidePaneContentWrapper = React.createProxyState(
[
calendarPageViewModel.selectedYear,
calendarPageViewModel.selectedMonth,
calendarPageViewModel.selectedDate,
],
() => {
const listState = calendarPageViewModel.getEventsForDate();
if (listState == undefined) {
return <div></div>;
} else {
const sidePaneContent = React.createProxyState(
[listState],
() => {
if (listState.value.size == 0) {
return (
<div class="width-100 height-100 flex-column justify-center align-center">
<span class="secondary slide-up">
{
translations.chatPage.calendar
.noEvents
}
</span>
</div>
);
} else {
return (
<div
class="flex-column gap slide-up padding-bottom"
children:append={[
listState,
TaskViewModelToEntry,
]}
></div>
);
}
},
);
return (
<div
class="width-100 height-100"
children:set={sidePaneContent}
></div>
);
}
},
);
const taskSettingsModal = React.createProxyState(
[calendarPageViewModel.selectedTaskViewModel],
() => {
if (
calendarPageViewModel.selectedTaskViewModel.value == undefined
) {
return <div></div>;
} else {
return TaskSettingsModal(
calendarPageViewModel.selectedTaskViewModel.value,
);
}
},
);
return (
<div id="calendar-page">
<div class="pane-wrapper grid-pane-wrapper">
<div class="pane">
<div class="toolbar">
<span>
<button
class="ghost"
aria-label={
translations.chatPage.calendar
.todayButtonAudioLabel
}
on:click={calendarPageViewModel.showToday}
>
<span class="icon">today</span>
</button>
</span>
<span>
<button
class="ghost"
aria-label={
translations.chatPage.calendar
.previousMonthButtonAudioLabel
}
on:click={
calendarPageViewModel.showPreviousMonth
}
>
<span class="icon">arrow_back</span>
</button>
<span class="input-wrapper">
<input
class="year-input"
type="number"
aria-label={
translations.chatPage.calendar
.yearInputAudioLabel
}
placeholder={
translations.chatPage.calendar
.yearInputPlaceholder
}
bind:value={
calendarPageViewModel.selectedYear
}
></input>
<input
class="month-input"
type="number"
aria-label={
translations.chatPage.calendar
.monthInputAudioLabel
}
placeholder={
translations.chatPage.calendar
.monthInputPlaceholder
}
bind:value={
calendarPageViewModel.selectedMonth
}
></input>
</span>
<button
class="ghost"
aria-label={
translations.chatPage.calendar
.nextMonthButtonAudioLabel
}
on:click={calendarPageViewModel.showNextMonth}
>
<span class="icon">arrow_forward</span>
</button>
</span>
<span>
<button
class="ghost"
aria-label={
translations.chatPage.task
.createTaskButtonAudioLabel
}
on:click={calendarPageViewModel.createEvent}
>
<span class="icon">add</span>
</button>
</span>
</div>
<div
class="content padding-0"
children:set={mainContent}
></div>
</div>
</div>
<div
class="pane-wrapper side background"
set:color={calendarPageViewModel.chatViewModel.displayedColor}
>
<div class="pane">
<div
class="content"
children:set={sidePaneContentWrapper}
></div>
</div>
</div>
<div children:set={taskSettingsModal}></div>
</div>
</div>
<div
class="pane-wrapper side background"
set:color={calendarPageViewModel.chatViewModel.displayedColor}
>
<div class="pane">
<div class="content" children:set={sidePaneContentWrapper}></div>
</div>
</div>
<div children:set={taskSettingsModal}></div>
</div>
);
);
}
+78 -78
View File
@@ -1,168 +1,168 @@
/* layout */
#chat-page #main #message-page .content {
padding: 0px;
padding: 0px;
overflow: hidden;
overflow: hidden;
display: grid;
grid-template-rows: 1fr var(--control-height) var(--safe-area-bottom);
display: grid;
grid-template-rows: 1fr var(--control-height) var(--safe-area-bottom);
}
#message-page #message-container {
overflow: auto;
overflow: auto;
}
/* composer */
#message-page #composer {
display: flex;
justify-content: center;
display: flex;
justify-content: center;
}
#message-page #composer input {
margin: 0px;
margin: 0px;
width: 100%;
max-width: unset;
flex: 1;
border-top: 1px solid var(--border);
border-left: 1px solid var(--border);
}
@media (max-width: 700px) {
#message-page #composer div {
width: 100%;
max-width: unset;
display: flex;
}
flex: 1;
border-top: 1px solid var(--border);
border-left: 1px solid var(--border);
}
@media (max-width: 700px) {
#message-page #composer div {
width: 100%;
max-width: unset;
display: flex;
}
}
@media (min-width: 700px) {
#message-page #composer .content-width-constraint,
#message-page #composer .input-width-constraint {
display: flex;
justify-content: flex-end;
#message-page #composer .content-width-constraint,
#message-page #composer .input-width-constraint {
display: flex;
justify-content: flex-end;
width: 100%;
max-width: var(--max-content-width);
}
width: 100%;
max-width: var(--max-content-width);
}
#message-page #composer .content-width-constraint {
max-width: var(--max-content-width);
}
#message-page #composer .content-width-constraint {
max-width: var(--max-content-width);
}
#message-page #composer .input-width-constraint {
max-width: var(--max-input-width);
}
#message-page #composer .input-width-constraint {
max-width: var(--max-input-width);
}
}
/* message */
#message-page #message-container {
display: flex;
flex-direction: column;
align-items: center;
display: flex;
flex-direction: column;
align-items: center;
padding: var(--content-padding);
gap: var(--gap-between-tiles);
padding: var(--content-padding);
gap: var(--gap-between-tiles);
height: auto;
max-height: unset;
height: auto;
max-height: unset;
scroll-behavior: smooth;
scroll-behavior: smooth;
}
.message-bubble {
display: flex;
width: 100%;
max-width: var(--max-content-width);
display: flex;
width: 100%;
max-width: var(--max-content-width);
}
.message-bubble .tile.main {
display: flex;
flex-direction: row;
align-items: start;
display: flex;
flex-direction: row;
align-items: start;
padding: 0px;
padding: 0px;
width: 100%;
max-width: var(--max-input-width);
overflow: hidden;
width: 100%;
max-width: var(--max-input-width);
overflow: hidden;
}
.message-bubble .tile.main .text-container {
display: flex;
flex-direction: column;
display: flex;
flex-direction: column;
overflow: hidden;
overflow: hidden;
width: 100%;
flex: 1;
width: 100%;
flex: 1;
}
.message-bubble .tile.main .button-container {
display: flex;
display: flex;
width: var(--control-height);
max-width: var(--control-height);
width: var(--control-height);
max-width: var(--control-height);
}
.message-bubble .tile.main button {
justify-content: center;
justify-content: center;
padding: 0px;
width: 100%;
padding: 0px;
width: 100%;
--surface: transparent;
--surface: transparent;
}
.message-bubble .tile.main .text-container {
padding: var(--control-padding);
padding: var(--control-padding);
}
.message-bubble .tile.main .timestamp,
.message-bubble .tile.main .timestamp .icon,
.message-bubble .tile.main .sender-name {
font-size: 0.8rem;
opacity: 0.6;
font-size: 0.8rem;
opacity: 0.6;
}
.message-bubble .tile.main .timestamp {
display: flex;
align-items: center;
gap: var(--gap-between-tiles);
display: flex;
align-items: center;
gap: var(--gap-between-tiles);
}
.message-bubble .tile.main .timestamp .icon {
display: none;
display: none;
}
.message-bubble .tile.main .sender-name {
font-weight: bold;
font-weight: bold;
}
.message-bubble .tile.main .body {
word-break: break-word;
word-break: break-word;
}
.message-bubble[sentbyuser] {
justify-content: flex-end;
justify-content: flex-end;
}
.message-bubble[sentbyuser] {
align-items: flex-end;
align-items: flex-end;
}
.message-bubble[sentbyuser] .tile.main,
.message-bubble[sentbyuser] .tile.main .timestamp {
flex-direction: row-reverse;
flex-direction: row-reverse;
}
.message-bubble[sentbyuser] .tile.main .timestamp .icon {
display: flex;
display: flex;
}
.message-bubble[sentbyuser] .tile.main span {
text-align: right;
text-align: right;
}
+70 -58
View File
@@ -7,69 +7,81 @@ import MessagePageViewModel from "../../ViewModel/Pages/messagePageViewModel";
import { translations } from "../translations";
export function MessagePage(messagePageViewModel: MessagePageViewModel) {
messagePageViewModel.loadData();
messagePageViewModel.loadData();
const messageContainer = (
<div
id="message-container"
children:append={[
messagePageViewModel.chatMessageViewModels,
ChatMessageViewModelToView,
]}
></div>
);
const messageContainer = (
<div
id="message-container"
children:append={[
messagePageViewModel.chatMessageViewModels,
ChatMessageViewModelToView,
]}
></div>
);
function scrollDown() {
messageContainer.scrollTop = messageContainer.scrollHeight;
}
function scrollDownIfApplicable() {
const scrollFromBottom =
messageContainer.scrollHeight -
(messageContainer.scrollTop + messageContainer.offsetHeight);
if (scrollFromBottom > 400) return;
function scrollDown() {
messageContainer.scrollTop = messageContainer.scrollHeight;
}
function scrollDownIfApplicable() {
const scrollFromBottom =
messageContainer.scrollHeight -
(messageContainer.scrollTop + messageContainer.offsetHeight);
if (scrollFromBottom > 400) return;
scrollDown();
}
messagePageViewModel.chatMessageViewModels.subscribeSilent(
scrollDownIfApplicable
);
setTimeout(() => scrollDown(), 100);
scrollDown();
}
messagePageViewModel.chatMessageViewModels.subscribeSilent(
scrollDownIfApplicable,
);
setTimeout(() => scrollDown(), 100);
return (
<div id="message-page">
<div class="pane-wrapper">
<div class="pane">
<div class="toolbar">
<span class="title">{translations.chatPage.message.messagesHeadline}</span>
</div>
<div class="content">
{messageContainer}
<div id="composer">
<div class="content-width-constraint">
<div class="input-width-constraint">
<input
bind:value={messagePageViewModel.composingMessage}
on:enter={messagePageViewModel.sendMessage}
placeholder={
translations.chatPage.message.composerInputPlaceholder
}
></input>
<button
class="primary"
aria-label={
translations.chatPage.message.sendMessageButtonAudioLabel
}
on:click={messagePageViewModel.sendMessage}
toggle:disabled={messagePageViewModel.cannotSendMessage}
>
<span class="icon">send</span>
</button>
return (
<div id="message-page">
<div class="pane-wrapper">
<div class="pane">
<div class="toolbar">
<span class="title">
{translations.chatPage.message.messagesHeadline}
</span>
</div>
<div class="content">
{messageContainer}
<div id="composer">
<div class="content-width-constraint">
<div class="input-width-constraint">
<input
bind:value={
messagePageViewModel.composingMessage
}
on:enter={
messagePageViewModel.sendMessage
}
placeholder={
translations.chatPage.message
.composerInputPlaceholder
}
></input>
<button
class="primary"
aria-label={
translations.chatPage.message
.sendMessageButtonAudioLabel
}
on:click={
messagePageViewModel.sendMessage
}
toggle:disabled={
messagePageViewModel.cannotSendMessage
}
>
<span class="icon">send</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
);
}
+169 -133
View File
@@ -8,141 +8,177 @@ import SettingsPageViewModel from "../../ViewModel/Pages/settingsPageViewModel";
import { translations } from "../translations";
export function SettingsPage(settingsPageViewModel: SettingsPageViewModel) {
settingsPageViewModel.loadData();
settingsPageViewModel.loadData();
const secondaryChannelConverter: React.StateItemConverter<string> = (
secondaryChannel: string
) => {
return DeletableListItem(secondaryChannel, <span></span>, () => {
settingsPageViewModel.removeSecondaryChannel(secondaryChannel);
});
};
const secondaryChannelConverter: React.StateItemConverter<string> = (
secondaryChannel: string,
) => {
return DeletableListItem(secondaryChannel, <span></span>, () => {
settingsPageViewModel.removeSecondaryChannel(secondaryChannel);
});
};
return (
<div id="settings-page">
<div class="pane-wrapper">
<div class="pane">
<div class="toolbar">
<span class="title">
{translations.chatPage.settings.settingsHeadline}
</span>
</div>
<div class="content">
<label class="tile flex-no">
<span class="icon">forum</span>
<div>
<span>
{translations.chatPage.settings.primaryChannelLabel}
</span>
<input
bind:value={settingsPageViewModel.primaryChannelInput}
on:enter={settingsPageViewModel.setPrimaryChannel}
></input>
</div>
</label>
<div class="flex-row justify-end width-input">
<button
class="width-50"
aria-label={
translations.chatPage.settings
.setPrimaryChannelButtonAudioLabel
}
on:click={settingsPageViewModel.setPrimaryChannel}
toggle:disabled={settingsPageViewModel.cannotSetPrimaryChannel}
>
{translations.general.setButton}
<span class="icon">check</span>
</button>
return (
<div id="settings-page">
<div class="pane-wrapper">
<div class="pane">
<div class="toolbar">
<span class="title">
{translations.chatPage.settings.settingsHeadline}
</span>
</div>
<div class="content">
<label class="tile flex-no">
<span class="icon">forum</span>
<div>
<span>
{
translations.chatPage.settings
.primaryChannelLabel
}
</span>
<input
bind:value={
settingsPageViewModel.primaryChannelInput
}
on:enter={
settingsPageViewModel.setPrimaryChannel
}
></input>
</div>
</label>
<div class="flex-row justify-end width-input">
<button
class="width-50"
aria-label={
translations.chatPage.settings
.setPrimaryChannelButtonAudioLabel
}
on:click={
settingsPageViewModel.setPrimaryChannel
}
toggle:disabled={
settingsPageViewModel.cannotSetPrimaryChannel
}
>
{translations.general.setButton}
<span class="icon">check</span>
</button>
</div>
<hr></hr>
<div class="flex-row width-input margin-bottom">
<input
aria-label={
translations.chatPage.settings
.newSecondaryChannelAudioLabel
}
placeholder={
translations.chatPage.settings
.newSecondaryChannelPlaceholder
}
bind:value={
settingsPageViewModel.newSecondaryChannelInput
}
on:enter={
settingsPageViewModel.addSecondaryChannel
}
></input>
<button
class="primary"
aria-label={
translations.chatPage.settings
.addSecondaryChannelButtonAudioLabel
}
on:click={
settingsPageViewModel.addSecondaryChannel
}
toggle:disabled={
settingsPageViewModel.cannotAddSecondaryChannel
}
>
<span class="icon">add</span>
</button>
</div>
<div
class="flex-column gap width-input"
children:append={[
settingsPageViewModel.secondaryChannels,
secondaryChannelConverter,
]}
></div>
<hr></hr>
<label class="tile flex-no">
<span class="icon">key</span>
<div>
<span>
{
translations.chatPage.settings
.encryptionKeyLabel
}
</span>
<input
bind:value={
settingsPageViewModel.encryptionKeyInput
}
on:enter={
settingsPageViewModel.setEncryptionKey
}
set:type={
settingsPageViewModel.encryptionKeyInputType
}
></input>
</div>
</label>
<div class="flex-row justify-end width-input">
<button
class="width-50"
aria-label={
translations.chatPage.settings
.setEncryptionKeyButtonAudioLabel
}
on:click={
settingsPageViewModel.setEncryptionKey
}
toggle:disabled={
settingsPageViewModel.cannotSetEncryptionKey
}
>
{translations.general.setButton}
<span class="icon">check</span>
</button>
</div>
<label class="inline">
<input
type="checkbox"
bind:checked={
settingsPageViewModel.shouldShowEncryptionKey
}
></input>
{translations.chatPage.settings.showEncryptionKey}
</label>
<hr></hr>
{ColorPicker(settingsPageViewModel.color)}
<hr></hr>
<div class="width-input">
{DangerousActionButton(
translations.chatPage.settings.deleteChatButton,
"chat_error",
settingsPageViewModel.remove,
)}
</div>
</div>
</div>
</div>
<hr></hr>
<div class="flex-row width-input margin-bottom">
<input
aria-label={
translations.chatPage.settings.newSecondaryChannelAudioLabel
}
placeholder={
translations.chatPage.settings.newSecondaryChannelPlaceholder
}
bind:value={settingsPageViewModel.newSecondaryChannelInput}
on:enter={settingsPageViewModel.addSecondaryChannel}
></input>
<button
class="primary"
aria-label={
translations.chatPage.settings
.addSecondaryChannelButtonAudioLabel
}
on:click={settingsPageViewModel.addSecondaryChannel}
toggle:disabled={
settingsPageViewModel.cannotAddSecondaryChannel
}
>
<span class="icon">add</span>
</button>
</div>
<div
class="flex-column gap width-input"
children:append={[
settingsPageViewModel.secondaryChannels,
secondaryChannelConverter,
]}
></div>
<hr></hr>
<label class="tile flex-no">
<span class="icon">key</span>
<div>
<span>{translations.chatPage.settings.encryptionKeyLabel}</span>
<input
bind:value={settingsPageViewModel.encryptionKeyInput}
on:enter={settingsPageViewModel.setEncryptionKey}
set:type={settingsPageViewModel.encryptionKeyInputType}
></input>
</div>
</label>
<div class="flex-row justify-end width-input">
<button
class="width-50"
aria-label={
translations.chatPage.settings
.setEncryptionKeyButtonAudioLabel
}
on:click={settingsPageViewModel.setEncryptionKey}
toggle:disabled={settingsPageViewModel.cannotSetEncryptionKey}
>
{translations.general.setButton}
<span class="icon">check</span>
</button>
</div>
<label class="inline">
<input
type="checkbox"
bind:checked={settingsPageViewModel.shouldShowEncryptionKey}
></input>
{translations.chatPage.settings.showEncryptionKey}
</label>
<hr></hr>
{ColorPicker(settingsPageViewModel.color)}
<hr></hr>
<div class="width-input">
{DangerousActionButton(
translations.chatPage.settings.deleteChatButton,
"chat_error",
settingsPageViewModel.remove
)}
</div>
</div>
</div>
</div>
</div>
);
);
}
+63 -63
View File
@@ -1,53 +1,53 @@
#chat-page > div > #main > #task-page > .pane-wrapper > .pane > .main-content {
width: 100%;
max-width: 100%;
width: 100%;
max-width: 100%;
padding: 0px;
padding: 0px;
}
#chat-page
> div
> #main
> #task-page
> .pane-wrapper
> .pane
> .main-content
> div {
animation: slide-up var(--animation-duration);
> div
> #main
> #task-page
> .pane-wrapper
> .pane
> .main-content
> div {
animation: slide-up var(--animation-duration);
}
#chat-page > div > #main > #task-page .task-grid {
display: flex;
flex-direction: column;
gap: var(--gap-between-tiles);
display: flex;
flex-direction: column;
gap: var(--gap-between-tiles);
padding: var(--content-padding);
padding: var(--content-padding);
}
#chat-page > div > #main > #task-page .kanban-board-wrapper,
#chat-page > div > #main > #task-page .status-page-content {
display: flex;
gap: var(--large-gap);
display: flex;
gap: var(--large-gap);
width: 100%;
height: 100%;
width: 100%;
height: 100%;
overflow: auto;
overflow: auto;
padding: var(--content-padding);
padding: var(--content-padding);
}
#chat-page > div > #main > #task-page .kanban-board-wrapper .kanban-column,
#chat-page > div > #main > #task-page .status-grid-wrapper .status-column {
display: flex;
flex-direction: column;
display: flex;
flex-direction: column;
gap: var(--gap-between-tiles);
gap: var(--gap-between-tiles);
}
#chat-page > div > #main > #task-page .property-input-wrapper {
display: flex;
flex-direction: row;
display: flex;
flex-direction: row;
}
#chat-page > div > #main > #task-page .kanban-board-wrapper .kanban-column,
@@ -55,72 +55,72 @@
#chat-page > div > #main > #task-page .status-grid-wrapper .status-column,
#chat-page > div > #main > #task-page .status-name-row > *,
#chat-page > div > #main > #task-page .status-name-row::before {
width: 18rem;
min-width: 18rem;
max-width: 18rem;
width: 18rem;
min-width: 18rem;
max-width: 18rem;
}
#chat-page > div > #main > #task-page .status-name-row,
#chat-page > div > #main > #task-page .status-grid-wrapper {
display: flex;
gap: var(--large-gap);
display: flex;
gap: var(--large-gap);
}
#chat-page > div > #main > #task-page .status-page-content,
#chat-page > div > #main > #task-page .status-grid-wrapper {
flex-direction: column;
flex-direction: column;
}
#chat-page > div > #main > #task-page .status-name-row::before {
content: "";
display: flex;
content: "";
display: flex;
}
#chat-page > div > #main > #task-page .board-close-button {
display: none;
display: none;
}
#chat-page > div > #main > #task-page .board-toggle-button {
display: flex;
display: flex;
}
#chat-page
> div
> #main
> #task-page[isshowingboard][showingboardlist="false"]
#board-list {
display: none;
> div
> #main
> #task-page[isshowingboard][showingboardlist="false"]
#board-list {
display: none;
}
@media (min-width: 640px) {
#chat-page > div > #main > #task-page .task-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
}
#chat-page > div > #main > #task-page .task-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
}
}
@media (max-width: 1024px) {
#chat-page > div > #main > #task-page #board-list {
display: false;
}
#chat-page > div > #main > #task-page #board-list {
display: false;
}
#chat-page > div > #main > #task-page #board-content {
display: none;
}
#chat-page > div > #main > #task-page #board-content {
display: none;
}
#chat-page > div > #main > #task-page[isshowingboard] #board-list {
display: none;
}
#chat-page > div > #main > #task-page[isshowingboard] #board-list {
display: none;
}
#chat-page > div > #main > #task-page[isshowingboard] #board-content {
display: flex;
}
#chat-page > div > #main > #task-page[isshowingboard] #board-content {
display: flex;
}
#chat-page > div > #main > #task-page .board-close-button {
display: flex;
}
#chat-page > div > #main > #task-page .board-close-button {
display: flex;
}
#chat-page > div > #main > #task-page .board-toggle-button {
display: none;
}
#chat-page > div > #main > #task-page .board-toggle-button {
display: none;
}
}
+86 -76
View File
@@ -8,87 +8,97 @@ import TaskPageViewModel from "../../ViewModel/Pages/taskPageViewModel";
import { translations } from "../translations";
export function TaskPage(taskPageViewModel: TaskPageViewModel) {
taskPageViewModel.loadData();
taskPageViewModel.loadData();
const isShowingBoard = React.createProxyState(
[taskPageViewModel.selectedBoardId],
() => taskPageViewModel.selectedBoardId.value != undefined
);
const isShowingBoard = React.createProxyState(
[taskPageViewModel.selectedBoardId],
() => taskPageViewModel.selectedBoardId.value != undefined,
);
const paneContent = React.createProxyState(
[taskPageViewModel.selectedBoardId],
() => {
const selectedBoardId = taskPageViewModel.selectedBoardId.value;
if (selectedBoardId == undefined) {
return (
<div class="pane align-center justify-center">
<span class="secondary">
{translations.chatPage.task.noBoardSelected}
</span>
</div>
);
}
const selectedBoard =
taskPageViewModel.boardViewModels.value.get(selectedBoardId);
if (selectedBoard == undefined) {
return (
<div class="pane align-center justify-center">
<span class="secondary">
{translations.chatPage.task.boardNotFound}
</span>
</div>
);
}
const paneContent = React.createProxyState(
[taskPageViewModel.selectedBoardId],
() => {
const selectedBoardId = taskPageViewModel.selectedBoardId.value;
if (selectedBoardId == undefined) {
return (
<div class="pane align-center justify-center">
<span class="secondary">
{translations.chatPage.task.noBoardSelected}
</span>
</div>
);
}
const selectedBoard =
taskPageViewModel.boardViewModels.value.get(selectedBoardId);
if (selectedBoard == undefined) {
return (
<div class="pane align-center justify-center">
<span class="secondary">
{translations.chatPage.task.boardNotFound}
</span>
</div>
);
}
return BoardPage(selectedBoard);
}
);
return BoardPage(selectedBoard);
},
);
return (
<div id="task-page" toggle:isshowingboard={isShowingBoard} set:showingboardlist={taskPageViewModel.isShowingBoadList}>
<div
id="board-list"
class="pane-wrapper side background"
set:color={taskPageViewModel.chatViewModel.displayedColor}
>
<div class="pane">
<div class="toolbar">
<div class="flex-row width-input">
<input
class="no-outline"
bind:value={taskPageViewModel.newBoardNameInput}
on:enter={taskPageViewModel.createBoard}
placeholder={translations.chatPage.task.newBoardNamePlaceholder}
></input>
<button
class="primary"
aria-label={
translations.chatPage.task.createBoardButtonAudioLabel
}
on:click={taskPageViewModel.createBoard}
toggle:disabled={taskPageViewModel.cannotCreateBoard}
>
<span class="icon">add</span>
</button>
</div>
</div>
<div class="content">
return (
<div
id="task-page"
toggle:isshowingboard={isShowingBoard}
set:showingboardlist={taskPageViewModel.isShowingBoadList}
>
<div
class="grid gap"
style="grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr))"
children:append={[
taskPageViewModel.boardViewModels,
BoardViewModelToEntry,
]}
id="board-list"
class="pane-wrapper side background"
set:color={taskPageViewModel.chatViewModel.displayedColor}
>
<div class="pane">
<div class="toolbar">
<div class="flex-row width-input">
<input
class="no-outline"
bind:value={taskPageViewModel.newBoardNameInput}
on:enter={taskPageViewModel.createBoard}
placeholder={
translations.chatPage.task
.newBoardNamePlaceholder
}
></input>
<button
class="primary"
aria-label={
translations.chatPage.task
.createBoardButtonAudioLabel
}
on:click={taskPageViewModel.createBoard}
toggle:disabled={
taskPageViewModel.cannotCreateBoard
}
>
<span class="icon">add</span>
</button>
</div>
</div>
<div class="content">
<div
class="grid gap"
style="grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr))"
children:append={[
taskPageViewModel.boardViewModels,
BoardViewModelToEntry,
]}
></div>
</div>
</div>
</div>
<div
id="board-content"
class="pane-wrapper"
children:set={paneContent}
></div>
</div>
</div>
</div>
<div
id="board-content"
class="pane-wrapper"
children:set={paneContent}
></div>
</div>
);
);
}
+22 -19
View File
@@ -4,29 +4,32 @@ import BoardViewModel from "../../ViewModel/Pages/boardViewModel";
import { allowDrop } from "../utility";
export function BoardEntry(boardViewModel: BoardViewModel) {
const view = (
<button
set:color={boardViewModel.color}
class="tile colored-tile"
toggle:selected={boardViewModel.isSelected}
on:click={boardViewModel.select}
on:dragover={allowDrop}
on:drop={boardViewModel.handleDropBetweenBoards}
>
<span class="shadow" subscribe:innerText={boardViewModel.name}></span>
<b subscribe:innerText={boardViewModel.name}></b>
</button>
);
const view = (
<button
set:color={boardViewModel.color}
class="tile colored-tile"
toggle:selected={boardViewModel.isSelected}
on:click={boardViewModel.select}
on:dragover={allowDrop}
on:drop={boardViewModel.handleDropBetweenBoards}
>
<span
class="shadow"
subscribe:innerText={boardViewModel.name}
></span>
<b subscribe:innerText={boardViewModel.name}></b>
</button>
);
boardViewModel.index.subscribe((newIndex) => {
view.style.order = newIndex;
});
boardViewModel.index.subscribe((newIndex) => {
view.style.order = newIndex;
});
return view;
return view;
}
export const BoardViewModelToEntry: React.StateItemConverter<BoardViewModel> = (
boardViewModel: BoardViewModel
boardViewModel: BoardViewModel,
) => {
return BoardEntry(boardViewModel);
return BoardEntry(boardViewModel);
};
+13 -13
View File
@@ -1,25 +1,25 @@
import * as React from "bloatless-react";
import BoardViewModel, {
BoardPageType,
BoardPageType,
} from "../../ViewModel/Pages/boardViewModel";
import { RibbonButton } from "./ribbonButton";
export function BoardViewToggleButton(
label: string,
icon: string,
page: BoardPageType,
boardViewModel: BoardViewModel
label: string,
icon: string,
page: BoardPageType,
boardViewModel: BoardViewModel,
) {
function select() {
boardViewModel.selectedPage.value = page;
}
function select() {
boardViewModel.selectedPage.value = page;
}
const isSelected = React.createProxyState(
[boardViewModel.selectedPage],
() => boardViewModel.selectedPage.value == page
);
const isSelected = React.createProxyState(
[boardViewModel.selectedPage],
() => boardViewModel.selectedPage.value == page,
);
return RibbonButton(label, icon, isSelected, select);
return RibbonButton(label, icon, isSelected, select);
}
+27 -23
View File
@@ -3,33 +3,37 @@ import * as React from "bloatless-react";
import ChatViewModel from "../../ViewModel/Chat/chatViewModel";
export function ChatEntry(chatViewModel: ChatViewModel) {
const view = (
<button
class="tile colored-tile chat-entry"
set:color={chatViewModel.settingsPageViewModel.color}
style="height: 8rem"
on:click={chatViewModel.open}
toggle:unread={chatViewModel.hasUnreadMessages}
>
<span
class="shadow"
subscribe:innerText={chatViewModel.settingsPageViewModel.primaryChannel}
></span>
<h2
subscribe:innerText={chatViewModel.settingsPageViewModel.primaryChannel}
></h2>
</button>
);
const view = (
<button
class="tile colored-tile chat-entry"
set:color={chatViewModel.settingsPageViewModel.color}
style="height: 8rem"
on:click={chatViewModel.open}
toggle:unread={chatViewModel.hasUnreadMessages}
>
<span
class="shadow"
subscribe:innerText={
chatViewModel.settingsPageViewModel.primaryChannel
}
></span>
<h2
subscribe:innerText={
chatViewModel.settingsPageViewModel.primaryChannel
}
></h2>
</button>
);
chatViewModel.index.subscribe((newIndex) => {
view.style.order = newIndex;
});
chatViewModel.index.subscribe((newIndex) => {
view.style.order = newIndex;
});
return view;
return view;
}
export const ChatViewModelToChatEntry: React.StateItemConverter<
ChatViewModel
ChatViewModel
> = (chatViewModel: ChatViewModel) => {
return ChatEntry(chatViewModel);
return ChatEntry(chatViewModel);
};
+55 -51
View File
@@ -6,61 +6,65 @@ import ChatMessageViewModel from "../../ViewModel/Chat/chatMessageViewModel";
import { translations } from "../translations";
export function ChatMessage(chatMessageViewModel: ChatMessageViewModel) {
const statusIcon = React.createProxyState(
[chatMessageViewModel.status],
() => {
switch (chatMessageViewModel.status.value) {
case ChatMessageStatus.Outbox:
return "hourglass_top";
case ChatMessageStatus.Sent:
return "check";
case ChatMessageStatus.Received:
return "done_all";
default:
return "warning";
}
}
);
return (
<div
class="message-bubble"
toggle:sentbyuser={chatMessageViewModel.sentByUser}
>
<div class="main tile">
<div class="text-container">
<span class="sender-name ellipsis">
{chatMessageViewModel.sender}
</span>
<span
class="body"
subscribe:innerText={chatMessageViewModel.body}
></span>
<span class="timestamp ellipsis">
<span class="icon" subscribe:innerText={statusIcon}></span>
{chatMessageViewModel.dateSent}
</span>
</div>
<div class="button-container">
<button
on:click={chatMessageViewModel.showInfoModal}
aria-label={
translations.chatPage.message.showMessageInfoButtonAudioLabel
const statusIcon = React.createProxyState(
[chatMessageViewModel.status],
() => {
switch (chatMessageViewModel.status.value) {
case ChatMessageStatus.Outbox:
return "hourglass_top";
case ChatMessageStatus.Sent:
return "check";
case ChatMessageStatus.Received:
return "done_all";
default:
return "warning";
}
>
<span class="icon">info</span>
</button>
</div>
</div>
},
);
{ChatMessageInfoModal(chatMessageViewModel)}
</div>
);
return (
<div
class="message-bubble"
toggle:sentbyuser={chatMessageViewModel.sentByUser}
>
<div class="main tile">
<div class="text-container">
<span class="sender-name ellipsis">
{chatMessageViewModel.sender}
</span>
<span
class="body"
subscribe:innerText={chatMessageViewModel.body}
></span>
<span class="timestamp ellipsis">
<span
class="icon"
subscribe:innerText={statusIcon}
></span>
{chatMessageViewModel.dateSent}
</span>
</div>
<div class="button-container">
<button
on:click={chatMessageViewModel.showInfoModal}
aria-label={
translations.chatPage.message
.showMessageInfoButtonAudioLabel
}
>
<span class="icon">info</span>
</button>
</div>
</div>
{ChatMessageInfoModal(chatMessageViewModel)}
</div>
);
}
export const ChatMessageViewModelToView: React.StateItemConverter<
ChatMessageViewModel
ChatMessageViewModel
> = (chatMessageViewModel: ChatMessageViewModel) => {
return ChatMessage(chatMessageViewModel);
return ChatMessage(chatMessageViewModel);
};
+13 -13
View File
@@ -1,25 +1,25 @@
import * as React from "bloatless-react";
import ChatViewModel, {
ChatPageType,
ChatPageType,
} from "../../ViewModel/Chat/chatViewModel";
import { RibbonButton } from "./ribbonButton";
export function ChatViewToggleButton(
label: string,
icon: string,
page: ChatPageType,
chatViewModel: ChatViewModel
label: string,
icon: string,
page: ChatPageType,
chatViewModel: ChatViewModel,
) {
function select() {
chatViewModel.selectedPage.value = page;
}
function select() {
chatViewModel.selectedPage.value = page;
}
const isSelected = React.createProxyState(
[chatViewModel.selectedPage],
() => chatViewModel.selectedPage.value == page
);
const isSelected = React.createProxyState(
[chatViewModel.selectedPage],
() => chatViewModel.selectedPage.value == page,
);
return RibbonButton(label, icon, isSelected, select);
return RibbonButton(label, icon, isSelected, select);
}
+22 -22
View File
@@ -3,28 +3,28 @@ import * as React from "bloatless-react";
import { Color } from "../../colors";
export function ColorPicker(selectedColor: React.State<Color>) {
return (
<div class="flex-row gap width-input">
{...Object.values(Color).map((color) => {
const isSelected = React.createProxyState(
[selectedColor],
() => selectedColor.value == color
);
return (
<div class="flex-row gap width-input">
{...Object.values(Color).map((color) => {
const isSelected = React.createProxyState(
[selectedColor],
() => selectedColor.value == color,
);
function setColor() {
selectedColor.value = color;
}
function setColor() {
selectedColor.value = color;
}
return (
<button
color={color}
class="fill-color width-100 flex"
style="height: 2rem"
toggle:selected={isSelected}
on:click={setColor}
></button>
);
})}
</div>
);
return (
<button
color={color}
class="fill-color width-100 flex"
style="height: 2rem"
toggle:selected={isSelected}
on:click={setColor}
></button>
);
})}
</div>
);
}
+38 -38
View File
@@ -3,45 +3,45 @@ import * as React from "bloatless-react";
import { translations } from "../translations";
export function DangerousActionButton(
label: string,
icon: string,
action: () => void
label: string,
icon: string,
action: () => void,
) {
const isActionRequested = new React.State(false);
const cannotConfirm = React.createProxyState(
[isActionRequested],
() => isActionRequested.value == false
);
const isActionRequested = new React.State(false);
const cannotConfirm = React.createProxyState(
[isActionRequested],
() => isActionRequested.value == false,
);
function requestAction() {
isActionRequested.value = true;
}
function abort() {
isActionRequested.value = false;
}
function requestAction() {
isActionRequested.value = true;
}
function abort() {
isActionRequested.value = false;
}
return (
<div class="flex-row">
<button class="flex" on:click={abort} toggle:hidden={cannotConfirm}>
{translations.general.abortButton}
<span class="icon">undo</span>
</button>
<button
class="danger flex"
on:click={requestAction}
toggle:hidden={isActionRequested}
>
{label}
<span class="icon">{icon}</span>
</button>
<button
class="danger flex"
on:click={action}
toggle:hidden={cannotConfirm}
>
{translations.general.confirmButton}
<span class="icon">warning</span>
</button>
</div>
);
return (
<div class="flex-row">
<button class="flex" on:click={abort} toggle:hidden={cannotConfirm}>
{translations.general.abortButton}
<span class="icon">undo</span>
</button>
<button
class="danger flex"
on:click={requestAction}
toggle:hidden={isActionRequested}
>
{label}
<span class="icon">{icon}</span>
</button>
<button
class="danger flex"
on:click={action}
toggle:hidden={cannotConfirm}
>
{translations.general.confirmButton}
<span class="icon">warning</span>
</button>
</div>
);
}
+18 -18
View File
@@ -3,25 +3,25 @@ import * as React from "bloatless-react";
import { translations } from "../translations";
export function DeletableListItem(
text: string,
primaryButton: HTMLElement,
ondelete: () => void
text: string,
primaryButton: HTMLElement,
ondelete: () => void,
) {
return (
<div class="tile flex-row justify-apart align-center padding-0">
<span class="padding-h ellipsis">{text}</span>
return (
<div class="tile flex-row justify-apart align-center padding-0">
<span class="padding-h ellipsis">{text}</span>
<div class="flex-row justify-end">
{primaryButton}
<div class="flex-row justify-end">
{primaryButton}
<button
class="danger"
aria-label={translations.general.deleteItemButtonAudioLabel}
on:click={ondelete}
>
<span class="icon">delete</span>
</button>
</div>
</div>
);
<button
class="danger"
aria-label={translations.general.deleteItemButtonAudioLabel}
on:click={ondelete}
>
<span class="icon">delete</span>
</button>
</div>
</div>
);
}
+56 -56
View File
@@ -1,79 +1,79 @@
import * as React from "bloatless-react";
import StorageModel, {
PATH_COMPONENT_SEPARATOR,
PATH_COMPONENT_SEPARATOR,
} from "../../Model/Global/storageModel";
import StorageViewModel from "../../ViewModel/Global/storageViewModel";
export function DirectoryItemList(
storageViewModel: StorageViewModel,
pathString: string = PATH_COMPONENT_SEPARATOR
storageViewModel: StorageViewModel,
pathString: string = PATH_COMPONENT_SEPARATOR,
) {
// converter
const StringToDirectoryItemList: React.StateItemConverter<string> = (
pathString: string
) => DirectoryItemList(storageViewModel, pathString);
// converter
const StringToDirectoryItemList: React.StateItemConverter<string> = (
pathString: string,
) => DirectoryItemList(storageViewModel, pathString);
// data
const path = StorageModel.stringToPathComponents(pathString);
const fileName = StorageModel.getFileName(path);
const items = new React.ListState<string>();
// data
const path = StorageModel.stringToPathComponents(pathString);
const fileName = StorageModel.getFileName(path);
const items = new React.ListState<string>();
const style = `text-indent: ${path.length}rem`;
const style = `text-indent: ${path.length}rem`;
// methods
function loadItems() {
items.clear();
// methods
function loadItems() {
items.clear();
const directoryItems = storageViewModel.storageModel.list(path);
const directoryItems = storageViewModel.storageModel.list(path);
for (const directoryItem of directoryItems) {
const itemPath = [...path, directoryItem];
const pathString = StorageModel.pathComponentsToString(...itemPath);
items.add(pathString);
for (const directoryItem of directoryItems) {
const itemPath = [...path, directoryItem];
const pathString = StorageModel.pathComponentsToString(...itemPath);
items.add(pathString);
}
}
}
function select() {
storageViewModel.selectedPath.value = pathString;
}
function select() {
storageViewModel.selectedPath.value = pathString;
}
// handlers
storageViewModel.lastDeletedItemPath.subscribe((lastDeletedItemPath) => {
if (!items.value.has(lastDeletedItemPath)) return;
// handlers
storageViewModel.lastDeletedItemPath.subscribe((lastDeletedItemPath) => {
if (!items.value.has(lastDeletedItemPath)) return;
select();
setTimeout(() => loadItems(), 50);
});
select();
setTimeout(() => loadItems(), 50);
});
// state
const isSelected = React.createProxyState(
[storageViewModel.selectedPath],
() => storageViewModel.selectedPath.value == pathString
);
// state
const isSelected = React.createProxyState(
[storageViewModel.selectedPath],
() => storageViewModel.selectedPath.value == pathString,
);
isSelected.subscribe(() => {
if (isSelected.value == false) return;
loadItems();
});
isSelected.subscribe(() => {
if (isSelected.value == false) return;
loadItems();
});
return (
<div class="flex-column">
<button
class="width-100 flex-1 clip"
toggle:selected={isSelected}
on:click={select}
>
<span class="ellipsis width-100 flex-1" style={style}>
{fileName}
</span>
</button>
return (
<div class="flex-column">
<button
class="width-100 flex-1 clip"
toggle:selected={isSelected}
on:click={select}
>
<span class="ellipsis width-100 flex-1" style={style}>
{fileName}
</span>
</button>
<div
class="flex-column"
children:append={[items, StringToDirectoryItemList]}
></div>
</div>
);
<div
class="flex-column"
children:append={[items, StringToDirectoryItemList]}
></div>
</div>
);
}
+33 -33
View File
@@ -1,62 +1,62 @@
.file-browser {
width: 100%;
height: 100%;
width: 100%;
height: 100%;
display: flex;
display: flex;
overflow-x: scroll;
overflow-y: hidden;
scroll-behavior: smooth;
scroll-snap-type: x mandatory;
overflow-x: scroll;
overflow-y: hidden;
scroll-behavior: smooth;
scroll-snap-type: x mandatory;
}
.file-browser > div {
display: flex;
flex-direction: column;
display: flex;
flex-direction: column;
width: 100%;
min-width: 100%;
flex: 0;
width: 100%;
min-width: 100%;
flex: 0;
scroll-snap-align: center;
scroll-snap-align: center;
}
.file-browser .scroll-area {
flex: 1;
width: 100%;
overflow-y: auto;
flex: 1;
width: 100%;
overflow-y: auto;
padding: var(--content-padding);
padding: var(--content-padding);
}
.file-browser .detail-button-wrapper {
display: flex;
justify-content: end;
display: flex;
justify-content: end;
border-top: 1px solid var(--border);
padding: var(--content-padding);
border-top: 1px solid var(--border);
padding: var(--content-padding);
}
.file-browser .detail-button-wrapper button {
max-width: 100%;
max-width: 100%;
}
.file-browser code {
width: 100%;
height: 100%;
width: 100%;
height: 100%;
font-size: .9rem;
font-size: 0.9rem;
}
@media (min-width: 640px) {
.file-browser > div {
width: 50%;
min-width: 50%;
.file-browser > div {
width: 50%;
min-width: 50%;
flex: 1;
}
flex: 1;
}
.file-browser .detail-button-wrapper {
display: none;
}
.file-browser .detail-button-wrapper {
display: none;
}
}
+73 -63
View File
@@ -9,75 +9,85 @@ import StorageViewModel from "../../ViewModel/Global/storageViewModel";
import { translations } from "../translations";
export function FileBrowser(storageViewModel: StorageViewModel) {
const detailView = React.createProxyState(
[storageViewModel.selectedPath],
() => {
if (storageViewModel.selectedPath.value == PATH_COMPONENT_SEPARATOR)
return (
<div>
<span class="secondary">{translations.storage.noItemSelected}</span>
const detailView = React.createProxyState(
[storageViewModel.selectedPath],
() => {
if (storageViewModel.selectedPath.value == PATH_COMPONENT_SEPARATOR)
return (
<div>
<span class="secondary">
{translations.storage.noItemSelected}
</span>
<hr></hr>
<hr></hr>
{DangerousActionButton(
translations.storage.removeJunkButton,
"delete_forever",
storageViewModel.removeJunk
)}
</div>
);
{DangerousActionButton(
translations.storage.removeJunkButton,
"delete_forever",
storageViewModel.removeJunk,
)}
</div>
);
return (
<div class="flex-column gap">
<div class="tile flex-no">
return (
<div class="flex-column gap">
<div class="tile flex-no">
<div>
<b>{translations.storage.path}</b>
<span
class="break-all"
subscribe:innerText={
storageViewModel.selectedPath
}
></span>
</div>
</div>
<div class="tile flex-no">
<div>
<b>{translations.storage.content}</b>
<code
subscribe:innerText={
storageViewModel.selectedFileContent
}
></code>
</div>
</div>
{DangerousActionButton(
translations.storage.deleteItem,
"delete_forever",
storageViewModel.deleteSelectedItem,
)}
</div>
);
},
);
const view = (
<div class="file-browser">
<div>
<b>{translations.storage.path}</b>
<span
class="break-all"
subscribe:innerText={storageViewModel.selectedPath}
></span>
<div class="scroll-area">
{DirectoryItemList(storageViewModel)}
</div>
<div class="detail-button-wrapper">
<button class="ghost" on:click={scrollToDetails}>
<span
class="ellipsis"
subscribe:innerText={
storageViewModel.selectedFileName
}
></span>
<span class="icon">arrow_forward</span>
</button>
</div>
</div>
</div>
<div class="tile flex-no">
<div>
<b>{translations.storage.content}</b>
<code
subscribe:innerText={storageViewModel.selectedFileContent}
></code>
</div>
</div>
{DangerousActionButton(
translations.storage.deleteItem,
"delete_forever",
storageViewModel.deleteSelectedItem
)}
<div class="scroll-area" children:set={detailView}></div>
</div>
);
);
function scrollToDetails() {
view.scrollLeft = view.scrollWidth;
}
);
const view = (
<div class="file-browser">
<div>
<div class="scroll-area">{DirectoryItemList(storageViewModel)}</div>
<div class="detail-button-wrapper">
<button class="ghost" on:click={scrollToDetails}>
<span
class="ellipsis"
subscribe:innerText={storageViewModel.selectedFileName}
></span>
<span class="icon">arrow_forward</span>
</button>
</div>
</div>
<div class="scroll-area" children:set={detailView}></div>
</div>
);
function scrollToDetails() {
view.scrollLeft = view.scrollWidth;
}
return view;
return view;
}
+18 -18
View File
@@ -1,30 +1,30 @@
import * as React from "bloatless-react";
import {
StringEntryObject,
checkDoesObjectMatchReference,
StringEntryObject,
checkDoesObjectMatchReference,
} from "../../Model/Utility/utility";
export function FilteredList<T>(
reference: StringEntryObject,
stringEntryObjectConverter: (object: T) => StringEntryObject,
objects: React.ListState<T> | React.MapState<T>,
viewBuilder: (matchingObjects: React.ListState<T>) => HTMLElement
reference: StringEntryObject,
stringEntryObjectConverter: (object: T) => StringEntryObject,
objects: React.ListState<T> | React.MapState<T>,
viewBuilder: (matchingObjects: React.ListState<T>) => HTMLElement,
) {
const matchingObjects: React.ListState<T> = new React.ListState();
const matchingObjects: React.ListState<T> = new React.ListState();
objects.handleAddition((newObject) => {
const doesMatch: boolean = checkDoesObjectMatchReference(
reference,
stringEntryObjectConverter(newObject)
);
if (doesMatch == false) return;
objects.handleAddition((newObject) => {
const doesMatch: boolean = checkDoesObjectMatchReference(
reference,
stringEntryObjectConverter(newObject),
);
if (doesMatch == false) return;
matchingObjects.add(newObject);
objects.handleRemoval(newObject, () => {
matchingObjects.remove(newObject);
matchingObjects.add(newObject);
objects.handleRemoval(newObject, () => {
matchingObjects.remove(newObject);
});
});
});
return viewBuilder(matchingObjects);
return viewBuilder(matchingObjects);
}
+55 -55
View File
@@ -1,106 +1,106 @@
.month-grid-wrapper {
display: flex;
flex-direction: column;
display: flex;
flex-direction: column;
gap: var(--gap-between-tiles);
gap: var(--gap-between-tiles);
height: 100%;
overflow: auto;
height: 100%;
overflow: auto;
}
.month-grid-wrapper .day-labels {
flex: 0;
flex: 0;
padding: var(--content-padding);
padding-bottom: 0px;
padding: var(--content-padding);
padding-bottom: 0px;
}
.month-grid-wrapper > div {
display: grid;
gap: var(--gap-between-tiles);
grid-template-columns: repeat(7, minmax(0px, 1fr));
display: grid;
gap: var(--gap-between-tiles);
grid-template-columns: repeat(7, minmax(0px, 1fr));
animation: slide-up var(--animation-duration);
animation: slide-up var(--animation-duration);
}
.month-grid {
overflow: visible;
overflow: visible;
padding: var(--content-padding);
padding-top: 0px;
padding: var(--content-padding);
padding-top: 0px;
flex: 1;
flex: 1;
grid-auto-rows: 1fr;
grid-auto-rows: 1fr;
}
.month-grid .tile {
animation: slide-up var(--animation-duration);
overflow: hidden;
animation: slide-up var(--animation-duration);
overflow: hidden;
align-items: start;
align-items: start;
}
.month-grid .tile[today] b {
color: var(--primary);
color: var(--primary);
}
.month-grid .tile > div {
width: 100%;
height: 100%;
width: 100%;
height: 100%;
}
.month-grid .tile .event-count {
display: none;
display: none;
background-color: var(--text-normal);
color: var(--background);
background-color: var(--text-normal);
color: var(--background);
opacity: 0.6;
border-radius: 100%;
opacity: 0.6;
border-radius: 100%;
width: 1rem;
height: 1rem;
font-size: 0.6rem;
font-weight: bold;
width: 1rem;
height: 1rem;
font-size: 0.6rem;
font-weight: bold;
align-items: center;
justify-content: center;
align-items: center;
justify-content: center;
}
.month-grid .tile .event-count[has-events] {
background-color: var(--primary);
opacity: 1;
background-color: var(--primary);
opacity: 1;
}
.month-grid .tile .event-list {
display: flex;
flex-direction: column;
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
height: 100%;
width: 100%;
gap: var(--gap-between-tiles);
gap: var(--gap-between-tiles);
}
.month-grid .tile .event-list span {
animation: slide-up var(--animation-duration);
animation: slide-up var(--animation-duration);
}
@media (max-width: 640px) and (max-height: 900px) {
.month-grid .tile {
padding: 0px;
}
.month-grid .tile {
padding: 0px;
}
.month-grid .tile > div {
align-items: center;
justify-content: center;
}
.month-grid .tile > div {
align-items: center;
justify-content: center;
}
.month-grid .tile .event-count {
display: flex;
}
.month-grid .tile .event-count {
display: flex;
}
.month-grid .tile .event-list {
display: none;
}
.month-grid .tile .event-list {
display: none;
}
}
+99 -97
View File
@@ -8,103 +8,105 @@ import { allowDrop } from "../utility";
import { localeCompare } from "../../Model/Utility/utility";
export function MonthGrid<T>(
monthGrid: MonthGrid<React.MapState<T>>,
selectedDate: React.State<number>,
handleDrop: (date: string) => void
monthGrid: MonthGrid<React.MapState<T>>,
selectedDate: React.State<number>,
handleDrop: (date: string) => void,
) {
const dayLabels: HTMLElement[] = [];
let currentWeekday = monthGrid.firstDayOfWeek;
while (dayLabels.length < 7) {
dayLabels.push(
<span class="secondary">
{translations.regional.weekdays.abbreviated[currentWeekday]}
</span>
const dayLabels: HTMLElement[] = [];
let currentWeekday = monthGrid.firstDayOfWeek;
while (dayLabels.length < 7) {
dayLabels.push(
<span class="secondary">
{translations.regional.weekdays.abbreviated[currentWeekday]}
</span>,
);
currentWeekday++;
if (currentWeekday == 7) currentWeekday = 0;
}
const offsetElements: HTMLElement[] = [];
for (let i = 0; i < monthGrid.offset; i++) {
offsetElements.push(<div></div>);
}
const converter: React.StateItemConverter<TaskViewModel> = (
taskViewModel,
) => {
const view = (
<span class="ellipsis secondary">{taskViewModel.task.name}</span>
);
taskViewModel.index.subscribe((newIndex) => {
view.style.order = newIndex;
});
return view;
};
return (
<div class="month-grid-wrapper">
<div class="day-labels">{...dayLabels}</div>
<div class="month-grid">
{...offsetElements}
{...Object.entries(monthGrid.days)
.sort((a, b) => localeCompare(a[0], b[0]))
.map((entry) => {
const [date, mapState] = entry;
const isSelected = React.createProxyState(
[selectedDate],
() => selectedDate.value == parseInt(date),
);
const isToday: boolean =
monthGrid.isCurrentMonth == true &&
parseInt(date) == new Date().getDate();
const eventCount: React.State<number> =
React.createProxyState(
[mapState],
() => mapState.value.size,
);
const hasEvents: React.State<boolean> =
React.createProxyState(
[eventCount],
() => eventCount.value != 0,
);
function select() {
selectedDate.value = parseInt(date);
}
function drop() {
handleDrop(date);
}
return (
<button
class="tile"
on:click={select}
toggle:selected={isSelected}
toggle:today={isToday}
on:dragover={allowDrop}
on:drop={drop}
>
<div>
<b>{date}</b>
<span
class="event-count"
toggle:has-events={hasEvents}
subscribe:innerText={eventCount}
></span>
<div
class="event-list"
children:append={[mapState, converter]}
></div>
</div>
</button>
);
})}
</div>
</div>
);
currentWeekday++;
if (currentWeekday == 7) currentWeekday = 0;
}
const offsetElements: HTMLElement[] = [];
for (let i = 0; i < monthGrid.offset; i++) {
offsetElements.push(<div></div>);
}
const converter: React.StateItemConverter<TaskViewModel> = (
taskViewModel
) => {
const view = (
<span class="ellipsis secondary">{taskViewModel.task.name}</span>
);
taskViewModel.index.subscribe((newIndex) => {
view.style.order = newIndex;
});
return view;
};
return (
<div class="month-grid-wrapper">
<div class="day-labels">{...dayLabels}</div>
<div class="month-grid">
{...offsetElements}
{...Object.entries(monthGrid.days)
.sort((a, b) => localeCompare(a[0], b[0]))
.map((entry) => {
const [date, mapState] = entry;
const isSelected = React.createProxyState(
[selectedDate],
() => selectedDate.value == parseInt(date)
);
const isToday: boolean =
monthGrid.isCurrentMonth == true &&
parseInt(date) == new Date().getDate();
const eventCount: React.State<number> = React.createProxyState(
[mapState],
() => mapState.value.size
);
const hasEvents: React.State<boolean> = React.createProxyState(
[eventCount],
() => eventCount.value != 0
);
function select() {
selectedDate.value = parseInt(date);
}
function drop() {
handleDrop(date);
}
return (
<button
class="tile"
on:click={select}
toggle:selected={isSelected}
toggle:today={isToday}
on:dragover={allowDrop}
on:drop={drop}
>
<div>
<b>{date}</b>
<span
class="event-count"
toggle:has-events={hasEvents}
subscribe:innerText={eventCount}
></span>
<div
class="event-list"
children:append={[mapState, converter]}
></div>
</div>
</button>
);
})}
</div>
</div>
);
}
+14 -14
View File
@@ -1,32 +1,32 @@
import * as React from "bloatless-react";
export function Option(text: string, value: string, selectedOnCreate: boolean) {
return (
<option value={value} toggle:selected={selectedOnCreate}>
{text}
</option>
);
return (
<option value={value} toggle:selected={selectedOnCreate}>
{text}
</option>
);
}
export const StringToOption: React.StateItemConverter<string> = (
string: string
string: string,
) => {
return Option(string, string, false);
return Option(string, string, false);
};
export const EntryToOption: React.StateItemConverter<Entry> = (
entry: Entry
entry: Entry,
) => {
return Option(entry[1], entry[0], false);
return Option(entry[1], entry[0], false);
};
export const VersionIdToOption: React.StateItemConverter<string> = (
versionId: string
versionId: string,
) => {
const [date, rest] = versionId.split("T");
const [time] = rest.split(".");
const readableName = `${date} ${time}`;
return Option(readableName, versionId, false);
const [date, rest] = versionId.split("T");
const [time] = rest.split(".");
const readableName = `${date} ${time}`;
return Option(readableName, versionId, false);
};
export type Entry = [string, string];
+49 -48
View File
@@ -1,70 +1,71 @@
import * as React from "bloatless-react";
import {
StringEntryObject,
collectObjectValuesForKey,
localeCompare,
StringEntryObject,
collectObjectValuesForKey,
localeCompare,
} from "../../Model/Utility/utility";
export function PropertyValueList<T>(
propertyKey: string,
stringEntryObjectConverter: (object: T) => StringEntryObject,
objects: React.ListState<T> | React.MapState<T>,
viewBuilder: (
keys: React.ListState<string>,
sortedKeys: React.State<string[]>
) => HTMLElement
propertyKey: string,
stringEntryObjectConverter: (object: T) => StringEntryObject,
objects: React.ListState<T> | React.MapState<T>,
viewBuilder: (
keys: React.ListState<string>,
sortedKeys: React.State<string[]>,
) => HTMLElement,
) {
const propertyValues: React.ListState<string> = new React.ListState();
const sortedPropertyValues: React.State<string[]> = createSortedPropertyValueState(propertyValues)
objects.subscribe(() => {
collectPropertyValuesToState(
propertyKey,
stringEntryObjectConverter,
objects,
propertyValues
);
});
const propertyValues: React.ListState<string> = new React.ListState();
const sortedPropertyValues: React.State<string[]> =
createSortedPropertyValueState(propertyValues);
objects.subscribe(() => {
collectPropertyValuesToState(
propertyKey,
stringEntryObjectConverter,
objects,
propertyValues,
);
});
return viewBuilder(propertyValues, sortedPropertyValues);
return viewBuilder(propertyValues, sortedPropertyValues);
}
export function collectPropertyValuesToState<T>(
propertyKey: string,
stringEntryObjectConverter: (object: T) => StringEntryObject,
objects: React.ListState<T> | React.MapState<T>,
propertyValues: React.ListState<string>
propertyKey: string,
stringEntryObjectConverter: (object: T) => StringEntryObject,
objects: React.ListState<T> | React.MapState<T>,
propertyValues: React.ListState<string>,
) {
const values: string[] = collectObjectValuesForKey(
propertyKey,
stringEntryObjectConverter,
[...objects.value.values()]
);
for (const existingValue of values) {
if (propertyValues.value.has(existingValue)) continue;
propertyValues.add(existingValue);
}
for (const displayedValue of propertyValues.value.values()) {
if (values.includes(displayedValue) == false) {
propertyValues.remove(displayedValue);
const values: string[] = collectObjectValuesForKey(
propertyKey,
stringEntryObjectConverter,
[...objects.value.values()],
);
for (const existingValue of values) {
if (propertyValues.value.has(existingValue)) continue;
propertyValues.add(existingValue);
}
for (const displayedValue of propertyValues.value.values()) {
if (values.includes(displayedValue) == false) {
propertyValues.remove(displayedValue);
}
}
}
}
export function createSortedPropertyValueState(
propertyValues: React.ListState<string>
propertyValues: React.ListState<string>,
): React.State<string[]> {
return React.createProxyState([propertyValues], () =>
[...propertyValues.value.values()].sort(localeCompare)
);
return React.createProxyState([propertyValues], () =>
[...propertyValues.value.values()].sort(localeCompare),
);
}
export function createPropertyValueIndexState(
sortedKeys: React.State<string[]>,
key: string
sortedKeys: React.State<string[]>,
key: string,
): React.State<number> {
return React.createProxyState([sortedKeys], () =>
sortedKeys.value.indexOf(key)
);
return React.createProxyState([sortedKeys], () =>
sortedKeys.value.indexOf(key),
);
}
+15 -15
View File
@@ -1,23 +1,23 @@
import * as React from "bloatless-react";
import BoardViewModel, {
BoardPageType,
BoardPageType,
} from "../../ViewModel/Pages/boardViewModel";
export function RibbonButton(
label: string,
icon: string,
isSelected: React.State<boolean>,
select: () => void
label: string,
icon: string,
isSelected: React.State<boolean>,
select: () => void,
) {
return (
<button
class="ribbon-button"
aria-label={label}
toggle:selected={isSelected}
on:click={select}
>
<span class="icon">{icon}</span>
</button>
);
return (
<button
class="ribbon-button"
aria-label={label}
toggle:selected={isSelected}
on:click={select}
>
<span class="icon">{icon}</span>
</button>
);
}
+46 -43
View File
@@ -3,55 +3,58 @@ import * as React from "bloatless-react";
import TaskViewModel from "../../ViewModel/Pages/taskViewModel";
export function TaskEntry(taskViewModel: TaskViewModel) {
const details: { [icon: string]: string } = {
description: taskViewModel.description.value || "---",
priority_high: taskViewModel.priority.value || "---",
category: taskViewModel.category.value || "---",
clock_loader_40: taskViewModel.status.value || "---",
calendar_month: taskViewModel.date.value || "---",
schedule: taskViewModel.time.value || "---",
};
const details: { [icon: string]: string } = {
description: taskViewModel.description.value || "---",
priority_high: taskViewModel.priority.value || "---",
category: taskViewModel.category.value || "---",
clock_loader_40: taskViewModel.status.value || "---",
calendar_month: taskViewModel.date.value || "---",
schedule: taskViewModel.time.value || "---",
};
const view = (
<button
draggable="true"
class="tile flex-no"
style="user-select: none; -webkit-user-select: none"
on:click={taskViewModel.open}
on:dragstart={taskViewModel.dragStart}
>
<div>
<b class="ellipsis" subscribe:innerText={taskViewModel.name}></b>
<hr></hr>
<div
class="grid secondary"
style="grid-template-columns: repeat(2, 1fr); column-gap: 1rem; row-gap: .5rem"
const view = (
<button
draggable="true"
class="tile flex-no"
style="user-select: none; -webkit-user-select: none"
on:click={taskViewModel.open}
on:dragstart={taskViewModel.dragStart}
>
{...Object.entries(details).map((entry) => (
<span
class="flex-row align-center width-100 flex-no clip"
style="gap: 1rem"
>
<span class="icon" style="font-size: 1.1rem">
{entry[0]}
</span>
<span class="ellipsis">{entry[1]}</span>
</span>
))}
</div>
</div>
</button>
);
<div>
<b
class="ellipsis"
subscribe:innerText={taskViewModel.name}
></b>
<hr></hr>
<div
class="grid secondary"
style="grid-template-columns: repeat(2, 1fr); column-gap: 1rem; row-gap: .5rem"
>
{...Object.entries(details).map((entry) => (
<span
class="flex-row align-center width-100 flex-no clip"
style="gap: 1rem"
>
<span class="icon" style="font-size: 1.1rem">
{entry[0]}
</span>
<span class="ellipsis">{entry[1]}</span>
</span>
))}
</div>
</div>
</button>
);
taskViewModel.index.subscribe((newIndex) => {
view.style.order = newIndex;
});
taskViewModel.index.subscribe((newIndex) => {
view.style.order = newIndex;
});
return view;
return view;
}
export const TaskViewModelToEntry: React.StateItemConverter<TaskViewModel> = (
taskViewModel: TaskViewModel
taskViewModel: TaskViewModel,
) => {
return TaskEntry(taskViewModel);
return TaskEntry(taskViewModel);
};
+2 -2
View File
@@ -1,7 +1,7 @@
import * as React from "bloatless-react";
export const StringToTextSpan: React.StateItemConverter<string> = (
string: string
string: string,
) => {
return <span class="ellipsis">{string}</span>
return <span class="ellipsis">{string}</span>;
};
+41 -36
View File
@@ -6,42 +6,47 @@ import { DangerousActionButton } from "../Components/dangerousActionButton";
import { translations } from "../translations";
export function BoardSettingsModal(boardViewModel: BoardViewModel) {
return (
<div class="modal" toggle:open={boardViewModel.isPresentingSettingsModal}>
<div>
<main>
<h2>{translations.chatPage.task.boardSettingsHeadline}</h2>
<label class="tile flex-no">
<span class="icon">label</span>
return (
<div
class="modal"
toggle:open={boardViewModel.isPresentingSettingsModal}
>
<div>
<span>{translations.chatPage.task.boardNameInputLabel}</span>
<input
on:enter={boardViewModel.saveSettings}
bind:value={boardViewModel.name}
></input>
<main>
<h2>{translations.chatPage.task.boardSettingsHeadline}</h2>
<label class="tile flex-no">
<span class="icon">label</span>
<div>
<span>
{translations.chatPage.task.boardNameInputLabel}
</span>
<input
on:enter={boardViewModel.saveSettings}
bind:value={boardViewModel.name}
></input>
</div>
</label>
<hr></hr>
{ColorPicker(boardViewModel.color)}
<hr></hr>
<div class="width-input">
{DangerousActionButton(
translations.chatPage.task.deleteBoardButton,
"delete_forever",
boardViewModel.deleteBoard,
)}
</div>
</main>
<button on:click={boardViewModel.hideSettings}>
{translations.general.closeButton}
<span class="icon">close</span>
</button>
</div>
</label>
<hr></hr>
{ColorPicker(boardViewModel.color)}
<hr></hr>
<div class="width-input">
{DangerousActionButton(
translations.chatPage.task.deleteBoardButton,
"delete_forever",
boardViewModel.deleteBoard
)}
</div>
</main>
<button on:click={boardViewModel.hideSettings}>
{translations.general.closeButton}
<span class="icon">close</span>
</button>
</div>
</div>
);
</div>
);
}
+88 -66
View File
@@ -4,73 +4,95 @@ import ChatMessageViewModel from "../../ViewModel/Chat/chatMessageViewModel";
import { translations } from "../translations";
export function ChatMessageInfoModal(
chatMessageViewModel: ChatMessageViewModel
chatMessageViewModel: ChatMessageViewModel,
) {
return (
<div class="modal" toggle:open={chatMessageViewModel.isPresentingInfoModal}>
<div>
<main>
<h2>{translations.chatPage.message.messageInfoHeadline}</h2>
return (
<div
class="modal"
toggle:open={chatMessageViewModel.isPresentingInfoModal}
>
<div>
<main>
<h2>{translations.chatPage.message.messageInfoHeadline}</h2>
<div class="flex-column gap">
<div class="tile">
<span class="icon">account_circle</span>
<div>
<span>{translations.chatPage.message.sentBy}</span>
<b class="break-word">{chatMessageViewModel.sender}</b>
</div>
<div class="flex-column gap">
<div class="tile">
<span class="icon">account_circle</span>
<div>
<span>
{translations.chatPage.message.sentBy}
</span>
<b class="break-word">
{chatMessageViewModel.sender}
</b>
</div>
</div>
<div class="tile">
<span class="icon">schedule</span>
<div>
<span>
{translations.chatPage.message.timeSent}
</span>
<b class="break-word">
{chatMessageViewModel.dateSent}
</b>
</div>
</div>
<div class="tile">
<span class="icon">forum</span>
<div>
<span>
{translations.chatPage.message.channel}
</span>
<b class="break-word">
{chatMessageViewModel.channel}
</b>
</div>
</div>
<div class="tile">
<span class="icon">description</span>
<div>
<span>
{
translations.chatPage.message
.messageContent
}
</span>
<b
class="break-word"
subscribe:innerText={
chatMessageViewModel.body
}
></b>
</div>
</div>
</div>
<hr></hr>
<div class="flex-column gap">
<button on:click={chatMessageViewModel.copyMessage}>
{translations.chatPage.message.copyMessageButton}
<span class="icon">content_copy</span>
</button>
<button on:click={chatMessageViewModel.resendMessage}>
{translations.chatPage.message.resendMessageButton}
<span class="icon">redo</span>
</button>
<button on:click={chatMessageViewModel.decryptMessage}>
{translations.chatPage.message.decryptMessageButton}
<span class="icon">key</span>
</button>
</div>
</main>
<button on:click={chatMessageViewModel.hideInfoModal}>
{translations.general.closeButton}
<span class="icon">close</span>
</button>
</div>
<div class="tile">
<span class="icon">schedule</span>
<div>
<span>{translations.chatPage.message.timeSent}</span>
<b class="break-word">{chatMessageViewModel.dateSent}</b>
</div>
</div>
<div class="tile">
<span class="icon">forum</span>
<div>
<span>{translations.chatPage.message.channel}</span>
<b class="break-word">{chatMessageViewModel.channel}</b>
</div>
</div>
<div class="tile">
<span class="icon">description</span>
<div>
<span>{translations.chatPage.message.messageContent}</span>
<b
class="break-word"
subscribe:innerText={chatMessageViewModel.body}
></b>
</div>
</div>
</div>
<hr></hr>
<div class="flex-column gap">
<button on:click={chatMessageViewModel.copyMessage}>
{translations.chatPage.message.copyMessageButton}
<span class="icon">content_copy</span>
</button>
<button on:click={chatMessageViewModel.resendMessage}>
{translations.chatPage.message.resendMessageButton}
<span class="icon">redo</span>
</button>
<button on:click={chatMessageViewModel.decryptMessage}>
{translations.chatPage.message.decryptMessageButton}
<span class="icon">key</span>
</button>
</div>
</main>
<button on:click={chatMessageViewModel.hideInfoModal}>
{translations.general.closeButton}
<span class="icon">close</span>
</button>
</div>
</div>
);
</div>
);
}
+56 -52
View File
@@ -5,58 +5,62 @@ import { DeletableListItem } from "../Components/deletableListItem";
import { translations } from "../translations";
export function ConnectionModal(connectionViewModel: ConnectionViewModel) {
const previousAddressConverter: React.StateItemConverter<string> = (
address: string
) => {
function connnect() {
connectionViewModel.connectToAddress(address);
}
const previousAddressConverter: React.StateItemConverter<string> = (
address: string,
) => {
function connnect() {
connectionViewModel.connectToAddress(address);
}
const cannotConnect = React.createProxyState(
[connectionViewModel.isConnected],
() =>
connectionViewModel.isConnected.value == true &&
connectionViewModel.connectionModel.address == address
const cannotConnect = React.createProxyState(
[connectionViewModel.isConnected],
() =>
connectionViewModel.isConnected.value == true &&
connectionViewModel.connectionModel.address == address,
);
return DeletableListItem(
address,
<button
class="primary"
on:click={connnect}
toggle:disabled={cannotConnect}
aria-label={
translations.connectionModal.connectButtonAudioLabel
}
>
<span class="icon">link</span>
</button>,
() => {
connectionViewModel.removePreviousAddress(address);
},
);
};
return (
<div
class="modal"
toggle:open={connectionViewModel.isShowingConnectionModal}
>
<div>
<main>
<h2>
{translations.connectionModal.connectionModalHeadline}
</h2>
<div
class="flex-column gap"
children:append={[
connectionViewModel.previousAddresses,
previousAddressConverter,
]}
></div>
</main>
<button on:click={connectionViewModel.hideConnectionModal}>
{translations.general.closeButton}
<span class="icon">close</span>
</button>
</div>
</div>
);
return DeletableListItem(
address,
<button
class="primary"
on:click={connnect}
toggle:disabled={cannotConnect}
aria-label={translations.connectionModal.connectButtonAudioLabel}
>
<span class="icon">link</span>
</button>,
() => {
connectionViewModel.removePreviousAddress(address);
}
);
};
return (
<div
class="modal"
toggle:open={connectionViewModel.isShowingConnectionModal}
>
<div>
<main>
<h2>{translations.connectionModal.connectionModalHeadline}</h2>
<div
class="flex-column gap"
children:append={[
connectionViewModel.previousAddresses,
previousAddressConverter,
]}
></div>
</main>
<button on:click={connectionViewModel.hideConnectionModal}>
{translations.general.closeButton}
<span class="icon">close</span>
</button>
</div>
</div>
);
}
+429 -351
View File
@@ -1,8 +1,8 @@
import * as React from "bloatless-react";
import FileTransferViewModel, {
FileTransferModal,
FileTransferOption,
FileTransferModal,
FileTransferOption,
} from "../../ViewModel/Global/fileTransferViewModel";
import ConnectionViewModel from "../../ViewModel/Global/connectionViewModel";
@@ -12,406 +12,484 @@ import { reload } from "../utility";
import { translations } from "../translations";
export function DataTransferModalWrapper(
connectionViewModel: ConnectionViewModel,
fileTransferViewModel: FileTransferViewModel
connectionViewModel: ConnectionViewModel,
fileTransferViewModel: FileTransferViewModel,
) {
return (
<div>
{DirectionSelectionModal(connectionViewModel, fileTransferViewModel)}
{FileSelectionModal(fileTransferViewModel)}
{TransferDataDisplayModal(fileTransferViewModel)}
{TransferDisplayModal(fileTransferViewModel)}
{TransferDataInputModal(fileTransferViewModel)}
{DataReceptionModal(fileTransferViewModel)}
</div>
);
return (
<div>
{DirectionSelectionModal(
connectionViewModel,
fileTransferViewModel,
)}
{FileSelectionModal(fileTransferViewModel)}
{TransferDataDisplayModal(fileTransferViewModel)}
{TransferDisplayModal(fileTransferViewModel)}
{TransferDataInputModal(fileTransferViewModel)}
{DataReceptionModal(fileTransferViewModel)}
</div>
);
}
function DirectionSelectionModal(
connectionViewModel: ConnectionViewModel,
fileTransferViewModel: FileTransferViewModel
connectionViewModel: ConnectionViewModel,
fileTransferViewModel: FileTransferViewModel,
) {
const isPresented = React.createProxyState(
[fileTransferViewModel.presentedModal],
() =>
fileTransferViewModel.presentedModal.value ==
FileTransferModal.DirectionSelection
);
const isPresented = React.createProxyState(
[fileTransferViewModel.presentedModal],
() =>
fileTransferViewModel.presentedModal.value ==
FileTransferModal.DirectionSelection,
);
const isDisconnected = React.createProxyState(
[connectionViewModel.isConnected],
() => connectionViewModel.isConnected.value == false
);
const isDisconnected = React.createProxyState(
[connectionViewModel.isConnected],
() => connectionViewModel.isConnected.value == false,
);
return (
<div class="modal" toggle:open={isPresented}>
<div>
<main>
<h2>{translations.dataTransferModal.transferDataHeadline}</h2>
return (
<div class="modal" toggle:open={isPresented}>
<div>
<main>
<h2>
{translations.dataTransferModal.transferDataHeadline}
</h2>
<p class="error" toggle:hidden={connectionViewModel.isConnected}>
{translations.dataTransferModal.notConnectedError}
</p>
<p
class="error"
toggle:hidden={connectionViewModel.isConnected}
>
{translations.dataTransferModal.notConnectedError}
</p>
<div
class="flex-column gap content-margin-bottom"
toggle:hidden={isDisconnected}
>
<button
class="tile"
on:click={fileTransferViewModel.showFileSelectionModal}
>
<span class="icon">upload</span>
<div>
<b>{translations.dataTransferModal.fromThisDeviceButton}</b>
</div>
<span class="icon">arrow_forward</span>
</button>
<button
class="tile"
on:click={fileTransferViewModel.showTransferDataInputModal}
>
<span class="icon">download</span>
<div>
<b>{translations.dataTransferModal.toThisDeviceButton}</b>
</div>
<span class="icon">arrow_forward</span>
</button>
</div>
</main>
<button on:click={fileTransferViewModel.hideModal}>
{translations.general.closeButton}
<span class="icon">close</span>
</button>
</div>
</div>
);
<div
class="flex-column gap content-margin-bottom"
toggle:hidden={isDisconnected}
>
<button
class="tile"
on:click={
fileTransferViewModel.showFileSelectionModal
}
>
<span class="icon">upload</span>
<div>
<b>
{
translations.dataTransferModal
.fromThisDeviceButton
}
</b>
</div>
<span class="icon">arrow_forward</span>
</button>
<button
class="tile"
on:click={
fileTransferViewModel.showTransferDataInputModal
}
>
<span class="icon">download</span>
<div>
<b>
{
translations.dataTransferModal
.toThisDeviceButton
}
</b>
</div>
<span class="icon">arrow_forward</span>
</button>
</div>
</main>
<button on:click={fileTransferViewModel.hideModal}>
{translations.general.closeButton}
<span class="icon">close</span>
</button>
</div>
</div>
);
}
function FileSelectionModal(fileTransferViewModel: FileTransferViewModel) {
// option
const OptionConverter: React.StateItemConverter<FileTransferOption> = (
fileOption: FileTransferOption
) => {
return OptionEntry(fileOption, fileTransferViewModel);
};
// option
const OptionConverter: React.StateItemConverter<FileTransferOption> = (
fileOption: FileTransferOption,
) => {
return OptionEntry(fileOption, fileTransferViewModel);
};
function OptionEntry(
fileOption: FileTransferOption,
fileTransferViewModel: FileTransferViewModel
) {
const isSelected = new React.State(false);
if (fileTransferViewModel.selectedPaths.value.has(fileOption.path)) {
isSelected.value = true;
}
isSelected.subscribeSilent((isSelected) => {
if (isSelected == true) {
fileTransferViewModel.selectedPaths.add(fileOption.path);
} else {
fileTransferViewModel.selectedPaths.remove(fileOption.path);
}
});
function toggle() {
isSelected.value = !isSelected.value;
function OptionEntry(
fileOption: FileTransferOption,
fileTransferViewModel: FileTransferViewModel,
) {
const isSelected = new React.State(false);
if (fileTransferViewModel.selectedPaths.value.has(fileOption.path)) {
isSelected.value = true;
}
isSelected.subscribeSilent((isSelected) => {
if (isSelected == true) {
fileTransferViewModel.selectedPaths.add(fileOption.path);
} else {
fileTransferViewModel.selectedPaths.remove(fileOption.path);
}
});
function toggle() {
isSelected.value = !isSelected.value;
}
return (
<button class="tile" toggle:selected={isSelected} on:click={toggle}>
<div>
<b class="ellipsis">{fileOption.label}</b>
<span class="secondary ellipsis">
{StorageModel.pathComponentsToString(
...fileOption.path,
)}
</span>
</div>
</button>
);
}
// state
const isPresented = React.createProxyState(
[fileTransferViewModel.presentedModal],
() =>
fileTransferViewModel.presentedModal.value ==
FileTransferModal.FileSelection,
);
return (
<button class="tile" toggle:selected={isSelected} on:click={toggle}>
<div>
<b class="ellipsis">{fileOption.label}</b>
<span class="secondary ellipsis">
{StorageModel.pathComponentsToString(...fileOption.path)}
</span>
<div class="modal" toggle:open={isPresented}>
<div>
<main>
<h2>
{translations.dataTransferModal.transferDataHeadline}
</h2>
<span class="secondary">
{translations.dataTransferModal.selectionDescription}
</span>
<hr></hr>
<h3>{translations.dataTransferModal.generalHeadline}</h3>
<div
class="flex-column gap content-margin-bottom"
children:append={[
fileTransferViewModel.generalFileOptions,
OptionConverter,
]}
></div>
<h3>{translations.dataTransferModal.chatsHeadline}</h3>
<div
class="flex-column gap"
children:append={[
fileTransferViewModel.chatFileOptions,
OptionConverter,
]}
></div>
</main>
<div class="flex-row width-100">
<button
class="flex"
on:click={
fileTransferViewModel.showDirectionSelectionModal
}
>
{translations.general.backButton}
</button>
<button
class="primary flex"
on:click={fileTransferViewModel.showTransferDataModal}
toggle:disabled={
fileTransferViewModel.hasNoPathsSelected
}
>
{translations.general.continueButton}
<span class="icon">arrow_forward</span>
</button>
</div>
</div>
</div>
</button>
);
}
// state
const isPresented = React.createProxyState(
[fileTransferViewModel.presentedModal],
() =>
fileTransferViewModel.presentedModal.value ==
FileTransferModal.FileSelection
);
return (
<div class="modal" toggle:open={isPresented}>
<div>
<main>
<h2>{translations.dataTransferModal.transferDataHeadline}</h2>
<span class="secondary">
{translations.dataTransferModal.selectionDescription}
</span>
<hr></hr>
<h3>{translations.dataTransferModal.generalHeadline}</h3>
<div
class="flex-column gap content-margin-bottom"
children:append={[
fileTransferViewModel.generalFileOptions,
OptionConverter,
]}
></div>
<h3>{translations.dataTransferModal.chatsHeadline}</h3>
<div
class="flex-column gap"
children:append={[
fileTransferViewModel.chatFileOptions,
OptionConverter,
]}
></div>
</main>
<div class="flex-row width-100">
<button
class="flex"
on:click={fileTransferViewModel.showDirectionSelectionModal}
>
{translations.general.backButton}
</button>
<button
class="primary flex"
on:click={fileTransferViewModel.showTransferDataModal}
toggle:disabled={fileTransferViewModel.hasNoPathsSelected}
>
{translations.general.continueButton}
<span class="icon">arrow_forward</span>
</button>
</div>
</div>
</div>
);
}
function TransferDataDisplayModal(
fileTransferViewModel: FileTransferViewModel
fileTransferViewModel: FileTransferViewModel,
) {
const isPresented = React.createProxyState(
[fileTransferViewModel.presentedModal],
() =>
fileTransferViewModel.presentedModal.value ==
FileTransferModal.TransferDataDisplay
);
const isPresented = React.createProxyState(
[fileTransferViewModel.presentedModal],
() =>
fileTransferViewModel.presentedModal.value ==
FileTransferModal.TransferDataDisplay,
);
return (
<div class="modal" toggle:open={isPresented}>
<div>
<main>
<h2>{translations.dataTransferModal.transferDataHeadline}</h2>
return (
<div class="modal" toggle:open={isPresented}>
<div>
<main>
<h2>
{translations.dataTransferModal.transferDataHeadline}
</h2>
<span class="secondary">
{translations.dataTransferModal.dataEntryDescription}
</span>
<span class="secondary">
{translations.dataTransferModal.dataEntryDescription}
</span>
<hr></hr>
<hr></hr>
<div class="flex-column gap content-margin-bottom">
<div class="tile">
<span class="icon">forum</span>
<div>
<span class="secondary">
{translations.dataTransferModal.transferChannelHeadline}
</span>
<b
subscribe:innerText={fileTransferViewModel.transferChannel}
></b>
</div>
<div class="flex-column gap content-margin-bottom">
<div class="tile">
<span class="icon">forum</span>
<div>
<span class="secondary">
{
translations.dataTransferModal
.transferChannelHeadline
}
</span>
<b
subscribe:innerText={
fileTransferViewModel.transferChannel
}
></b>
</div>
</div>
<div class="tile">
<span class="icon">key</span>
<div>
<span class="secondary">
{
translations.dataTransferModal
.transferKeyHeadline
}
</span>
<b
subscribe:innerText={
fileTransferViewModel.transferKey
}
></b>
</div>
</div>
</div>
</main>
<div class="flex-row width-100">
<button
class="flex"
on:click={fileTransferViewModel.showFileSelectionModal}
>
{translations.general.backButton}
</button>
<button
class="primary flex"
on:click={fileTransferViewModel.initiateTransfer}
>
{translations.dataTransferModal.sendButton}
<span class="icon">arrow_forward</span>
</button>
</div>
</div>
<div class="tile">
<span class="icon">key</span>
<div>
<span class="secondary">
{translations.dataTransferModal.transferKeyHeadline}
</span>
<b subscribe:innerText={fileTransferViewModel.transferKey}></b>
</div>
</div>
</div>
</main>
<div class="flex-row width-100">
<button
class="flex"
on:click={fileTransferViewModel.showFileSelectionModal}
>
{translations.general.backButton}
</button>
<button
class="primary flex"
on:click={fileTransferViewModel.initiateTransfer}
>
{translations.dataTransferModal.sendButton}
<span class="icon">arrow_forward</span>
</button>
</div>
</div>
</div>
);
);
}
function TransferDisplayModal(fileTransferViewModel: FileTransferViewModel) {
const isPresented = React.createProxyState(
[fileTransferViewModel.presentedModal],
() =>
fileTransferViewModel.presentedModal.value ==
FileTransferModal.TransferDisplay
);
const isPresented = React.createProxyState(
[fileTransferViewModel.presentedModal],
() =>
fileTransferViewModel.presentedModal.value ==
FileTransferModal.TransferDisplay,
);
return (
<div class="modal" toggle:open={isPresented}>
<div>
<main>
<h2>{translations.dataTransferModal.transferDataHeadline}</h2>
return (
<div class="modal" toggle:open={isPresented}>
<div>
<main>
<h2>
{translations.dataTransferModal.transferDataHeadline}
</h2>
<p
class="secondary"
subscribe:innerText={fileTransferViewModel.filesSentText}
></p>
<p
class="secondary"
subscribe:innerText={
fileTransferViewModel.filesSentText
}
></p>
<p
class="secondary"
toggle:hidden={fileTransferViewModel.didNotFinishSending}
>
{translations.dataTransferModal.allFilesSent}
</p>
<p
class="secondary"
toggle:hidden={
fileTransferViewModel.didNotFinishSending
}
>
{translations.dataTransferModal.allFilesSent}
</p>
<hr></hr>
<hr></hr>
<div
class="tile flex-column align-start"
children:append={[
fileTransferViewModel.filePathsSent,
StringToTextSpan,
]}
></div>
</main>
<div class="flex-row width-100">
<button
class="flex"
on:click={fileTransferViewModel.initiateTransfer}
>
{translations.dataTransferModal.sendAgainButton}
<span class="icon">restart_alt</span>
</button>
<button
class="flex"
on:click={fileTransferViewModel.hideModal}
toggle:disabled={fileTransferViewModel.didNotFinishSending}
>
{translations.general.closeButton}
<span class="icon">close</span>
</button>
<div
class="tile flex-column align-start"
children:append={[
fileTransferViewModel.filePathsSent,
StringToTextSpan,
]}
></div>
</main>
<div class="flex-row width-100">
<button
class="flex"
on:click={fileTransferViewModel.initiateTransfer}
>
{translations.dataTransferModal.sendAgainButton}
<span class="icon">restart_alt</span>
</button>
<button
class="flex"
on:click={fileTransferViewModel.hideModal}
toggle:disabled={
fileTransferViewModel.didNotFinishSending
}
>
{translations.general.closeButton}
<span class="icon">close</span>
</button>
</div>
</div>
</div>
</div>
</div>
);
);
}
function TransferDataInputModal(fileTransferViewModel: FileTransferViewModel) {
const isPresented = React.createProxyState(
[fileTransferViewModel.presentedModal],
() =>
fileTransferViewModel.presentedModal.value ==
FileTransferModal.TransferDataInput
);
const isPresented = React.createProxyState(
[fileTransferViewModel.presentedModal],
() =>
fileTransferViewModel.presentedModal.value ==
FileTransferModal.TransferDataInput,
);
return (
<div class="modal" toggle:open={isPresented}>
<div>
<main>
<h2>{translations.dataTransferModal.transferDataHeadline}</h2>
return (
<div class="modal" toggle:open={isPresented}>
<div>
<main>
<h2>
{translations.dataTransferModal.transferDataHeadline}
</h2>
<span class="secondary">
{translations.dataTransferModal.dataEntryInputDescription}
</span>
<span class="secondary">
{
translations.dataTransferModal
.dataEntryInputDescription
}
</span>
<hr></hr>
<hr></hr>
<div class="flex-column gap content-margin-bottom">
<label class="tile">
<span class="icon">forum</span>
<div>
<span class="secondary">
{translations.dataTransferModal.transferChannelHeadline}
</span>
<input
bind:value={fileTransferViewModel.receivingTransferChannel}
></input>
</div>
</label>
<label class="tile">
<span class="icon">key</span>
<div>
<span class="secondary">
{translations.dataTransferModal.transferKeyHeadline}
</span>
<input
bind:value={fileTransferViewModel.receivingTransferKey}
></input>
</div>
</label>
</div>
</main>
<div class="flex-row width-100">
<button
class="flex"
on:click={fileTransferViewModel.showDirectionSelectionModal}
>
{translations.general.backButton}
</button>
<button
class="primary flex"
on:click={fileTransferViewModel.prepareReceivingData}
toggle:disabled={fileTransferViewModel.cannotPrepareToReceive}
>
{translations.general.continueButton}
<span class="icon">arrow_forward</span>
</button>
<div class="flex-column gap content-margin-bottom">
<label class="tile">
<span class="icon">forum</span>
<div>
<span class="secondary">
{
translations.dataTransferModal
.transferChannelHeadline
}
</span>
<input
bind:value={
fileTransferViewModel.receivingTransferChannel
}
></input>
</div>
</label>
<label class="tile">
<span class="icon">key</span>
<div>
<span class="secondary">
{
translations.dataTransferModal
.transferKeyHeadline
}
</span>
<input
bind:value={
fileTransferViewModel.receivingTransferKey
}
></input>
</div>
</label>
</div>
</main>
<div class="flex-row width-100">
<button
class="flex"
on:click={
fileTransferViewModel.showDirectionSelectionModal
}
>
{translations.general.backButton}
</button>
<button
class="primary flex"
on:click={fileTransferViewModel.prepareReceivingData}
toggle:disabled={
fileTransferViewModel.cannotPrepareToReceive
}
>
{translations.general.continueButton}
<span class="icon">arrow_forward</span>
</button>
</div>
</div>
</div>
</div>
</div>
);
);
}
function DataReceptionModal(fileTransferViewModel: FileTransferViewModel) {
const isPresented = React.createProxyState(
[fileTransferViewModel.presentedModal],
() =>
fileTransferViewModel.presentedModal.value ==
FileTransferModal.ReceptionDisplay
);
const isPresented = React.createProxyState(
[fileTransferViewModel.presentedModal],
() =>
fileTransferViewModel.presentedModal.value ==
FileTransferModal.ReceptionDisplay,
);
return (
<div class="modal" toggle:open={isPresented}>
<div>
<main>
<h2>{translations.dataTransferModal.transferDataHeadline}</h2>
return (
<div class="modal" toggle:open={isPresented}>
<div>
<main>
<h2>
{translations.dataTransferModal.transferDataHeadline}
</h2>
<p class="secondary">
{translations.dataTransferModal.readyToReceiveDescription}
</p>
<p class="secondary">
{
translations.dataTransferModal
.readyToReceiveDescription
}
</p>
<p
class="secondary"
subscribe:innerText={fileTransferViewModel.filesReceivedText}
></p>
<p
class="secondary"
subscribe:innerText={
fileTransferViewModel.filesReceivedText
}
></p>
<hr></hr>
<hr></hr>
<div
class="tile flex-column align-start"
children:append={[
fileTransferViewModel.filePathsReceived,
StringToTextSpan,
]}
></div>
</main>
<button on:click={reload}>
{translations.general.reloadAppButton}
<span class="icon">refresh</span>
</button>
</div>
</div>
);
<div
class="tile flex-column align-start"
children:append={[
fileTransferViewModel.filePathsReceived,
StringToTextSpan,
]}
></div>
</main>
<button on:click={reload}>
{translations.general.reloadAppButton}
<span class="icon">refresh</span>
</button>
</div>
</div>
);
}
+56 -48
View File
@@ -6,57 +6,65 @@ import { translations } from "../translations";
import { v4 } from "uuid";
export function SearchModal<T>(
searchViewModel: SearchViewModel<T>,
headline: string,
converter: React.StateItemConverter<T>,
isOpen: React.State<boolean>
searchViewModel: SearchViewModel<T>,
headline: string,
converter: React.StateItemConverter<T>,
isOpen: React.State<boolean>,
) {
function close() {
isOpen.value = false;
}
function close() {
isOpen.value = false;
}
const suggestionId = v4();
const suggestionId = v4();
return (
<div class="modal" toggle:open={isOpen}>
<div style="max-width: 64rem">
<main>
<h2>{headline}</h2>
<div class="flex-row width-input">
<input
placeholder={translations.general.searchLabel}
bind:value={searchViewModel.searchInput}
on:enter={searchViewModel.applySearch}
list={suggestionId}
></input>
<datalist
hidden
id={suggestionId}
children:append={[searchViewModel.suggestions, StringToOption]}
></datalist>
<button
class="primary"
aria-label={translations.general.searchButtonAudioLabel}
on:click={searchViewModel.applySearch}
toggle:disabled={searchViewModel.cannotApplySearch}
>
<span class="icon">search</span>
</button>
</div>
return (
<div class="modal" toggle:open={isOpen}>
<div style="max-width: 64rem">
<main>
<h2>{headline}</h2>
<div class="flex-row width-input">
<input
placeholder={translations.general.searchLabel}
bind:value={searchViewModel.searchInput}
on:enter={searchViewModel.applySearch}
list={suggestionId}
></input>
<datalist
hidden
id={suggestionId}
children:append={[
searchViewModel.suggestions,
StringToOption,
]}
></datalist>
<button
class="primary"
aria-label={
translations.general.searchButtonAudioLabel
}
on:click={searchViewModel.applySearch}
toggle:disabled={searchViewModel.cannotApplySearch}
>
<span class="icon">search</span>
</button>
</div>
<hr></hr>
<hr></hr>
<div
class="grid gap"
style="grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr))"
children:append={[searchViewModel.matchingObjects, converter]}
></div>
</main>
<button on:click={close}>
{translations.general.closeButton}
<span class="icon">close</span>
</button>
</div>
</div>
);
<div
class="grid gap"
style="grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr))"
children:append={[
searchViewModel.matchingObjects,
converter,
]}
></div>
</main>
<button on:click={close}>
{translations.general.closeButton}
<span class="icon">close</span>
</button>
</div>
</div>
);
}
+11 -11
View File
@@ -5,15 +5,15 @@ import StorageViewModel from "../../ViewModel/Global/storageViewModel";
import { translations } from "../translations";
export function StorageModal(storageViewModel: StorageViewModel) {
return (
<div class="modal" toggle:open={storageViewModel.isShowingStorageModal}>
<div style="max-width: 64rem">
<main class="padding-0">{FileBrowser(storageViewModel)}</main>
<button on:click={storageViewModel.hideStorageModal}>
{translations.general.closeButton}
<span class="icon">close</span>
</button>
</div>
</div>
);
return (
<div class="modal" toggle:open={storageViewModel.isShowingStorageModal}>
<div style="max-width: 64rem">
<main class="padding-0">{FileBrowser(storageViewModel)}</main>
<button on:click={storageViewModel.hideStorageModal}>
{translations.general.closeButton}
<span class="icon">close</span>
</button>
</div>
</div>
);
}
+190 -152
View File
@@ -1,11 +1,11 @@
import * as React from "bloatless-react";
import {
Entry,
EntryToOption,
Option,
StringToOption,
VersionIdToOption,
Entry,
EntryToOption,
Option,
StringToOption,
VersionIdToOption,
} from "../Components/option";
import { DangerousActionButton } from "../Components/dangerousActionButton";
@@ -14,156 +14,194 @@ import { translations } from "../translations";
import { v4 } from "uuid";
export function TaskSettingsModal(taskViewModel: TaskViewModel) {
const categorySuggestionId = v4();
const statusSuggestionId = v4();
const categorySuggestionId = v4();
const statusSuggestionId = v4();
const BoardOptionConverter: React.StateItemConverter<Entry> = (
entry: Entry
) => {
const isSelected = entry[0] == taskViewModel.task.boardId;
return Option(entry[1], entry[0], isSelected);
};
const BoardOptionConverter: React.StateItemConverter<Entry> = (
entry: Entry,
) => {
const isSelected = entry[0] == taskViewModel.task.boardId;
return Option(entry[1], entry[0], isSelected);
};
return (
<div class="modal" open>
<div>
<main>
<h2>{translations.chatPage.task.taskSettingsHeadline}</h2>
<label class="tile flex-no">
<span class="icon">history</span>
return (
<div class="modal" open>
<div>
<span>{translations.general.fileVersionLabel}</span>
<select
bind:value={taskViewModel.selectedVersionId}
children:append={[taskViewModel.versionIds, VersionIdToOption]}
></select>
<span class="icon">arrow_drop_down</span>
<main>
<h2>{translations.chatPage.task.taskSettingsHeadline}</h2>
<label class="tile flex-no">
<span class="icon">history</span>
<div>
<span>{translations.general.fileVersionLabel}</span>
<select
bind:value={taskViewModel.selectedVersionId}
children:append={[
taskViewModel.versionIds,
VersionIdToOption,
]}
></select>
<span class="icon">arrow_drop_down</span>
</div>
</label>
<hr></hr>
<label class="tile flex-no">
<span class="icon">label</span>
<div>
<span>
{translations.chatPage.task.taskNameLabel}
</span>
<input bind:value={taskViewModel.name}></input>
</div>
</label>
<label class="tile flex-no">
<span class="icon">category</span>
<div>
<span>
{translations.chatPage.task.taskBoardLabel}
</span>
<select
bind:value={taskViewModel.boardId}
children:append={[
taskViewModel.chatViewModel
.taskBoardSuggestions,
BoardOptionConverter,
]}
></select>
<span class="icon">arrow_drop_down</span>
</div>
</label>
<label class="tile flex-no">
<span class="icon">description</span>
<div>
<span>
{
translations.chatPage.task
.taskDescriptionLabel
}
</span>
<textarea
rows="10"
bind:value={taskViewModel.description}
></textarea>
</div>
</label>
<hr></hr>
<label class="tile flex-no">
<span class="icon">category</span>
<div>
<span>
{translations.chatPage.task.taskCategoryLabel}
</span>
<input
bind:value={taskViewModel.category}
list={categorySuggestionId}
></input>
</div>
</label>
<datalist
hidden
id={categorySuggestionId}
children:append={[
taskViewModel.coreViewModel.taskCategorySuggestions,
StringToOption,
]}
></datalist>
<label class="tile flex-no">
<span class="icon">clock_loader_40</span>
<div>
<span>
{translations.chatPage.task.taskStatusLabel}
</span>
<input
bind:value={taskViewModel.status}
list={statusSuggestionId}
></input>
</div>
</label>
<datalist
hidden
id={statusSuggestionId}
children:append={[
taskViewModel.coreViewModel.taskStatusSuggestions,
StringToOption,
]}
></datalist>
<label class="tile flex-no">
<span class="icon">priority_high</span>
<div>
<span>
{translations.chatPage.task.taskPriorityLabel}
</span>
<input
type="number"
bind:value={taskViewModel.priority}
></input>
</div>
</label>
<hr></hr>
<label class="tile flex-no">
<span class="icon">calendar_month</span>
<div>
<span>
{translations.chatPage.task.taskDateLabel}
</span>
<input
type="date"
bind:value={taskViewModel.date}
></input>
</div>
</label>
<label class="tile flex-no">
<span class="icon">schedule</span>
<div>
<span>
{translations.chatPage.task.taskTimeLabel}
</span>
<input
type="time"
bind:value={taskViewModel.time}
></input>
</div>
</label>
<hr></hr>
<div class="width-input">
{DangerousActionButton(
translations.chatPage.task.deleteTaskButton,
"delete_forever",
taskViewModel.deleteTask,
)}
</div>
</main>
<div class="flex-row width-100">
<button
class="flex"
on:click={taskViewModel.closeAndDiscard}
>
{translations.general.closeButton}
</button>
<button
class="flex primary"
on:click={taskViewModel.closeAndSave}
>
{translations.general.saveButton}
<span class="icon">save</span>
</button>
</div>
</div>
</label>
<hr></hr>
<label class="tile flex-no">
<span class="icon">label</span>
<div>
<span>{translations.chatPage.task.taskNameLabel}</span>
<input bind:value={taskViewModel.name}></input>
</div>
</label>
<label class="tile flex-no">
<span class="icon">category</span>
<div>
<span>{translations.chatPage.task.taskBoardLabel}</span>
<select
bind:value={taskViewModel.boardId}
children:append={[
taskViewModel.chatViewModel.taskBoardSuggestions,
BoardOptionConverter,
]}
></select>
<span class="icon">arrow_drop_down</span>
</div>
</label>
<label class="tile flex-no">
<span class="icon">description</span>
<div>
<span>{translations.chatPage.task.taskDescriptionLabel}</span>
<textarea
rows="10"
bind:value={taskViewModel.description}
></textarea>
</div>
</label>
<hr></hr>
<label class="tile flex-no">
<span class="icon">category</span>
<div>
<span>{translations.chatPage.task.taskCategoryLabel}</span>
<input
bind:value={taskViewModel.category}
list={categorySuggestionId}
></input>
</div>
</label>
<datalist
hidden
id={categorySuggestionId}
children:append={[
taskViewModel.coreViewModel.taskCategorySuggestions,
StringToOption,
]}
></datalist>
<label class="tile flex-no">
<span class="icon">clock_loader_40</span>
<div>
<span>{translations.chatPage.task.taskStatusLabel}</span>
<input
bind:value={taskViewModel.status}
list={statusSuggestionId}
></input>
</div>
</label>
<datalist
hidden
id={statusSuggestionId}
children:append={[
taskViewModel.coreViewModel.taskStatusSuggestions,
StringToOption,
]}
></datalist>
<label class="tile flex-no">
<span class="icon">priority_high</span>
<div>
<span>{translations.chatPage.task.taskPriorityLabel}</span>
<input type="number" bind:value={taskViewModel.priority}></input>
</div>
</label>
<hr></hr>
<label class="tile flex-no">
<span class="icon">calendar_month</span>
<div>
<span>{translations.chatPage.task.taskDateLabel}</span>
<input type="date" bind:value={taskViewModel.date}></input>
</div>
</label>
<label class="tile flex-no">
<span class="icon">schedule</span>
<div>
<span>{translations.chatPage.task.taskTimeLabel}</span>
<input type="time" bind:value={taskViewModel.time}></input>
</div>
</label>
<hr></hr>
<div class="width-input">
{DangerousActionButton(
translations.chatPage.task.deleteTaskButton,
"delete_forever",
taskViewModel.deleteTask
)}
</div>
</main>
<div class="flex-row width-100">
<button class="flex" on:click={taskViewModel.closeAndDiscard}>
{translations.general.closeButton}
</button>
<button class="flex primary" on:click={taskViewModel.closeAndSave}>
{translations.general.saveButton}
<span class="icon">save</span>
</button>
</div>
</div>
</div>
);
);
}
+100 -100
View File
@@ -1,29 +1,29 @@
/* show/hide */
#chat-page-wrapper {
display: none;
display: none;
}
body[showing-chat] #home-page {
display: none;
display: none;
}
body[showing-chat] #chat-page-wrapper {
display: flex;
display: flex;
position: absolute;
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
position: absolute;
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
background-color: var(--background-glass);
background-color: var(--background-glass);
z-index: 2;
z-index: 2;
}
/* layout */
#chat-page {
--ribbon-size: var(--control-height);
--ribbon-size: var(--control-height);
}
#chat-page > div,
@@ -35,179 +35,179 @@ body[showing-chat] #chat-page-wrapper {
#chat-page > div > #main > div > .pane-wrapper > .pane > .toolbar,
#chat-page > div > #main > div > .pane-wrapper > .pane > .toolbar > span,
#chat-page > div > #main > div > .pane-wrapper > .pane > .content {
display: flex;
display: flex;
}
#chat-page > div {
padding: 0px;
overflow: hidden;
padding: 0px;
overflow: hidden;
}
#chat-page > div > #main {
width: calc(100% - var(--ribbon-size) - 1px);
width: calc(100% - var(--ribbon-size) - 1px);
}
#chat-page #main > div,
#chat-page > div > #main > div > .pane-wrapper,
#chat-page > div > #main > div > .pane-wrapper > .pane,
#chat-page > div > #main > div > .pane-wrapper > .pane > .content {
width: 100%;
height: 100%;
flex: 1;
width: 100%;
height: 100%;
flex: 1;
}
#chat-page > div > #main > div > .pane-wrapper,
#chat-page > div > #main > div > .pane-wrapper > .pane {
flex-direction: column;
scroll-snap-align: center;
flex-direction: column;
scroll-snap-align: center;
min-width: 100%;
overflow: hidden;
min-width: 100%;
overflow: hidden;
}
#chat-page > div > #main > div > .pane-wrapper > .pane:not(.side),
#chat-page > div > #main > div > .pane-wrapper > .pane:not(.side) .content {
animation: slide-up var(--animation-duration);
animation: slide-up var(--animation-duration);
}
@media (min-width: 1024px) {
#chat-page > div > #main > div > .pane-wrapper {
min-width: unset;
}
#chat-page > div > #main > div > .pane-wrapper {
min-width: unset;
}
#chat-page > div > #main > div > .pane-wrapper.side {
width: 18rem;
min-width: 18rem;
max-width: 18rem;
}
#chat-page > div > #main > div > .pane-wrapper.side {
width: 18rem;
min-width: 18rem;
max-width: 18rem;
}
}
#chat-page > div > #main > div > .pane-wrapper > .pane > .content {
flex-direction: column;
overflow: auto;
padding: var(--content-padding);
flex-direction: column;
overflow: auto;
padding: var(--content-padding);
}
#chat-page #ribbon,
#chat-page #main,
#chat-page #main .content {
flex-direction: column;
flex-direction: column;
}
/* ribbon, toolbar */
#chat-page > div > #ribbon,
#chat-page > div > #main > div > .pane-wrapper > .pane > .toolbar {
align-items: center;
align-items: center;
}
#chat-page > div > #ribbon {
width: calc(var(--ribbon-size) + 1px);
border-right: 1px solid var(--border);
padding-bottom: var(--safe-area-bottom);
width: calc(var(--ribbon-size) + 1px);
border-right: 1px solid var(--border);
padding-bottom: var(--safe-area-bottom);
}
#chat-page > div > #ribbon > span,
#chat-page
> div
> #main
> div
> .pane-wrapper
> .pane
> .toolbar
> span.ribbon {
gap: var(--gap-between-tiles);
padding: var(--gap-between-tiles);
> div
> #main
> div
> .pane-wrapper
> .pane
> .toolbar
> span.ribbon {
gap: var(--gap-between-tiles);
padding: var(--gap-between-tiles);
}
#chat-page > div > #main > div > .pane-wrapper > .pane > .toolbar {
justify-content: start;
justify-content: space-between;
justify-content: start;
justify-content: space-between;
}
#chat-page > div > #ribbon > span {
height: 100%;
height: 100%;
flex: 1;
overflow-y: auto;
flex: 1;
overflow-y: auto;
flex-direction: column;
justify-content: end;
flex-direction: column;
justify-content: end;
}
#chat-page > div > #ribbon button,
#chat-page > div > #main > div > .pane-wrapper > .pane > .toolbar button {
padding: 0px;
padding: 0px;
width: var(--control-height);
max-width: var(--control-height);
height: var(--control-height);
min-height: var(--control-height);
width: var(--control-height);
max-width: var(--control-height);
height: var(--control-height);
min-height: var(--control-height);
align-items: center;
justify-content: center;
align-items: center;
justify-content: center;
}
#chat-page > div > #ribbon button.ribbon-button,
#chat-page
> div
> #main
> div
> .pane-wrapper
> .pane
> .toolbar
button.ribbon-button {
min-width: calc(var(--control-height) - 2 * var(--gap-between-tiles));
width: calc(var(--control-height) - 2 * var(--gap-between-tiles));
max-width: calc(var(--control-height) - 2 * var(--gap-between-tiles));
> div
> #main
> div
> .pane-wrapper
> .pane
> .toolbar
button.ribbon-button {
min-width: calc(var(--control-height) - 2 * var(--gap-between-tiles));
width: calc(var(--control-height) - 2 * var(--gap-between-tiles));
max-width: calc(var(--control-height) - 2 * var(--gap-between-tiles));
min-height: unset;
height: calc(var(--control-height) - 2 * var(--gap-between-tiles));
min-height: unset;
height: calc(var(--control-height) - 2 * var(--gap-between-tiles));
color: var(--primary);
color: var(--primary);
}
#chat-page > div > #ribbon button > .icon,
#chat-page
> div
> #main
> div
> .pane-wrapper
> .pane
> .toolbar
button
> .icon {
font-size: 1.1rem;
> div
> #main
> div
> .pane-wrapper
> .pane
> .toolbar
button
> .icon {
font-size: 1.1rem;
}
#chat-page > div > #ribbon > span {
width: 100%;
width: 100%;
}
#chat-page > div > #main > div > .pane-wrapper > .pane > .toolbar {
height: var(--ribbon-size);
height: var(--ribbon-size);
overflow-x: auto;
overflow-x: auto;
padding: 0px;
padding: 0px;
}
#chat-page > div > #main > div > .pane-wrapper > .pane > .toolbar > span {
display: flex;
align-items: center;
display: flex;
align-items: center;
}
#chat-page > div > #main > div > .pane-wrapper > .pane > .toolbar > span.title {
padding-left: var(--content-padding);
padding-left: var(--content-padding);
}
#chat-page
> div
> #main
> div
> .pane-wrapper
> .pane
> .toolbar
:is(input, select, button) {
height: 100%;
flex: 1;
> div
> #main
> div
> .pane-wrapper
> .pane
> .toolbar
:is(input, select, button) {
height: 100%;
flex: 1;
}
+66 -66
View File
@@ -12,73 +12,73 @@ import { TaskPage } from "./ChatPages/taskPage";
import { translations } from "./translations";
export function ChatPage(chatViewModel: ChatViewModel) {
const mainContent = React.createProxyState(
[chatViewModel.selectedPage],
() => {
chatViewModel.closeSubPages();
const mainContent = React.createProxyState(
[chatViewModel.selectedPage],
() => {
chatViewModel.closeSubPages();
switch (chatViewModel.selectedPage.value) {
case ChatPageType.Settings: {
return SettingsPage(chatViewModel.settingsPageViewModel);
}
case ChatPageType.Tasks: {
return TaskPage(chatViewModel.taskPageViewModel);
}
case ChatPageType.Calendar: {
return CalendarPage(chatViewModel.calendarViewModel)
}
default: {
return MessagePage(chatViewModel.messagePageViewModel);
}
}
}
);
switch (chatViewModel.selectedPage.value) {
case ChatPageType.Settings: {
return SettingsPage(chatViewModel.settingsPageViewModel);
}
case ChatPageType.Tasks: {
return TaskPage(chatViewModel.taskPageViewModel);
}
case ChatPageType.Calendar: {
return CalendarPage(chatViewModel.calendarViewModel);
}
default: {
return MessagePage(chatViewModel.messagePageViewModel);
}
}
},
);
return (
<article
id="chat-page"
set:color={chatViewModel.displayedColor}
class="subtle-background"
>
<div>
<div id="ribbon">
<button
class="ghost"
aria-label={translations.chatPage.closeChatAudioLabe}
on:click={chatViewModel.close}
>
<span class="icon">close</span>
</button>
return (
<article
id="chat-page"
set:color={chatViewModel.displayedColor}
class="subtle-background"
>
<div>
<div id="ribbon">
<button
class="ghost"
aria-label={translations.chatPage.closeChatAudioLabe}
on:click={chatViewModel.close}
>
<span class="icon">close</span>
</button>
<span>
{ChatViewToggleButton(
translations.chatPage.pages.calendar,
"calendar_month",
ChatPageType.Calendar,
chatViewModel
)}
{ChatViewToggleButton(
translations.chatPage.pages.tasks,
"task_alt",
ChatPageType.Tasks,
chatViewModel
)}
{ChatViewToggleButton(
translations.chatPage.pages.messages,
"forum",
ChatPageType.Messages,
chatViewModel
)}
{ChatViewToggleButton(
translations.chatPage.pages.settings,
"settings",
ChatPageType.Settings,
chatViewModel
)}
</span>
</div>
<div id="main" children:set={mainContent}></div>
</div>
</article>
);
<span>
{ChatViewToggleButton(
translations.chatPage.pages.calendar,
"calendar_month",
ChatPageType.Calendar,
chatViewModel,
)}
{ChatViewToggleButton(
translations.chatPage.pages.tasks,
"task_alt",
ChatPageType.Tasks,
chatViewModel,
)}
{ChatViewToggleButton(
translations.chatPage.pages.messages,
"forum",
ChatPageType.Messages,
chatViewModel,
)}
{ChatViewToggleButton(
translations.chatPage.pages.settings,
"settings",
ChatPageType.Settings,
chatViewModel,
)}
</span>
</div>
<div id="main" children:set={mainContent}></div>
</div>
</article>
);
}
+11 -16
View File
@@ -6,21 +6,16 @@ import ChatListViewModel from "../ViewModel/Chat/chatListViewModel";
import { ChatPage } from "./chatPage";
export function ChatPageWrapper(chatListViewModel: ChatListViewModel) {
const chatPageContent = React.createProxyState(
[chatListViewModel.selectedChat],
() => {
if (chatListViewModel.selectedChat.value == undefined) {
return <div></div>;
} else {
return ChatPage(chatListViewModel.selectedChat.value);
}
}
);
const chatPageContent = React.createProxyState(
[chatListViewModel.selectedChat],
() => {
if (chatListViewModel.selectedChat.value == undefined) {
return <div></div>;
} else {
return ChatPage(chatListViewModel.selectedChat.value);
}
},
);
return (
<div
id="chat-page-wrapper"
children:set={chatPageContent}
></div>
);
return <div id="chat-page-wrapper" children:set={chatPageContent}></div>;
}
+52 -52
View File
@@ -1,88 +1,88 @@
#home-page > div {
display: flex;
flex-direction: row;
display: flex;
flex-direction: row;
gap: var(--large-gap);
gap: var(--large-gap);
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
overflow: hidden;
overflow-x: scroll;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
overflow: hidden;
overflow-x: scroll;
padding: 0px;
padding: 0px;
}
#home-page > div > div {
display: flex;
flex-direction: column;
justify-content: start;
display: flex;
flex-direction: column;
justify-content: start;
gap: var(--gap-between-tiles);
padding: var(--content-padding);
padding-top: calc(var(--header-height) + var(--content-margin));
gap: var(--gap-between-tiles);
padding: var(--content-padding);
padding-top: calc(var(--header-height) + var(--content-margin));
width: 100%;
min-width: 100%;
height: 100%;
overflow-y: auto;
width: 100%;
min-width: 100%;
height: 100%;
overflow-y: auto;
scroll-snap-align: center;
scroll-snap-align: center;
}
#home-page #overview-section,
#home-page #chat-section {
padding-bottom: calc(
var(--content-padding) + var(--header-height) + var(--safe-area-bottom)
);
padding-bottom: calc(
var(--content-padding) + var(--header-height) + var(--safe-area-bottom)
);
}
#home-page #chat-grid {
margin-top: var(--content-padding);
margin-top: var(--content-padding);
display: grid;
gap: var(--gap-between-tiles);
display: grid;
gap: var(--gap-between-tiles);
grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
}
.chat-entry[unread] {
border: var(--outline-width) solid black;
border: var(--outline-width) solid black;
}
.chat-entry[unread]:not(:hover):not(:active) {
background: rgb(255, 217, 0) !important;
color: black;
background: rgb(255, 217, 0) !important;
color: black;
}
@media (min-width: 640px) {
#home-page > div {
overflow-x: hidden;
}
#home-page > div {
overflow-x: hidden;
}
#home-page #overview-section {
width: 20rem;
min-width: 20rem;
}
#home-page #overview-section {
width: 20rem;
min-width: 20rem;
}
#home-page #chat-section {
flex: 1;
min-width: unset;
}
#home-page #chat-section {
flex: 1;
min-width: unset;
}
}
@media (min-width: 1200px) {
#home-page #overview-section,
#home-page #chat-section {
padding-left: 3rem;
padding-right: 3rem;
}
#home-page #overview-section,
#home-page #chat-section {
padding-left: 3rem;
padding-right: 3rem;
}
#home-page #chat-grid {
margin-top: 3rem;
}
#home-page #chat-grid {
margin-top: 3rem;
}
#home-page #overview-section {
width: var(--max-input-width);
min-width: var(--max-input-width);
}
#home-page #overview-section {
width: var(--max-input-width);
min-width: var(--max-input-width);
}
}
+183 -171
View File
@@ -13,184 +13,196 @@ import StorageViewModel from "../ViewModel/Global/storageViewModel";
import { translations } from "./translations";
export function HomePage(
storageViewModel: StorageViewModel,
settingsViewModel: SettingsViewModel,
connectionViewModel: ConnectionViewModel,
fileTransferViewModel: FileTransferViewModel,
chatListViewModel: ChatListViewModel
storageViewModel: StorageViewModel,
settingsViewModel: SettingsViewModel,
connectionViewModel: ConnectionViewModel,
fileTransferViewModel: FileTransferViewModel,
chatListViewModel: ChatListViewModel,
) {
// sections
const overviewSection = (
<div id="overview-section">
<h2>{translations.homePage.overviewHeadline}</h2>
// sections
const overviewSection = (
<div id="overview-section">
<h2>{translations.homePage.overviewHeadline}</h2>
<label class="tile flex-no">
<span class="icon">cell_tower</span>
<div>
<span>{translations.homePage.serverAddress}</span>
<input
list="previous-connection-list"
placeholder={translations.homePage.serverAddressPlaceholder}
bind:value={connectionViewModel.serverAddressInput}
on:enter={connectionViewModel.connect}
></input>
<datalist
hidden
id="previous-connection-list"
children:append={[
connectionViewModel.previousAddresses,
StringToOption,
]}
></datalist>
<label class="tile flex-no">
<span class="icon">cell_tower</span>
<div>
<span>{translations.homePage.serverAddress}</span>
<input
list="previous-connection-list"
placeholder={
translations.homePage.serverAddressPlaceholder
}
bind:value={connectionViewModel.serverAddressInput}
on:enter={connectionViewModel.connect}
></input>
<datalist
hidden
id="previous-connection-list"
children:append={[
connectionViewModel.previousAddresses,
StringToOption,
]}
></datalist>
</div>
</label>
<div class="flex-row">
<button
class="danger flex justify-center"
aria-label={translations.homePage.disconnectAudioLabel}
on:click={connectionViewModel.disconnect}
toggle:disabled={connectionViewModel.cannotDisonnect}
>
<span class="icon">link_off</span>
</button>
<button
class="flex justify-center"
aria-label={
translations.homePage.manageConnectionsAudioLabel
}
on:click={connectionViewModel.showConnectionModal}
toggle:disabled={
connectionViewModel.hasNoPreviousConnections
}
>
<span class="icon">history</span>
</button>
<button
class="primary flex justify-center"
aria-label={translations.homePage.connectAudioLabel}
on:click={connectionViewModel.connect}
toggle:disabled={connectionViewModel.cannotConnect}
>
<span class="icon">link</span>
</button>
</div>
<hr></hr>
<label class="tile flex-no">
<span class="icon">account_circle</span>
<div>
<span>{translations.homePage.yourNameLabel}</span>
<input
placeholder={translations.homePage.yourNamePlaceholder}
bind:value={settingsViewModel.usernameInput}
on:enter={settingsViewModel.setName}
></input>
</div>
</label>
<div class="flex-row justify-end">
<button
class="width-50"
on:click={settingsViewModel.setName}
toggle:disabled={settingsViewModel.cannotSetName}
aria-label={translations.homePage.setNameButtonAudioLabel}
>
{translations.general.setButton}
<span class="icon">check</span>
</button>
</div>
<hr></hr>
<label class="tile flex-no">
<span class="icon">calendar_month</span>
<div>
<span>{translations.homePage.firstDayOfWeekLabel}</span>
<select bind:value={settingsViewModel.firstDayOfWeekInput}>
{...translations.regional.weekdays.full.map(
(weekdayName, i) =>
Option(
weekdayName,
i.toString(),
i.toString() ==
settingsViewModel.firstDayOfWeekInput
.value,
),
)}
</select>
<span class="icon">arrow_drop_down</span>
</div>
</label>
<hr></hr>
<button
class="tile flex-no"
on:click={fileTransferViewModel.showDirectionSelectionModal}
>
<span class="icon">sync_alt</span>
<div>
<span>{translations.homePage.transferDataButton}</span>
</div>
</button>
<button
class="tile flex-no"
on:click={storageViewModel.showStorageModal}
>
<span class="icon">hard_drive_2</span>
<div>
<span>{translations.homePage.manageStorageButton}</span>
</div>
</button>
<div class="mobile-only">
<hr></hr>
<div class="flex-row justify-end">
<button class="ghost width-50" on:click={scrollToChat}>
{translations.homePage.scrollToChatButton}
<span class="icon">arrow_forward</span>
</button>
</div>
</div>
</div>
</label>
);
<div class="flex-row">
<button
class="danger flex justify-center"
aria-label={translations.homePage.disconnectAudioLabel}
on:click={connectionViewModel.disconnect}
toggle:disabled={connectionViewModel.cannotDisonnect}
>
<span class="icon">link_off</span>
</button>
<button
class="flex justify-center"
aria-label={translations.homePage.manageConnectionsAudioLabel}
on:click={connectionViewModel.showConnectionModal}
toggle:disabled={connectionViewModel.hasNoPreviousConnections}
>
<span class="icon">history</span>
</button>
<button
class="primary flex justify-center"
aria-label={translations.homePage.connectAudioLabel}
on:click={connectionViewModel.connect}
toggle:disabled={connectionViewModel.cannotConnect}
>
<span class="icon">link</span>
</button>
</div>
const chatSection = (
<div id="chat-section">
<h2>{translations.homePage.chatsHeadline}</h2>
<hr></hr>
<div class="flex-row width-input">
<input
placeholder={translations.homePage.addChatPlaceholder}
aria-label={translations.homePage.addChatAudioLabel}
bind:value={chatListViewModel.newChatPrimaryChannel}
on:enter={chatListViewModel.createChat}
></input>
<button
class="primary"
aria-label={translations.homePage.addChatButton}
on:click={chatListViewModel.createChat}
toggle:disabled={chatListViewModel.cannotCreateChat}
>
<span class="icon">add</span>
</button>
</div>
<label class="tile flex-no">
<span class="icon">account_circle</span>
<div>
<span>{translations.homePage.yourNameLabel}</span>
<input
placeholder={translations.homePage.yourNamePlaceholder}
bind:value={settingsViewModel.usernameInput}
on:enter={settingsViewModel.setName}
></input>
<div
id="chat-grid"
children:append={[
chatListViewModel.chatViewModels,
ChatViewModelToChatEntry,
]}
></div>
</div>
</label>
<div class="flex-row justify-end">
<button
class="width-50"
on:click={settingsViewModel.setName}
toggle:disabled={settingsViewModel.cannotSetName}
aria-label={translations.homePage.setNameButtonAudioLabel}
>
{translations.general.setButton}
<span class="icon">check</span>
</button>
</div>
);
<hr></hr>
// methods
function scrollToChat() {
chatSection.scrollIntoView();
}
<label class="tile flex-no">
<span class="icon">calendar_month</span>
<div>
<span>{translations.homePage.firstDayOfWeekLabel}</span>
<select bind:value={settingsViewModel.firstDayOfWeekInput}>
{...translations.regional.weekdays.full.map((weekdayName, i) =>
Option(
weekdayName,
i.toString(),
i.toString() == settingsViewModel.firstDayOfWeekInput.value
)
)}
</select>
<span class="icon">arrow_drop_down</span>
</div>
</label>
<hr></hr>
<button
class="tile flex-no"
on:click={fileTransferViewModel.showDirectionSelectionModal}
>
<span class="icon">sync_alt</span>
<div>
<span>{translations.homePage.transferDataButton}</span>
</div>
</button>
<button class="tile flex-no" on:click={storageViewModel.showStorageModal}>
<span class="icon">hard_drive_2</span>
<div>
<span>{translations.homePage.manageStorageButton}</span>
</div>
</button>
<div class="mobile-only">
<hr></hr>
<div class="flex-row justify-end">
<button class="ghost width-50" on:click={scrollToChat}>
{translations.homePage.scrollToChatButton}
<span class="icon">arrow_forward</span>
</button>
</div>
</div>
</div>
);
const chatSection = (
<div id="chat-section">
<h2>{translations.homePage.chatsHeadline}</h2>
<div class="flex-row width-input">
<input
placeholder={translations.homePage.addChatPlaceholder}
aria-label={translations.homePage.addChatAudioLabel}
bind:value={chatListViewModel.newChatPrimaryChannel}
on:enter={chatListViewModel.createChat}
></input>
<button
class="primary"
aria-label={translations.homePage.addChatButton}
on:click={chatListViewModel.createChat}
toggle:disabled={chatListViewModel.cannotCreateChat}
>
<span class="icon">add</span>
</button>
</div>
<div
id="chat-grid"
children:append={[
chatListViewModel.chatViewModels,
ChatViewModelToChatEntry,
]}
></div>
</div>
);
// methods
function scrollToChat() {
chatSection.scrollIntoView();
}
// final
return (
<article id="home-page">
<div>
{overviewSection}
{chatSection}
</div>
</article>
);
// final
return (
<article id="home-page">
<div>
{overviewSection}
{chatSection}
</div>
</article>
);
}
+8 -8
View File
@@ -3,12 +3,12 @@ import * as React from "bloatless-react";
import { translations } from "./translations";
export function MainPage() {
return (
<article id="home-page">
<header>
<span>{translations.homePage.appName}</span>
</header>
<div></div>
</article>
);
return (
<article id="home-page">
<header>
<span>{translations.homePage.appName}</span>
</header>
<div></div>
</article>
);
}
+722 -708
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -1,11 +1,11 @@
export function allowDrop(event: DragEvent) {
event.preventDefault();
event.preventDefault();
}
export function allowDrag(event: DragEvent) {
event.dataTransfer?.setData("text", "")
event.dataTransfer?.setData("text", "");
}
export function reload() {
window.location.reload();
}
window.location.reload();
}