Remove unnecessary files

main
NB 4 years ago
parent b199718edb
commit 28a8ea5811
  1. 4
      package.json
  2. 37
      public/css/global.css
  3. 9
      public/css/index.css
  4. BIN
      public/favicons/favicon_24x24.png
  5. BIN
      public/favicons/favicon_48x48.png
  6. BIN
      public/favicons/favicon_96x96.png
  7. 35
      public/index.html
  8. 15
      public/manifest.json
  9. 3
      public/robots.txt
  10. 90
      src/components/commons/Snackbar.tsx
  11. 15
      src/components/commons/ViewerWrapper.tsx
  12. 72
      src/components/contextMenu/ColorItem.tsx
  13. 34
      src/components/contextMenu/Item.tsx
  14. 65
      src/components/contextMenu/Wrapper.tsx
  15. 28
      src/components/footer/Item.tsx
  16. 56
      src/components/footer/MoveBtn.tsx
  17. 17
      src/components/footer/Wrapper.tsx
  18. 34
      src/components/header/ControlBtn.tsx
  19. 132
      src/components/header/ControlIconBtn.tsx
  20. 34
      src/components/header/Layout.tsx
  21. 20
      src/components/header/VerticalLine.tsx
  22. 16
      src/components/header/Wrapper.tsx
  23. 11
      src/components/learning/Layout.tsx
  24. 34
      src/components/learning/SortedBtn.tsx
  25. 10
      src/components/learning/SortedBtnWrapper.tsx
  26. 21
      src/components/learning/highlight/PageNum.tsx
  27. 24
      src/components/learning/highlight/Post.tsx
  28. 9
      src/components/learning/highlight/Title.tsx
  29. 32
      src/components/learning/highlight/Wrapper.tsx
  30. 50
      src/components/nav/BookInfo.tsx
  31. 43
      src/components/nav/NavItem.tsx
  32. 79
      src/components/option/Dropdown.tsx
  33. 33
      src/components/option/DropdownItem.tsx
  34. 48
      src/components/option/DropdownItemWrapper.tsx
  35. 84
      src/components/option/DropdownValue.tsx
  36. 8
      src/components/option/Layout.tsx
  37. 9
      src/components/option/OptionTitle.tsx
  38. 18
      src/components/option/OptionValue.tsx
  39. 7
      src/components/option/OptionWrapper.tsx
  40. 56
      src/components/option/Slider.tsx
  41. 136
      src/components/option/SliderValue.tsx
  42. 40
      src/components/sideMenu/CloseBtn.tsx
  43. 23
      src/components/sideMenu/MenuEmpty.tsx
  44. 65
      src/components/sideMenu/Wrapper.tsx
  45. 25
      src/containers/Footer.tsx
  46. 125
      src/containers/Header.tsx
  47. 223
      src/containers/Reader.tsx
  48. 207
      src/containers/commons/ContextMenu.tsx
  49. 56
      src/containers/commons/Snackbar.tsx
  50. 52
      src/containers/menu/Nav.tsx
  51. 100
      src/containers/menu/Note.tsx
  52. 137
      src/containers/menu/Option.tsx
  53. 71
      src/containers/menu/commons/Highlight.tsx
  54. 20
      src/index.tsx
  55. 261
      src/lib/hooks/useHighlight.ts
  56. 124
      src/lib/hooks/useMenu.ts
  57. 62
      src/lib/styles/animations.ts
  58. 31
      src/lib/styles/media.ts
  59. 79
      src/lib/styles/palette.ts
  60. 24
      src/lib/styles/readerStyle.css
  61. 35
      src/lib/styles/styles.ts
  62. 14
      src/lib/styles/viewerLayout.ts
  63. 13
      src/lib/styles/zIndex.ts
  64. 36
      src/lib/svg/BookClose.js
  65. 32
      src/lib/svg/BookOpen.js
  66. 23
      src/lib/svg/Bookmark.js
  67. 22
      src/lib/svg/Bookmarker.js
  68. 26
      src/lib/svg/Close.js
  69. 35
      src/lib/svg/Controller.js
  70. 22
      src/lib/svg/Down.js
  71. 29
      src/lib/svg/Left.js
  72. 18
      src/lib/svg/Menu.js
  73. 26
      src/lib/svg/Next.js
  74. 26
      src/lib/svg/Prev.js
  75. 38
      src/lib/svg/ScrollHorizontal.js
  76. 33
      src/lib/svg/ScrollVertical.js
  77. 27
      src/lib/svg/Search.js
  78. 10
      src/lib/svg/TempIcon.tsx
  79. 1
      src/lib/svg/bookClose.svg
  80. 1
      src/lib/svg/bookOpen.svg
  81. 1
      src/lib/svg/bookmark.svg
  82. 1
      src/lib/svg/bookmarker.svg
  83. 1
      src/lib/svg/close.svg
  84. 1
      src/lib/svg/controller.svg
  85. 1
      src/lib/svg/down.svg
  86. 31
      src/lib/svg/index.ts
  87. 1
      src/lib/svg/left.svg
  88. 1
      src/lib/svg/menu.svg
  89. 1
      src/lib/svg/next.svg
  90. 1
      src/lib/svg/prev.svg
  91. 1
      src/lib/svg/scrollHorizontal.svg
  92. 1
      src/lib/svg/scrollVertical.svg
  93. 1
      src/lib/svg/search.svg
  94. 123
      src/slices/book.ts
  95. 24
      src/slices/index.ts
  96. 74
      src/slices/snackbar.ts
  97. 5
      yarn.lock

@ -48,7 +48,6 @@
"@babel/preset-env": "^7.13.12",
"@babel/preset-react": "^7.13.13",
"@babel/preset-typescript": "^7.13.0",
"@reduxjs/toolkit": "^1.5.1",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
@ -56,15 +55,12 @@
"@types/node": "^12.0.0",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"@types/react-redux": "^7.1.16",
"@types/styled-components": "^5.1.7",
"@types/throttle-debounce": "^2.1.0",
"babel-plugin-module-resolver": "4.0.0",
"babel-plugin-styled-components": "^1.12.0",
"install": "^0.13.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-redux": "^7.2.3",
"react-scripts": "^4.0.3",
"styled-components": "^5.2.3",
"typescript": "^4.2.3"

@ -1,37 +0,0 @@
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@100;300;400;500;700&display=swap');
html, body {
scroll-behavior: auto;
-webkit-overflow-scrolling: touch;
font-size: 16px;
height: 100%;
}
body {
padding: 0;
margin: 0;
}
button {
background: none;
border: none;
cursor: pointer;
}
a {
text-decoration: none;
}
* {
font-family: "Roboto", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: auto;
font-weight: 400;
-webkit-tap-highlight-color: rgba(0,0,0,0);
font-size: 1rem;
color: #333;
}
#root {
height : 100%;
}

