diff --git a/.eslintrc.js b/.eslintrc.js index a5b82de..ee6effd 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,3 +1,3 @@ module.exports = { - "extends": "standard" -}; \ No newline at end of file + extends: "standard" +}; diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index c1f19fc..df095fe 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -8,19 +8,19 @@ In the interest of fostering an open and welcoming environment, we as contributo Examples of behavior that contributes to creating a positive environment include: -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members Examples of unacceptable behavior by participants include: -* The use of sexualized language or imagery and unwelcome sexual attention or advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a professional setting +- The use of sexualized language or imagery and unwelcome sexual attention or advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or electronic address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities diff --git a/LICENSE.md b/LICENSE.md index b823bcc..5bf2230 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,8 +1,6 @@ -CC0 1.0 Universal -================== +# CC0 1.0 Universal -Statement of Purpose ---------------------- +## Statement of Purpose The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work"). @@ -11,7 +9,9 @@ Certain owners wish to permanently relinquish those rights to a Work for the pur For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights. 1. Copyright and Related Rights. --------------------------------- + +--- + A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following: i. the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work; @@ -23,15 +23,20 @@ vi. database rights (such as those arising under Directive 96/9/EC of the Europe vii. other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof. 2. Waiver. ------------ + +--- + To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose. 3. Public License Fallback. ----------------------------- + +--- + Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose. 4. Limitations and Disclaimers. --------------------------------- + +--- a. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document. b. Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law. diff --git a/README.md b/README.md index 21da3d5..deb63eb 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,59 @@ # H2 -Inspired by the Helium app, **H2** is a minimalist browser to watch embeded videos and more in picture-in-picture mode. + +Inspired by the Helium app, **H2** is a minimalist browser to watch embeded videos and more in picture-in-picture mode. + # Features + :tv: Works with any Youtube URL. :scroll: Works with PDFs links, Google Docs URLs. :computer: Works on all platforms, thanks to electron ❤️ -:clock9:[WIP] After [#22](https://github.com/poush/H2/pull/22) a basic image editor, many other features coming soon. +:art: Image editor supports contrast, brightness, hue, sepia and save as PNG. + +:clock9:[WIP] Many other features coming soon. + # Example + ![demo](img/demo.gif) + ## Installation -* [Windows](docs/windows.md) -* [Mac](docs/mac.md) -* [Linux](docs/linux.md) + +- [Windows](docs/windows.md) +- [Mac](docs/mac.md) +- [Linux](docs/linux.md) + ## Usage -* Copy any youtube, PDF or google docs URL and press `Ctrl/Command + Shift + V` -* [Keyboards Shortcuts](docs/shortcuts.md) + +- Copy any youtube, PDF or google docs URL and press `Ctrl/Command + Shift + V` +- [Keyboards Shortcuts](docs/shortcuts.md) + ## Contributing + ```bash # Clone this repository + git clone https://github.com/poush/h2 + # Go into the repository + cd h2 + # Install dependencies + npm install + # Run the app + npm start ``` + ## Bug + [Known Bugs](https://github.com/poush/H2/issues?q=is%3Aopen+is%3Aissue+label%3Abug) -## Credits + ## License + [CC0-1.0](https://github.com/poush/H2/blob/master/LICENSE.md) diff --git a/ServiceProviders/MediaProviders/baseMediaProvider.js b/ServiceProviders/MediaProviders/baseMediaProvider.js index b4fe110..ef73f3c 100644 --- a/ServiceProviders/MediaProviders/baseMediaProvider.js +++ b/ServiceProviders/MediaProviders/baseMediaProvider.js @@ -1,50 +1,46 @@ class baseMediaProvider { - - constructor() { - this.version = '0.1' - this.name = 'base' - this.response = { - eventName: 'default', - type: 'link', - content: '' - } - } - - set text(link) { - this.extractContents(link) - } - - get content() { - if(this.response.content != '' || this.response.link != undefined){ - // console.log(this.response) - return this - } - - throw "InvalidContentByContentExtraction" - } - - matcher(link) { - - } - - //method - extractContents(link) { - if (link != undefined || link != '') { - this.response.content = link - } - - return true - } - - postWinLoad(win){ - return true - } - - preWinLoad(win) { - //must return true - return true - } - + constructor() { + this.version = "0.1"; + this.name = "base"; + this.response = { + eventName: "default", + type: "link", + content: "" + }; + } + + set text(link) { + this.extractContents(link); + } + + get content() { + if (this.response.content != "" || this.response.link != undefined) { + // console.log(this.response) + return this; + } + + throw "InvalidContentByContentExtraction"; + } + + matcher(link) {} + + //method + extractContents(link) { + if (link != undefined || link != "") { + this.response.content = link; + } + + return true; + } + + postWinLoad(win) { + return true; + } + + preWinLoad(win) { + //must return true + return true; + } } -module.exports = baseMediaProvider \ No newline at end of file +module.exports = baseMediaProvider; diff --git a/ServiceProviders/MediaProviders/docs.js b/ServiceProviders/MediaProviders/docs.js index 6eff392..b3dd892 100644 --- a/ServiceProviders/MediaProviders/docs.js +++ b/ServiceProviders/MediaProviders/docs.js @@ -1,36 +1,32 @@ -const baseMediaProvider = require('./baseMediaProvider') +const baseMediaProvider = require("./baseMediaProvider"); class docsProvider extends baseMediaProvider { - - constructor(){ - super() - this.name = 'googleDocs' - this.response.eventName = 'googleDocs' - this.response.type = 'iframe' - } - - matcher(link){ - const regex = /https:\/\/docs.google.com\/document\/d\/[0-9A-Za-z]+/gm; - var match = regex.exec(link) - if(match) - return match; - - return false; - } - - extractContents(link) { - if (link != undefined || link != '') { - this.response.content = link - } - - return true - - } - - postWinLoad(win){ - return true - } - + constructor() { + super(); + this.name = "googleDocs"; + this.response.eventName = "googleDocs"; + this.response.type = "iframe"; + } + + matcher(link) { + const regex = /https:\/\/docs.google.com\/document\/d\/[0-9A-Za-z]+/gm; + var match = regex.exec(link); + if (match) return match; + + return false; + } + + extractContents(link) { + if (link != undefined || link != "") { + this.response.content = link; + } + + return true; + } + + postWinLoad(win) { + return true; + } } -module.exports = docsProvider \ No newline at end of file +module.exports = docsProvider; diff --git a/ServiceProviders/MediaProviders/pdf.js b/ServiceProviders/MediaProviders/pdf.js index ee7bfda..6599925 100644 --- a/ServiceProviders/MediaProviders/pdf.js +++ b/ServiceProviders/MediaProviders/pdf.js @@ -1,36 +1,31 @@ -const baseMediaProvider = require('./baseMediaProvider') +const baseMediaProvider = require("./baseMediaProvider"); class pdfProvider extends baseMediaProvider { - - constructor(){ - super() - this.name = 'pdf' - this.response.type = 'link' - } - - matcher(link){ - const regex = /(https?:\/\/\S*?\.pdf)/gm; - var match = regex.exec(link) - if(match) - return match; - - return false; - } - - extractContents(link) { - - if (link != undefined || link != '') { - this.response.content = link - } - - return true - - } - - postWinLoad(win){ - return true - } - + constructor() { + super(); + this.name = "pdf"; + this.response.type = "link"; + } + + matcher(link) { + const regex = /(https?:\/\/\S*?\.pdf)/gm; + var match = regex.exec(link); + if (match) return match; + + return false; + } + + extractContents(link) { + if (link != undefined || link != "") { + this.response.content = link; + } + + return true; + } + + postWinLoad(win) { + return true; + } } -module.exports = pdfProvider \ No newline at end of file +module.exports = pdfProvider; diff --git a/ServiceProviders/MediaProviders/youtube.js b/ServiceProviders/MediaProviders/youtube.js index fe644d5..b4e6efe 100644 --- a/ServiceProviders/MediaProviders/youtube.js +++ b/ServiceProviders/MediaProviders/youtube.js @@ -1,37 +1,33 @@ -const baseMediaProvider = require('./baseMediaProvider') +const baseMediaProvider = require("./baseMediaProvider"); class youtubeProvider extends baseMediaProvider { - - constructor(){ - super() - this.name = 'youtube' - this.response.type = 'iframe' - this.response.eventName = 'youtube' - } - - matcher(link){ - - if (link != undefined || link != '') { - var regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=|\?v=)([^#\&\?]*).*/; - var match = link.match(regExp); - if (match && match[2].length == 11) { - return match[2] - } - } - - return false; - - } - - extractContents(link) { - let match = this.matcher(link) - this.response.content = match - // let web = 'https://www.youtube.com/embed/' + match + '?autoplay=1&enablejsapi=1' - // this.response.content = `` - - return true - } - + constructor() { + super(); + this.name = "youtube"; + this.response.type = "iframe"; + this.response.eventName = "youtube"; + } + + matcher(link) { + if (link != undefined || link != "") { + var regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=|\?v=)([^#\&\?]*).*/; + var match = link.match(regExp); + if (match && match[2].length == 11) { + return match[2]; + } + } + + return false; + } + + extractContents(link) { + let match = this.matcher(link); + this.response.content = match; + // let web = 'https://www.youtube.com/embed/' + match + '?autoplay=1&enablejsapi=1' + // this.response.content = `` + + return true; + } } -module.exports = youtubeProvider \ No newline at end of file +module.exports = youtubeProvider; diff --git a/ServiceProviders/mediaProviderApplier.js b/ServiceProviders/mediaProviderApplier.js index 4276bc8..712f4c6 100644 --- a/ServiceProviders/mediaProviderApplier.js +++ b/ServiceProviders/mediaProviderApplier.js @@ -1,22 +1,20 @@ -const baseMediaProvider = require('./MediaProviders/baseMediaProvider') +const baseMediaProvider = require("./MediaProviders/baseMediaProvider"); -module.exports = function (toApply, win){ +module.exports = function(toApply, win) { + if (toApply instanceof baseMediaProvider) { + if (toApply.response.type === "iframe") { + win.loadFile("./index.html"); - if(toApply instanceof baseMediaProvider){ - - if(toApply.response.type === 'iframe'){ - win.loadFile('./index.html') - - win.webContents.once('dom-ready', () => { - win.webContents.send(toApply.response.eventName, toApply.response.content) - }) - } - - else { - toApply.preWinLoad(win) - win.loadURL(toApply.response.content) - toApply.postWinLoad(win) - } - - } -} \ No newline at end of file + win.webContents.once("dom-ready", () => { + win.webContents.send( + toApply.response.eventName, + toApply.response.content + ); + }); + } else { + toApply.preWinLoad(win); + win.loadURL(toApply.response.content); + toApply.postWinLoad(win); + } + } +}; diff --git a/ServiceProviders/providers.js b/ServiceProviders/providers.js index 0f611de..f17b69f 100644 --- a/ServiceProviders/providers.js +++ b/ServiceProviders/providers.js @@ -1,43 +1,38 @@ -const youtubeProvider = require('./MediaProviders/youtube') -const pdfProvider = require('./MediaProviders/pdf') -const docsProvider = require('./MediaProviders/docs') -const applyMedia = require('./mediaProviderApplier') -const {clipboard} = require('electron') - +const youtubeProvider = require("./MediaProviders/youtube"); +const pdfProvider = require("./MediaProviders/pdf"); +const docsProvider = require("./MediaProviders/docs"); +const applyMedia = require("./mediaProviderApplier"); +const { clipboard } = require("electron"); let matchers = { - - 'youtube' : new youtubeProvider(), - 'pdf': new pdfProvider(), - 'docs': new docsProvider(), - -} + youtube: new youtubeProvider(), + pdf: new pdfProvider(), + docs: new docsProvider() +}; module.exports = { - - run(win) { - - let text = clipboard.readText('selection'); - - let provider = null - for(let key in matchers){ - if(matchers[key].matcher(text) !== false){ - provider = key - break; - } - } - - if(provider == null){ - win.webContents.send('invalidUrl', 'ping') - return - } - - matchers[provider].text = text; - - if(arguments[1]) { - matchers[provider].text = arguments[1]; - } - - applyMedia(matchers[provider].content, win) - } -} \ No newline at end of file + run(win) { + let text = clipboard.readText("selection"); + + let provider = null; + for (let key in matchers) { + if (matchers[key].matcher(text) !== false) { + provider = key; + break; + } + } + + if (provider == null) { + win.webContents.send("invalidUrl", "ping"); + return; + } + + matchers[provider].text = text; + + if (arguments[1]) { + matchers[provider].text = arguments[1]; + } + + applyMedia(matchers[provider].content, win); + } +}; diff --git a/assets/.DS_Store b/assets/.DS_Store new file mode 100644 index 0000000..4e58249 Binary files /dev/null and b/assets/.DS_Store differ diff --git a/assets/css/main.css b/assets/css/main.css new file mode 100644 index 0000000..16b9193 --- /dev/null +++ b/assets/css/main.css @@ -0,0 +1,396 @@ +:root { + --contrast: 100%; + --brightness: 100%; + --hue-rotate: 0deg; + --sepia: 0%; + --accent: #4a90e2; + --dark-accent: #3f7ac0; + --gray: #e6e7f0; + --dark-gray: #4e5056; +} + +* { + padding: 0; + margin: 0; +} + +body { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; + width: 100vw; + background-image: linear-gradient(0deg, #ebfcff 0%, #ffffff 89%); + font-family: Roboto, sans-serif; +} + +.drop-it-hot { + background-color: white; + width: 100%; + height: 100%; + position: relative; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 8px 15px 0 rgba(0, 0, 0, 0.1); + overflow: hidden; + align-items: flex-start; +} + +.drop-it-hot:after { + content: ""; + position: absolute; + width: calc(100% - 20px); + height: calc(100% - 20px); + z-index: 0; +} + +.info { + display: flex; + align-items: center; + width: 100%; + align-items: center; + justify-content: center; +} + +.info p { + padding: 15px; + text-align: center; +} + +.info.heading { + flex-direction: column; + margin-top: 5vh; + max-width: 400px; + align-self: flex-start; +} + +.info.drag { + margin-top: 82vh; + max-width: 400px; + position: absolute; +} + +.choose-files { + align-self: flex-end; + z-index: 50; + display: none; +} + +.drop-it-hot .circle { + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + position: absolute; + color: var(--accent); + background-color: white; + border: 3px solid var(--gray); + width: 100px; + height: 100px; + margin-top: 45vh; +} + +.circle { + transition: transform 150ms ease-in; + z-index: 10; +} +.circle svg { + width: 40px; + height: 40px; +} +.circle:before { + content: ""; + background-color: var(--accent); + width: 130px; + height: 130px; + border-radius: 50%; + position: absolute; + opacity: 0; + transition: transform 250ms ease-in, opacity 200ms ease-in; + z-index: 0; +} +.circle:after { + content: ""; + position: absolute; + width: 100px; + height: 100px; + border-radius: 50%; +} +.circle:hover { + transform: scale(1.2); + opacity: 0.9; +} +.circle:hover:before { + transform: scale(8); + opacity: 1; +} +.circle:hover:after { + border: 3px solid white; +} +.circle:hover svg { + color: white; + z-index: 1; +} + +.highlight:before { + transform: scale(8); + opacity: 1; +} +.highlight:after { + border: 3px solid white; +} +.highlight svg { + color: white; + z-index: 1; +} + +#gallery { + position: absolute; + height: 90%; + top: 0; + left: 0; + z-index: 20; + width: 100%; +} + +canvas { + min-width: 300px; + width: 100%; + height: auto; +} + +.download a img { + max-width: 25px; + margin-top: 5px; +} + +#gallery img { + display: none; + background-repeat: no-repeat; + background-size: cover; + object-fit: cover; + filter: contrast(var(--contrast)) brightness(var(--brightness)) + sepia(var(--sepia)) hue-rotate(var(--hue-rotate)); +} + +.editor { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + width: 100%; + padding: 20px; + margin: auto; + box-sizing: border-box; + background-color: white; + z-index: 100; + bottom: 0px; + position: fixed; + align-items: flex-start; + transform: translateY(50px); + opacity: 0; +} + +.is-visible { + transition: transform 500ms ease-in-out, opacity 500ms ease-in-out; + opacity: 1; + transform: translateY(0); +} + +.range-wrapper { + display: flex; + flex-direction: column; + width: 20%; +} + +.range { + -webkit-appearance: none; + height: 2px; + border-radius: 5px; + background: var(--gray); + outline: none; + padding: 0; + margin: 10px 0; +} + +::-moz-range-track { + background: var(--gray); + border: 0; +} + +input::-moz-focus-inner, +input::-moz-focus-outer { + border: 0; +} + +input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + height: 12px; + width: 12px; + border-radius: 8px; + background-color: var(--accent); + cursor: pointer; +} + +input[type="range"]::-moz-range-thumb { + -webkit-appearance: none; + height: 12px; + width: 12px; + border-radius: 8px; + background-color: var(--accent); + cursor: pointer; +} + +input[type="range"]::-ms-thumb { + -webkit-appearance: none; + height: 12px; + width: 12px; + border-radius: 8px; + background-color: var(--accent); + cursor: pointer; +} +.loading { + position: fixed; + z-index: 99; + height: 2em; + width: 2em; + overflow: show; + margin: auto; + top: 0; + left: 0; + bottom: 0; + right: 0; +} + +/* Transparent Overlay */ +.loading:before { + content: ""; + display: block; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: radial-gradient(rgba(20, 20, 20, 0.8), rgba(0, 0, 0, 0.8)); + + background: -webkit-radial-gradient( + rgba(20, 20, 20, 0.8), + rgba(0, 0, 0, 0.8) + ); +} + +/* :not(:required) hides these rules from IE9 and below */ +.loading:not(:required) { + /* hide "loading..." text */ + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; +} + +.loading:not(:required):after { + content: ""; + display: block; + font-size: 10px; + width: 1em; + height: 1em; + margin-top: -0.5em; + -webkit-animation: spinner 1500ms infinite linear; + -moz-animation: spinner 1500ms infinite linear; + -ms-animation: spinner 1500ms infinite linear; + -o-animation: spinner 1500ms infinite linear; + animation: spinner 1500ms infinite linear; + border-radius: 0.5em; + -webkit-box-shadow: rgba(255, 255, 255, 0.75) 1.5em 0 0 0, + rgba(255, 255, 255, 0.75) 1.1em 1.1em 0 0, + rgba(255, 255, 255, 0.75) 0 1.5em 0 0, + rgba(255, 255, 255, 0.75) -1.1em 1.1em 0 0, + rgba(255, 255, 255, 0.75) -1.5em 0 0 0, + rgba(255, 255, 255, 0.75) -1.1em -1.1em 0 0, + rgba(255, 255, 255, 0.75) 0 -1.5em 0 0, + rgba(255, 255, 255, 0.75) 1.1em -1.1em 0 0; + box-shadow: rgba(255, 255, 255, 0.75) 1.5em 0 0 0, + rgba(255, 255, 255, 0.75) 1.1em 1.1em 0 0, + rgba(255, 255, 255, 0.75) 0 1.5em 0 0, + rgba(255, 255, 255, 0.75) -1.1em 1.1em 0 0, + rgba(255, 255, 255, 0.75) -1.5em 0 0 0, + rgba(255, 255, 255, 0.75) -1.1em -1.1em 0 0, + rgba(255, 255, 255, 0.75) 0 -1.5em 0 0, + rgba(255, 255, 255, 0.75) 1.1em -1.1em 0 0; +} + +/* Animation */ + +@-webkit-keyframes spinner { + 0% { + -webkit-transform: rotate(0deg); + -moz-transform: rotate(0deg); + -ms-transform: rotate(0deg); + -o-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(360deg); + -moz-transform: rotate(360deg); + -ms-transform: rotate(360deg); + -o-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@-moz-keyframes spinner { + 0% { + -webkit-transform: rotate(0deg); + -moz-transform: rotate(0deg); + -ms-transform: rotate(0deg); + -o-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(360deg); + -moz-transform: rotate(360deg); + -ms-transform: rotate(360deg); + -o-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@-o-keyframes spinner { + 0% { + -webkit-transform: rotate(0deg); + -moz-transform: rotate(0deg); + -ms-transform: rotate(0deg); + -o-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(360deg); + -moz-transform: rotate(360deg); + -ms-transform: rotate(360deg); + -o-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@keyframes spinner { + 0% { + -webkit-transform: rotate(0deg); + -moz-transform: rotate(0deg); + -ms-transform: rotate(0deg); + -o-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(360deg); + -moz-transform: rotate(360deg); + -ms-transform: rotate(360deg); + -o-transform: rotate(360deg); + transform: rotate(360deg); + } +} +#h2-icon { + height: 22px; + width: 23px; +} +#h2-icon .white { + fill: #ffffff; +} diff --git a/assets/js/dragAndDrop.js b/assets/js/dragAndDrop.js new file mode 100644 index 0000000..adceef5 --- /dev/null +++ b/assets/js/dragAndDrop.js @@ -0,0 +1,131 @@ +const dragEvents = ["dragenter", "dragover", "dragleave", "drop"]; +const dragHighlight = ["dragenter", "dragover"]; +const dragUnighlight = ["dragleave", "drop"]; +const inputRange = document.querySelectorAll(".editor input"); + +const dropArea = document.getElementById("drop-area"); +const iconElement = document.querySelector(".circle"); +const editorElement = document.querySelector(".editor"); + +const preventDefaults = e => { + e.preventDefault(); + e.stopPropagation(); +}; + +const highlight = () => { + iconElement.classList.add("highlight"); +}; + +const unhighlight = () => { + iconElement.classList.remove("highlight"); +}; + +// Preview file +const previewFile = file => { + document.getElementById("gallery").innerHTML = ""; + document.getElementById("heading").setAttribute("style", "opacity:0;"); + document.getElementById("circle").setAttribute("style", "opacity:0;"); + document.getElementById("drag").setAttribute("style", "opacity:0;"); + let reader = new FileReader(); + reader.readAsDataURL(file); + reader.onloadend = function() { + let img = document.createElement("img"); + img.src = reader.result; + img.setAttribute("id", "uploadedImage"); + document.getElementById("gallery").appendChild(img); + editorElement.classList.add("is-visible"); + img.onload = drawCanvas; + }; + + function drawCanvas() { + // Create an out of view canvas + var canvas = document.querySelector("canvas"), + ctx = canvas.getContext("2d"); + // Set canvas dimensions to original image + canvas.width = this.width; + canvas.height = this.height; + ctx.drawImage(this, 0, 0); + saveImage(); + } +}; + +// Setup to handle multiple files +// For now, keep as single file upload until I can do testing +const handleFiles = files => { + files = [...files]; + files.forEach(previewFile); +}; +// Drag and drop file(s) - Note: Keep as one file input for now. +const handleDrop = e => { + let dt = e.dataTransfer; + let files = dt.files; + const filetype = files[0].type.split("/")[0]; + if (filetype !== "image") { + alert("Sorry, you can only drag and drop images."); + } else { + handleFiles(files); + } +}; + +// Editor controls +function handleUpdate() { + const suffix = this.dataset.unit; + document.documentElement.style.setProperty( + `--${this.name}`, + this.value + suffix + ); +} + +// Save image +function saveImage() { + var canvas = document.querySelector("canvas"); + + // Set download link to save image + var link = document.getElementById("downloadImage"); + link.setAttribute("download", "h2-image.png"); + link.setAttribute( + "href", + canvas.toDataURL("image/png").replace("image/png", "image/octet-stream") + ); +} + +document.getElementById("downloadImage").onclick = function() { + location.reload(); +}; + +inputRange.forEach(input => input.addEventListener("change", handleUpdate)); +inputRange.forEach(input => input.addEventListener("mousemove", handleUpdate)); + +inputRange.forEach(input => input.addEventListener("change", updateCanvas)); +function updateCanvas() { + // Create an out of view canvas + const img = document.getElementById("uploadedImage"); + const canvas = document.querySelector("canvas"), + ctx = canvas.getContext("2d"); + // Get filter values + let filterValues = document.documentElement.style.cssText; + // Replace values to canvas filter CSS + filterValues = filterValues + .replace(/--/g, "") + .replace(/:/g, "(") + .replace(/;/g, ")"); + // Apply filters + ctx.filter = filterValues; + // Update canvas and save image href + ctx.drawImage(img, 0, 0); + saveImage(); +} + +dragEvents.forEach(eventName => { + dropArea.addEventListener(eventName, preventDefaults, false); +}); + +dragHighlight.forEach(eventName => { + dropArea.addEventListener(eventName, highlight, false); +}); + +dragUnighlight.forEach(eventName => { + dropArea.addEventListener(eventName, unhighlight, false); +}); + +dropArea.addEventListener("drop", handleDrop, false); diff --git a/assets/js/waitForLoader.js b/assets/js/waitForLoader.js new file mode 100644 index 0000000..89c9489 --- /dev/null +++ b/assets/js/waitForLoader.js @@ -0,0 +1,29 @@ +const waitForLoader = (DOMSelector, MAX_TIME = 10000) => { + let timeout = 0; + + const waitForContainerElement = (resolve, reject) => { + const container = document.querySelector(DOMSelector); + timeout += 30; + if (timeout >= MAX_TIME) reject("Element not found"); + if (!container || container.length === 0) { + setTimeout(waitForContainerElement.bind(this, resolve, reject), 30); + } else { + resolve(container); + } + }; + return new Promise((resolve, reject) => { + waitForContainerElement(resolve, reject); + }); +}; +let loader = document.createElement("div"); +loader.setAttribute("id", "loading"); +loader.setAttribute("class", "loading"); +loader.innerHTML = "Loading…"; +document.body.appendChild(loader); +waitForLoader("#circle") + .then(res => { + document.getElementById("loading").remove(); + }) + .catch(err => { + alert("Something went wrong, you may need to reinstall H2."); + }); diff --git a/docs/linux.md b/docs/linux.md index 32e2e96..2f30cf5 100644 --- a/docs/linux.md +++ b/docs/linux.md @@ -6,9 +6,9 @@ Follow the steps from below ### Shell script installation -* For first time install clone the repo https://github.com/poush/h2.git -* Run the shell script `cd h2/install` -* `sh ./linux.sh` +- For first time install clone the repo https://github.com/poush/h2.git +- Run the shell script `cd h2/install` +- `sh ./linux.sh` Once started we will be default window like below @@ -18,15 +18,14 @@ Paste any youtube url on opened window using `cmd + shift + v` ![Alt text](../img/step1.png) - Move the window to any place! - ![Alt text](../img/step2.png) ### Problems on Ubuntu 18.04 On Ubuntu 18.04 this error message can be displayed + ``` error while loading shared libraries: libgconf-2.so.4: cannot open shared object file: No such file or directory npm ERR! file sh diff --git a/docs/mac.md b/docs/mac.md index af1c088..663845f 100644 --- a/docs/mac.md +++ b/docs/mac.md @@ -6,9 +6,9 @@ Follow the steps from below ### Shell script installation -* For first time install clone the repo https://github.com/poush/h2.git -* Run the shell script `cd h2/install` -* `sh ./mac.sh` +- For first time install clone the repo https://github.com/poush/h2.git +- Run the shell script `cd h2/install` +- `sh ./mac.sh` Once started we will be default window like below @@ -18,11 +18,6 @@ Paste any youtube url on opened window using `cmd + shift + v` ![Alt text](../img/step1.png) - Move the window to any place! - ![Alt text](../img/step2.png) - - - diff --git a/docs/windows.md b/docs/windows.md index dc894d4..61f956a 100644 --- a/docs/windows.md +++ b/docs/windows.md @@ -1,49 +1,62 @@ # Installation instructions for Windows systems + These instructions have been tested on Windows 10 1803 x64 # Install Node.js - - Go to https://nodejs.org - - Download the "Current" version - - If the homepage doesn't show a download link get it from the "Downloads" section (https://nodejs.org/en/download/current/) - - Run the .msi package and complete the install - - Verify the installation by opening Powershell and typing `node --version` and hitting Enter. The version number of your node installation is shown in the next line - + +- Go to https://nodejs.org +- Download the "Current" version + - If the homepage doesn't show a download link get it from the "Downloads" section (https://nodejs.org/en/download/current/) +- Run the .msi package and complete the install +- Verify the installation by opening Powershell and typing `node --version` and hitting Enter. The version number of your node installation is shown in the next line + # Install h2 -## Cloning from Github - - Clone this repository + +## Cloning from Github + +- Clone this repository + ``` git clone https://github.com/poush/h2` ``` - - Open Powershell - - Change to the folder containing the cloned version - - Install dependencies - this might take a few minutes + +- Open Powershell +- Change to the folder containing the cloned version +- Install dependencies - this might take a few minutes + ``` npm install ``` ## Using the zip file from Github - - Save the zip to your disk - - Unzip it to the desired target location - - Open Powershell - - Change to the folder containing the cloned version (__h2-master__) - - Install dependencies - this might take a few minutes + +- Save the zip to your disk +- Unzip it to the desired target location +- Open Powershell +- Change to the folder containing the cloned version (**h2-master**) +- Install dependencies - this might take a few minutes + ``` npm install --dev ``` # Run h2 + After the installation is complete stay in the h2 directory and type + ``` nmp start ``` # Play a video - - Once the program has started you see the H2 window on top containing the text + +- Once the program has started you see the H2 window on top containing the text + ``` Hello People! Start by pasting any youtube url. For bugs, you can create issues at github.com/poush/h2 ``` - - Open a video on youtube using your browser, copy its URL - - Paste the URL into the H2 window using `++V` and the video automatically starts. - + +- Open a video on youtube using your browser, copy its URL +- Paste the URL into the H2 window using `++V` and the video automatically starts. diff --git a/icons/H2Icon.svg b/icons/H2Icon.svg new file mode 100644 index 0000000..2870bef --- /dev/null +++ b/icons/H2Icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/img/download.png b/img/download.png new file mode 100644 index 0000000..4c52034 Binary files /dev/null and b/img/download.png differ diff --git a/index.html b/index.html index 8850aa7..afbfd99 100644 --- a/index.html +++ b/index.html @@ -1,21 +1,64 @@ - - - H2 - - - -
-

