Compare commits

...

10 Commits

Author SHA1 Message Date
92d80d403d Refresh the video after getting a new video event
Some checks failed
continuous-integration/drone/push Build is failing
2021-08-24 14:06:29 -10:00
2cba59a798 Update the list details spacing on phones 2021-08-24 10:51:53 -10:00
6e7559206f Update the video metadata style 2021-08-24 10:50:55 -10:00
4cae6cc479 Add a component to show the progress of the download
Some checks failed
continuous-integration/drone/push Build is failing
2021-08-24 09:48:42 -10:00
b7c75b2e7e Cleanup the html head 2021-08-24 09:48:42 -10:00
1d96024a6f Update webpack css config
* Create a separate file for css
* Clean the css from unused styles
2021-08-24 09:48:42 -10:00
c0906f7c21 Remove moment to use timeago.js
Moment is huge, timeago.js is tiny !
2021-08-23 14:57:55 -10:00
26357d0627 Make the app more "PWA"
Add maskable icons and service worker.
2021-08-23 14:57:55 -10:00
de32ee578b Fix embedded subtitles in the web player 2021-08-22 12:32:31 -10:00
37799e3d8e Update to go 1.17
Some checks failed
continuous-integration/drone/push Build is failing
2021-08-22 12:21:12 -10:00
23 changed files with 1860 additions and 117 deletions

View File

