implement auto reconnect
This commit is contained in:
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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>;
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+4
-4
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user