Email folder sidebar

This commit is contained in:
Melon 2023-11-20 22:06:16 +00:00
parent a7a0008eb1
commit ceaa664d78
Signed by: melon
GPG Key ID: 6C9D970C50D26A25
8 changed files with 211 additions and 17 deletions

View File

@ -21,5 +21,8 @@
"tslib": "^2.6.0",
"typescript": "^5.0.2",
"vite": "^4.4.5"
},
"dependencies": {
"lucide-svelte": "^0.292.0"
}
}

View File

@ -3,12 +3,22 @@
import {getBearer, loginStore} from "./stores/login";
import {openLoginPopup} from "./utils/login-popup";
import type {ImapFolder} from "./types/imap";
import type {TreeFolder, RootFolder} from "./types/internal";
import type {TreeFolder, RootFolder, FolderSelection} from "./types/internal";
import TreePath from "./components/TreePath.svelte";
let mainWS: WebSocket;
$: window.mainWS = mainWS;
let folders: RootFolder[] = [];
let selectedFolder: FolderSelection = {name: "Inbox", path: "INBOX"};
let inboxOption: string = "*";
let inboxOptions: string[] = [];
function changeSelectedFolder(p: FolderSelection) {
selectedFolder = p;
}
$: console.log("Selected", selectedFolder);
function countChar(s: string, c: string) {
let result = 0;
@ -53,12 +63,12 @@
imapFolders = imapFolders.sort((a, b) => countChar(a.Name, a.Delimiter) - countChar(b.Name, b.Delimiter));
// Setup root folders
let INBOX: RootFolder = {role: "Inbox", name: "Inbox", attr: new Set(), children: []};
let DRAFTS: RootFolder = {role: "Drafts", name: "Drafts", attr: new Set(["\\\\Drafts"]), children: []};
let SENT: RootFolder = {role: "Sent", name: "Sent", attr: new Set(["\\\\Sent"]), children: []};
let ARCHIVE: RootFolder = {role: "Archive", name: "Archive", attr: new Set(["\\\\Archive"]), children: []};
let JUNK: RootFolder = {role: "Junk", name: "Junk", attr: new Set(["\\\\Junk"]), children: []};
let TRASH: RootFolder = {role: "Trash", name: "Trash", attr: new Set(["\\\\Trash"]), children: []};
let INBOX: RootFolder = {role: "Inbox", name: "Inbox", path: "", attr: new Set(), children: []};
let DRAFTS: RootFolder = {role: "Drafts", name: "Drafts", path: "", attr: new Set(["\\Drafts"]), children: []};
let SENT: RootFolder = {role: "Sent", name: "Sent", path: "", attr: new Set(["\\Sent"]), children: []};
let ARCHIVE: RootFolder = {role: "Archive", name: "Archive", path: "", attr: new Set(["\\Archive"]), children: []};
let JUNK: RootFolder = {role: "Junk", name: "Junk", path: "", attr: new Set(["\\Junk"]), children: []};
let TRASH: RootFolder = {role: "Trash", name: "Trash", path: "", attr: new Set(["\\Trash"]), children: []};
// Setup map to find special folders
let ROOT: Map<string, RootFolder> = new Map();
@ -132,6 +142,7 @@
// add child to current parent
pObj.children.push({
name: x.Name.slice(pIdx),
path: x.Name,
attr: new Set(x.Attributes),
children: [],
});
@ -179,21 +190,35 @@
{:else}
<div id="sidebar">
{#each folders as folder}
<button class:selected={folder.name === "INBOX"}>{folder.name}</button>
<div>{folder.children.length}</div>
<TreePath data={folder} selected={selectedFolder.name} on:select={n => changeSelectedFolder(n.detail)} />
{/each}
</div>
<div id="option-view">
<div style="padding:8px;background-color:#bb7900;">Warning: This is currently still under development</div>
<button on:click={() => connectWS()}>Connect WS</button>
<div>
<code>
<pre>{JSON.stringify(folders, null, 2)}</pre>
</code>
</div>
</div>
{/if}
</main>
<footer>
<div class="meta-version">
Version: <code>{import.meta.env.VITE_APP_VERSION}</code>
, {import.meta.env.VITE_APP_LASTMOD}
</div>
<div>
<a href="https://github.com/1f349/mail.1f349.com" target="_blank">Source</a>
</div>
<div>
<label>
<span>Inbox:</span>
<select bind:value={inboxOption}>
<option value="*">Default</option>
{#each inboxOptions as inbox}
<option value={inbox}>{inbox}</option>
{/each}
</select>
</label>
</div>
</footer>
<style lang="scss">
header {
@ -254,11 +279,12 @@
display: flex;
flex-grow: 1;
align-items: stretch;
height: calc(100% - 70px);
height: 0;
#sidebar {
width: 150px;
min-width: 150px;
width: auto;
min-width: 250px;
overflow-y: auto;
button {
background-color: #2c2c2c;
@ -294,4 +320,13 @@
flex-grow: 1;
}
}
footer {
padding: 8px;
background-color: #2c2c2c;
box-shadow: 0 -4px 8px #0003, 0 -6px 20px #00000030;
display: flex;
flex-direction: row;
justify-content: space-between;
}
</style>

View File

@ -45,6 +45,7 @@ $theme-bg: #242424;
display: flex;
flex-direction: column;
align-content: stretch;
overflow: hidden;
}
a {
@ -59,6 +60,7 @@ a:hover {
body {
margin: 0;
overflow: hidden;
}
code,

View File

@ -0,0 +1,128 @@
<script lang="ts">
import {slide} from "svelte/transition";
import type {FolderSelection, RootFolder, TreeFolder} from "../types/internal";
import {Archive, ChevronRight, Flame, Folder, Inbox, Send, Text, Trash2} from "lucide-svelte";
import {createEventDispatcher} from "svelte";
let folderIcons: {[key: string]: any} = {
Inbox: Inbox,
Drafts: Text,
Sent: Send,
Archive: Archive,
Junk: Flame,
Trash: Trash2,
};
const dispatch = createEventDispatcher();
export let data: TreeFolder;
let expanded: boolean = false;
export let treeOffset: number = 0;
export let selected: string = "";
function isRootFolder(obj: any): obj is RootFolder {
return "role" in obj;
}
function getFolderIcon(): any {
if (isRootFolder(data)) return folderIcons[data.role];
return Folder;
}
function getNextSelected(full: string): string {
if (full.indexOf(data.name) != 0) return "";
return full.slice(data.name.length + 1);
}
function triggerSelected(n?: FolderSelection) {
dispatch("select", n == undefined ? {name: data.name, path: data.path} : n);
}
</script>
<div class="tree-item" class:expanded class:selected={selected === data.name} style="--tree-offset: {treeOffset};">
<div class="tree-arrow" class:no-children={data.attr.has("\\HasNoChildren")}>
<button on:click={() => (expanded = !expanded)}><ChevronRight /></button>
</div>
<button on:click={() => triggerSelected()} class="tree-icon"><svelte:component this={getFolderIcon()} /></button>
<button on:click={() => triggerSelected()} class="tree-title"><div>{data.name}</div></button>
</div>
{#if expanded}
<div class="tree-children" class:expanded transition:slide style="--tree-offset: {treeOffset};">
{#each data.children as child (child.name)}
<svelte:self
data={child}
treeOffset={treeOffset + 1}
selected={getNextSelected(selected)}
on:select={n => triggerSelected({name: data.name + "/" + n.detail.name, path: n.detail.path})}
/>
{/each}
</div>
{/if}
<style lang="scss">
.tree-item {
display: grid;
grid-template-columns: 32px 32px auto;
padding-left: calc(var(--tree-offset) * 32px);
.tree-arrow {
width: 32px;
height: 32px;
grid-column: 1;
&.no-children {
display: none;
}
> button {
display: flex;
justify-content: center;
align-items: center;
width: 32px;
height: 32px;
background: transparent;
border: none;
padding: 0;
margin: 0;
}
}
.tree-icon {
display: flex;
justify-content: center;
align-items: center;
width: 32px;
height: 32px;
grid-column: 2;
background: transparent;
border: none;
padding: 0;
margin: 0;
}
.tree-title {
display: flex;
justify-content: left;
align-items: center;
grid-column: 3;
background: transparent;
border: none;
padding: 0;
margin: 0;
> div {
white-space: nowrap;
padding-inline: 8px;
}
}
&.expanded > .tree-arrow > button {
transform: rotate(90deg);
}
&:hover,
&.selected {
background-color: #1c1c1c;
}
}
</style>

View File

@ -1,5 +1,6 @@
export interface TreeFolder {
name: string;
path: string;
attr: Set<string>;
children: TreeFolder[];
}
@ -7,3 +8,8 @@ export interface TreeFolder {
export interface RootFolder extends TreeFolder {
role: string;
}
export interface FolderSelection {
name: string;
path: string;
}

View File

@ -1,5 +1,6 @@
{
"compilerOptions": {
"target": "ESNext",
"composite": true,
"skipLibCheck": true,
"module": "ESNext",

View File

@ -1,5 +1,19 @@
import {defineConfig} from "vite";
import {svelte} from "@sveltejs/vite-plugin-svelte";
import {exec} from "child_process";
import {promisify} from "util";
// Get current tag/commit and last commit date from git
const pexec = promisify(exec);
let [version, lastmod] = (
await Promise.allSettled([
pexec("git describe --tags || git rev-parse --short HEAD"),
pexec('git log -1 --format=%cd --date=format:"%Y-%m-%d %H:%M"'),
])
).map(v => (v as {value?: {stdout: string}}).value?.stdout.trim());
process.env.VITE_APP_VERSION = version;
process.env.VITE_APP_LASTMOD = lastmod;
// https://vitejs.dev/config/
export default defineConfig({

View File

@ -504,6 +504,11 @@ locate-character@^3.0.0:
resolved "https://registry.yarnpkg.com/locate-character/-/locate-character-3.0.0.tgz#0305c5b8744f61028ef5d01f444009e00779f974"
integrity sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==
lucide-svelte@^0.292.0:
version "0.292.0"
resolved "https://registry.yarnpkg.com/lucide-svelte/-/lucide-svelte-0.292.0.tgz#75a6f07ca37b50f870bc04e8218bd91412b19755"
integrity sha512-bnTpg9pbm6pQDc+YiLK2yxtRFk2Cc+hbzwjAPaV85k56x10CJ9LsXjon6wRrlNTSdxJR7GOsRjz0A5ZNu3Z7dg==
magic-string@^0.27.0:
version "0.27.0"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.27.0.tgz#e4a3413b4bab6d98d2becffd48b4a257effdbbf3"