around the svg
![]()
, not to the
![]()
itself
9 | .svg-button {
10 | cursor: pointer;
11 | display: flex;
12 | justify-content: center;
13 | align-items: center;
14 | padding: 15px 10px;
15 | user-select: none;
16 | }
17 |
18 | .svg-button img {
19 | user-select: none;
20 | }
21 |
22 | .svg-button.disabled {
23 | filter: grayscale(100%) brightness(50%);
24 | pointer-events: none;
25 | }
26 |
27 | @media (hover: hover) and (pointer: fine) {
28 | .svg-button:hover img {
29 | filter: brightness(150%);
30 | }
31 | }
32 |
33 | @media only screen and (max-width: 350px) {
34 | .svg-button {
35 | padding: 15px 7px;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/assets/arrow-left.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/arrow-right.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/burger.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/error.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lijandrew/ame-reader/1303068369df31e4de5e461aa77604acfd4182f6/src/assets/error.jpg
--------------------------------------------------------------------------------
/src/assets/expand.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/src/assets/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lijandrew/ame-reader/1303068369df31e4de5e461aa77604acfd4182f6/src/assets/favicon.ico
--------------------------------------------------------------------------------
/src/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lijandrew/ame-reader/1303068369df31e4de5e461aa77604acfd4182f6/src/assets/icon.png
--------------------------------------------------------------------------------
/src/assets/loading.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lijandrew/ame-reader/1303068369df31e4de5e461aa77604acfd4182f6/src/assets/loading.gif
--------------------------------------------------------------------------------
/src/assets/shrink.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/src/assets/splash.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lijandrew/ame-reader/1303068369df31e4de5e461aa77604acfd4182f6/src/assets/splash.gif
--------------------------------------------------------------------------------
/src/assets/x.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/zoom-in.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/zoom-out.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/Burger/Burger.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import "./Burger.scss";
4 |
5 | export default class Burger extends React.Component {
6 | constructor(props) {
7 | super(props);
8 | }
9 |
10 | render() {
11 | return (
12 |
13 |
})
20 |
21 | );
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/Burger/Burger.scss:
--------------------------------------------------------------------------------
1 | @import "../../variables.scss";
2 |
3 | .Burger {
4 | position: absolute;
5 | left: 10px;
6 | top: 10px;
7 | padding: 20px;
8 | border: none;
9 | background-color: $dark;
10 | border-radius: 0;
11 | cursor: pointer;
12 | user-select: none;
13 | img {
14 | transition: transform 200ms ease-out;
15 | }
16 | }
17 |
18 | @media (hover: hover) and (pointer: fine) {
19 | .Burger:hover {
20 | background-color: $primary;
21 | img {
22 | filter: brightness(0);
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/QuickNav/QuickNav.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import "./QuickNav.scss";
4 |
5 | export default class QuickNav extends React.Component {
6 | constructor(props) {
7 | super(props);
8 | }
9 |
10 | canPrevViewerFile = () => {
11 | return (
12 | this.props.viewerFile &&
13 | this.props.files.indexOf(this.props.viewerFile) !== 0
14 | );
15 | };
16 |
17 | canNextViewerFile = () => {
18 | return (
19 | this.props.viewerFile &&
20 | this.props.files.indexOf(this.props.viewerFile) !==
21 | this.props.files.length - 1
22 | );
23 | };
24 |
25 | render() {
26 | return (
27 |
28 |
32 |
})
38 |
39 |
40 |
{this.props.filename.split(".")[0]}
41 |
42 |
46 |
})
52 |
53 |
54 | );
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/components/QuickNav/QuickNav.scss:
--------------------------------------------------------------------------------
1 | @import "../../variables.scss";
2 |
3 | .QuickNav {
4 | display: flex;
5 | flex-direction: row;
6 | width: 100%;
7 | justify-content: center;
8 | align-items: center;
9 | font-family: $font-family;
10 | color: $primary;
11 | background-color: $darker;
12 |
13 | // Ensures Burger menu doesn't cover QuickNav's previous file button
14 | // when the open file has a very long name
15 | margin: 80px 0;
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/Sidebar/Sidebar.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import SidebarFilelist from "../SidebarFilelist/SidebarFilelist";
3 | import SidebarUploader from "../SidebarUploader/SidebarUploader";
4 | import Toolbar from "../Toolbar/Toolbar.jsx";
5 | import Burger from "../Burger/Burger.jsx";
6 |
7 | import "./Sidebar.scss";
8 |
9 | export default class Sidebar extends React.Component {
10 | constructor(props) {
11 | super(props);
12 | this.state = {
13 | isVisible: true,
14 | };
15 | this.sidebarFilelistRef = React.createRef();
16 | this.toolbarRef = React.createRef();
17 | this.sidebarUploaderRef = React.createRef();
18 | }
19 |
20 | /**
21 | * Opens Sidebar and scrolls Filelist down to bottom to show newly-uploaded files.
22 | */
23 | revealUploadedFiles = () => {
24 | this.setState(
25 | {
26 | isVisible: true,
27 | },
28 | () => {
29 | this.sidebarFilelistRef.current.scrollToBottom();
30 | }
31 | );
32 | };
33 |
34 | toggleSidebar = () => {
35 | this.setState((state) => ({
36 | isVisible: !state.isVisible,
37 | }));
38 | };
39 |
40 | render() {
41 | return (
42 |
43 |
44 |
45 |
46 |
49 |
54 |
71 |
78 |
79 |
80 | );
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/components/Sidebar/Sidebar.scss:
--------------------------------------------------------------------------------
1 | @import "../../variables.scss";
2 |
3 | // TODO: limit overflow-y scrollbar to Filelist only (Upload button should not scroll)
4 |
5 | .Sidebar {
6 | z-index: 2;
7 | position: relative;
8 | display: flex;
9 | flex-direction: row-reverse;
10 | height: 100%;
11 | max-height: 100%;
12 |
13 | // -84px to ensure there is always room for the Burger
14 | // Required b/c Burger is position: absolute
15 | max-width: calc(100% - 85px);
16 | }
17 |
18 | .Sidebar-Burger-wrapper {
19 | position: relative;
20 | }
21 |
22 | .Sidebar-content {
23 | background-color: $dark;
24 |
25 | // Make bottom vertical child scrollable
26 | display: flex;
27 | flex-direction: column;
28 |
29 | overflow-x: hidden;
30 | }
31 |
32 | .Sidebar-content.hidden {
33 | width: 0;
34 | }
35 |
36 | @media only screen and (max-width: 700px) {
37 | .Sidebar {
38 | position: fixed;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/SidebarFile/SidebarFile.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import "./SidebarFile.scss";
4 |
5 | export default class SidebarFile extends React.Component {
6 | constructor(props) {
7 | super(props);
8 | this.state = {
9 | active: false,
10 | };
11 | this.elemRef = React.createRef();
12 | }
13 |
14 | componentDidUpdate(prevProps) {
15 | if (this.props.viewerFile !== prevProps.viewerFile) {
16 | if (this.props.viewerFile === this.props.file) {
17 | this.setState({
18 | active: true,
19 | });
20 | } else {
21 | this.setState({
22 | active: false,
23 | });
24 | }
25 | }
26 | }
27 |
28 | handleClick = (e) => {
29 | this.props.setViewerFile(this.props.file);
30 | };
31 |
32 | handleAuxClick = (e) => {
33 | if (e.button === 1) {
34 | this.handleDelete(e);
35 | }
36 | };
37 |
38 | handleDelete = (e) => {
39 | e.stopPropagation();
40 | this.props.setViewerFile(); // Reset Viewer by calling without giving a File
41 | this.props.deleteFile(this.props.file);
42 | };
43 |
44 | render() {
45 | return (
46 |
51 | {this.props.file.name}
52 |
53 |
})
54 |
55 |
56 | );
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/components/SidebarFile/SidebarFile.scss:
--------------------------------------------------------------------------------
1 | @import "../../variables.scss";
2 |
3 | .SidebarFile {
4 | padding: 10px;
5 | background-color: $dark;
6 | color: $primary;
7 | font-family: $font-family;
8 | display: flex;
9 | flex-direction: row;
10 | justify-content: space-between;
11 | user-select: none;
12 | cursor: pointer;
13 | transition: background-color 200ms ease-out;
14 | &:not(:nth-child(1)) {
15 | border-top: 1px solid $primary;
16 | }
17 | white-space: pre-line;
18 | // white-space: pre-wrap;
19 | }
20 |
21 | .delete {
22 | display: flex;
23 | cursor: pointer;
24 | margin-left: 20px;
25 | img {
26 | transition: transform 200ms ease-out;
27 | width: 15px;
28 | }
29 | }
30 |
31 | .SidebarFile.active {
32 | background-color: $primary;
33 | color: $darker;
34 | .delete img {
35 | filter: grayscale(100%) brightness(0);
36 | }
37 | }
38 |
39 | @media (hover: hover) and (pointer: fine) {
40 | .SidebarFile:hover {
41 | background-color: $primary;
42 | color: $darker;
43 | .delete img {
44 | filter: grayscale(100%) brightness(0);
45 | }
46 | }
47 | .delete:hover img {
48 | transform: rotate(90deg);
49 | }
50 | }
--------------------------------------------------------------------------------
/src/components/SidebarFilelist/SidebarFilelist.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import SidebarFile from "../SidebarFile/SidebarFile.jsx";
4 |
5 | import "./SidebarFilelist.scss";
6 |
7 | export default class SidebarFilelist extends React.Component {
8 | constructor(props) {
9 | super(props);
10 | this.sidebarFilelistRef = React.createRef();
11 | }
12 |
13 | scrollToBottom = () => {
14 | console.log("scrollToBottom called");
15 | this.sidebarFilelistRef.current.scrollTop =
16 | this.sidebarFilelistRef.current.scrollHeight;
17 | };
18 |
19 | getFileElems = () => {
20 | let fileElems = [];
21 | for (let i = 0; i < this.props.files.length; i++) {
22 | fileElems.push(
23 |
30 | );
31 | }
32 | return fileElems;
33 | };
34 |
35 | render() {
36 | return (
37 |
38 | {this.getFileElems()}
39 |
40 | );
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/components/SidebarFilelist/SidebarFilelist.scss:
--------------------------------------------------------------------------------
1 | .SidebarFilelist {
2 | // Make bottom vertical child scrollable
3 | overflow-y: auto;
4 | }
5 |
--------------------------------------------------------------------------------
/src/components/SidebarUploader/SidebarUploader.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import "./SidebarUploader.scss";
4 |
5 | export default class SidebarUploader extends React.Component {
6 | constructor(props) {
7 | super(props);
8 | this.pickerInput = null;
9 | this.pickerButton = null;
10 | this.dropper = null;
11 | this.pickerInputRef = React.createRef();
12 | this.pickerButtonRef = React.createRef();
13 | this.dropperRef = React.createRef();
14 | }
15 |
16 | componentDidMount() {
17 | this.pickerInput = this.pickerInputRef.current;
18 | this.pickerButton = this.pickerButtonRef.current;
19 |
20 | // Set up drag-n-drop file upload
21 | let dropper = this.dropperRef.current;
22 |
23 | let showDropper = () => {
24 | dropper.style.visibility = "visible";
25 | };
26 |
27 | let hideDropper = () => {
28 | dropper.style.visibility = "hidden";
29 | };
30 |
31 | let allowDrag = (e) => {
32 | e.preventDefault();
33 | e.dataTransfer.dropEffect = "copy";
34 | };
35 |
36 | let handleDrop = (e) => {
37 | e.preventDefault();
38 | hideDropper();
39 | this.uploadFiles(e.dataTransfer.files);
40 | };
41 |
42 | // Show dropper when dragged into anywhere in the window
43 | window.addEventListener("dragenter", (e) => {
44 | showDropper();
45 | });
46 |
47 | // Constantly call preventDefault to allow drag to continue
48 | dropper.addEventListener("dragenter", allowDrag);
49 | dropper.addEventListener("dragover", allowDrag);
50 |
51 | // Hide the dropper upon leaving the dropper area
52 | dropper.addEventListener("dragleave", (e) => {
53 | hideDropper();
54 | });
55 |
56 | // Handle the drop
57 | dropper.addEventListener("drop", handleDrop);
58 | }
59 |
60 | handleFileInputChange = (event) => {
61 | if (event.target.files && event.target.files[0]) {
62 | this.uploadFiles(event.target.files);
63 | }
64 | };
65 |
66 | /**
67 | * Sorts files by name, then calls this.props.addFiles on them
68 | * @param {File[]} files Files to add
69 | */
70 | uploadFiles = (files) => {
71 | files = Array.from(files); // In case files passed in was directly from event object
72 | files.sort((a, b) => (a.name > b.name ? 1 : -1)); // Sort by filename
73 | this.props.addFiles(files, this.props.revealUploadedFiles);
74 | };
75 |
76 | handlePickerInputClick = () => {
77 | this.pickerInput.value = null;
78 | };
79 |
80 | handlePickerButtonClick = () => {
81 | this.pickerInput.click();
82 | };
83 |
84 | render() {
85 | return (
86 |
87 |
88 |
96 |
101 | Upload
102 |
103 |
104 |
105 |
106 |
107 | );
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/src/components/SidebarUploader/SidebarUploader.scss:
--------------------------------------------------------------------------------
1 | @import "../../variables.scss";
2 |
3 | .SidebarUploader {
4 | position: relative;
5 | user-select: none;
6 | }
7 |
8 | .picker input {
9 | display: none;
10 | }
11 |
12 | .picker-button {
13 | position: relative;
14 | background-color: $primary;
15 | color: black;
16 | padding: 20px;
17 | text-align: center;
18 | cursor: pointer;
19 | overflow: hidden;
20 | font-family: $font-family;
21 | &::before {
22 | content: "";
23 | position: absolute;
24 | left: 50%;
25 | top: 50%;
26 | background-color: $darker;
27 | height: 350%;
28 | width: 150%;
29 | border-radius: 50%;
30 | transform: translate(-50%, -50%) scale(0);
31 | transition: transform 100ms ease-out;
32 | }
33 | span {
34 | // Needed because only elements with non-static position are affected by z-index,
35 | // and we want span to appear above ::before
36 | position: relative;
37 | }
38 | }
39 |
40 | @media (hover: hover) and (pointer: fine) {
41 | .picker-button:hover {
42 | transition: color 100ms ease-out;
43 | color: white;
44 | &::before {
45 | transform: translate(-50%, -50%) scale(1);
46 | transition: transform 100ms ease-out;
47 | }
48 | }
49 | }
50 |
51 | .dropper {
52 | z-index: 999;
53 | position: fixed;
54 | width: 100%;
55 | height: 100%;
56 | top: 0;
57 | left: 0;
58 | background-color: black;
59 | opacity: 0.8;
60 | visibility: hidden;
61 | }
62 |
--------------------------------------------------------------------------------
/src/components/Toolbar/Toolbar.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import "./Toolbar.scss";
4 |
5 | export default class Toolbar extends React.Component {
6 | constructor(props) {
7 | super(props);
8 | }
9 |
10 | canDecreaseMargin = () => {
11 | return this.props.viewerFile && this.props.margin > this.props.minMargin;
12 | };
13 |
14 | canIncreaseMargin = () => {
15 | return this.props.viewerFile && this.props.margin < this.props.maxMargin;
16 | };
17 |
18 | canDecreaseZoom = () => {
19 | return this.props.viewerFile && this.props.zoom > this.props.minZoom;
20 | };
21 |
22 | canIncreaseZoom = () => {
23 | return this.props.viewerFile && this.props.zoom < this.props.maxZoom;
24 | };
25 |
26 | canPrevViewerFile = () => {
27 | return (
28 | this.props.viewerFile &&
29 | this.props.files.indexOf(this.props.viewerFile) !== 0
30 | );
31 | };
32 |
33 | canNextViewerFile = () => {
34 | return (
35 | this.props.viewerFile &&
36 | this.props.files.indexOf(this.props.viewerFile) !==
37 | this.props.files.length - 1
38 | );
39 | };
40 |
41 | render() {
42 | return (
43 |
44 |
48 |
})
54 |
55 |
56 |
60 |
})
66 |
67 |
68 |
72 |
})
78 |
79 |
80 |
84 |
})
90 |
91 |
92 |
96 |
})
102 |
103 |
104 |
108 |
})
114 |
115 |
116 | );
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/components/Toolbar/Toolbar.scss:
--------------------------------------------------------------------------------
1 | @import "../../variables.scss";
2 |
3 | .Toolbar {
4 | background-color: $dark;
5 | display: flex;
6 | flex-direction: row;
7 | justify-content: center;
8 | align-items: center;
9 | flex-shrink: 0;
10 | }
11 |
--------------------------------------------------------------------------------
/src/components/Viewer/Viewer.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | const jszip = require("jszip");
3 |
4 | import QuickNav from "../QuickNav/QuickNav.jsx";
5 |
6 | import "./Viewer.scss";
7 |
8 | export default class Viewer extends React.Component {
9 | constructor(props) {
10 | super(props);
11 | this.state = {
12 | imageUrls: [], // The image URLs directly used in
![]()
tags.
13 | isLoaded: false, // Show loading animation?
14 | error: false, // Show error?
15 | };
16 | this.viewerRef = React.createRef();
17 | }
18 |
19 | componentDidMount() {
20 | this.processFile(this.props.viewerFile);
21 | }
22 |
23 | componentDidUpdate(prevProps) {
24 | if (this.props.viewerFile !== prevProps.viewerFile) {
25 | this.processFile(this.props.viewerFile);
26 | }
27 | if (
28 | this.props.zoom !== prevProps.zoom ||
29 | this.props.margin !== prevProps.margin
30 | ) {
31 | this.forceUpdate();
32 | }
33 | }
34 |
35 | /**
36 | * Processes the file for viewing and sets the state accordingly
37 | * Unzips the file, creates URLs for all entries, and sets the URLs into state
38 | * If file is falsy, will revoke all URLs and wipe this.state.imageUrls to force
39 | * a rerender of the welcome screen.
40 | * @param {File} file The file to process for viewing.
41 | */
42 | processFile = (file) => {
43 | this.revokeUrls(this.state.imageUrls); // Always free memory first.
44 | if (file) {
45 | this.setState(
46 | {
47 | // Clear images, show loader, and hide error.
48 | imageUrls: [],
49 | isLoaded: false,
50 | error: false,
51 | },
52 | () => {
53 | // Process file (unzip, load image, or show error).
54 | const zipRe = /\.(cbz|cbr|zip|rar|7z|7zip)$/gi;
55 | const imageRe = /\.(jpe?g|png|gif)$/gi;
56 | if (zipRe.test(file.name)) {
57 | // Display using zip file.
58 | this.unzip(this.props.viewerFile).then((blobs) => {
59 | this.setState(
60 | {
61 | imageUrls: this.createUrls(blobs),
62 | isLoaded: true,
63 | },
64 | () => {
65 | this.viewerRef.current.scrollTop = 0;
66 | }
67 | );
68 | });
69 | } else if (imageRe.test(file.name)) {
70 | // Display single image.
71 | this.setState(
72 | {
73 | imageUrls: this.createUrls([file]),
74 | isLoaded: true,
75 | },
76 | () => {
77 | this.viewerRef.current.scrollTop = 0;
78 | }
79 | );
80 | } else {
81 | // Display error.
82 | this.setState({
83 | isLoaded: true,
84 | error: true,
85 | });
86 | }
87 | }
88 | );
89 | } else {
90 | // No file selected. Clear images, hide loader, hide error.
91 | this.setState({
92 | imageUrls: [],
93 | isLoaded: true,
94 | error: false,
95 | });
96 | }
97 | };
98 |
99 | /**
100 | * Returns an array of Promises of Blobs of each zip entry
101 | * @param {File} zipFile The ZIP file to unzip
102 | * @returns {Promise} Promise of array of Blobs
103 | */
104 | unzip = (zipFile) => {
105 | return jszip.loadAsync(zipFile).then(function (zip) {
106 | let re = /\.(jpe?g|png|gif)$/i;
107 | let imageFilenames = Object.keys(zip.files).filter(function (filename) {
108 | // Ignore non-image files
109 | return re.test(filename.toLowerCase());
110 | }).sort((a, b) => a.localeCompare(b));
111 |
112 | let blobPromises = [];
113 | for (let filename of imageFilenames) {
114 | let file = zip.files[filename];
115 | blobPromises.push(file.async("blob"));
116 | }
117 | return Promise.all(blobPromises);
118 | });
119 | };
120 |
121 | /**
122 | * Returns an array of object URLs for passed array of files
123 | * @param {File[]} files Array of files to create object URLs for
124 | */
125 | createUrls = (files) => {
126 | let urls = [];
127 | for (let file of files) {
128 | urls.push(URL.createObjectURL(file));
129 | }
130 | return urls;
131 | };
132 |
133 | /**
134 | * Revokes all URLs in the passed array, freeing memory
135 | * @param {DOMString[]} urls Array of DOMString URLs (of images, in this case)
136 | */
137 | revokeUrls = (urls) => {
138 | for (let url of urls) {
139 | URL.revokeObjectURL(url);
140 | }
141 | };
142 |
143 | /**
144 | * Creates and returns an array of
![]()
files with src attributes linked
145 | * @returns Array of
![]()
146 | */
147 | getImageElems = () => {
148 | let imageElemArr = [];
149 | for (let i = 0; i < this.state.imageUrls.length; i++) {
150 | let imageUrl = this.state.imageUrls[i];
151 | imageElemArr.push(
152 |

160 | );
161 | }
162 | return imageElemArr;
163 | };
164 |
165 | render() {
166 | return (
167 |
168 | {this.props.viewerFile && this.state.isLoaded ? (
169 |
176 | ) : (
177 | ""
178 | )}
179 |
180 | {this.state.isLoaded ? (
181 | ""
182 | ) : (
183 |
184 |
})
185 |
186 | )}
187 |
188 | {this.props.viewerFile ? (
189 | ""
190 | ) : (
191 |
192 |
})
193 |
194 | drag 'n drop your .cbz files
195 |
196 |
197 | or use the Upload button
198 |
199 |
200 | )}
201 |
202 | {this.state.error ? (
203 |
204 |
})
205 |
Could not load this file.
206 |
207 | Are you using an unsupported file extension?
208 |
209 |
210 | ) : (
211 | ""
212 | )}
213 |
214 |
220 | {this.getImageElems()}
221 |
222 |
223 | {this.props.viewerFile && this.state.isLoaded ? (
224 |
231 | ) : (
232 | ""
233 | )}
234 |
235 | );
236 | }
237 | }
238 |
--------------------------------------------------------------------------------
/src/components/Viewer/Viewer.scss:
--------------------------------------------------------------------------------
1 | @import "../../variables.scss";
2 |
3 | .Viewer {
4 | z-index: 1;
5 | position: relative;
6 | background-color: $darker;
7 | overflow-y: auto;
8 | flex-grow: 1;
9 | display: flex;
10 | flex-direction: column;
11 | width: 100%;
12 | align-items: center;
13 | user-select: none;
14 | }
15 |
16 | .Viewer-QuickNav-top {
17 | margin-top: 80px;
18 | }
19 |
20 | .Viewer-image-wrapper {
21 | display: flex;
22 | flex-direction: column;
23 | align-items: center;
24 | max-width: 100%;
25 | img {
26 | max-width: 100%;
27 | width: 100%;
28 | }
29 | }
30 |
31 | .Viewer-loading,
32 | .Viewer-error,
33 | .Viewer-splash {
34 | max-width: 100%;
35 | width: 100%;
36 | height: 100%;
37 | display: flex;
38 | flex-direction: column;
39 | justify-content: center;
40 | align-items: center;
41 | }
42 |
43 | .Viewer-loading img,
44 | .Viewer-error img,
45 | .Viewer-splash img {
46 | width: 500px;
47 | max-width: 100%;
48 | }
49 |
50 | .Viewer-error-text,
51 | .Viewer-error-text-small,
52 | .Viewer-splash-text,
53 | .Viewer-splash-text-small {
54 | font-family: $font-family;
55 | color: white;
56 | }
57 |
58 | .Viewer-error-text,
59 | .Viewer-splash-text {
60 | font-size: 32px;
61 | margin-top: 20px;
62 | }
63 |
64 | .Viewer-error-text-small,
65 | .Viewer-splash-text-small {
66 | font-size: 14px;
67 | }
68 |
69 | @media only screen and (max-width: 700px) {
70 | .Viewer-error-text,
71 | .Viewer-splash-text {
72 | font-size: 20px;
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
Ame
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import App from "./App.jsx";
4 |
5 | import "./index.scss";
6 |
7 | ReactDOM.render(
, document.querySelector("#root"));
8 |
9 | /*
10 | if ("serviceworker" in navigator) {
11 | window.addEventListener("load", () => {
12 | navigator.serviceWorker.register("/sw.js");
13 | });
14 | }
15 | */
16 |
--------------------------------------------------------------------------------
/src/index.scss:
--------------------------------------------------------------------------------
1 | @import "./variables.scss";
2 |
3 | // Reset styles
4 | * {
5 | margin: 0;
6 | padding: 0;
7 | }
8 |
9 | // Fullscreen application
10 | html,
11 | body,
12 | #root,
13 | #root > div {
14 | width: 100%;
15 | height: 100%;
16 | }
17 |
18 | body {
19 | font-size: 16px;
20 | background-color: $darker;
21 | }
22 |
--------------------------------------------------------------------------------
/src/variables.scss:
--------------------------------------------------------------------------------
1 | // @import url("https://fonts.googleapis.com/css2?family=Raleway:wght@400;700&display=swap");
2 | @import url("https://fonts.googleapis.com/css2?family=Raleway:wght@700&display=swap");
3 |
4 | $primary: #74b9ff;
5 | $secondary: #0984e3;
6 |
7 | $dark: #222222;
8 | $darker: #111111;
9 |
10 | $font-family: "Raleway", sans-serif;
11 |
--------------------------------------------------------------------------------
/webpack.common.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 | const HtmlWebpackPlugin = require("html-webpack-plugin");
3 | const { CleanWebpackPlugin } = require("clean-webpack-plugin");
4 | const CopyWebpackPlugin = require("copy-webpack-plugin");
5 |
6 | module.exports = {
7 | entry: path.join(__dirname, "src", "index.js"),
8 | resolve: {
9 | extensions: ["*", ".js", ".jsx"],
10 | },
11 | plugins: [
12 | new CleanWebpackPlugin(),
13 | new CopyWebpackPlugin({
14 | patterns: [{ from: "./src/assets/icon.png", to: "assets/icon.png" }],
15 | }),
16 | new HtmlWebpackPlugin({
17 | favicon: path.join(__dirname, "src", "assets", "favicon.ico"),
18 | template: path.join(__dirname, "src", "index.html"),
19 | }),
20 | ],
21 | };
22 |
--------------------------------------------------------------------------------
/webpack.dev.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 | const webpack = require("webpack");
3 | const { merge } = require("webpack-merge");
4 | const common = require("./webpack.common.js");
5 |
6 | module.exports = merge(common, {
7 | mode: "development",
8 | output: {
9 | path: path.join(__dirname, "docs"),
10 | filename: "index.bundle.js",
11 | },
12 | module: {
13 | rules: [
14 | {
15 | test: /\.jsx?$/,
16 | exclude: /node_modules/,
17 | use: ["babel-loader"],
18 | },
19 | {
20 | test: /\.scss$/,
21 | use: ["style-loader", "css-loader", "sass-loader"],
22 | },
23 | {
24 | test: /\.(png|svg|jpg|jpeg|gif)$/i,
25 | type: "asset/resource",
26 | generator: {
27 | filename: "assets/[name][ext]",
28 | },
29 | },
30 | ],
31 | },
32 | plugins: [new webpack.HotModuleReplacementPlugin()],
33 | devServer: {
34 | contentBase: path.join(__dirname, "docs"),
35 | hot: true,
36 | },
37 | });
38 |
--------------------------------------------------------------------------------
/webpack.prod.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 | const { merge } = require("webpack-merge");
3 | const common = require("./webpack.common.js");
4 |
5 | module.exports = merge(common, {
6 | mode: "production",
7 | output: {
8 | path: path.join(__dirname, "docs"),
9 | filename: "index.[contenthash].bundle.js",
10 | },
11 | module: {
12 | rules: [
13 | {
14 | test: /\.jsx?$/,
15 | exclude: /node_modules/,
16 | use: ["babel-loader"],
17 | },
18 | {
19 | test: /\.scss$/,
20 | use: ["style-loader", "css-loader", "sass-loader"],
21 | },
22 | {
23 | test: /\.(png|svg|jpg|jpeg|gif)$/i,
24 | type: "asset/resource",
25 | generator: {
26 | filename: "assets/[hash][ext]",
27 | },
28 | },
29 | ],
30 | },
31 | });
32 |
--------------------------------------------------------------------------------