├── renderer
├── actions
│ ├── index.js
│ ├── ui.js
│ └── search.js
├── static
│ ├── icon.ico
│ ├── icon.png
│ ├── icon.icns
│ ├── loading.gif
│ └── icon.iconset
│ │ ├── ..icns
│ │ ├── icon_16x16.png
│ │ ├── icon_32x32.png
│ │ ├── icon_64x64.png
│ │ ├── icon_1024x1024.png
│ │ ├── icon_128x128.png
│ │ ├── icon_256x256.png
│ │ └── icon_512x512.png
├── reducers
│ ├── index.js
│ ├── ui.js
│ └── search.js
├── components
│ ├── Loading.js
│ ├── Title.js
│ ├── Logo.js
│ ├── TitleBar.js
│ ├── FooterAbout.js
│ ├── __tests__
│ │ ├── Info.test.js
│ │ ├── __snapshots__
│ │ │ ├── Info.test.js.snap
│ │ │ └── Content.test.js.snap
│ │ └── Content.test.js
│ ├── LanguageToggle.js
│ ├── Info.js
│ ├── Meta.js
│ ├── Content.js
│ ├── ListEmpty.js
│ ├── Footer.js
│ ├── SearchField.js
│ ├── SoftwareItem.js
│ ├── Credits.js
│ ├── Drop.js
│ ├── Search.js
│ ├── ListItem.js
│ ├── Layout.js
│ ├── List.js
│ └── FilePath.js
├── utils
│ ├── index.js
│ └── tracking.js
├── store
│ └── index.js
├── containers
│ ├── Footer.js
│ ├── Search.js
│ └── Content.js
├── next.config.js
├── pages
│ ├── check.js
│ ├── about.js
│ ├── progress.js
│ └── start.js
├── types
│ └── index.js
└── data
│ ├── languages.js
│ └── software.js
├── .eslintignore
├── .travis.yml
├── .gitignore
├── .babelrc
├── main
├── notification.js
├── settings.js
├── touchbar.js
├── data
│ └── extensions.js
├── sources.js
├── windows
│ ├── check.js
│ ├── progress.js
│ ├── about.js
│ └── main.js
├── donate.js
├── utils.js
├── updater.js
├── download.js
├── index.js
└── menu.js
├── LICENSE
├── .github
└── issue_template.md
├── .eslintrc
├── CONTRIBUTING.md
├── CODE_OF_CONDUCT.md
├── package.json
└── README.md
/renderer/actions/index.js:
--------------------------------------------------------------------------------
1 | export * from "./ui";
2 | export * from "./search";
3 |
--------------------------------------------------------------------------------
/renderer/static/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zog/caption/master/renderer/static/icon.ico
--------------------------------------------------------------------------------
/renderer/static/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zog/caption/master/renderer/static/icon.png
--------------------------------------------------------------------------------
/renderer/static/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zog/caption/master/renderer/static/icon.icns
--------------------------------------------------------------------------------
/renderer/static/loading.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zog/caption/master/renderer/static/loading.gif
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | **/node_modules
2 | server.js
3 | public
4 | build
5 | build-ci
6 | flow-typed
7 | flow
8 |
--------------------------------------------------------------------------------
/renderer/static/icon.iconset/..icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zog/caption/master/renderer/static/icon.iconset/..icns
--------------------------------------------------------------------------------
/renderer/static/icon.iconset/icon_16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zog/caption/master/renderer/static/icon.iconset/icon_16x16.png
--------------------------------------------------------------------------------
/renderer/static/icon.iconset/icon_32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zog/caption/master/renderer/static/icon.iconset/icon_32x32.png
--------------------------------------------------------------------------------
/renderer/static/icon.iconset/icon_64x64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zog/caption/master/renderer/static/icon.iconset/icon_64x64.png
--------------------------------------------------------------------------------
/renderer/static/icon.iconset/icon_1024x1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zog/caption/master/renderer/static/icon.iconset/icon_1024x1024.png
--------------------------------------------------------------------------------
/renderer/static/icon.iconset/icon_128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zog/caption/master/renderer/static/icon.iconset/icon_128x128.png
--------------------------------------------------------------------------------
/renderer/static/icon.iconset/icon_256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zog/caption/master/renderer/static/icon.iconset/icon_256x256.png
--------------------------------------------------------------------------------
/renderer/static/icon.iconset/icon_512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zog/caption/master/renderer/static/icon.iconset/icon_512x512.png
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - '8'
4 |
5 | jobs:
6 | include:
7 | - stage: test
8 | script: npm run test
9 | - stage: build
10 | script: npm run build
--------------------------------------------------------------------------------
/renderer/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from "redux";
2 |
3 | import ui from "./ui";
4 | import search from "./search";
5 |
6 | export default combineReducers({
7 | ui,
8 | search,
9 | });
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # build output
2 | dist
3 | renderer/.next
4 | renderer/out
5 |
6 | # dependencies
7 | node_modules
8 |
9 | # logs
10 | npm-debug.log
11 |
12 | # mac
13 | .DS_Store
14 |
15 | # dotenv
16 | .env
17 |
18 | # update files
19 | dev-app-update.yml
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "development": {
4 | "presets": "next/babel"
5 | },
6 | "production": {
7 | "presets": "next/babel"
8 | },
9 | "test": {
10 | "presets": [["env", { "modules": "commonjs" }], "next/babel"]
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/renderer/components/Loading.js:
--------------------------------------------------------------------------------
1 | const Loading = () =>
2 |
3 |
4 |
5 |
12 | ;
13 |
14 | export default Loading;
15 |
--------------------------------------------------------------------------------
/main/notification.js:
--------------------------------------------------------------------------------
1 | const { Notification } = require("electron");
2 |
3 | const notification = message => {
4 | if (Notification.isSupported()) {
5 | const notify = new Notification({
6 | title: "Caption",
7 | body: message,
8 | });
9 | notify.show();
10 | }
11 | };
12 |
13 | module.exports = notification;
14 |
--------------------------------------------------------------------------------
/renderer/components/Title.js:
--------------------------------------------------------------------------------
1 | const Title = ({ title }) =>
2 |
3 | {title}
4 |
5 |
14 | ;
15 |
16 | export default Title;
17 |
--------------------------------------------------------------------------------
/renderer/components/Logo.js:
--------------------------------------------------------------------------------
1 | const Logo = ({ size = 80, margin = "0 auto" }) => (
2 |
3 |
4 |
5 |
13 |
14 | );
15 |
16 | export default Logo;
17 |
--------------------------------------------------------------------------------
/renderer/reducers/ui.js:
--------------------------------------------------------------------------------
1 | import * as types from "./../types";
2 |
3 | const initialState = {
4 | language: "eng",
5 | };
6 |
7 | export default function reducer(state = initialState, action) {
8 | switch (action.type) {
9 | case types.SET_LANGUAGE:
10 | return {
11 | ...state,
12 | language: action.payload.language,
13 | };
14 | default:
15 | return state;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/renderer/components/TitleBar.js:
--------------------------------------------------------------------------------
1 | import Title from "./Title";
2 |
3 | const TitleBar = ({ title }) => (
4 |
16 | );
17 |
18 | export default TitleBar;
19 |
--------------------------------------------------------------------------------
/renderer/utils/index.js:
--------------------------------------------------------------------------------
1 | // File size readable
2 | const fileSizeReadable = size => {
3 | if (size >= 1000000000) {
4 | return `${Math.ceil(size / 1000000000)}GB`;
5 | } else if (size >= 1000000) {
6 | return `${Math.ceil(size / 1000000)}MB`;
7 | } else if (size >= 1000) {
8 | return `${Math.ceil(size / 1000)}kB`;
9 | }
10 | return `${Math.ceil(size)}B`;
11 | };
12 |
13 | // Export
14 | export { fileSizeReadable };
15 |
--------------------------------------------------------------------------------
/main/settings.js:
--------------------------------------------------------------------------------
1 | const { ipcMain } = require("electron");
2 |
3 | const initSettings = () => {
4 | const { store } = global;
5 |
6 | if (!store.has("language")) {
7 | store.set("language", "eng");
8 | }
9 |
10 | ipcMain.on("getStore", (event, setting) => {
11 | if (setting === "language") {
12 | const { mainWindow } = global.windows;
13 | const language = store.get("language");
14 | mainWindow.webContents.send("language", language);
15 | }
16 | });
17 |
18 | ipcMain.on("setStore", (event, key, value) => {
19 | store.set(key, value);
20 | });
21 | };
22 |
23 | module.exports = initSettings;
24 |
--------------------------------------------------------------------------------
/renderer/store/index.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware } from "redux";
2 | import { composeWithDevTools } from "redux-devtools-extension";
3 |
4 | // Middleware
5 | import thunkMiddleware from "redux-thunk";
6 | import { createLogger } from "redux-logger";
7 |
8 | // Root reducer
9 | import rootReducer from "./../reducers";
10 |
11 | const initStore = (initialState = {}) => {
12 | const loggerMiddleware = createLogger();
13 |
14 | return createStore(
15 | rootReducer,
16 | initialState,
17 | composeWithDevTools(applyMiddleware(thunkMiddleware, loggerMiddleware)),
18 | );
19 | };
20 |
21 | export default initStore;
22 |
--------------------------------------------------------------------------------
/renderer/containers/Footer.js:
--------------------------------------------------------------------------------
1 | import { connect } from "react-redux";
2 |
3 | import { setLanguage } from "./../actions";
4 | import Footer from "./../components/Footer";
5 |
6 | const mapStateToProps = ({ ui, search }) => ({
7 | language: ui.language,
8 | loading: !search.searchCompleted,
9 | results: search.results,
10 | showResults: search.searchAttempts > 0,
11 | isFileSearch: search.files.length > 0,
12 | totalFiles: search.files.length,
13 | foundFiles: search.files.filter(({ status }) => status === "done").length,
14 | });
15 | const mapDispatchToProps = {
16 | onLanguageChange: setLanguage,
17 | };
18 |
19 | export default connect(mapStateToProps, mapDispatchToProps)(Footer);
20 |
--------------------------------------------------------------------------------
/renderer/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | webpack(config) {
3 | config.target = "electron-renderer";
4 |
5 | // config.plugins = config.plugins.filter(plugin => {
6 | // return plugin.constructor.name !== "UglifyJsPlugin";
7 | // });
8 |
9 | return config;
10 | },
11 |
12 | exportPathMap() {
13 | // Let Next.js know where to find the entry page
14 | // when it's exporting the static bundle for the use
15 | // in the production version of your app
16 | return {
17 | "/start": { page: "/start" },
18 | "/about": { page: "/about" },
19 | "/check": { page: "/check" },
20 | "/progress": { page: "/progress" },
21 | };
22 | },
23 | };
24 |
--------------------------------------------------------------------------------
/renderer/containers/Search.js:
--------------------------------------------------------------------------------
1 | import { connect } from "react-redux";
2 |
3 | import Search from "./../components/Search";
4 |
5 | import { updateSearchQuery, resetSearch } from "./../actions";
6 |
7 | const mapStateToProps = ({ search }) => ({
8 | value: search.searchQuery || search.dropFilePath,
9 | placeholder: search.placeholder,
10 | dropFilePath: search.dropFilePath,
11 | dropFilePathClean: search.dropFilePathClean,
12 | });
13 |
14 | const mapDispatchToProps = {
15 | onChange: event => dispatch => {
16 | const searchQuery = event.target.value;
17 | dispatch(updateSearchQuery(searchQuery));
18 | },
19 | onReset: resetSearch,
20 | };
21 |
22 | export default connect(mapStateToProps, mapDispatchToProps, null, {
23 | withRef: true,
24 | })(Search);
25 |
--------------------------------------------------------------------------------
/renderer/utils/tracking.js:
--------------------------------------------------------------------------------
1 | import ReactGA from "react-ga";
2 |
3 | export const initGA = () => {
4 | ReactGA.initialize("UA-89238300-2");
5 | };
6 |
7 | export const logPageView = () => {
8 | ReactGA.set({ page: "/v2/start" });
9 | ReactGA.set({ version: "2" });
10 | ReactGA.pageview("/v2/start");
11 | };
12 |
13 | export const logQuery = query => {
14 | ReactGA.event({
15 | category: "search",
16 | action: query,
17 | label: `Searched for ${query}`,
18 | });
19 | };
20 |
21 | export const logDonated = () => {
22 | ReactGA.event({
23 | category: "interaction",
24 | action: "Donate button clicked",
25 | });
26 | };
27 |
28 | export const logAbout = () => {
29 | ReactGA.event({
30 | category: "interaction",
31 | action: "About window opened",
32 | });
33 | };
34 |
--------------------------------------------------------------------------------
/renderer/containers/Content.js:
--------------------------------------------------------------------------------
1 | import { connect } from "react-redux";
2 | import { ipcRenderer } from "electron";
3 |
4 | import Content from "./../components/Content";
5 |
6 | const mapStateToProps = ({ search }) => ({
7 | searchQuery: search.searchQuery,
8 | files: search.files,
9 | results: search.results,
10 | loading: search.loading,
11 | });
12 |
13 | const mapDispatchToProps = {
14 | onDrop: event => () => {
15 | const droppedItems = [];
16 | const rawFiles = event.dataTransfer
17 | ? event.dataTransfer.files
18 | : event.target.files;
19 |
20 | Object.keys(rawFiles).map(key => droppedItems.push(rawFiles[key].path));
21 |
22 | ipcRenderer.send("processFiles", droppedItems);
23 | },
24 | };
25 |
26 | export default connect(mapStateToProps, mapDispatchToProps)(Content);
27 |
--------------------------------------------------------------------------------
/renderer/components/FooterAbout.js:
--------------------------------------------------------------------------------
1 | import { shell } from "electron";
2 |
3 | const FooterAbout = () => (
4 |
5 | Made with 💝 by{" "}
6 | shell.openExternal("https://twitter.com/gielcobben")}>
7 | Giel
8 | {" "}
9 | &{" "}
10 | shell.openExternal("https://twitter.com/vernon_dg")}>
11 | Vernon
12 |
13 |
27 |
28 | );
29 |
30 | export default FooterAbout;
31 |
--------------------------------------------------------------------------------
/renderer/components/__tests__/Info.test.js:
--------------------------------------------------------------------------------
1 | import ReactTestRenderer from "react-test-renderer";
2 | import Info from "./../Info";
3 |
4 | describe(" ", () => {
5 | it("should show an empty state when no results are found", () => {
6 | const tree = ReactTestRenderer.create( );
7 | expect(tree.toJSON()).toMatchSnapshot();
8 | });
9 |
10 | it("should show the total results if any are present", () => {
11 | const results = [{}, {}];
12 | const tree = ReactTestRenderer.create( );
13 | expect(tree.toJSON()).toMatchSnapshot();
14 | });
15 |
16 | it("should show a loader when there are no results yet", () => {
17 | const tree = ReactTestRenderer.create( );
18 | expect(tree.toJSON()).toMatchSnapshot();
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/main/touchbar.js:
--------------------------------------------------------------------------------
1 | const { TouchBar, shell } = require("electron");
2 |
3 | const { TouchBarButton, TouchBarSpacer } = TouchBar;
4 | const { showAboutWindow } = require("./windows/about");
5 |
6 | const aboutCaptionButton = new TouchBarButton({
7 | label: "🎬 About Caption",
8 | click: () => {
9 | showAboutWindow();
10 | },
11 | });
12 |
13 | const donateButton = new TouchBarButton({
14 | label: "💰 Donate",
15 | click: () => {
16 | const { mainWindow } = global.windows;
17 | shell.openExternal("https://www.paypal.me/gielcobben");
18 | mainWindow.webContents.send("logDonated");
19 | },
20 | });
21 |
22 | const touchBar = new TouchBar([
23 | new TouchBarSpacer({ size: "flexible" }),
24 | aboutCaptionButton,
25 | new TouchBarSpacer({ size: "large" }),
26 | donateButton,
27 | new TouchBarSpacer({ size: "flexible" }),
28 | ]);
29 |
30 | module.exports = touchBar;
31 |
--------------------------------------------------------------------------------
/renderer/components/LanguageToggle.js:
--------------------------------------------------------------------------------
1 | import languages from "../data/languages";
2 |
3 | const LanguageToggle = ({ language, onLanguageChange }) => (
4 |
5 | {languages.map((lang, index) => (
6 |
7 | {lang.name
8 | .replace(/(?!\w|\s)./g, "")
9 | .replace(/\s+/g, " ")
10 | .replace(/^(\s*)([\W\w]*)(\b\s*$)/g, "$2")}
11 |
12 | ))}
13 |
14 |
28 |
29 | );
30 |
31 | export default LanguageToggle;
32 |
--------------------------------------------------------------------------------
/renderer/components/__tests__/__snapshots__/Info.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` should show a loader when there are no results yet 1`] = `
4 |
7 |
10 |
14 |
15 |
16 | `;
17 |
18 | exports[` should show an empty state when no results are found 1`] = `
19 |
22 |
25 | Nothing Found
26 |
27 |
28 | `;
29 |
30 | exports[` should show the total results if any are present 1`] = `
31 |
34 |
37 | 2 Results
38 |
39 |
40 | `;
41 |
--------------------------------------------------------------------------------
/renderer/components/Info.js:
--------------------------------------------------------------------------------
1 | import Loading from "./Loading";
2 |
3 | const Info = ({
4 | results = [],
5 | loading = false,
6 | isFileSearch = false,
7 | totalFiles = 0,
8 | foundFiles = 0,
9 | }) => (
10 |
11 | {!loading &&
12 | !isFileSearch && (
13 |
14 | {results.length <= 0 ? `Nothing Found` : `${results.length} Results`}
15 |
16 | )}
17 |
18 | {!loading &&
19 | isFileSearch && (
20 | {`${foundFiles} / ${totalFiles} Subtitles found`}
21 | )}
22 |
23 | {loading && }
24 |
36 |
37 | );
38 |
39 | export default Info;
40 |
--------------------------------------------------------------------------------
/renderer/components/Meta.js:
--------------------------------------------------------------------------------
1 | import { remote } from "electron";
2 |
3 | import Logo from "./Logo";
4 |
5 | const Meta = ({ appVersion }) =>
6 |
7 |
8 |
9 |
Caption
10 | Version: {appVersion}
11 |
12 |
13 |
42 | ;
43 |
44 | export default Meta;
45 |
--------------------------------------------------------------------------------
/renderer/pages/check.js:
--------------------------------------------------------------------------------
1 | import Layout from "../components/Layout";
2 | import Logo from "../components/Logo";
3 |
4 | const Check = () => (
5 |
6 |
7 |
8 |
9 |
Checking for updates...
10 |
11 |
12 |
13 |
38 |
39 |
40 | );
41 |
42 | export default Check;
43 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Giel Cobben (gielcobben.com)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/renderer/components/__tests__/Content.test.js:
--------------------------------------------------------------------------------
1 | import ReactTestRenderer from "react-test-renderer";
2 | import Content from "./../Content";
3 |
4 | describe(" ", () => {
5 | it("should show when there is a query but no results", () => {
6 | const tree = ReactTestRenderer.create( );
7 | expect(tree.toJSON()).toMatchSnapshot();
8 | });
9 |
10 | it("should show when there is no searchQuery and no files dropped", () => {
11 | const tree = ReactTestRenderer.create( );
12 | expect(tree.toJSON()).toMatchSnapshot();
13 | });
14 |
15 | it("should render
when files dropped are dropped", () => {
16 | const files = [{}, {}];
17 | const tree = ReactTestRenderer.create( );
18 | expect(tree.toJSON()).toMatchSnapshot();
19 | });
20 |
21 | it("should render
when there are results", () => {
22 | const results = [{}, {}];
23 | const tree = ReactTestRenderer.create( );
24 | expect(tree.toJSON()).toMatchSnapshot();
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/main/data/extensions.js:
--------------------------------------------------------------------------------
1 | const extensions = [
2 | "3g2",
3 | "3gp",
4 | "3gp2",
5 | "3gpp",
6 | "60d",
7 | "ajp",
8 | "asf",
9 | "asx",
10 | "avchd",
11 | "avi",
12 | "bik",
13 | "bix",
14 | "box",
15 | "cam",
16 | "dat",
17 | "divx",
18 | "dmf",
19 | "dv",
20 | "dvr-ms",
21 | "evo",
22 | "flc",
23 | "fli",
24 | "flic",
25 | "flv",
26 | "flx",
27 | "gvi",
28 | "gvp",
29 | "h264",
30 | "m1v",
31 | "m2p",
32 | "m2ts",
33 | "m2v",
34 | "m4e",
35 | "m4v",
36 | "mjp",
37 | "mjpeg",
38 | "mjpg",
39 | "mkv",
40 | "moov",
41 | "mov",
42 | "movhd",
43 | "movie",
44 | "movx",
45 | "mp4",
46 | "mpe",
47 | "mpeg",
48 | "mpg",
49 | "mpv",
50 | "mpv2",
51 | "mxf",
52 | "nsv",
53 | "nut",
54 | "ogg",
55 | "ogm",
56 | "omf",
57 | "ps",
58 | "qt",
59 | "ram",
60 | "rm",
61 | "rmvb",
62 | "swf",
63 | "ts",
64 | "vfw",
65 | "vid",
66 | "video",
67 | "viv",
68 | "vivo",
69 | "vob",
70 | "vro",
71 | "wm",
72 | "wmv",
73 | "wmx",
74 | "wrap",
75 | "wvx",
76 | "wx",
77 | "x264",
78 | "xvid",
79 | ];
80 |
81 | module.exports = extensions;
82 |
--------------------------------------------------------------------------------
/renderer/types/index.js:
--------------------------------------------------------------------------------
1 | // ui
2 | export const SET_LANGUAGE = "SET_LANGUAGE";
3 | export const SHOW_NOTIFICATION = "SHOW_NOTIFICATION";
4 | export const LOG_DONATED = "LOG_DONATED";
5 | export const LOG_ABOUT = "LOG_ABOUT";
6 |
7 | // search
8 | export const RESET_SEARCH = "RESET_SEARCH";
9 | export const HIDE_SEARCH_PLACEHOLDER = "HIDE_SEARCH_PLACEHOLDER";
10 | export const SHOW_SEARCH_PLACEHOLDER = "SHOW_SEARCH_PLACEHOLDER";
11 | export const UPDATE_SEARCH_QUERY = "UPDATE_SEARCH_QUERY";
12 | export const SHOW_SEARCH_SPINNER = "SHOW_SEARCH_SPINNER";
13 | export const DOWNLOAD_COMPLETE = "DOWNLOAD_COMPLETE";
14 | export const SEARCH_BY_QUERY = "SEARCH_BY_QUERY";
15 | export const SEARCH_BY_FILES = "SEARCH_BY_FILES";
16 | export const DROP_FILES = "DROP_FILES";
17 | export const UPDATE_SEARCH_RESULTS = "UPDATE_SEARCH_RESULTS";
18 | export const INCREASE_SEARCH_ATTEMPTS = "INCREASE_SEARCH_ATTEMPTS";
19 | export const LOG_SEARCH_QUERY = "LOG_SEARCH_QUERY";
20 | export const SET_DROPPED_FILE_PATH = "SET_DROPPED_FILE_PATH";
21 | export const UPDATE_FILE_SEARCH_STATUS = "UPDATE_FILE_SEARCH_STATUS";
22 |
--------------------------------------------------------------------------------
/renderer/pages/about.js:
--------------------------------------------------------------------------------
1 | import electron from "electron";
2 | import { Component } from "react";
3 | import isDev from "electron-is-dev";
4 |
5 | import Layout from "../components/Layout";
6 | import TitleBar from "../components/TitleBar";
7 | import Meta from "../components/Meta";
8 | import Credits from "../components/Credits";
9 | import FooterAbout from "../components/FooterAbout";
10 |
11 | class About extends Component {
12 | constructor(props) {
13 | super(props);
14 |
15 | this.state = {
16 | version: "0.0.0",
17 | };
18 |
19 | this.remote = electron.remote || false;
20 | }
21 |
22 | componentWillMount() {
23 | if (!this.remote) {
24 | return;
25 | }
26 |
27 | let version;
28 |
29 | if (isDev) {
30 | version = this.remote.process.env.npm_package_version;
31 | } else {
32 | version = this.remote.app.getVersion();
33 | }
34 |
35 | this.setState({
36 | version,
37 | });
38 | }
39 |
40 | render() {
41 | return (
42 |
43 |
44 |
45 |
46 |
47 |
48 | );
49 | }
50 | }
51 |
52 | export default About;
53 |
--------------------------------------------------------------------------------
/renderer/actions/ui.js:
--------------------------------------------------------------------------------
1 | import * as types from "./../types";
2 | import { ipcRenderer } from "electron";
3 |
4 | import { logDonated, logAbout } from "./../utils/tracking";
5 | import { startSearch } from "./index";
6 |
7 | export const setLanguage = language => (dispatch, getState) => {
8 | const { search } = getState();
9 |
10 | dispatch({
11 | type: types.SET_LANGUAGE,
12 | payload: {
13 | language,
14 | },
15 | });
16 |
17 | // Store current language in settings
18 | ipcRenderer.send("setStore", "language", language);
19 |
20 | // If there are any results and user switches language, search again using new language
21 | if (search.results.length > 0 || search.files.length > 0) {
22 | dispatch(startSearch());
23 | }
24 | };
25 |
26 | export const showNotification = message => dispatch => {
27 | dispatch({
28 | type: types.SHOW_NOTIFICATION,
29 | });
30 |
31 | ipcRenderer.send("notification", message);
32 | };
33 |
34 | export const logDonatedButtonClicked = () => dispatch => {
35 | dispatch({
36 | type: types.LOG_DONATED,
37 | });
38 |
39 | logDonated();
40 | };
41 |
42 | export const logAboutWindowOpend = () => dispatch => {
43 | dispatch({
44 | type: types.LOG_ABOUT,
45 | });
46 |
47 | logAbout();
48 | };
49 |
--------------------------------------------------------------------------------
/main/sources.js:
--------------------------------------------------------------------------------
1 | const { multiDownload } = require("./download");
2 | const Caption = require("caption-core");
3 |
4 | const textSearch = async (...args) => {
5 | const { mainWindow } = global.windows;
6 |
7 | Caption.searchByQuery(...args)
8 | .on("fastest", results => {
9 | const subtitles = {
10 | results,
11 | isFinished: false,
12 | };
13 |
14 | mainWindow.webContents.send("results", subtitles);
15 | })
16 | .on("completed", results => {
17 | const subtitles = {
18 | results,
19 | isFinished: true,
20 | };
21 |
22 | mainWindow.webContents.send("results", subtitles);
23 | });
24 | };
25 |
26 | const markFilesNotFound = files => {
27 | const { mainWindow } = global.windows;
28 |
29 | files.forEach(file => {
30 | mainWindow.webContents.send("updateFileSearchStatus", {
31 | filePath: file.path,
32 | status: "not_found",
33 | });
34 | });
35 | };
36 |
37 | const fileSearch = async (files, ...args) => {
38 | Caption.searchByFiles(files, ...args).on("completed", results => {
39 | const foundFilePaths = results.map(({ file }) => file.path);
40 | const notFound = files.filter(({ path }) => !foundFilePaths.includes(path));
41 |
42 | markFilesNotFound(notFound);
43 | multiDownload(results);
44 | });
45 | };
46 |
47 | module.exports = { textSearch, fileSearch };
48 |
--------------------------------------------------------------------------------
/main/windows/check.js:
--------------------------------------------------------------------------------
1 | const { format } = require("url");
2 | const { BrowserWindow } = require("electron");
3 | const isDev = require("electron-is-dev");
4 | const { resolve } = require("app-root-path");
5 |
6 | const createCheckWindow = () => {
7 | const checkWindow = new BrowserWindow({
8 | width: 400,
9 | height: 130,
10 | title: "Looking for Updates...",
11 | center: true,
12 | show: false,
13 | resizable: false,
14 | minimizable: false,
15 | maximizable: false,
16 | closable: false,
17 | fullscreenable: false,
18 | backgroundColor: "#ECECEC",
19 | webPreferences: {
20 | backgroundThrottling: false,
21 | webSecurity: false,
22 | },
23 | });
24 |
25 | const devPath = "http://localhost:8000/check";
26 |
27 | const prodPath = format({
28 | pathname: resolve("renderer/out/check/index.html"),
29 | protocol: "file:",
30 | slashes: true,
31 | });
32 |
33 | const url = isDev ? devPath : prodPath;
34 | checkWindow.loadURL(url);
35 |
36 | return checkWindow;
37 | };
38 |
39 | const showCheckWindow = () => {
40 | const { checkWindow } = global.windows;
41 | checkWindow.show();
42 | checkWindow.focus();
43 | };
44 |
45 | const closeCheckWindow = () => {
46 | const { checkWindow } = global.windows;
47 | checkWindow.hide();
48 | };
49 |
50 | module.exports = {
51 | createCheckWindow,
52 | showCheckWindow,
53 | closeCheckWindow,
54 | };
55 |
--------------------------------------------------------------------------------
/renderer/components/Content.js:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types";
2 |
3 | import Drop from "../components/Drop";
4 | import ListEmpty from "../components/ListEmpty";
5 | import List from "../components/List";
6 |
7 | const Content = ({
8 | searchQuery = "",
9 | files = [],
10 | results = [],
11 | loading,
12 | onDrop,
13 | isWindows,
14 | }) => (
15 |
16 | {searchQuery !== "" && results.length === 0 && }
17 | {searchQuery === "" && files.length === 0 && }
18 | {files.length > 0 &&
}
19 | {results.length > 0 &&
}
20 |
21 |
36 |
37 | );
38 |
39 | Content.propTypes = {
40 | searchQuery: PropTypes.string,
41 | files: PropTypes.array,
42 | results: PropTypes.array,
43 | loading: PropTypes.bool,
44 | isWindows: PropTypes.bool,
45 | };
46 |
47 | Content.defaultProps = {
48 | searchQuery: "",
49 | files: [],
50 | results: [],
51 | isWindows: true,
52 | loading: false,
53 | };
54 |
55 | export default Content;
56 |
--------------------------------------------------------------------------------
/renderer/components/ListEmpty.js:
--------------------------------------------------------------------------------
1 | class ListEmpty extends React.Component {
2 | constructor(props) {
3 | super(props);
4 | this.state = { amount: 10 };
5 | this.setAmount = this.setAmount.bind(this);
6 | }
7 |
8 | setAmount() {
9 | if (typeof window !== "undefined") {
10 | const amount = Math.ceil((window.innerHeight - 125) / 30);
11 | this.setState({ amount });
12 | }
13 | }
14 |
15 | componentWillMount() {
16 | if (typeof window !== "undefined") {
17 | this.setAmount();
18 | window.addEventListener("resize", this.setAmount);
19 | }
20 | }
21 |
22 | componentWillUnMount() {
23 | if (typeof window !== "undefined") {
24 | window.removeEventListener("resize", this.setAmount);
25 | }
26 | }
27 |
28 | render() {
29 | const list = [...Array(this.state.amount).keys()];
30 |
31 | return (
32 |
33 | {list.map((li, index) => {
34 | return ;
35 | })}
36 |
37 |
56 |
57 | );
58 | }
59 | }
60 |
61 | export default ListEmpty;
62 |
--------------------------------------------------------------------------------
/.github/issue_template.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 | - [ ] I have searched the [issues](https://github.com/gielcobben/Caption/issues) of this repository and believe that this is not a duplicate.
10 |
11 | ## Expected Behavior
12 |
13 |
14 |
15 | ## Current Behavior
16 |
17 |
18 |
19 | ## Steps to Reproduce (for bugs)
20 |
21 |
22 | 1.
23 | 2.
24 | 3.
25 | 4.
26 |
27 | ## Context
28 |
29 |
30 |
31 | ## Your Environment
32 |
33 | | Tech | Version |
34 | |----------------|---------|
35 | | app version | |
36 | | OS | |
37 | | etc | |
--------------------------------------------------------------------------------
/renderer/components/Footer.js:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types";
2 | import LanguageToggle from "./LanguageToggle";
3 | import Info from "./Info";
4 |
5 | const Footer = ({
6 | results = [],
7 | language,
8 | loading,
9 | onLanguageChange,
10 | showResults,
11 | isFileSearch,
12 | totalFiles,
13 | foundFiles,
14 | }) => (
15 |
16 | onLanguageChange(event.target.value)}
19 | />
20 |
21 | {showResults && (
22 |
29 | )}
30 |
31 |
44 |
45 | );
46 |
47 | Footer.propTypes = {
48 | results: PropTypes.array.isRequired,
49 | language: PropTypes.string.isRequired,
50 | loading: PropTypes.bool.isRequired,
51 | onLanguageChange: PropTypes.func.isRequired,
52 | showResults: PropTypes.bool.isRequired,
53 | isFileSearch: PropTypes.bool.isRequired,
54 | totalFiles: PropTypes.number.isRequired,
55 | foundFiles: PropTypes.number.isRequired,
56 | };
57 |
58 | export default Footer;
59 |
--------------------------------------------------------------------------------
/main/windows/progress.js:
--------------------------------------------------------------------------------
1 | const { format } = require("url");
2 | const { BrowserWindow } = require("electron");
3 | const isDev = require("electron-is-dev");
4 | const { resolve } = require("app-root-path");
5 |
6 | const createProgressWindow = () => {
7 | const progressWindow = new BrowserWindow({
8 | width: 400,
9 | height: 130,
10 | title: "Updating Caption",
11 | center: true,
12 | show: false,
13 | resizable: false,
14 | minimizable: false,
15 | maximizable: false,
16 | closable: false,
17 | fullscreenable: false,
18 | backgroundColor: "#ECECEC",
19 | webPreferences: {
20 | backgroundThrottling: false,
21 | webSecurity: false,
22 | },
23 | });
24 |
25 | const devPath = "http://localhost:8000/progress";
26 |
27 | const prodPath = format({
28 | pathname: resolve("renderer/out/progress/index.html"),
29 | protocol: "file:",
30 | slashes: true,
31 | });
32 |
33 | const url = isDev ? devPath : prodPath;
34 | progressWindow.loadURL(url);
35 |
36 | return progressWindow;
37 | };
38 |
39 | const showProgressWindow = () => {
40 | const { progressWindow } = global.windows;
41 | progressWindow.show();
42 | progressWindow.focus();
43 | };
44 |
45 | const closeProgressWindow = (event, willQuitApp) => {
46 | const { progressWindow } = global.windows;
47 |
48 | if (willQuitApp) {
49 | global.windows.progressWindow = null;
50 | return;
51 | }
52 |
53 | event.preventDefault();
54 | progressWindow.hide();
55 | };
56 |
57 | module.exports = {
58 | createProgressWindow,
59 | showProgressWindow,
60 | closeProgressWindow,
61 | };
62 |
--------------------------------------------------------------------------------
/main/windows/about.js:
--------------------------------------------------------------------------------
1 | const { format } = require("url");
2 | const { BrowserWindow } = require("electron");
3 | const isDev = require("electron-is-dev");
4 | const { resolve } = require("app-root-path");
5 |
6 | const createAboutWindow = () => {
7 | const aboutWindow = new BrowserWindow({
8 | width: 260,
9 | height: 340,
10 | resizable: false,
11 | minimizable: false,
12 | maximizable: false,
13 | fullscreenable: false,
14 | vibrancy: "sidebar",
15 | title: "About",
16 | titleBarStyle: "hidden-inset",
17 | show: false,
18 | center: true,
19 | autoHideMenuBar: true,
20 | acceptFirstMouse: true,
21 | webPreferences: {
22 | backgroundThrottling: false,
23 | webSecurity: false,
24 | },
25 | });
26 |
27 | const devPath = "http://localhost:8000/about";
28 | const prodPath = format({
29 | pathname: resolve("renderer/out/about/index.html"),
30 | protocol: "file:",
31 | slashes: true,
32 | });
33 | const url = isDev ? devPath : prodPath;
34 | aboutWindow.loadURL(url);
35 |
36 | return aboutWindow;
37 | };
38 |
39 | const showAboutWindow = () => {
40 | const { aboutWindow, mainWindow } = global.windows;
41 | aboutWindow.show();
42 | aboutWindow.focus();
43 | mainWindow.webContents.send("logAbout");
44 | };
45 |
46 | const closeAboutWindow = (event, willQuitApp) => {
47 | const { aboutWindow } = global.windows;
48 | if (willQuitApp) {
49 | global.windows.aboutWindow = null;
50 | return;
51 | }
52 |
53 | event.preventDefault();
54 | aboutWindow.hide();
55 | };
56 |
57 | module.exports = { createAboutWindow, showAboutWindow, closeAboutWindow };
58 |
--------------------------------------------------------------------------------
/renderer/components/SearchField.js:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types";
2 |
3 | class SearchField extends React.Component {
4 | render() {
5 | const {
6 | value,
7 | placeholder,
8 | onSubmit,
9 | onChange,
10 | onFocus,
11 | onBlur,
12 | } = this.props;
13 |
14 | return (
15 |
52 | );
53 | }
54 | }
55 |
56 | SearchField.propTypes = {
57 | value: PropTypes.string.isRequired,
58 | placeholder: PropTypes.string.isRequired,
59 | onSubmit: PropTypes.func.isRequired,
60 | onChange: PropTypes.func.isRequired,
61 | onFocus: PropTypes.func.isRequired,
62 | onBlur: PropTypes.func.isRequired,
63 | };
64 |
65 | export default SearchField;
66 |
--------------------------------------------------------------------------------
/main/windows/main.js:
--------------------------------------------------------------------------------
1 | const { format } = require("url");
2 | const { BrowserWindow } = require("electron");
3 | const isDev = require("electron-is-dev");
4 | const { resolve } = require("app-root-path");
5 | const windowStateKeeper = require("electron-window-state");
6 | const touchBar = require("./../touchbar");
7 |
8 | const createMainWindow = () => {
9 | const windowState = windowStateKeeper({
10 | defaultWidth: 360,
11 | defaultHeight: 440,
12 | });
13 |
14 | const mainWindow = new BrowserWindow({
15 | width: windowState.width,
16 | height: windowState.height,
17 | x: windowState.x,
18 | y: windowState.y,
19 | title: "Caption",
20 | minWidth: 300,
21 | minHeight: 300,
22 | vibrancy: "sidebar",
23 | titleBarStyle: "hidden-inset",
24 | show: false,
25 | center: true,
26 | autoHideMenuBar: true,
27 | acceptFirstMouse: true,
28 | opacity: 1,
29 | webPreferences: {
30 | backgroundThrottling: false,
31 | webSecurity: false,
32 | },
33 | });
34 |
35 | windowState.manage(mainWindow);
36 |
37 | const devPath = "http://localhost:8000/start";
38 | const prodPath = format({
39 | pathname: resolve("renderer/out/start/index.html"),
40 | protocol: "file:",
41 | slashes: true,
42 | });
43 | const url = isDev ? devPath : prodPath;
44 | mainWindow.loadURL(url);
45 |
46 | mainWindow.webContents.on("did-finish-load", () => {
47 | mainWindow.show();
48 | mainWindow.focus();
49 | });
50 |
51 | // Add Touchbar support for MacOS
52 | if (process.platform === "darwin") {
53 | mainWindow.setTouchBar(touchBar);
54 | }
55 |
56 | return mainWindow;
57 | };
58 |
59 | module.exports = { createMainWindow };
60 |
--------------------------------------------------------------------------------
/renderer/components/SoftwareItem.js:
--------------------------------------------------------------------------------
1 | const arrowRight = (
2 |
9 |
10 |
11 | );
12 |
13 | const arrowDown = (
14 |
21 |
22 |
23 | );
24 |
25 | class SoftwareItem extends React.Component {
26 | constructor(props) {
27 | super(props);
28 |
29 | this.state = {
30 | open: false
31 | };
32 | }
33 |
34 | render() {
35 | const { pkg } = this.props;
36 | const { open } = this.state;
37 |
38 | return (
39 |
40 | {open && arrowDown}
41 | {!open && arrowRight}
42 | {
44 | this.setState({ open: !open });
45 | }}
46 | >
47 | {pkg.name}
48 |
49 |
50 | {open && {pkg.description}
}
51 |
52 |
74 |
75 | );
76 | }
77 | }
78 |
79 | export default SoftwareItem;
80 |
--------------------------------------------------------------------------------
/renderer/components/Credits.js:
--------------------------------------------------------------------------------
1 | // Packages
2 | import { shell } from "electron";
3 |
4 | // Components
5 | import software from "../data/software";
6 | import SoftwareItem from "../components/SoftwareItem";
7 |
8 | const Credits = () => (
9 |
10 | Special thanks to:
11 |
12 | shell.openExternal("https://twitter.com/rygu")}>
13 | Rick Wong
14 |
15 | shell.openExternal("https://twitter.com/gelissenhuub")}>
16 | Huub Gelissen
17 |
18 | shell.openExternal("https://www.opensubtitles.org/")}>
19 | Opensubtitles
20 |
21 | shell.openExternal("http://www.addic7ed.com/")}>
22 | Addic7ed
23 |
24 |
25 |
26 | 3rd party software
27 |
28 | {software.map((pkg, index) => {
29 | return ;
30 | })}
31 |
32 |
33 |
66 |
67 | );
68 |
69 | export default Credits;
70 |
--------------------------------------------------------------------------------
/main/donate.js:
--------------------------------------------------------------------------------
1 | const { dialog, shell } = require("electron");
2 |
3 | const getDownloadCount = () =>
4 | parseInt(global.store.get("download-count", 0), 10);
5 |
6 | const increaseDownloadCounter = () => {
7 | const currentCount = getDownloadCount();
8 | global.store.set("download-count", currentCount + 1);
9 | };
10 |
11 | const preventFuturePopups = () =>
12 | global.store.set("prevent-donate-popups", true);
13 |
14 | const allowFuturePopups = () =>
15 | global.store.set("prevent-donate-popups", false);
16 |
17 | const shouldHidePopups = () => global.store.get("prevent-donate-popups", false);
18 |
19 | const showDonatePopup = () => {
20 | if (shouldHidePopups()) {
21 | return;
22 | }
23 |
24 | const callback = (clickedButtonIndex, hideFuturePopups = false) => {
25 | const { mainWindow } = global.windows;
26 |
27 | if (clickedButtonIndex === 0) {
28 | shell.openExternal("https://www.paypal.me/gielcobben");
29 | mainWindow.webContents.send("logDonated");
30 | }
31 |
32 | if (hideFuturePopups) {
33 | preventFuturePopups();
34 | }
35 | };
36 |
37 | dialog.showMessageBox(
38 | {
39 | buttons: ["Donate", "Later"],
40 | defaultId: 0,
41 | title: "Thank you for using Caption!",
42 | message:
43 | "Thanks for using Caption! Caption is and will always be free. If you enjoy using it, please consider a donation to the authors.",
44 | checkboxLabel: "Don't show this again",
45 | },
46 | callback,
47 | );
48 | };
49 |
50 | const triggerDonateWindow = () => {
51 | increaseDownloadCounter();
52 | const currentDownloadCount = getDownloadCount();
53 |
54 | if (currentDownloadCount >= 9 && currentDownloadCount % 3 === 0) {
55 | showDonatePopup();
56 | }
57 | };
58 |
59 | module.exports = { triggerDonateWindow, allowFuturePopups };
60 |
--------------------------------------------------------------------------------
/main/utils.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 | const path = require("path");
3 | const movieExtension = require("./data/extensions");
4 |
5 | const transform = filePaths =>
6 | filePaths.map(file => {
7 | const extension = file.substr(file.lastIndexOf(".") + 1);
8 | const { size } = fs.statSync(file);
9 | const name = file.replace(/^.*[\\\/]/, "");
10 |
11 | return {
12 | extension,
13 | size,
14 | name,
15 | path: file,
16 | status: "loading",
17 | };
18 | });
19 |
20 | const checkExtension = file => {
21 | const extension = file.substr(file.lastIndexOf(".") + 1);
22 | return movieExtension.indexOf(extension) > 0;
23 | };
24 |
25 | const readDir = dir =>
26 | fs
27 | .readdirSync(dir)
28 | .filter(file => {
29 | const isDirectory = fs.statSync(path.join(dir, file)).isDirectory();
30 |
31 | if (isDirectory) {
32 | return true;
33 | }
34 |
35 | return checkExtension(file);
36 | })
37 | .reduce((files, file) => {
38 | const isDirectory = fs.statSync(path.join(dir, file)).isDirectory();
39 |
40 | if (isDirectory) {
41 | return files.concat(readDir(path.join(dir, file)));
42 | }
43 |
44 | return files.concat(path.join(dir, file));
45 | }, []);
46 |
47 | const processFiles = droppedItems => {
48 | const { mainWindow } = global.windows;
49 | const filePaths = [];
50 |
51 | droppedItems.map(item => {
52 | if (fs.statSync(item).isDirectory()) {
53 | filePaths.push(...readDir(item));
54 | } else if (checkExtension(item)) {
55 | filePaths.push(item);
56 | }
57 |
58 | return false;
59 | });
60 |
61 | const transformedObject = transform(filePaths);
62 | mainWindow.webContents.send("processedFiles", transformedObject);
63 | };
64 |
65 | module.exports = { processFiles };
66 |
--------------------------------------------------------------------------------
/renderer/components/Drop.js:
--------------------------------------------------------------------------------
1 | class Drop extends React.Component {
2 | constructor(props) {
3 | super(props);
4 | this.state = { dragging: false };
5 | this.onDragEnter = this.onDragEnter.bind(this);
6 | this.onDragLeave = this.onDragLeave.bind(this);
7 | this.onDragOver = this.onDragOver.bind(this);
8 | }
9 |
10 | onDragEnter() {
11 | this.setState({
12 | dragging: true,
13 | });
14 | }
15 |
16 | onDragLeave() {
17 | if (this.state.dragging) {
18 | this.setState({
19 | dragging: false,
20 | });
21 | }
22 | }
23 |
24 | onDragOver() {
25 | if (!this.state.dragging) {
26 | this.setState({
27 | dragging: true,
28 | });
29 | }
30 | }
31 |
32 | render() {
33 | const { dragging } = this.state;
34 |
35 | return (
36 |
42 |
43 |
Drop an episode or season.
44 |
45 |
46 |
77 |
78 | );
79 | }
80 | }
81 |
82 | export default Drop;
83 |
--------------------------------------------------------------------------------
/renderer/components/Search.js:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types";
2 | import { shell } from "electron";
3 | import FilePath from "./FilePath";
4 | import SearchField from "./SearchField";
5 |
6 | class Search extends React.Component {
7 | render() {
8 | const {
9 | value,
10 | placeholder,
11 | dropFilePath,
12 | dropFilePathClean,
13 | onReset,
14 | onSubmit,
15 | onChange,
16 | onFocus,
17 | onBlur,
18 | } = this.props;
19 |
20 | return (
21 |
22 | {dropFilePath && (
23 |
28 | )}
29 |
30 | {!dropFilePath && (
31 | {
39 | this.searchField = searchField;
40 | }}
41 | />
42 | )}
43 |
44 |
55 |
56 | );
57 | }
58 | }
59 |
60 | Search.propTypes = {
61 | value: PropTypes.string.isRequired,
62 | placeholder: PropTypes.string.isRequired,
63 | dropFilePath: PropTypes.string.isRequired,
64 | dropFilePathClean: PropTypes.string,
65 | onReset: PropTypes.func.isRequired,
66 | onSubmit: PropTypes.func.isRequired,
67 | onChange: PropTypes.func.isRequired,
68 | onFocus: PropTypes.func.isRequired,
69 | onBlur: PropTypes.func.isRequired,
70 | };
71 |
72 | Search.defaultProps = {
73 | dropFilePathClean: undefined,
74 | };
75 |
76 | export default Search;
77 |
--------------------------------------------------------------------------------
/main/updater.js:
--------------------------------------------------------------------------------
1 | const { dialog, ipcMain } = require("electron");
2 | const isDev = require("electron-is-dev");
3 | const { autoUpdater } = require("electron-updater");
4 | const { showCheckWindow, closeCheckWindow } = require("./windows/check");
5 | const { showProgressWindow } = require("./windows/progress");
6 |
7 | // Functions
8 | const cancelUpdater = () => {
9 | const { progressWindow } = global.windows;
10 | const { cancellationToken } = global.updater;
11 |
12 | cancellationToken.cancel();
13 | progressWindow.hide();
14 | };
15 |
16 | const checkForUpdates = async () => {
17 | const checking = await autoUpdater.checkForUpdates();
18 | const { cancellationToken } = checking;
19 |
20 | global.updater = {
21 | cancellationToken,
22 | onStartup: false,
23 | };
24 | };
25 |
26 | // IPC Events
27 | ipcMain.on("cancelUpdate", event => {
28 | cancelUpdater();
29 | });
30 |
31 | ipcMain.on("installUpdate", event => {
32 | autoUpdater.quitAndInstall();
33 | });
34 |
35 | // UPDATER
36 | autoUpdater.allowPrerelease = isDev;
37 | autoUpdater.autoDownload = false;
38 |
39 | autoUpdater.on("checking-for-update", () => {
40 | const { onStartup } = global.updater;
41 | if (!onStartup) {
42 | showCheckWindow();
43 | }
44 | });
45 |
46 | autoUpdater.on("update-available", info => {
47 | const { cancellationToken } = global.updater;
48 | closeCheckWindow();
49 | showProgressWindow();
50 | autoUpdater.downloadUpdate(cancellationToken);
51 | });
52 |
53 | autoUpdater.on("update-not-available", info => {
54 | const { onStartup } = global.updater;
55 | closeCheckWindow();
56 |
57 | if (!onStartup) {
58 | const options = {
59 | type: "info",
60 | message: "Caption is up to date",
61 | detail: "It looks like you're already rocking the latest version!",
62 | };
63 |
64 | dialog.showMessageBox(null, options);
65 | }
66 | });
67 |
68 | autoUpdater.on("error", (event, error) => {
69 | console.log(error);
70 | closeCheckWindow();
71 | });
72 |
73 | autoUpdater.on("download-progress", progressObj => {
74 | const { progressWindow } = global.windows;
75 | progressWindow.webContents.send("progress", progressObj);
76 | });
77 |
78 | autoUpdater.on("update-downloaded", info => {
79 | console.log(`Update downloaded; will install in 5 seconds.`, info);
80 | });
81 |
82 | module.exports = { checkForUpdates };
83 |
--------------------------------------------------------------------------------
/main/download.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 | const { dialog } = require("electron");
3 | const notification = require("./notification");
4 | const Caption = require("caption-core");
5 | const { triggerDonateWindow } = require("./donate");
6 |
7 | const multiDownload = files => {
8 | const resultSet = [];
9 | const { mainWindow } = global.windows;
10 |
11 | try {
12 | const downloadFiles = files.map(item =>
13 | new Promise(resolve => {
14 | const downloadLocation = path.dirname(item.file.path);
15 | const originalFileName = item.file.name;
16 | const subtitleFilename = originalFileName.replace(/\.[^/.]+$/, "");
17 |
18 | return Caption.download(
19 | item,
20 | item.source,
21 | `${downloadLocation}/${subtitleFilename}.srt`,
22 | ).then(() => {
23 | resultSet.push(`${downloadLocation}/${subtitleFilename}.srt`);
24 | mainWindow.webContents.send("updateFileSearchStatus", {
25 | filePath: item.file.path,
26 | status: "done",
27 | });
28 | resolve();
29 | });
30 | }));
31 |
32 | Promise.all(downloadFiles).then(() => {
33 | const message =
34 | resultSet.length > 0
35 | ? `${resultSet.length} subtitles have been successfully downloaded!`
36 | : "Could not find any subtitles.";
37 |
38 | notification(message);
39 | mainWindow.webContents.send("allFilesDownloaded");
40 |
41 | triggerDonateWindow();
42 | });
43 | } catch (err) {
44 | console.log("error", err);
45 | }
46 | };
47 |
48 | const singleDownload = async item => {
49 | const hasExtension = item.name.includes(".srt");
50 | const filename = hasExtension ? item.name : `${item.name}.srt`;
51 | const { mainWindow } = global.windows;
52 | const saveToPath = await new Promise(resolve => {
53 | dialog.showSaveDialog(
54 | mainWindow,
55 | {
56 | title: "Download",
57 | defaultPath: filename,
58 | },
59 | resolve,
60 | );
61 | });
62 |
63 | if (!saveToPath) {
64 | return;
65 | }
66 |
67 | try {
68 | Caption.download(item, item.source, saveToPath)
69 | .then(() => {
70 | notification(`${item.name} is successfully downloaded!`);
71 | mainWindow.webContents.send("singleDownloadSuccesfull", item);
72 |
73 | triggerDonateWindow();
74 | })
75 | .catch(err => console.log("error", err));
76 | } catch (err) {
77 | console.log("err", err);
78 | }
79 | };
80 |
81 | module.exports = { multiDownload, singleDownload };
82 |
--------------------------------------------------------------------------------
/renderer/pages/progress.js:
--------------------------------------------------------------------------------
1 | import { ipcRenderer } from "electron";
2 | import { fileSizeReadable } from "../utils";
3 | import Layout from "../components/Layout";
4 | import Logo from "../components/Logo";
5 |
6 | class Progress extends React.Component {
7 | constructor(props) {
8 | super(props);
9 | this.state = {
10 | total: 0,
11 | transferred: 0,
12 | percent: 0,
13 | };
14 | }
15 |
16 | componentDidMount() {
17 | ipcRenderer.on("progress", (event, { transferred, total, percent }) => {
18 | this.setState({
19 | percent,
20 | total,
21 | transferred,
22 | });
23 | });
24 | }
25 |
26 | render() {
27 | const { total, transferred, percent } = this.state;
28 |
29 | return (
30 |
31 |
32 |
33 |
34 | {percent !== 100 &&
Downloading update... }
35 | {percent === 100 &&
Ready to Install }
36 |
37 | {percent !== 100 && (
38 |
39 |
40 | {fileSizeReadable(transferred)} of {fileSizeReadable(total)}
41 |
42 | ipcRenderer.send("cancelUpdate")}>
43 | Cancel
44 |
45 |
46 | )}
47 | {percent === 100 && (
48 |
49 | ipcRenderer.send("installUpdate")}>
50 | Install and Relaunch
51 |
52 |
53 | )}
54 |
55 |
56 |
57 |
96 |
97 | );
98 | }
99 | }
100 |
101 | export default Progress;
102 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["airbnb"],
3 | "env": {
4 | "browser": true,
5 | "jest": true,
6 | "node": true
7 | },
8 | "globals": {
9 | __CLIENT__: true,
10 | __DEVELOPMENT__: true,
11 | __PRODUCTION__: true
12 | },
13 | "parser": "babel-eslint",
14 | "rules": {
15 | "class-methods-use-this": "warn",
16 | "global-require": "off",
17 | "indent": ["error", 2, { "SwitchCase": 1 }],
18 | "key-spacing": [
19 | "error",
20 | {
21 | "mode": "minimum",
22 | "beforeColon": false,
23 | "afterColon": true
24 | }
25 | ],
26 | "react/react-in-jsx-scope": "off",
27 | "arrow-parens": "off",
28 | "react/jsx-curly-brace-presence": "off",
29 | "react/jsx-wrap-multilines": "off",
30 | "max-len": "off",
31 | "new-cap": ["error", { "capIsNewExceptions": ["Immutable"] }],
32 | "no-confusing-arrow": ["error", { "allowParens": true }],
33 | "no-fallthrough": ["error", { "commentPattern": "break[\\s\\w]*omitted" }],
34 | "no-lonely-if": "warn",
35 | "no-mixed-operators": "warn",
36 | "no-multi-spaces": [
37 | "error",
38 | {
39 | "exceptions": {
40 | "VariableDeclarator": true,
41 | "AssignmentExpression": true,
42 | "ImportDeclaration": true
43 | }
44 | }
45 | ],
46 | "no-plusplus": "off",
47 | "no-underscore-dangle": "off",
48 | "no-use-before-define": "warn",
49 | "no-useless-escape": "warn",
50 | "quotes": 0,
51 | // React rules
52 | "react/forbid-prop-types": "off",
53 | "react/jsx-filename-extension": [
54 | "error",
55 | { "extensions": [".jsx", ".js"] }
56 | ],
57 | "react/jsx-first-prop-new-line": "off",
58 | "react/no-unused-prop-types": "off",
59 | "react/self-closing-comp": ["error", { "component": true, "html": false }],
60 | // jsx-a11y
61 | "jsx-a11y/img-has-alt": "warn",
62 | "jsx-a11y/label-has-for": "warn",
63 | "jsx-a11y/no-static-element-interactions": "warn",
64 | // Import rules
65 | "import/default": "error",
66 | "import/named": "error",
67 | "import/namespace": "error",
68 | "import/no-extraneous-dependencies": "warn",
69 | "import/no-named-as-default": "warn",
70 | "import/prefer-default-export": "warn",
71 | "jsx-a11y/href-no-hash": "off",
72 | "jsx-a11y/anchor-is-valid": ["warn", { "aspects": ["invalidHref"] }],
73 | "jsx-a11y/no-noninteractive-element-interactions": "off"
74 | },
75 | "plugins": ["react", "import"],
76 | "settings": {
77 | "import/ignore": ["node_modules", ".(scss|less|css|svg)$"],
78 | "import/parser": "babel-eslint",
79 | "import/resolver": {
80 | "webpack": {
81 | "config": {
82 | "resolve": {
83 | "extensions": ["", ".js", ".jsx", ".json"],
84 | "modulesDirectories": ["node_modules", "src"]
85 | }
86 | }
87 | }
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ## Contributing to Caption
2 |
3 | 1. Fork this repository to your own GitHub account and then clone it to your local device
4 | 2. Install the dependencies: npm install
5 | 3. Run the app by building the code and watch for changes: npm start
6 | 4. Build the actual app for all platforms (Mac, Windows and Linux): `npm run dist`
7 |
8 | ## Financial contributions
9 |
10 | We also welcome financial contributions in full transparency on our [open collective](https://opencollective.com/caption).
11 | Anyone can file an expense. If the expense makes sense for the development of the community, it will be "merged" in the ledger of our open collective by the core contributors and the person who filed the expense will be reimbursed.
12 |
13 | ### Contributors
14 |
15 | Thank you to all the people who have already contributed to Caption!
16 |
17 |
18 |
19 | ### Backers
20 |
21 | Thank you to all our backers! [[Become a backer](https://opencollective.com/caption#backer)]
22 |
23 |
24 |
25 |
26 | ### Sponsors
27 |
28 | Thank you to all our sponsors! (please ask your company to also support this open source project by [becoming a sponsor](https://opencollective.com/caption#sponsor))
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/renderer/reducers/search.js:
--------------------------------------------------------------------------------
1 | /* eslint no-case-declarations: 0 */
2 |
3 | import * as types from "./../types";
4 |
5 | const initialState = {
6 | files: [],
7 | results: [],
8 | loading: false,
9 | searchCompleted: true,
10 | searchAttempts: 0,
11 | searchQuery: "",
12 | placeholder: "Search for a show...",
13 | dropFilePath: "",
14 | };
15 |
16 | export default function reducer(state = initialState, action) {
17 | switch (action.type) {
18 | case types.RESET_SEARCH:
19 | return {
20 | ...state,
21 | files: [],
22 | results: [],
23 | loading: false,
24 | searchCompleted: true,
25 | searchAttempts: 0,
26 | searchQuery: "",
27 | placeholder: "Search for a show...",
28 | dropFilePath: "",
29 | };
30 | case types.SHOW_SEARCH_PLACEHOLDER:
31 | return {
32 | ...state,
33 | placeholder: "Search for a show...",
34 | };
35 | case types.HIDE_SEARCH_PLACEHOLDER:
36 | return {
37 | ...state,
38 | placeholder: "",
39 | };
40 | case types.UPDATE_SEARCH_QUERY:
41 | return {
42 | ...state,
43 | files: [],
44 | results: [],
45 | searchQuery: action.payload.query,
46 | dropFilePath: "",
47 | };
48 | case types.SHOW_SEARCH_SPINNER:
49 | return {
50 | ...state,
51 | loading: true,
52 | searchCompleted: false,
53 | };
54 | case types.DOWNLOAD_COMPLETE:
55 | return {
56 | ...state,
57 | loading: false,
58 | searchCompleted: true,
59 | };
60 | case types.UPDATE_SEARCH_RESULTS:
61 | return {
62 | ...state,
63 | loading: false,
64 | results: action.payload.results,
65 | searchCompleted: action.payload.searchCompleted,
66 | };
67 | case types.DROP_FILES:
68 | return {
69 | ...state,
70 | searchQuery: "",
71 | files: action.payload.files,
72 | };
73 | case types.INCREASE_SEARCH_ATTEMPTS:
74 | return {
75 | ...state,
76 | searchAttempts: action.payload.attempts,
77 | };
78 | case types.SET_DROPPED_FILE_PATH:
79 | return {
80 | ...state,
81 | dropFilePath: action.payload.realPath,
82 | dropFilePathClean: action.payload.cleanPath,
83 | searchQuery: "",
84 | };
85 | case types.UPDATE_FILE_SEARCH_STATUS:
86 | const correspondingFile = state.files.find(file => file.path === action.payload.filePath);
87 | const otherFiles = state.files.filter(file => file.path !== action.payload.filePath);
88 |
89 | return {
90 | ...state,
91 | files: [
92 | ...otherFiles,
93 | {
94 | ...correspondingFile,
95 | status: action.payload.status,
96 | },
97 | ],
98 | };
99 | default:
100 | return state;
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/renderer/data/languages.js:
--------------------------------------------------------------------------------
1 | const languages = [
2 | { code: "afr", name: "Afrikaans" },
3 | { code: "alb", name: "Albanian" },
4 | { code: "ara", name: "Arabic" },
5 | { code: "arm", name: "Armenian" },
6 | { code: "aze", name: "Azerbaijani" },
7 | { code: "baq", name: "Basque" },
8 | { code: "bel", name: "Belarusian" },
9 | { code: "ben", name: "Bengali" },
10 | { code: "bos", name: "Bosnian" },
11 | { code: "bre", name: "Breton" },
12 | { code: "bul", name: "Bulgarian" },
13 | { code: "bur", name: "Burmese" },
14 | { code: "cat", name: "Catalan" },
15 | { code: "chi", name: "Chinese (simplified)" },
16 | { code: "zht", name: "Chinese (traditional)" },
17 | { code: "zhe", name: "Chinese bilingual" },
18 | { code: "hrv", name: "Croatian" },
19 | { code: "cze", name: "Czech" },
20 | { code: "dan", name: "Danish" },
21 | { code: "dut", name: "Dutch" },
22 | { code: "eng", name: "English" },
23 | { code: "epo", name: "Esperanto" },
24 | { code: "est", name: "Estonian" },
25 | { code: "eus", name: "Euskera" },
26 | { code: "fin", name: "Finnish" },
27 | { code: "fre", name: "French" },
28 | { code: "glg", name: "Galician" },
29 | { code: "geo", name: "Georgian" },
30 | { code: "ger", name: "German" },
31 | { code: "ell", name: "Greek" },
32 | { code: "heb", name: "Hebrew" },
33 | { code: "hin", name: "Hindi" },
34 | { code: "hun", name: "Hungarian" },
35 | { code: "ice", name: "Icelandic" },
36 | { code: "ind", name: "Indonesian" },
37 | { code: "ita", name: "Italian" },
38 | { code: "jpn", name: "Japanese" },
39 | { code: "kaz", name: "Kazakh" },
40 | { code: "khm", name: "Khmer" },
41 | { code: "kor", name: "Korean" },
42 | { code: "lav", name: "Latvian" },
43 | { code: "lit", name: "Lithuanian" },
44 | { code: "ltz", name: "Luxembourgish" },
45 | { code: "mac", name: "Macedonian" },
46 | { code: "may", name: "Malay" },
47 | { code: "mal", name: "Malayalam" },
48 | { code: "mni", name: "Manipuri" },
49 | { code: "mon", name: "Mongolian" },
50 | { code: "mne", name: "Montenegrin" },
51 | { code: "nor", name: "Norwegian" },
52 | { code: "oci", name: "Occitan" },
53 | { code: "per", name: "Persian" },
54 | { code: "pol", name: "Polish" },
55 | { code: "por", name: "Portuguese" },
56 | { code: "pob", name: "Portuguese (BR)" },
57 | { code: "rum", name: "Romanian" },
58 | { code: "rus", name: "Russian" },
59 | { code: "scc", name: "Serbian" },
60 | { code: "sin", name: "Sinhalese" },
61 | { code: "slo", name: "Slovak" },
62 | { code: "slv", name: "Slovenian" },
63 | { code: "spa", name: "Spanish" },
64 | { code: "swa", name: "Swahili" },
65 | { code: "swe", name: "Swedish" },
66 | { code: "syr", name: "Syriac" },
67 | { code: "tgl", name: "Tagalog" },
68 | { code: "tam", name: "Tamil" },
69 | { code: "tel", name: "Telugu" },
70 | { code: "tha", name: "Thai" },
71 | { code: "tur", name: "Turkish" },
72 | { code: "ukr", name: "Ukrainian" },
73 | { code: "urd", name: "Urdu" },
74 | { code: "vie", name: "Vietnamese" },
75 | ];
76 |
77 | export default languages;
78 |
--------------------------------------------------------------------------------
/renderer/components/ListItem.js:
--------------------------------------------------------------------------------
1 | import { fileSizeReadable } from "../utils";
2 |
3 | const ListItem = ({
4 | item,
5 | selected,
6 | onClick,
7 | onDoubleClick,
8 | onContextMenu,
9 | }) => (
10 |
16 | {item.name}
17 |
18 | {item.size && (
19 |
20 | {fileSizeReadable(item.size)}
21 |
22 | {item.extension}
23 |
24 | )}
25 |
26 | {item.status === "done" && (
27 |
28 | {" "}
29 |
30 |
34 |
35 |
36 | )}
37 |
38 | {item.status === "not_found" && (
39 |
40 | {" "}
41 |
42 |
46 |
47 |
48 | )}
49 |
50 |
120 |
121 | );
122 |
123 | export default ListItem;
124 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
6 |
7 | ## Our Standards
8 |
9 | Examples of behavior that contributes to creating a positive environment include:
10 |
11 | * Using welcoming and inclusive language
12 | * Being respectful of differing viewpoints and experiences
13 | * Gracefully accepting constructive criticism
14 | * Focusing on what is best for the community
15 | * Showing empathy towards other community members
16 |
17 | Examples of unacceptable behavior by participants include:
18 |
19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances
20 | * Trolling, insulting/derogatory comments, and personal or political attacks
21 | * Public or private harassment
22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission
23 | * Other conduct which could reasonably be considered inappropriate in a professional setting
24 |
25 | ## Our Responsibilities
26 |
27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
28 |
29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
30 |
31 | ## Scope
32 |
33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
34 |
35 | ## Enforcement
36 |
37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at g.cobben@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
38 |
39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
40 |
41 | ## Attribution
42 |
43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
44 |
45 | [homepage]: http://contributor-covenant.org
46 | [version]: http://contributor-covenant.org/version/1/4/
47 |
--------------------------------------------------------------------------------
/renderer/components/Layout.js:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 |
3 | const Layout = ({ children }) => (
4 |
5 |
6 | Caption
7 |
8 |
9 |
10 |
11 | {children}
12 |
13 |
163 |
164 | );
165 |
166 | export default Layout;
167 |
--------------------------------------------------------------------------------
/main/index.js:
--------------------------------------------------------------------------------
1 | const Store = require("electron-store");
2 | const prepareNext = require("electron-next");
3 | const { app, ipcMain, dialog } = require("electron");
4 | const { moveToApplications } = require("electron-lets-move");
5 |
6 | const buildMenu = require("./menu");
7 | const initSettings = require("./settings");
8 | const notification = require("./notification");
9 | const { processFiles } = require("./utils");
10 | const { checkForUpdates } = require("./updater");
11 | const { singleDownload } = require("./download");
12 | const { textSearch, fileSearch } = require("./sources");
13 |
14 | // Windows
15 | const { createMainWindow } = require("./windows/main");
16 | const { createAboutWindow, closeAboutWindow } = require("./windows/about");
17 | const { createCheckWindow, closeCheckWindow } = require("./windows/check");
18 | const {
19 | createProgressWindow,
20 | closeProgressWindow,
21 | } = require("./windows/progress");
22 |
23 | const store = new Store();
24 |
25 | // Window variables
26 | let willQuitApp = false;
27 |
28 | // Functions
29 | const downloadSubtitle = item => {
30 | if (!item) {
31 | return false;
32 | }
33 |
34 | return singleDownload(item);
35 | };
36 |
37 | const showErrorDialog = online => {
38 | if (!online) {
39 | dialog.showErrorBox(
40 | "Oops, something went wrong",
41 | "It seems like your computer is offline! Please connect to the internet to use Caption.",
42 | );
43 | }
44 | };
45 |
46 | // App Events
47 | app.on("before-quit", () => {
48 | global.windows.checkWindow = null;
49 | willQuitApp = true;
50 | });
51 |
52 | app.on("window-all-closed", () => {
53 | if (process.platform !== "darwin") {
54 | app.quit();
55 | }
56 | });
57 |
58 | app.on("activate", () => {
59 | const { mainWindow } = global.windows;
60 |
61 | if (mainWindow === null) {
62 | global.windows.mainWindow = createMainWindow();
63 | }
64 | });
65 |
66 | app.on("ready", async () => {
67 | await prepareNext("./renderer");
68 |
69 | if (!store.get("moved")) {
70 | await moveToApplications();
71 | store.set("moved", true);
72 | }
73 |
74 | global.store = store;
75 |
76 | global.updater = {
77 | onStartup: true,
78 | };
79 |
80 | // Windows
81 | global.windows = {
82 | mainWindow: createMainWindow(),
83 | aboutWindow: createAboutWindow(),
84 | checkWindow: createCheckWindow(),
85 | progressWindow: createProgressWindow(),
86 | };
87 |
88 | const {
89 | mainWindow,
90 | aboutWindow,
91 | checkWindow,
92 | progressWindow,
93 | } = global.windows;
94 |
95 | mainWindow.on("close", () => {
96 | global.windows = null;
97 | app.exit();
98 | app.quit();
99 | });
100 | aboutWindow.on("close", event => closeAboutWindow(event, willQuitApp));
101 | checkWindow.on("close", event => closeCheckWindow(event, willQuitApp));
102 | progressWindow.on("close", event => closeProgressWindow(event, willQuitApp));
103 |
104 | // Setup
105 | buildMenu();
106 | initSettings();
107 | checkForUpdates();
108 |
109 | // IPC events
110 | ipcMain.on("textSearch", (event, query, language) => {
111 | textSearch(query, language, "all");
112 | });
113 |
114 | ipcMain.on("fileSearch", (event, files, language) => {
115 | fileSearch(files, language, "best");
116 | });
117 |
118 | ipcMain.on("downloadSubtitle", (event, item) => {
119 | downloadSubtitle(item);
120 | });
121 |
122 | ipcMain.on("online", (event, online) => {
123 | showErrorDialog(online);
124 | });
125 |
126 | ipcMain.on("notification", (event, message) => {
127 | notification(message);
128 | });
129 |
130 | ipcMain.on("processFiles", (event, droppedItems) => {
131 | processFiles(droppedItems);
132 | });
133 | });
134 |
--------------------------------------------------------------------------------
/main/menu.js:
--------------------------------------------------------------------------------
1 | const { app, shell, Menu } = require("electron");
2 | const isDev = require("electron-is-dev");
3 | const { checkForUpdates } = require("./updater");
4 | const { showAboutWindow } = require("./windows/about");
5 | const { allowFuturePopups: allowDonationPopups } = require("./donate");
6 | const { platform } = require("os");
7 |
8 | const isWindows = platform() === "win32";
9 | const isLinux = platform() === "linux";
10 |
11 | const helpMenu = [
12 | {
13 | label: "Donate",
14 | click: () => {
15 | const { mainWindow } = global.windows;
16 | shell.openExternal("https://www.paypal.me/gielcobben");
17 | mainWindow.webContents.send("logDonated");
18 | },
19 | },
20 | {
21 | label: "Learn More",
22 | click: () => shell.openExternal("https://getcaption.co/"),
23 | },
24 | {
25 | label: "Support",
26 | click: () => shell.openExternal("https://twitter.com/gielcobben"),
27 | },
28 | {
29 | label: "Report Issue",
30 | click: () =>
31 | shell.openExternal("https://github.com/gielcobben/caption/issues/new"),
32 | },
33 | {
34 | label: "Search Issues",
35 | click: () =>
36 | shell.openExternal("https://github.com/gielcobben/Caption/issues"),
37 | },
38 | ];
39 |
40 | if (isWindows || isLinux) {
41 | helpMenu.splice(0, 0, {
42 | label: "Check for updates...",
43 | click: () => checkForUpdates(),
44 | });
45 | }
46 |
47 | const buildMenu = () => {
48 | const template = [
49 | {
50 | label: "Edit",
51 | submenu: [
52 | { role: "undo" },
53 | { role: "redo" },
54 | { type: "separator" },
55 | { role: "cut" },
56 | { role: "copy" },
57 | { role: "paste" },
58 | { role: "pasteandmatchstyle" },
59 | { role: "delete" },
60 | { role: "selectall" },
61 | ],
62 | },
63 | {
64 | label: "View",
65 | submenu: isDev
66 | ? [
67 | { role: "reload" },
68 | { role: "forcereload" },
69 | { role: "toggledevtools" },
70 | { type: "separator" },
71 | {
72 | label: "Allow donation popups",
73 | click: () => allowDonationPopups(),
74 | },
75 | { type: "separator" },
76 | ]
77 | : [{ role: "togglefullscreen" }],
78 | },
79 | {
80 | role: "window",
81 | submenu: [{ role: "minimize" }, { role: "close" }],
82 | },
83 | {
84 | role: "help",
85 | submenu: helpMenu,
86 | },
87 | ];
88 |
89 | if (process.platform === "darwin") {
90 | template.unshift({
91 | label: app.getName(),
92 | submenu: [
93 | {
94 | label: `About ${app.getName()}`,
95 | click: () => showAboutWindow(),
96 | },
97 | { label: "Check for updates...", click: () => checkForUpdates() },
98 | { type: "separator" },
99 | { role: "services", submenu: [] },
100 | { type: "separator" },
101 | { role: "hide" },
102 | { role: "hideothers" },
103 | { role: "unhide" },
104 | { type: "separator" },
105 | { role: "quit" },
106 | ],
107 | });
108 |
109 | // Edit menu
110 | template[1].submenu.push(
111 | { type: "separator" },
112 | {
113 | label: "Speech",
114 | submenu: [{ role: "startspeaking" }, { role: "stopspeaking" }],
115 | },
116 | );
117 |
118 | // Window menu
119 | template[3].submenu = [
120 | { role: "close" },
121 | { role: "minimize" },
122 | { type: "separator" },
123 | { role: "front" },
124 | ];
125 | }
126 |
127 | const menu = Menu.buildFromTemplate(template);
128 | Menu.setApplicationMenu(menu);
129 | };
130 |
131 | module.exports = buildMenu;
132 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "caption",
3 | "description": "Find the right subtitles. Easy.",
4 | "author": {
5 | "name": "Giel Cobben",
6 | "email": "g.cobben@gmail.com",
7 | "url": "https://gielcobben.com"
8 | },
9 | "contributors": [
10 | {
11 | "name": "Vernon de Goede",
12 | "email": "info@vernondegoede.com",
13 | "url": "https://github.com/vernondegoede"
14 | },
15 | {
16 | "name": "Giel Cobben",
17 | "email": "g.cobben@gmail.com",
18 | "url": "https://github.com/gielcobben"
19 | }
20 | ],
21 | "productName": "Caption",
22 | "version": "2.0.1",
23 | "main": "main/index.js",
24 | "license": "MIT",
25 | "repository": "gielcobben/Caption",
26 | "scripts": {
27 | "start": "electron --inspect=5858 .",
28 | "build": "npm run test && next build renderer && next export renderer",
29 | "dist:mac": "export CSC_IDENTITY_AUTO_DISCOVERY=\"true\"; node -r dotenv/config node_modules/.bin/build --mac -p always",
30 | "dist:windows": "export CSC_NAME=\"Defringe\"; export CSC_IDENTITY_AUTO_DISCOVERY=\"false\"; export CSC_LINK=\"~/Defringe.p12\"; node -r dotenv/config node_modules/.bin/build --windows -p always",
31 | "dist:linux": "node -r dotenv/config node_modules/.bin/build --linux -p always",
32 | "dist": "npm run build && npm run dist:mac && npm run dist:windows && npm run dist:linux",
33 | "test": "jest",
34 | "test:watch": "jest --watch",
35 | "postinstall": "opencollective postinstall"
36 | },
37 | "devDependencies": {
38 | "babel-eslint": "^8.0.1",
39 | "babel-preset-stage-0": "^6.24.1",
40 | "electron": "^1.8.2",
41 | "electron-builder": "^19.37.2",
42 | "eslint": "^4.10.0",
43 | "eslint-config-airbnb": "^16.1.0",
44 | "eslint-loader": "^1.9.0",
45 | "eslint-plugin-import": "^2.8.0",
46 | "eslint-plugin-jsx-a11y": "^6.0.2",
47 | "eslint-plugin-react": "^7.4.0",
48 | "jest": "^20.0.4",
49 | "next": "5.0.0",
50 | "react": "16.0.0",
51 | "react-dom": "16.0.0",
52 | "react-test-renderer": "^16.0.0"
53 | },
54 | "dependencies": {
55 | "addic7ed-api": "^1.3.2",
56 | "app-root-path": "2.0.1",
57 | "bluebird": "^3.5.1",
58 | "caption-core": "^2.1.1",
59 | "electron-is-dev": "0.3.0",
60 | "electron-lets-move": "0.0.5",
61 | "electron-next": "3.1.1",
62 | "electron-store": "^1.3.0",
63 | "electron-updater": "^2.16.1",
64 | "electron-window-state": "^4.1.1",
65 | "lodash": "^4.17.4",
66 | "next-redux-wrapper": "^1.3.4",
67 | "opencollective": "^1.0.3",
68 | "opensubtitles-api": "latest",
69 | "prop-types": "^15.6.0",
70 | "react-ga": "^2.3.5",
71 | "react-redux": "^5.0.6",
72 | "redux": "^3.7.2",
73 | "redux-devtools-extension": "^2.13.2",
74 | "redux-logger": "^3.0.6",
75 | "redux-thunk": "^2.2.0"
76 | },
77 | "build": {
78 | "publish": [
79 | {
80 | "provider": "github",
81 | "owner": "gielcobben",
82 | "repo": "Caption"
83 | }
84 | ],
85 | "files": [
86 | "**/*",
87 | "!.env",
88 | "!renderer",
89 | "renderer/out"
90 | ],
91 | "mac": {
92 | "target": [
93 | "dmg",
94 | "zip"
95 | ],
96 | "icon": "./renderer/static/icon.icns"
97 | },
98 | "linux": {
99 | "target": [
100 | "deb"
101 | ],
102 | "icon": "./renderer/static/icon.iconset/"
103 | },
104 | "win": {
105 | "target": [
106 | "nsis"
107 | ],
108 | "icon": "./renderer/static/icon.ico",
109 | "publisherName": "Defringe"
110 | }
111 | },
112 | "collective": {
113 | "type": "opencollective",
114 | "url": "https://opencollective.com/caption",
115 | "logo": "https://opencollective.com/opencollective/logo.txt"
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/renderer/components/__tests__/__snapshots__/Content.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` should render
when files dropped are dropped 1`] = `
4 |
25 | `;
26 |
27 | exports[` should render
when there are results 1`] = `
28 |
32 |
38 |
41 |
44 | Drop an episode or season.
45 |
46 |
47 |
48 |
64 |
65 | `;
66 |
67 | exports[` should show when there is no searchQuery and no files dropped 1`] = `
68 |
72 |
78 |
81 |
84 | Drop an episode or season.
85 |
86 |
87 |
88 |
89 | `;
90 |
91 | exports[` should show when there is a query but no results 1`] = `
92 |
96 |
99 |
102 |
105 |
108 |
111 |
114 |
117 |
120 |
123 |
126 |
129 |
132 |
135 |
138 |
141 |
144 |
147 |
150 |
153 |
156 |
159 |
162 |
165 |
166 |
167 | `;
168 |
--------------------------------------------------------------------------------
/renderer/components/List.js:
--------------------------------------------------------------------------------
1 | // Packages
2 | import { Menu, MenuItem, remote, shell, ipcRenderer } from "electron";
3 |
4 | // Components
5 | import ListItem from "./ListItem";
6 |
7 | // Global variables
8 | const ARROW_DOWN_KEY = 40;
9 | const ARROW_UP_KEY = 38;
10 | const ENTER_KEY = 13;
11 |
12 | class List extends React.Component {
13 | constructor(props) {
14 | super(props);
15 |
16 | this.state = {
17 | selected: null,
18 | };
19 |
20 | this.onKeyDown = this.onKeyDown.bind(this);
21 | this.onArrowUp = this.onArrowUp.bind(this);
22 | this.onArrowDown = this.onArrowDown.bind(this);
23 | this.onDoubleClick = this.onDoubleClick.bind(this);
24 | }
25 |
26 | componentDidMount() {
27 | document.addEventListener("keydown", this.onKeyDown);
28 | }
29 |
30 | componentWillUnmount() {
31 | document.removeEventListener("keydown", this.onKeyDown);
32 | }
33 |
34 | onKeyDown(event) {
35 | const { onDoubleClick } = this.props;
36 |
37 | if (event.keyCode === ARROW_DOWN_KEY) {
38 | this.onArrowDown();
39 | }
40 |
41 | if (event.keyCode === ARROW_UP_KEY) {
42 | this.onArrowUp();
43 | }
44 |
45 | if (event.keyCode === ENTER_KEY) {
46 | this.onDoubleClick();
47 | }
48 | }
49 |
50 | onArrowDown() {
51 | const { results } = this.props;
52 | const currentSelected = this.state.selected;
53 | const { length } = results;
54 | const selected = currentSelected !== null ? currentSelected + 1 : 0;
55 |
56 | if (length !== selected) {
57 | this.setState({ selected });
58 | }
59 | }
60 |
61 | onArrowUp() {
62 | const currentSelected = this.state.selected;
63 | const selected = currentSelected - 1;
64 |
65 | if (currentSelected !== 0) {
66 | this.setState({ selected });
67 | }
68 | }
69 |
70 | onDoubleClick() {
71 | const { results } = this.props;
72 | const { selected } = this.state;
73 | const item = results[selected];
74 |
75 | // If size is specified, the file is dropped.
76 | if (item.size) {
77 | shell.openItem(item.path);
78 | } else {
79 | ipcRenderer.send("downloadSubtitle", item);
80 | }
81 | }
82 |
83 | onContextMenu(clicked) {
84 | let template;
85 | const { Menu } = remote;
86 | const { results } = this.props;
87 | const item = results[clicked];
88 |
89 | // If size is specified, the file is dropped.
90 | if (item.size) {
91 | template = Menu.buildFromTemplate([
92 | {
93 | label: "Open",
94 | click: () => {
95 | shell.openItem(item.path);
96 | },
97 | },
98 | {
99 | label: "Reveal in Folder...",
100 | click: () => {
101 | shell.showItemInFolder(item.path);
102 | },
103 | },
104 | ]);
105 | } else {
106 | template = Menu.buildFromTemplate([
107 | {
108 | label: "Download",
109 | click: () => {
110 | ipcRenderer.send("downloadSubtitle", item);
111 | },
112 | },
113 | ]);
114 | }
115 |
116 | // Wait till state is set.
117 | this.setState({ selected: clicked }, () => {
118 | setTimeout(() => {
119 | template.popup(remote.getCurrentWindow());
120 | }, 10);
121 | });
122 | }
123 |
124 | render() {
125 | const { results } = this.props;
126 | const { selected } = this.state;
127 |
128 | return (
129 |
130 | {results.map((item, index) => (
131 | this.setState({ selected: index })}
136 | onDoubleClick={this.onDoubleClick}
137 | onContextMenu={this.onContextMenu.bind(this, index)}
138 | />
139 | ))}
140 |
141 |
149 |
150 | );
151 | }
152 | }
153 |
154 | export default List;
155 |
--------------------------------------------------------------------------------
/renderer/actions/search.js:
--------------------------------------------------------------------------------
1 | import path from "path";
2 | import { ipcRenderer } from "electron";
3 | import { sortBy, first } from "lodash";
4 | import { logQuery } from "./../utils/tracking";
5 | import * as types from "./../types";
6 |
7 | export const hideSearchPlaceholder = () => dispatch => {
8 | dispatch({
9 | type: types.HIDE_SEARCH_PLACEHOLDER,
10 | });
11 | };
12 |
13 | export const showSearchPlaceholder = () => dispatch => {
14 | dispatch({
15 | type: types.SHOW_SEARCH_PLACEHOLDER,
16 | });
17 | };
18 |
19 | export const resetSearch = () => dispatch => {
20 | dispatch({
21 | type: types.RESET_SEARCH,
22 | });
23 |
24 | dispatch(showSearchPlaceholder());
25 | };
26 |
27 | export const updateSearchQuery = query => dispatch => {
28 | dispatch({
29 | type: types.UPDATE_SEARCH_QUERY,
30 | payload: {
31 | query,
32 | },
33 | });
34 | };
35 |
36 | export const showSearchSpinner = () => dispatch => {
37 | dispatch({
38 | type: types.SHOW_SEARCH_SPINNER,
39 | });
40 | };
41 |
42 | export const downloadComplete = () => dispatch => {
43 | dispatch({
44 | type: types.DOWNLOAD_COMPLETE,
45 | });
46 | };
47 |
48 | export const logSearchQuery = query => dispatch => {
49 | dispatch({
50 | type: types.LOG_SEARCH_QUERY,
51 | });
52 |
53 | logQuery(query);
54 | };
55 |
56 | export const searchByQuery = () => (dispatch, getState) => {
57 | const state = getState();
58 | const { language } = state.ui;
59 | const { searchQuery } = state.search;
60 |
61 | dispatch({
62 | type: types.SEARCH_BY_QUERY,
63 | });
64 |
65 | dispatch(logSearchQuery(searchQuery));
66 |
67 | ipcRenderer.send("textSearch", searchQuery, language);
68 | };
69 |
70 | export const updateDroppedFilePath = (realPath, cleanPath) => dispatch => {
71 | dispatch({
72 | type: types.SET_DROPPED_FILE_PATH,
73 | payload: {
74 | realPath,
75 | cleanPath,
76 | },
77 | });
78 | };
79 |
80 | export const searchByFiles = () => (dispatch, getState) => {
81 | const state = getState();
82 | const { language } = state.ui;
83 | const { files } = state.search;
84 |
85 | dispatch({
86 | type: types.SEARCH_BY_FILES,
87 | });
88 |
89 | if (files.length > 0) {
90 | const folders = files.map(file => path.dirname(file.path));
91 | const highestFolder = first(sortBy(folders, "length"));
92 | const cleanPath = highestFolder;
93 | const realPath = path.basename(cleanPath);
94 |
95 | dispatch(updateDroppedFilePath(cleanPath, realPath));
96 | }
97 |
98 | ipcRenderer.send("fileSearch", files, language);
99 | };
100 |
101 | export const increaseSearchAttempts = () => (dispatch, getState) => {
102 | const state = getState();
103 | const previousSearchAttempts = state.search.searchAttempts;
104 |
105 | dispatch({
106 | type: types.INCREASE_SEARCH_ATTEMPTS,
107 | payload: {
108 | attempts: previousSearchAttempts + 1,
109 | },
110 | });
111 | };
112 |
113 | export const startSearch = () => (dispatch, getState) => {
114 | const state = getState();
115 | const { searchQuery, files } = state.search;
116 |
117 | dispatch(showSearchSpinner());
118 | dispatch(increaseSearchAttempts());
119 |
120 | if (searchQuery !== "") {
121 | return dispatch(searchByQuery());
122 | }
123 |
124 | if (files.length > 0) {
125 | return dispatch(searchByFiles());
126 | }
127 |
128 | return dispatch(resetSearch());
129 | };
130 |
131 | export const dropFiles = files => dispatch => {
132 | dispatch({
133 | type: types.DROP_FILES,
134 | payload: {
135 | files,
136 | },
137 | });
138 |
139 | dispatch(startSearch());
140 | };
141 |
142 | export const updateFileSearchStatus = (filePath, status) => dispatch => {
143 | dispatch({
144 | type: types.UPDATE_FILE_SEARCH_STATUS,
145 | payload: {
146 | filePath,
147 | status,
148 | },
149 | });
150 | };
151 |
152 | export const updateSearchResults = ({
153 | results,
154 | searchCompleted,
155 | }) => dispatch => {
156 | dispatch({
157 | type: types.UPDATE_SEARCH_RESULTS,
158 | payload: {
159 | searchCompleted,
160 | results,
161 | },
162 | });
163 | };
164 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | Caption
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
INTRODUCTION
14 | Caption takes the effort out of finding and setting up the right subtitles. A simple design, drag & drop search, and automatic downloading & renaming let you just start watching. Caption is multi-platform, open-source, and built entirely on web technology.
15 | Download Caption.
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | ## ⚡️ Contribute
26 |
27 | Caption is completely open-source. We've tried to make it as easy as possible to
28 | contribute. If you'd like to help out by adding features, working on bug fixes,
29 | or assisting in other parts of development, here's how to get started:
30 |
31 | ###### To begin working locally:
32 |
33 | 1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your
34 | own GitHub account
35 | 2. [Clone](https://help.github.com/articles/cloning-a-repository/) it to your
36 | local device: `git clone git@github.com:gielcobben/caption.git`
37 | 3. Install the dependencies: `npm install`
38 | 4. Run the app by starting electron, building the code and watch for changes:
39 | `npm start`
40 | ###### To build for production (should not generally be used):
41 | 5. Build the actual app for all platforms (Mac, Windows and Linux): `npm run
42 | dist`
43 |
44 |
45 |
46 | ## 📦 Sources
47 |
48 | Caption currently uses 2 sources to gather subtitles. We're continuously adding
49 | sources, but the app's open-source nature also allows you to add your own when
50 | desired. This can be done in
51 | [Caption Core](https://github.com/gielcobben/caption-core).
52 |
53 | ###### Standard sources:
54 |
55 | * [x] OpenSubtitles
56 | * [x] Addic7ed
57 |
58 |
59 |
60 | ## ⭐️ Links
61 |
62 | ###### Makers:
63 |
64 | * [Giel Cobben](https://github.com/gielcobben)
65 | * [Vernon de Goede](https://github.com/vernondegoede)
66 |
67 | ###### Friends:
68 |
69 | * [Rick Wong](https://github.com/RickWong)
70 | * [Huub Gelissen](https://twitter.com/gelissenhuub)
71 |
72 | ###### Repositories:
73 |
74 | * [Caption Core](https://github.com/gielcobben/caption-core)
75 | * [Caption Website](https://github.com/gielcobben/getcaption.co)
76 |
77 |
78 |
79 | ## 👨👨👧👦 Open-source
80 |
81 | ###### Contributors:
82 |
83 |
84 |
85 | ###### Backers:
86 |
87 |
88 |
89 | ###### Sponsors:
90 |
91 | Support this project by becoming a sponsor. Your logo will show up here
92 | with a link to your website.
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 | ## 🔑 License
116 |
117 | [MIT](https://github.com/gielcobben/Caption/blob/master/LICENSE) ©
118 | [Giel Cobben](https://twitter.com/gielcobben)
119 |
--------------------------------------------------------------------------------
/renderer/data/software.js:
--------------------------------------------------------------------------------
1 | const software = [
2 | {
3 | name: "next.js",
4 | description: `The MIT License (MIT) Copyright (c) 2016 Zeit, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.`,
5 | },
6 | {
7 | name: "electron-is-dev",
8 | description: `The MIT License (MIT) Copyright (c) Sindre Sorhus (sindresorhus.com) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.`,
9 | },
10 | {
11 | name: "electron-next",
12 | description: `MIT License (MIT) Copyright (c) 2017 Leo Lamprecht Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.`,
13 | },
14 | {
15 | name: "electron-store",
16 | description: `MIT License Copyright (c) Sindre Sorhus (sindresorhus.com) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.`,
17 | },
18 | {
19 | name: "electron-window-state",
20 | description: `The MIT License (MIT) Copyright (c) 2015 Jakub Szwacz Copyright (c) Marcel Wiehle (http://marcel.wiehle.me) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE`,
21 | },
22 | ];
23 |
24 | export default software;
25 |
--------------------------------------------------------------------------------
/renderer/pages/start.js:
--------------------------------------------------------------------------------
1 | // Packages
2 | import { platform } from "os";
3 | import { ipcRenderer } from "electron";
4 | import withRedux from "next-redux-wrapper";
5 | import React, { Component } from "react";
6 | import PropTypes from "prop-types";
7 |
8 | // Components
9 | import Layout from "../components/Layout";
10 | import TitleBar from "../components/TitleBar";
11 |
12 | // Containers
13 | import Search from "../containers/Search";
14 | import Content from "../containers/Content";
15 | import Footer from "../containers/Footer";
16 |
17 | // Redux store
18 | import initStore from "./../store";
19 |
20 | // Redux action creators
21 | import {
22 | setLanguage,
23 | showNotification,
24 | resetSearch,
25 | showSearchPlaceholder,
26 | hideSearchPlaceholder,
27 | updateSearchQuery,
28 | startSearch,
29 | searchByQuery,
30 | downloadComplete,
31 | showSearchSpinner,
32 | searchByFiles,
33 | dropFiles,
34 | updateSearchResults,
35 | logDonatedButtonClicked,
36 | logAboutWindowOpend,
37 | updateFileSearchStatus,
38 | } from "./../actions";
39 |
40 | // Analytics
41 | import { initGA, logPageView } from "./../utils/tracking";
42 |
43 | // Global variables
44 | const ESC_KEY = 27;
45 |
46 | class MainApp extends Component {
47 | constructor(props) {
48 | super(props);
49 |
50 | this.isWindows = platform() === "win32";
51 | this.onLanguageChange = this.onLanguageChange.bind(this);
52 | this.checkIfOnline = this.checkIfOnline.bind(this);
53 | this.onKeyDown = this.onKeyDown.bind(this);
54 | this.onSearch = this.onSearch.bind(this);
55 | this.onFocus = this.onFocus.bind(this);
56 | this.onBlur = this.onBlur.bind(this);
57 | }
58 |
59 | // handling escape close
60 | componentDidMount() {
61 | initGA();
62 | logPageView();
63 | this.checkIfOnline();
64 |
65 | ipcRenderer.on("results", (event, { results, isFinished }) => {
66 | this.props.updateSearchResults({
67 | results,
68 | searchCompleted: isFinished,
69 | });
70 | });
71 |
72 | ipcRenderer.on("language", (event, language) => {
73 | this.props.setLanguage(language);
74 | });
75 |
76 | ipcRenderer.on("allFilesDownloaded", () => {
77 | this.props.downloadComplete();
78 | });
79 |
80 | ipcRenderer.on("openFile", async (event, file) => {
81 | const rawFiles = [file];
82 | this.props.dropFiles(rawFiles);
83 | });
84 |
85 | ipcRenderer.on("processedFiles", (event, files) => {
86 | this.props.dropFiles(files);
87 | });
88 |
89 | ipcRenderer.on("logDonated", () => {
90 | this.props.logDonatedButtonClicked();
91 | });
92 |
93 | ipcRenderer.on("logAbout", () => {
94 | this.props.logAboutWindowOpend();
95 | });
96 |
97 | ipcRenderer.on("updateFileSearchStatus", (event, { filePath, status }) => {
98 | this.props.updateFileSearchStatus(filePath, status);
99 | });
100 |
101 | ipcRenderer.send("getStore", "language");
102 | document.addEventListener("keydown", this.onKeyDown);
103 |
104 | // Prevent drop on document
105 | document.addEventListener(
106 | "dragover",
107 | event => {
108 | event.preventDefault();
109 | return false;
110 | },
111 | false,
112 | );
113 |
114 | document.addEventListener(
115 | "drop",
116 | event => {
117 | event.preventDefault();
118 | return false;
119 | },
120 | false,
121 | );
122 | }
123 |
124 | componentWillUnmount() {
125 | document.removeEventListener("keydown", this.onKeyDown);
126 | window.removeEventListener("online", this.checkIfOnline);
127 | }
128 |
129 | onKeyDown(event) {
130 | if (event.keyCode >= 48 && event.keyCode <= 90) {
131 | this.onFocus();
132 | }
133 |
134 | if (event.keyCode === ESC_KEY) {
135 | this.props.resetSearch();
136 | this.onBlur();
137 | }
138 | }
139 |
140 | onFocus() {
141 | this.props.hideSearchPlaceholder();
142 | this.search.getWrappedInstance().searchField.textInput.focus();
143 | }
144 |
145 | onBlur() {
146 | this.props.showSearchPlaceholder();
147 | this.search.getWrappedInstance().searchField.textInput.blur();
148 | }
149 |
150 | onLanguageChange(event) {
151 | const language = event.target.value;
152 |
153 | this.props.setLanguage(language);
154 | }
155 |
156 | onSearch(event) {
157 | if (event) {
158 | event.preventDefault();
159 | }
160 |
161 | this.props.startSearch();
162 | }
163 |
164 | checkIfOnline() {
165 | ipcRenderer.send("online", navigator.onLine);
166 | window.addEventListener("offline", () => {
167 | ipcRenderer.send("online", navigator.onLine);
168 | });
169 | }
170 |
171 | render() {
172 | return (
173 |
174 | {!this.isWindows && }
175 | {
180 | this.search = search;
181 | }}
182 | />
183 |
184 |
185 |
186 | );
187 | }
188 | }
189 |
190 | MainApp.propTypes = {
191 | downloadComplete: PropTypes.func.isRequired,
192 | updateSearchResults: PropTypes.func.isRequired,
193 | setLanguage: PropTypes.func.isRequired,
194 | resetSearch: PropTypes.func.isRequired,
195 | hideSearchPlaceholder: PropTypes.func.isRequired,
196 | showSearchPlaceholder: PropTypes.func.isRequired,
197 | startSearch: PropTypes.func.isRequired,
198 | showNotification: PropTypes.func.isRequired,
199 | dropFiles: PropTypes.func.isRequired,
200 | logDonatedButtonClicked: PropTypes.func.isRequired,
201 | logAboutWindowOpend: PropTypes.func.isRequired,
202 | updateFileSearchStatus: PropTypes.func.isRequired,
203 | };
204 |
205 | const mapStateToProps = ({ ui, search }) => ({
206 | language: ui.language,
207 | searchQuery: search.searchQuery,
208 | files: search.files,
209 | placeholder: search.placeholder,
210 | results: search.results,
211 | loading: search.loading,
212 | searchCompleted: search.searchCompleted,
213 | });
214 |
215 | const mapDispatchToProps = {
216 | setLanguage,
217 | showNotification,
218 | resetSearch,
219 | showSearchPlaceholder,
220 | hideSearchPlaceholder,
221 | startSearch,
222 | searchByQuery,
223 | updateSearchQuery,
224 | downloadComplete,
225 | showSearchSpinner,
226 | searchByFiles,
227 | dropFiles,
228 | updateSearchResults,
229 | logDonatedButtonClicked,
230 | logAboutWindowOpend,
231 | updateFileSearchStatus,
232 | };
233 |
234 | export default withRedux(initStore, mapStateToProps, mapDispatchToProps)(MainApp);
235 |
--------------------------------------------------------------------------------
/renderer/components/FilePath.js:
--------------------------------------------------------------------------------
1 | import { platform } from "os";
2 | import { shell } from "electron";
3 | import PropTypes from "prop-types";
4 |
5 | const defaultFolderIcon = (
6 |
12 |
13 |
17 |
21 |
22 |
23 | );
24 |
25 | const windowsFilderIcon = (
26 |
32 |
33 |
37 |
38 |
39 |
40 |
41 |
45 |
50 |
51 |
52 | );
53 |
54 | class FilePath extends React.Component {
55 | constructor(props) {
56 | super(props);
57 | this.isWindows = platform() === "win32";
58 | }
59 |
60 | render() {
61 | const { dropFilePath, dropFilePathClean, onReset } = this.props;
62 |
63 | return (
64 |
65 |
66 | {this.isWindows ? windowsFilderIcon : defaultFolderIcon}
67 |
68 |
shell.showItemInFolder(dropFilePath)}
71 | >
72 | {dropFilePathClean}
73 |
74 |
75 | {" "}
76 |
82 |
83 |
84 |
89 |
90 |
91 |
92 |
93 |
131 |
132 | );
133 | }
134 | }
135 |
136 | FilePath.propTypes = {
137 | dropFilePath: PropTypes.string.isRequired,
138 | dropFilePathClean: PropTypes.string.isRequired,
139 | onReset: PropTypes.func.isRequired,
140 | };
141 |
142 | export default FilePath;
143 |
--------------------------------------------------------------------------------