First Alpha
This commit is contained in:
@ -1,7 +1,6 @@
|
||||
import { h } from "preact";
|
||||
import "./add_button.scss";
|
||||
import Plus from "feather-icons/dist/icons/plus.svg";
|
||||
|
||||
export default function AddButton({ onClick }: { onClick: () => void }) {
|
||||
return <button title={"add button"} class="add_button_button circular primary shadowed" onClick={() => onClick()}><Plus width={undefined} height={undefined} /></button>
|
||||
}
|
@ -2,11 +2,13 @@ import { Router } from "./Routing";
|
||||
import { h } from "preact";
|
||||
import NotificationsComponent from "./notifications";
|
||||
import { ModalComponent } from "./modals/Modal";
|
||||
import { Footer } from "./Footer";
|
||||
|
||||
export default function App() {
|
||||
return <div>
|
||||
<ModalComponent />
|
||||
<NotificationsComponent />
|
||||
<Router />
|
||||
<Footer />
|
||||
</div>
|
||||
}
|
70
src/components/Footer.tsx
Normal file
70
src/components/Footer.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import { h, Component } from "preact";
|
||||
import Notes from "../notes";
|
||||
import Refresh from "feather-icons/dist/icons/refresh-cw.svg";
|
||||
import "./footer.scss";
|
||||
import Notifications from "../notifications";
|
||||
|
||||
export class Footer extends Component<{}, { synced: boolean, syncing: boolean }> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { synced: false, syncing: false };
|
||||
this.onSyncedChange = this.onSyncedChange.bind(this);
|
||||
this.onSyncChange = this.onSyncChange.bind(this);
|
||||
}
|
||||
|
||||
async componentWillMount() {
|
||||
Notes.syncedObservable.subscribe(this.onSyncedChange);
|
||||
Notes.syncObservable.subscribe(this.onSyncChange);
|
||||
this.setState({ synced: await Notes.isSynced() })
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
Notes.syncedObservable.unsubscribe(this.onSyncedChange);
|
||||
Notes.syncObservable.unsubscribe(this.onSyncChange);
|
||||
}
|
||||
|
||||
onSyncChange(state: boolean) {
|
||||
console.log("sync", state);
|
||||
this.setState({ syncing: state })
|
||||
}
|
||||
|
||||
onSyncedChange(state: boolean) {
|
||||
console.log("synced", state);
|
||||
this.setState({ synced: state })
|
||||
}
|
||||
|
||||
onSyncClick() {
|
||||
Notes.sync().then(() => {
|
||||
Notifications.sendInfo("Finished Synchronisation");
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
let extrac = "";
|
||||
let color;
|
||||
let text;
|
||||
if (this.state.syncing) {
|
||||
color = "orange";
|
||||
text = "syncing";
|
||||
extrac = "reloading"
|
||||
} else {
|
||||
if (this.state.synced) {
|
||||
color = "green";
|
||||
text = "synced";
|
||||
} else {
|
||||
color = "red";
|
||||
text = "not synced";
|
||||
}
|
||||
}
|
||||
return <footer>
|
||||
<span>
|
||||
<span class={extrac} ><a onClick={() => this.onSyncClick()} ><Refresh style="height: 1em; width: 1em;"></Refresh></a></span>
|
||||
<span style={"margin-left: 8px; color:" + color}>{text}</span>
|
||||
</span>
|
||||
<span>
|
||||
Welcome <b>{Notes.name}</b>
|
||||
</span>
|
||||
</footer>
|
||||
}
|
||||
|
||||
}
|
@ -12,14 +12,14 @@ export class Router extends Component<{}, { next?: JSX.Element, current: JSX.Ele
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
Navigation.pageObservable.subscribe(this.onChange, true)
|
||||
Navigation.pageObservable.subscribe(this.onChange)
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
Navigation.pageObservable.unsubscribe(this.onChange)
|
||||
}
|
||||
|
||||
onChange([page]: JSX.Element[]) {
|
||||
onChange(page: JSX.Element) {
|
||||
this.setState({ next: page, current: this.state.next || this.state.current });
|
||||
}
|
||||
|
||||
@ -42,7 +42,7 @@ export class Router extends Component<{}, { next?: JSX.Element, current: JSX.Ele
|
||||
{this.state.next}
|
||||
</div>
|
||||
}
|
||||
return <div style="overflow:hidden; width: 1vw;">
|
||||
return <div style="position: relative; overflow-x: hidden; width: 100vw; height: 100vh;">
|
||||
<div class="transition_container" key={this.state.current.key} ref={elm => this.mounted = elm}>
|
||||
{this.state.current}
|
||||
</div>
|
||||
|
19
src/components/footer.scss
Normal file
19
src/components/footer.scss
Normal file
@ -0,0 +1,19 @@
|
||||
.reloading { animation: turner 1s infinite linear }
|
||||
|
||||
@keyframes turner{
|
||||
from{ transform: rotate(0deg) }
|
||||
to { transform: rotate(360deg) }
|
||||
}
|
||||
|
||||
footer {
|
||||
z-index: 128;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
padding: calc(0.9 * var(--universal-padding)) !important;
|
||||
}
|
@ -23,7 +23,7 @@ export class InputModal extends Modal<string> {
|
||||
|
||||
render() {
|
||||
return <Modal.BaseModal modal={this.props.modal}>
|
||||
<fieldset style="border:none;">
|
||||
<fieldset style="border:none; min-inline-size:0;">
|
||||
<label for={this.rand}>{this.props.modal.fieldname}</label>
|
||||
<input style="min-width: 85%" autofocus ref={elm => {
|
||||
this.input = elm
|
||||
@ -47,41 +47,4 @@ export class InputModal extends Modal<string> {
|
||||
getComponent() {
|
||||
return <InputModal.IMD modal={this} />
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// export class InputModal extends Component<{ title: string, fieldname: string, type: "text" | "password", onResult: (result) => void }, {}> {
|
||||
// input: HTMLInputElement;
|
||||
// rand: string;
|
||||
// constructor(props) {
|
||||
// super(props);
|
||||
// this.rand = Math.random().toString();
|
||||
// }
|
||||
|
||||
// componentWillUnmount() {
|
||||
// if (this.input)
|
||||
// this.input.value = "";
|
||||
// }
|
||||
|
||||
// render() {
|
||||
// return <Modal title={this.props.title} onClose={() => this.props.onResult(null)}>
|
||||
// <fieldset style="border:none;">
|
||||
// <label for={this.rand}>{this.props.fieldname}</label>
|
||||
// <input style="min-width: 85%" autofocus ref={elm => {
|
||||
// this.input = elm
|
||||
// if (this.input)
|
||||
// setTimeout(() => this.input.focus(), 0)
|
||||
// }} type={this.props.type} id={this.rand} placeholder={this.props.fieldname} onKeyDown={evt => {
|
||||
// if (evt.keyCode === 13) {
|
||||
// this.props.onResult(this.input.value)
|
||||
// }
|
||||
// }} />
|
||||
// <div style="text-align: right;">
|
||||
// <button class="primary" style="display: inline-block;" onClick={() => {
|
||||
// this.props.onResult(this.input.value);
|
||||
// }}>Enter</button>
|
||||
// </div>
|
||||
// </fieldset>
|
||||
// </Modal>
|
||||
// }
|
||||
// }
|
||||
}
|
@ -1,9 +1,10 @@
|
||||
import Observable from "../../helper/observable";
|
||||
import { Observable } from "@hibas123/utils";
|
||||
import { h, Component } from "preact";
|
||||
import CloseIcon from "feather-icons/dist/icons/x.svg";
|
||||
|
||||
export default abstract class Modal<T> {
|
||||
// Static
|
||||
private static modalObservableServer = new Observable<{ modal: Modal<any>, close: boolean }>(false);
|
||||
private static modalObservableServer = new Observable<{ modal: Modal<any>, close: boolean }>();
|
||||
public static modalObservable = Modal.modalObservableServer.getPublicApi();
|
||||
|
||||
|
||||
@ -11,9 +12,12 @@ export default abstract class Modal<T> {
|
||||
|
||||
// Private
|
||||
private onResult: (result: T | null) => void;
|
||||
private closeOnResult: boolean;
|
||||
|
||||
// Protected
|
||||
protected result(value: T | null) {
|
||||
if (this.closeOnResult)
|
||||
this.close()
|
||||
if (this.onResult)
|
||||
this.onResult(value);
|
||||
}
|
||||
@ -32,7 +36,8 @@ export default abstract class Modal<T> {
|
||||
*
|
||||
* Call close when successful
|
||||
*/
|
||||
public async getResult() {
|
||||
public async getResult(close = true) {
|
||||
this.closeOnResult = close;
|
||||
this.show();
|
||||
return new Promise<T | null>((yes) => this.onResult = yes);
|
||||
}
|
||||
@ -65,7 +70,12 @@ export default abstract class Modal<T> {
|
||||
}
|
||||
}}>
|
||||
<div class="card" >
|
||||
<h3 class="section">{this.props.modal.title}</h3>
|
||||
<div class="section" style="display:flex;justify-content:space-between;">
|
||||
<h3>{this.props.modal.title}</h3>
|
||||
<h3>
|
||||
<CloseIcon onClick={() => this.props.modal.result(null)} width={undefined} height={undefined} style="height:calc(1rem * var(--heading-ratio) * var(--heading-ratio))" />
|
||||
</h3>
|
||||
</div>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
@ -74,62 +84,24 @@ export default abstract class Modal<T> {
|
||||
|
||||
}
|
||||
|
||||
|
||||
// export default abstract class Modal<T, S> extends Component<{}, S> {
|
||||
|
||||
|
||||
// // Abstract
|
||||
// protected abstract renderChilds(): JSX.Element;
|
||||
// protected abstract title: string;
|
||||
|
||||
|
||||
|
||||
// render() {
|
||||
// return <div class="modal_container" onClick={(evt) => {
|
||||
// let path = evt.composedPath();
|
||||
// if (!path.find(e => {
|
||||
// let res = false;
|
||||
// let s = (e as Element);
|
||||
// if (s) {
|
||||
// if (s.classList) {
|
||||
// res = s.classList.contains("card")
|
||||
// }
|
||||
// }
|
||||
// return res;
|
||||
// })) {
|
||||
// this.result(null)
|
||||
// }
|
||||
// }} onKeyDown={evt => {
|
||||
// if (evt.keyCode === 27) {
|
||||
// this.result(null)
|
||||
// }
|
||||
// }}>
|
||||
// <div class="card" >
|
||||
// <h3 class="section">{this.title}</h3>
|
||||
// {this.renderChilds()}
|
||||
// </div>
|
||||
// </div>
|
||||
// }
|
||||
// }
|
||||
|
||||
export class ModalComponent extends Component<{}, { modal: Modal<any> | undefined, component: JSX.Element | undefined }>{
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onModal = this.onModal.bind(this);
|
||||
}
|
||||
|
||||
onModal([{ modal, close }]: { modal: Modal<any>, close: boolean }[]) {
|
||||
onModal({ modal, close }: { modal: Modal<any>, close: boolean }) {
|
||||
if (!close && this.state.modal !== modal) {
|
||||
this.setState({ modal: modal, component: modal.getComponent() })
|
||||
}
|
||||
else {
|
||||
if (this.state.modal === modal) // Only close if the same
|
||||
if (this.state.modal === modal && close) // Only close if the same
|
||||
this.setState({ modal: undefined, component: undefined })
|
||||
}
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
Modal.modalObservable.subscribe(this.onModal, true);
|
||||
Modal.modalObservable.subscribe(this.onModal);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -28,7 +28,7 @@ export class YesNoModal extends Modal<boolean> {
|
||||
|
||||
render() {
|
||||
return <Modal.BaseModal modal={this.props.modal}>
|
||||
<fieldset style="border:none;">
|
||||
<fieldset style="border:none;min-inline-size:0;">
|
||||
<div style="text-align: right;">
|
||||
<button class="primary" style="display: inline-block;" onClick={() => {
|
||||
this.props.modal.result(false);
|
||||
|
@ -1,9 +1,20 @@
|
||||
:root {
|
||||
--notification-margin: 15px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1000px) {
|
||||
:root {
|
||||
--notification-margin: 35px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.notifications_container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 128;
|
||||
margin-top: 35px;
|
||||
margin-top: calc(var(--notification-margin) - 10px);
|
||||
}
|
||||
|
||||
.notifications_success {
|
||||
@ -30,6 +41,7 @@
|
||||
|
||||
.notifications_notification {
|
||||
h4 {
|
||||
color: inherit;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
}
|
||||
@ -41,7 +53,7 @@
|
||||
|
||||
position: static;
|
||||
margin-top:10px;
|
||||
margin-right:35px;
|
||||
margin-right:var(--notification-margin);
|
||||
padding: 0.5em;
|
||||
width: 300px;
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ export default class NotificationsComponent extends Component<{}, {
|
||||
Notifications.messageObservable.unsubscribe(this.onNotification);
|
||||
}
|
||||
|
||||
onNotification([not]: { message: string, type: MessageType }[]) {
|
||||
onNotification(not: { message: string, type: MessageType }) {
|
||||
console.log("Got notification", not)
|
||||
let n = this.state.notifications.slice(0);
|
||||
n.push(not);
|
||||
|
@ -31,7 +31,8 @@ export default class EntryComponent extends Component<{ vault: Promise<IVault>,
|
||||
|
||||
private toVault() {
|
||||
// history.back()
|
||||
Navigation.setPage("/vault", { id: this.vault.id }, { entry: "false" }, true);
|
||||
history.back();
|
||||
// Navigation.setPage("/vault", { id: this.vault.id }, { entry: "false" }, true);
|
||||
}
|
||||
|
||||
async componentWillMount() {
|
||||
@ -128,7 +129,6 @@ export default class EntryComponent extends Component<{ vault: Promise<IVault>,
|
||||
if (this.state.changed) {
|
||||
let modal = new YesNoModal("Really want to quit?");
|
||||
let res = await modal.getResult();
|
||||
modal.close();
|
||||
if (res === true) {
|
||||
this.skip_save = true;
|
||||
this.toVault();
|
||||
@ -138,7 +138,6 @@ export default class EntryComponent extends Component<{ vault: Promise<IVault>,
|
||||
}
|
||||
|
||||
textAreaKeyPress(evt: KeyboardEvent) {
|
||||
console.log(evt);
|
||||
if ((evt.keyCode === 83 || evt.keyCode === 13) && evt.ctrlKey) {
|
||||
evt.preventDefault()
|
||||
this.save();
|
||||
|
@ -3,7 +3,6 @@ import Notes, { IVault, BaseNote } from "../../../notes";
|
||||
import AddButton from "../../AddButton";
|
||||
import Navigation from "../../../navigation";
|
||||
import ArrowLeft from "feather-icons/dist/icons/arrow-left.svg"
|
||||
import MoreVertival from "feather-icons/dist/icons/more-vertical.svg"
|
||||
import ContextMenu from "../../modals/context";
|
||||
import Notifications from "../../../notifications";
|
||||
|
||||
@ -17,7 +16,9 @@ export default class EntryList extends Component<{ vault: Promise<IVault> }, { n
|
||||
}
|
||||
vault: IVault;
|
||||
|
||||
reloadNotes() {
|
||||
reloadNotes(s?: boolean) {
|
||||
if (s)
|
||||
return;
|
||||
return new Promise(yes => this.vault.getAllNotes().then(entries => this.setState({ notes: entries }, yes)));
|
||||
}
|
||||
|
||||
@ -169,7 +170,6 @@ export default class EntryList extends Component<{ vault: Promise<IVault> }, { n
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-8 col-lg-6 col-md-offset-2 col-lg-offset-3">
|
||||
<div class="card fluid">
|
||||
<h1 class="section double-padded">Notes: </h1>
|
||||
<div class="section">
|
||||
{elms}
|
||||
</div>
|
||||
|
@ -7,6 +7,7 @@ import EntryList from "./EntryList";
|
||||
|
||||
import "./vault.scss"
|
||||
|
||||
|
||||
export interface VaultProps {
|
||||
state: { id: string };
|
||||
hidden: { entry: string, id: string, note: string };
|
||||
@ -21,10 +22,19 @@ export default class VaultPage extends Page<VaultProps, { entries: BaseNote[] }>
|
||||
|
||||
async componentWillMount() {
|
||||
this.vault = Notes.getVault(this.props.state.id, Notes.getVaultKey(this.props.state.id))
|
||||
|
||||
this.vault.then(vlt => {
|
||||
window.debug.activeVault = vlt;
|
||||
window.debug.createNotes = (cnt = 10) => {
|
||||
for (let i = 0; i < cnt; i++) {
|
||||
let nt = vlt.newNote();
|
||||
nt.__value = `Random Note ${i}\ With some Content ${i}`;
|
||||
vlt.saveNote(nt);
|
||||
}
|
||||
}
|
||||
})
|
||||
this.vault.catch(err => {
|
||||
Navigation.setPage("/")
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -24,7 +24,9 @@ export default class VaultsPage extends Page<VaultsProps, { vaults: VaultList, m
|
||||
this.updateVaults = this.updateVaults.bind(this);
|
||||
}
|
||||
|
||||
updateVaults() {
|
||||
updateVaults(s?: boolean) {
|
||||
if (s)
|
||||
return;
|
||||
return new Promise(yes => {
|
||||
Notes.getVaults().then(vaults => this.setState({ vaults }, yes))
|
||||
})
|
||||
@ -43,9 +45,11 @@ export default class VaultsPage extends Page<VaultsProps, { vaults: VaultList, m
|
||||
let inp_mod = new InputModal("Enter password for " + vault.name, "Password", "password");
|
||||
let key = undefined;
|
||||
while (true) {
|
||||
let value = await inp_mod.getResult();
|
||||
// inp_mod.show();
|
||||
let value = await inp_mod.getResult(false);
|
||||
if (value === null) {
|
||||
inp_mod.close();
|
||||
console.log("Value is null")
|
||||
inp_mod.close()
|
||||
return false;
|
||||
} else {
|
||||
key = Notes.passwordToKey(value);
|
||||
@ -57,13 +61,12 @@ export default class VaultsPage extends Page<VaultsProps, { vaults: VaultList, m
|
||||
}
|
||||
}
|
||||
}
|
||||
inp_mod.close();
|
||||
inp_mod.close()
|
||||
|
||||
let perm = false;
|
||||
if (permanent) {
|
||||
let save_modal = new YesNoModal("Save permanent?");
|
||||
let res = await save_modal.getResult();
|
||||
save_modal.close();
|
||||
if (res === undefined) {
|
||||
res = false;
|
||||
}
|
||||
@ -101,19 +104,16 @@ export default class VaultsPage extends Page<VaultsProps, { vaults: VaultList, m
|
||||
async addButtonClick() {
|
||||
let name_modal = new InputModal("Enter new name", "Name", "text");
|
||||
let name = await name_modal.getResult();
|
||||
name_modal.close();
|
||||
if (name === null) return;
|
||||
|
||||
let encrypted_modal = new YesNoModal("Encrypt?");
|
||||
let encrypted = encrypted_modal.getResult();
|
||||
encrypted_modal.close();
|
||||
if (encrypted === null) return;
|
||||
|
||||
let password;
|
||||
if (encrypted) {
|
||||
let password_modal = new InputModal("Enter new password", "Password", "password");
|
||||
password = await password_modal.getResult();
|
||||
password_modal.close();
|
||||
if (password === null) return;
|
||||
}
|
||||
|
||||
@ -127,11 +127,6 @@ export default class VaultsPage extends Page<VaultsProps, { vaults: VaultList, m
|
||||
|
||||
onContext(evt: MouseEvent, vault: { name: string, encrypted: boolean, id: string }) {
|
||||
evt.preventDefault();
|
||||
console.log("Context", evt);
|
||||
|
||||
// let context = <div style={{ position: "fixed", left: evt.pageX, top: evt.pageY, zIndex: 10 }}>
|
||||
// <button>Action 1</button>
|
||||
// </div>
|
||||
|
||||
const close = () => {
|
||||
window.removeEventListener("click", close);
|
||||
@ -142,7 +137,6 @@ export default class VaultsPage extends Page<VaultsProps, { vaults: VaultList, m
|
||||
let deleteb = <button onClick={async () => {
|
||||
let delete_modal = new YesNoModal("Delete Vault? Cannot be undone!");
|
||||
let result = await delete_modal.getResult();
|
||||
delete_modal.close();
|
||||
if (result) {
|
||||
Notes.deleteVault(vault.id).then(() => {
|
||||
this.updateVaults();
|
||||
@ -157,7 +151,10 @@ export default class VaultsPage extends Page<VaultsProps, { vaults: VaultList, m
|
||||
|
||||
let delete_key;
|
||||
if (Notes.getVaultKey(vault.id)) {
|
||||
delete_key = <button onClick={() => { Notes.forgetVaultKey(vault.id); }}>
|
||||
delete_key = <button onClick={() => {
|
||||
Notes.forgetVaultKey(vault.id);
|
||||
Notifications.sendSuccess("Forgot key!")
|
||||
}}>
|
||||
forget password
|
||||
</button>;
|
||||
}
|
||||
@ -219,14 +216,16 @@ export default class VaultsPage extends Page<VaultsProps, { vaults: VaultList, m
|
||||
return <div style={{ marginTop: "-12px", paddingTop: "12px" }} >
|
||||
{this.state.modal}
|
||||
{this.state.context}
|
||||
<header>
|
||||
<span></span>
|
||||
<h1 style="display:inline" class="button header_title" onClick={() => Navigation.setPage("/")}>Your vaults:</h1>
|
||||
<span></span>
|
||||
</header>
|
||||
<AddButton onClick={() => this.addButtonClick()} />
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-8 col-lg-6 col-md-offset-2 col-lg-offset-3">
|
||||
<div class="card fluid">
|
||||
|
||||
<h1 class="section double-padded">Your vaults:</h1>
|
||||
|
||||
<div class="section">
|
||||
{elms}
|
||||
</div>
|
||||
|
@ -6,9 +6,13 @@
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
background: var(--back-color);
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.transition_slidein {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
animation-name: slidein;
|
||||
animation-duration: 0.3s;
|
||||
z-index: 64;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import Lock from "./lock";
|
||||
import { Lock } from "@hibas123/utils";
|
||||
|
||||
import { DB, openDb, Transaction } from "idb";
|
||||
|
||||
|
@ -1,36 +0,0 @@
|
||||
export type Release = { release: () => void };
|
||||
export default class Lock {
|
||||
private _locked: boolean = false;
|
||||
get locked() {
|
||||
return this._locked;
|
||||
}
|
||||
private toCome: (() => void)[] = [];
|
||||
|
||||
constructor() {
|
||||
this.release = this.release.bind(this);
|
||||
}
|
||||
|
||||
async getLock(): Promise<Release> {
|
||||
if (!this._locked) return { release: this.lock() };
|
||||
else {
|
||||
return new Promise<Release>((resolve) => {
|
||||
this.toCome.push(() => {
|
||||
resolve({ release: this.lock() });
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private lock() {
|
||||
this._locked = true;
|
||||
return this.release;
|
||||
}
|
||||
|
||||
private async release() {
|
||||
if (this.toCome.length > 0) {
|
||||
this.toCome.shift()();
|
||||
} else {
|
||||
this._locked = false;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
export type ObserverCallback<T> = (data: T[]) => void;
|
||||
|
||||
export default class Observable<T = any> {
|
||||
private subscriber: { callback: ObserverCallback<T>, one: boolean }[] = [];
|
||||
private events: T[] = [];
|
||||
private timeout = undefined;
|
||||
|
||||
constructor(private collect: boolean = true, private collect_intervall: number = 100) { }
|
||||
|
||||
getPublicApi() {
|
||||
return {
|
||||
subscribe: (callback: ObserverCallback<T>, one: boolean = false) => {
|
||||
let oldcb = this.subscriber.find(e => e.callback === callback);
|
||||
if (oldcb)
|
||||
oldcb.one = one
|
||||
else
|
||||
this.subscriber.push({ callback, one })
|
||||
},
|
||||
unsubscribe: (callback: ObserverCallback<T>) => {
|
||||
let idx = this.subscriber.findIndex(e => e.callback === callback);
|
||||
if (idx >= 0) {
|
||||
this.subscriber.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
send(data: T) {
|
||||
if (!this.collect)
|
||||
this.subscriber.forEach(e => e.callback([data]));
|
||||
else {
|
||||
this.events.push(data);
|
||||
if (!this.timeout) {
|
||||
this.timeout = setTimeout(() => {
|
||||
this.subscriber.forEach(cb => {
|
||||
if (cb.one)
|
||||
this.events.forEach(e => cb.callback([e]));
|
||||
else
|
||||
cb.callback(this.events)
|
||||
});
|
||||
this.timeout = 0;
|
||||
}, this.collect_intervall);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -6,20 +6,17 @@
|
||||
<meta charset="utf8" />
|
||||
<meta name="Description" content="Notes app">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<link rel="manifest" href="/public/manifest.json">
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<!-- <link rel="shortcut icon" href="/public/icon-72x72.png"> -->
|
||||
|
||||
<!-- Add to home screen for Safari on iOS -->
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="apple-mobile-web-app-title" content="eBook">
|
||||
<meta name="apple-mobile-web-app-title" content="Secure Notes">
|
||||
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/public/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/public/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/public/favicon-16x16.png">
|
||||
<link rel="manifest" href="/public/site.webmanifest">
|
||||
<link rel="mask-icon" href="/public/safari-pinned-tab.svg" color="#3E9AE9">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#3E9AE9">
|
||||
<meta name="msapplication-TileColor" content="#3E9AE9">
|
||||
<meta name="theme-color" content="#3E9AE9">
|
||||
</head>
|
||||
@ -30,13 +27,33 @@
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<script>
|
||||
// Check that service workers are registered
|
||||
// // Check that service workers are registered
|
||||
// if ('serviceWorker' in navigator) {
|
||||
// // Use the window load event to keep the page load performant
|
||||
// window.addEventListener('load', () => {
|
||||
// navigator.serviceWorker.register('/sw.js');
|
||||
// navigator.serviceWorker.register('/public/serviceworker.js');
|
||||
// });
|
||||
// }
|
||||
if (navigator.serviceWorker.controller) {
|
||||
if (localStorage.getItem("debug")) {
|
||||
console.warn("Debuggung and service worker found, make shure to clear cache!");
|
||||
}
|
||||
console.log('active service worker found, no need to register')
|
||||
} else {
|
||||
if (localStorage.getItem("debug")) {
|
||||
console.warn("Disabling Service Worker in debug mode!")
|
||||
} else {
|
||||
// Register the ServiceWorker
|
||||
navigator.serviceWorker.register('/serviceworker.js', {
|
||||
scope: '/'
|
||||
}).then(function (reg) {
|
||||
console.log('Service worker has been registered for scope:' + reg.scope);
|
||||
navigator.serviceWorker.controller.addEventListener("cleared_cache", evt => {
|
||||
console.log(evt);
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
@ -1,8 +1,12 @@
|
||||
window.debug = {};
|
||||
|
||||
import { h, render } from 'preact';
|
||||
import App from './components/App';
|
||||
|
||||
import "mini.css/src/flavors/mini-dark.scss"
|
||||
// import "mini.css/src/flavors/mini-default.scss"
|
||||
// import "mini.css/src/flavors/mini-dark.scss"
|
||||
import "mini.css/src/flavors/mini-default.scss"
|
||||
import "uikit";
|
||||
import "uikit/dist/css/uikit.css"
|
||||
import "./index.scss"
|
||||
import Navigation from './navigation';
|
||||
import VaultsPage from './components/routes/vaults/Vaults';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import Observable from "./helper/observable";
|
||||
import { Observable } from "@hibas123/utils";
|
||||
import { Page, PageProps } from "./page";
|
||||
import { h, VNode } from "preact";
|
||||
|
||||
@ -27,7 +27,7 @@ function parseQuery(query: string) {
|
||||
export default class Navigation {
|
||||
private static _pages: Map<string, typeof Page> = new Map();
|
||||
private static _page: { route: string, page: JSX.Element };
|
||||
private static pageObservableServer = new Observable<JSX.Element>(false);
|
||||
private static pageObservableServer = new Observable<JSX.Element>();
|
||||
public static pageObservable = Navigation.pageObservableServer.getPublicApi();
|
||||
private static _state: { [key: string]: any };
|
||||
private static _hidden_state: { [key: string]: any };
|
||||
@ -52,13 +52,14 @@ export default class Navigation {
|
||||
}
|
||||
|
||||
public static setPage(route: string, state?: { [key: string]: string }, hidden?: { [key: string]: string }, replace?: boolean) {
|
||||
if (!state) state = {};
|
||||
let component = Navigation._pages.get(route);
|
||||
if (!component && route !== "/404") {
|
||||
Navigation.setPage("/404", { route, key: "404" })
|
||||
} else {
|
||||
if (!Navigation.page || Navigation.page.route !== route || JSON.stringify(state) !== JSON.stringify(Navigation._state) || JSON.stringify(Navigation._hidden_state) !== JSON.stringify(hidden)) {
|
||||
let s = "";
|
||||
if (state) {
|
||||
if (Object.keys(state).length > 0) {
|
||||
s = "?" + serializQuery(state)
|
||||
}
|
||||
let newhash = "#" + route + s;
|
||||
@ -92,6 +93,7 @@ export default class Navigation {
|
||||
Navigation._state = state;
|
||||
Navigation._hidden_state = hidden;
|
||||
Navigation._page = { page, route };
|
||||
console.log(route, state, hidden);
|
||||
Navigation.pageObservableServer.send(page);
|
||||
}
|
||||
}
|
||||
@ -142,4 +144,6 @@ export default class Navigation {
|
||||
})
|
||||
Navigation.onHashChange(undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.debug.navigation = Navigation;
|
45
src/notes.ts
45
src/notes.ts
@ -1,6 +1,5 @@
|
||||
import Observable from "./helper/observable";
|
||||
import * as config from "../config.json"
|
||||
import Lock from "./helper/lock"
|
||||
import { Lock, Observable } from "@hibas123/utils"
|
||||
|
||||
import SecureFile, { IFile } from "@hibas123/secure-file-wrapper";
|
||||
import * as b64 from "./helper/base64"
|
||||
@ -33,9 +32,7 @@ export interface ViewNote extends BaseNote {
|
||||
__value: string;
|
||||
}
|
||||
|
||||
// import * as AES from "crypto-js/aes"
|
||||
import * as aesjs from "aes-js";
|
||||
// import { } from "js-sha512"
|
||||
import { sha256 } from "js-sha256"
|
||||
import clonedeep = require("lodash.clonedeep");
|
||||
import * as uuidv4 from "uuid/v4"
|
||||
@ -87,11 +84,16 @@ export interface IVault {
|
||||
deleteNote(id: string): Promise<void>;
|
||||
}
|
||||
|
||||
const awaitTimeout = (ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms));
|
||||
|
||||
class NotesProvider {
|
||||
private notesObservableServer = new Observable<Note>(true)
|
||||
private notesObservableServer = new Observable<Note>()
|
||||
public notesObservable = this.notesObservableServer.getPublicApi()
|
||||
|
||||
private syncObservableServer = new Observable<null>(true)
|
||||
private syncObservableServer = new Observable<boolean>()
|
||||
/**
|
||||
* Will send false once finished and true on start
|
||||
*/
|
||||
public syncObservable = this.syncObservableServer.getPublicApi()
|
||||
|
||||
private database = new IDB("notes", ["notes", "oplog"]);
|
||||
@ -110,6 +112,18 @@ class NotesProvider {
|
||||
|
||||
private generalEncryption: Uint8Array = undefined;
|
||||
|
||||
private syncedObservableServer = new Observable<boolean>();
|
||||
public syncedObservable = this.syncedObservableServer.getPublicApi();
|
||||
|
||||
public async isSynced() {
|
||||
return (await this.oplogDB.getAll()).length <= 0
|
||||
}
|
||||
|
||||
private _name;
|
||||
public get name() {
|
||||
return this._name;
|
||||
}
|
||||
|
||||
loginRequired() {
|
||||
return !localStorage.getItem("refreshtoken") || !this.generalEncryption;
|
||||
}
|
||||
@ -123,6 +137,8 @@ class NotesProvider {
|
||||
let res = await req.json();
|
||||
if (!res.error) {
|
||||
localStorage.setItem("refreshtoken", res.token);
|
||||
localStorage.setItem("name", res.profile.name);
|
||||
this._name = res.profile.name;
|
||||
let kb = this.passwordToKey(res.profile.enc_key);
|
||||
localStorage.setItem("enc_key", b64.encode(kb));
|
||||
this.generalEncryption = kb
|
||||
@ -133,7 +149,7 @@ class NotesProvider {
|
||||
|
||||
constructor(public readonly baseurl = "") {
|
||||
this._secureFile = new SecureFile(config.secure_file_server);
|
||||
this._secureFile.jwtObservable.subscribe(async ([callback]) => {
|
||||
this._secureFile.jwtObservable.subscribe(async (callback) => {
|
||||
let jwt = await this.getJWT();
|
||||
callback(jwt);
|
||||
})
|
||||
@ -142,6 +158,7 @@ class NotesProvider {
|
||||
if (key) {
|
||||
this.generalEncryption = b64.decode(key)
|
||||
}
|
||||
this._name = localStorage.getItem("name");
|
||||
}
|
||||
|
||||
public async start() {
|
||||
@ -150,8 +167,10 @@ class NotesProvider {
|
||||
this.sync().then(() => next);
|
||||
}, 30000)
|
||||
}
|
||||
|
||||
return this.apiLockRls.then(lock => lock.release()).then(() => { this.sync() }).then(() => next())
|
||||
this.syncedObservableServer.send((await this.oplogDB.getAll()).length <= 0);
|
||||
let prs = this.apiLockRls.then(lock => lock.release());
|
||||
prs.then(() => awaitTimeout(5000)).then(() => this.sync()).then(() => next());
|
||||
return prs;
|
||||
}
|
||||
|
||||
private async getJWT() {
|
||||
@ -182,11 +201,13 @@ class NotesProvider {
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
private async sync() {
|
||||
async sync() {
|
||||
const lock = await this.syncLock.getLock()
|
||||
const log = (...message: any[]) => {
|
||||
console.log("[SYNC]: ", ...message)
|
||||
}
|
||||
|
||||
this.syncObservableServer.send(true);
|
||||
log("Start")
|
||||
try {
|
||||
log("Fetching");
|
||||
@ -351,10 +372,11 @@ class NotesProvider {
|
||||
await this.oplogDB.delete(id);
|
||||
}
|
||||
log("Stats", stats);
|
||||
this.syncedObservableServer.send((await this.oplogDB.getAll()).length <= 0)
|
||||
} finally {
|
||||
log("Finished")
|
||||
lock.release()
|
||||
this.syncObservableServer.send(null);
|
||||
this.syncObservableServer.send(false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -464,6 +486,7 @@ class NotesProvider {
|
||||
type,
|
||||
values
|
||||
})
|
||||
this.syncedObservableServer.send(false);
|
||||
await this.oplogDB.set(note_id, oplog, tx);
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import Observable from "./helper/observable";
|
||||
import { Observable } from "@hibas123/utils";
|
||||
|
||||
export enum MessageType {
|
||||
INFO,
|
||||
@ -8,7 +8,7 @@ export enum MessageType {
|
||||
}
|
||||
|
||||
export default class Notifications {
|
||||
private static messageObservableServer = new Observable<{ message: string, type: MessageType }>(false)
|
||||
private static messageObservableServer = new Observable<{ message: string, type: MessageType }>()
|
||||
public static messageObservable = Notifications.messageObservableServer.getPublicApi()
|
||||
|
||||
|
||||
|
15
src/types.d.ts
vendored
15
src/types.d.ts
vendored
@ -1,2 +1,13 @@
|
||||
// import { Component } from "preact";
|
||||
declare module "*.svg";
|
||||
declare module "*.svg" {
|
||||
const SVG: (props: {
|
||||
width?: number | undefined
|
||||
height?: number | undefined
|
||||
onClick?: (evt: MouseEvent) => void;
|
||||
style?: string;
|
||||
}) => JSX.Element;
|
||||
export default SVG;
|
||||
}
|
||||
|
||||
interface Window {
|
||||
debug: any;
|
||||
}
|
Reference in New Issue
Block a user