Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add preview tabs #54

Merged
merged 3 commits into from
Apr 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions packages/marqus-desktop/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,7 @@ export async function main(): Promise<void> {
app.on("ready", createWindow);
}
} catch (err) {
logger.error("Error: Failed to initialize app.");
logger.error(err);
logger.error("Error: Failed to initialize app.", err);

mainWindow.webContents.openDevTools();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { z } from "zod";
import { ModelViewState, Section } from "../../../shared/ui/app";
import { DATE_OR_STRING_SCHEMA, PX_REGEX } from "../../../shared/domain";
import { NoteSort } from "../../../shared/domain/note";
import { AppStateV3 } from "./3_addIsPinned";

export interface AppStateV4 {
version: number;
sidebar: Sidebar;
editor: Editor;
focused: Section[];
}

interface Sidebar {
searchString?: string;
hidden?: boolean;
width: string;
scroll: number;
selected?: string[];
expanded?: string[];
sort: NoteSort;
}

interface Editor {
isEditing: boolean;
scroll: number;
tabs: EditorTab[];
tabsScroll: number;
activeTabNoteId?: string;
}

interface EditorTab {
noteId: string;
noteContent?: string;
lastActive?: Date | string;
viewState?: ModelViewState["viewState"];
isPinned?: boolean;
isPreview?: boolean;
}

export const appStateV4 = z.preprocess(
obj => {
const state = obj as AppStateV3 | AppStateV4;
if (state.version === 3) {
state.version = 4;
}

return state;
},
z.object({
version: z.literal(4),
sidebar: z.object({
width: z.string().regex(PX_REGEX),
scroll: z.number(),
hidden: z.boolean().optional(),
selected: z.array(z.string()).optional(),
expanded: z.array(z.string()).optional(),
sort: z.nativeEnum(NoteSort),
searchString: z.string().optional(),
}),
editor: z.object({
isEditing: z.boolean(),
scroll: z.number(),
tabs: z.array(
z.object({
noteId: z.string(),
// Intentionally omitted noteContent
lastActive: DATE_OR_STRING_SCHEMA.optional(),
viewState: z.any().optional(),
isPinned: z.boolean().optional(),
isPreview: z.boolean().optional(),
}),
),
tabsScroll: z.number(),
activeTabNoteId: z.string().optional(),
}),
focused: z.array(z.nativeEnum(Section)),
}),
);
2 changes: 2 additions & 0 deletions packages/marqus-desktop/src/main/schemas/appState/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { appStateV1 } from "./1_initialDefinition";
import { appStateV2 } from "./2_addViewState";
import { appStateV3 } from "./3_addIsPinned";
import { appStateV4 } from "./4_addIsPreview";

export const APP_STATE_SCHEMAS = {
1: appStateV1,
2: appStateV2,
3: appStateV3,
4: appStateV4,
};
1 change: 1 addition & 0 deletions packages/marqus-desktop/src/renderer/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ export async function loadInitialState(
note: getNoteById(notes, t.noteId, false),
lastActive: t.lastActive,
isPinned: t.isPinned,
isPreview: t.isPreview,
}))
.filter(t => t.note != null) as EditorTab[];

