mirror of
https://github.com/1f349/mail.1f349.com.git
synced 2025-02-22 06:04:57 +00:00
Email folder sidebar
This commit is contained in:
parent
a7a0008eb1
commit
ceaa664d78
@ -21,5 +21,8 @@
|
|||||||
"tslib": "^2.6.0",
|
"tslib": "^2.6.0",
|
||||||
"typescript": "^5.0.2",
|
"typescript": "^5.0.2",
|
||||||
"vite": "^4.4.5"
|
"vite": "^4.4.5"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"lucide-svelte": "^0.292.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,12 +3,22 @@
|
|||||||
import {getBearer, loginStore} from "./stores/login";
|
import {getBearer, loginStore} from "./stores/login";
|
||||||
import {openLoginPopup} from "./utils/login-popup";
|
import {openLoginPopup} from "./utils/login-popup";
|
||||||
import type {ImapFolder} from "./types/imap";
|
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;
|
let mainWS: WebSocket;
|
||||||
$: window.mainWS = mainWS;
|
$: window.mainWS = mainWS;
|
||||||
|
|
||||||
let folders: RootFolder[] = [];
|
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) {
|
function countChar(s: string, c: string) {
|
||||||
let result = 0;
|
let result = 0;
|
||||||
@ -53,12 +63,12 @@
|
|||||||
imapFolders = imapFolders.sort((a, b) => countChar(a.Name, a.Delimiter) - countChar(b.Name, b.Delimiter));
|
imapFolders = imapFolders.sort((a, b) => countChar(a.Name, a.Delimiter) - countChar(b.Name, b.Delimiter));
|
||||||
|
|
||||||
// Setup root folders
|
// Setup root folders
|
||||||
let INBOX: RootFolder = {role: "Inbox", name: "Inbox", attr: new Set(), children: []};
|
let INBOX: RootFolder = {role: "Inbox", name: "Inbox", path: "", attr: new Set(), children: []};
|
||||||
let DRAFTS: RootFolder = {role: "Drafts", name: "Drafts", attr: new Set(["\\\\Drafts"]), children: []};
|
let DRAFTS: RootFolder = {role: "Drafts", name: "Drafts", path: "", attr: new Set(["\\Drafts"]), children: []};
|
||||||
let SENT: RootFolder = {role: "Sent", name: "Sent", attr: new Set(["\\\\Sent"]), children: []};
|
let SENT: RootFolder = {role: "Sent", name: "Sent", path: "", attr: new Set(["\\Sent"]), children: []};
|
||||||
let ARCHIVE: RootFolder = {role: "Archive", name: "Archive", attr: new Set(["\\\\Archive"]), children: []};
|
let ARCHIVE: RootFolder = {role: "Archive", name: "Archive", path: "", attr: new Set(["\\Archive"]), children: []};
|
||||||
let JUNK: RootFolder = {role: "Junk", name: "Junk", attr: new Set(["\\\\Junk"]), children: []};
|
let JUNK: RootFolder = {role: "Junk", name: "Junk", path: "", attr: new Set(["\\Junk"]), children: []};
|
||||||
let TRASH: RootFolder = {role: "Trash", name: "Trash", attr: new Set(["\\\\Trash"]), children: []};
|
let TRASH: RootFolder = {role: "Trash", name: "Trash", path: "", attr: new Set(["\\Trash"]), children: []};
|
||||||
|
|
||||||
// Setup map to find special folders
|
// Setup map to find special folders
|
||||||
let ROOT: Map<string, RootFolder> = new Map();
|
let ROOT: Map<string, RootFolder> = new Map();
|
||||||
@ -132,6 +142,7 @@
|
|||||||
// add child to current parent
|
// add child to current parent
|
||||||
pObj.children.push({
|
pObj.children.push({
|
||||||
name: x.Name.slice(pIdx),
|
name: x.Name.slice(pIdx),
|
||||||
|
path: x.Name,
|
||||||
attr: new Set(x.Attributes),
|
attr: new Set(x.Attributes),
|
||||||
children: [],
|
children: [],
|
||||||
});
|
});
|
||||||
@ -179,21 +190,35 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<div id="sidebar">
|
<div id="sidebar">
|
||||||
{#each folders as folder}
|
{#each folders as folder}
|
||||||
<button class:selected={folder.name === "INBOX"}>{folder.name}</button>
|
<TreePath data={folder} selected={selectedFolder.name} on:select={n => changeSelectedFolder(n.detail)} />
|
||||||
<div>{folder.children.length}</div>
|
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<div id="option-view">
|
<div id="option-view">
|
||||||
<div style="padding:8px;background-color:#bb7900;">Warning: This is currently still under development</div>
|
<div style="padding:8px;background-color:#bb7900;">Warning: This is currently still under development</div>
|
||||||
<button on:click={() => connectWS()}>Connect WS</button>
|
<button on:click={() => connectWS()}>Connect WS</button>
|
||||||
<div>
|
|
||||||
<code>
|
|
||||||
<pre>{JSON.stringify(folders, null, 2)}</pre>
|
|
||||||
</code>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</main>
|
</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">
|
<style lang="scss">
|
||||||
header {
|
header {
|
||||||
@ -254,11 +279,12 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
height: calc(100% - 70px);
|
height: 0;
|
||||||
|
|
||||||
#sidebar {
|
#sidebar {
|
||||||
width: 150px;
|
width: auto;
|
||||||
min-width: 150px;
|
min-width: 250px;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
button {
|
button {
|
||||||
background-color: #2c2c2c;
|
background-color: #2c2c2c;
|
||||||
@ -294,4 +320,13 @@
|
|||||||
flex-grow: 1;
|
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>
|
</style>
|
||||||
|
@ -45,6 +45,7 @@ $theme-bg: #242424;
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-content: stretch;
|
align-content: stretch;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
@ -59,6 +60,7 @@ a:hover {
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
code,
|
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 {
|
export interface TreeFolder {
|
||||||
name: string;
|
name: string;
|
||||||
|
path: string;
|
||||||
attr: Set<string>;
|
attr: Set<string>;
|
||||||
children: TreeFolder[];
|
children: TreeFolder[];
|
||||||
}
|
}
|
||||||
@ -7,3 +8,8 @@ export interface TreeFolder {
|
|||||||
export interface RootFolder extends TreeFolder {
|
export interface RootFolder extends TreeFolder {
|
||||||
role: string;
|
role: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FolderSelection {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
"composite": true,
|
"composite": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
|
@ -1,5 +1,19 @@
|
|||||||
import {defineConfig} from "vite";
|
import {defineConfig} from "vite";
|
||||||
import {svelte} from "@sveltejs/vite-plugin-svelte";
|
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/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
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"
|
resolved "https://registry.yarnpkg.com/locate-character/-/locate-character-3.0.0.tgz#0305c5b8744f61028ef5d01f444009e00779f974"
|
||||||
integrity sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==
|
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:
|
magic-string@^0.27.0:
|
||||||
version "0.27.0"
|
version "0.27.0"
|
||||||
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.27.0.tgz#e4a3413b4bab6d98d2becffd48b4a257effdbbf3"
|
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.27.0.tgz#e4a3413b4bab6d98d2becffd48b4a257effdbbf3"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user