Hello People!

-

Start by pasting any youtube url. For bugs, you can create issues at github.com/poush/h2

+ + + + H2 + + + + + +
+
+
+

Hello People!

+

Start by pasting any youtube url. For bugs, you can create issues at github.com/poush/h2

+
+
+ + + + +
+
+

Drag and drop file here!

+
+
+ +
+ +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
- + - - - - + \ No newline at end of file diff --git a/lib/fullscreen-toggle.js b/lib/fullscreen-toggle.js index 1591877..f1da2f8 100644 --- a/lib/fullscreen-toggle.js +++ b/lib/fullscreen-toggle.js @@ -1,11 +1,11 @@ function fullscreenToggle(screen, escCmd) { - if(!screen.isFullScreen() && !escCmd) { - screen.setFullScreenable(true) - screen.setFullScreen(true) - } else { - screen.setFullScreen(false) - screen.setFullScreenable(false) - } + if (!screen.isFullScreen() && !escCmd) { + screen.setFullScreenable(true); + screen.setFullScreen(true); + } else { + screen.setFullScreen(false); + screen.setFullScreenable(false); + } } -module.exports = fullscreenToggle; \ No newline at end of file +module.exports = fullscreenToggle; diff --git a/lib/notifications.js b/lib/notifications.js index a244600..8ba92cd 100644 --- a/lib/notifications.js +++ b/lib/notifications.js @@ -1,8 +1,6 @@ -module.exports = function (message, onclick=null){ - - console.log(Notification) - let notif = new Notification('H2', { - body: message - }) - -} \ No newline at end of file +module.exports = function(message, onclick = null) { + console.log(Notification); + let notif = new Notification("H2", { + body: message + }); +}; diff --git a/lib/util.js b/lib/util.js index c33031e..9b46a52 100644 --- a/lib/util.js +++ b/lib/util.js @@ -1,3 +1,3 @@ module.exports.resetWindowToFloat = function resetWindowToFloat(window) { window.setAlwaysOnTop(true, "floating", 1); -} \ No newline at end of file +}; diff --git a/main.js b/main.js index 3ead727..362a12a 100644 --- a/main.js +++ b/main.js @@ -1,14 +1,21 @@ -const { app, Menu, Tray, BrowserWindow, globalShortcut, session, ipcMain } = require('electron') -const providers = require('./ServiceProviders/providers') -const fullscreenToggle = require('./lib/fullscreen-toggle') -const utils = require('./lib/util'); +const { + app, + Menu, + Tray, + BrowserWindow, + globalShortcut, + session, + ipcMain +} = require("electron"); +const providers = require("./ServiceProviders/providers"); +const fullscreenToggle = require("./lib/fullscreen-toggle"); +const utils = require("./lib/util"); // Keep a global reference of the window object, if you don't, the window will // be closed automatically when the JavaScript object is garbage collected. -let mainWindow -let tray +let mainWindow; +let tray; function createWindow() { - // create config let config = { width: 400, @@ -17,144 +24,154 @@ function createWindow() { webPreferences: { plugins: true }, - titleBarStyle: 'customButtonsOnHover', - } + titleBarStyle: "customButtonsOnHover" + }; // Create the browser window. - mainWindow = new BrowserWindow(config) - mainWindow.setMenu(null) + mainWindow = new BrowserWindow(config); + mainWindow.setMenu(null); + + // Set always on top - if (process.platform == 'darwin') - app.dock.hide() - + + if (process.platform == "darwin") app.dock.hide(); + mainWindow.setVisibleOnAllWorkspaces(true); mainWindow.setFullScreenable(false); // and load the index.html of the app. - mainWindow.loadFile('index.html') + mainWindow.loadFile("index.html"); // mainWindow.loadURL('https://www.netflix.com') // Open the DevTools. - if (process.env.DEV) - mainWindow.webContents.openDevTools() + if (process.env.DEV) mainWindow.webContents.openDevTools(); // Emitted when the window is closed. - mainWindow.on('closed', function () { + mainWindow.on("closed", function() { // Dereference the window object, usually you would store windows // in an array if your app supports multi windows, this is the time // when you should delete the corresponding element. - mainWindow = null - }) + + mainWindow = null; + }); // Disable new browser windows and popups - mainWindow.webContents.on("new-window", function (e, url) { + mainWindow.webContents.on("new-window", function(e, url) { e.preventDefault(); - providers.run(mainWindow, url) + providers.run(mainWindow, url); mainWindow.focus(); }); - - globalShortcut.register('CommandOrControl+Shift+V', () => { - providers.run(mainWindow) - }) - globalShortcut.register('CommandOrControl+Shift+T', () => { + + globalShortcut.register("CommandOrControl+Shift+V", () => { + providers.run(mainWindow); + }); + globalShortcut.register("CommandOrControl+Shift+T", () => { // toggles translucent mode var translucency = 1; - if(mainWindow.getOpacity() == 1){ - translucency = 0.7 + if (mainWindow.getOpacity() == 1) { + translucency = 0.7; } mainWindow.setOpacity(translucency); - mainWindow.setIgnoreMouseEvents(translucency<1); - }) - - globalShortcut.register('Alt+Shift+T', () => { + mainWindow.setIgnoreMouseEvents(translucency < 1); + }); + + globalShortcut.register("Alt+Shift+T", () => { // brings the window to top always utils.resetWindowToFloat(mainWindow); - }) + }); - globalShortcut.register('CommandOrControl+Shift+Space', () => { - mainWindow.webContents.send('togglePlay', 'ping') - }) - globalShortcut.register('Alt+Shift+F', () => { - fullscreenToggle(mainWindow, false) - }) + globalShortcut.register("CommandOrControl+Shift+1", () => { + mainWindow.webContents.send("pause", "ping"); + }); + globalShortcut.register("CommandOrControl+Shift+2", () => { + mainWindow.webContents.send("play", "ping"); + }); + globalShortcut.register("Alt+Shift+F", () => { + fullscreenToggle(mainWindow, false); + }); // Useful in a scenario where the window becomes irresponsive // and the native "quit" shortcut doesn't work - globalShortcut.register('CommandOrControl+H+Q', () => { - app.quit() - }) + globalShortcut.register("CommandOrControl+H+Q", () => { + app.quit(); + }); } let createMenuTray = () => { - tray = new Tray(__dirname + '/tray.png') + tray = new Tray(__dirname + "/tray.png"); const trayMenus = [ - { role: 'about' }, - { label: 'Exit Fullscreen', - click() { - fullscreenToggle(mainWindow, true) + { role: "about" }, + { + label: "Exit Fullscreen", + click() { + fullscreenToggle(mainWindow, true); + } + }, + { + label: "Quit", + click() { + app.quit(); } }, - { label: 'Quit', click() { app.quit() } }, - { label: 'Bring H2 to the front', + { + label: "Bring H2 to the front", click() { utils.resetWindowToFloat(mainWindow); } } ]; - + const contextMenu = Menu.buildFromTemplate(trayMenus); - tray.setToolTip('H2') - tray.setContextMenu(contextMenu) - tray.setTitle('H2'); - tray.on('click', function (event) { - console.log('called') + tray.setToolTip("H2"); + tray.setContextMenu(contextMenu); + tray.setTitle("H2"); + tray.on("click", function(event) { + console.log("called"); !mainWindow.isFocused() ? mainWindow.focus() : true; - }) - -} + }); +}; // This method will be called when Electron has finished // initialization and is ready to create browser windows. // Some APIs can only be used after this event occurs. -app.on('ready', () => { - session.defaultSession.clearStorageData() - createWindow() + +app.on("ready", () => { + session.defaultSession.clearStorageData(); + createWindow(); utils.resetWindowToFloat(mainWindow); - createMenuTray() -}) + createMenuTray(); +}); -app.on('will-quit', () => { +app.on("will-quit", () => { // Unregister all shortcuts. - globalShortcut.unregisterAll() -}) - + globalShortcut.unregisterAll(); +}); // Quit when all windows are closed. -app.on('window-all-closed', function () { +app.on("window-all-closed", function() { // On OS X it is common for applications and their menu bar // to stay active until the user quits explicitly with Cmd + Q // if (process.platform !== 'darwin') { - app.quit() + app.quit(); // } -}) +}); // Unregister all shortcuts. -app.on('will-quit', () => { - globalShortcut.unregisterAll() - -}) +app.on("will-quit", () => { + globalShortcut.unregisterAll(); +}); -app.on('activate', function () { +app.on("activate", function() { // On OS X it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. if (mainWindow === null) { - createWindow() + createWindow(); } -}) +}); -ipcMain.on('exit-full-screen', () => { - fullscreenToggle(mainWindow, true) -}) +ipcMain.on("exit-full-screen", () => { + fullscreenToggle(mainWindow, true); +}); // In this file you can include the rest of your app's specific main process -// code. You can also put them in separate files and require them here. \ No newline at end of file +// code. You can also put them in separate files and require them here. diff --git a/renderer.js b/renderer.js index c481c82..c712749 100644 --- a/renderer.js +++ b/renderer.js @@ -2,8 +2,8 @@ // be executed in the renderer process for that window. // All of the Node.js APIs are available in this process. -let { clipboard, ipcRenderer } = require('electron') -const notif = require('./lib/notifications') +let { clipboard, ipcRenderer } = require("electron"); +const notif = require("./lib/notifications"); let player; @@ -12,35 +12,34 @@ let apiPromise = new Promise(resolve => { window.onYouTubeIframeAPIReady = () => { // console.log("YouTube API loaded"); resolve(); - } - const tag = document.createElement('script'); + }; + const tag = document.createElement("script"); tag.src = "https://www.youtube.com/iframe_api"; - const firstScriptTag = document.getElementsByTagName('script')[0]; + const firstScriptTag = document.getElementsByTagName("script")[0]; firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); }); - async function putYoutube(videoId) { - - await apiPromise + await apiPromise; player = new YT.Player(document.querySelector("#video"), { - height: '100%', - width: '100%', + height: "100%", + width: "100%", videoId: videoId, - playerVars: { - 'autoplay': 1, - 'fs': 0, - 'modestbranding': 1 + playerVars: { + autoplay: 1, + fs: 0, + modestbranding: 1 }, events: { - onReady: (event) => { + onReady: event => { // console.log(event) }, - onStateChange: (event) => { + onStateChange: event => { // console.log(event) } } }); + document.body.innerHTML += ``; } function togglePlay() { @@ -50,16 +49,14 @@ function togglePlay() { } else if (player.getPlayerState() != 1) { player.playVideo() } - } else { alert("Play a video first"); } } function defaultiFrame(arg) { - let web = `` - document.querySelector('#video').innerHTML = web - + let web = ``; + document.querySelector("#video").innerHTML = web; } window.addEventListener('keyup', function(e){ @@ -71,18 +68,18 @@ ipcRenderer.on('togglePlay', (ev, arg) => { togglePlay() }) -ipcRenderer.on('youtube', (ev, arg) => { - console.log('called') - putYoutube(arg) -}) +ipcRenderer.on("youtube", (ev, arg) => { + console.log("called"); + putYoutube(arg); +}); -ipcRenderer.on('googleDocs', (ev, arg) => { - defaultiFrame(arg) -}) +ipcRenderer.on("googleDocs", (ev, arg) => { + defaultiFrame(arg); +}); -ipcRenderer.on('invalidUrl', () => { - notif('Oops! This isn\'t supported URL') -}) +ipcRenderer.on("invalidUrl", () => { + notif("Oops! This isn't supported URL"); +}); ipcRenderer.on("alertUser", (event, message, url) => { var userInput = confirm(message); diff --git a/storage/storage.js b/storage/storage.js index 48f9e83..f085821 100644 --- a/storage/storage.js +++ b/storage/storage.js @@ -1,42 +1,44 @@ /** * A simple storage utility to save the user config as json file - * + * */ -const electron = require('electron'); -const path = require('path'); -const fs = require('fs'); - class Store { +const electron = require("electron"); +const path = require("path"); +const fs = require("fs"); +class Store { constructor(opts = {}) { let defaultConfig = { - configName: opts.configName || 'user-preference', + configName: opts.configName || "user-preference", defaults: {} - } - opts.defaults = Object.assign(defaultConfig.defaults, opts.defaultConfig); - const userDataPath = (electron.app || electron.remote.app).getPath('userData'); + }; + opts.defaults = Object.assign(defaultConfig.defaults, opts.defaultConfig); + const userDataPath = (electron.app || electron.remote.app).getPath( + "userData" + ); // Use the `configName` property to set the file name and path.join to bring it all together as a string - this.path = path.join(userDataPath, opts.configName + '.json'); - + this.path = path.join(userDataPath, opts.configName + ".json"); + this.data = parseDataFile(this.path, opts.defaults); } - + get(key) { return this.data[key]; } - + set(key, val) { this.data[key] = val; - try{ + try { fs.writeFileSync(this.path, JSON.stringify(this.data)); - }catch(err) { - console.error('Failed to save user config'); + } catch (err) { + console.error("Failed to save user config"); } } } - function parseDataFile(filePath, defaults) { +function parseDataFile(filePath, defaults) { try { return JSON.parse(fs.readFileSync(filePath)); - } catch(error) { + } catch (error) { return defaults; } } - module.exports = Store; \ No newline at end of file +module.exports = Store;