Expand Down
3 changes: 3 additions & 0 deletions packages/marqus-desktop/src/renderer/components/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ const setContent: Listener<"editor.setContent"> = async ({ value }, ctx) => {
if (prev.editor.tabs[index].isNewNote) {
delete prev.editor.tabs[index].isNewNote;
}
if (prev.editor.tabs[index].isPreview) {
delete prev.editor.tabs[index].isPreview;
}

return {
editor: {
Expand Down
9 changes: 6 additions & 3 deletions packages/marqus-desktop/src/renderer/components/EditorTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export interface EditorTabProps {
noteName: string;
active?: boolean;
isPinned?: boolean;
isPreview?: boolean;
onClick: (noteId: string) => void;
onClose: (noteId: string) => void;
onUnpin: (noteId: string) => void;
Expand Down Expand Up @@ -109,7 +110,7 @@ export function EditorTab(props: EditorTabProps): JSX.Element {
<StyledTab active={active}>
<FlexRow>
<StyledNoteIcon icon={faFile} size="lg" />
<StyledText>{noteName}</StyledText>
<StyledText isPreview={props.isPreview}>{noteName}</StyledText>
</FlexRow>
</StyledTab>
</CursorFollower>,
Expand Down Expand Up @@ -148,7 +149,7 @@ export function EditorTab(props: EditorTabProps): JSX.Element {
<StyledTab ref={wrapper} title={notePath} active={active}>
<FlexRow>
<StyledNoteIcon icon={faFile} size="lg" />
<StyledText>{noteName}</StyledText>
<StyledText isPreview={props.isPreview}>{noteName}</StyledText>
</FlexRow>
{action}
</StyledTab>
Expand Down Expand Up @@ -209,13 +210,15 @@ const StyledTab = styled.a<{ active?: boolean }>`
}
`;

const StyledText = styled.span`
const StyledText = styled.span<{ isPreview?: boolean }>`
font-size: 1.2rem;
font-weight: 500;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
min-width: 0;

font-style: ${p => (p.isPreview ? "italic" : "normal")};
`;

const StyledDeleteIcon = styled(Icon)`
Expand Down
96 changes: 64 additions & 32 deletions packages/marqus-desktop/src/renderer/components/EditorToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export function EditorToolbar(props: EditorToolbarProps): JSX.Element {
notePath={notePath}
active={activeTabNoteId === note.id}
isPinned={tab.isPinned}
isPreview={tab.isPreview}
onClick={onClick}
onClose={onClose}
onUnpin={onUnpin}
Expand Down Expand Up @@ -231,10 +232,15 @@ export function EditorToolbar(props: EditorToolbarProps): JSX.Element {
}

ctx.setCache(prev => {
const newlyClosedTabs: ClosedEditorTab[] = noteIdsToClose.map(noteId => ({
noteId,
previousIndex: editor.tabs.findIndex(t => t.note.id === noteId),
}));
const newlyClosedTabs: ClosedEditorTab[] = noteIdsToClose.map(noteId => {
const previousIndex = editor.tabs.findIndex(t => t.note.id === noteId);

return {
noteId,
previousIndex,
isPreview: editor.tabs[previousIndex].isPreview,
};
});

const closedTabs = [...newlyClosedTabs, ...prev.closedTabs];

Expand Down Expand Up @@ -449,12 +455,11 @@ const TabsScrollable = styled(Scrollable)`

export const openTab: Listener<"editor.openTab"> = async (ev, ctx) => {
// Keep in sync with sidebar.openSelectedNotes listener
const { editor, notes } = ctx.getState();

if (ev.value?.note == null) {
return;
}

const { notes } = ctx.getState();
const notesToOpen = arrayify(ev.value.note);
const noteIds: string[] = [];
let activeTabNoteId: string | undefined = undefined;
Expand All @@ -481,30 +486,7 @@ export const openTab: Listener<"editor.openTab"> = async (ev, ctx) => {
return;
}

const tabs = [...editor.tabs];

for (const noteId of noteIds) {
let newTab = false;
let tab = editor.tabs.find(t => t.note.id === noteId);

if (tab == null) {
newTab = true;
const note = getNoteById(notes, noteId);
tab = { note };
}

tab.lastActive = new Date();

if (newTab) {
tabs.push(tab);
}
}

ctx.setUI({
editor: {
tabs,
},
});
openTabsForNotes(ctx, noteIds);

setActiveTab(ctx, activeTabNoteId);

Expand All @@ -524,7 +506,7 @@ export const reopenClosedTab: Listener<"editor.reopenClosedTab"> = async (
return;
}

const { noteId, previousIndex } = closedTabs[0];
const { noteId, previousIndex, isPreview } = closedTabs[0];
ctx.setCache(prev => {
const closedTabs = prev.closedTabs.slice(1);

Expand All @@ -545,7 +527,7 @@ export const reopenClosedTab: Listener<"editor.reopenClosedTab"> = async (
const { tabs } = prev.editor;

const newIndex = Math.min(previousIndex, tabs.length);
tabs.splice(newIndex, 0, { note });
tabs.splice(newIndex, 0, { note, isPreview });

return {
editor: {
Expand Down Expand Up @@ -683,6 +665,56 @@ export const moveTab: Listener<"editor.moveTab"> = async ({ value }, ctx) => {
});
};

export function openTabsForNotes(ctx: StoreContext, noteIds: string[]): void {
if (noteIds.length === 0) {
return;
}

const { editor, notes } = ctx.getState();
let tabs = [...editor.tabs];

for (const noteId of noteIds) {
let newTab = false;
let tab = editor.tabs.find(t => t.note.id === noteId);

if (tab == null) {
newTab = true;
const note = getNoteById(notes, noteId);
tab = { note };

// Only open tabs in preview mode if we opened a single tab.
if (noteIds.length === 1) {
tab.isPreview = true;

// Only one preview tab can be open at once.
tabs = tabs.filter(t => !t.isPreview);
}
}
// Second open of a tab takes it out of preview mode.
else if (tab.isPreview) {
delete tab.isPreview;
}

tab.lastActive = new Date();

if (newTab) {
tabs.push(tab);
}
}

let { activeTabNoteId } = editor;
if (!activeTabNoteId) {
activeTabNoteId = tabs[0].note.id;
}

ctx.setUI({
editor: {
tabs,
activeTabNoteId,
},
});
}

export function setActiveTab(
ctx: StoreContext,
activeTabNoteId: string | undefined,
Expand Down
38 changes: 5 additions & 33 deletions packages/marqus-desktop/src/renderer/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,10 @@ import {
faChevronRight,
} from "@fortawesome/free-solid-svg-icons";
import { SidebarSearch } from "./SidebarSearch";
import { EditorTab } from "../../shared/ui/app";
import { SidebarNewNoteButton } from "./SidebarNewNoteButton";
import { Section } from "../../shared/ui/app";
import { deleteNoteIfConfirmed } from "../utils/deleteNoteIfConfirmed";
import { cleanupClosedTabsCache } from "./EditorToolbar";
import { cleanupClosedTabsCache, openTabsForNotes } from "./EditorToolbar";

const EXPANDED_ICON = faChevronDown;
const COLLAPSED_ICON = faChevronRight;
Expand Down Expand Up @@ -723,47 +722,20 @@ export const openSelectedNotes: Listener<"sidebar.openSelectedNotes"> = async (
ctx,
) => {
// Keep in sync with editor.openTab listener
const { sidebar, editor, notes } = ctx.getState();
const { selected } = sidebar;
const {
sidebar: { selected },
} = ctx.getState();

if (selected == null || selected.length === 0) {
return;
}

const notesToOpen = selected.map(s => getNoteById(notes, s));
const tabs = [...editor.tabs];

let firstTab: EditorTab | undefined;
for (const note of notesToOpen) {
let newTab = false;
let tab = editor.tabs.find(t => t.note.id === note.id);

if (tab == null) {
newTab = true;
tab = { note };
}

tab.lastActive = new Date();

if (newTab) {
tabs.push(tab);
}

if (firstTab == null) {
firstTab = tab;
}
}
openTabsForNotes(ctx, selected);

// Editor is not set as focused when a note is opened from the sidebar because
// the user may not want to start editing the note yet. This makes it easier
// to delete a note because otherwise each time they clicked on a note, they'd
// have to click back into the editor and then hit delete.
ctx.setUI(prev => ({
editor: {
tabs,
activeTabNoteId: firstTab?.note.id ?? prev.editor.activeTabNoteId,
},
}));

cleanupClosedTabsCache(ctx);
};
Expand Down
Loading