feat: bookmark add / get / remove

This commit is contained in:
Michal Szczepanski 2023-01-07 03:27:37 +01:00
parent 1c13d31afd
commit 688cb9654e
12 changed files with 202 additions and 88 deletions

4
.env

@ -1,4 +1,6 @@
VERSION=1
API_URL='https://pinmenote.com'
SHORT_URL='https://pmn.cl'
WEBSITE_URL='https://pinmenote.com'
IS_PRODUCTION=true
IS_PRODUCTION=true
OBJ_LIST_LIMIT=10000

@ -1,4 +1,6 @@
VERSION=1
API_URL='http://localhost:3000'
SHORT_URL='http://localhost:8001'
WEBSITE_URL='http://localhost:4200'
IS_PRODUCTION=false
IS_PRODUCTION=false
OBJ_LIST_LIMIT=10

2
@types

@ -1 +1 @@
Subproject commit 4675716d73e30b7dd967d51253b6cdf64412e8f0
Subproject commit acd09b0317ad6153003f753b3e2c56376c64a1d9

@ -1,4 +1,21 @@
/*
* This file is part of the pinmenote-extension distribution (https://github.com/pinmenote/pinmenote-extension).
* Copyright (c) 2023 Michal Szczepanski.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { BrowserStorageWrapper } from '../../service/browser.storage.wrapper';
import { ObjAddIdCommand } from '../obj/obj-add-id.command';
import { ObjNextIdCommand } from '../obj/obj-next-id.command';
import { ObjectStoreKeys } from '../../keys/object.store.keys';
import ICommand = Pinmenote.Common.ICommand;
@ -11,20 +28,23 @@ export class BookmarkAddCommand implements ICommand<Promise<BookmarkDto>> {
async execute(): Promise<BookmarkDto> {
const key = `${ObjectStoreKeys.OBJECT_BOOKMARK}:${this.url.href}`;
const id = await new ObjNextIdCommand().execute();
const data: BookmarkDto = {
id,
value: this.value,
url: this.url,
isDirectory: false
url: this.url
};
await BrowserStorageWrapper.set(key, data);
await this.addBookmarkToList(this.url);
await this.addBookmarkToList(id);
await new ObjAddIdCommand(id).execute();
return data;
}
private async addBookmarkToList(url: PinUrl): Promise<void> {
const bookmarkList = (await BrowserStorageWrapper.get<string[] | undefined>(ObjectStoreKeys.BOOKMARK_LIST)) || [];
bookmarkList.push(url.href);
private async addBookmarkToList(id: number): Promise<void> {
const bookmarkList = (await BrowserStorageWrapper.get<number[] | undefined>(ObjectStoreKeys.BOOKMARK_LIST)) || [];
bookmarkList.push(id);
await BrowserStorageWrapper.set(ObjectStoreKeys.BOOKMARK_LIST, bookmarkList);
}
}

@ -0,0 +1,30 @@
/*
* This file is part of the pinmenote-extension distribution (https://github.com/pinmenote/pinmenote-extension).
* Copyright (c) 2023 Michal Szczepanski.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { BrowserStorageWrapper } from '../../service/browser.storage.wrapper';
import { ObjectStoreKeys } from '../../keys/object.store.keys';
import BookmarkDto = Pinmenote.Bookmark.BookmarkDto;
import ICommand = Pinmenote.Common.ICommand;
import PinUrl = Pinmenote.Pin.PinUrl;
export class BookmarkGetCommand implements ICommand<Promise<BookmarkDto | undefined>> {
constructor(private url: PinUrl) {}
async execute(): Promise<BookmarkDto | undefined> {
const key = `${ObjectStoreKeys.OBJECT_BOOKMARK}:${this.url.href}`;
const bookmark = await BrowserStorageWrapper.get<BookmarkDto | undefined>(key);
return bookmark;
}
}

@ -1,22 +1,42 @@
/*
* This file is part of the pinmenote-extension distribution (https://github.com/pinmenote/pinmenote-extension).
* Copyright (c) 2023 Michal Szczepanski.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { BrowserStorageWrapper } from '../../service/browser.storage.wrapper';
import { ObjRemoveIdCommand } from '../obj/obj-remove-id.command';
import { ObjectStoreKeys } from '../../keys/object.store.keys';
import BookmarkDto = Pinmenote.Bookmark.BookmarkDto;
import ICommand = Pinmenote.Common.ICommand;
import PinUrl = Pinmenote.Pin.PinUrl;
export class BookmarkRemoveCommand implements ICommand<Promise<void>> {
constructor(private url: PinUrl) {}
constructor(private bookmark: BookmarkDto) {}
async execute(): Promise<void> {
const key = `${ObjectStoreKeys.OBJECT_BOOKMARK}:${this.url.href}`;
const key = `${ObjectStoreKeys.OBJECT_BOOKMARK}:${this.bookmark.url.href}`;
await BrowserStorageWrapper.remove(key);
await this.removeBookmarkFromList(this.url);
await this.removeBookmarkFromList(this.bookmark.id);
await new ObjRemoveIdCommand(this.bookmark.id).execute();
}
private async removeBookmarkFromList(url: PinUrl): Promise<void> {
const bookmarkList = (await BrowserStorageWrapper.get<string[] | undefined>(ObjectStoreKeys.BOOKMARK_LIST)) || [];
private async removeBookmarkFromList(id: number): Promise<void> {
const bookmarkList = (await BrowserStorageWrapper.get<number[] | undefined>(ObjectStoreKeys.BOOKMARK_LIST)) || [];
await BrowserStorageWrapper.set(
ObjectStoreKeys.BOOKMARK_LIST,
bookmarkList.filter((u) => u !== url.href)
bookmarkList.filter((u) => u !== id)
);
}
}

@ -17,10 +17,11 @@
import { BrowserStorageWrapper } from '../../service/browser.storage.wrapper';
import { ObjUpdateLastIdCommand } from './obj-update-last-id.command';
import { ObjectStoreKeys } from '../../keys/object.store.keys';
import { environmentConfig } from '../../environment';
import ICommand = Pinmenote.Common.ICommand;
export class ObjAddIdCommand implements ICommand<Promise<void>> {
private readonly listLimit = 10;
private readonly listLimit = environmentConfig.objListLimit;
constructor(private id: number) {}
async execute(): Promise<void> {
@ -28,6 +29,7 @@ export class ObjAddIdCommand implements ICommand<Promise<void>> {
let ids = await this.getList(listId);
// hit limit so create new list
// this way we get faster writes and can batch
if (ids.length >= this.listLimit) {
listId += 1;
ids = [];

@ -33,6 +33,7 @@ interface EnvironmentConfig {
isProduction: boolean;
settings: SettingsConfig;
version: number;
objListLimit: number;
}
export const environmentConfig: EnvironmentConfig = {
@ -48,5 +49,6 @@ export const environmentConfig: EnvironmentConfig = {
borderStyle: '2px solid #ff0000',
videoDisplayTime: 5
},
version: 1
objListLimit: parseInt(process.env.OBJ_LIST_LIMIT || '100000'),
version: parseInt(process.env.VERSION || '1')
};

@ -21,13 +21,10 @@ export interface ObjLinkDto {
export interface ObjIdentityDto {
user: string;
fingerprint: string;
group: string;
}
export interface ObjEncryptionDto {
encrypted: boolean;
group: string;
}
export enum ObjTypeDto {

@ -1,6 +1,6 @@
/*
* This file is part of the pinmenote-extension distribution (https://github.com/pinmenote/pinmenote-extension).
* Copyright (c) 2022 Michal Szczepanski.
* Copyright (c) 2023 Michal Szczepanski.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -20,6 +20,7 @@ import { ActiveTabStore } from '../../store/active-tab.store';
import AddIcon from '@mui/icons-material/Add';
import { BookmarkAddCommand } from '../../../common/command/bookmark/bookmark-add.command';
import BookmarkBorderIcon from '@mui/icons-material/BookmarkBorder';
import { BookmarkGetCommand } from '../../../common/command/bookmark/bookmark-get.command';
import BookmarkIcon from '@mui/icons-material/Bookmark';
import { BookmarkRemoveCommand } from '../../../common/command/bookmark/bookmark-remove.command';
import { BrowserApi } from '../../../common/service/browser.api.wrapper';
@ -27,17 +28,21 @@ import { BusMessageType } from '../../../common/model/bus.model';
import { LogManager } from '../../../common/popup/log.manager';
import { PinPopupInitData } from '../../../common/model/pin.model';
import { TinyEventDispatcher } from '../../../common/service/tiny.event.dispatcher';
import BookmarkDto = Pinmenote.Bookmark.BookmarkDto;
export const ObjectCreateComponent: FunctionComponent = () => {
const [isAdding, setIsAdding] = useState<boolean>(ActiveTabStore.isAddingNote);
const [isBookmarked, setIsBookmarked] = useState<boolean>(ActiveTabStore.isBookmarked);
const [bookmarkData, setBookmarkData] = useState<BookmarkDto | undefined>(undefined);
useEffect(() => {
const addingKey = TinyEventDispatcher.addListener<PinPopupInitData>(
BusMessageType.POPUP_INIT,
(event, key, value) => {
async (event, key, value) => {
setIsAdding(value.isAddingNote);
setIsBookmarked(value.isBookmarked);
if (ActiveTabStore.url) {
const bookmark = await new BookmarkGetCommand(ActiveTabStore.url).execute();
setBookmarkData(bookmark);
}
}
);
return () => {
@ -69,14 +74,14 @@ export const ObjectCreateComponent: FunctionComponent = () => {
const handleBookmarkAdd = async () => {
if (!ActiveTabStore.url) return;
await new BookmarkAddCommand(ActiveTabStore.pageTitle, ActiveTabStore.url).execute();
window.close();
const bookmark = await new BookmarkAddCommand(ActiveTabStore.pageTitle, ActiveTabStore.url).execute();
setBookmarkData(bookmark);
};
const handleBookmarkRemove = async () => {
if (!ActiveTabStore.url) return;
await new BookmarkRemoveCommand(ActiveTabStore.url).execute();
window.close();
if (!bookmarkData) return;
await new BookmarkRemoveCommand(bookmarkData).execute();
setBookmarkData(undefined);
};
const pinBtn = isAdding ? (
@ -89,15 +94,16 @@ export const ObjectCreateComponent: FunctionComponent = () => {
</Button>
);
const bookmarkBtn = isBookmarked ? (
<IconButton title="Remove bookmark" onClick={handleBookmarkRemove}>
<BookmarkIcon />
</IconButton>
) : (
<IconButton title="Add bookmark" onClick={handleBookmarkAdd}>
<BookmarkBorderIcon />
</IconButton>
);
const bookmarkBtn =
bookmarkData !== undefined ? (
<IconButton title="Remove bookmark" onClick={handleBookmarkRemove}>
<BookmarkIcon />
</IconButton>
) : (
<IconButton title="Add bookmark" onClick={handleBookmarkAdd}>
<BookmarkBorderIcon />
</IconButton>
);
return (
<div style={{ display: 'flex' }}>

@ -0,0 +1,72 @@
/*
* This file is part of the pinmenote-extension distribution (https://github.com/pinmenote/pinmenote-extension).
* Copyright (c) 2023 Michal Szczepanski.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { ChangeEvent, FunctionComponent, useState } from 'react';
import ClearIcon from '@mui/icons-material/Clear';
import { IconButton } from '@mui/material';
import Input from '@mui/material/Input';
import { PinBoardStore } from '../store/pin-board.store';
import SearchIcon from '@mui/icons-material/Search';
import { fnConsoleLog } from '../../../common/fn/console.fn';
export const BoardSearchInput: FunctionComponent = () => {
const [searchValue, setSearchValue] = useState<string>(PinBoardStore.getSearch() || '');
const handleSearchChange = (e: ChangeEvent<HTMLInputElement>): void => {
fnConsoleLog('handleSearchChange');
clearTimeout(PinBoardStore.timeout);
setSearchValue(e.target.value);
PinBoardStore.clearSearch();
// setPinData([]);
if (e.target.value.length <= 2) {
PinBoardStore.timeout = window.setTimeout(async () => {
await PinBoardStore.sendRange();
}, 1000);
return;
} else {
PinBoardStore.setSearch(e.target.value);
}
PinBoardStore.timeout = window.setTimeout(async () => {
await PinBoardStore.sendSearch();
}, 1000);
};
const handleClearSearch = async () => {
fnConsoleLog('handleClearSearch');
setSearchValue('');
PinBoardStore.clearSearch();
await PinBoardStore.sendRange();
};
return (
<div style={{ width: '50%' }}>
<Input
startAdornment={<SearchIcon />}
placeholder="Find object"
style={{ width: '100%' }}
type="text"
value={searchValue}
onChange={handleSearchChange}
endAdornment={
PinBoardStore.getSearch() ? (
<IconButton onClick={handleClearSearch}>
<ClearIcon />
</IconButton>
) : undefined
}
/>
</div>
);
};

@ -1,6 +1,6 @@
/*
* This file is part of the pinmenote-extension distribution (https://github.com/pinmenote/pinmenote-extension).
* Copyright (c) 2022 Michal Szczepanski.
* Copyright (c) 2023 Michal Szczepanski.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -14,23 +14,21 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { ChangeEvent, FunctionComponent, useEffect, useRef, useState } from 'react';
import React, { FunctionComponent, useEffect, useRef, useState } from 'react';
import { BoardSearchInput } from '../menu/board-search.input';
import Box from '@mui/material/Box';
import { BusMessageType } from '../../../common/model/bus.model';
import ClearIcon from '@mui/icons-material/Clear';
import { IconButton } from '@mui/material';
import Input from '@mui/material/Input';
import { PinBoardStore } from '../store/pin-board.store';
import { PinElement } from './pin.element';
import { PinObject } from '../../../common/model/pin.model';
import SearchIcon from '@mui/icons-material/Search';
import Stack from '@mui/material/Stack';
import { TinyEventDispatcher } from '../../../common/service/tiny.event.dispatcher';
import Typography from '@mui/material/Typography';
import { fnConsoleLog } from '../../../common/fn/console.fn';
export const PinBoard: FunctionComponent = () => {
const [pinData, setPinData] = useState<PinObject[]>(PinBoardStore.pins);
const [searchValue, setSearchValue] = useState<string>(PinBoardStore.getSearch() || '');
const stackRef = useRef<HTMLDivElement>();
@ -46,7 +44,7 @@ export const PinBoard: FunctionComponent = () => {
BusMessageType.OPTIONS_PIN_SEARCH,
(event, key, value) => {
PinBoardStore.pins.push(...value);
setSearchValue(PinBoardStore.getSearch() || '');
// setSearchValue(PinBoardStore.getSearch() || '');
setPinData(PinBoardStore.pins.concat());
PinBoardStore.setLoading(false);
}
@ -99,50 +97,13 @@ export const PinBoard: FunctionComponent = () => {
}, 250);
};
const handleSearchChange = (e: ChangeEvent<HTMLInputElement>): void => {
fnConsoleLog('handleSearchChange');
clearTimeout(PinBoardStore.timeout);
setSearchValue(e.target.value);
PinBoardStore.clearSearch();
setPinData([]);
if (e.target.value.length <= 2) {
PinBoardStore.timeout = window.setTimeout(async () => {
await PinBoardStore.sendRange();
}, 1000);
return;
} else {
PinBoardStore.setSearch(e.target.value);
}
PinBoardStore.timeout = window.setTimeout(async () => {
await PinBoardStore.sendSearch();
}, 1000);
};
const handleClearSearch = async () => {
fnConsoleLog('handleClearSearch');
setSearchValue('');
PinBoardStore.clearSearch();
await PinBoardStore.sendRange();
};
return (
<div style={{ width: '100%', marginLeft: 20, marginTop: 10 }}>
<Box style={{ margin: 10 }}>
<Input
startAdornment={<SearchIcon />}
placeholder="Find note"
style={{ width: '50%' }}
type="text"
value={searchValue}
onChange={handleSearchChange}
endAdornment={
PinBoardStore.getSearch() ? (
<IconButton onClick={handleClearSearch}>
<ClearIcon />
</IconButton>
) : undefined
}
/>
<Box style={{ margin: 10, display: 'flex', flexDirection: 'row' }}>
<BoardSearchInput></BoardSearchInput>
<IconButton>
<Typography>aaaa</Typography>
</IconButton>
</Box>
<Stack direction="row" flexWrap="wrap" ref={stackRef} style={{ overflow: 'auto', height: 'calc(100vh - 65px)' }}>
{pinData.map((pin) => (