mirror of
https://github.com/1f349/mail.1f349.com.git
synced 2025-02-21 13:45:05 +00:00
Email folder sidebar
This commit is contained in:
parent
a7a0008eb1
commit
ceaa664d78
@ -21,5 +21,8 @@
|
||||
"tslib": "^2.6.0",
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "^4.4.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"lucide-svelte": "^0.292.0"
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
128
src/components/TreePath.svelte
Normal file
128
src/components/TreePath.svelte
Normal 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>
|
@ -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;
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
|
@ -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({
|
||||
|
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user