@ -16,10 +16,10 @@ steps:
- npm run-script build - npm run-script build
- name: backend - name: backend
image: golang:1.16.7-alpine3.14 image: golang:1.17.0-alpine3.14
commands: commands:
- apk --no-cache add git - apk --no-cache add git
- GO111MODULE=off go get -tags 'postgres' -u github.com/golang-migrate/migrate/cmd/migrate - go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
- cp $$GOPATH/bin/migrate migrate - cp $$GOPATH/bin/migrate migrate
- CGO_ENABLED=0 go build -ldflags '-extldflags "-static"' -trimpath -v -o canapeapp/app backend/*.go - CGO_ENABLED=0 go build -ldflags '-extldflags "-static"' -trimpath -v -o canapeapp/app backend/*.go

6
dev.sh
View File

@ -69,9 +69,9 @@ _ensure_command npm
_check_command migrate || { _check_command migrate || {
_log_info "Installing migrate" _log_info "Installing migrate"
GO111MODULE=off \ go install \
go get -tags 'postgres' \ -tags 'postgres' \
-u -v github.com/golang-migrate/migrate/cmd/migrate github.com/golang-migrate/migrate/v4/cmd/migrate@latest
} }
_check_command fresh || { _check_command fresh || {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 622 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 850 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -1,46 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="16.000000pt" height="16.000000pt" viewBox="0 0 16.000000 16.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.11, written by Peter Selinger 2001-2013
</metadata>
<g transform="translate(0.000000,16.000000) scale(0.002286,-0.002286)"
fill="#000000" stroke="none">
<path d="M3220 6574 c-154 -16 -224 -25 -230 -29 -3 -2 -26 -6 -50 -10 -130
-18 -361 -83 -545 -152 -418 -159 -812 -431 -1075 -743 -30 -36 -60 -69 -66
-75 -18 -17 -111 -150 -156 -222 -42 -69 -138 -248 -138 -257 0 -3 35 -22 78
-43 42 -20 102 -55 132 -77 162 -118 604 -587 765 -810 11 -16 36 -49 56 -75
124 -161 287 -425 391 -634 28 -56 54 -104 58 -107 6 -4 2072 -5 2116 -1 6 1
21 25 35 54 39 84 161 304 227 408 218 344 467 647 809 982 156 153 220 203
330 258 94 46 92 34 26 159 -217 413 -559 756 -1009 1011 -57 32 -106 59 -109
59 -2 0 -33 14 -67 31 -181 89 -548 207 -723 233 -22 4 -65 11 -95 16 -30 5
-77 11 -105 14 -27 3 -77 8 -110 12 -98 11 -432 10 -545 -2z"/>
<path d="M578 4828 c-182 -24 -427 -197 -515 -363 -45 -87 -62 -162 -59 -264
6 -159 63 -291 211 -491 109 -146 231 -350 317 -529 l59 -125 41 47 c93 109
276 207 418 224 30 4 281 7 557 8 l502 0 -27 50 c-137 263 -367 594 -587 845
-113 129 -355 376 -454 463 -121 107 -296 158 -463 135z"/>
<path d="M6270 4832 c-43 -6 -133 -33 -180 -54 -78 -35 -124 -73 -287 -236
-212 -210 -283 -288 -438 -482 -139 -175 -273 -366 -338 -483 -14 -25 -37 -63
-51 -86 -14 -22 -26 -43 -26 -45 0 -3 -13 -28 -30 -56 -16 -27 -26 -50 -22
-51 4 0 243 -2 532 -4 504 -2 528 -3 598 -24 132 -39 263 -118 340 -207 l41
-48 60 125 c108 223 174 334 317 530 153 210 216 369 209 528 -6 144 -49 242
-154 354 -65 68 -196 162 -261 187 -19 7 -39 17 -45 21 -27 19 -202 40 -265
31z"/>
<path d="M1135 3039 c-13 -4 -26 -5 -28 -2 -7 7 -98 -22 -147 -47 -109 -56
-195 -177 -217 -305 -7 -45 -7 -1013 1 -1067 23 -164 130 -288 296 -344 53
-17 155 -18 2460 -18 1970 0 2412 2 2445 13 180 60 288 180 311 349 8 55 9
1035 1 1072 -14 70 -61 166 -104 215 -55 62 -168 121 -253 131 -70 9 -4732 11
-4765 3z"/>
<path d="M1222 962 c-18 -2 -31 -6 -28 -10 2 -4 7 -34 10 -67 10 -108 56 -458
61 -465 6 -9 344 -10 350 -1 2 4 7 32 10 62 3 30 8 65 10 79 2 14 7 48 10 75
5 50 10 91 20 163 3 20 7 59 10 85 3 27 7 53 11 58 3 5 -2 12 -10 15 -16 6
-399 11 -454 6z"/>
<path d="M5367 962 c-32 -2 -56 -6 -53 -10 2 -4 7 -32 10 -62 7 -63 13 -110
21 -160 2 -19 7 -57 10 -85 3 -27 7 -61 10 -75 2 -14 7 -52 10 -85 4 -32 9
-63 11 -67 6 -9 343 -8 349 1 3 6 17 100 31 216 2 22 7 58 10 80 11 80 13 91
19 150 3 33 8 68 11 78 4 14 -1 17 -28 18 -230 3 -363 3 -411 1z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -5,10 +5,7 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="Description" content="Canapé"> <meta name="Description" content="Canapé">
<link rel="icon" type="image/png" href="<%= require("./img/favicon-16x16.png").default %>" sizes="16x16"> <meta name="msapplication-navbutton-color" content="#4e5d6c">
<link rel="icon" type="image/png" href="<%= require("./img/favicon-32x32.png").default %>" sizes="32x32">
<meta name="msapplication-navbutton-color" content="#4E5D6C">
<link rel="mask-icon" href="<%= require("./img/safari-pinned-tab.svg").default %>" color="#5bbad5">
<title>Canapé</title> <title>Canapé</title>
</head> </head>
<body> <body>

View File

@ -1,12 +1,6 @@
// Import default image // Import default image
import "../img/noimage.png"; import "../img/noimage.png";
// Import favicon settings
import "../img/favicon-16x16.png";
import "../img/favicon-32x32.png";
import "../img/favicon.ico";
import "../img/safari-pinned-tab.svg";
// Styles // Styles
import "../scss/app.scss"; import "../scss/app.scss";
@ -91,6 +85,12 @@ const App = () => (
</div> </div>
); );
if ("serviceWorker" in navigator) {
window.addEventListener("load", () => {
navigator.serviceWorker.register("/service-worker.js");
});
}
ReactDOM.render( ReactDOM.render(
<Provider store={store}> <Provider store={store}>
<Router history={history}> <Router history={history}>

View File

@ -1,14 +1,14 @@
import React from "react"; import React from "react";
import moment from "moment";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { format } from "timeago.js";
import { UserEdit } from "./userEdit"; import { UserEdit } from "./userEdit";
export const User = ({ id }) => { export const User = ({ id }) => {
const user = useSelector((state) => state.admin.users.get(id)); const user = useSelector((state) => state.admin.users.get(id));
const polochon = user.polochon; const polochon = user.polochon;
const lastSeen = moment(user.last_seen, "YYYY-MM-DDTHH:mm:ss.SZ"); const lastSeen = new Date(user.last_seen);
return ( return (
<tr> <tr>
@ -33,7 +33,7 @@ export const User = ({ id }) => {
} }
></span> ></span>
</td> </td>
<td>{lastSeen.isValid() ? lastSeen.fromNow() : "-"}</td> <td>{isNaN(lastSeen) ? "-" : format(lastSeen)}</td>
<td> <td>
<UserEdit id={id} /> <UserEdit id={id} />
</td> </td>

View File

@ -76,15 +76,21 @@ const Player = ({ url, subtitles }) => {
<video className="embed-responsive-item" controls> <video className="embed-responsive-item" controls>
<source src={url} type="video/mp4" /> <source src={url} type="video/mp4" />
{subtitles && {subtitles &&
[...subtitles.entries()].map(([lang, sub]) => ( [...subtitles.entries()].map(([lang, sub]) => {
<track if (sub.embedded) {
key={lang} return null;
kind="subtitles" }
label={sub.language}
src={sub.vvt_file} return (
srcLang={sub.language} <track
/> key={lang}
))} kind="subtitles"
label={sub.lang}
src={sub.vvt_file}
srcLang={sub.lang}
/>
);
})}
</video> </video>
</div> </div>
); );

View File

@ -0,0 +1,77 @@
import React from "react";
import PropTypes from "prop-types";
import { useSelector } from "react-redux";
import { useHistory } from "react-router-dom";
export const DownloadProgress = ({ imdbId, season, episode }) => {
let history = useHistory();
const torrentGroup = useSelector((state) =>
state.torrents.torrents.get(imdbId)
);
if (!torrentGroup || torrentGroup.length === 0) {
return null;
}
let torrent;
const type = torrentGroup[0].type;
switch (type) {
case "movie":
torrent = torrentGroup[0];
break;
case "episode": {
if (!season || !episode) {
return null;
}
const torrents = torrentGroup.filter(
(torrent) => torrent.episode === episode && torrent.season === season
);
if (torrents.length !== 1) {
return null;
}
torrent = torrents[0];
break;
}
default:
return null;
}
if (!torrent || !torrent.status) {
return null;
}
const progress = Number(torrent.status.percent_done).toFixed(1);
if (progress === 0) {
return null;
}
const handleClick = () => {
history.push("/torrents/list");
};
return (
<div className="w-100 mt-n2 clickable" onClick={handleClick}>
<small className="text text-muted">Downloading...</small>
<div
className="progress"
style={{
height: "0.2rem",
}}
>
<div
className="progress-bar bg-warning"
style={{
width: `${progress}%`,
}}
/>
</div>
</div>
);
};
DownloadProgress.propTypes = {
imdbId: PropTypes.string.isRequired,
season: PropTypes.number,
episode: PropTypes.number,
};

View File

@ -8,7 +8,6 @@ export const PolochonMetadata = ({
container, container,
videoCodec, videoCodec,
audioCodec, audioCodec,
releaseGroup,
size, size,
}) => { }) => {
if (!quality || quality === "") { if (!quality || quality === "") {
@ -17,12 +16,12 @@ export const PolochonMetadata = ({
const s = size === 0 ? "" : prettySize(size); const s = size === 0 ? "" : prettySize(size);
const metadata = [quality, container, videoCodec, audioCodec, releaseGroup, s] const metadata = [quality, s, container, videoCodec, audioCodec]
.filter((m) => m && m !== "") .filter((m) => m && m !== "")
.join(", "); .join(", ");
return ( return (
<span> <span className="text text-muted">
<i className="fa fa-file-video-o mr-1" /> <i className="fa fa-file-video-o mr-1" />
{metadata} {metadata}
</span> </span>

View File

@ -1,21 +1,26 @@
import React from "react"; import React from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import moment from "moment"; import { format } from "timeago.js";
const prettyDate = (input) => { const prettyDate = (input) => {
if (typeof input !== "string" || input === "") { if (typeof input !== "string" || input === "") {
return input; return input;
} }
const date = moment(input, "YYYY-MM-DD HH:mm:ss Z"); const date = new Date(input);
if (!date.isValid()) { if (isNaN(date)) {
return ""; return "";
} }
let output = date.format("DD/MM/YYYY"); const dd = date.getDay().toString().padStart(2, "0");
const mm = date.getMonth().toString().padStart(2, "0");
const yyyy = date.getFullYear();
let output = `${dd}/${mm}/${yyyy}`;
if (date > moment().subtract(1, "month") && date < moment().add(1, "month")) { const now = new Date();
output += " (" + date.fromNow() + ")"; const days = Math.abs((now - date) / (24 * 60 * 60 * 1000));
if (days < 31) {
output += ` (${format(date)})`;
} }
return output; return output;

View File

@ -12,6 +12,7 @@ import { Rating } from "../details/rating";
import { ReleaseDate } from "../details/releaseDate"; import { ReleaseDate } from "../details/releaseDate";
import { Runtime } from "../details/runtime"; import { Runtime } from "../details/runtime";
import { Title } from "../details/title"; import { Title } from "../details/title";
import { DownloadProgress } from "../details/downloadProgress";
const ListDetails = (props) => { const ListDetails = (props) => {
if (!props.data || Object.keys(props.data).length === 0) { if (!props.data || Object.keys(props.data).length === 0) {
@ -41,6 +42,7 @@ const ListDetails = (props) => {
subtitles={props.data.subtitles} subtitles={props.data.subtitles}
/> />
</div> </div>
<DownloadProgress imdbId={props.data.imdb_id} />
<TrackingLabel <TrackingLabel
wishlisted={props.data.wishlisted} wishlisted={props.data.wishlisted}
inLibrary={props.data.polochon_url !== ""} inLibrary={props.data.polochon_url !== ""}

View File

@ -20,6 +20,7 @@ import { PolochonMetadata } from "../details/polochon";
import { TrackingLabel } from "../details/tracking"; import { TrackingLabel } from "../details/tracking";
import { Genres } from "../details/genres"; import { Genres } from "../details/genres";
import { Runtime } from "../details/runtime"; import { Runtime } from "../details/runtime";
import { DownloadProgress } from "../details/downloadProgress";
import { DownloadAndStream } from "../buttons/download"; import { DownloadAndStream } from "../buttons/download";
import { ImdbBadge } from "../buttons/imdb"; import { ImdbBadge } from "../buttons/imdb";
@ -122,6 +123,9 @@ export const Header = () => {
subtitles={subtitles} subtitles={subtitles}
/> />
</div> </div>
<div className="card-text mt-2">
<DownloadProgress imdbId={imdbId} />
</div>
<p className="card-text"> <p className="card-text">
<TrackingLabel inLibrary={inLibrary} wishlisted={wishlisted} /> <TrackingLabel inLibrary={inLibrary} wishlisted={wishlisted} />
</p> </p>

View File

@ -11,6 +11,7 @@ import { PolochonMetadata } from "../../details/polochon";
import { ReleaseDate } from "../../details/releaseDate"; import { ReleaseDate } from "../../details/releaseDate";
import { Runtime } from "../../details/runtime"; import { Runtime } from "../../details/runtime";
import { Title } from "../../details/title"; import { Title } from "../../details/title";
import { DownloadProgress } from "../../details/downloadProgress";
import { DownloadAndStream } from "../../buttons/download"; import { DownloadAndStream } from "../../buttons/download";
import { ShowMore } from "../../buttons/showMore"; import { ShowMore } from "../../buttons/showMore";
@ -62,6 +63,7 @@ export const Episode = ({ season, episode }) => {
/> />
<ReleaseDate date={data.aired} /> <ReleaseDate date={data.aired} />
<Runtime runtime={data.runtime} /> <Runtime runtime={data.runtime} />
<DownloadProgress imdbId={imdbId} season={season} episode={episode} />
<Plot plot={data.plot} /> <Plot plot={data.plot} />
<DownloadAndStream <DownloadAndStream
name={prettyEpisodeName(showTitle, season, episode)} name={prettyEpisodeName(showTitle, season, episode)}

View File

@ -2,7 +2,7 @@ import React, { useEffect } from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { UAParser } from "ua-parser-js"; import { UAParser } from "ua-parser-js";
import moment from "moment"; import { format } from "timeago.js";
import { getUserTokens, deleteUserToken } from "../../actions/users"; import { getUserTokens, deleteUserToken } from "../../actions/users";
@ -27,6 +27,9 @@ export const UserTokens = () => {
const Token = ({ token }) => { const Token = ({ token }) => {
const ua = UAParser(token.user_agent); const ua = UAParser(token.user_agent);
const lastUsed = new Date(token.last_used);
const createdAt = new Date(token.created_at);
return ( return (
<div className="card mt-3"> <div className="card mt-3">
<div className="card-header"> <div className="card-header">
@ -39,8 +42,8 @@ const Token = ({ token }) => {
<div className="card-body row"> <div className="card-body row">
<div className="col-12 col-md-6"> <div className="col-12 col-md-6">
<p>Last IP: {token.ip}</p> <p>Last IP: {token.ip}</p>
<p>Last used: {moment(token.last_used).fromNow()}</p> <p>Last used: {isNaN(lastUsed) ? "-" : format(lastUsed)}</p>
<p>Created: {moment(token.created_at).fromNow()}</p> <p>Created: {isNaN(createdAt) ? "-" : format(createdAt)}</p>
</div> </div>
<div className="col-12 col-md-6"> <div className="col-12 col-md-6">
<p> <p>

View File

@ -1,8 +1,8 @@
import { useEffect, useState, useCallback } from "react"; import { useEffect, useState, useCallback } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { setFetchedTorrents } from "../actions/torrents"; import { setFetchedTorrents } from "../actions/torrents";
import { newMovieEvent } from "../actions/movies"; import { newMovieEvent, getMovieDetails } from "../actions/movies";
import { newEpisodeEvent } from "../actions/shows"; import { newEpisodeEvent, getEpisodeDetails } from "../actions/shows";
export const WsHandler = () => { export const WsHandler = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -67,8 +67,16 @@ export const WsHandler = () => {
case "newVideo": case "newVideo":
if (data.message.type === "movie") { if (data.message.type === "movie") {
dispatch(newMovieEvent(data.message.data)); dispatch(newMovieEvent(data.message.data));
dispatch(getMovieDetails(data.message.data.imdb_id));
} else if (data.message.type === "episode") { } else if (data.message.type === "episode") {
dispatch(newEpisodeEvent(data.message.data)); dispatch(newEpisodeEvent(data.message.data));
dispatch(
getEpisodeDetails(
data.message.data.show_imdb_id,
data.message.data.season,
data.message.data.episode
)
);
} }
break; break;
} }

View File

@ -84,19 +84,30 @@ export default (state = defaultState, action) =>
draft.show.tracked_episode = action.payload.episode; // eslint-disable-line camelcase draft.show.tracked_episode = action.payload.episode; // eslint-disable-line camelcase
break; break;
case "EPISODE_GET_DETAILS_PENDING": case "EPISODE_GET_DETAILS_PENDING": {
draft.show.seasons const imdbId = action.payload.main.imdbId;
.get(action.payload.main.season) if (!draft.show || draft.show.imdb_id !== imdbId) {
.get(action.payload.main.episode).fetching = true; break;
}
const season = action.payload.main.season;
const episode = action.payload.main.episode;
draft.show.seasons.get(season).get(episode).fetching = true;
break; break;
}
case "EPISODE_GET_DETAILS_FULFILLED": { case "EPISODE_GET_DETAILS_FULFILLED": {
let episode = action.payload.response.data; const imdbId = action.payload.main.imdbId;
if (!episode) { if (!draft.show || draft.show.imdb_id !== imdbId) {
return draft; break;
} }
formatEpisode(episode);
draft.show.seasons.get(episode.season).set(episode.episode, episode); const season = action.payload.main.season;
const episode = action.payload.main.episode;
let data = action.payload.response.data;
formatEpisode(data);
draft.show.seasons.get(season).set(episode, data);
break; break;
} }

File diff suppressed because it is too large Load Diff

View File

@ -14,7 +14,6 @@
"immer": "^9.0.5", "immer": "^9.0.5",
"jquery": "^3.6.0", "jquery": "^3.6.0",
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
"moment": "^2.29.1",
"popper.js": "^1.15.0", "popper.js": "^1.15.0",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"react": "^17.0.2", "react": "^17.0.2",
@ -49,14 +48,18 @@
"eslint-plugin-react": "^7.24.0", "eslint-plugin-react": "^7.24.0",
"eslint-plugin-react-hooks": "^4.2.0", "eslint-plugin-react-hooks": "^4.2.0",
"html-webpack-plugin": "^5.3.2", "html-webpack-plugin": "^5.3.2",
"mini-css-extract-plugin": "^2.2.0",
"postcss-loader": "^6.1.1", "postcss-loader": "^6.1.1",
"prettier": "^2.3.2", "prettier": "^2.3.2",
"purgecss-webpack-plugin": "^4.0.3",
"sass": "^1.37.5", "sass": "^1.37.5",
"sass-loader": "^12.1.0", "sass-loader": "^12.1.0",
"style-loader": "^3.2.1", "style-loader": "^3.2.1",
"timeago.js": "^4.0.2",
"universal-cookie": "^4.0.4", "universal-cookie": "^4.0.4",
"webpack": "^5.50.0", "webpack": "^5.50.0",
"webpack-cli": "^4.7.2", "webpack-cli": "^4.7.2",
"webpack-pwa-manifest": "^4.3.0" "webpack-pwa-manifest": "^4.3.0",
"workbox-webpack-plugin": "^6.2.4"
} }
} }

View File

@ -78,7 +78,10 @@ div.show.dropdown.nav-item > div {
.video-details { .video-details {
> div, > p, > span { > div, > p, > span {
margin-bottom: 1rem; margin-bottom: 1rem;
@media (max-width: 330px) { @media (max-height: 820px) {
margin-bottom: 0.5rem;
}
@media (max-height: 580px) {
margin-bottom: 0rem; margin-bottom: 0rem;
} }
} }

View File

@ -1,8 +1,12 @@
var webpack = require("webpack"); var webpack = require("webpack");
var path = require("path"); var path = require("path");
const glob = require("glob");
var WebpackPwaManifest = require("webpack-pwa-manifest"); var WebpackPwaManifest = require("webpack-pwa-manifest");
var HtmlWebpackPlugin = require("html-webpack-plugin"); var HtmlWebpackPlugin = require("html-webpack-plugin");
var { CleanWebpackPlugin } = require("clean-webpack-plugin"); var { CleanWebpackPlugin } = require("clean-webpack-plugin");
const WorkboxPlugin = require("workbox-webpack-plugin");
const PurgeCSSPlugin = require("purgecss-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
var mode = "development"; var mode = "development";
var BUILD_DIR = path.resolve(__dirname, "../build/public/"); var BUILD_DIR = path.resolve(__dirname, "../build/public/");
@ -20,7 +24,7 @@ const config = {
output: { output: {
path: BUILD_DIR, path: BUILD_DIR,
filename: "[contenthash]-app.js", filename: "[contenthash]-app.js",
assetModuleFilename: "[hash]-[name][ext][query]", assetModuleFilename: "[contenthash]-[name][ext][query]",
}, },
optimization: {}, optimization: {},
module: { module: {
@ -35,9 +39,18 @@ const config = {
}, },
}, },
}, },
{
test: /\.css$/i,
use: [MiniCssExtractPlugin.loader, "css-loader", "postcss-loader"],
},
{ {
test: /\.scss$/, test: /\.scss$/,
use: ["style-loader", "css-loader", "sass-loader", "postcss-loader"], use: [
MiniCssExtractPlugin.loader,
"css-loader",
"sass-loader",
"postcss-loader",
],
}, },
{ {
test: /\.(png|jpg|svg|ico)$/, test: /\.(png|jpg|svg|ico)$/,
@ -62,12 +75,13 @@ const config = {
}), }),
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
template: path.join(SRC_DIR, "index.html"), template: path.join(SRC_DIR, "index.html"),
favicon: path.join(SRC_DIR, "img/favicon.ico"),
}), }),
new WebpackPwaManifest({ new WebpackPwaManifest({
fingerprints: true, fingerprints: true,
inject: true, inject: true,
ios: { ios: {
"apple-mobile-web-app-status-bar-style": "default", "apple-mobile-web-app-status-bar-style": "#4e5d6c",
"apple-mobile-web-app-title": "Canapé", "apple-mobile-web-app-title": "Canapé",
}, },
name: "Canapé", name: "Canapé",
@ -84,6 +98,11 @@ const config = {
src: path.resolve(__dirname, "img/android-chrome-512x512.png"), src: path.resolve(__dirname, "img/android-chrome-512x512.png"),
sizes: [96, 128, 192, 256, 384, 512], sizes: [96, 128, 192, 256, 384, 512],
}, },
{
src: path.resolve(__dirname, "img/maskable_icon_x512.png"),
size: "512x512",
purpose: "maskable",
},
{ {
src: path.resolve(__dirname, "img/apple-touch-icon.png"), src: path.resolve(__dirname, "img/apple-touch-icon.png"),
sizes: [80, 120, 152, 167, 180], sizes: [80, 120, 152, 167, 180],
@ -91,6 +110,10 @@ const config = {
}, },
], ],
}), }),
new MiniCssExtractPlugin({
filename: "[contenthash].css",
}),
new WorkboxPlugin.GenerateSW(),
], ],
resolve: { resolve: {
extensions: [".js"], extensions: [".js"],
@ -98,4 +121,12 @@ const config = {
devtool: mode === "production" ? false : "source-map", devtool: mode === "production" ? false : "source-map",
}; };
if (mode === "production") {
config.plugins.push(
new PurgeCSSPlugin({
paths: () => glob.sync(`${SRC_DIR}/**/*`, { nodir: true }),
})
);
}
module.exports = config; module.exports = config;