This commit is contained in:
marlon-erler
2026-09-26 23:32:38 +02:00
parent ce9f6d89f0
commit 9622c196cd
40 changed files with 2866 additions and 2706 deletions
+4 -1
View File
@@ -46,7 +46,10 @@ export function BoardPage(
);
const wrapper = (
<div class="content main-content allow-drag-move" children:set={pages}></div>
<div
class="content main-content allow-drag-move"
children:set={pages}
></div>
);
Utility.implementPinchZoom(wrapper, boardViewModel.pinchToZoomData);
+4 -3
View File
@@ -36,11 +36,11 @@
width: 100%;
padding-left: var(--content-padding);
gap: var(--gap-in-controls);
display: flex;
align-items: center;
justify-content: space-between;
background: var(--surface);
}
@@ -285,7 +285,8 @@
padding: var(--gap-between-tiles);
}
#message-page[reply-view] :is(.reply-link, .inline-reply, #composer .reply-preview-wrapper) {
#message-page[reply-view]
:is(.reply-link, .inline-reply, #composer .reply-preview-wrapper) {
display: none;
}
+169 -148
View File
@@ -15,156 +15,177 @@ export function MessagePage(
messagePageViewModel.loadData();
const ChatMessageViewModelToView: React.StateItemConverter<
ChatMessageViewModel
> = (chatMessageViewModel: ChatMessageViewModel) => {
return ChatMessage(coreViewModel, chatMessageViewModel);
};
ChatMessageViewModel
> = (chatMessageViewModel: ChatMessageViewModel) => {
return ChatMessage(coreViewModel, chatMessageViewModel);
};
const messageContainer = (
<div
id="message-container"
children:append={[
messagePageViewModel.filteredMessageViewModels,
ChatMessageViewModelToView,
]}
></div>
);
const messageContainer = (
<div
id="message-container"
children:append={[
messagePageViewModel.filteredMessageViewModels,
ChatMessageViewModelToView,
]}
></div>
);
const persistenceId = messagePageViewModel.chatViewModel.chatModel.id;
const replyPreview = ViewController.inlineReplies.setState(
persistenceId,
() =>
React.createProxyState(
[messagePageViewModel.replyingMessage],
() => {
if (messagePageViewModel.replyingMessage.value == undefined)
return <div></div>;
return ReplyPreview(
coreViewModel,
messagePageViewModel.replyingMessage.value,
);
},
),
);
const persistenceId = messagePageViewModel.chatViewModel.chatModel.id;
const replyPreview = ViewController.inlineReplies.setState(
persistenceId,
() =>
React.createProxyState(
[messagePageViewModel.replyingMessage],
() => {
if (messagePageViewModel.replyingMessage.value == undefined)
return <div></div>;
return ReplyPreview(
coreViewModel,
messagePageViewModel.replyingMessage.value,
);
},
),
);
function scrollDown(hard: boolean = false) {
if (hard) {
messageContainer.setAttribute("scroll-hard", "");
function scrollDown(hard: boolean = false) {
if (hard) {
messageContainer.setAttribute("scroll-hard", "");
}
messageContainer.scrollTop = messageContainer.scrollHeight;
messageContainer.removeAttribute("scroll-hard");
}
messageContainer.scrollTop = messageContainer.scrollHeight;
messageContainer.removeAttribute("scroll-hard");
}
function scrollDownIfApplicable() {
const scrollFromBottom =
messageContainer.scrollHeight -
(messageContainer.scrollTop + messageContainer.offsetHeight);
if (scrollFromBottom > 400) return;
scrollDown();
}
messagePageViewModel.filteredMessageViewModels.subscribeSilent(
scrollDownIfApplicable,
);
React.bulkSubscribe([messagePageViewModel.reactionFilter, messagePageViewModel.searchViewModel.appliedQuery], scrollDown)
messagePageViewModel.replyViewSelectedMessage.subscribeSilent(selectedMessage => {
if (selectedMessage != undefined) return;
setTimeout(scrollDown, 100);
})
setTimeout(() => scrollDown(true), 100);
messagePageViewModel.focusSetter.subscribeSilent(() => {
ViewController.setFocusWithDelay();
});
const isInReplyView = React.createProxyState([messagePageViewModel.replyViewSelectedMessage], ()=>messagePageViewModel.replyViewSelectedMessage.value != undefined);
const title = React.createProxyState([messagePageViewModel.isReplyViewActive], ()=>messagePageViewModel.isReplyViewActive.value ? coreViewModel.translations.chatPage.message.replyHeaderLabel((messagePageViewModel.replyViewSelectedMessage.value!).body.value) : coreViewModel.translations.chatPage.message.messagesHeadline);
return (
<div id="message-page" toggle:reply-view={isInReplyView}>
<div class="pane-wrapper">
<div class="pane">
<div class="toolbar">
<button
class="ghost"
on:click={messagePageViewModel.resetReplyView}
toggle:hidden={messagePageViewModel.isReplyViewInactive}
>
<span class="icon">arrow_back</span>
</button>
<span
class="title ellipsis width-100 flex"
subscribe:innerText={title}
>
</span>
<span>
<button
class="ghost inset-outline"
on:click={messagePageViewModel.showFilterModal}
aria-label={
coreViewModel.translations.chatPage.message
.filterMessagesButtonAudioLabel
}
toggle:selected={
messagePageViewModel.isFilterActive
}
>
<span class="icon">filter_alt</span>
</button>
</span>
</div>
<div class="content">
{messageContainer}
<div id="composer">
<div class="content-width-constraint">
<div
class="reply-preview-wrapper"
children:set={replyPreview}
></div>
<div class="input-width-constraint">
<input
id="focused"
class="blur"
bind:value={
messagePageViewModel.composingMessage
}
on:enter={
messagePageViewModel.sendMessage
}
placeholder={
coreViewModel.translations.chatPage
.message
.composerInputPlaceholder
}
></input>
<button
class="primary blur"
aria-label={
coreViewModel.translations.chatPage
.message
.sendMessageButtonAudioLabel
}
on:click={
messagePageViewModel.sendMessage
}
toggle:disabled={
messagePageViewModel.cannotSendMessage
}
>
<span class="icon">send</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
{MessageFilterModal(
coreViewModel,
messagePageViewModel,
ChatMessageViewModelToView,
)}
</div>
);
function scrollDownIfApplicable() {
const scrollFromBottom =
messageContainer.scrollHeight -
(messageContainer.scrollTop + messageContainer.offsetHeight);
if (scrollFromBottom > 400) return;
scrollDown();
}
messagePageViewModel.filteredMessageViewModels.subscribeSilent(
scrollDownIfApplicable,
);
React.bulkSubscribe(
[
messagePageViewModel.reactionFilter,
messagePageViewModel.searchViewModel.appliedQuery,
],
scrollDown,
);
messagePageViewModel.replyViewSelectedMessage.subscribeSilent(
(selectedMessage) => {
if (selectedMessage != undefined) return;
setTimeout(scrollDown, 100);
},
);
setTimeout(() => scrollDown(true), 100);
messagePageViewModel.focusSetter.subscribeSilent(() => {
ViewController.setFocusWithDelay();
});
const isInReplyView = React.createProxyState(
[messagePageViewModel.replyViewSelectedMessage],
() => messagePageViewModel.replyViewSelectedMessage.value != undefined,
);
const title = React.createProxyState(
[messagePageViewModel.isReplyViewActive],
() =>
messagePageViewModel.isReplyViewActive.value
? coreViewModel.translations.chatPage.message.replyHeaderLabel(
messagePageViewModel.replyViewSelectedMessage.value!.body
.value,
)
: coreViewModel.translations.chatPage.message.messagesHeadline,
);
return (
<div id="message-page" toggle:reply-view={isInReplyView}>
<div class="pane-wrapper">
<div class="pane">
<div class="toolbar">
<button
class="ghost"
on:click={messagePageViewModel.resetReplyView}
toggle:hidden={
messagePageViewModel.isReplyViewInactive
}
>
<span class="icon">arrow_back</span>
</button>
<span
class="title ellipsis width-100 flex"
subscribe:innerText={title}
></span>
<span>
<button
class="ghost inset-outline"
on:click={messagePageViewModel.showFilterModal}
aria-label={
coreViewModel.translations.chatPage.message
.filterMessagesButtonAudioLabel
}
toggle:selected={
messagePageViewModel.isFilterActive
}
>
<span class="icon">filter_alt</span>
</button>
</span>
</div>
<div class="content">
{messageContainer}
<div id="composer">
<div class="content-width-constraint">
<div
class="reply-preview-wrapper"
children:set={replyPreview}
></div>
<div class="input-width-constraint">
<input
id="focused"
class="blur"
bind:value={
messagePageViewModel.composingMessage
}
on:enter={
messagePageViewModel.sendMessage
}
placeholder={
coreViewModel.translations.chatPage
.message
.composerInputPlaceholder
}
></input>
<button
class="primary blur"
aria-label={
coreViewModel.translations.chatPage
.message
.sendMessageButtonAudioLabel
}
on:click={
messagePageViewModel.sendMessage
}
toggle:disabled={
messagePageViewModel.cannotSendMessage
}
>
<span class="icon">send</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
{MessageFilterModal(
coreViewModel,
messagePageViewModel,
ChatMessageViewModelToView,
)}
</div>
);
}
+8 -15
View File
@@ -47,12 +47,8 @@ export function SettingsPage(
}
</span>
<input
bind:value={
settingsPageViewModel.nameInput
}
on:enter={
settingsPageViewModel.setName
}
bind:value={settingsPageViewModel.nameInput}
on:enter={settingsPageViewModel.setName}
></input>
</div>
</label>
@@ -63,9 +59,7 @@ export function SettingsPage(
coreViewModel.translations.chatPage.settings
.setNameButtonAudioLabel
}
on:click={
settingsPageViewModel.setName
}
on:click={settingsPageViewModel.setName}
toggle:disabled={
settingsPageViewModel.cannotSetPrimaryChannel
}
@@ -177,7 +171,7 @@ export function SettingsPage(
<hr></hr>
{ColorPicker(settingsPageViewModel.color)}
{ColorPicker(settingsPageViewModel.color)}
<div class="flex-row justify-end width-input">
<button
@@ -186,17 +180,16 @@ export function SettingsPage(
coreViewModel.translations.chatPage.settings
.setColorButtonAudioLabel
}
on:click={
settingsPageViewModel.applyColor
}
toggle:disabled={settingsPageViewModel.cannotSetColor}
on:click={settingsPageViewModel.applyColor}
toggle:disabled={
settingsPageViewModel.cannotSetColor
}
>
{coreViewModel.translations.general.setButton}
<span class="icon">check</span>
</button>
</div>
<hr></hr>
<div class="width-input">
+9 -4
View File
@@ -2,11 +2,11 @@ import "./taskPage.css";
import { BoardPage } from "./boardPage";
import { ViewController } from "../viewController";
import { PlaceholderView } from "../Components/placeholderView";
import { NewItemEntry } from "../Components/newItemEntry";
import { BoardViewModelToEntry } from "../Components/boardEntry";
import * as React from "../../react";
import TaskPageViewModel from "../../ViewModel/Pages/taskPageViewModel";
import CoreViewModel from "../../ViewModel/Global/coreViewModel";
import {NewItemEntry} from "../Components/newItemEntry";
export function TaskPage(
coreViewModel: CoreViewModel,
@@ -66,13 +66,18 @@ export function TaskPage(
bind:value={taskPageViewModel.boardQuery}
on:enter={taskPageViewModel.createBoard}
placeholder={
coreViewModel.translations.general.filterOrCreateLabel
coreViewModel.translations.general
.filterOrCreateLabel
}
></input>
</div>
</div>
<div class="content gap">
{NewItemEntry(coreViewModel, taskPageViewModel.boardQuery, taskPageViewModel.createBoard)}
<div class="content gap">
{NewItemEntry(
coreViewModel,
taskPageViewModel.boardQuery,
taskPageViewModel.createBoard,
)}
<div
class="grid gap"
style="grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr))"
+2 -6
View File
@@ -12,14 +12,10 @@ export function ChatEntry(chatViewModel: ChatViewModel) {
>
<span
class="shadow"
subscribe:innerText={
chatViewModel.settingsPageViewModel.name
}
subscribe:innerText={chatViewModel.settingsPageViewModel.name}
></span>
<h2
subscribe:innerText={
chatViewModel.settingsPageViewModel.name
}
subscribe:innerText={chatViewModel.settingsPageViewModel.name}
></h2>
</button>
);
+6 -3
View File
@@ -1,3 +1,4 @@
import { ReplyLink } from "./replyLink";
import { MessageReactionButtonRow } from "./messageReactionButtonRow";
import { InlineReply } from "./inlineReply";
import { ChatMessageInfoModal } from "../Modals/chatMessageInfoModal";
@@ -5,7 +6,6 @@ import * as React from "../../react";
import CoreViewModel from "../../ViewModel/Global/coreViewModel";
import ChatMessageViewModel from "../../ViewModel/Chat/chatMessageViewModel";
import { ChatMessageStatuses } from "../../Model/Chat/chatModel";
import {ReplyLink} from "./replyLink";
export function ChatMessage(
coreViewModel: CoreViewModel,
@@ -69,8 +69,11 @@ export function ChatMessage(
aria-label={
coreViewModel.translations.chatPage.message
.replyToMessageButton
}
toggle:hidden={chatMessageViewModel.messagePageViewModel.isReplyViewActive}
}
toggle:hidden={
chatMessageViewModel.messagePageViewModel
.isReplyViewActive
}
>
<span class="icon">reply</span>
</button>
+19 -11
View File
@@ -1,16 +1,24 @@
import * as React from "../../react";
import CoreViewModel from "../../ViewModel/Global/coreViewModel";
export function NewItemEntry(coreViewModel: CoreViewModel, query: React.State<string>, fn: ()=>void) {
const isHidden = React.createProxyState([query],()=>query.value == "");
const label = React.createProxyState([query],()=>coreViewModel.translations.general.createLabel(query.value));
export function NewItemEntry(
coreViewModel: CoreViewModel,
query: React.State<string>,
fn: () => void,
) {
const isHidden = React.createProxyState([query], () => query.value == "");
const label = React.createProxyState([query], () =>
coreViewModel.translations.general.createLabel(query.value),
);
return <button
class="standard slide-up"
toggle:hidden={isHidden}
on:click={fn}
>
<span subscribe:innerText={label}></span>
<span class="icon">add</span>
</button>
return (
<button
class="standard slide-up"
toggle:hidden={isHidden}
on:click={fn}
>
<span subscribe:innerText={label}></span>
<span class="icon">add</span>
</button>
);
}
+20 -11
View File
@@ -1,22 +1,31 @@
import * as React from "../../react";
import ChatMessageViewModel from "../../ViewModel/Chat/chatMessageViewModel";
import CoreViewModel from "../../ViewModel/Global/coreViewModel";
import ChatMessageViewModel from "../../ViewModel/Chat/chatMessageViewModel";
export function ReplyLink(coreViewModel: CoreViewModel, chatMessageViewModel: ChatMessageViewModel) {
const isHidden = React.createProxyState([chatMessageViewModel.replyCount], ()=>chatMessageViewModel.replyCount.value == 0);
export function ReplyLink(
coreViewModel: CoreViewModel,
chatMessageViewModel: ChatMessageViewModel,
) {
const isHidden = React.createProxyState(
[chatMessageViewModel.replyCount],
() => chatMessageViewModel.replyCount.value == 0,
);
function select() {
chatMessageViewModel.messagePageViewModel.setReplyView(chatMessageViewModel);
chatMessageViewModel.messagePageViewModel.setReplyView(
chatMessageViewModel,
);
}
return (
<div
class="reply-link"
toggle:hidden={isHidden}
on:click={select}
>
<span>{coreViewModel.translations.chatPage.message.replyPrefixLabel}</span>
<b class="ellipsis" subscribe:innerText={chatMessageViewModel.replyCount}></b>
<div class="reply-link" toggle:hidden={isHidden} on:click={select}>
<span>
{coreViewModel.translations.chatPage.message.replyPrefixLabel}
</span>
<b
class="ellipsis"
subscribe:innerText={chatMessageViewModel.replyCount}
></b>
</div>
);
}
+40 -18
View File
@@ -1,26 +1,48 @@
import * as React from "../../react";
import CoreViewModel from "../../ViewModel/Global/coreViewModel";
import MessagePageViewModel from "../../ViewModel/Pages/messagePageViewModel";
import CoreViewModel from "../../ViewModel/Global/coreViewModel";
export function ReplyViewHeader(coreViewModel: CoreViewModel, messagePageViewModel: MessagePageViewModel) {
const isHidden = React.createProxyState([messagePageViewModel.isReplyViewActive], ()=>!messagePageViewModel.isReplyViewActive.value);
export function ReplyViewHeader(
coreViewModel: CoreViewModel,
messagePageViewModel: MessagePageViewModel,
) {
const isHidden = React.createProxyState(
[messagePageViewModel.isReplyViewActive],
() => !messagePageViewModel.isReplyViewActive.value,
);
const label = React.createProxyState([messagePageViewModel.replyViewSelectedMessage], ()=>messagePageViewModel.replyViewSelectedMessage.value == undefined ? "" : coreViewModel.translations.chatPage.message.replyHeaderLabel(messagePageViewModel.replyViewSelectedMessage.value.sender));
const message = React.createProxyState([messagePageViewModel.replyViewSelectedMessage], ()=>messagePageViewModel.replyViewSelectedMessage.value == undefined ? "" : messagePageViewModel.replyViewSelectedMessage.value.body.value);
const label = React.createProxyState(
[messagePageViewModel.replyViewSelectedMessage],
() =>
messagePageViewModel.replyViewSelectedMessage.value == undefined
? ""
: coreViewModel.translations.chatPage.message.replyHeaderLabel(
messagePageViewModel.replyViewSelectedMessage.value
.sender,
),
);
const message = React.createProxyState(
[messagePageViewModel.replyViewSelectedMessage],
() =>
messagePageViewModel.replyViewSelectedMessage.value == undefined
? ""
: messagePageViewModel.replyViewSelectedMessage.value.body
.value,
);
return (
<div
class="reply-header"
toggle:hidden={isHidden}
>
<b class="ellipsis" subscribe:innerText={label}></b>
<span class="secondary ellipsis" subscribe:innerText={message}></span>
<button
class="standard square"
on:click={messagePageViewModel.resetReplyView}
>
<span class="icon">close</span>
</button>
</div>
<div class="reply-header" toggle:hidden={isHidden}>
<b class="ellipsis" subscribe:innerText={label}></b>
<span
class="secondary ellipsis"
subscribe:innerText={message}
></span>
<button
class="standard square"
on:click={messagePageViewModel.resetReplyView}
>
<span class="icon">close</span>
</button>
</div>
);
}
+42 -40
View File
@@ -11,9 +11,11 @@ export function TaskEntry(taskViewModel: TaskViewModel) {
schedule: taskViewModel.time.value || "---",
};
const className = React.createProxyState([taskViewModel.isNotNext], ()=>
`${taskViewModel.isNotNext.value ? "standard" : "primary"} tile flex-no`
)
const className = React.createProxyState(
[taskViewModel.isNotNext],
() =>
`${taskViewModel.isNotNext.value ? "standard" : "primary"} tile flex-no`,
);
const view = (
<button
@@ -22,45 +24,45 @@ export function TaskEntry(taskViewModel: TaskViewModel) {
style="user-select: none; -webkit-user-select: none"
on:click={taskViewModel.open}
on:dragstart={taskViewModel.dragStart}
>
<div>
<span
class="secondary"
toggle:hidden={taskViewModel.isNotNext}
>
{taskViewModel.coreViewModel.translations.chatPage.calendar.eventNext}
<hr></hr>
</span>
<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>
);
>
<div>
<span class="secondary" toggle:hidden={taskViewModel.isNotNext}>
{
taskViewModel.coreViewModel.translations.chatPage
.calendar.eventNext
}
<hr></hr>
</span>
<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> = (
+20 -20
View File
@@ -1,26 +1,26 @@
import * as React from "../../react";
export function UsageBar(label: string, valueLabel: React.State<string>, value: React.State<number>, maximum: React.State<number>) {
const style = React.createProxyState([value, maximum], ()=>`width: ${100*(value.value/maximum.value)}%`)
export function UsageBar(
label: string,
valueLabel: React.State<string>,
value: React.State<number>,
maximum: React.State<number>,
) {
const style = React.createProxyState(
[value, maximum],
() => `width: ${100 * (value.value / maximum.value)}%`,
);
return (
<div
class="surface flex-column padding gap"
>
<b>{label}</b>
<div
class="width-100 surface-alt"
style="height: 18px"
>
<div
class="height-100 background-primary"
set:style={style}
></div>
</div>
<span
class="secondary"
subscribe:innerText={valueLabel}
></span>
</div>
<div class="surface flex-column padding gap">
<b>{label}</b>
<div class="width-100 surface-alt" style="height: 18px">
<div
class="height-100 background-primary"
set:style={style}
></div>
</div>
<span class="secondary" subscribe:innerText={valueLabel}></span>
</div>
);
}
+7 -4
View File
@@ -72,10 +72,13 @@ export function ChatMessageInfoModal(
}
<span class="icon">key</span>
</button>
<button
on:click={chatMessageViewModel.reply}
toggle:disabled={chatMessageViewModel.messagePageViewModel.isReplyViewActive}
>
<button
on:click={chatMessageViewModel.reply}
toggle:disabled={
chatMessageViewModel.messagePageViewModel
.isReplyViewActive
}
>
{
coreViewModel.translations.chatPage.message
.replyToMessageButton
+95 -92
View File
@@ -1,10 +1,10 @@
import { v4 } from "uuid";
import { ViewController } from "../viewController";
import { StringToOption } from "../Components/option";
import { DeletableListItem } from "../Components/deletableListItem";
import * as React from "../../react";
import SearchViewModel from "../../ViewModel/Utility/searchViewModel";
import CoreViewModel from "../../ViewModel/Global/coreViewModel";
import {DeletableListItem} from "../Components/deletableListItem";
export function SearchModal<T>(
coreViewModel: CoreViewModel,
@@ -13,92 +13,92 @@ export function SearchModal<T>(
isOpen: React.State<boolean>,
) {
function close() {
isOpen.value = false;
isOpen.value = false;
}
const suggestionId = v4();
isOpen.subscribe((isOpen) => {
if (!isOpen) return;
ViewController.setFocusWithDelay();
if (!isOpen) return;
ViewController.setFocusWithDelay();
});
return (
<div class="modal" toggle:open={isOpen} extended>
<div>
<main>
<h2>{headline}</h2>
<div class="flex-row">
<input
id="focused"
style="max-width: unset"
placeholder={
coreViewModel.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="standard"
aria-label={
coreViewModel.translations.general
.searchButtonClearAudioLabel
}
on:click={searchViewModel.clear}
toggle:disabled={searchViewModel.cannotClear}
>
<span class="icon">close</span>
</button>
<button
class="primary"
aria-label={
coreViewModel.translations.general
.searchButtonAudioLabel
}
on:click={searchViewModel.applySearch}
toggle:disabled={searchViewModel.cannotApplySearch}
>
<span class="icon">search</span>
</button>
</div>
<div class="modal" toggle:open={isOpen} extended>
<div>
<main>
<h2>{headline}</h2>
<div class="flex-row">
<input
id="focused"
style="max-width: unset"
placeholder={
coreViewModel.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="standard"
aria-label={
coreViewModel.translations.general
.searchButtonClearAudioLabel
}
on:click={searchViewModel.clear}
toggle:disabled={searchViewModel.cannotClear}
>
<span class="icon">close</span>
</button>
<button
class="primary"
aria-label={
coreViewModel.translations.general
.searchButtonAudioLabel
}
on:click={searchViewModel.applySearch}
toggle:disabled={searchViewModel.cannotApplySearch}
>
<span class="icon">search</span>
</button>
</div>
<div toggle:hidden={searchViewModel.hasNoSuggestions}>
<hr></hr>
<h3>
{
coreViewModel.translations.general
.searchSuggestionsLabel
}
</h3>
<div
class="flex-column gap"
children:append={[
searchViewModel.suggestions,
(suggestion: string) =>
SuggestionView(
suggestion,
searchViewModel,
coreViewModel,
),
]}
></div>
</div>
</main>
<button on:click={close}>
{coreViewModel.translations.general.closeButton}
<span class="icon">close</span>
</button>
</div>
</div>
<div toggle:hidden={searchViewModel.hasNoSuggestions}>
<hr></hr>
<h3>
{
coreViewModel.translations.general
.searchSuggestionsLabel
}
</h3>
<div
class="flex-column gap"
children:append={[
searchViewModel.suggestions,
(suggestion: string) =>
SuggestionView(
suggestion,
searchViewModel,
coreViewModel,
),
]}
></div>
</div>
</main>
<button on:click={close}>
{coreViewModel.translations.general.closeButton}
<span class="icon">close</span>
</button>
</div>
</div>
);
}
@@ -108,26 +108,29 @@ function SuggestionView<T>(
coreViewModel: CoreViewModel,
) {
const isApplied = React.createProxyState(
[searchViewModel.appliedQuery],
() => searchViewModel.appliedQuery.value == suggestion,
[searchViewModel.appliedQuery],
() => searchViewModel.appliedQuery.value == suggestion,
);
function deleteSuggestion() {
searchViewModel.deleteSuggestion(suggestion);
searchViewModel.deleteSuggestion(suggestion);
}
function applySuggestion() {
searchViewModel.search(suggestion);
searchViewModel.search(suggestion);
}
return DeletableListItem(coreViewModel, suggestion,
<button
class="primary"
aria-label={coreViewModel.translations.general.applyButton}
on:click={applySuggestion}
toggle:disabled={isApplied}
>
<span class="icon">check</span>
</button>
, deleteSuggestion)
return DeletableListItem(
coreViewModel,
suggestion,
<button
class="primary"
aria-label={coreViewModel.translations.general.applyButton}
on:click={applySuggestion}
toggle:disabled={isApplied}
>
<span class="icon">check</span>
</button>,
deleteSuggestion,
);
}
+65 -70
View File
@@ -125,25 +125,20 @@ function SettingsInfoPane(
settingsViewModel.coreViewModel.version,
)}
<button
class="primary"
on:click={coreViewModel.update}
toggle:hidden={coreViewModel.noUpdateAvailable}
>
<span
subscribe:innerText={coreViewModel.updateText}
></span>
<span class="icon">update</span>
</button>
<button
class="primary"
on:click={coreViewModel.update}
toggle:hidden={coreViewModel.noUpdateAvailable}
>
<span subscribe:innerText={coreViewModel.updateText}></span>
<span class="icon">update</span>
</button>
<button
class="standard"
on:click={coreViewModel.checkUpdates}
>
{coreViewModel.translations.settings.checkUpdatesButton}
</button>
</div>
</div>
<button class="standard" on:click={coreViewModel.checkUpdates}>
{coreViewModel.translations.settings.checkUpdatesButton}
</button>
</div>
</div>
);
}
@@ -152,30 +147,30 @@ function SettingsRegionalPane(
settingsViewModel: SettingsViewModel,
) {
return (
<div class="slide-up">
<h2>{coreViewModel.translations.settings.pages.regional}</h2>
<div class="slide-up">
<h2>{coreViewModel.translations.settings.pages.regional}</h2>
<hr></hr>
<h3>{coreViewModel.translations.settings.language}</h3>
{OptionButtonList(
new React.ListState<[string, string]>(
Object.values(Languages).map((x) => [languageNames[x], x]),
),
settingsViewModel.language,
)}
<hr></hr>
<h3>{coreViewModel.translations.settings.language}</h3>
{OptionButtonList(
new React.ListState<[string, string]>(
Object.values(Languages).map((x) => [languageNames[x], x]),
),
settingsViewModel.language,
)}
<hr></hr>
<h3>{coreViewModel.translations.settings.firstDayOfWeekLabel}</h3>
{OptionButtonList(
new React.ListState<[string, string]>(
coreViewModel.translations.regional.weekdays.full.map(
(x, i) => [x, i.toString()],
),
),
settingsViewModel.firstDayOfWeek,
)}
</div>
);
<hr></hr>
<h3>{coreViewModel.translations.settings.firstDayOfWeekLabel}</h3>
{OptionButtonList(
new React.ListState<[string, string]>(
coreViewModel.translations.regional.weekdays.full.map(
(x, i) => [x, i.toString()],
),
),
settingsViewModel.firstDayOfWeek,
)}
</div>
);
}
function SettingsAppearancePane(
@@ -183,34 +178,34 @@ function SettingsAppearancePane(
settingsViewModel: SettingsViewModel,
) {
return (
<div class="slide-up">
<h2>{coreViewModel.translations.settings.pages.appearance}</h2>
<hr></hr>
{OptionButtonList(
new React.ListState<[string, string]>([
[
coreViewModel.translations.settings.themes.dynamic,
ThemeSettings.Dynamic,
],
[
coreViewModel.translations.settings.themes.system,
ThemeSettings.System,
],
[
coreViewModel.translations.settings.themes.dark,
ThemeSettings.Dark,
],
[
coreViewModel.translations.settings.themes.light,
ThemeSettings.Light,
],
[
coreViewModel.translations.settings.themes.black,
ThemeSettings.Black,
],
]),
settingsViewModel.theme,
)}
</div>
);
<div class="slide-up">
<h2>{coreViewModel.translations.settings.pages.appearance}</h2>
<hr></hr>
{OptionButtonList(
new React.ListState<[string, string]>([
[
coreViewModel.translations.settings.themes.dynamic,
ThemeSettings.Dynamic,
],
[
coreViewModel.translations.settings.themes.system,
ThemeSettings.System,
],
[
coreViewModel.translations.settings.themes.dark,
ThemeSettings.Dark,
],
[
coreViewModel.translations.settings.themes.light,
ThemeSettings.Light,
],
[
coreViewModel.translations.settings.themes.black,
ThemeSettings.Black,
],
]),
settingsViewModel.theme,
)}
</div>
);
}
+81 -70
View File
@@ -1,94 +1,105 @@
import { SplitModal } from "./splitModal";
import { UsageBar } from "../Components/usageBar";
import { DirectoryItemList } from "../Components/directoryItemList";
import { DangerousActionButton } from "../Components/dangerousActionButton";
import * as React from "../../react";
import StorageViewModel from "../../ViewModel/Global/storageViewModel";
import CoreViewModel from "../../ViewModel/Global/coreViewModel";
import { PATH_COMPONENT_SEPARATOR } from "../../Model/Global/storageModel";
import {UsageBar} from "../Components/usageBar";
export function StorageModal(
coreViewModel: CoreViewModel,
storageViewModel: StorageViewModel,
) {
const usageValueLabel = React.createProxyState([storageViewModel.occupiedSpaceMB, storageViewModel.maximumSpaceMB], ()=>
coreViewModel.translations.storage.usageVauleLabel(storageViewModel.occupiedSpaceMB.value, storageViewModel.maximumSpaceMB.value));
const usageValueLabel = React.createProxyState(
[storageViewModel.occupiedSpaceMB, storageViewModel.maximumSpaceMB],
() =>
coreViewModel.translations.storage.usageVauleLabel(
storageViewModel.occupiedSpaceMB.value,
storageViewModel.maximumSpaceMB.value,
),
);
const detailView = React.createProxyState(
[storageViewModel.selectedPath],
() => {
if (storageViewModel.selectedPath.value == PATH_COMPONENT_SEPARATOR)
return (
<div class="flex-column gap">
<span class="secondary">
{coreViewModel.translations.storage.noItemSelected}
</span>
[storageViewModel.selectedPath],
() => {
if (storageViewModel.selectedPath.value == PATH_COMPONENT_SEPARATOR)
return (
<div class="flex-column gap">
<span class="secondary">
{coreViewModel.translations.storage.noItemSelected}
</span>
<hr></hr>
<hr></hr>
{UsageBar(coreViewModel.translations.storage.usageLabel, usageValueLabel, storageViewModel.occupiedSpaceMB, storageViewModel.maximumSpaceMB)}
{UsageBar(
coreViewModel.translations.storage.usageLabel,
usageValueLabel,
storageViewModel.occupiedSpaceMB,
storageViewModel.maximumSpaceMB,
)}
<hr></hr>
<hr></hr>
{DangerousActionButton(
coreViewModel,
coreViewModel.translations.storage.removeJunkButton,
"delete_forever",
storageViewModel.removeJunk,
)}
</div>
);
{DangerousActionButton(
coreViewModel,
coreViewModel.translations.storage.removeJunkButton,
"delete_forever",
storageViewModel.removeJunk,
)}
</div>
);
return (
<div class="flex-column gap">
<div class="tile flex-no">
<div>
<b>{coreViewModel.translations.storage.path}</b>
<span
class="break-all"
subscribe:innerText={
storageViewModel.selectedPath
}
></span>
</div>
</div>
<div class="tile flex-no">
<div>
<b>{coreViewModel.translations.storage.content}</b>
<code
subscribe:innerText={
storageViewModel.selectedFileContent
}
></code>
</div>
</div>
return (
<div class="flex-column gap">
<div class="tile flex-no">
<div>
<b>{coreViewModel.translations.storage.path}</b>
<span
class="break-all"
subscribe:innerText={
storageViewModel.selectedPath
}
></span>
</div>
</div>
<div class="tile flex-no">
<div>
<b>{coreViewModel.translations.storage.content}</b>
<code
subscribe:innerText={
storageViewModel.selectedFileContent
}
></code>
</div>
</div>
{DangerousActionButton(
coreViewModel,
coreViewModel.translations.storage.deleteItem,
"delete_forever",
storageViewModel.deleteSelectedItem,
)}
</div>
);
},
{DangerousActionButton(
coreViewModel,
coreViewModel.translations.storage.deleteItem,
"delete_forever",
storageViewModel.deleteSelectedItem,
)}
</div>
);
},
);
return (
<div class="modal" toggle:open={storageViewModel.isShowingStorageModal}>
<div>
<main class="padding-0">
{SplitModal(
coreViewModel,
new React.State(DirectoryItemList(storageViewModel)),
detailView,
true,
)}
</main>
<button on:click={storageViewModel.close}>
{coreViewModel.translations.general.closeButton}
<span class="icon">close</span>
</button>
</div>
</div>
<div class="modal" toggle:open={storageViewModel.isShowingStorageModal}>
<div>
<main class="padding-0">
{SplitModal(
coreViewModel,
new React.State(DirectoryItemList(storageViewModel)),
detailView,
true,
)}
</main>
<button on:click={storageViewModel.close}>
{coreViewModel.translations.general.closeButton}
<span class="icon">close</span>
</button>
</div>
</div>
);
}
+174 -176
View File
@@ -20,195 +20,193 @@ export function HomePage(
) {
// sections
const overviewSection = (
<div id="overview-section">
<h2>{coreViewModel.translations.homePage.overviewHeadline}</h2>
<div id="overview-section">
<h2>{coreViewModel.translations.homePage.overviewHeadline}</h2>
<label class="tile flex-no">
<span class="icon">cell_tower</span>
<div>
<span>
{coreViewModel.translations.homePage.serverAddress}
</span>
<input
list="previous-connection-list"
placeholder={
coreViewModel.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>
<label class="tile flex-no">
<span class="icon">cell_tower</span>
<div>
<span>
{coreViewModel.translations.homePage.serverAddress}
</span>
<input
list="previous-connection-list"
placeholder={
coreViewModel.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={
coreViewModel.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={
coreViewModel.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={
coreViewModel.translations.homePage.connectAudioLabel
}
on:click={connectionViewModel.connect}
toggle:disabled={connectionViewModel.cannotConnect}
>
<span class="icon">link</span>
</button>
</div>
<div class="flex-row">
<button
class="danger flex justify-center"
aria-label={
coreViewModel.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={
coreViewModel.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={
coreViewModel.translations.homePage.connectAudioLabel
}
on:click={connectionViewModel.connect}
toggle:disabled={connectionViewModel.cannotConnect}
>
<span class="icon">link</span>
</button>
</div>
<hr></hr>
<hr></hr>
<label class="tile flex-no">
<span class="icon">account_circle</span>
<div>
<span>
{coreViewModel.translations.homePage.yourNameLabel}
</span>
<input
placeholder={
coreViewModel.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={
coreViewModel.translations.homePage
.setNameButtonAudioLabel
}
>
{coreViewModel.translations.general.setButton}
<span class="icon">check</span>
</button>
</div>
<label class="tile flex-no">
<span class="icon">account_circle</span>
<div>
<span>
{coreViewModel.translations.homePage.yourNameLabel}
</span>
<input
placeholder={
coreViewModel.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={
coreViewModel.translations.homePage
.setNameButtonAudioLabel
}
>
{coreViewModel.translations.general.setButton}
<span class="icon">check</span>
</button>
</div>
<hr></hr>
<hr></hr>
{HomePageButton(
settingsViewModel.showSettingsModal,
coreViewModel.translations.homePage.settingsButton,
"settings",
)}
{HomePageButton(
fileTransferViewModel.showDirectionSelectionModal,
coreViewModel.translations.homePage.transferDataButton,
"sync_alt",
)}
{HomePageButton(
storageViewModel.showStorageModal,
coreViewModel.translations.homePage.manageStorageButton,
"hard_drive",
)}
{HomePageButton(
settingsViewModel.showSettingsModal,
coreViewModel.translations.homePage.settingsButton,
"settings",
)}
{HomePageButton(
fileTransferViewModel.showDirectionSelectionModal,
coreViewModel.translations.homePage.transferDataButton,
"sync_alt",
)}
{HomePageButton(
storageViewModel.showStorageModal,
coreViewModel.translations.homePage.manageStorageButton,
"hard_drive",
)}
<button
class="primary"
on:click={coreViewModel.update}
toggle:hidden={coreViewModel.noUpdateAvailable}
>
<span
subscribe:innerText={coreViewModel.updateText}
></span>
<span class="icon">update</span>
</button>
<button
class="primary"
on:click={coreViewModel.update}
toggle:hidden={coreViewModel.noUpdateAvailable}
>
<span subscribe:innerText={coreViewModel.updateText}></span>
<span class="icon">update</span>
</button>
<div class="mobile-only">
<hr></hr>
<div class="mobile-only">
<hr></hr>
<div class="flex-row justify-end">
<button class="ghost width-50" on:click={scrollToChat}>
{coreViewModel.translations.homePage.scrollToChatButton}
<span class="icon">arrow_forward</span>
</button>
</div>
</div>
</div>
);
<div class="flex-row justify-end">
<button class="ghost width-50" on:click={scrollToChat}>
{coreViewModel.translations.homePage.scrollToChatButton}
<span class="icon">arrow_forward</span>
</button>
</div>
</div>
</div>
);
const chatSection = (
<div id="chat-section">
<h2>{coreViewModel.translations.homePage.chatsHeadline}</h2>
const chatSection = (
<div id="chat-section">
<h2>{coreViewModel.translations.homePage.chatsHeadline}</h2>
<div class="flex-row width-input">
<input
placeholder={
coreViewModel.translations.homePage.addChatPlaceholder
}
aria-label={
coreViewModel.translations.homePage.addChatAudioLabel
}
bind:value={chatListViewModel.newChatPrimaryChannel}
on:enter={chatListViewModel.createChat}
></input>
<button
class="primary"
aria-label={
coreViewModel.translations.homePage.addChatButton
}
on:click={chatListViewModel.createChat}
toggle:disabled={chatListViewModel.cannotCreateChat}
>
<span class="icon">add</span>
</button>
</div>
<div class="flex-row width-input">
<input
placeholder={
coreViewModel.translations.homePage.addChatPlaceholder
}
aria-label={
coreViewModel.translations.homePage.addChatAudioLabel
}
bind:value={chatListViewModel.newChatPrimaryChannel}
on:enter={chatListViewModel.createChat}
></input>
<button
class="primary"
aria-label={
coreViewModel.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>
);
<div
id="chat-grid"
children:append={[
chatListViewModel.chatViewModels,
ChatViewModelToChatEntry,
]}
></div>
</div>
);
// methods
function scrollToChat() {
chatSection.scrollIntoView();
}
// 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>
);
}
+790 -786
View File
File diff suppressed because it is too large Load Diff