brainplug 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- brainplug/__init__.py +3 -0
- brainplug/__main__.py +300 -0
- brainplug/agent.py +749 -0
- brainplug/analysis.py +453 -0
- brainplug/app.py +184 -0
- brainplug/brain.py +184 -0
- brainplug/connect.py +354 -0
- brainplug/demo.py +165 -0
- brainplug/export.py +142 -0
- brainplug/memory.py +356 -0
- brainplug/notion_api.py +135 -0
- brainplug/pipeline.py +95 -0
- brainplug/render.py +47 -0
- brainplug/sources.py +1193 -0
- brainplug/stopwords.py +33 -0
- brainplug/sync.py +109 -0
- brainplug/templates/app.html +212 -0
- brainplug/templates/viewer.html +1050 -0
- brainplug/vendor/INTER_LICENSE +93 -0
- brainplug/vendor/THREE_LICENSE +21 -0
- brainplug/vendor/inter-latin.woff2 +0 -0
- brainplug/vendor/three.module.min.js +6 -0
- brainplug-0.4.0.dist-info/METADATA +364 -0
- brainplug-0.4.0.dist-info/RECORD +28 -0
- brainplug-0.4.0.dist-info/WHEEL +5 -0
- brainplug-0.4.0.dist-info/entry_points.txt +2 -0
- brainplug-0.4.0.dist-info/licenses/LICENSE +21 -0
- brainplug-0.4.0.dist-info/top_level.txt +1 -0
brainplug/stopwords.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Stop words for English and Spanish chats, plus words that are typical of
|
|
2
|
+
how people talk *to* an AI but say nothing about the topic."""
|
|
3
|
+
|
|
4
|
+
from sklearn.feature_extraction.text import ENGLISH_STOP_WORDS
|
|
5
|
+
|
|
6
|
+
SPANISH = """
|
|
7
|
+
a al algo algunas algunos ante antes aquel aquella aquellas aquello aquellos aqui aquí
|
|
8
|
+
asi así aun aún bien cada casi como cómo con contra cual cuál cuales cuando cuándo cuanto
|
|
9
|
+
de del desde donde dónde dos el él ella ellas ello ellos en entre era eran es esa esas ese
|
|
10
|
+
eso esos esta está estaba estaban estamos estan están estar estas este esto estos estoy fue
|
|
11
|
+
fueron ha habia había han hasta hay he la las le les lo los mas más me mi mis mucho muchos
|
|
12
|
+
muy nada ni no nos nosotros o os otra otras otro otros para pero poco por porque que qué
|
|
13
|
+
quien quién se ser si sí sido sin sobre son su sus tambien también tan tanto te tener tengo
|
|
14
|
+
ti tiene tienen todo todos tu tú tus un una unas uno unos usted ustedes va vamos van vez y
|
|
15
|
+
ya yo eres soy hace hacer hago puede puedes puedo pues sea solo sólo ser será sería tenia
|
|
16
|
+
tenía mio mío mia mía tuyo suyo nuestra nuestro vosotros esto oye vale ok okay
|
|
17
|
+
""".split()
|
|
18
|
+
|
|
19
|
+
CHAT_FILLER = """
|
|
20
|
+
explain explica explicame explícame help ayuda ayudame ayúdame example ejemplo ejemplos
|
|
21
|
+
question pregunta quick rapido rápido best mejor way manera forma understand entender
|
|
22
|
+
trying intentando sure gracias thanks thank please porfa porfavor favor want quiero
|
|
23
|
+
quieres need necesito give dame make hazme haz tell dime know saber sabes hola hello hi
|
|
24
|
+
good bueno buena better pasa get got use usar using like como can could would should
|
|
25
|
+
does did do done think creo really realmente something algo thing cosa cosas también
|
|
26
|
+
also just now ahora aqui here there alli allí yes no sí claro okay vale mira look see
|
|
27
|
+
ver new nuevo nueva one dos three tres first primero last último vs versus etc
|
|
28
|
+
http https www com org html saludos regards cheers dear estimado estimada hey abrazo
|
|
29
|
+
buenas buenos dias días tardes noches kind best atentamente cordial cordialmente un
|
|
30
|
+
una note notes nota notas todo page página pagina re fwd
|
|
31
|
+
""".split()
|
|
32
|
+
|
|
33
|
+
STOP_WORDS = sorted(set(ENGLISH_STOP_WORDS) | set(SPANISH) | set(CHAT_FILLER))
|
brainplug/sync.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Keep your brainplug up to date.
|
|
2
|
+
|
|
3
|
+
``brainplug sync --init …`` saves which files and folders make up your record
|
|
4
|
+
in ``brainplug.json``. After that, ``brainplug sync`` rebuilds it in one
|
|
5
|
+
step, and ``brainplug sync --watch`` rebuilds whenever something changes: a
|
|
6
|
+
new note in your vault, a fresh export, a new capture file from the browser
|
|
7
|
+
extension.
|
|
8
|
+
|
|
9
|
+
To use it on several devices, keep the output (or the ``--vault`` notes) in a
|
|
10
|
+
folder that already syncs: iCloud, Google Drive, Dropbox, Obsidian Sync.
|
|
11
|
+
Brainplug has no servers of its own.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import hashlib
|
|
17
|
+
import json
|
|
18
|
+
import time
|
|
19
|
+
from dataclasses import asdict, fields
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Callable
|
|
22
|
+
|
|
23
|
+
from .pipeline import Options, run
|
|
24
|
+
|
|
25
|
+
CONFIG = "brainplug.json"
|
|
26
|
+
_PATHS = ("out", "vault", "json", "dataset")
|
|
27
|
+
MAX_FILES = 200_000
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def write_config(o: Options, path: str | Path = CONFIG) -> Path:
|
|
31
|
+
path = Path(path)
|
|
32
|
+
data = asdict(o)
|
|
33
|
+
data["inputs"] = [str(Path(p).expanduser().resolve()) for p in o.inputs]
|
|
34
|
+
data["rename"] = {str(k + 1): v for k, v in o.rename.items()} # people count topics from 1
|
|
35
|
+
path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
36
|
+
return path
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def read_config(path: str | Path = CONFIG) -> Options:
|
|
40
|
+
path = Path(path)
|
|
41
|
+
if not path.exists():
|
|
42
|
+
raise FileNotFoundError(f"No {path.name} here. Create it with: brainplug sync --init <your files and folders>")
|
|
43
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
44
|
+
known = {f.name for f in fields(Options)}
|
|
45
|
+
unknown = sorted(set(data) - known)
|
|
46
|
+
if unknown:
|
|
47
|
+
raise ValueError(f"{path.name}: unknown setting(s): {', '.join(unknown)}")
|
|
48
|
+
base = path.resolve().parent
|
|
49
|
+
|
|
50
|
+
def rel(p): # paths in the file are relative to the file
|
|
51
|
+
p = Path(str(p)).expanduser()
|
|
52
|
+
return str(p if p.is_absolute() else base / p)
|
|
53
|
+
|
|
54
|
+
data["inputs"] = [rel(p) for p in data.get("inputs") or []]
|
|
55
|
+
for key in _PATHS:
|
|
56
|
+
if data.get(key):
|
|
57
|
+
data[key] = rel(data[key])
|
|
58
|
+
data["rename"] = {int(k) - 1: v for k, v in (data.get("rename") or {}).items()}
|
|
59
|
+
return Options(**data)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def fingerprint(inputs: list[str]) -> str:
|
|
63
|
+
"""Changes whenever a file under the inputs is added, removed or modified."""
|
|
64
|
+
h = hashlib.sha1()
|
|
65
|
+
count = 0
|
|
66
|
+
for raw in inputs:
|
|
67
|
+
root = Path(raw)
|
|
68
|
+
if not root.exists():
|
|
69
|
+
h.update(f"missing:{raw}".encode())
|
|
70
|
+
continue
|
|
71
|
+
files = [root] if root.is_file() else sorted(
|
|
72
|
+
p for p in root.rglob("*")
|
|
73
|
+
if p.is_file() and not any(part.startswith(".") for part in p.relative_to(root).parts))
|
|
74
|
+
for p in files:
|
|
75
|
+
try:
|
|
76
|
+
st = p.stat()
|
|
77
|
+
except OSError:
|
|
78
|
+
continue
|
|
79
|
+
h.update(f"{p}|{st.st_size}|{st.st_mtime_ns}".encode())
|
|
80
|
+
count += 1
|
|
81
|
+
if count >= MAX_FILES:
|
|
82
|
+
break
|
|
83
|
+
return h.hexdigest()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def watch(config: str | Path = CONFIG, every: float = 30.0, say: Callable[[str], None] = print,
|
|
87
|
+
cycles: int | None = None, build: Callable[[Options], object] | None = None) -> int:
|
|
88
|
+
"""Rebuild now, then again each time the inputs change. Returns the number of builds."""
|
|
89
|
+
build = build or (lambda o: run(o, say))
|
|
90
|
+
builds, seen, pending = 0, None, None
|
|
91
|
+
n = 0
|
|
92
|
+
while cycles is None or n < cycles:
|
|
93
|
+
n += 1
|
|
94
|
+
try:
|
|
95
|
+
o = read_config(config)
|
|
96
|
+
from .sources import session_folders
|
|
97
|
+
now = fingerprint(list(o.inputs) + ([str(p) for p in session_folders()] if o.sessions else []))
|
|
98
|
+
# wait until the files stop changing (a sync client may still be writing)
|
|
99
|
+
if now != seen and (seen is None or now == pending):
|
|
100
|
+
say(f"[{time.strftime('%H:%M:%S')}] " + ("building…" if seen is None else "change detected, rebuilding…"))
|
|
101
|
+
build(o)
|
|
102
|
+
builds, seen, pending = builds + 1, now, None
|
|
103
|
+
else:
|
|
104
|
+
pending = now if now != seen else None
|
|
105
|
+
except (ValueError, FileNotFoundError) as err:
|
|
106
|
+
say(f"[{time.strftime('%H:%M:%S')}] {err}")
|
|
107
|
+
if cycles is None or n < cycles:
|
|
108
|
+
time.sleep(every)
|
|
109
|
+
return builds
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Brainplug</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root { --line: #232328; --text: #f4f4f6; --soft: #c9c9d1; --muted: #8d8d96;
|
|
9
|
+
--spectrum: linear-gradient(90deg, #6f8cff 0%, #a77bff 22%, #ff6ad5 45%, #ff5d7a 62%, #ff8a3c 82%, #ffb62e 100%);
|
|
10
|
+
font-family: "Inter", "Segoe UI", system-ui, -apple-system, sans-serif; }
|
|
11
|
+
* { box-sizing: border-box; }
|
|
12
|
+
body { margin: 0; min-height: 100vh; color: var(--text); background: #000; -webkit-font-smoothing: antialiased; }
|
|
13
|
+
main { max-width: 760px; margin: 0 auto; padding: 56px 22px 64px; }
|
|
14
|
+
.mark { font-weight: 600; letter-spacing: -0.02em; font-size: 17px; margin: 0 0 34px; }
|
|
15
|
+
h1 { font-size: clamp(30px, 5vw, 46px); line-height: 1.08; letter-spacing: -0.03em; font-weight: 600; margin: 0 0 14px; }
|
|
16
|
+
h1 span { background: var(--spectrum); -webkit-background-clip: text; background-clip: text; color: transparent;
|
|
17
|
+
-webkit-box-decoration-break: clone; box-decoration-break: clone; padding-bottom: 0.14em; }
|
|
18
|
+
p.lead { color: var(--soft); margin: 0 0 30px; line-height: 1.5; font-size: 17px; }
|
|
19
|
+
#drop { border: 1px solid #3a3a42; border-radius: 20px; padding: 40px 22px; text-align: center; }
|
|
20
|
+
#drop.over { border-color: transparent; background: linear-gradient(#000, #000) padding-box, var(--spectrum) border-box; }
|
|
21
|
+
#drop b { display: block; font-size: 20px; font-weight: 600; letter-spacing: -0.02em; margin-bottom: 6px; }
|
|
22
|
+
#drop small, .muted { color: var(--muted); font-size: 14px; line-height: 1.5; }
|
|
23
|
+
.btn { all: unset; cursor: pointer; padding: 12px 22px; border-radius: 99px; background: var(--text); color: #000; font-weight: 500; font-size: 15px; border: 1px solid var(--text); }
|
|
24
|
+
.btn.ghost { background: none; color: var(--text); border-color: #3a3a42; }
|
|
25
|
+
.btn[disabled] { opacity: 0.4; cursor: default; }
|
|
26
|
+
.btn:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible, summary:focus-visible { outline: 2px solid #a77bff; outline-offset: 2px; }
|
|
27
|
+
.rowb { display: flex; gap: 10px; justify-content: center; margin-top: 18px; flex-wrap: wrap; }
|
|
28
|
+
#files { margin: 16px 0 0; font-size: 14.5px; }
|
|
29
|
+
#files div { display: flex; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid var(--line); }
|
|
30
|
+
#files div span:last-child { color: var(--muted); }
|
|
31
|
+
details { margin: 20px 0; } summary { cursor: pointer; color: var(--muted); font-size: 14px; }
|
|
32
|
+
textarea, input[type=text], select { width: 100%; background: #000; border: 1px solid #3a3a42; border-radius: 10px;
|
|
33
|
+
color: var(--text); padding: 10px 12px; font: inherit; font-size: 15px; }
|
|
34
|
+
textarea { min-height: 70px; margin-top: 8px; font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 13px; }
|
|
35
|
+
.opts { display: grid; grid-template-columns: 1fr 1fr; gap: 14px 18px; margin: 20px 0 26px; }
|
|
36
|
+
.opts label { font-size: 13.5px; color: var(--soft); display: grid; gap: 6px; }
|
|
37
|
+
.opts .wide { grid-column: 1 / -1; }
|
|
38
|
+
.check { grid-column: 1 / -1; display: flex !important; align-items: center; gap: 9px; font-size: 14.5px !important; }
|
|
39
|
+
.check input { accent-color: #a77bff; width: 16px; height: 16px; }
|
|
40
|
+
#status { margin: 20px 0; font-size: 15px; min-height: 20px; color: var(--soft); }
|
|
41
|
+
#status.err { color: #ff8a9a; }
|
|
42
|
+
#done { display: none; border-top: 1px solid var(--line); padding: 22px 0 0; }
|
|
43
|
+
#done h2 { margin: 0 0 8px; font-size: 22px; font-weight: 600; letter-spacing: -0.02em; }
|
|
44
|
+
#done li { font-size: 14.5px; margin: 4px 0; color: var(--soft); }
|
|
45
|
+
#notes { color: #ffb62e; font-size: 14px; margin: 8px 0 0; }
|
|
46
|
+
ul { padding-left: 18px; }
|
|
47
|
+
@media (max-width: 600px) { .opts { grid-template-columns: 1fr; } }
|
|
48
|
+
</style>
|
|
49
|
+
</head>
|
|
50
|
+
<body>
|
|
51
|
+
<main>
|
|
52
|
+
<p class="mark">Brainplug</p>
|
|
53
|
+
<h1><span data-t="say"></span></h1>
|
|
54
|
+
<p class="lead" data-t="lead"></p>
|
|
55
|
+
|
|
56
|
+
<div id="drop">
|
|
57
|
+
<b data-t="drop"></b>
|
|
58
|
+
<small data-t="kinds"></small>
|
|
59
|
+
<div class="rowb">
|
|
60
|
+
<button class="btn ghost" id="pickFiles" data-t="pickFiles"></button>
|
|
61
|
+
<button class="btn ghost" id="pickFolder" data-t="pickFolder"></button>
|
|
62
|
+
</div>
|
|
63
|
+
<input type="file" id="inFiles" multiple hidden>
|
|
64
|
+
<input type="file" id="inFolder" webkitdirectory hidden>
|
|
65
|
+
</div>
|
|
66
|
+
<div id="files"></div>
|
|
67
|
+
|
|
68
|
+
<details>
|
|
69
|
+
<summary data-t="pathsSum"></summary>
|
|
70
|
+
<textarea id="paths" spellcheck="false" placeholder="C:\Users\you\Documents\MyVault /Users/you/Downloads/Takeout/Mail/Sent.mbox"></textarea>
|
|
71
|
+
</details>
|
|
72
|
+
|
|
73
|
+
<div class="opts">
|
|
74
|
+
<label><span data-t="title"></span><input type="text" id="title" value="My Brainplug"></label>
|
|
75
|
+
<label><span data-t="lang"></span><select id="lang"><option value="en">English</option><option value="es">Español</option></select></label>
|
|
76
|
+
<label><span data-t="topics"></span><input type="text" id="topics" placeholder="auto"></label>
|
|
77
|
+
<span></span>
|
|
78
|
+
<label class="wide"><span data-t="me"></span><input type="text" id="me" placeholder="you@gmail.com, Your Name" autocomplete="off"></label>
|
|
79
|
+
<label class="check"><input type="checkbox" id="private"><span data-t="private"></span></label>
|
|
80
|
+
<label class="check"><input type="checkbox" id="pdf"><span data-t="pdf"></span></label>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<button class="btn" id="go" data-t="go"></button>
|
|
84
|
+
<button class="btn ghost" id="clear" data-t="clear" style="margin-left:8px"></button>
|
|
85
|
+
<div id="status"></div>
|
|
86
|
+
|
|
87
|
+
<div id="done">
|
|
88
|
+
<h2 data-t="done"></h2>
|
|
89
|
+
<p class="muted" id="sum"></p>
|
|
90
|
+
<p id="notes"></p>
|
|
91
|
+
<ul id="topicList"></ul>
|
|
92
|
+
<div class="rowb" style="justify-content:flex-start">
|
|
93
|
+
<a class="btn" id="open" target="_blank" data-t="open"></a>
|
|
94
|
+
<a class="btn ghost" id="save" data-t="save"></a>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
<p class="muted" style="margin-top:30px" data-t="foot"></p>
|
|
98
|
+
</main>
|
|
99
|
+
<script>
|
|
100
|
+
const TOKEN = '__TOKEN__';
|
|
101
|
+
const ES = (navigator.language || '').toLowerCase().startsWith('es');
|
|
102
|
+
const T = ES ? {
|
|
103
|
+
say: 'Suelta aquí lo que has escrito y mira qué forma tiene.',
|
|
104
|
+
lead: 'Todo se procesa en este ordenador. No se sube nada a internet.',
|
|
105
|
+
drop: 'Suelta aquí tus archivos o carpetas',
|
|
106
|
+
kinds: 'Sirven tal cual te los da cada app: el .zip de ChatGPT, Claude, Notion o Google Takeout, tu carpeta de Obsidian, un .mbox de Gmail, chats de WhatsApp, Word y PDF.',
|
|
107
|
+
pickFiles: 'Elegir archivos', pickFolder: 'Elegir carpeta',
|
|
108
|
+
pathsSum: 'O pega la ruta de una carpeta o archivo (mejor para buzones y carpetas muy grandes)',
|
|
109
|
+
title: 'Título', lang: 'Idioma de la página', me: 'Quién eres: tu correo (Gmail) y tu nombre tal como sale en WhatsApp, separados por comas. Opcional.', topics: 'Número de temas',
|
|
110
|
+
private: 'Modo privado: no guardar títulos ni texto en el resultado (para compartirlo)',
|
|
111
|
+
pdf: 'Leer también los PDF que haya dentro de las carpetas (por defecto se saltan)',
|
|
112
|
+
go: 'Crear mi brainplug', clear: 'Vaciar', done: 'Listo', open: 'Abrir mi brainplug', save: 'Descargar HTML',
|
|
113
|
+
foot: 'Funciona solo en 127.0.0.1. Cierra la terminal (Ctrl+C) para parar y borrar las copias temporales.',
|
|
114
|
+
copying: (a, b) => `Copiando ${a} de ${b}…`, analysing: 'Analizando… (los buzones grandes pueden tardar unos minutos)',
|
|
115
|
+
none: 'Añade primero algún archivo o carpeta.', summary: (n, s) => `${n} elementos · ${s}`, filesN: n => `${n} archivos listos`,
|
|
116
|
+
} : {
|
|
117
|
+
say: 'Drop in what you have written and see the shape of it.',
|
|
118
|
+
lead: 'Everything is processed on this computer. Nothing is uploaded.',
|
|
119
|
+
drop: 'Drop your files or folders here',
|
|
120
|
+
kinds: 'Use them exactly as each app gives them to you: the .zip from ChatGPT, Claude, Notion or Google Takeout, your Obsidian folder, a Gmail .mbox, WhatsApp chats, Word and PDF.',
|
|
121
|
+
pickFiles: 'Choose files', pickFolder: 'Choose folder',
|
|
122
|
+
pathsSum: 'Or paste the path of a folder or file (best for very large mailboxes and folders)',
|
|
123
|
+
title: 'Title', lang: 'Page language', me: 'Who you are: your email (Gmail) and your name as it appears in WhatsApp, separated by commas. Optional.', topics: 'Number of topics',
|
|
124
|
+
private: 'Private mode: keep titles and text out of the result (safe to share)',
|
|
125
|
+
pdf: 'Also read the PDFs inside folders (skipped by default)',
|
|
126
|
+
go: 'Build my brainplug', clear: 'Clear', done: 'Done', open: 'Open my brainplug', save: 'Download HTML',
|
|
127
|
+
foot: 'Runs on 127.0.0.1 only. Close the terminal (Ctrl+C) to stop and delete the temporary copies.',
|
|
128
|
+
copying: (a, b) => `Copying ${a} of ${b}…`, analysing: 'Analysing… (large mailboxes can take a few minutes)',
|
|
129
|
+
none: 'Add a file or folder first.', summary: (n, s) => `${n} items · ${s}`, filesN: n => `${n} files ready`,
|
|
130
|
+
};
|
|
131
|
+
document.querySelectorAll('[data-t]').forEach(el => { el.textContent = T[el.dataset.t]; });
|
|
132
|
+
if (ES) document.getElementById('lang').value = 'es';
|
|
133
|
+
const $ = id => document.getElementById(id);
|
|
134
|
+
const api = (path, opts = {}) => fetch(path, { ...opts, headers: { 'X-Brainplug-Token': TOKEN, ...(opts.headers || {}) } });
|
|
135
|
+
|
|
136
|
+
const queue = []; // {file, path}
|
|
137
|
+
const marked = new Set(); // vault roots already flagged
|
|
138
|
+
const KEEP = ['.zip', '.md', '.markdown', '.txt', '.mbox', '.docx', '.pdf'];
|
|
139
|
+
function wanted(path) {
|
|
140
|
+
const parts = path.split('/'), name = parts[parts.length - 1].toLowerCase();
|
|
141
|
+
if (parts.slice(0, -1).some(p => p.startsWith('.') || p === 'node_modules')) return false;
|
|
142
|
+
if (name.endsWith('.json')) return parts.length === 1 || name === 'conversations.json' || name.includes('activ') || name.includes('brainplug');
|
|
143
|
+
if (name.endsWith('.html')) return name.includes('activ'); // Gemini's MyActivity.html
|
|
144
|
+
return KEEP.some(ext => name.endsWith(ext));
|
|
145
|
+
}
|
|
146
|
+
function add(file, path) {
|
|
147
|
+
const vault = path.match(/^([^/]+)\/\.obsidian\//);
|
|
148
|
+
if (vault && !marked.has(vault[1])) { marked.add(vault[1]); queue.push({ file: new Blob(['{}']), path: `${vault[1]}/.obsidian/brainplug.json`, hidden: true }); }
|
|
149
|
+
if (wanted(path)) queue.push({ file, path });
|
|
150
|
+
}
|
|
151
|
+
function list() {
|
|
152
|
+
const shown = queue.filter(q => !q.hidden);
|
|
153
|
+
const groups = new Map();
|
|
154
|
+
shown.forEach(q => { const root = q.path.split('/')[0]; const g = groups.get(root) || { n: 0, size: 0 }; g.n++; g.size += q.file.size; groups.set(root, g); });
|
|
155
|
+
$('files').innerHTML = [...groups].map(([name, g]) => `<div><span>${name.replace(/[<>&]/g, '')}${g.n > 1 ? ` (${g.n})` : ''}</span><span>${g.size < 1e6 ? Math.max(1, Math.round(g.size / 1e3)) + ' KB' : (g.size / 1e6).toFixed(1) + ' MB'}</span></div>`).join('');
|
|
156
|
+
$('status').className = ''; $('status').textContent = shown.length ? T.filesN(shown.length) : '';
|
|
157
|
+
}
|
|
158
|
+
const readAll = reader => new Promise((res, rej) => reader.readEntries(res, rej));
|
|
159
|
+
async function walk(entry, prefix) {
|
|
160
|
+
if (entry.isFile) { const file = await new Promise((res, rej) => entry.file(res, rej)); add(file, prefix + file.name); }
|
|
161
|
+
else if (entry.isDirectory) {
|
|
162
|
+
const reader = entry.createReader();
|
|
163
|
+
for (let batch = await readAll(reader); batch.length; batch = await readAll(reader))
|
|
164
|
+
for (const child of batch) await walk(child, prefix + entry.name + '/');
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const drop = $('drop');
|
|
168
|
+
['dragenter', 'dragover'].forEach(ev => drop.addEventListener(ev, e => { e.preventDefault(); drop.classList.add('over'); }));
|
|
169
|
+
['dragleave', 'drop'].forEach(ev => drop.addEventListener(ev, e => { e.preventDefault(); drop.classList.remove('over'); }));
|
|
170
|
+
drop.addEventListener('drop', async e => {
|
|
171
|
+
const entries = [...e.dataTransfer.items].map(i => i.webkitGetAsEntry && i.webkitGetAsEntry()).filter(Boolean);
|
|
172
|
+
if (entries.length) for (const entry of entries) await walk(entry, '');
|
|
173
|
+
else [...e.dataTransfer.files].forEach(f => add(f, f.name));
|
|
174
|
+
list();
|
|
175
|
+
});
|
|
176
|
+
$('pickFiles').onclick = () => $('inFiles').click();
|
|
177
|
+
$('pickFolder').onclick = () => $('inFolder').click();
|
|
178
|
+
['inFiles', 'inFolder'].forEach(id => $(id).addEventListener('change', e => { [...e.target.files].forEach(f => add(f, f.webkitRelativePath || f.name)); e.target.value = ''; list(); }));
|
|
179
|
+
$('clear').onclick = async () => { queue.length = 0; marked.clear(); await api('/reset', { method: 'POST' }); list(); $('done').style.display = 'none'; };
|
|
180
|
+
|
|
181
|
+
$('go').onclick = async () => {
|
|
182
|
+
const paths = $('paths').value.split('\n').map(s => s.trim()).filter(Boolean);
|
|
183
|
+
const status = $('status');
|
|
184
|
+
status.className = '';
|
|
185
|
+
if (!queue.length && !paths.length) { status.className = 'err'; status.textContent = T.none; return; }
|
|
186
|
+
$('go').disabled = true; $('done').style.display = 'none';
|
|
187
|
+
try {
|
|
188
|
+
await api('/reset', { method: 'POST' });
|
|
189
|
+
for (let i = 0; i < queue.length; i++) {
|
|
190
|
+
status.textContent = T.copying(i + 1, queue.length);
|
|
191
|
+
const r = await api('/upload?name=' + encodeURIComponent(queue[i].path), { method: 'PUT', body: queue[i].file });
|
|
192
|
+
if (!r.ok) throw new Error('upload failed: ' + queue[i].path);
|
|
193
|
+
}
|
|
194
|
+
status.textContent = T.analysing;
|
|
195
|
+
const res = await (await api('/build', { method: 'POST', body: JSON.stringify({
|
|
196
|
+
paths, title: $('title').value, lang: $('lang').value, me: $('me').value,
|
|
197
|
+
topics: parseInt($('topics').value) || null, private: $('private').checked, pdf: $('pdf').checked,
|
|
198
|
+
}) })).json();
|
|
199
|
+
if (!res.ok) throw new Error(res.error);
|
|
200
|
+
status.textContent = '';
|
|
201
|
+
$('sum').textContent = T.summary(res.items.toLocaleString(), res.sources.map(s => `${s.name} ${s.count}`).join(' · '));
|
|
202
|
+
$('notes').textContent = (res.notes || []).join(' ');
|
|
203
|
+
$('topicList').innerHTML = res.topics.map(t => `<li>${t.name.replace(/[<>&]/g, '')} <span class="muted">· ${t.count}</span></li>`).join('');
|
|
204
|
+
$('open').href = '/result?t=' + TOKEN; $('save').href = '/result?download=1&t=' + TOKEN;
|
|
205
|
+
$('done').style.display = 'block';
|
|
206
|
+
$('done').scrollIntoView({ behavior: 'smooth' });
|
|
207
|
+
} catch (err) { status.className = 'err'; status.textContent = err.message; }
|
|
208
|
+
$('go').disabled = false;
|
|
209
|
+
};
|
|
210
|
+
</script>
|
|
211
|
+
</body>
|
|
212
|
+
</html>
|