@ -0,0 +1,9 @@
html, body {
padding: 0;
margin: 0;
}
html {
width: 100vw;
height: 100vh;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 475 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 981 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

@ -1,22 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>React-Epub-Viewer Demo</title>
<meta
name="description"
content="React-Epub-Viewer Demo Page"
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="icon" sizes="24x24" href="%PUBLIC_URL%/favicons/favicon_24x24.png" />
<link rel="icon" sizes="48x48" href="%PUBLIC_URL%/favicons/favicon_48x48.png" />
<link rel="icon" sizes="96x96" href="%PUBLIC_URL%/favicons/favicon_96x96.png" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/favicons/favicon_96x96.png" />
<link rel="stylesheet" type="text/css" href="%PUBLIC_URL%/css/global.css">
</head>
<body>
<div id="root"></div>
</body>
</html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>React Epub Viewer</title>
<meta
name="description"
content="React Epub Viewer powered by Epub.js"
/>
<link rel="stylesheet" type="text/css" href="%PUBLIC_URL%/css/index.css">
</head>
<body>
<div id="root"></div>
</body>
</html>

@ -1,15 +0,0 @@
{
"short_name": "React-Epub-Viewer",
"name": "React-Epub-Viewer Demo",
"icons": [
{
"src": "favicons/favicon_96x96.png",
"sizes": "96x96 48x48 24x24",
"type": "image/x-icon"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

@ -1,90 +0,0 @@
import styled from 'styled-components'
// lib
import * as styles from 'lib/styles/styles'
import media from 'lib/styles/media'
import palette from 'lib/styles/palette'
import animations from 'lib/styles/animations'
// types
import SnackbarType from 'types/snackbar'
const Snackbar = ({ onClick, text, type }: Props)=> {
return (
<Container onClick={onClick} type={type}>
<Content>
<Message>{text}</Message>
</Content>
</Container>
);
}
const Container = styled.div<{ type: SnackbarType }>`
position: relative;
width: 320px;
height: auto;
max-height: 800px;
background-color: ${({ type }) => {
if (type === 'SUCCESS') {
return palette.green4;
} else if (type === 'WARNING') {
return palette.orange4;
} else if (type === 'ERROR') {
return palette.red3;
} else if (type === 'INFO') {
return palette.blue4;
} else {
return palette.gray4;
}
}
};
box-shadow: ${styles.boxShadow.regular};
border-radius: 4px;
animation: ${animations.fadeInBottom} .3s cubic-bezier(0.25,0.1,0.25,1),
${animations.fadeOutBottom} .5s cubic-bezier(0.25,0.1,0.25,1) 3.6s;
cursor: pointer;
transition: .2s ${styles.transition};
${styles.noselect};
&:active {
transform: scale(.97, .97);
}
${media.small} {
border-radius: 0px;
width: 100%;
min-height: 1.8rem;
}
`;
const Content = styled.div`
position: relative;
display: flex;
width: 100%;
height: auto;
min-height: 2rem;
box-sizing: border-box;
padding: 12px 20px;
${media.small} {
min-height: 1.6rem;
line-height: 1.6rem;
}
`;
const Message = styled.div`
position: relative;
flex-grow: 1;
color: white;
font-size: 14px;
white-space: pre-wrap;
word-break: keep-all;
vertical-align: top;
`;
interface Props {
onClick(e: React.MouseEvent<HTMLDivElement, MouseEvent>): void;
text: string;
type: SnackbarType;
}
export default Snackbar

@ -1,15 +0,0 @@
import styled from 'styled-components'
// lib
import * as styles from 'lib/styles/styles'
const ViewerWrapper = styled.div`
position: relative;
width: 100vw;
height: 100vh;
overflow-x: hidden;
display: flex;
flex-direction: column;
${styles.scrollbar(0)};
`;
export default ViewerWrapper

@ -1,72 +0,0 @@
import styled from 'styled-components'
// lib
import * as styles from 'lib/styles/styles'
import palette from 'lib/styles/palette'
const ColorItem = ({ name, color, onClick }: Props) => {
const onClickItem = (e: any) => {
onClick();
e.target.blur();
};
return (
<Wrapper onClick={onClickItem}>
<ColorWrapper>
<Color color={color} />
</ColorWrapper>
<Message>{name}</Message>
</Wrapper>
);
}
const Wrapper = styled.button`
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 32px;
border-radius: 6px;
cursor: pointer;
background-color: rgba(0,0,0,0);
transition: .1s ${styles.transition};
outline: none;
&:hover, &:focus {
background-color: ${palette.blue3};
& > span {
color: #fff;
}
}
`;
const ColorWrapper = styled.div`
width: 16px;
height: 16px;
border-radius: 16px;
background-color: #fff;
margin-right: 16px;
`;
const Color = styled.div<{ color: string }>`
width: 16px;
height: 16px;
border-radius: 16px;
background-color: ${({ color }) => color};
opacity: .4;
`;
const Message = styled.span`
width: 48px;
font-size: 14px;
color: #ddd;
line-height: 20px;
`;
interface Props {
name: string;
color: string;
onClick: () => void;
}
export default ColorItem

@ -1,34 +0,0 @@
import styled from 'styled-components'
// lib
import * as styles from 'lib/styles/styles'
import palette from 'lib/styles/palette'
const Item = ({ text, onClick }: Props) => {
return <Container onClick={onClick}>{text}</Container>;
}
const Container = styled.button`
width: 100%;
height: 32px;
line-height: 32px;
text-align: center;
font-size: 14px;
color: #ccc;
transition: .1s ${styles.transition};
background-color: rgba(0,0,0,0);
border-radius: 6px;
cursor: pointer;
outline: none;
&:focus, &:hover {
color: #fff;
background-color: ${palette.red3};
}
`;
interface Props {
text: string;
onClick: (e?: any) => void;
}
export default Item

@ -1,65 +0,0 @@
import styled, { css } from 'styled-components'
// lib
import { contextmenuWidth } from 'lib/styles/viewerLayout'
import * as styles from 'lib/styles/styles'
import zIndex from 'lib/styles/zIndex'
const Wrapper = styled.div<Props>`
position: absolute;
left: ${({ x }) => window.innerWidth < x + contextmenuWidth
? `${window.innerWidth - contextmenuWidth}px`
: `${x}px`
};
top: ${({ y }) => window.innerHeight < y + 40
? `${window.innerHeight - 40}px`
: `${y}px`
};
width: ${({ width }) => width + 'px'};
height: ${({ height }) => height + 'px'};
box-sizing: border-box;
padding: ${({ width }) => width > 0
? `4px`
: ``
};
border-radius: 8px;
background-color: rgba(0,0,0,.8);
box-shadow: ${styles.boxShadow.regular};
z-index: ${zIndex.menu};
& > div:first-child {
height: 100%;
overflow-y: auto;
${styles.scrollbar(0)};
}
&::before {
display: ${({ width }) => width > 0
? `black`
: `none`
};
content: "";
position: absolute;
left: 80px;
${({ isReverse }) => isReverse
? css`bottom: -16px;`
: css`top: -16px;`
}
transform: translateX(-8px);
border: 8px solid rgba(0,0,0,0);
${({ isReverse }) => isReverse
? css`border-top: 8px solid rgba(0,0,0,.8);`
: css`border-bottom: 8px solid rgba(0,0,0,.8);`
}
z-index: 1;
}
`;
interface Props {
x: number;
y: number;
width: number;
height: number;
isReverse: boolean;
}
export default Wrapper

@ -1,28 +0,0 @@
import styled from 'styled-components'
// lib
import palette from 'lib/styles/palette'
const Item = ({ text }: Props) => {
return (
<Container>
<span>{text}</span>
</Container>
);
}
const Container = styled.div`
height: 100%;
flex-basis: 100%;
display: flex;
align-items: center;
justify-content: center;
& > span {
color: ${palette.gray4};
}
`;
interface Props {
text: string;
}
export default Item;

@ -1,56 +0,0 @@
import styled from 'styled-components'
// icons
import { PrevIcon, NextIcon } from 'lib/svg'
// lib
import * as styles from 'lib/styles/styles'
const MoveBtn = ({ type, onClick }: Props) => {
const Icon = type === "PREV"
? PrevIcon
: NextIcon;
const msg = type === "PREV"
? "이전 페이지"
: "다음 페이지";
return (
<Container onClick={onClick} title={msg}>
<Content>
<Icon />
</Content>
</Container>
);
}
const Container = styled.button`
min-width: 60px;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
outline: none;
&:focus, &:hover {
filter: invert(40%) sepia(85%) saturate(1256%) hue-rotate(210deg) brightness(113%) contrast(101%);
}
`;
const Content = styled.div`
display: flex;
align-items: center;
justify-content: center;
transition: .1s ${styles.transition};
& > svg {
width: 18px;
height: 18px;
}
`;
interface Props {
type: "PREV" | "NEXT"
onClick: () => void;
}
export default MoveBtn;

@ -1,17 +0,0 @@
import styled from 'styled-components'
// lib
import palette from 'lib/styles/palette'
const Wrapper = styled.div`
position: fixed;
left: 0;
bottom: 0;
width: 100%;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
background-color: ${palette.white};
`;
export default Wrapper

@ -1,34 +0,0 @@
import styled from 'styled-components'
// lib
import * as styles from 'lib/styles/styles'
import palette from 'lib/styles/palette'
const ControlBtn = ({ message, onClick }: Props) => {
return <Btn onClick={() => onClick()}>{message}</Btn>;
}
const Btn = styled.button`
height: 100%;
padding: 0 16px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: .1s ${styles.transition};
outline: none;
&:focus, &:hover {
color: ${palette.blue3};
}
&:first-child {
margin-left: 8px;
}
`;
interface Props {
message: string;
onClick: (value?: boolean) => void;
}
export default ControlBtn

@ -1,132 +0,0 @@
import styled, { css } from 'styled-components'
// icons
import {
ScrollHorizontalIcon,
ScrollVerticalIcon,
BookCloseIcon,
BookOpenIcon
} from 'lib/svg'
// lib
import * as styles from 'lib/styles/styles'
import palette from 'lib/styles/palette'
const ControlIconBtn = ({ type, alt, active, isSelected, onClick }: Props) => {
let HeaderIcon = null;
switch (type) {
case "ScrollVertical":
HeaderIcon = ScrollVerticalIcon;
break;
case "ScrollHorizontal":
HeaderIcon = ScrollHorizontalIcon;
break;
case "BookOpen":
HeaderIcon = BookOpenIcon;
break;
case "BookClose":
HeaderIcon = BookCloseIcon;
break;
}
const onClickBtn = () => {
if (active) onClick();
}
return (
<Btn
onClick={onClickBtn}
isSelected={isSelected}
active={active}
title={alt}
>
<Icon>
<HeaderIcon />
</Icon>
</Btn>
);
}
const Btn = styled.button<{ isSelected: boolean, active: boolean }>`
position: relative;
height: 100%;
padding: 0 12px;
display: flex;
align-items: center;
justify-content: center;
z-index: 0;
transition: .3s ${styles.transition};
opacity: ${({active}) => active
? '1'
: '.3'
};
cursor: ${({active}) => active
? 'pointer'
: 'default'
};
outline: none;
&::before {
content: '';
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: auto;
z-index: -1;
border-radius: 30px;
width: ${({isSelected}) => isSelected
? '32px'
: '8px'
};
height: ${({isSelected}) => isSelected
? '32px'
: '8px'
};
opacity: ${({active, isSelected}) => active && isSelected
? '.7'
: '0'
};
transition: .1s ${styles.transition};
background-color: ${palette.blue1};
}
&:focus, &:hover {
${({active}) => active
? css`
&:before {
width: 32px;
height: 32px;
opacity: 1;
}
`
: ""
};
}
`;
const Icon = styled.div`
width: 16px;
height: 16px;
& > div, & > svg {
width: 16px;
height: 16px;
}
`;
type HeaderIcon =
"ScrollHorizontal" |
"ScrollVertical" |
"BookOpen" |
"BookClose";
interface Props {
type: HeaderIcon;
alt: string;
active: boolean;
isSelected: boolean;
onClick: () => void;
}
export default ControlIconBtn

@ -1,34 +0,0 @@
import styled from 'styled-components'
// lib
import palette from 'lib/styles/palette'
import zIndex from 'lib/styles/zIndex'
const Layout = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 54px;
background-color: ${palette.white};
z-index: ${zIndex.header};
`;
export const AutoLayout = styled.div`
width: 100%;
flex-grow: 1;
display: flex;
justify-content: space-between;
border-bottom: 2px solid ${palette.gray1};
& > div {
display: flex;
align-items: center;
justify-content: flex-start;
}
& > div:last-child {
margin-right: 8px;
}
`;
export default Layout

@ -1,20 +0,0 @@
import styled from 'styled-components'
// lib
import palette from 'lib/styles/palette'
const VerticalLine = ({ height=16 }: Props) => {
return <Bar height={height} />
}
const Bar = styled.div<{height: number}>`
width: 1px;
height: ${({height}) => height+'px'};
margin: 0 8px;
background-color: ${palette.gray2};
`;
interface Props {
height?: number
}
export default VerticalLine

@ -1,16 +0,0 @@
import styled from 'styled-components'
// lib
import palette from 'lib/styles/palette'
import zIndex from 'lib/styles/zIndex'
const Wrapper = styled.div`
position: sticky;
width: 100%;
height: auto;
left: 0;
top: 0;
background-color: ${palette.white};
z-index: ${zIndex.header};
`;
export default Wrapper

@ -1,11 +0,0 @@
import styled from 'styled-components'
const Layout = styled.div`
flex-grow: 1;
display: flex;
flex-direction: column;
box-sizing: border-box;
padding: 24px 0;
`;
export default Layout

@ -1,34 +0,0 @@
import styled from 'styled-components'
// lib
import * as styles from 'lib/styles/styles'
import palette from 'lib/styles/palette'
const SortedBtn = ({ text, isSelected, onClick }: Props) => {
return <Btn isSelected={isSelected} onClick={onClick}>{text}</Btn>;
}
const Btn = styled.button<{isSelected: boolean}>`
height: 20px;
line-height: 20px;
font-size: 14px;
cursor: pointer;
text-align: center;
margin-left: 16px;
color: ${({isSelected}) => isSelected
? palette.blue3
: palette.gray4
};
transition: .2s ${styles.transition};
&:hover {
color: ${palette.blue3};
}
`;
interface Props {
text: string;
isSelected: boolean;
onClick: () => void;
}
export default SortedBtn

@ -1,10 +0,0 @@
import styled from 'styled-components'
const SortedBtnWrapper = styled.div`
display: flex;
align-items: center;
justify-content: flex-end;
padding: 0 24px 24px 24px;
`;
export default SortedBtnWrapper

@ -1,21 +0,0 @@
import styled from 'styled-components'
// lib
import palette from 'lib/styles/palette'
const PageNum = ({ num }: Props) => {
return <Content>{num}p</Content>
}
const Content = styled.span`
min-width: 32px;
margin-right: 16px;
font-size: 14px;
font-weight: 500;
color: ${palette.gray4};
`;
interface Props {
num: number;
}
export default PageNum

@ -1,24 +0,0 @@
import styled from 'styled-components'
const Post = styled.div<{ color: string }>`
position: relative;
font-size: 14px;
margin-top: 16px;
padding: 8px;
box-sizing: border-box;
&::before {
content: '';
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
background-color: ${({ color }) => color};
opacity: .3;
border-radius: 8px;
mix-blend-mode: multiply;
}
`;
export default Post

@ -1,9 +0,0 @@
import styled from 'styled-components'
const Title = styled.div`
font-size: 14px;
font-weight: 600;
color: inherit;
`;
export default Title

@ -1,32 +0,0 @@
import styled from 'styled-components'
// lib
import palette from 'lib/styles/palette'
import * as styles from 'lib/styles/styles'
const Wrapper = styled.button`
padding: 16px 24px;
display: flex;
align-items: flex-start;
justify-content: flex-start;
background-color: ${palette.gray0};
transition: .1s ${styles.transition};
cursor: pointer;
outline: none;
text-align: left;
border-bottom: 1px solid ${palette.gray1};
&:nth-child(2) {
border-top: 1px solid ${palette.gray1};
}
& > div {
color: inherit;
flex-grow: 1;
}
&:hover > *, &:focus > * {
color: ${palette.blue3};
}
`;
export default Wrapper

@ -1,50 +0,0 @@
import styled from 'styled-components'
const BookInfo = ({src="", title ,publisher, author}: Props) => {
return (
<Container>
<BookImg src={src} alt={title} />
<BookContent>
<Title>{title}</Title>
<Info>{publisher}</Info>
<Info>{author}</Info>
</BookContent>
</Container>
);
}
const Container = styled.div`
display: flex;
padding: 24px;
`;
const BookImg = styled.img`
margin-right: 12px;
width: 44%;
min-width: 120px;
border-radius: 4px;
background: #eee;
`;
const BookContent = styled.div`
flex-grow: 1;
`;
const Title = styled.div`
font-weight: 500;
margin-bottom: 4px;
`;
const Info = styled.div`
font-size: 14px;
margin-bottom: 4px;
`;
interface Props {
src: string;
title: string;
publisher: string;
author: string;
}
export default BookInfo

@ -1,43 +0,0 @@
import styled from 'styled-components'
// lib
import * as styles from 'lib/styles/styles'
import palette from 'lib/styles/palette'
const NavItem = ({ message, onClick }: Props) => {
return (
<Content onClick={onClick}>
<span>{message}</span>
</Content>
);
}
const Content = styled.button`
width: 100%;
height: 48px;
box-sizing: border-box;
padding: 12px 24px;
display: flex;
align-items: center;
cursor: pointer;
background-color: ${palette.gray0};
outline: none;
& > span {
transition: .2s ${styles.transition};
font-size: 14px;
}
&:focus, &:hover {
& > span {
color: ${palette.blue3};
margin-left: 12px;
}
}
`;
interface Props {
message: string;
onClick: () => void;
}
export default NavItem

@ -1,79 +0,0 @@
import { useState, useEffect, useRef, useCallback } from 'react'
import styled from 'styled-components'
// components
import OptionWrapper from 'components/option/OptionWrapper'
import OptionTitle from 'components/option/OptionTitle'
import DropdownValue from 'components/option/DropdownValue'
import DropdownItemWrapper from 'components/option/DropdownItemWrapper'
import DropdownItem from 'components/option/DropdownItem'
// types
import { BookFontFamily } from 'types/book'
const Dropdown = ({
title,
defaultValue,
valueList,
onSelect
}: Props) => {
const ref = useRef<HTMLDivElement | null>(null);
const [visible, setVisible] = useState<boolean>(false);
const value = defaultValue === "Origin" ? "Original" : defaultValue;
/** Toggle dropdown */
const onToggle = useCallback(() => setVisible(!visible), [visible]);
/** Close dropdown */
const onClose = useCallback((e: any) => {
if (!ref || !ref.current) return;
if (![...e.path].includes(ref.current)) {
onToggle();
}
}, [ref, onToggle]);
const Items = valueList.map(font =>
<DropdownItem key={font} value={font} onClick={() => {
onSelect(font);
onToggle();
}} />
);
/** Register dropdown close event */
useEffect(() => {
if (visible) {
document.addEventListener('click', onClose);
} else {
document.removeEventListener('click', onClose);
}
return () => {
document.removeEventListener('click', onClose);
}
}, [visible, onClose]);
return (
<OptionWrapper>
<OptionTitle>{title}</OptionTitle>
<DropdownWrapper ref={ref}>
<DropdownValue value={value}
isDropdown={visible}
onClick={onToggle} />
<DropdownItemWrapper show={visible}>
{Items}
</DropdownItemWrapper>
</DropdownWrapper>
</OptionWrapper>
);
}
const DropdownWrapper = styled.div`
position: relative;
`;
interface Props {
title: string;
defaultValue: BookFontFamily;
valueList: BookFontFamily[];
onSelect: (font: BookFontFamily) => void;
}
export default Dropdown

@ -1,33 +0,0 @@
import styled from 'styled-components'
// lib
import * as styles from 'lib/styles/styles'
const DropdownItem = ({ value, onClick }: Props) => {
const text = value === "Origin" ? "Original" : value;
return <Container onClick={onClick}>{text}</Container>;
}
const Container = styled.button`
width: 100%;
height: 40px;
box-sizing: border-box;
padding: 0 16px;
transition: .1s ${styles.transition};
font-size: 14px;
line-height: 40px;
cursor: pointer;
text-align: left;
outline: none;
&:focus, &:hover {
background-color: rgba(0,0,0,.05);
}
`;
interface Props {
value: string;
onClick: () => void;
}
export default DropdownItem

@ -1,48 +0,0 @@
import styled from 'styled-components'
// lib
import * as styles from 'lib/styles/styles'
import palette from 'lib/styles/palette'
const DropdownItemWrapper = ({ show, children }: Props) => {
return <Container show={show}>{children}</Container>
}
const Container = styled.div<{show: boolean}>`
position: absolute;
top: 100%;
left: 0;
width: 100%;
max-height: 200px;
overflow-y: auto;
box-sizing: border-box;
padding: 0 0 16px 0;
background-color: ${palette.white};
border: 2px solid ${palette.blue3};
z-index: 3;
border-radius: ${({show}) => show
? '0 0 20px 20px'
: '20px'
};
transition: .4s ${styles.transition};
opacity: ${({show}) => show
? '1'
: '0'
};
transform-origin: top;
transform: ${({show}) => show
? 'translateY(-2px) scaleY(1);'
: 'translateY(-40px) scaleY(0);'
};
${styles.scrollbar(6)}
${({show}) => show
? ''
: styles.noselect
};
`;
interface Props {
show: boolean;
children: React.ReactElement[];
}
export default DropdownItemWrapper

@ -1,84 +0,0 @@
import styled from 'styled-components'
// lib
import * as styles from 'lib/styles/styles'
import palette from 'lib/styles/palette'
// icons
import { DownIcon } from 'lib/svg'
const DropdownValue = ({ value, isDropdown, onClick }: Props) => {
return (
<Container
onClick={onClick}
isDropdown={isDropdown}
title="Select font"
>
<Content>{value}</Content>
<Icon isDropdown={isDropdown}>
<DownIcon />
</Icon>
</Container>
);
}
const Container = styled.button<{isDropdown: boolean}>`
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
width: 100%;
height: 40px;
background-color: ${palette.white};
border: ${({isDropdown}) => isDropdown
? `2px solid ${palette.blue3}`
: `2px solid ${palette.gray1}`
};
border-radius: ${({isDropdown}) => isDropdown
? "20px 20px 0 0"
: "20px"
};
box-sizing: border-box;
padding: 0px 16px;
cursor: pointer;
z-index: 4;
transition: .1s ${styles.transition};
text-align: left;
outline: none;
&:hover, &:focus {
border: 2px solid ${palette.blue3};
& > div {
opacity: 1;
filter: invert(40%) sepia(85%) saturate(1256%) hue-rotate(210deg) brightness(113%) contrast(101%);
}
}
`;
const Content = styled.span`
flex-grow: 1;
margin-right: 16px;
font-size: 14px;
`;
const Icon = styled.div<{isDropdown: boolean}>`
display: flex;
width: 12px;
height: 12px;
opacity: .3;
& > svg {
transform-origin: center;
transform: ${({isDropdown}) => isDropdown
? 'rotate(180deg)'
: 'rotate(0deg)'
};
}
`;
interface Props {
value: string;
isDropdown: boolean;
onClick: (e: any) => void;
}
export default DropdownValue

@ -1,8 +0,0 @@
import styled from 'styled-components'
const Wrapper = styled.div`
box-sizing: border-box;
padding: 24px;
`;
export default Wrapper

@ -1,9 +0,0 @@
import styled from 'styled-components'
const OptionTitle = styled.div`
font-size: 14px;
font-weight: 500;
margin-bottom: 16px;
`;
export default OptionTitle

@ -1,18 +0,0 @@
import styled from 'styled-components'
// lib
import palette from 'lib/styles/palette'
const OptionValue = styled.span<{ active: boolean }>`
display: inline-block;
font-size: 14px;
color: ${props => props.active
? palette.blue3
: palette.gray4
};
margin-right: 8px;
width: 40px;
text-align: right;
line-height: 20px;
`;
export default OptionValue

@ -1,7 +0,0 @@
import styled from 'styled-components'
const OptionWrapper = styled.div`
margin-bottom: 24px;
`;
export default OptionWrapper

@ -1,56 +0,0 @@
import styled from 'styled-components'
// components
import OptionWrapper from 'components/option/OptionWrapper'
import OptionTitle from 'components/option/OptionTitle'
import OptionValue from 'components/option/OptionValue'
import SliderValue from 'components/option/SliderValue'
const Slider = ({
active,
title,
minValue,
maxValue,
defaultValue,
step,
onChange
}: Props) => {
const onChangeValue = (e: any) => {
if (!active) return;
onChange(e);
}
return (
<OptionWrapper>
<OptionTitle>{title}</OptionTitle>
<SliderWrapper>
<OptionValue active={active}>{defaultValue}</OptionValue>
<SliderValue active={active}
minValue={minValue}
maxValue={maxValue}
defaultValue={defaultValue}
step={step}
onChange={onChangeValue} />
</SliderWrapper>
</OptionWrapper>
);
}
const SliderWrapper = styled.div`
display: flex;
align-items: center;
height: 20px;
`;
interface Props {
active: boolean;
title: string;
minValue: number;
maxValue: number;
defaultValue: number;
step: number;
onChange: (e: any) => void;
}
export default Slider

@ -1,136 +0,0 @@
import styled from 'styled-components'
// lib
import palette from 'lib/styles/palette'
import * as styles from 'lib/styles/styles'
const SliderValue = ({
active,
minValue,
maxValue,
defaultValue,
step,
onChange
}: Props) => {
const percentage = Math.round((defaultValue - minValue) / (maxValue - minValue) * 100);
return (<>
<Icon>-</Icon>
<SliderWrapper>
<SliderBackground />
<SliderBar
active={active}
percentage={percentage}
/>
<Slider
type="range"
min={minValue}
max={maxValue}
defaultValue={defaultValue}
step={step}
onChange={onChange}
active={active}
disabled={!active} />
</SliderWrapper>
<Icon>+</Icon>
</>);
}
const SliderWrapper = styled.div`
flex-grow: 1;
display: flex;
position: relative;
width: auto;
`;
const SliderBackground = styled.div`
position: absolute;
width: 100%;
height: 6px;
background-color: #eee;
border-radius: 6px;
z-index: 0;
`;
const SliderBar = styled.div<{ percentage: number, active: boolean }>`
position: absolute;
top: 50%;
left: 0;
transform: translateY(-50%);
width: ${({ percentage }) => percentage + '%'};
height: 6px;
border-radius: 6px 0 0 6px;
background-color: ${props => props.active
? palette.blue4
: palette.gray2
};
z-index: 1;
`;
const Slider = styled.input<{ active: boolean }>`
appearance: none;
width: 100%;
height: 6px;
background-color: transparent;
border-radius: 6px;
margin: 0;
z-index: 2;
outline: none;
&::-webkit-slider-thumb {
appearance: none;
width: 18px;
height: 18px;
border-radius: 18px;
box-shadow: ${styles.boxShadow.regular};
background-color: white;
transition: .2s ${styles.transition};
transition-property: box-shadow, background-color;
border: ${props => props.active
? `2px solid ${palette.blue3}`
: `2px solid ${palette.gray4}`
};
cursor: ${props => props.active
? 'pointer'
: ''
};;
&:hover {
background-color: ${props => props.active
? palette.blue1
: ''
};
}
&:active {
box-shadow: none;
background-color: ${props => props.active
? palette.blue3
: ''
};
}
}
&:focus::-webkit-slider-thumb {
box-shadow: none;
background-color: ${palette.blue3};
}
`;
const Icon = styled.span`
font-size: 12px;
font-weight: 500;
color: ${palette.gray4};
margin: 0 8px;
line-height: 20px;
`;
interface Props {
active: boolean;
minValue: number;
maxValue: number;
defaultValue: number;
step: number;
onChange: (e: any) => void;
}
export default SliderValue

@ -1,40 +0,0 @@
import styled from 'styled-components'
// lib
import * as styles from 'lib/styles/styles'
// icons
import { CloseIcon } from 'lib/svg'
const CloseBtn = ({ onClick }: Props) => {
return (
<Btn onClick={onClick}>
<Icon />
</Btn>
);
}
const Btn = styled.button`
position: relative;
height: 100%;
padding: 0 24px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: .1s ${styles.transition};
outline: none;
&:focus, &:hover {
filter: invert(40%) sepia(85%) saturate(1256%) hue-rotate(210deg) brightness(113%) contrast(101%);
}
`;
const Icon = styled(CloseIcon)`
width: 18px;
height: 18px;
`;
interface Props {
onClick: () => void;
}
export default CloseBtn;

@ -1,23 +0,0 @@
import styled from 'styled-components'
// lib
import palette from 'lib/styles/palette'
const MenuEmpty = ({ text }: Props) => {
return <Content>{text}</Content>;
}
const Content = styled.div`
flex-grow: 1;
height: auto;
display: flex;
align-items: center;
justify-content: center;
color: ${palette.blue2};
font-size: 14px;
`;
interface Props {
text: string;
}
export default MenuEmpty

@ -1,65 +0,0 @@
import React from 'react'
import styled from 'styled-components'
// components
import CloseBtn from 'components/sideMenu/CloseBtn'
// lib
import * as styles from 'lib/styles/styles'
import zIndex from 'lib/styles/zIndex'
import palette from 'lib/styles/palette'
const Wrapper = ({ title, show, onClose, children }: Props, ref: any) => {
return (
<Container show={show} ref={ref}>
<Header>
<span>{title}</span>
<CloseBtn onClick={onClose} />
</Header>
{children}
</Container>
)
}
const Container = styled.div`
display: flex;
flex-direction: column;
position: fixed;
width: 340px;
height: 100vh;
top: 0;
right: 0;
z-index: ${zIndex.menu};
box-shadow: -4px 0 8px 0 rgba(0,0,0,.16);
background-color: ${palette.white};
border-radius: 16px 0 0 16px;
transform: ${({ show }: { show: boolean }) => show
? `translateX(0px) scale(1)`
: `translateX(420px) scale(.8)`
};
transition: .3s ${styles.transition};
overflow-y: auto;
${styles.scrollbar()};
`;
const Header = styled.div`
width: 100%;
height: 64px;
display: flex;
align-items: center;
justify-content: space-between;
padding-top: 30px;
& > span {
padding-left: 24px;
font-size: 21px;
font-weight: 600;
}
`;
interface Props {
title: string;
show: boolean;
onClose: () => void;
children?: React.ReactNode;
}
export default React.forwardRef(Wrapper)

@ -1,25 +0,0 @@
// components
import Wrapper from 'components/footer/Wrapper'
import Item from 'components/footer/Item'
import MoveBtn from 'components/footer/MoveBtn'
const Footer = ({ title, nowPage, totalPage, onPageMove }: Props) => {
return (
<Wrapper>
<MoveBtn type="PREV" onClick={() => onPageMove("PREV")} />
<Item text={title} />
<Item text={`${nowPage} / ${totalPage}`} />
<MoveBtn type="NEXT" onClick={() => onPageMove("NEXT")} />
</Wrapper>
);
}
interface Props {
title: string;
nowPage: number;
totalPage: number;
onPageMove: (type: "PREV" | "NEXT") => void;
}
export default Footer

@ -1,125 +0,0 @@
import { useState } from 'react'
// components
import Wrapper from 'components/header/Wrapper'
import Layout, { AutoLayout } from 'components/header/Layout'
import ControlBtn from 'components/header/ControlBtn'
import ControlIconBtn from 'components/header/ControlIconBtn'
import VerticalLine from 'components/header/VerticalLine'
// types
import { BookOption } from 'types/book'
const Header = ({
title,
bookOption,
onNavToggle,
onOptionToggle,
onLearningToggle,
onBookOptionChange
}: Props) => {
const [isScrollHorizontal, setIsScrollHorizontal] = useState<boolean>(true);
const [viewType, setViewType] = useState<ViewType>({
active: true,
spread: true
});
/**
* Select view direction
* @param type Direction
*/
const onClickDirection = (type: "Horizontal" | "Vertical") => {
if (type === "Horizontal") {
setIsScrollHorizontal(true);
setViewType({ ...viewType, active: true });
onBookOptionChange({
...bookOption,
flow: "paginated"
});
} else {
setIsScrollHorizontal(false);
setViewType({ ...viewType, active: false });
onBookOptionChange({
...bookOption,
flow: "scrolled-doc"
});
}
}
/**
* Select isSpread
* @param isSpread Whether spread view
*/
const onClickViewType = (isSpread: boolean) => {
if (isSpread) {
setViewType({ ...viewType, spread: true });
onBookOptionChange({
...bookOption,
spread: "auto"
});
} else {
setViewType({ ...viewType, spread: false });
onBookOptionChange({
...bookOption,
spread: "none"
});
}
}
return (
<Wrapper>
<Layout>
<AutoLayout>
<div>
<ControlBtn message="Contents" onClick={onNavToggle} />
<ControlBtn message="Setting" onClick={onOptionToggle} />
<ControlBtn message="Highlights" onClick={onLearningToggle} />
</div>
<div>
<ControlIconBtn type="ScrollHorizontal"
alt="Horizontal View"
active={true}
isSelected={isScrollHorizontal}
onClick={() => onClickDirection("Horizontal")} />
<ControlIconBtn type="ScrollVertical"
alt="Vertical View"
active={true}
isSelected={!isScrollHorizontal}
onClick={() => onClickDirection("Vertical")} />
<VerticalLine />
<ControlIconBtn type="BookOpen"
alt="Two Page View"
active={viewType.active}
isSelected={viewType.spread}
onClick={() => onClickViewType(true)} />
<ControlIconBtn type="BookClose"
alt="One Page View"
active={viewType.active}
isSelected={!viewType.spread}
onClick={() => onClickViewType(false)} />
</div>
</AutoLayout>
</Layout>
</Wrapper>
);
}
interface Props {
title: string;
bookOption: BookOption;
onNavToggle: (value?: boolean) => void;
onOptionToggle: (value?: boolean) => void;
onLearningToggle: (value?: boolean) => void;
onBookOptionChange: (bookOption: BookOption) => void;
}
type ViewType = {
active: boolean,
spread: boolean
}
export default Header

@ -1,223 +0,0 @@
import { useState, useRef } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { Provider } from 'react-redux'
// containers
import Viewer from 'modules/reactViewer/ReactViewer'
import Header from 'containers/Header'
import Footer from 'containers/Footer'
import Nav from 'containers/menu/Nav'
import Option from 'containers/menu/Option'
import Learning from 'containers/menu/Note'
import ContextMenu from 'containers/commons/ContextMenu'
import Snackbar from 'containers/commons/Snackbar'
// components
import ViewerWrapper from 'components/commons/ViewerWrapper'
import LoadingView from 'LoadingView'
// slices
import store from 'slices'
import { updateBook, updateCurrentPage, updateToc } from 'slices/book'
// hooks
import useMenu from 'lib/hooks/useMenu'
import useHighlight from 'lib/hooks/useHighlight'
// styles
import 'lib/styles/readerStyle.css'
import viewerLayout from 'lib/styles/viewerLayout'
// types
import { RootState } from 'slices'
import { ViewerRef } from 'types'
import Book, { BookStyle, BookOption } from 'types/book'
import Page from 'types/page'
import Toc from 'types/toc'
const Reader = ({ url, loadingView }: Props) => {
const dispatch = useDispatch();
const book = useSelector<RootState, Book>(state => state.book.book);
const currentLocation = useSelector<RootState, Page>(state => state.book.currentLocation);
const viewerRef = useRef<ViewerRef | null>(null);
const navRef = useRef<HTMLDivElement | null>(null);
const optionRef = useRef<HTMLDivElement | null>(null);
const learningRef = useRef<HTMLDivElement | null>(null);
const [isContextMenu, setIsContextMenu] = useState<boolean>(false);
const [bookStyle, setBookStyle] = useState<BookStyle>({
fontFamily: 'Origin',
fontSize: 18,
lineHeight: 1.4,
marginHorizontal: 15,
marginVertical: 5
});
const [bookOption, setBookOption] = useState<BookOption>({
flow: "paginated",
resizeOnOrientationChange: true,
spread: "auto"
});
const [navControl, onNavToggle] = useMenu(navRef, 300.);
const [optionControl, onOptionToggle, emitEvent] = useMenu(optionRef, 300);
const [learningControl, onLearningToggle] = useMenu(learningRef, 300);
const {
selection,
onSelection,
onClickHighlight,
onAddHighlight,
onRemoveHighlight,
onUpdateHighlight
} = useHighlight(viewerRef, setIsContextMenu, bookStyle, bookOption.flow);
/**
* Change Epub book information
* @param book Epub Book Info
*/
const onBookInfoChange = (book: Book) => dispatch(updateBook(book));
/**
* Change Epub location
* @param loc epubCFI or href
*/
const onLocationChange = (loc: string) => {
if (!viewerRef.current) return;
viewerRef.current.setLocation(loc);
}
/**
* Move page
* @param type Direction
*/
const onPageMove = (type: "PREV" | "NEXT") => {
const node = viewerRef.current;
if (!node || !node.prevPage || !node.nextPage) return;
type === "PREV" && node.prevPage();
type === "NEXT" && node.nextPage();
};
/**
* Set toc
* @param toc Table of Epub contents
*/
const onTocChange = (toc: Toc[]) => dispatch(updateToc(toc));
/**
* Set Epub viewer styles
* @param bokkStyle_ viewer style
*/
const onBookStyleChange = (bookStyle_: BookStyle) => setBookStyle(bookStyle_);
/**
* Set Epub viewer options
* @param bookOption_ viewer option
*/
const onBookOptionChange = (bookOption_: BookOption) => setBookOption(bookOption_);
/**
* Change current page
* @param page Epub page
*/
const onPageChange = (page: Page) => dispatch(updateCurrentPage(page));
/**
* ContextMenu on
* @param cfiRange CfiRange
*/
const onContextMenu = (cfiRange: string) => {
const result = onSelection(cfiRange);
setIsContextMenu(result);
}
/** ContextMenu off */
const onContextmMenuRemove = () => setIsContextMenu(false);
return (<>
<ViewerWrapper>
<Header
title={book.title}
bookOption={bookOption}
onNavToggle={onNavToggle}
onOptionToggle={onOptionToggle}
onLearningToggle={onLearningToggle}
onBookOptionChange={onBookOptionChange}
/>
<Viewer
url={url}
viewerLayout={viewerLayout}
viewerStyle={bookStyle}
viewerOption={bookOption}
onBookInfoChange={onBookInfoChange}
onPageChange={onPageChange}
onTocChange={onTocChange}
onSelection={onContextMenu}
loadingView={loadingView || <LoadingView />}
ref={viewerRef}
/>
<Footer
title={currentLocation.chapterName}
nowPage={currentLocation.currentPage}
totalPage={currentLocation.totalPage}
onPageMove={onPageMove}
/>
</ViewerWrapper>
<Nav
control={navControl}
onToggle={onNavToggle}
onLocation={onLocationChange}
ref={navRef}
/>
<Option
control={optionControl}
bookStyle={bookStyle}
bookFlow={bookOption.flow}
onToggle={onOptionToggle}
emitEvent={emitEvent}
onBookStyleChange={onBookStyleChange}
ref={optionRef}
/>
<Learning
control={learningControl}
onToggle={onLearningToggle}
onClickHighlight={onClickHighlight}
emitEvent={emitEvent}
viewerRef={viewerRef}
ref={learningRef}
/>
<ContextMenu
active={isContextMenu}
viewerRef={viewerRef}
selection={selection}
onAddHighlight={onAddHighlight}
onRemoveHighlight={onRemoveHighlight}
onUpdateHighlight={onUpdateHighlight}
onContextmMenuRemove={onContextmMenuRemove}
/>
<Snackbar />
</>);
}
const ReaderWrapper = ({ url, loadingView }: Props) => {
return (
<Provider store={store}>
<Reader url={url} loadingView={loadingView} />
</Provider>
);
}
interface Props {
url: string;
loadingView?: React.ReactNode;
}
export default ReaderWrapper

@ -1,207 +0,0 @@
import { useState, useEffect, useCallback, useRef } from 'react'
import { useSelector } from 'react-redux'
// components
import Wrapper from 'components/contextMenu/Wrapper'
import ColorItem from 'components/contextMenu/ColorItem'
import Item from 'components/contextMenu/Item'
// utils
import { getParagraphCfi } from 'lib/utils/commonUtil'
// slices
import { contextmenuWidth } from 'lib/styles/viewerLayout'
// types
import { RootState } from 'slices'
import Highlight, { Color } from 'types/highlight'
import Selection from 'types/selection'
const ContextMenu = ({
active,
viewerRef,
selection,
onAddHighlight,
onRemoveHighlight,
onUpdateHighlight,
onContextmMenuRemove
}: Props) => {
const highlights = useSelector<RootState, Highlight[]>(state => state.book.highlights);
const colorList = useSelector<RootState, Color[]>(state => state.book.colorList);
const menuRef = useRef<HTMLDivElement | null>(null);
const [highlight, setHighlight] = useState<Highlight | null>(null);
const [display, setDisplay] = useState<boolean>(false);
const [isEraseBtn, setIsEraseBtn] = useState<boolean>(false);
const [isReverse, setIsReverse] = useState<boolean>(false);
const [height, setHeight] = useState<number>(0);
const [y, setY] = useState<number>(selection.y);
const ColorList = colorList.map(
color => <ColorItem key={color.code}
name={color.name}
color={color.code}
onClick={selection.update
? () => onUpdateHighlight(highlight, color.code)
: () => onAddHighlight(color.code)
} />
);
/** Remove highlight */
const onRemoveHighlight_ = useCallback(() => {
if (!highlight) return;
onRemoveHighlight(highlight.key, highlight.cfiRange);
onContextmMenuRemove();
setIsEraseBtn(false);
}, [highlight, onRemoveHighlight, onContextmMenuRemove]);
/**
* Remove contextmenu
* @param e Mouse Event
* @param e.path
*/
const onRemove = useCallback(({ path }: any) => {
if (!menuRef.current) return;
if ([...path].includes(menuRef.current)) return;
onContextmMenuRemove();
}, [menuRef, onContextmMenuRemove]);
/**
* Arrow event
* @param e Keyboard Event
* @param e.key
*/
const onKeyPress = useCallback(({ key }: any) => {
key && key === "ArrowLeft" && onContextmMenuRemove();
key && key === "ArrowRight" && onContextmMenuRemove();
}, [onContextmMenuRemove]);
/** Check whether the menu button is visible */
useEffect(() => {
if (!active) setIsEraseBtn(false);
const paragraphCfi = getParagraphCfi(selection.cfiRange);
if (!paragraphCfi) return;
const filtered = highlights.filter(highlight => highlight.key === paragraphCfi + selection.cfiRange);
if (!filtered.length) return;
const highlight_ = filtered[0];
setHighlight(highlight_);
if (selection.update) {
setIsEraseBtn(true);
} else {
setIsEraseBtn(false);
}
}, [
active,
highlights,
selection.cfiRange,
selection.update
]);
/** Register contextmenu events */
useEffect(() => {
if (!viewerRef.current) return;
const iframe = document.querySelector('iframe');
const node = iframe && iframe.contentWindow && iframe.contentWindow.document;
const scrolledTarget = viewerRef.current.querySelector('div');
if (active) {
setDisplay(true);
scrolledTarget && scrolledTarget.addEventListener('scroll', onContextmMenuRemove);
node && node.addEventListener('mousedown', onRemove);
node && node.addEventListener('keyup', onKeyPress);
document.addEventListener('mousedown', onRemove);
document.addEventListener('keyup', onKeyPress);
} else {
setDisplay(false);
scrolledTarget && scrolledTarget.removeEventListener('scroll', onContextmMenuRemove);
node && node.removeEventListener('mousedown', onRemove);
node && node.removeEventListener('keyup', onKeyPress);
document.removeEventListener('mousedown', onRemove);
document.removeEventListener('keyup', onKeyPress);
};
return () => {
node && node.removeEventListener('mousedown', onContextmMenuRemove);
node && node.removeEventListener('keyup', onKeyPress);
document.removeEventListener('keyup', onKeyPress);
}
}, [
viewerRef,
active,
onRemove,
onContextmMenuRemove,
onKeyPress
]);
/** Set modified contextmenu height & whether menu is reverse */
useEffect(() => {
const menuPadding = 8;
const itemHeight = 32;
let itemCnt = ColorList.length;
if (isEraseBtn) itemCnt += 1;
const defaultHeight = itemCnt * itemHeight + menuPadding;
let y_ = selection.y;
const { innerHeight } = window;
if (selection.y + defaultHeight > innerHeight) {
y_ = selection.y - selection.height - (defaultHeight);
if (y_ < 0) {
setHeight(defaultHeight + y_ - 8);
y_ = 8;
} else {
setHeight(defaultHeight);
}
setIsReverse(true);
} else {
setHeight(defaultHeight);
setIsReverse(false);
}
setY(y_);
}, [
selection.y,
selection.height,
ColorList,
isEraseBtn,
setHeight
]);
return (<>
{display && <Wrapper x={selection.x}
y={y}
width={contextmenuWidth}
height={height}
isReverse={isReverse}
ref={menuRef}>
<div>
{ColorList}
{isEraseBtn && <Item text="Remove" onClick={onRemoveHighlight_} />}
</div>
</Wrapper>}
</>);
}
interface Props {
active: boolean;
viewerRef: any;
selection: Selection;
onAddHighlight: (color: string) => void;
onRemoveHighlight: (key: string, cfiRange: string) => void;
onUpdateHighlight: (highlight: Highlight | null, color: string) => void;
onContextmMenuRemove: () => void;
}
export default ContextMenu

@ -1,56 +0,0 @@
import styled from 'styled-components'
import { useSelector, useDispatch } from 'react-redux'
// components
import Snackbar from 'components/commons/Snackbar'
// lib
import zIndex from 'lib/styles/zIndex'
import media from 'lib/styles/media'
// slices
import { deleteSnackbar } from 'slices/snackbar'
// types
import { RootState } from 'slices'
import { SnackbarState } from 'slices/snackbar'
const SnackbarWrapper = ()=> {
const dispatch = useDispatch();
const snackbar = useSelector<RootState, SnackbarState>(state => state.snackbar);
const onClick = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
if (e) {
e.preventDefault();
e.stopPropagation();
}
dispatch(deleteSnackbar());
};
return (
<Container>
{snackbar.text !== '' && <Snackbar onClick={onClick}
text={snackbar.text}
type={snackbar.type} />}
</Container>
);
}
const Container = styled.div`
position: fixed;
top: 40px;
right: 30px;
width: 320px;
height: auto;
z-index: ${zIndex.snackbar};
${media.small} {
width: 100%;
top: initial;
bottom: 0;
left: 0;
right: 0;
margin: auto;
}
`;
export default SnackbarWrapper

@ -1,52 +0,0 @@
import React from 'react'
import { useSelector } from 'react-redux'
// components
import Wrapper from 'components/sideMenu/Wrapper'
import BookInfo from 'components/nav/BookInfo'
import NavItem from 'components/nav/NavItem'
// types
import { RootState } from 'slices'
import Book from 'types/book'
import Toc from 'types/toc'
import { MenuControl } from 'lib/hooks/useMenu'
const Nav = ({ control, onToggle, onLocation }: Props, ref: any) => {
const book = useSelector<RootState, Book>(state => state.book.book);
const bookToc = useSelector<RootState, Toc[]>(state => state.book.toc);
/** Click nav item */
const onClickItem = (loc: string) => {
onLocation(loc);
onToggle();
}
const Tocs = bookToc.map((t, idx) =>
<NavItem key={idx}
message={t.label}
onClick={() => onClickItem(t.href)} />
);
return (<>
{control.display && <Wrapper title="Contents"
show={control.open}
onClose={onToggle}
ref={ref}>
<BookInfo
src={book.coverURL}
title={book.title}
publisher={book.publisher}
author={book.author}
/>
{Tocs}
</Wrapper>}
</>);
}
interface Props {
control: MenuControl;
onToggle: () => void;
onLocation: (loc: string) => void;
}
export default React.forwardRef(Nav)

@ -1,100 +0,0 @@
import React, { useState, useEffect } from 'react'
import { useSelector } from 'react-redux'
// containers
import Highlight from 'containers/menu/commons/Highlight'
// components
import Wrapper from 'components/sideMenu/Wrapper'
import LearningLayout from 'components/learning/Layout'
import SortedBtnWrapper from 'components/learning/SortedBtnWrapper'
import SortedBtn from 'components/learning/SortedBtn'
import MenuEmpty from 'components/sideMenu/MenuEmpty'
// types
import { RootState } from 'slices'
import HighlightType from 'types/highlight'
import { MenuControl } from 'lib/hooks/useMenu'
const Note = ({
control,
onToggle,
onClickHighlight,
emitEvent,
viewerRef
}: Props, ref: any) => {
const highlights = useSelector<RootState, HighlightType[]>(state => state.book.highlights);
const [sorted, setSorted] = useState<Sorted>("CART");
const [highlightList, setHighlightList] = useState<any[]>([]);
/**
* Select sort type
* @param type Viewed or Created
*/
const onSortedClick = (type: Sorted) => setSorted(type);
/** Set highlight contents */
useEffect(() => {
const sortedKey = sorted === "LOOK"
? "accessTime"
: "createTime";
const highlightList = highlights.slice().sort(function (a, b) {
return new Date(b[sortedKey]).getTime() - new Date(a[sortedKey]).getTime();
});
const Items = highlightList.map(h => <Highlight key={h.key}
highlight={h}
onClick={onClickHighlight}
emitEvent={emitEvent}
viewerRef={viewerRef} />
);
setHighlightList(Items);
}, [
viewerRef,
sorted,
highlights,
onClickHighlight,
emitEvent,
setHighlightList
]);
return (<>
{control.display && <Wrapper title="Highlight"
show={control.open}
onClose={onToggle}
ref={ref} >
<LearningLayout>
<SortedBtnWrapper>
<SortedBtn
text="viewed"
isSelected={sorted === "LOOK"}
onClick={() => onSortedClick("LOOK")}
/>
<SortedBtn
text="created"
isSelected={sorted === "CART"}
onClick={() => onSortedClick("CART")}
/>
</SortedBtnWrapper>
{highlightList.length > 0
? highlightList
: <MenuEmpty text="Empty highlights!" />
}
</LearningLayout>
</Wrapper>}
</>);
}
type Sorted = "LOOK" | "CART"
interface Props {
control: MenuControl;
onToggle: () => void;
onClickHighlight: (highlightNode: any) => void;
emitEvent: () => void;
viewerRef: any;
}
export default React.forwardRef(Note)

@ -1,137 +0,0 @@
import React from 'react'
import { useState, useEffect } from 'react'
// components
import Wrapper from 'components/sideMenu/Wrapper'
import OptionLayout from 'components/option/Layout'
import OptionDropdown from 'components/option/Dropdown'
import OptionSlider from 'components/option/Slider'
// types
import { BookStyle, BookFontFamily, BookFlow } from 'types/book'
import { MenuControl } from 'lib/hooks/useMenu'
const Option = ({
control,
bookStyle,
bookFlow,
onToggle,
emitEvent,
onBookStyleChange
}: Props, ref: any) => {
const [fontFamily, setFontFamily] = useState<BookFontFamily>(bookStyle.fontFamily);
const [fontSize, setFontSize] = useState<number>(bookStyle.fontSize);
const [lineHeight, setLineHeight] = useState<number>(bookStyle.lineHeight);
const [marginHorizontal, setMarginHorizontal] = useState<number>(bookStyle.marginHorizontal);
const [marginVertical, setMarginVertical] = useState<number>(bookStyle.marginVertical);
/** Change font family */
const onSelectFontFamily = (font: BookFontFamily) => setFontFamily(font);
/** Change font style and layout */
const onChangeSlider = (type: SliderType, e: any) => {
if (!e || !e.target) return;
switch (type) {
case "FontSize":
setFontSize(e.target.value);
break;
case "LineHeight":
setLineHeight(e.target.value);
break;
case "MarginHorizontal":
setMarginHorizontal(e.target.value);
break;
case "MarginVertical":
setMarginVertical(e.target.value);
break;
default:
break;
}
}
/* Save userdata */
// TODO Fix the infinite re-rendering issue, when inlcude `onBookStyleChange` to dependencies array.
/* eslint-disable */
useEffect(() => {
const timer = window.setTimeout(() => {
onBookStyleChange({
fontFamily,
fontSize,
lineHeight,
marginHorizontal,
marginVertical
});
}, 250);
return () => window.clearTimeout(timer);
}, [
fontFamily,
fontSize,
lineHeight,
marginHorizontal,
marginVertical
]);
/* eslint-enable */
/** Re-register close event, when after set */
useEffect(() => emitEvent(), [bookStyle, emitEvent]);
return (<>
{control.display && <Wrapper title="Setting"
show={control.open}
onClose={onToggle}
ref={ref}>
<OptionLayout>
<OptionDropdown title="Font"
defaultValue={fontFamily}
valueList={["Origin", "Roboto"]}
onSelect={onSelectFontFamily} />
<OptionSlider active={true}
title="Size"
minValue={8}
maxValue={36}
defaultValue={fontSize}
step={1}
onChange={(e) => onChangeSlider("FontSize", e)} />
<OptionSlider active={true}
title="Line height"
minValue={1}
maxValue={3}
defaultValue={lineHeight}
step={0.1}
onChange={(e) => onChangeSlider("LineHeight", e)} />
<OptionSlider active={true}
title="Horizontal margin"
minValue={0}
maxValue={100}
defaultValue={marginHorizontal}
step={1}
onChange={(e) => onChangeSlider("MarginHorizontal", e)} />
<OptionSlider active={bookFlow === "paginated"}
title="Vertical margin"
minValue={0}
maxValue={100}
defaultValue={marginVertical}
step={1}
onChange={(e) => onChangeSlider("MarginVertical", e)} />
</OptionLayout>
</Wrapper>}
</>);
}
interface Props {
control: MenuControl;
bookStyle: BookStyle;
bookFlow: BookFlow;
onToggle: () => void;
emitEvent: () => void;
onBookStyleChange: (bookStyle: BookStyle) => void;
}
type SliderType = "FontSize"
| "LineHeight"
| "MarginHorizontal"
| "MarginVertical";
export default React.forwardRef(Option)

@ -1,71 +0,0 @@
import { useDispatch } from 'react-redux'
// components
import Wrapper from 'components/learning/highlight/Wrapper'
import Title from 'components/learning/highlight/Title'
import Post from 'components/learning/highlight/Post'
import PageNum from 'components/learning/highlight/PageNum'
// slices
import { updateHighlight } from 'slices/book'
// utils
import { cfiRangeSpliter } from 'lib/utils/commonUtil'
// types
import HighlightType from 'types/highlight'
const Highlight = ({
highlight,
onClick,
emitEvent,
viewerRef
}: Props) => {
const dispatch = useDispatch();
/** 하이라이트 클릭 */
const onClickHighlight = () => {
if (!viewerRef.current) return;
const now = new Date().toISOString();
dispatch(updateHighlight({
...highlight,
accessTime: now
}));
const splitCfi = cfiRangeSpliter(highlight.cfiRange);
if (!splitCfi) return;
const { startCfi } = splitCfi;
viewerRef.current.setLocation(startCfi);
emitEvent();
const svgContainer = viewerRef.current.querySelector("svg");
if (!svgContainer) return;
const targetSvg = svgContainer.querySelector(`g[data-epubcfi="${highlight.cfiRange}"]`);
if (!targetSvg) return;
onClick(targetSvg.childNodes[0]);
}
return (
<Wrapper onClick={onClickHighlight}>
<PageNum num={highlight.pageNum} />
<div>
<Title>{highlight.chpaterName}</Title>
<Post color={highlight.color}>{highlight.content}</Post>
</div>
</Wrapper>
);
}
interface Props {
highlight: HighlightType;
onClick: (highlightNode: any) => void;
emitEvent: () => void;
viewerRef: any;
}
export default Highlight

@ -1,6 +1,20 @@
import { useRef } from 'react'
import ReactDOM from 'react-dom';
import Reader from 'containers/Reader'
import ReactEpubViewer from 'modules/reactViewer/ReactViewer'
import { ViewerRef } from 'types'
const EPUB_URL = "files/Alices Adventures in Wonderland.epub";
ReactDOM.render(<Reader url={EPUB_URL} />, document.getElementById('root'));
const App = () => {
const EPUB_URL = "files/Alices Adventures in Wonderland.epub";
const ref = useRef<ViewerRef>(null);
return (
<ReactEpubViewer
url={EPUB_URL}
ref={ref}
/>
);
}
ReactDOM.render(<App />, document.getElementById('root'));

@ -1,261 +0,0 @@
import React, { useState, useEffect, useCallback } from 'react'
import { useSelector, useDispatch } from 'react-redux'
// slices
import { pushHighlight, popHighlight, updateHighlight } from 'slices/book'
import { newSnackbar } from 'slices/snackbar'
// utils
import {
getParagraphCfi,
clashCfiRange,
getSelectionPosition,
compareCfi,
cfiRangeSpliter,
getNodefromCfi
} from 'lib/utils/commonUtil'
// styles
import viewerLayout, { contextmenuWidth } from 'lib/styles/viewerLayout'
// types
import { RootState } from 'slices'
import { BookStyle, BookFlow } from 'types/book'
import Page from 'types/page'
import Selection from 'types/selection'
import Highlight from 'types/highlight'
/**
* Epub Reader의 Highlight hook
* - [selection]: selection의
* - [onSelection]: selection (Iframe에 selection .)
* - [onClickHighlight]: highlight
* - [onAddHighlight] highlight
* - [onRemoveHighlight]: highlight
* - [onUpdateHighlight]: highlight
*
* @param viewerRef Viewer에 useRef
* @param setIsContextMenu Contextmenu를 useState
*
* @example
* const [isContextMenu, setIsContextMenu] = useState<boolean>(false);
*
* const {
* selection,
* onSelection,
* onClickHighlight,
* onRemoveHighlight
* } = useSelection(viewerRef, setIsContextMenu);
*/
const useHighlight = (
viewerRef: any,
setIsContextMenu: React.Dispatch<React.SetStateAction<boolean>>,
bookStyle: BookStyle,
bookFlow: BookFlow
) => {
const dispatch = useDispatch();
const currentLocation = useSelector<RootState, Page>(state => state.book.currentLocation);
const highlights = useSelector<RootState, Highlight[]>(state => state.book.highlights);
const [selection, setSelection] = useState<Selection>({
update: false,
x: 0,
y: 0,
height: 0,
cfiRange: '',
content: ''
});
/**
* Selection
* @param cfiRange CfiRange
* @return Whether event is success
*/
const onSelection = useCallback((cfiRange: string): boolean => {
if (!viewerRef.current) return false;
const iframe = viewerRef.current.querySelector('iframe');
if (!iframe) return false;
const iframeWin = iframe.contentWindow;
if (!iframeWin) return false;
const filtered = highlights.filter(
highlight => clashCfiRange(highlight.cfiRange, cfiRange)
);
if (filtered.length > 0) {
dispatch(newSnackbar({
text: "There are already registered highlights in your selection.",
type: "WARNING"
}));
iframeWin.getSelection().removeAllRanges();
return false;
}
const position = getSelectionPosition(
viewerRef.current,
bookStyle,
bookFlow,
viewerLayout.MIN_VIEWER_WIDTH,
viewerLayout.MIN_VIEWER_HEIGHT,
viewerLayout.VIEWER_HEADER_HEIGHT,
contextmenuWidth
);
if (!position) return false;
const { x, y, height } = position;
const content = iframeWin.getSelection().toString().trim();
if (content.length === 0) return false;
setSelection({
update: false,
x,
y,
height,
cfiRange,
content
});
return true;
}, [
dispatch,
viewerRef,
highlights,
bookFlow,
bookStyle,
setSelection
]);
/** Highlight 클릭 이벤트 */
const onClickHighlight = useCallback((highlightNode: HTMLElement) => {
const targetNode = highlightNode.parentElement;
if (!targetNode) return;
const cfiRange = targetNode.dataset.epubcfi;
if (!cfiRange) return;
const { x, y, width, height } = targetNode.getBoundingClientRect();
setSelection({
update: true,
x: x + width / 2 - contextmenuWidth / 2,
y: y + height,
height,
cfiRange,
content: ""
});
}, [setSelection]);
/** 하이라이트 생성 함수 */
const onAddHighlight = useCallback((color: string) => {
const paragraphCfi = getParagraphCfi(selection.cfiRange);
if (!paragraphCfi) return;
const highlight: Highlight = {
key: paragraphCfi + selection.cfiRange,
accessTime: new Date().toISOString(),
createTime: new Date().toISOString(),
color,
paragraphCfi,
cfiRange: selection.cfiRange,
chpaterName: currentLocation.chapterName,
pageNum: currentLocation.currentPage,
content: selection.content
};
dispatch(pushHighlight(highlight));
setSelection({ ...selection, update: true });
}, [dispatch, selection, currentLocation]);
/**
*
* @param color Hex
*/
const onUpdateHighlight = useCallback((highlight: Highlight | null, color: string) => {
if (!highlight) return;
dispatch(updateHighlight({
...highlight,
color
}));
}, [dispatch]);
/**
*
* @param key
*/
const onRemoveHighlight = useCallback((key: string, cfiRange: string) => {
if (!viewerRef.current || !key) return;
dispatch(popHighlight(key));
dispatch(newSnackbar({
text: "The highlight removed successful!",
type: "INFO"
}));
viewerRef.current.offHighlight(cfiRange);
}, [dispatch, viewerRef]);
/**
*
* -
*/
useEffect(() => {
if (!viewerRef.current) return;
const iframe = viewerRef.current.querySelector('iframe');
if (!iframe) return;
const iframeWin = iframe.contentWindow;
if (!iframeWin) return;
highlights.forEach(highlight => {
const cfiRange = cfiRangeSpliter(highlight.cfiRange);
if (!cfiRange) return;
const { startCfi } = cfiRange;
if (compareCfi(currentLocation.startCfi, startCfi) < 1
&& compareCfi(currentLocation.endCfi, startCfi) > -1) {
const node = getNodefromCfi(highlight.paragraphCfi);
if (!node) return;
viewerRef.current.onHighlight(
highlight.cfiRange,
(e: any) => {
onClickHighlight(e.target);
setIsContextMenu(true);
},
highlight.color
);
iframeWin.getSelection().removeAllRanges();
}
});
}, [
dispatch,
viewerRef,
currentLocation,
highlights,
onRemoveHighlight,
onClickHighlight,
setIsContextMenu
]);
return {
selection,
onSelection,
onClickHighlight,
onAddHighlight,
onRemoveHighlight,
onUpdateHighlight
};
}
export default useHighlight;

@ -1,124 +0,0 @@
import { useState, useEffect, useCallback } from 'react';
/**
* Epub Reader hook
* - control의 display를 Menu / .
* - control의 show를 Animaton .
* - Menu Close Event
*
* @param ref DOM을 useRef
* @param delay (ms)
*
* @example
* const Viewer = () => {
* const menuDelay = 300;
*
* const ref = useRef<HTMLDivElement | null>(null);
* const [control, onToggle] = usemenu(ref, menuDelay);
*
* return (<>
* <MenuOpenBtn onClick={onToggle} />
* <Menu control={control} onToggle={onToggle} ref={ref} />
* </>);
* }
*
* const Menu = ({ control, onToggle }, ref) => {
* return (<>
* {control.display && <MenuWrapper show={control.show}
* delay={menuDelay}
* ref={ref}>
* <CloseBtn onClick={onToggle} />
* </MenuWrapper>}
* </>);
* }
*
* const MenuWrapper = styled.div<{show: boolean; delay: boolean;}>`
* transform: ${({show}) => show
* ? "translateX(0px)"
* : "translateX(420px)"
* };
* transition: ${({delay}) => `${delay}ms ease`};
* `;
*/
function useMenu(ref: { current: any }, delay: number) {
const [eventSignal, setEventSignal] = useState<boolean>(true);
const [control, setControl] = useState<MenuControl>({
display: false,
open: false
});
/** Menu 토글 */
const onToggle = useCallback(() => {
let event: any = null;
window.clearTimeout(event);
if (!control.display) {
setControl({ display: true, open: false });
event = window.setTimeout(() => setControl({ display: true, open: true}), 0);
} else {
setControl({ display: true, open: false });
event = window.setTimeout(() => setControl({ display: false, open: false }), delay - 50);
}
}, [control.display, delay]);
/**
*
* - Event path에서 , .
*/
const onClose = useCallback((e: any) => {
if (!ref || !ref.current) return;
if (!e.path.includes(ref.current)) {
onToggle();
}
}, [ref, onToggle]);
/**
*
* - '보기설정' rendition이 ,
* - Iframe [About 250ms]
*/
const emitEvent = useCallback(() => {
window.setTimeout(() => setEventSignal(true), 300);
}, [setEventSignal]);
/**
*
* - Epub이 iframe으로 , iframe에도 Event를 .
*/
useEffect(() => {
if (!eventSignal && !control.display) return;
const epubIframe = document.querySelector('iframe');
if (control.display) {
document.addEventListener('click', onClose);
if (epubIframe && epubIframe.contentWindow) {
epubIframe.contentWindow.document.addEventListener('click', onClose);
}
} else {
document.removeEventListener('click', onClose);
if (epubIframe && epubIframe.contentWindow) {
epubIframe.contentWindow.document.removeEventListener('click', onClose);
}
}
setEventSignal(false);
return () => {
document.removeEventListener('click', onClose);
if (epubIframe && epubIframe.contentWindow) {
epubIframe.contentWindow.document.removeEventListener('click', onClose);
}
}
}, [control.display, onClose, eventSignal]);
return [control, onToggle, emitEvent] as const;
}
export type MenuControl = {
display: boolean;
open: boolean;
}
export default useMenu

@ -1,62 +0,0 @@
import { keyframes } from 'styled-components';
const fadeIn = keyframes`
0% { opacity: 0; }
100% { opacity: 1;}
`;
const fadeOut = keyframes`
0% { opacity: 1; }
100% { opacity: 0; }
`;
const fadeInTop = keyframes`
0% { opacity: 0; transform: translateY(-40px); }
100% { opacity: 1; transform: translateY(0); }
`;
const fadeOutTop = keyframes`
0% { opacity: 1; transform: translateY(0); }
100% { opacity: 0; transform: translateY(-40px); }
`;
const fadeInBottom = keyframes`
0% { opacity: 0; transform: translateY(40px); }
100% { opacity: 1; transform: translateY(0); }
`;
const fadeOutBottom = keyframes`
0% { opacity: 1; transform: translateY(0); }
100% { opacity: 0; transform: translateY(40px); }
`;
const fadeInLeft = keyframes`
0% { opacity: 0; transform: translateX(-40px); }
100% { opacity: 1; transform: translateX(0); }
`;
const fadeOutLeft = keyframes`
0% { opacity: 1; transform: translateX(0); }
100% { opacity: 0; transform: translateX(-40px); }
`;
const fadeInRight = keyframes`
0% { opacity: 0; transform: translateX(40px); }
100% { opacity: 1; transform: translateX(0); }
`;
const fadeOutRight = keyframes`
0% { opacity: 1; transform: translateX(0); }
100% { opacity: 0; transform: translateX(40px); }
`;
const spin = keyframes`
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
`;
const animations = {
fadeIn,
fadeOut,
fadeInTop,
fadeOutTop,
fadeInBottom,
fadeOutBottom,
fadeInLeft,
fadeOutLeft,
fadeInRight,
fadeOutRight,
spin
};
export default animations

@ -1,31 +0,0 @@
export const mediaQuery = (maxWidth: number): string => `
@media (max-width: ${maxWidth}px)
`;
export const mediaValue = {
xlarge: 1600,
large: 1440,
medium: 1200,
small: 702,
xsmall: 350
};
type Media = {
xlarge : string,
large : string,
medium : string,
small : string,
xsmall : string,
custom : (maxWidth: number) => string
}
const media: Media = {
xlarge : mediaQuery(mediaValue.xlarge),
large : mediaQuery(mediaValue.large),
medium : mediaQuery(mediaValue.medium),
small : mediaQuery(mediaValue.small),
xsmall : mediaQuery(mediaValue.xsmall),
custom : mediaQuery,
};
export default media;

@ -1,79 +0,0 @@
/*
:::: Color Varient ::::
*/
const palette = {
white: '#ffffff',
black: '#000000',
/* gray */
gray0: '#fafafa',
gray1: '#e7e7e7',
gray2: '#c3c3c3',
gray3: '#9e9e9e',
gray4: '#868686',
gray5: '#656565',
gray6: '#505050',
gray7: '#3c3c3c',
gray8: '#282828',
/* red */
red0: '#ffe8e8',
red1: '#ffd0d1',
red2: '#ff8a8b',
red3: '#ff4445',
red4: '#ff1517',
red5: '#bf1011',
red6: '#990d0e',
red7: '#73090a',
red8: '#4d0607',
/* orange */
orange0: '#fff0ec',
orange1: '#ffe2d9',
orange2: '#ffb6a0',
orange3: '#ff8966',
orange4: '#ff6c40',
orange5: '#bf5130',
orange6: '#994126',
orange7: '#73311d',
orange8: '#4d2013',
/* yellow */
yellow0: '#fffce6',
yellow1: '#fffacc',
yellow2: '#fff280',
yellow3: '#feea33',
yellow4: '#fee500',
yellow5: '#bfac00',
yellow6: '#988900',
yellow7: '#726700',
yellow8: '#4c4500',
/* green */
green0: '#e6f7e6',
green1: '#cdefcc',
green2: '#83d880',
green3: '#39c133',
green4: '#07b100',
green5: '#058500',
green6: '#046a00',
green7: '#035000',
green8: '#023500',
/* blue */
blue0: '#ebf1ff',
blue1: '#d7e3ff ',
blue2: '#9cb9ff ',
blue3: '#618eff ',
blue4: '#3972ff ',
blue5: '#2b56bf ',
blue6: '#224499 ',
blue7: '#1a3373 ',
blue8: '#11224d ',
/* purple */
purple0: '#f2ebff',
purple1: '#d7e3ff',
purple2: '#be9cff',
purple3: '#9761ff',
purple4: '#7d39ff',
purple5: '#5e2bbf',
purple6: '#4b2299',
purple7: '#381a73',
purple8: '#26114d',
};
export default palette

@ -1,24 +0,0 @@
a {
text-decoration: none;
}
input {
border: none;
background-color: rgba(0,0,0,0);
}
::selection {
background-color: #d4e9ff;
}
.epub-highlight {
fill-opacity: 0.3;
mix-blend-mode: multiply;
cursor: pointer;
pointer-events: fill;
transition: .1s ease-in-out;
}
.epub-highlight:hover {
fill-opacity: 0.4;
}

@ -1,35 +0,0 @@
export const noselect = (): string => `
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
`;
export const transition = (): string =>`
cubic-bezier(0.25, 0.1, 0.25, 1);
`;
export const boxShadow = {
light: "0px 4px 16px rgba(0, 0, 0, 0.04)",
regular: "0px 8px 12px rgba(0, 0, 0, 0.2)",
bold: "0 8px 10px 1px rgba(0,0,0,0.14), 0 3px 8px 2px rgba(0,0,0,0.12), 0 5px 5px -3px rgba(0,0,0,0.2)"
};
export const scrollbar = (width=8) => `
&::-webkit-scrollbar {
width: ${width}px;
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: #ddd;
border-radius: 10px;
}
&::-webkit-scrollbar-thumb:hover {
background: #ccc;
}
&::-webkit-scrollbar-track {
background: transparent;
}
`

@ -1,14 +0,0 @@
import ViewerLayout from 'types/viewerLayout'
/** Epub viewer layout size */
const viewerLayout: ViewerLayout = {
MIN_VIEWER_WIDTH: 600,
MIN_VIEWER_HEIGHT: 300,
VIEWER_HEADER_HEIGHT: 54,
VIEWER_FOOTER_HEIGHT: 60,
VIEWER_SIDEMENU_WIDTH: 0
}
export const contextmenuWidth = 160;
export default viewerLayout

@ -1,13 +0,0 @@
export const zIndexSet = (layer: number): string => `
${layer}
`;
const zIndex = {
header: zIndexSet(5),
menu: zIndexSet(10),
modal: zIndexSet(100),
snackbar: zIndexSet(400),
tooltip: zIndexSet(500)
};
export default zIndex;

@ -1,36 +0,0 @@
import * as React from "react";
function SvgBookClose(props) {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" {...props}>
<defs>
<style>
{
".bookClose_svg__cls-3{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2px}"
}
</style>
</defs>
<g
id="bookClose_svg__\uB808\uC774\uC5B4_2"
data-name="\uB808\uC774\uC5B4 2"
>
<g
id="bookClose_svg__\uB808\uC774\uC5B4_1-2"
data-name="\uB808\uC774\uC5B4 1"
>
<path fill="none" d="M0 0h48v48H0z" opacity={0.2} />
<path
className="bookClose_svg__cls-3"
d="M37.12 3H10.88a3 3 0 00-.43 0A1.71 1.71 0 009.1 4.11a1.87 1.87 0 00-.1.59v29.18c0 1 0 .94 1.88 1.88l18 9a1.8 1.8 0 002.6-1.2 3 3 0 000-.31v-7.58h5.62A1.88 1.88 0 0039 33.8V4.87A1.88 1.88 0 0037.12 3z"
/>
<path
className="bookClose_svg__cls-3"
d="M31.5 35.67V14.84a2 2 0 00-1.1-1.78L10.45 3"
/>
</g>
</g>
</svg>
);
}
export default SvgBookClose;

@ -1,32 +0,0 @@
import * as React from "react";
function SvgBookOpen(props) {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" {...props}>
<defs>
<style>
{
".bookOpen_svg__cls-3{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2px}"
}
</style>
</defs>
<g
id="bookOpen_svg__\uB808\uC774\uC5B4_2"
data-name="\uB808\uC774\uC5B4 2"
>
<g
id="bookOpen_svg__\uB808\uC774\uC5B4_1-2"
data-name="\uB808\uC774\uC5B4 1"
>
<path fill="none" d="M0 0h48v48H0z" opacity={0.2} />
<path
className="bookOpen_svg__cls-3"
d="M4 6.5a27.24 27.24 0 0120 1v34a27.24 27.24 0 00-20-1zM24 7.5a27.24 27.24 0 0120-1v34a27.24 27.24 0 00-20 1"
/>
</g>
</g>
</svg>
);
}
export default SvgBookOpen;

@ -1,23 +0,0 @@
import * as React from "react";
function SvgBookmark(props) {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" {...props}>
<g data-name="\uB808\uC774\uC5B4 2">
<g data-name="\uB808\uC774\uC5B4 1">
<path fill="none" d="M0 0h48v48H0z" opacity={0.2} />
<path
d="M10 5.07V44a1 1 0 001.71.73l10.88-11.29a1.94 1.94 0 012.82 0l10.88 11.25A1 1 0 0038 44V5.07A2 2 0 0036 3H12a2 2 0 00-2 2.07z"
stroke="#000"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
fill="none"
/>
</g>
</g>
</svg>
);
}
export default SvgBookmark;

@ -1,22 +0,0 @@
import * as React from "react";
function SvgBookmarker(props) {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" {...props}>
<g data-name="\uB808\uC774\uC5B4 2">
<g data-name="\uB808\uC774\uC5B4 1">
<path fill="none" d="M0 0h48v48H0z" opacity={0.2} />
<path
d="M42.93 12H4c-.93 0-1.39.92-.73 1.46l11.25 9.33a1.51 1.51 0 010 2.42L3.31 34.54c-.66.54-.2 1.46.69 1.46h38.93A1.92 1.92 0 0045 34.29V13.71A1.92 1.92 0 0042.93 12z"
stroke="#000"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
/>
</g>
</g>
</svg>
);
}
export default SvgBookmarker;

@ -1,26 +0,0 @@
import * as React from "react";
function SvgClose(props) {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" {...props}>
<defs>
<style>
{
".close_svg__cls-3{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2px}"
}
</style>
</defs>
<g id="close_svg__\uB808\uC774\uC5B4_2" data-name="\uB808\uC774\uC5B4 2">
<g
id="close_svg__\uB808\uC774\uC5B4_1-2"
data-name="\uB808\uC774\uC5B4 1"
>
<path fill="none" d="M0 0h48v48H0z" opacity={0.2} />
<path className="close_svg__cls-3" d="M4 4l40 40M4 44L44 4" />
</g>
</g>
</svg>
);
}
export default SvgClose;

@ -1,35 +0,0 @@
import * as React from "react";
function SvgController(props) {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" {...props}>
<defs>
<style>
{
".controller_svg__cls-3{fill:none;stroke:#000;stroke-miterlimit:10;stroke-width:2px}"
}
</style>
</defs>
<g
id="controller_svg__\uB808\uC774\uC5B4_2"
data-name="\uB808\uC774\uC5B4 2"
>
<g
id="controller_svg__\uB808\uC774\uC5B4_1-2"
data-name="\uB808\uC774\uC5B4 1"
>
<path fill="none" d="M0 0h48v48H0z" opacity={0.2} />
<circle className="controller_svg__cls-3" cx={39} cy={40} r={4} />
<circle className="controller_svg__cls-3" cx={24} cy={24} r={4} />
<path d="M20 24a4 4 0 01.55-2H4a2 2 0 000 4h16.55a4 4 0 01-.55-2zM44 22H27.45a3.91 3.91 0 010 4H44a2 2 0 000-4z" />
<circle className="controller_svg__cls-3" cx={9} cy={8} r={4} />
<path d="M44 6H12.45a3.91 3.91 0 010 4H44a2 2 0 000-4zM5 8a4 4 0 01.55-2H4a2 2 0 000 4h1.55A4 4 0 015 8z" />
<circle className="controller_svg__cls-3" cx={39} cy={40} r={4} />
<path d="M35 40a4 4 0 01.55-2H4a2 2 0 000 4h31.55a4 4 0 01-.55-2zM44 38h-1.55a3.91 3.91 0 010 4H44a2 2 0 000-4z" />
</g>
</g>
</svg>
);
}
export default SvgController;

@ -1,22 +0,0 @@
import * as React from "react";
function SvgDown(props) {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" {...props}>
<g data-name="\uB808\uC774\uC5B4 2">
<g data-name="\uB808\uC774\uC5B4 1">
<path fill="none" d="M0 0h48v48H0z" opacity={0.2} />
<path
d="M8.33 15.82l14.32 20.46a1.63 1.63 0 002.7 0l14.32-20.46A1.78 1.78 0 0038.32 13H9.68a1.78 1.78 0 00-1.35 2.82z"
stroke="#000"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
/>
</g>
</g>
</svg>
);
}
export default SvgDown;

@ -1,29 +0,0 @@
import * as React from "react";
function SvgLeft(props) {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" {...props}>
<defs>
<style>
{
".left_svg__cls-3{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2px}"
}
</style>
</defs>
<g id="left_svg__\uB808\uC774\uC5B4_2" data-name="\uB808\uC774\uC5B4 2">
<g
id="left_svg__\uB808\uC774\uC5B4_1-2"
data-name="\uB808\uC774\uC5B4 1"
>
<path fill="none" d="M0 0h48v48H0z" opacity={0.2} />
<path
className="left_svg__cls-3"
d="M4 24L18.5 9.5M18.5 38.5L4 24M44 24H4"
/>
</g>
</g>
</svg>
);
}
export default SvgLeft;

@ -1,18 +0,0 @@
import * as React from "react";
function SvgMenu(props) {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" {...props}>
<g data-name="\uB808\uC774\uC5B4 2">
<g data-name="\uB808\uC774\uC5B4 1">
<path fill="none" d="M0 0h48v48H0z" opacity={0.2} />
<rect x={2} y={6} width={44} height={4} rx={2} />
<rect x={2} y={22} width={44} height={4} rx={2} />
<rect x={2} y={38} width={44} height={4} rx={2} />
</g>
</g>
</svg>
);
}
export default SvgMenu;

@ -1,26 +0,0 @@
import * as React from "react";
function SvgNext(props) {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 44 44" {...props}>
<defs>
<style>
{
".next_svg__cls-3{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2px}"
}
</style>
</defs>
<g id="next_svg__\uB808\uC774\uC5B4_2" data-name="\uB808\uC774\uC5B4 2">
<g
id="next_svg__\uB808\uC774\uC5B4_1-2"
data-name="\uB808\uC774\uC5B4 1"
>
<path fill="none" d="M0 0h44v44H0z" opacity={0.3} />
<path className="next_svg__cls-3" d="M32 22L12 2M32 22L12 42" />
</g>
</g>
</svg>
);
}
export default SvgNext;

@ -1,26 +0,0 @@
import * as React from "react";
function SvgPrev(props) {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 44 44" {...props}>
<defs>
<style>
{
".prev_svg__cls-3{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2px}"
}
</style>
</defs>
<g id="prev_svg__\uB808\uC774\uC5B4_2" data-name="\uB808\uC774\uC5B4 2">
<g
id="prev_svg__\uB808\uC774\uC5B4_1-2"
data-name="\uB808\uC774\uC5B4 1"
>
<path fill="none" d="M0 0h44v44H0z" opacity={0.3} />
<path className="prev_svg__cls-3" d="M12 22L32 2M12 22l20 20" />
</g>
</g>
</svg>
);
}
export default SvgPrev;

@ -1,38 +0,0 @@
import * as React from "react";
function SvgScrollHorizontal(props) {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" {...props}>
<defs>
<style>
{
".scrollHorizontal_svg__cls-3{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2px}"
}
</style>
</defs>
<g
id="scrollHorizontal_svg__\uB808\uC774\uC5B4_2"
data-name="\uB808\uC774\uC5B4 2"
>
<g
id="scrollHorizontal_svg__\uB808\uC774\uC5B4_1-2"
data-name="\uB808\uC774\uC5B4 1"
>
<path fill="none" d="M0 0h48v48H0z" opacity={0.2} />
<circle
className="scrollHorizontal_svg__cls-3"
cx={24}
cy={24}
r={5}
/>
<path
className="scrollHorizontal_svg__cls-3"
d="M3 24l12-12M3 24l12 12M45 24L33 12M45 24L33 36"
/>
</g>
</g>
</svg>
);
}
export default SvgScrollHorizontal;

@ -1,33 +0,0 @@
import * as React from "react";
function SvgScrollVertical(props) {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" {...props}>
<defs>
<style>
{
".scrollVertical_svg__cls-3{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2px}"
}
</style>
</defs>
<g
id="scrollVertical_svg__\uB808\uC774\uC5B4_2"
data-name="\uB808\uC774\uC5B4 2"
>
<g
id="scrollVertical_svg__\uB808\uC774\uC5B4_1-2"
data-name="\uB808\uC774\uC5B4 1"
>
<path fill="none" d="M0 0h48v48H0z" opacity={0.2} />
<circle className="scrollVertical_svg__cls-3" cx={24} cy={24} r={5} />
<path
className="scrollVertical_svg__cls-3"
d="M24 3l12 12M24 3L12 15M24 45l12-12M24 45L12 33"
/>
</g>
</g>
</svg>
);
}
export default SvgScrollVertical;

@ -1,27 +0,0 @@
import * as React from "react";
function SvgSearch(props) {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" {...props}>
<defs>
<style>
{
".search_svg__cls-3{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2px}"
}
</style>
</defs>
<g id="search_svg__\uB808\uC774\uC5B4_2" data-name="\uB808\uC774\uC5B4 2">
<g
id="search_svg__\uB808\uC774\uC5B4_1-2"
data-name="\uB808\uC774\uC5B4 1"
>
<path fill="none" d="M0 0h48v48H0z" opacity={0.2} />
<circle className="search_svg__cls-3" cx={17} cy={17} r={14} />
<path className="search_svg__cls-3" d="M44 44L27 27" />
</g>
</g>
</svg>
);
}
export default SvgSearch;

@ -1,10 +0,0 @@
import styled from 'styled-components'
const TempIcon = styled.div`
width: 32px;
height: 32px;
background-color: #ccc;
border-radius: 32px;
`;
export default TempIcon

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><defs><style>.cls-1{opacity:0.2;}.cls-2,.cls-3{fill:none;}.cls-3{stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2px;}</style></defs><g id="레이어_2" data-name="레이어 2"><g id="레이어_1-2" data-name="레이어 1"><g class="cls-1"><rect class="cls-2" width="48" height="48"/></g><path class="cls-3" d="M37.12,3H10.88a3,3,0,0,0-.43,0h0A1.71,1.71,0,0,0,9.1,4.11,1.87,1.87,0,0,0,9,4.7V33.88c0,1,0,.94,1.88,1.88l18,9a1.8,1.8,0,0,0,2.6-1.2,3,3,0,0,0,0-.31V35.67h5.62A1.88,1.88,0,0,0,39,33.8V4.87A1.88,1.88,0,0,0,37.12,3Z"/><path class="cls-3" d="M31.5,35.67V14.84a2,2,0,0,0-1.1-1.78L10.45,3"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 691 B

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><defs><style>.cls-1{opacity:0.2;}.cls-2,.cls-3{fill:none;}.cls-3{stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2px;}</style></defs><g id="레이어_2" data-name="레이어 2"><g id="레이어_1-2" data-name="레이어 1"><g class="cls-1"><rect class="cls-2" width="48" height="48"/></g><path class="cls-3" d="M4,6.5a27.24,27.24,0,0,1,20,1v34a27.24,27.24,0,0,0-20-1Z"/><path class="cls-3" d="M24,7.5a27.24,27.24,0,0,1,20-1v34a27.24,27.24,0,0,0-20,1"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 546 B

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><defs><style>.cls-1{opacity:0.2;}.cls-2,.cls-3{fill:none;}.cls-3{stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2px;}</style></defs><g id="레이어_2" data-name="레이어 2"><g id="레이어_1-2" data-name="레이어 1"><g class="cls-1"><rect class="cls-2" width="48" height="48"/></g><path class="cls-3" d="M10,5.07V44a1,1,0,0,0,1.71.73L22.59,33.44a1.94,1.94,0,0,1,2.82,0L36.29,44.69A1,1,0,0,0,38,44V5.07A2,2,0,0,0,36,3H12A2,2,0,0,0,10,5.07Z"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 542 B

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><defs><style>.cls-1{opacity:0.2;}.cls-2{fill:none;}.cls-3{stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2px;}</style></defs><g id="레이어_2" data-name="레이어 2"><g id="레이어_1-2" data-name="레이어 1"><g class="cls-1"><rect class="cls-2" width="48" height="48"/></g><path class="cls-3" d="M42.93,12H4c-.93,0-1.39.92-.73,1.46l11.25,9.33a1.51,1.51,0,0,1,0,2.42L3.31,34.54C2.65,35.08,3.11,36,4,36H42.93A1.92,1.92,0,0,0,45,34.29V13.71A1.92,1.92,0,0,0,42.93,12Z"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 568 B

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><defs><style>.cls-1{opacity:0.2;}.cls-2,.cls-3{fill:none;}.cls-3{stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2px;}</style></defs><g id="레이어_2" data-name="레이어 2"><g id="레이어_1-2" data-name="레이어 1"><g class="cls-1"><rect class="cls-2" width="48" height="48"/></g><line class="cls-3" x1="4" y1="4" x2="44" y2="44"/><line class="cls-3" x1="4" y1="44" x2="44" y2="4"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 484 B

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><defs><style>.cls-1{opacity:0.2;}.cls-2,.cls-3{fill:none;}.cls-3{stroke:#000;stroke-miterlimit:10;stroke-width:2px;}</style></defs><g id="레이어_2" data-name="레이어 2"><g id="레이어_1-2" data-name="레이어 1"><g class="cls-1"><rect class="cls-2" width="48" height="48"/></g><circle class="cls-3" cx="39" cy="40" r="4"/><circle class="cls-3" cx="24" cy="24" r="4"/><path d="M20,24a4,4,0,0,1,.55-2H4a2,2,0,0,0,0,4H20.55A4,4,0,0,1,20,24Z"/><path d="M44,22H27.45a3.91,3.91,0,0,1,0,4H44a2,2,0,0,0,0-4Z"/><circle class="cls-3" cx="9" cy="8" r="4"/><path d="M44,6H12.45a3.91,3.91,0,0,1,0,4H44a2,2,0,0,0,0-4Z"/><path d="M5,8a4,4,0,0,1,.55-2H4a2,2,0,0,0,0,4H5.55A4,4,0,0,1,5,8Z"/><circle class="cls-3" cx="39" cy="40" r="4"/><path d="M35,40a4,4,0,0,1,.55-2H4a2,2,0,0,0,0,4H35.55A4,4,0,0,1,35,40Z"/><path d="M44,38H42.45a3.91,3.91,0,0,1,0,4H44a2,2,0,0,0,0-4Z"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 937 B

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><defs><style>.cls-1{opacity:0.2;}.cls-2{fill:none;}.cls-3{stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2px;}</style></defs><g id="레이어_2" data-name="레이어 2"><g id="레이어_1-2" data-name="레이어 1"><g class="cls-1"><rect class="cls-2" width="48" height="48"/></g><path class="cls-3" d="M8.33,15.82,22.65,36.28a1.63,1.63,0,0,0,2.7,0L39.67,15.82A1.78,1.78,0,0,0,38.32,13H9.68A1.78,1.78,0,0,0,8.33,15.82Z"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 516 B

@ -1,31 +0,0 @@
import MenuIcon from './Menu'
import ControllerIcon from './Controller';
import PrevIcon from './Prev';
import NextIcon from './Next';
import BookCloseIcon from './BookClose';
import BookOpenIcon from './BookOpen';
import BookmarkIcon from './Bookmark';
import BookmarkerIcon from './Bookmarker';
import CloseIcon from './Close';
import DownIcon from './Down';
import LeftIcon from './Left';
import ScrollHorizontalIcon from './ScrollHorizontal';
import ScrollVerticalIcon from './ScrollVertical';
import SearchIcon from './Search';
export {
MenuIcon,
ControllerIcon,
PrevIcon,
NextIcon,
BookCloseIcon,
BookOpenIcon,
BookmarkIcon,
BookmarkerIcon,
CloseIcon,
DownIcon,
LeftIcon,
ScrollHorizontalIcon,
ScrollVerticalIcon,
SearchIcon
};

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><defs><style>.cls-1{opacity:0.2;}.cls-2,.cls-3{fill:none;}.cls-3{stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2px;}</style></defs><g id="레이어_2" data-name="레이어 2"><g id="레이어_1-2" data-name="레이어 1"><g class="cls-1"><rect class="cls-2" width="48" height="48"/></g><line class="cls-3" x1="4" y1="24" x2="18.5" y2="9.5"/><line class="cls-3" x1="18.5" y1="38.5" x2="4" y2="24"/><line class="cls-3" x1="44" y1="24" x2="4" y2="24"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 545 B

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><defs><style>.cls-1{opacity:0.2;}.cls-2{fill:none;}</style></defs><g id="레이어_2" data-name="레이어 2"><g id="레이어_1-2" data-name="레이어 1"><g class="cls-1"><rect class="cls-2" width="48" height="48"/></g><rect x="2" y="6" width="44" height="4" rx="2"/><rect x="2" y="22" width="44" height="4" rx="2"/><rect x="2" y="38" width="44" height="4" rx="2"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 441 B

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 44 44"><defs><style>.cls-1{opacity:0.3;}.cls-2,.cls-3{fill:none;}.cls-3{stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2px;}</style></defs><g id="레이어_2" data-name="레이어 2"><g id="레이어_1-2" data-name="레이어 1"><g class="cls-1"><rect class="cls-2" width="44" height="44"/></g><line class="cls-3" x1="32" y1="22" x2="12" y2="2"/><line class="cls-3" x1="32" y1="22" x2="12" y2="42"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 487 B

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 44 44"><defs><style>.cls-1{opacity:0.3;}.cls-2,.cls-3{fill:none;}.cls-3{stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2px;}</style></defs><g id="레이어_2" data-name="레이어 2"><g id="레이어_1-2" data-name="레이어 1"><g class="cls-1"><rect class="cls-2" width="44" height="44"/></g><line class="cls-3" x1="12" y1="22" x2="32" y2="2"/><line class="cls-3" x1="12" y1="22" x2="32" y2="42"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 487 B

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><defs><style>.cls-1{opacity:0.2;}.cls-2,.cls-3{fill:none;}.cls-3{stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2px;}</style></defs><g id="레이어_2" data-name="레이어 2"><g id="레이어_1-2" data-name="레이어 1"><g class="cls-1"><rect class="cls-2" width="48" height="48"/></g><circle class="cls-3" cx="24" cy="24" r="5"/><line class="cls-3" x1="3" y1="24" x2="15" y2="12"/><line class="cls-3" x1="3" y1="24" x2="15" y2="36"/><line class="cls-3" x1="45" y1="24" x2="33" y2="12"/><line class="cls-3" x1="45" y1="24" x2="33" y2="36"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 637 B

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><defs><style>.cls-1{opacity:0.2;}.cls-2,.cls-3{fill:none;}.cls-3{stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2px;}</style></defs><g id="레이어_2" data-name="레이어 2"><g id="레이어_1-2" data-name="레이어 1"><g class="cls-1"><rect class="cls-2" width="48" height="48"/></g><circle class="cls-3" cx="24" cy="24" r="5"/><line class="cls-3" x1="24" y1="3" x2="36" y2="15"/><line class="cls-3" x1="24" y1="3" x2="12" y2="15"/><line class="cls-3" x1="24" y1="45" x2="36" y2="33"/><line class="cls-3" x1="24" y1="45" x2="12" y2="33"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 637 B

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><defs><style>.cls-1{opacity:0.2;}.cls-2,.cls-3{fill:none;}.cls-3{stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2px;}</style></defs><g id="레이어_2" data-name="레이어 2"><g id="레이어_1-2" data-name="레이어 1"><g class="cls-1"><rect class="cls-2" width="48" height="48"/></g><circle class="cls-3" cx="17" cy="17" r="14"/><line class="cls-3" x1="44" y1="44" x2="27" y2="27"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 481 B

@ -1,123 +0,0 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
// types
import Book from 'types/book'
import Page from 'types/page'
import Toc from 'types/toc'
import Highlight, { Color } from 'types/highlight'
// lib
import palette from 'lib/styles/palette'
/*
Initial State
*/
/** 초기 Book 상태 */
const initialBook: Book = {
coverURL: '',
title: '',
description: '',
published_date: '',
modified_date: '',
author: '',
publisher: '',
language: ''
}
/** 초기 CurrentPage 상태 */
const initialCurrentLocation: Page = {
chapterName: "-",
currentPage: 0,
totalPage: 0,
startCfi: '',
endCfi: '',
base: ''
}
/** 초기 색상표 상태 */
const initialColorList: Color[] = [
{ name: 'Red', code: palette.red4 },
{ name: 'Orange', code: palette.orange4 },
{ name: 'Yellow', code: palette.yellow4 },
{ name: 'Green', code: palette.green4 },
{ name: 'Blue', code: palette.blue4 },
{ name: 'Purple', code: palette.purple4 }
]
const initialState: BookState = {
book: initialBook,
currentLocation: initialCurrentLocation,
toc: [],
highlights: [],
colorList: initialColorList,
}
/*
Slice
*/
const bookSlice = createSlice({
name: 'book',
initialState,
reducers: {
/** Book 갱신 @dispatch */
updateBook(state, action: PayloadAction<Book>) {
state.book = action.payload;
},
/** CurrentPage 갱신 @dispatch */
updateCurrentPage(state, action: PayloadAction<Page>) {
state.currentLocation = action.payload;
},
/** Book 초기화 @dispatch */
clearBook(state) {
state.book = initialBook;
},
/** 목차 갱신 @dispatch */
updateToc(state, action: PayloadAction<Toc[]>) {
state.toc = action.payload;
},
/** 목차 초기화 @dispatch */
clearToc(state) {
state.toc = [];
},
/** 하이라이트 추가 */
pushHighlight(state, action: PayloadAction<Highlight>) {
const check = state.highlights.filter(h => h.key === action.payload.key);
if (check.length > 0) return;
state.highlights.push(action.payload);
},
/** 하이라이트 갱신 */
updateHighlight(state, action: PayloadAction<Highlight>) {
const new_idx = action.payload.key;
const old_idx = state.highlights.map(h => h.key).indexOf(new_idx);
state.highlights.splice(old_idx, 1, action.payload);
},
/** 하이라이트 삭제 */
popHighlight(state, action: PayloadAction<string>) {
const idx = state.highlights.map(h => h.key).indexOf(action.payload);
state.highlights.splice(idx, 1);
}
}
});
export interface BookState {
book: Book;
currentLocation: Page;
toc: Toc[];
highlights: Highlight[];
colorList: Color[];
}
export const {
updateBook,
clearBook,
updateCurrentPage,
updateToc,
clearToc,
pushHighlight,
updateHighlight,
popHighlight
} = bookSlice.actions;
export default bookSlice.reducer

@ -1,24 +0,0 @@
import { configureStore } from "@reduxjs/toolkit";
import { combineReducers } from 'redux'
// reducers
import book from 'slices/book'
import snackbar from 'slices/snackbar'
/**
* Root
*/
const reducer = combineReducers({
book,
snackbar
});
export type RootState = ReturnType<typeof reducer>
/**
* Store
*/
const store = configureStore({
reducer,
});
export default store

@ -1,74 +0,0 @@
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'
// types
import Snackbar from 'types/snackbar'
const RefEvent: number[] = [];
/*
Thunks
*/
export const newSnackbar = createAsyncThunk(
'snackbar/newSnackbar',
async (
{ text, type }: SnackbarState,
{ dispatch }
) => {
if(RefEvent.length !== 0) {
let event = RefEvent.shift();
clearTimeout(event);
}
dispatch(SnackbarSlice.actions.deleteSnackbar());
await setTimeout(function() {
dispatch(SnackbarSlice.actions.appendSnackbar({ text, type: type ? type: "INFO" }));
let event = window.setTimeout(function() {
dispatch(SnackbarSlice.actions.deleteSnackbar());
}, 4000);
RefEvent.push(event);
}, 50);
}
);
/*
Initial State
*/
const initialState: SnackbarState = {
text: "",
type: "INFO"
}
/*
Slice
*/
const SnackbarSlice = createSlice({
name: 'snackbar',
initialState,
reducers: {
/** 스낵바 갱신 @dispatch */
appendSnackbar(state, action: PayloadAction<SnackbarState>) {
state.text = action.payload.text;
state.type = action.payload.type;
},
/** 스낵바 초기화 @dispatch */
deleteSnackbar(state) {
state.text = "";
state.type = "INFO"
}
}
});
export interface SnackbarState {
text: string;
type: Snackbar;
}
export const {
appendSnackbar,
deleteSnackbar
} = SnackbarSlice.actions
export default SnackbarSlice.reducer

@ -6167,11 +6167,6 @@ ini@^1.3.5:
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
install@^0.13.0:
version "0.13.0"
resolved "https://registry.yarnpkg.com/install/-/install-0.13.0.tgz#6af6e9da9dd0987de2ab420f78e60d9c17260776"
integrity sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA==
internal-ip@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-4.3.0.tgz#845452baad9d2ca3b69c635a137acb9a0dad0907"

Loading…
Cancel
Save