diff --git a/.github/workflows/integrate.yml b/.github/workflows/integrate.yml index dde5de7..ed64761 100644 --- a/.github/workflows/integrate.yml +++ b/.github/workflows/integrate.yml @@ -1,4 +1,4 @@ -name: Build Test +name: build on: push: @@ -9,7 +9,7 @@ on: branches: [ main ] jobs: - test_pull_request: + test_build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 diff --git a/README.md b/README.md index cd8be59..3f6e192 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,17 @@ +# DEV NOTE: + +Hey! It's been a long while since I last updated Random Browser. It's currently December 23th 2023, v2.0.0 is finally out, or at least, it'll be soon. + +Since the first line of code, Random Browser was just an experiment and I didn't even plan to update it... + +But here we are, Random Browser's last release is out. + +This version of Random Browser won't receive new features, just small bug fixes (if any). + +Note how I said "this version", though. I've been planning to migrate the entire projet to a different framework or programming language. + +We'll see... +

Random Browser

@@ -40,7 +54,7 @@ Once everything is installed, build the typescript code: ```bash yarn build ``` -**NOTE: typescript is not listed as a dependency in package.json. It should be installed globally on your computer, if not, you can install it globally or in the cloned repository.** + And **you're done**! Now run: ```bash @@ -75,7 +89,6 @@ Once everything is installed, build the typescript code: ```bash yarn build ``` -**NOTE: typescript is not listed as a dependency in package.json. It should be installed globally on your computer, if not, you can install it globally or in the cloned repository.** Now, you have to run the project from the command line before packaging, just run: @@ -87,7 +100,7 @@ When you do that, the public folder (where all resources for views are) will be > If something goes wrong, you can copy the folder manually. Just copy **./src/public/** to **./build/** -Once this is done and the browser's window is now open, you can close the browser and finally package the app: +Once this is done and the browser's window is open, you can close the browser and finally package the app: For Windows: ```bash @@ -107,10 +120,10 @@ yarn pack-mac When that's finished, you should see a folder named **dist**, inside of it you can find executable. ## Contributing setup -Sorry, this setup is not documented yet and neither is the project's code. You can follow the [development setup](#development-setup), just make sure to follow these rules: +> **** Currently the project's code is not so well documented as I'd like. I'll be working on documenting everything better so contributors can more easily understand what the code is doing. If you'd like to contribute, I'm glad having your help on this project, just know that it may be a little confusing for now. -- **USE** Yarn -- Make sure to exclude any file that shouldn't be uploaded to github. + +Before you can get to contributing, please read the `contributing` file. # License Creative Commons License
Random Browser by YisusGaming is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License. diff --git a/changelog.txt b/changelog.txt new file mode 100644 index 0000000..be0130d --- /dev/null +++ b/changelog.txt @@ -0,0 +1,12 @@ +[2.0.0] => { + Features => { + [DONE] Custom App Frame. + [DONE] New Tab System. + [DONE] Initial App Loader. + [DONE] Download Progress Bar. + [DELAYED->v3.0.0] New Settings Menu. + } + Fixes => { + [DONE] Fixed Random Browser's Page Link. + } +} diff --git a/package.json b/package.json index 95f2be6..5af8913 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "private": true, "name": "random-browser", "productName": "Random Browser", - "version": "1.2.2", + "version": "2.0.0", "description": "Just a random browser...", "main": "build/index.js", "scripts": { @@ -10,9 +10,7 @@ "build": "tsc", "start:dev": "nodemon build/index.js", "build:dev": "tsc -w", - "pack-mac": "electron-packager . --overwrite --platform=darwin --arch=x64 --icon=assets/icons/icon.icns --prune=true --out=dist", - "pack-win": "electron-packager . random-browser --overwrite --asar=false --platform=win32 --arch=ia32 --icon=assets/icons/icon.ico --prune=true --out=dist --version-string.CompanyName=YisusGaming --version-string.FileDescription=\"Just a random browser...\" --version-string.ProductName=\"Random Browser\"", - "pack-linux": "electron-packager . random-browser --overwrite --asar=false --platform=linux --arch=x64 --icon=assets/icons/1024x1024.png --prune=true --out=dist" + "pack-win": "electron-packager . random-browser --overwrite --asar=false --platform=win32 --arch=ia32 --icon=assets/icons/icon.ico --prune=true --out=dist --version-string.CompanyName=YisusGaming --version-string.FileDescription=\"Just a random browser...\" --version-string.ProductName=\"Random Browser\"" }, "repository": "https://github.com/YisusGaming/random-browser", "author": "YisusGaming", @@ -22,6 +20,7 @@ }, "devDependencies": { "electron-packager": "^17.1.1", - "nodemon": "^2.0.20" + "nodemon": "^2.0.20", + "typescript": "^4.9.5" } } diff --git a/src/config/app.json b/src/config/app.json index 11f88d2..63cb415 100644 --- a/src/config/app.json +++ b/src/config/app.json @@ -1,3 +1,3 @@ { - "version": "v1.2.2" + "version": "v2.0.0" } \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index d189c6e..79890e0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,68 +1,100 @@ -import { app, BrowserWindow, ipcMain, Menu, MenuItemConstructorOptions } from 'electron'; +import { app, BrowserWindow, ipcMain, Menu } from 'electron'; +import Logger from './logs/Logger.js'; +import TabManager from './tabs/TabManager.js'; import configs from './config/app.json'; import './config/user.json'; import fs from 'fs'; import path from 'path'; -const publicPath = path.join(__dirname, 'public'); +// Instance of the logger class for future console logs. +// Also exports the instance for use in other files +// without the need to reinstance the class again. +export const logger = new Logger(configs.version); + +export const publicPath = path.join(__dirname, 'public'); const userConfigPath = path.join(__dirname, 'config', 'user.json'); -console.log(`Looking for user's configs in ${userConfigPath}`); +// console.log(`Looking for user's configs in ${userConfigPath}`); +logger.logMessage(`Looking for user's configs in ${userConfigPath}`); let main : BrowserWindow; -let searchWin : BrowserWindow; +let tabModal : BrowserWindow; let backgroundSelect : BrowserWindow; +let appLoader : BrowserWindow; app.on('ready', () => { + logger.logMessage(`App is ready.`); + spawnAppLoader(); main = new BrowserWindow({ title: 'Random Browser - Loading...', webPreferences: { nodeIntegration: true, contextIsolation: false }, + frame: false, show: false }); + // ! Make sure to set the application menu to null in production. Menu.setApplicationMenu(null); main.loadFile(path.join(publicPath, 'index.html')); main.on('ready-to-show', () => { + logger.logMessage(`Main window ready to show.`); main.show(); - main.maximize(); + // ! Make sure to destoy appLoader in production. + appLoader.destroy(); const rawConfig = fs.readFileSync(userConfigPath, { encoding: 'utf-8' }); const configs = JSON.parse(rawConfig); main.webContents.send('update-background', configs.background); }); - main.on('close', () => { - if (backgroundSelect != null) { - try { - backgroundSelect.close(); - } catch(err) { - console.log(`[FAILED] close background select window [->] ${err}.`); - } - } - }); }); -function searchWindow(url: string) { - searchWin = new BrowserWindow({ - title: `Searching ${url}...`, - minimizable: false - }); - searchWin.loadURL(url); +/** + * Works as a *bridge* or *gateway* that allows communication with main's webCotents. + * @param event The event that will be send to main via `webContents.send` + * + * @throws {EvalError} if the string passed as event is empty. + */ +export function mainWindowGateway(event: string, ...args: any[]): void { + logger.tempLogMessage(`Main Gateway used, event: ${event}`); + if (event.trim() == '') throw new EvalError("Event cannot be empty."); + + main.webContents.send(event, args); +} - const menuTemplate: Array = [ - { - label: 'Refresh', - role: 'reload' +function spawnAppLoader() { + logger.logMessage("App loader spawned."); + appLoader = new BrowserWindow({ + webPreferences: { + nodeIntegration: true, + contextIsolation: false }, - { - label: 'Inspect', - role: 'toggleDevTools' - } - ] + title: 'Random Browser - Loading...', + frame: false, + closable: false, + maximizable: false, + resizable: false, + height: 400, + width: 320 + }); + appLoader.loadFile(path.join(publicPath, 'loader.html')); + appLoader.on('closed', () => { + logger.logMessage("App loader closed."); + main.flashFrame(true); + }); +} - searchWin.setMenu(Menu.buildFromTemplate(menuTemplate)); +/** + * Createsa a Tab and builds it. + * Tabs will show up once they're built. + */ +export function spawnTab(url: string) { + tabModal = TabManager.createTab(url, main).build(); } +/** + * Spawns a Browser Window for the user to select a custom + * background. + */ function selectBackground() { backgroundSelect = new BrowserWindow({ title: "Change Browser's Background", @@ -71,26 +103,49 @@ function selectBackground() { contextIsolation: false }, minimizable: false, - maximizable: false + maximizable: false, + parent: main }); backgroundSelect.setMenu(null); backgroundSelect.loadFile(path.join(publicPath, 'background.html')); } -/* IPC */ +ipcMain.on('minimize-main', (event) => { + // Minimize the main window + main.minimize(); +}); + +ipcMain.on('maximize-main', (event) => { + // Maximize the main window + main.maximize(); +}); + +ipcMain.on('close-main', (event) => { + // Close the main window + main.close(); +}); + +ipcMain.on('open-tab', (event, tabId: number) => { + TabManager.openTab(tabId); +}); + +ipcMain.on('tab-deleted', (event, tabId: number) => { + TabManager.deleteTab(tabId); +}); + ipcMain.on('new-search', (event, search: string) => { if (search.trim() == '') return; if (/[*.*]/g.test(search)) { if (search.startsWith('https://') || search.startsWith('http://')) { - searchWindow(search); + spawnTab(search); return; } - searchWindow(`https://${search}`); + spawnTab(`https://${search}`); return; } - searchWindow(`https://google.com/search?q=${search}`); + spawnTab(`https://google.com/search?q=${search}`); }); ipcMain.on('new-background-image', (event) => { @@ -126,4 +181,8 @@ ipcMain.on('clear-background', (event) => { backgroundSelect.close(); main.webContents.send('update-background', ''); main.loadFile(path.join(publicPath, 'index.html')); -}); \ No newline at end of file +}); + +ipcMain.on('minimize-apploader', (event) => { + appLoader.minimize(); +}); diff --git a/src/logs/Logger.ts b/src/logs/Logger.ts new file mode 100644 index 0000000..aebc4a7 --- /dev/null +++ b/src/logs/Logger.ts @@ -0,0 +1,54 @@ +/** + * The logger class. + * @since v2.0.0 + * + * Prints messages to `stdout` and `stderr`. + */ +export default class Logger { + private browserVersion: string; + + /** + * @param browserVersion Random Browser's current version allocated in `app.json` + */ + constructor(browserVersion: string) { + this.browserVersion = browserVersion; + } + + /** + * Prints a message to the `stdout`. + */ + logMessage(msg: string): void { + console.log(`[LOG:${this.browserVersion}] => ${msg}`); + } + + /** + * Prints a message to the `stdout` with a label + * saying it should be removed before the app + * gets to production. + */ + tempLogMessage(msg: string): void { + console.log(`The following log is temporal and should be removed before production.\n[TEMPORAL LOG:${this.browserVersion}] => ${msg}`); + } + + /** + * Prints a warning to the `stderr`. + */ + logWarning(warn: string): void { + console.warn(`[WARNING!] => ${warn} | ${this.browserVersion}`); + } + + /** + * Prints an `string` or an `Error` to `stderr`. + */ + logError(err: string | Error, origin: { file: string }): void { + if (typeof err == 'string') { + console.error( + `[ERR!] => ${err} | ${this.browserVersion} [${origin.file}]` + ); + } else { + console.error( + `[ERR!] => ${err.name}: ${err.message} | ${this.browserVersion} [${origin.file}]` + ); + } + } +} diff --git a/src/public/assets/icon/icon.ico b/src/public/assets/icon/icon.ico new file mode 100644 index 0000000..874fbbd Binary files /dev/null and b/src/public/assets/icon/icon.ico differ diff --git a/src/public/css/download.css b/src/public/css/download.css new file mode 100644 index 0000000..6dc8084 --- /dev/null +++ b/src/public/css/download.css @@ -0,0 +1,78 @@ +@import url('https://fonts.googleapis.com/css2?family=Raleway:wght@100;200;300;400;500;600;700&display=swap'); + +* { + margin: 0; + padding: 0; +} + +body { + position: relative; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + font-family: 'Raleway', sans-serif; + background: #202020; + color: #ffffff; +} + +.frame { + padding: 20px; + border: #303030 2px solid; + border-radius: 8px; +} + +#download-info { + display: flex; + flex-direction: column; + justify-content: center; + text-align: center; + align-items: center; + margin-bottom: 10px; +} + +.btns { + display: flex; + justify-content: center; + align-items: center; +} + +button { + background: transparent; + color: #ffffff; + padding: 8px; + margin: 0 4px; + border-radius: 8px; + border: #ffffff 2px solid; + cursor: pointer; + transition: 250ms; +} + +button#btn-resume { + border-color: #101060; +} + +button#btn-resume:hover { + background: #101060; +} + +button#btn-pause { + border-color: #dddd00; +} + +button#btn-pause:hover { + background: #dddd00; +} + +button#btn-cancel { + border-color: #dd0020; +} + +button#btn-cancel:hover { + background: #dd0020; +} + +button:disabled { + cursor: not-allowed; + opacity: .7; +} diff --git a/src/public/css/index.css b/src/public/css/index.css index 83943b1..14936cf 100644 --- a/src/public/css/index.css +++ b/src/public/css/index.css @@ -24,6 +24,92 @@ body:has(:focus) { height: 100vh; } +#tabs-container { + -webkit-app-region: no-drag; + display: flex; + flex-direction: row; + grid-area: tabs; + width: 70%; + overflow-x: scroll; +} + +#tabs-container::-webkit-scrollbar { + height: 4px; +} + +#tabs-container::-webkit-scrollbar-thumb { + background: white; +} + +.tab { + -webkit-app-region: no-drag; + display: flex; + justify-content: space-between; + width: 100%; + padding: 10px; + max-width: 100px; + margin: 0 4px; + background: #202020; + transition: 250ms; +} + +.tab p { + cursor: pointer; + font-family: 'Raleway', sans-serif; +} + +.tab:has(p:hover) { + background: #505050; +} + +.tab button { + background: transparent; + border: none; + color: white; + cursor: pointer; +} + +.tab button:hover { + color: lightcoral; +} + +.tab button:active { + color: red; +} + +.frame { + position: fixed; + -webkit-app-region: drag; + width: 100vw; + display: grid; + grid-template-columns: 1fr auto; + grid-template-areas: + "tabs buttons"; + background: #101010; + padding: 8px 0; +} + +.frame #buttons { + padding: 0 6px; + margin: auto; + grid-area: buttons; +} + +.frame #buttons button { + font-size: 1.6rem; + cursor: pointer; + background: transparent; + color: #606060; + border: none; + margin: 0 4px; + -webkit-app-region: no-drag; + transition: 250ms all; +} + +.frame #buttons button:hover { + color: #ffffff; +} + #legend { font-family: 'Raleway', sans-serif; font-size: 2.8rem; @@ -102,31 +188,9 @@ body:has(:focus) { padding: 5px; } -#about-btn { - position: absolute; - top: 0; - left: 0; - font-size: 2.3rem; - background: transparent; - color: #707070; - text-decoration: none; - padding: 10px; - border: none; - transform-origin: center; - transition: all 250ms; -} - -#about-btn:hover { - color: #ffffff; -} - -#about-btn:active { - color: #000000; -} - #background-btn { position: absolute; - top: 0; + top: 40px; right: 0; font-size: 2.3rem; background: transparent; diff --git a/src/public/css/loader.css b/src/public/css/loader.css new file mode 100644 index 0000000..c046089 --- /dev/null +++ b/src/public/css/loader.css @@ -0,0 +1,47 @@ +@import url('https://fonts.googleapis.com/css2?family=Raleway:wght@100;200;300;400;500;600;700&display=swap'); + +* { + margin: 0; + padding: 0; +} + +body { + position: relative; + height: 100vh; + width: 100vw; + -webkit-app-region: drag; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-family: 'Raleway', sans-serif; + background: #202020; + color: #ffffff; +} + +.button-wrap { + position: fixed; + padding: 5px; + top: 0; + left: 0; +} + +.button-wrap button { + -webkit-app-region: no-drag; + background-color: transparent; + color: white; + border: 2px solid #505050; + padding: 6px; + cursor: pointer; + transition: 250ms; +} + +.button-wrap button:hover { + background: #505050; +} + +img, +h1, +p { + -webkit-app-region: no-drag; +} \ No newline at end of file diff --git a/src/public/css/user.css b/src/public/css/user.css deleted file mode 100644 index e69de29..0000000 diff --git a/src/public/download.html b/src/public/download.html new file mode 100644 index 0000000..655b473 --- /dev/null +++ b/src/public/download.html @@ -0,0 +1,26 @@ + + + + + + Downloading... + + + +
+
+

Dowloading file...

+

Status: ?

+

Progress: ?

+

0 bytes out of ? bytes

+
+
+ + + +
+
+ + + + diff --git a/src/public/index.html b/src/public/index.html index 8cfa1df..d262ed7 100644 --- a/src/public/index.html +++ b/src/public/index.html @@ -6,12 +6,25 @@ Random Browser - - - +
+
+
+ + + +
+
+ +

Random Browser

`; + tabsContainer.innerHTML += tabTemplate; + document.querySelectorAll('.delete-tab-btn').forEach(( /** @type {HTMLButtonElement} */ btn) => { + btn.onclick = () => { + btn.parentElement.remove(); + ipcRenderer.send('tab-deleted', Number(btn.dataset.tabId)); + } + }); + document.querySelectorAll(".tab-title").forEach(( /** @type {HTMLParagraphElement} */ title) => { + title.onclick = () => { + ipcRenderer.send('open-tab', Number(title.dataset.tabId)); + }; + }); +}); /* Configs */ ipcRenderer.on('update-background', (event, file) => { @@ -6,11 +56,6 @@ ipcRenderer.on('update-background', (event, file) => { updateBackground(file); }); -/* Learn Page */ -document.getElementById('about-btn').addEventListener('click', (event) => { - ipcRenderer.send('new-search', 'https://yisusgaming.github.io/random-browser'); -}); - /* Browser Background */ document.getElementById('background-btn').addEventListener('click', (event) => { ipcRenderer.send('new-background-image'); @@ -34,10 +79,12 @@ ipcRenderer.on('app-version', (event, version) => { }); /** - * @param {string} file + * @param {string} url */ -function updateBackground(file) { - const parsedUrl = new URL(file); +function updateBackground(url) { + if (url.trim() == "") return; + + const parsedUrl = new URL(url); document.body.style.backgroundSize = 'cover'; document.body.style.backgroundImage = `url('${parsedUrl.toString()}')`; -} \ No newline at end of file +} diff --git a/src/public/js/loader.js b/src/public/js/loader.js new file mode 100644 index 0000000..9cd0bdc --- /dev/null +++ b/src/public/js/loader.js @@ -0,0 +1,5 @@ +const { ipcRenderer } = require('electron'); + +document.getElementById('background-load-btn').addEventListener('click', () => { + ipcRenderer.send('minimize-apploader'); +}); \ No newline at end of file diff --git a/src/public/loader.html b/src/public/loader.html new file mode 100644 index 0000000..b39fcb1 --- /dev/null +++ b/src/public/loader.html @@ -0,0 +1,20 @@ + + + + + + + Random Browser - Loading... + + + +
+ +
+ Random Browser Icon. If it's not visible, check your internet connection. +

Please wait,

+

Loading the browser...

+ + + + \ No newline at end of file diff --git a/src/tabs/BrowserTab.ts b/src/tabs/BrowserTab.ts new file mode 100644 index 0000000..049b088 --- /dev/null +++ b/src/tabs/BrowserTab.ts @@ -0,0 +1,180 @@ +import { BrowserWindow, Menu, MenuItemConstructorOptions, webContents } from 'electron'; +import DownloadHandler from './downloads/DownloadHandler.js'; +import { spawnTab } from '../index.js'; + +/** + * The BrowserTab class. + * @since v2.0.0 + * + * This class shouldn't be instanced directly. Use `TabManager.createTab` instead. + */ +export default class BrowserTab { + private url: string; + private parent: BrowserWindow; + private tabId: number; + private visitedUrls: string[]; + private currentUrl: number; + + /** + * @param url The URL the tab is pointing to. + * @param parent The parent of this modal. It should be the main browser's window. + * @param tabId A zero-based id that identifies this tab. Provided automatically if the tab instance was created with `TabManager.createTab`. + */ + constructor(url: string, parent: BrowserWindow, tabId: number) { + this.url = url; + this.parent = parent; + this.tabId = tabId; + this.visitedUrls = []; + this.currentUrl = 0; + } + + public get TabId() : number { + return this.tabId; + } + + /** + * @param window The window that owns this menu. + */ + private tabMenu(window: BrowserWindow): Menu { + let template: MenuItemConstructorOptions[] = [ + { + label: 'Go to First', + click: () => { + this.currentUrl = 0; + window.loadURL(this.visitedUrls[this.currentUrl]); + } + }, + { + label: 'Go Back', + click: () => { + if (this.currentUrl <= 0) return; + + this.currentUrl--; + window.loadURL(this.visitedUrls[this.currentUrl]); + } + }, + { + label: 'Go Forwards', + click: () => { + if (this.currentUrl + 1 >= this.visitedUrls.length) return; + + this.currentUrl++; + window.loadURL(this.visitedUrls[this.currentUrl]); + } + }, + { + type: 'separator' + }, + { + label: 'Page Actions', + submenu: [ + { + role: 'copy' + }, + { + role: 'cut' + }, + { + role: 'paste' + }, + { + role: 'undo' + }, + { + role: 'redo' + }, + { + label: 'Inspect', + role: 'toggleDevTools' + } + ] + }, + { + type: 'separator' + }, + { + label: `Tab ${this.tabId + 1} Controls`, + submenu: [ + { + label: 'Close Tab', + role: 'close' + }, + { + role: 'reload' + }, + { + label: `Zoom (${Math.floor(window.webContents.zoomFactor * 100)}%)`, + submenu: [ + { + role: 'zoomIn' + }, + { + role: 'zoomOut' + }, + { + role: 'resetZoom' + } + ] + } + ] + } + ]; + + return Menu.buildFromTemplate(template); + } + + /** + * Builds, shows and returns the tab modal. + */ + public build(): BrowserWindow { + let tabModal = new BrowserWindow({ + title: `Searching ${this.url}...`, + parent: this.parent, + modal: true, + frame: false, + width: this.parent.getSize()[0], + height: this.parent.getSize()[1], + x: this.parent.getBounds().x, + y: this.parent.getBounds().y, + resizable: false, + }); + + tabModal.webContents.addListener('context-menu', () => { + this.tabMenu(tabModal).popup({ + window: tabModal + }); + }); + + tabModal.webContents.addListener('did-navigate', (event, url) => { + if (this.visitedUrls.lastIndexOf(url) == -1) { + this.visitedUrls.push(url); + } + let index = this.visitedUrls.lastIndexOf(url); + // If index is equal or greater than 0 use index, + // otherwise, use 0. + this.currentUrl = index >= 0 ? index : 0; + + console.log(this.visitedUrls, this.currentUrl); + }); + + tabModal.webContents.addListener('did-create-window', (window, details) => { + window.destroy(); // Destroying the created window + spawnTab(details.url); // Opening a tab with the destroyed window's url. + }); + tabModal.webContents.session.on('will-download', (event, item, webContents) => { + new DownloadHandler(item, this.parent); + }); + + // Center the tab modal if the main window is maximized to make sure it fills all the screen. + if (this.parent.isMaximized()) { + tabModal.setBounds({ + x: 0, + y: 0, + }); + } + tabModal.loadURL(this.url); + this.parent.webContents.send('tab-builded', this.tabId); + + return tabModal; + } +} diff --git a/src/tabs/TabManager.ts b/src/tabs/TabManager.ts new file mode 100644 index 0000000..b608d07 --- /dev/null +++ b/src/tabs/TabManager.ts @@ -0,0 +1,71 @@ +import { BrowserWindow } from 'electron'; +import BrowserTab from './BrowserTab.js'; +import { logger } from '../index.js'; + +/** + * TabManager class + * @since v2.0.0 + * + * Manages all the tab system. + */ +class TabManager { + private tabs: BrowserTab[]; + constructor() { + this.tabs = []; + } + + /** + * Creates a new tab and stores it in a list of tabs. + * @param url The url that the tab will load. + * @param parent The parent of this tab. In most cases it'll be the main browser's window. + */ + public createTab(url: string, parent: BrowserWindow): BrowserTab { + let tabId = this.generateId(); + let tab = new BrowserTab(url, parent, tabId); + this.tabs.push(tab); + + return tab; + } + + /** + * Opens a tab, previously created and stored in the + * tabs array. + */ + public openTab(tabId: number): void { + let tabIndex = this.tabs.findIndex((val, index) => { + return val.TabId == tabId; + }); + let tab = this.tabs[tabIndex]; + if (tab) { + tab.build(); + } else { + logger.logError(`TAB NOT FOUND. Tab id ${tabId} was not found.`, { + file: 'TabManager.ts' + }); + } + } + + /** + * Deletes a tab from the tab list. + */ + public deleteTab(tabId: number): void { + this.tabs = this.tabs.filter((tab, index) => { + return tab.TabId != tabId; + }); + } + + /** + * Generates an unique ID for a tab. + */ + private generateId(): number { + let ids : Array = []; + this.tabs.forEach((tab) => { + ids.push(tab.TabId); + }); + + ids.sort(); // Sort in a way that the bigger ID is the last element of the array. + return ids.pop()! + 1 || 0; // Return the last element of the array (bigger id) + 1, or return 0 if the array is empty. + } +} + +export default new TabManager(); diff --git a/src/tabs/downloads/DownloadHandler.ts b/src/tabs/downloads/DownloadHandler.ts new file mode 100644 index 0000000..2f6c832 --- /dev/null +++ b/src/tabs/downloads/DownloadHandler.ts @@ -0,0 +1,106 @@ +import { DownloadItem, BrowserWindow, dialog } from "electron"; +import { logger, publicPath } from "../../index.js"; + +import path from 'path'; + +export interface Download { + id: number; + filename: string, + totalBytes: number, + receivedBytes: number, + item: DownloadItem +} + +/** + * DownloadHandler class + * @since v2.0.0 + * + * Handles downloads, typically triggered in tabs. + */ +export default class DownloadHandler { + /** + * Creates a new DownloadHandler for an specific download. + * + * Once a DownloadHandler is constructed, it automatically + * starts handling it. This creates a window where the progress + * is shown to the user, and other things. + * + * @param item The item being downloaded. + * @param parent The parent for this handler's progress window. + */ + constructor(item: DownloadItem, parent: BrowserWindow) { + logger.logMessage(`Download triggered.`); + + let downloadModal: BrowserWindow = new BrowserWindow({ + title: 'Downloading...', + parent: parent, + resizable: false, + height: 300, + width: 400, + webPreferences: { + nodeIntegration: true, + contextIsolation: false + } + }); + + parent.on('close', () => { + item.cancel(); + downloadModal.destroy(); + return; + }); + + downloadModal.on('close', () => { + item.cancel(); + downloadModal.destroy(); + return; + }); + + downloadModal.loadFile(path.join(publicPath, "download.html")); + + logger.logMessage(`Spawned download window.`); + + downloadModal.webContents.ipc.on('download-resume', () => { + if (item.canResume()) { + item.resume(); + return; + } + + dialog.showErrorBox("Can't resume", "Download can't be resumed."); + }); + + downloadModal.webContents.ipc.on('download-pause', () => { + item.pause(); + }); + + downloadModal.webContents.ipc.on('download-cancel', () => { + item.cancel(); + }); + + item.on('updated', (_event, state) => { + downloadModal.webContents.send('title-update', `${item.getFilename()}...`); + if (state == 'interrupted') { + downloadModal.webContents.send('status-update', 'Status: Interrupted.'); + } else if (state == 'progressing') { + if (item.isPaused()) { + downloadModal.webContents.send('status-update', 'Status: Paused.'); + } else { + downloadModal.webContents.send('status-update', 'Status: Downloading.'); + downloadModal.webContents.send('progress-update', + `Progress: ${Math.round((item.getReceivedBytes() / item.getTotalBytes()) * 100)}%`, + `${item.getReceivedBytes()} bytes out of ${item.getTotalBytes()} bytes.` + ); + } + } + }); + + item.once('done', (_event, state) => { + if (state == 'completed') { + logger.logMessage(`Download completed succesfully.`); + downloadModal.webContents.send('status-update', 'Status: Completed.'); + } else { + downloadModal.webContents.send('status-update', `Status: Failed, ${state}.`); + logger.logWarning(`Download failed: ${state}`); + } + }); + } +} diff --git a/tsconfig.json b/tsconfig.json index 3cb2598..20d8693 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -63,7 +63,7 @@ // "newLine": "crlf", /* Set the newline character for emitting files. */ // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ - // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ @@ -77,7 +77,7 @@ /* Type Checking */ "strict": true /* Enable all strict type-checking options. */, - // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ diff --git a/yarn.lock b/yarn.lock index 05c33ea..a8b2544 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1229,6 +1229,11 @@ type-fest@^0.13.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.13.1.tgz#0172cb5bce80b0bd542ea348db50c7e21834d934" integrity sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg== +typescript@4.9.5: + version "4.9.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" + integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== + undefsafe@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c"