Compare commits
2 Commits
168df6cfbc
...
533694f3bc
Author | SHA1 | Date |
---|---|---|
Adam Veldhousen | 533694f3bc | 4 years ago |
Adam Veldhousen | 31a991cd69 | 4 years ago |
@ -0,0 +1,95 @@
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
|
||||
function listNotesInDirectory(directoryPath = "") {
|
||||
return new Promise(function(resolve, reject) {
|
||||
fs.readdir(directoryPath, function(err, files) {
|
||||
if (err) return reject(err);
|
||||
|
||||
resolve(
|
||||
files
|
||||
.map(fPath => ({
|
||||
ext: path.extname(fPath),
|
||||
name: fPath,
|
||||
title: path.basename(fPath, path.extname(fPath)),
|
||||
path: path.join(directoryPath, fPath)
|
||||
}))
|
||||
.filter(
|
||||
({ ext }) =>
|
||||
ext === ".md" || ext === ".txt" || ext === ".utf8"
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getNoteTime(fullPath) {
|
||||
return new Promise((a, r) => {
|
||||
fs.stat(fullPath, (err, stats) => {
|
||||
if (err) return r(err);
|
||||
|
||||
a({
|
||||
lastModified: stats.mtime,
|
||||
created: stats.ctime,
|
||||
size: stats.size
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getNoteContent(fullPath) {
|
||||
return new Promise((a, r) =>
|
||||
fs.readFile(fullPath, "utf8", (err, data) => {
|
||||
if (err) return r(err);
|
||||
a({ content: data });
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async function loadNotesInDirectory(directoryPath = "") {
|
||||
const files = await listNotesInDirectory(directoryPath);
|
||||
|
||||
return await Promise.all(
|
||||
files.map(async ({ path, title, ext }) => {
|
||||
const [noteTime, noteContent] = await Promise.all([
|
||||
getNoteTime(path),
|
||||
getNoteContent(path)
|
||||
]);
|
||||
|
||||
return {
|
||||
path,
|
||||
title,
|
||||
...noteTime,
|
||||
...noteContent
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async function saveNoteInDirectory(
|
||||
{ directory, defaultExtension },
|
||||
{ title, content }
|
||||
) {
|
||||
let fullPath = path.join(directory, `${title}.${defaultExtension}`);
|
||||
|
||||
const match = (await listNotesInDirectory(directory)).filter(
|
||||
f => f.title === title
|
||||
);
|
||||
|
||||
if (match && match.length > 0) fullPath = match[0].path;
|
||||
console.log("tryint to write note: ", fullPath, " - ", match);
|
||||
|
||||
return Promise.all([
|
||||
new Promise((a, r) => {
|
||||
fs.writeFile(fullPath, content, "utf8", err => {
|
||||
if (err) return r(err);
|
||||
a();
|
||||
});
|
||||
})
|
||||
]);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
loadNotesInDirectory,
|
||||
saveNoteInDirectory
|
||||
};
|
@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
defaultSettings: {
|
||||
directory: "./",
|
||||
defaultExtension: "txt",
|
||||
autoSave: true, // if notes should save automatically
|
||||
autoSaveDelay: 1000, // ms to wait after last key press to save
|
||||
themes: [{ name: "default" }]
|
||||
}
|
||||
};
|
@ -0,0 +1,59 @@
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
|
||||
function loadTags(directoryPath) {
|
||||
return new Promise((a, r) =>
|
||||
fs.readFile(
|
||||
path.join(directoryPath, "meta.json"),
|
||||
"utf8",
|
||||
async (err, data) => {
|
||||
if (err) {
|
||||
if (!err.message.includes("ENOENT")) return r(err);
|
||||
await saveTags(directoryPath, {});
|
||||
}
|
||||
|
||||
const tagCache = JSON.parse(data);
|
||||
a(tagCache);
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function saveTags(directoryPath, tags = {}) {
|
||||
return new Promise((a, r) =>
|
||||
fs.writeFile(
|
||||
path.join(directoryPath, "meta.json"),
|
||||
JSON.stringify(tags),
|
||||
"utf8",
|
||||
err => {
|
||||
if (err) return r(err);
|
||||
a({});
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async function updateTagsForNote(directoryPath, { title, tags = [] }) {
|
||||
if (!title || title === "") {
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
const allTags = await loadTags(directoryPath);
|
||||
allTags[title] = [...tags];
|
||||
await saveTags(directoryPath, allTags);
|
||||
}
|
||||
|
||||
async function getTagsForNote(directoryPath, { title }) {
|
||||
if (!title || title === "") {
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
const allTags = await loadTags(directoryPath);
|
||||
return allTags[title];
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
loadTags,
|
||||
getTagsForNote,
|
||||
updateTagsForNote
|
||||
};
|
@ -1,175 +0,0 @@
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
|
||||
const defaultSettings = {
|
||||
directory: "./",
|
||||
defaultExtension: "txt"
|
||||
};
|
||||
|
||||
function listNotesInDirectory(directoryPath = "") {
|
||||
return new Promise(function(resolve, reject) {
|
||||
fs.readdir(directoryPath, function(err, files) {
|
||||
if (err) return reject(err);
|
||||
|
||||
resolve(
|
||||
files
|
||||
.map(fPath => ({
|
||||
ext: path.extname(fPath),
|
||||
name: fPath,
|
||||
friendlyName: path.basename(fPath, path.extname(fPath)),
|
||||
fullPath: path.join(directoryPath, fPath)
|
||||
}))
|
||||
.filter(
|
||||
({ ext }) =>
|
||||
ext === ".md" || ext === ".txt" || ext === ".utf8"
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getNoteTime(fullPath) {
|
||||
return new Promise((a, r) => {
|
||||
fs.stat(fullPath, (err, stats) => {
|
||||
if (err) return r(err);
|
||||
|
||||
a({
|
||||
lastModified: stats.mtime,
|
||||
created: stats.ctime,
|
||||
size: stats.size
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getNoteContent(fullPath) {
|
||||
return new Promise((a, r) =>
|
||||
fs.readFile(fullPath, "utf8", (err, data) => {
|
||||
if (err) return r(err);
|
||||
a({ content: data });
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function loadTags(directoryPath) {
|
||||
return new Promise((a, r) =>
|
||||
fs.readFile(
|
||||
path.join(directoryPath, "meta.json"),
|
||||
"utf8",
|
||||
(err, data) => {
|
||||
if (err) {
|
||||
if (!err.message.includes("ENOENT")) return r(err);
|
||||
return a(saveTags(directoryPath, {}));
|
||||
}
|
||||
|
||||
a(JSON.parse(data));
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function saveTags(directoryPath, tags = {}) {
|
||||
return new Promise((a, r) =>
|
||||
fs.writeFile(
|
||||
path.join(directoryPath, "meta.json"),
|
||||
JSON.stringify(tags),
|
||||
"utf8",
|
||||
err => {
|
||||
if (err) return r(err);
|
||||
a({});
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
let tagsCache = {};
|
||||
function getNoteTags(directoryPath, friendlyName) {
|
||||
return Promise.resolve(tagsCache[friendlyName] || []);
|
||||
}
|
||||
|
||||
async function loadNotesInDirectory(directoryPath = "") {
|
||||
const tagCache = await loadTags(directoryPath);
|
||||
const files = await listNotesInDirectory(directoryPath);
|
||||
|
||||
return await Promise.all(
|
||||
files.map(async ({ fullPath, friendlyName, ext }) => {
|
||||
const [noteTime, noteContent] = await Promise.all([
|
||||
getNoteTime(fullPath),
|
||||
getNoteContent(fullPath),
|
||||
getNoteTags(directoryPath, friendlyName)
|
||||
]);
|
||||
|
||||
return {
|
||||
path: fullPath,
|
||||
title: friendlyName,
|
||||
tags: tagsCache[friendlyName] || [],
|
||||
...noteTime,
|
||||
...noteContent
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async function saveNoteInDirectory(
|
||||
{ directory, defaultExtension },
|
||||
{ title, content, tags }
|
||||
) {
|
||||
let fullPath = path.join(directory, `${title}.${defaultExtension}`);
|
||||
|
||||
const match = (await listNotesInDirectory(directory)).filter(
|
||||
f => f.friendlyName === title
|
||||
);
|
||||
|
||||
if (match && match.length > 0) fullPath = match[0].path;
|
||||
|
||||
tagsCache[title] = tags;
|
||||
return Promise.all([
|
||||
new Promise((a, r) => {
|
||||
fs.writeFile(fullPath, content, "utf8", err => {
|
||||
if (err) return r(err);
|
||||
a();
|
||||
});
|
||||
}),
|
||||
saveTags(directory, tagsCache)
|
||||
]);
|
||||
}
|
||||
|
||||
function newNoteBackend(initSettings) {
|
||||
let settings = { ...defaultSettings, ...initSettings };
|
||||
|
||||
async function onNoteCommand({ type, payload }) {
|
||||
console.log(`got command: ${type} -n ${JSON.stringify(payload)}`);
|
||||
const { directory } = settings;
|
||||
switch (type) {
|
||||
case "getNotes":
|
||||
const files = await loadNotesInDirectory(directory);
|
||||
return {
|
||||
type: "getNotes",
|
||||
payload: {
|
||||
notes: files.map(f => ({ ...f, tags: [] }))
|
||||
}
|
||||
};
|
||||
case "saveNote":
|
||||
const { title, content, tags } = payload;
|
||||
const result = await saveNoteInDirectory(settings, {
|
||||
title,
|
||||
content,
|
||||
tags
|
||||
});
|
||||
return { type: "saveNote", payload: result };
|
||||
default:
|
||||
return { type: "noop" };
|
||||
}
|
||||
}
|
||||
|
||||
function updateSettings({ directory }) {
|
||||
settings.directory = directory;
|
||||
}
|
||||
|
||||
return {
|
||||
onNoteCommand,
|
||||
updateSettings
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { createBackend: newNoteBackend };
|
@ -1,4 +0,0 @@
|
||||
export const defaultSettings = {
|
||||
directory: "./",
|
||||
defaultExtension: "txt"
|
||||
};
|
@ -1,93 +0,0 @@
|
||||
import {
|
||||
getNotes as getNotesBackend,
|
||||
upsertNote as upsertNoteBackend
|
||||
} from "./api";
|
||||
|
||||
const initialState = {
|
||||
notes: [],
|
||||
selectedNote: null,
|
||||
error: null
|
||||
};
|
||||
|
||||
export const reducer = (cs = initialState, { type, payload }) => {
|
||||
switch (type) {
|
||||
case "SELECT_NOTE":
|
||||
return { ...cs, selectedNote: payload.selectedNote };
|
||||
case "SAVE_NOTE_START":
|
||||
case "SAVE_NOTE_SUCCESS":
|
||||
case "GET_NOTES_START":
|
||||
return { ...cs, notesLoading: true };
|
||||
case "GET_NOTES_SUCCESS":
|
||||
return {
|
||||
...cs,
|
||||
notesLoading: false,
|
||||
error: null,
|
||||
notes: [...payload.notes]
|
||||
};
|
||||
case "SAVE_NOTE_FAIL":
|
||||
case "GET_NOTES_FAIL":
|
||||
return { ...cs, notesLoading: false, error: payload.error };
|
||||
default:
|
||||
return { ...cs };
|
||||
}
|
||||
};
|
||||
|
||||
const selectNote = ({ title }) => (dispatch, getState) => {
|
||||
const normalizedTitle = title.toLocaleLowerCase();
|
||||
const { selectedNote, notes } = getState();
|
||||
|
||||
let foundNote = selectedNote;
|
||||
const notesFilter = notes.filter(
|
||||
n => n.title.toLocaleLowerCase() === normalizedTitle
|
||||
);
|
||||
if (notesFilter.length > 0) {
|
||||
foundNote = notesFilter[0];
|
||||
}
|
||||
dispatch({ type: "SELECT_NOTE", payload: { selectedNote: foundNote } });
|
||||
};
|
||||
|
||||
const deleteNote = ({ title }) => (dispatch, getState) => {};
|
||||
|
||||
const createNote = ({ title, content, tags = [] }) => async (
|
||||
dispatch,
|
||||
getState
|
||||
) => {
|
||||
dispatch({ type: "SAVE_NOTE_START" });
|
||||
try {
|
||||
const result = await upsertNoteBackend({ title, content, tags });
|
||||
dispatch({ type: "SAVE_NOTE_SUCCESS", payload: { ...result } });
|
||||
} catch (error) {
|
||||
dispatch({ type: "SAVE_NOTE_FAIL", payload: { error } });
|
||||
}
|
||||
};
|
||||
|
||||
const getNotes = () => async (dispatch, getState) => {
|
||||
const { notesLoading } = getState();
|
||||
if (notesLoading) {
|
||||
console.warn(
|
||||
"trying to load notes when the operation has already begun."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({ type: "GET_NOTES_START" });
|
||||
try {
|
||||
const result = await getNotesBackend();
|
||||
console.log(result);
|
||||
dispatch({ type: "GET_NOTES_SUCCESS", payload: result });
|
||||
} catch (error) {
|
||||
dispatch({ type: "GET_NOTES_FAIL", payload: { error } });
|
||||
}
|
||||
};
|
||||
|
||||
const updateTags = ({ title, tags }) => (dispatch, getState) => {
|
||||
console.log(`adding tags ${tags} to ${title}`);
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
getNotes,
|
||||
deleteNote,
|
||||
createNote,
|
||||
selectNote,
|
||||
updateTags
|
||||
};
|
@ -1,8 +1,18 @@
|
||||
import weedux, { middleware } from "weedux";
|
||||
import { reducer as noteReducer, actions as acts } from "./actions";
|
||||
import { applyMiddleware, createStore, combineReducers } from "redux";
|
||||
import thunkMiddleware from "redux-thunk";
|
||||
import { createLogger } from "redux-logger";
|
||||
import { reducer as noteReducer, actions as acts } from "./notes";
|
||||
import { reducer as settingsReducer } from "./settings";
|
||||
|
||||
const { thunk } = middleware;
|
||||
const loggerMiddleware = createLogger();
|
||||
|
||||
const reducer = combineReducers({
|
||||
settings: settingsReducer,
|
||||
notes: noteReducer
|
||||
});
|
||||
// note CRUD
|
||||
export const store = new weedux({}, noteReducer, [thunk]);
|
||||
export const store = createStore(
|
||||
reducer,
|
||||
applyMiddleware(thunkMiddleware, loggerMiddleware)
|
||||
);
|
||||
export const actions = acts;
|
||||
|
@ -0,0 +1,131 @@
|
||||
import {
|
||||
loadNotes as loadNotesBackend,
|
||||
updateNote as updateNoteBackend,
|
||||
deleteNote as deleteNoteBackend
|
||||
} from "./api";
|
||||
|
||||
const SELECT_NOTE = "SELECT_NOTE";
|
||||
const LOAD_NOTES_START = "LOAD_NOTES_START";
|
||||
const LOAD_NOTES_SUCCESS = "LOAD_NOTES_SUCCESS";
|
||||
const OPERATION_FAIL = "OPERATION_FAIL";
|
||||
|
||||
const UPDATE_NOTE_START = "UPDATE_NOTE_START";
|
||||
const UPDATE_NOTE_SUCCESS = "UPDATE_NOTE_SUCCESS";
|
||||
|
||||
const CREATE_NOTE_START = "CREATE_NOTE_START";
|
||||
const CREATE_NOTE_SUCCESS = "CREATE_NOTE_SUCCESS";
|
||||
|
||||
const DELETE_NOTE_START = "DELETE_NOTE_START";
|
||||
const DELETE_NOTE_SUCCESS = "DELETE_NOTE_SUCCESS";
|
||||
|
||||
const initialState = {
|
||||
notes: [],
|
||||
selectedNoteId: null,
|
||||
error: null
|
||||
};
|
||||
|
||||
export const reducer = (cs = initialState, { type, payload }) => {
|
||||
switch (type) {
|
||||
case SELECT_NOTE:
|
||||
case UPDATE_NOTE_SUCCESS:
|
||||
case CREATE_NOTE_SUCCESS:
|
||||
return { ...cs, selectedNote: payload };
|
||||
case LOAD_NOTES_SUCCESS:
|
||||
return { ...cs, notes: payload };
|
||||
case OPERATION_FAIL:
|
||||
return { ...cs, error: payload };
|
||||
case DELETE_NOTE_SUCCESS:
|
||||
default:
|
||||
return { ...cs };
|
||||
}
|
||||
};
|
||||
|
||||
const updateNote = ({ title, tags = [], content = "" }) => async (
|
||||
dispatch,
|
||||
getState
|
||||
) => {
|
||||
try {
|
||||
if (title === "" || !title) {
|
||||
throw new Error("title cannot be null or empty");
|
||||
}
|
||||
const {
|
||||
notes: { selectedNote }
|
||||
} = getState();
|
||||
const { payload } = await updateNoteBackend({ title, tags, content });
|
||||
console.log(`save note result: `, payload);
|
||||
dispatch({
|
||||
type: UPDATE_NOTE_SUCCESS,
|
||||
payload: { ...selectedNote, ...payload, content }
|
||||
});
|
||||
} catch (error) {
|
||||
dispatch({ type: OPERATION_FAIL, payload: error });
|
||||
}
|
||||
};
|
||||
|
||||
const createNote = ({ title }) => async (dispatch, getState) => {
|
||||
try {
|
||||
if (title === "" || !title) {
|
||||
throw new Error("title cannot be null or empty");
|
||||
}
|
||||
const result = await updateNoteBackend({ title });
|
||||
dispatch({ type: CREATE_NOTE_SUCCESS, payload: result });
|
||||
} catch (error) {
|
||||
dispatch({ type: OPERATION_FAIL, payload: error });
|
||||
}
|
||||
};
|
||||
|
||||
const selectNote = ({ title }) => (dispatch, getState) => {
|
||||
const normalizedTitle = title.toLocaleLowerCase();
|
||||
const { selectedNote, notes } = getState().notes;
|
||||
|
||||
let foundNote = selectedNote;
|
||||
const notesFilter = notes.filter(
|
||||
n => n.title.toLocaleLowerCase() === normalizedTitle
|
||||
);
|
||||
|
||||
if (notesFilter.length > 0) {
|
||||
foundNote = notesFilter[0];
|
||||
dispatch({
|
||||
type: "SELECT_NOTE",
|
||||
payload: { ...foundNote }
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const deleteNote = ({ title }) => async (dispatch, getState) => {
|
||||
try {
|
||||
if (!title || title === "") {
|
||||
throw new Error("title cannot be null or empty");
|
||||
}
|
||||
const result = await deleteNoteBackend({ title });
|
||||
dispatch({ type: DELETE_NOTE_SUCCESS, payload: result });
|
||||
} catch (error) {
|
||||
dispatch({ type: OPERATION_FAIL, payload: error });
|
||||
}
|
||||
};
|
||||
|
||||
const loadNotes = () => async (dispatch, getState) => {
|
||||
const { notesLoading } = getState().notes;
|
||||
if (notesLoading) {
|
||||
console.warn(
|
||||
"trying to load notes when the operation has already begun."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { payload } = await loadNotesBackend();
|
||||
dispatch({ type: LOAD_NOTES_SUCCESS, payload: payload.notes || [] });
|
||||
} catch (error) {
|
||||
throw error;
|
||||
dispatch({ type: OPERATION_FAIL, payload: { error } });
|
||||
}
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
loadNotes,
|
||||
selectNote,
|
||||
deleteNote,
|
||||
createNote,
|
||||
updateNote
|
||||
};
|
@ -0,0 +1,22 @@
|
||||
import { getSettings as getSettingsbackend } from "./api";
|
||||
|
||||
import { defaultSettings } from "../server/backend/settings";
|
||||
|
||||
const initialState = {
|
||||
settings: { ...defaultSettings },
|
||||
loading: false,
|
||||
error: null
|
||||
};
|
||||
|
||||
export const reducer = (cs = initialState, { type, payload }) => {
|
||||
switch (type) {
|
||||
case "GET_SETTINGS_START":
|
||||
return { ...cs, loading: true };
|
||||
case "GET_SETTINGS_SUCCESS":
|
||||
return { ...cs, settings: { ...payload.settings }, loading: false };
|
||||
case "GET_SETTINGS_FAIL":
|
||||
return { ...cs, error: payload.error, loading: false };
|
||||
default:
|
||||
return { ...cs };
|
||||
}
|
||||
};
|
Loading…
Reference in new issue