├── .browserslistrc ├── .editorconfig ├── .electron-builder.config.js ├── .electron-vendors.cache.json ├── .eslintrc.json ├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md ├── actions │ ├── discord-release │ │ ├── 192.index.js │ │ ├── action.yml │ │ ├── index.js │ │ └── licenses.txt │ └── release-notes │ │ ├── action.yml │ │ ├── licenses.txt │ │ └── main.js ├── dependabot.yml ├── pull_request_template.md ├── renovate.json └── workflows │ ├── codeql-analysis.yml │ ├── lint.yml │ ├── release-beta.yml │ ├── release.yml │ ├── tests.yml │ ├── typechecking.yml │ ├── update-electron-vendors.yml │ └── wiki-update.yml ├── .gitignore ├── .idea ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── deployment.xml ├── inspectionProfiles │ └── Project_Default.xml ├── jsLibraryMappings.xml ├── jsLinters │ └── eslint.xml ├── modules.xml ├── vcs.xml ├── vite-electron-builder.iml └── webResources.xml ├── .nano-staged.json ├── .simple-git-hooks.json ├── .vscode ├── extensions.json ├── i18n-ally-reviews.yml └── settings.json ├── CONTACT.md ├── Dockerfile ├── INSTALL.md ├── LICENSE ├── README.md ├── buildResources ├── .gitkeep ├── icon.svg ├── icon_128.png ├── icon_16.png ├── icon_256.ico ├── icon_256.png ├── icon_32.ico ├── icon_32.png └── icon_64.png ├── contributing.md ├── docker-compose-example.yml ├── package-lock.json ├── package.json ├── packages ├── api │ ├── src │ │ ├── app │ │ │ ├── index.ts │ │ │ └── types │ │ │ │ └── index.ts │ │ ├── client │ │ │ ├── index.ts │ │ │ └── types │ │ │ │ └── index.ts │ │ ├── db │ │ │ ├── helpers │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── mangas.ts │ │ │ ├── settings.ts │ │ │ ├── tokens.ts │ │ │ └── uuids.ts │ │ ├── env.d.ts │ │ ├── index.ts │ │ ├── models │ │ │ ├── abstracts │ │ │ │ ├── index.ts │ │ │ │ ├── mymangareadercms.ts │ │ │ │ └── selfhosted.ts │ │ │ ├── fallenangels.ts │ │ │ ├── icons │ │ │ │ ├── amr-importer.png │ │ │ │ ├── fallenangels.png │ │ │ │ ├── komga-importer.png │ │ │ │ ├── komga.png │ │ │ │ ├── mangadex-importer.png │ │ │ │ ├── mangadex.png │ │ │ │ ├── mangafox.png │ │ │ │ ├── mangahasu.png │ │ │ │ ├── scanfr.png │ │ │ │ ├── tachidesk-importer.png │ │ │ │ └── tachidesk.png │ │ │ ├── imports │ │ │ │ ├── abstracts │ │ │ │ │ └── index.ts │ │ │ │ ├── all-mangas-reader.ts │ │ │ │ ├── index.ts │ │ │ │ ├── interfaces │ │ │ │ │ └── index.ts │ │ │ │ ├── komga.ts │ │ │ │ ├── mangadex.ts │ │ │ │ ├── tachidesk.ts │ │ │ │ └── types │ │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── interfaces │ │ │ │ └── index.ts │ │ │ ├── komga.ts │ │ │ ├── mangadex.ts │ │ │ ├── mangafox.ts │ │ │ ├── mangahasu.ts │ │ │ ├── scan-fr.ts │ │ │ ├── tachidesk.ts │ │ │ └── types │ │ │ │ ├── chapter.ts │ │ │ │ ├── constructor.ts │ │ │ │ ├── errors.ts │ │ │ │ ├── manga.ts │ │ │ │ ├── search.ts │ │ │ │ └── shared.ts │ │ ├── server │ │ │ ├── helpers │ │ │ │ ├── arrayEquals.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── scheduler.ts │ │ │ └── types │ │ │ │ ├── index.ts │ │ │ │ └── scheduler.ts │ │ ├── types │ │ │ └── index.ts │ │ └── utils │ │ │ ├── certificate.ts │ │ │ ├── crawler.ts │ │ │ ├── fileserv.ts │ │ │ ├── standalone.ts │ │ │ ├── steno.ts │ │ │ └── types │ │ │ └── crawler.ts │ ├── tsconfig.json │ └── vite.config.js ├── i18n │ ├── locales │ │ ├── cs.json │ │ ├── de.json │ │ ├── en.json │ │ ├── fr.json │ │ ├── nl.json │ │ ├── pl.json │ │ └── tr.json │ ├── src │ │ ├── availableLangs.ts │ │ ├── findLocale.ts │ │ ├── index.ts │ │ ├── isoConvert.ts │ │ └── loadLocale.ts │ ├── tsconfig.json │ └── vite.config.js ├── main │ ├── src │ │ ├── appReady.ts │ │ ├── forkAPI.ts │ │ ├── index.ts │ │ ├── mainWindow.ts │ │ └── security-restrictions.ts │ ├── tests │ │ └── unit.spec.ts │ ├── tsconfig.json │ └── vite.config.js ├── preload │ ├── exposedInMainWorld.d.ts │ ├── src │ │ ├── apiServer.ts │ │ ├── config.ts │ │ └── index.ts │ ├── tsconfig.json │ └── vite.config.js └── renderer │ ├── .eslintrc.json │ ├── assets │ ├── 404.png │ ├── 404.svg │ ├── 404_portrait.png │ ├── 404_portrait.svg │ ├── favicon.ico │ ├── icon.svg │ ├── icon_128.png │ ├── icon_16.png │ ├── icon_32.png │ └── icon_64.png │ ├── index.html │ ├── src │ ├── App.vue │ ├── components │ │ ├── AppLayout.vue │ │ ├── explore │ │ │ ├── App.vue │ │ │ ├── CarouselSlide.vue │ │ │ ├── GroupCard.vue │ │ │ ├── GroupMenu.vue │ │ │ └── SourceExplore.vue │ │ ├── helpers │ │ │ ├── login.ts │ │ │ ├── mirrorFilters.ts │ │ │ ├── routePusher.ts │ │ │ ├── socket.ts │ │ │ ├── toggleFullScreen.ts │ │ │ ├── transformIMGurl.ts │ │ │ └── typechecker.ts │ │ ├── import │ │ │ └── App.vue │ │ ├── library │ │ │ ├── @types │ │ │ │ └── index.ts │ │ │ ├── App.vue │ │ │ ├── GroupCard.vue │ │ │ ├── GroupFilter.vue │ │ │ ├── GroupMenu.vue │ │ │ ├── MirrorChips.vue │ │ │ └── migrate │ │ │ │ ├── App.vue │ │ │ │ ├── EntryFixer.vue │ │ │ │ └── MigrateButton.vue │ │ ├── login │ │ │ └── App.vue │ │ ├── logs │ │ │ └── App.vue │ │ ├── manga │ │ │ └── App.vue │ │ ├── reader │ │ │ ├── App.vue │ │ │ ├── ImageStack.vue │ │ │ ├── ImagesContainer.vue │ │ │ ├── NavOverlay.vue │ │ │ ├── ReaderHeader.vue │ │ │ ├── RightDrawer.vue │ │ │ ├── ThumbnailNavigation.vue │ │ │ └── helpers │ │ │ │ └── index.ts │ │ ├── search │ │ │ └── App.vue │ │ ├── settings │ │ │ ├── App.vue │ │ │ ├── fileSystem.vue │ │ │ ├── helpers │ │ │ │ └── checkReaderSettings.ts │ │ │ ├── languageList.vue │ │ │ ├── mainOptions.vue │ │ │ ├── mirrorSetup.vue │ │ │ └── mirrorsOptions.vue │ │ └── setup │ │ │ └── App.vue │ ├── env.d.ts │ ├── index.ts │ ├── locales │ │ └── index.ts │ ├── shims-vue.d.ts │ ├── socketClient.ts │ └── stores │ │ ├── history │ │ └── index.ts │ │ ├── localStorage.ts │ │ └── settings │ │ ├── index.ts │ │ └── types.ts │ ├── tsconfig.json │ └── vite.config.js ├── scripts ├── release.js ├── update-electron-vendors.js └── watch.js ├── tests └── e2e.spec.ts ├── types └── env.d.ts ├── vetur.config.js ├── vitest.config.js ├── wiki ├── Home.md ├── _Sidebar.md ├── locale │ └── fr │ │ ├── Home-fr.md │ │ ├── _Sidebar.md │ │ ├── meta-fr.md │ │ ├── mirrors-fr.md │ │ ├── setup-fr.md │ │ ├── setup-issues-fr.md │ │ └── setup-requirements-fr.md ├── meta.md ├── mirrors.md ├── setup-issues.md ├── setup-requirements.md └── setup.md └── workflow.png /.browserslistrc: -------------------------------------------------------------------------------- 1 | Chrome 112 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # https://github.com/jokeyrhyme/standard-editorconfig 4 | 5 | # top-most EditorConfig file 6 | root = true 7 | 8 | # defaults 9 | [*] 10 | charset = utf-8 11 | end_of_line = lf 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | indent_size = 2 15 | indent_style = space 16 | 17 | [*.md] 18 | trim_trailing_whitespace = false 19 | -------------------------------------------------------------------------------- /.electron-builder.config.js: -------------------------------------------------------------------------------- 1 | const PACKAGEJSON = JSON.parse(require('fs').readFileSync('./package.json').toString()) 2 | 3 | if (process.env.VITE_APP_VERSION === undefined) { 4 | process.env.VITE_APP_VERSION = PACKAGEJSON.version; 5 | } 6 | 7 | /** 8 | * @type {import('electron-builder').Configuration} 9 | * @see https://www.electron.build/configuration/configuration 10 | */ 11 | const config = { 12 | appId: 'com.electron.fukayo', 13 | productName: 'Fukayo', 14 | asar: false, 15 | directories: { 16 | output: 'dist', 17 | buildResources: 'buildResources', 18 | }, 19 | files: [ 20 | 'packages/**/dist/**', 21 | ], 22 | extraMetadata: { 23 | version: process.env.VITE_APP_VERSION, 24 | }, 25 | linux: { 26 | target: "appImage", 27 | synopsis: PACKAGEJSON.description, 28 | category: 'AudioVideo', 29 | icon: 'icon_256.png', 30 | }, 31 | win: { 32 | target: "nsis", 33 | icon: 'icon_256.ico', 34 | // legalTrademarks: undefined, 35 | }, 36 | nsis: { 37 | oneClick: true, 38 | perMachine: false, 39 | installerIcon: "icon_256.ico", 40 | }, 41 | // mac: { 42 | // category: 'public.app-category.utilities', 43 | // target: 'dmg', 44 | // icon: 'buildResources/icons/mac/icon.icns', 45 | // }, 46 | // dmg: { 47 | 48 | // } 49 | }; 50 | 51 | module.exports = config; 52 | -------------------------------------------------------------------------------- /.electron-vendors.cache.json: -------------------------------------------------------------------------------- 1 | { 2 | "chrome": "112", 3 | "node": "18" 4 | } 5 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "es2021": true, 5 | "node": true, 6 | "browser": false 7 | }, 8 | "extends": [ 9 | "eslint:recommended", 10 | /** @see https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/eslint-plugin#recommended-configs */ 11 | "plugin:@typescript-eslint/recommended" 12 | ], 13 | "parser": "@typescript-eslint/parser", 14 | "parserOptions": { 15 | "ecmaVersion": 12, 16 | "sourceType": "module" 17 | }, 18 | "plugins": [ 19 | "@typescript-eslint" 20 | ], 21 | "ignorePatterns": [ 22 | "packages/preload/exposedInMainWorld.d.ts", 23 | "node_modules/**", 24 | "**/dist/**" 25 | ], 26 | "rules": { 27 | "@typescript-eslint/no-unused-vars": "error", 28 | "@typescript-eslint/no-var-requires": "off", 29 | "@typescript-eslint/consistent-type-imports": "error", 30 | 31 | /** 32 | * Having a semicolon helps the optimizer interpret your code correctly. 33 | * This avoids rare errors in optimized code. 34 | * @see https://twitter.com/alex_kozack/status/1364210394328408066 35 | */ 36 | "semi": [ 37 | "error", 38 | "always" 39 | ], 40 | /** 41 | * This will make the history of changes in the hit a little cleaner 42 | */ 43 | "comma-dangle": [ 44 | "warn", 45 | "always-multiline" 46 | ], 47 | /** 48 | * Just for beauty 49 | */ 50 | "quotes": [ 51 | "warn", "single" 52 | ] 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | .github/actions/**/*.js linguist-detectable=false 2 | scripts/*.js linguist-detectable=false 3 | *.config.js linguist-detectable=false 4 | packages/api/docs/** linguist-detectable=false 5 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: JiPaix 4 | patreon: Fukayo 5 | custom: paypal.fukayo.com 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: JiPaix 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Questions & Discussions 4 | url: https://github.com/JiPaix/AMR/discussions/categories/q-a 5 | about: Use GitHub discussions for message-board style questions and discussions. 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE]" 5 | labels: enhancement 6 | assignees: JiPaix 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/actions/discord-release/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Release Notes on discord' 2 | description: 'Return release notes based on Git Commits (extension)' 3 | inputs: 4 | discord-token: 5 | description: 'Discord Token' 6 | required: true 7 | discord-channel: 8 | description: 'Discord Channel ID' 9 | required: true 10 | discord-role: 11 | description: 'Discord Role (ID) to ping' 12 | required: true 13 | release-note-json: 14 | description: 'json formatted release notes' 15 | required: true 16 | release-version: 17 | description: 'version' 18 | required: true 19 | runs: 20 | using: 'node16' 21 | main: 'index.js' 22 | -------------------------------------------------------------------------------- /.github/actions/discord-release/licenses.txt: -------------------------------------------------------------------------------- 1 | file-type 2 | MIT 3 | MIT License 4 | 5 | Copyright (c) Sindre Sorhus (https://sindresorhus.com) 6 | 7 | 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: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | 11 | 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. 12 | 13 | 14 | ieee754 15 | BSD-3-Clause 16 | Copyright 2008 Fair Oaks Labs, Inc. 17 | 18 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 19 | 20 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 21 | 22 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 23 | 24 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 25 | 26 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /.github/actions/release-notes/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Release Notes' 2 | description: 'Return release notes based on Git Commits' 3 | inputs: 4 | from: 5 | description: 'Commit from which start log' 6 | required: true 7 | to: 8 | description: 'Commit to which end log' 9 | required: true 10 | include-commit-body: 11 | description: 'Should the commit body be in notes' 12 | required: false 13 | default: 'false' 14 | include-abbreviated-commit: 15 | description: 'Should the commit sha be in notes' 16 | required: false 17 | default: 'true' 18 | outputs: 19 | release-note: # id of output 20 | description: 'Release notes' 21 | release-note-json: # id of output 22 | description: 'JSON formatted Release notes' 23 | runs: 24 | using: 'node16' 25 | main: 'main.js' 26 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Enable version updates for npm 4 | - package-ecosystem: "npm" 5 | # Look for `package.json` and `lock` files in the `root` directory 6 | directory: "/" 7 | # Check the npm registry for updates every day (weekdays) 8 | schedule: 9 | interval: "daily" 10 | target-branch: "beta" 11 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. 4 | 5 | Fixes # (issue) 6 | 7 | ## Type of change 8 | 9 | - [ ] Bug fix (non-breaking change which fixes an issue) 10 | - [ ] New feature (non-breaking change which adds functionality) 11 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 12 | - [ ] This change requires a documentation update 13 | 14 | # How Has This Been Tested? 15 | 16 | **Test Configuration**: 17 | - [ ] Linux 18 | - [ ] MacOS 19 | - [ ] Windows 20 | 21 | Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration 22 | 23 | # Checklist: 24 | 25 | - [ ] My code follows the style guidelines of this project 26 | - [ ] I have performed a self-review of my own code 27 | - [ ] I have commented my code, particularly in hard-to-understand areas 28 | - [ ] I have made corresponding changes to the documentation 29 | - [ ] My changes generate no new warnings 30 | - [ ] I have added tests that prove my fix is effective or that my feature works 31 | - [ ] New and existing unit tests pass locally with my changes 32 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base", 4 | ":semanticCommits", 5 | ":automergeTypes", 6 | ":disableDependencyDashboard" 7 | ], 8 | "labels": [ 9 | "dependencies" 10 | ], 11 | "baseBranches": [ 12 | "main" 13 | ], 14 | "bumpVersion": "patch", 15 | "patch": { 16 | "automerge": true 17 | }, 18 | "minor": { 19 | "automerge": true 20 | }, 21 | "packageRules": [ 22 | { 23 | "packageNames": [ 24 | "node", 25 | "npm" 26 | ], 27 | "enabled": false 28 | }, 29 | { 30 | "depTypeList": [ 31 | "devDependencies" 32 | ], 33 | "semanticCommitType": "build" 34 | }, 35 | { 36 | "matchSourceUrlPrefixes": [ 37 | "https://github.com/vitejs/vite/" 38 | ], 39 | "groupName": "Vite monorepo packages", 40 | "automerge": false 41 | }, 42 | { 43 | "matchPackagePatterns": [ 44 | "^@typescript-eslint", 45 | "^eslint" 46 | ], 47 | "automerge": true, 48 | "groupName": "eslint" 49 | }, 50 | { 51 | "matchPackageNames": [ 52 | "electron" 53 | ], 54 | "separateMajorMinor": false 55 | } 56 | ], 57 | "rangeStrategy": "pin" 58 | } 59 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: 17 | - main 18 | - beta 19 | pull_request: 20 | # The branches below must be a subset of the branches above 21 | branches: 22 | - main 23 | - beta 24 | paths: 25 | - '**.js' 26 | - '**.ts' 27 | - '**.vue' 28 | - 'package-lock.json' 29 | - '.github/workflows/codeql-analysis.yml' 30 | schedule: 31 | - cron: '40 19 * * 2' 32 | 33 | jobs: 34 | analyze: 35 | name: Analyze 36 | runs-on: ubuntu-latest 37 | permissions: 38 | actions: read 39 | contents: read 40 | security-events: write 41 | 42 | strategy: 43 | fail-fast: false 44 | matrix: 45 | language: [ 'javascript' ] 46 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 47 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 48 | 49 | steps: 50 | - name: Checkout repository 51 | uses: actions/checkout@v3 52 | 53 | # Initializes the CodeQL tools for scanning. 54 | - name: Initialize CodeQL 55 | uses: github/codeql-action/init@v2 56 | with: 57 | languages: ${{ matrix.language }} 58 | # If you wish to specify custom queries, you can do so here or in a config file. 59 | # By default, queries listed here will override any specified in a config file. 60 | # Prefix the list here with "+" to use these queries and those in the config file. 61 | 62 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 63 | # queries: security-extended,security-and-quality 64 | 65 | 66 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 67 | # If this step fails, then you should remove it and run the build manually (see below) 68 | - name: Autobuild 69 | uses: github/codeql-action/autobuild@v2 70 | 71 | # ℹ️ Command-line programs to run using the OS shell. 72 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 73 | 74 | # If the Autobuild fails above, remove it and uncomment the following three lines. 75 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 76 | 77 | # - run: | 78 | # echo "Run, Build Application using script" 79 | # ./location_of_script_within_repo/buildscript.sh 80 | 81 | - name: Perform CodeQL Analysis 82 | uses: github/codeql-action/analyze@v2 83 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Linters 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - beta 7 | paths: 8 | - '**.js' 9 | - '**.ts' 10 | - '**.vue' 11 | - 'package-lock.json' 12 | - '.github/workflows/lint.yml' 13 | pull_request: 14 | paths: 15 | - '**.js' 16 | - '**.ts' 17 | - '**.vue' 18 | - 'package-lock.json' 19 | - '.github/workflows/lint.yml' 20 | 21 | defaults: 22 | run: 23 | shell: 'bash' 24 | 25 | jobs: 26 | eslint: 27 | runs-on: ubuntu-latest 28 | 29 | steps: 30 | - uses: actions/checkout@v3 31 | - uses: actions/setup-node@v3 32 | with: 33 | node-version: 18 # Need for npm >=7.7 34 | cache: 'npm' 35 | 36 | # TODO: Install not all dependencies, but only those required for this workflow 37 | - name: Install dependencies 38 | run: npm ci 39 | 40 | - run: npm run lint 41 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - beta 7 | paths: 8 | - 'packages/**' 9 | - 'tests/**' 10 | - 'package-lock.json' 11 | - '.github/workflows/tests.yml' 12 | pull_request: 13 | branches: 14 | - main 15 | - beta 16 | paths: 17 | - 'packages/**' 18 | - 'tests/**' 19 | - 'package-lock.json' 20 | - '.github/workflows/tests.yml' 21 | 22 | jobs: 23 | e2e: 24 | strategy: 25 | matrix: 26 | os: [ ubuntu-latest, windows-latest ] 27 | runs-on: ${{ matrix.os }} 28 | steps: 29 | - uses: actions/checkout@v3 30 | - uses: actions/setup-node@v3 31 | with: 32 | node-version: 18 33 | cache: 'npm' 34 | - name: Clean install 35 | run: npm ci 36 | - name: Run tests 37 | uses: hankolsen/xvfb-action@dcb076c1c3802845f73bb6fe14a009d8d3377255 38 | with: 39 | run: npm run test 40 | -------------------------------------------------------------------------------- /.github/workflows/typechecking.yml: -------------------------------------------------------------------------------- 1 | name: Typechecking 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - beta 7 | paths: 8 | - '**.ts' 9 | - '**.vue' 10 | - '**/tsconfig.json' 11 | - 'package-lock.json' 12 | - '.github/workflows/typechecking.yml' 13 | pull_request: 14 | branches: 15 | - main 16 | - beta 17 | paths: 18 | - '**.ts' 19 | - '**.vue' 20 | - '**/tsconfig.json' 21 | - 'package-lock.json' 22 | - '.github/workflows/typechecking.yml' 23 | 24 | defaults: 25 | run: 26 | shell: 'bash' 27 | 28 | jobs: 29 | typescript: 30 | runs-on: ubuntu-latest 31 | 32 | steps: 33 | - uses: actions/checkout@v3 34 | - uses: actions/setup-node@v3 35 | with: 36 | node-version: 18 # Need for npm >=7.7 37 | cache: 'npm' 38 | 39 | # TODO: Install not all dependencies, but only those required for this workflow 40 | - name: Install dependencies 41 | run: npm ci 42 | 43 | - run: npm run typecheck 44 | -------------------------------------------------------------------------------- /.github/workflows/update-electron-vendors.yml: -------------------------------------------------------------------------------- 1 | name: Update Electon vendors versions 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - beta 7 | paths: 8 | - 'package-lock.json' 9 | 10 | defaults: 11 | run: 12 | shell: 'bash' 13 | 14 | jobs: 15 | node-chrome: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | - uses: actions/setup-node@v3 21 | with: 22 | node-version: 18 # Need for npm >=7.7 23 | cache: 'npm' 24 | 25 | # TODO: Install not all dependencies, but only those required for this workflow 26 | - name: Install dependencies 27 | run: npm ci 28 | 29 | - run: node ./scripts/update-electron-vendors.js 30 | 31 | - name: Create Pull Request 32 | uses: peter-evans/create-pull-request@v4.2.0 33 | with: 34 | delete-branch: true 35 | commit-message: "chore(build): Update electron vendors" 36 | branch: autoupdates/electron-vendors 37 | title: "chore(build): Update electron vendors" 38 | body: Updated versions of electron vendors in `.electron-vendors.cache.json` and `.browserslistrc` files 39 | -------------------------------------------------------------------------------- /.github/workflows/wiki-update.yml: -------------------------------------------------------------------------------- 1 | name: update wiki 2 | 3 | on: 4 | push: 5 | branches: 6 | - beta 7 | paths: 8 | - 'wiki/**' 9 | - '!package.json' 10 | - '!package-lock.json' 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | # Additional steps to generate documentation in "Documentation" directory 17 | - name: Upload Documentation to Wiki 18 | uses: SwiftDocOrg/github-wiki-publish-action@v1 19 | with: 20 | path: "wiki" 21 | env: 22 | GH_PERSONAL_ACCESS_TOKEN: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | *.local 5 | thumbs.db 6 | 7 | .eslintcache 8 | 9 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 10 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 11 | 12 | # User-specific stuff 13 | .idea/**/workspace.xml 14 | .idea/**/tasks.xml 15 | .idea/**/usage.statistics.xml 16 | .idea/**/dictionaries 17 | .idea/**/shelf 18 | 19 | # Generated files 20 | .idea/**/contentModel.xml 21 | 22 | # Sensitive or high-churn files 23 | .idea/**/dataSources/ 24 | .idea/**/dataSources.ids 25 | .idea/**/dataSources.local.xml 26 | .idea/**/sqlDataSources.xml 27 | .idea/**/dynamic.xml 28 | .idea/**/uiDesigner.xml 29 | .idea/**/dbnavigator.xml 30 | 31 | # Gradle 32 | .idea/**/gradle.xml 33 | .idea/**/libraries 34 | 35 | # Gradle and Maven with auto-import 36 | # When using Gradle or Maven with auto-import, you should exclude module files, 37 | # since they will be recreated, and may cause churn. Uncomment if using 38 | # auto-import. 39 | .idea/artifacts 40 | .idea/compiler.xml 41 | .idea/jarRepositories.xml 42 | .idea/modules.xml 43 | .idea/*.iml 44 | .idea/modules 45 | *.iml 46 | *.ipr 47 | 48 | # Mongo Explorer plugin 49 | .idea/**/mongoSettings.xml 50 | 51 | # File-based project format 52 | *.iws 53 | 54 | # Editor-based Rest Client 55 | .idea/httpRequests 56 | /.idea/csv-plugin.xml 57 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/deployment.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/jsLibraryMappings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/jsLinters/eslint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/vite-electron-builder.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /.idea/webResources.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.nano-staged.json: -------------------------------------------------------------------------------- 1 | { 2 | "*.{js,ts,vue}": "eslint --cache --fix" 3 | } 4 | -------------------------------------------------------------------------------- /.simple-git-hooks.json: -------------------------------------------------------------------------------- 1 | { 2 | "pre-commit": "npx nano-staged" 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "formulahendry.auto-close-tag", 4 | "steoates.autoimport", 5 | "formulahendry.auto-rename-tag", 6 | "naumovs.color-highlight", 7 | "vivaxy.vscode-conventional-commits", 8 | "editorconfig.editorconfig", 9 | "cschleiden.vscode-github-actions", 10 | "lokalise.i18n-ally", 11 | "gruntfuggly.todo-tree", 12 | "dbaeumer.vscode-eslint", 13 | "github.vscode-pull-request-github", 14 | "visualstudioexptteam.vscodeintellicode", 15 | "mflo999.lintel", 16 | "christian-kohler.path-intellisense", 17 | "abdelaziz18003.quasar-snippets", 18 | "lihui.vs-color-picker", 19 | "vue.volar", 20 | "sdras.vue-vscode-snippets" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /CONTACT.md: -------------------------------------------------------------------------------- 1 | Welcome to the Safe Fukayo community 2 | 3 | Grant and indulge critique constructively, within desired privacy. 4 | Settle disputes within these confines. 5 | Finding yourselves unable, raise an issue here, answered by 6 | Jean-Philippe ALLEGRO (JiPaix), the project maintainer. 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM almalinux:9-minimal 2 | 3 | ENV FUKAYO_USERNAME=${FUKAYO_USERNAME:-admin} \ 4 | FUKAYO_PASSWORD=${FUKAYO_PASSWORD:-password} \ 5 | FUKAYO_PORT=${FUKAYO_PORT:-4444} 6 | 7 | RUN microdnf install -y epel-release && \ 8 | microdnf install -y chromium xorg-x11-server-Xvfb && \ 9 | microdnf remove -y chromium && \ 10 | microdnf clean all 11 | 12 | RUN useradd -m fukayo && \ 13 | mkdir /home/fukayo/app && chown -R fukayo:fukayo /home/fukayo/app && chmod -R 755 /home/fukayo/app && \ 14 | mkdir -p /home/fukayo/.config/fukayo && chown -R fukayo:fukayo /home/fukayo/.config && \ 15 | chmod -R 755 /home/fukayo/.config/fukayo 16 | 17 | WORKDIR /home/fukayo/app 18 | COPY --chown=fukayo:fukayo dist/linux-unpacked/ ./ 19 | VOLUME ["/home/fukayo/.config/fukayo"] 20 | USER fukayo 21 | EXPOSE $FUKAYO_PORT 22 | CMD xvfb-run ./fukayo --server --login=$FUKAYO_USERNAME --password=$FUKAYO_PASSWORD --port=$FUKAYO_PORT --no-sandbox 23 | LABEL org.opencontainers.image.authors="Farah Nur (farahnur42@outlook.com)" 24 | -------------------------------------------------------------------------------- /INSTALL.md: -------------------------------------------------------------------------------- 1 | # Docker/Podman Deployment 2 | This document assumes that you have installed Docker/Podman already, if you have not yet done so, here are the relevant links for them - Install Docker Desktop, Install Docker Engine, Install Podman. 3 | 4 | ## CLI deployment: 5 | 6 | For Docker/Podman (substitute the command name as necessary, I would also recommend using a non-root shell with Podman) - this is not the way I would recommend in general over using Docker Desktop or better yet Docker Compose. 7 | ``` 8 | # docker run -d --name fukayo \ 9 | -e FUKAYO_USERNAME= \ 10 | -e FUKAYO_PASSWORD= \ 11 | -p :4444 \ 12 | -u "1000:1000" \ 13 | -v :/home/fukayo/.config/fukayo \ 14 | jipaix/fukayo:beta 15 | ``` 16 | 17 | ## Docker Desktop deployment: 18 | 19 | TBD 20 | 21 | ## Docker Compose deployment: 22 | 23 | There is an example [docker-compose](docker-compose-example.yml) file included in this repository to be used with docker-compose/docker compose or any other relevant tool such as Portainer. The two less obvious tunables in it (as well as the CLI deployment) are as follows: 24 | 25 | 1. `` = The port that your host exposes for Fukayo 26 | 2. `` = The config directory on your **host** that you want Fukayo to use 27 | 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Jean-Philippe ALLEGRO 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 41 | 42 | 43 | 49 | 50 |
4 | fukayo's logo 5 | 7 |

8 | Fukayo 9 | windows-badge 10 | linux-badge 11 |
12 | 13 | patreon-badge 14 | 15 | 16 | gh-sponsor-badge 17 | 18 | 19 | 20 | paypal-badge 21 | 22 | 23 | discord-badge 24 | 25 |

26 |

A desktop application to read your favorite manga/manhwa/manhua from your favorite websites:

27 |
    28 |
  • Keep track of your reading progress
  • 29 |
  • Always get the latest releases directly from their sources
  • 30 |
  • Track from multiple sources in different languages
  • 31 |
  • And access it from anywhere, because it's also a server!
  • 32 |
33 | 34 | 35 | Download the latest version!
36 |
37 | Alternatively click the link here to find out how to deploy it on Docker/Podman! 38 | 39 | 40 |
44 |

Help us translate!

45 | 46 | Translation status 47 | 48 |
51 | 52 | -------------------------------------------------------------------------------- /buildResources/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiPaix/Fukayo/f9bb5ba83d7f10a35f849a10c5513dbf814bfb94/buildResources/.gitkeep -------------------------------------------------------------------------------- /buildResources/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /buildResources/icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiPaix/Fukayo/f9bb5ba83d7f10a35f849a10c5513dbf814bfb94/buildResources/icon_128.png -------------------------------------------------------------------------------- /buildResources/icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiPaix/Fukayo/f9bb5ba83d7f10a35f849a10c5513dbf814bfb94/buildResources/icon_16.png -------------------------------------------------------------------------------- /buildResources/icon_256.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiPaix/Fukayo/f9bb5ba83d7f10a35f849a10c5513dbf814bfb94/buildResources/icon_256.ico -------------------------------------------------------------------------------- /buildResources/icon_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiPaix/Fukayo/f9bb5ba83d7f10a35f849a10c5513dbf814bfb94/buildResources/icon_256.png -------------------------------------------------------------------------------- /buildResources/icon_32.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiPaix/Fukayo/f9bb5ba83d7f10a35f849a10c5513dbf814bfb94/buildResources/icon_32.ico -------------------------------------------------------------------------------- /buildResources/icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiPaix/Fukayo/f9bb5ba83d7f10a35f849a10c5513dbf814bfb94/buildResources/icon_32.png -------------------------------------------------------------------------------- /buildResources/icon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiPaix/Fukayo/f9bb5ba83d7f10a35f849a10c5513dbf814bfb94/buildResources/icon_64.png -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | First and foremost, thank you! We appreciate that you want to contribute to Fukayo, your time is valuable, and your contributions mean a lot to us. 4 | 5 | ## Contribution Guidelines 6 | - Your contribution(s) must comply with our [Code of Conduct](CODE_OF_CONDUCT.md) 7 | - Simple and descriptive commit message, as they appear in the changelog _anybody_* should be able to understand what it's about. 8 | - Treat commits as PRs, 1 commit = 1 feature/fix 9 | - Do not submit PRs to the main branch 10 | - Before submitting a PR test your changes 11 | - Changes on packages `main` and `renderer` must be tested on both windows and linux 12 | - Changes to package renderer must be tested in and outside the electron environment 13 | 14 | *developers and end users 15 | 16 | ## Repo Setup 17 | 18 | 1. Clone repo 19 | 1. `npm install` install dependencies 20 | 1. `npm run watch` start electron app in watch mode. 21 | 1. `npm run compile` build app but for local debugging only. 22 | 1. `npm run lint` lint your code. 23 | 1. `npm run typecheck` Run typescript check. 24 | 1. `npm run test` Run app test. 25 | 26 | ## Tree 27 | The root folder contains config files, build ressources/scripts and tests, 28 | the main part code is located at `/AMR/packages` 29 | - `/AMR/packages/main/src` 30 | - `index.ts` electron startup 31 | - `mainWindow.ts` create main window 32 | - `forkAPI.ts` api to communicate with fork process 33 | - `/AMR/packages/api/src` 34 | - `/app` api starter 35 | - `/client` a client to communicate with the api via socket.io 36 | - `/server` server handling socket.io commands 37 | - `/models` source/mirrors implementations 38 | - `/db` databases used by the server and the models 39 | - `/utils` 40 | - `certificate.ts` SSL certificate generator 41 | - `crawler.ts` headless browser to fetch data 42 | - `standalone.ts` check the env if the api is used outside electron 43 | - `/AMR/packages/renderer/src` 44 | - `/store` pinia stores 45 | - `/locales` internationalization files 46 | - `/components` Vue components 47 | -------------------------------------------------------------------------------- /docker-compose-example.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: "3.8" 3 | services: 4 | fukayo: 5 | image: jipaix/fukayo:beta 6 | container_name: fukayo 7 | ports: 8 | - :4444 9 | user: "1000:1000" 10 | volumes: 11 | - type: bind 12 | source: 13 | target: /home/fukayo/.config/fukayo 14 | environment: 15 | - FUKAYO_USERNAME= 16 | - FUKAYO_PASSWORD= 17 | restart: unless-stopped 18 | -------------------------------------------------------------------------------- /packages/api/src/app/types/index.ts: -------------------------------------------------------------------------------- 1 | export type Message = ShutdownMessage | StartMessage | PingMessage; 2 | 3 | export type ShutdownMessage = { 4 | type: 'shutdown'|'shutdownFromWeb'; 5 | } 6 | 7 | export type StartMessage = { 8 | type: 'start'; 9 | payload: startPayload 10 | } 11 | 12 | export type PingMessage = { 13 | type: 'ping'; 14 | } 15 | 16 | export type ForkResponse = { 17 | type: 'start' | 'shutdown' | 'shutdownFromWeb' | 'pong', 18 | success?: boolean, 19 | message?: string, 20 | } 21 | 22 | export type startPayload = { 23 | login: string, 24 | password: string, 25 | port: number, 26 | hostname?: string, 27 | ssl: 'false' | 'provided' | 'app', 28 | cert?: string | null, 29 | key?: string | null, 30 | verbose?:boolean 31 | } 32 | -------------------------------------------------------------------------------- /packages/api/src/client/index.ts: -------------------------------------------------------------------------------- 1 | import type { ForkResponse, startPayload } from '@api/app/types'; 2 | import type { ClientToServerEvents, LoginAuth, SocketClientConstructor } from '@api/client/types'; 3 | import type { ServerToClientEvents } from '@api/server/types'; 4 | import type { IpcRenderer } from 'electron'; 5 | import type { Socket } from 'socket.io-client'; 6 | 7 | import io from 'socket.io-client'; 8 | 9 | declare global { 10 | interface Window { 11 | readonly apiServer: { 12 | startServer: (payload: startPayload) => Promise; 13 | stopServer: () => Promise; 14 | copyImageToClipboard: (string: string) => Promise; 15 | toggleFullScreen: () => void; 16 | onFullScreen(cb: (fullscreen: boolean) => void): IpcRenderer 17 | getEnv: string; 18 | } 19 | } 20 | } 21 | 22 | /** 23 | * Initialize the socket.io client. 24 | */ 25 | export default class socket { 26 | 27 | private socket?: Socket; 28 | private settings: SocketClientConstructor; 29 | 30 | constructor(settings: SocketClientConstructor) { 31 | this.settings = settings; 32 | } 33 | 34 | private removeListeners() { 35 | if(!this.socket) return; 36 | this.socket.removeAllListeners('token'); 37 | this.socket.removeAllListeners('refreshToken'); 38 | this.socket.removeAllListeners('unauthorized'); 39 | this.socket.removeAllListeners('authorized'); 40 | this.socket.removeAllListeners('connect_error'); 41 | } 42 | 43 | private initSocket() { 44 | if(!this.socket) throw Error('unreachable'); 45 | this.removeListeners(); 46 | this.socket.on('token', t => this.settings.accessToken = t); 47 | this.socket.on('refreshToken', t => this.settings.refreshToken = t); 48 | this.socket.emit('getConnectivityStatus'); 49 | return this.socket; 50 | } 51 | 52 | private unplugSocket(socket:Socket, reason?:string) { 53 | if(socket) { 54 | this.removeListeners(); 55 | socket.disconnect(); 56 | } 57 | return reason; 58 | } 59 | 60 | private getServer() { 61 | const location = '127.0.0.1:'+this.settings.port; 62 | // When the server is running in standalone mode, the client uses localhost 63 | if(!window) return (this.settings.ssl === 'false' ? 'http://': 'https://') + location; 64 | 65 | const url = window.location.href; 66 | 67 | // Electron file:// protocol 68 | if(url.includes('file://')) { 69 | if(this.settings.ssl === 'false') return 'http://' + location; 70 | return 'https://' + location; 71 | } 72 | 73 | // Electron http(s) protocol 74 | if(window.apiServer) { 75 | if(window.apiServer.getEnv === 'development') { 76 | if(this.settings.ssl === 'false') return 'http://' + location; 77 | return 'https://' + location; 78 | } 79 | } 80 | 81 | // Browser 82 | const host = url.split('/')[2].replace(/:\d+/g, ''); 83 | const finalLocation = host+':'+this.settings.port; 84 | if(url.includes('https')) return 'https://' + finalLocation; 85 | return 'http://' + finalLocation; 86 | } 87 | connect(auth?: LoginAuth): Promise> { 88 | 89 | return new Promise((resolve, reject) => { 90 | if(this.socket && this.socket.connected) { 91 | return resolve(this.initSocket()); 92 | } 93 | const authentification = auth ? auth : { token: this.settings.accessToken, refreshToken: this.settings.refreshToken }; 94 | const socket:Socket = io(this.getServer(), 95 | { 96 | auth: authentification, 97 | reconnection: true, 98 | }); 99 | socket.once('authorized', () => { 100 | this.socket = socket; 101 | resolve(this.initSocket()); 102 | }); 103 | 104 | socket.once('unauthorized', () => { 105 | const reason = auth ? 'badpassword' : 'expiredtoken'; 106 | reject(this.unplugSocket(socket, reason)); 107 | }); 108 | 109 | socket.once('connect_error', (e) => { 110 | if(e.message === 'xhr poll error') { 111 | reject(this.unplugSocket(socket, e.message)); 112 | } else { 113 | reject(e.message); 114 | } 115 | }); 116 | }); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /packages/api/src/client/types/index.ts: -------------------------------------------------------------------------------- 1 | import type SettingsDB from '@api/db/settings'; 2 | import type Importer from '@api/models/imports/interfaces'; 3 | import type { MangaInDB, MangaPage } from '@api/models/types/manga'; 4 | import type { mirrorInfo } from '@api/models/types/shared'; 5 | import type Scheduler from '@api/server/scheduler'; 6 | import type { ServerToClientEvents } from '@api/server/types'; 7 | import type { mirrorsLangsType } from '@i18n'; 8 | import type { Socket } from 'socket.io-client'; 9 | 10 | export type SocketClientConstructor = { 11 | accessToken?: string | null, 12 | refreshToken?: string | null, 13 | ssl: 'false' | 'provided' | 'app', 14 | port: number, 15 | } 16 | 17 | export type LoginAuth = { login: string, password:string } 18 | 19 | 20 | export type ClientToServerEvents = { 21 | getConnectivityStatus: () => void; 22 | getMirrors: (showdisabled:boolean, callback: (m: mirrorInfo[]) => void) => void; 23 | getImports: (showdisabled:boolean, callback: (m: Importer['showInfo'][]) => void) => void 24 | searchInMirrors: (query:string, id:number, mirrors: string[], langs:mirrorsLangsType[], callback: (nbOfDonesToExpect:number)=>void) => void; 25 | stopSearchInMirrors: () => void; 26 | stopShowManga: () => void; 27 | stopShowChapter: () => void; 28 | stopShowRecommend: () => void; 29 | stopShowLibrary: () => void; 30 | showManga: (id:number, opts: {mirror?:string, langs:mirrorsLangsType[], id?:string, url?:string, force?:boolean }) => void; 31 | showChapter: (id:number, opts: { mangaId:string, chapterId: string, url?:string, mirror:string, lang:mirrorsLangsType, retryIndex?:number }, callback?: (nbOfPagesToExpect:number)=>void) => void; 32 | showRecommend: (id:number, mirror:string, langs:mirrorsLangsType[] ) => void; 33 | changeMirrorSettings: (mirror:string, options:Record, callback: (m: mirrorInfo[])=>void) => void; 34 | getCacheSize: (callback: (size: number, files:number) => void) => void; 35 | emptyCache: (files?:string[]) => void; 36 | addManga: ( payload: { manga:MangaPage|MangaInDB, settings?:MangaInDB['meta']['options'] }, callback:(dbManga: MangaInDB)=>void) => void; 37 | removeManga: (dbManga:MangaInDB, lang:mirrorsLangsType, callback:(manga: MangaPage)=>void) => void; 38 | showLibrary:(id:number) => void; 39 | forceUpdates: () => void; 40 | isUpdating: (callback:(isUpdating:boolean)=>void) => void; 41 | schedulerLogs: (callback:(logs:Scheduler['logs'])=>void) => void; 42 | getSettings: (callback:(settings:SettingsDB['data'])=>void) => void; 43 | changeSettings: (settings:SettingsDB['data'], callback:(settings:SettingsDB['data'])=>void) => void; 44 | markAsRead: ({ mirror, lang, url, chapterUrls, read, mangaId }: { mirror:string, lang:mirrorsLangsType, url:string, chapterUrls:string[], read:boolean, mangaId:string }) => void; 45 | showImports: (id:number, mirrorName: string, langs:mirrorsLangsType[], json?:string) => void; 46 | stopShowImports: () => void; 47 | } 48 | 49 | export type socketClientInstance = Socket 50 | -------------------------------------------------------------------------------- /packages/api/src/db/helpers/index.ts: -------------------------------------------------------------------------------- 1 | import type { MangaErrorMessage } from '@api/models/types/errors'; 2 | import type { MangaInDB, MangaPage } from '@api/models/types/manga'; 3 | import type { LogChapterError, LogChapterNew, LogChapterRead, LogMangaNewMetadata } from '@api/server/types/scheduler'; 4 | 5 | export function isMangaInDB(res: MangaPage | MangaInDB | MangaErrorMessage): res is MangaInDB { 6 | return (res as MangaInDB).inLibrary === true && (res as MangaInDB).meta !== undefined; 7 | } 8 | 9 | export function isManga(res: MangaPage | MangaErrorMessage | MangaInDB | unknown): res is MangaPage|MangaInDB { 10 | return (res as MangaPage).inLibrary !== undefined; 11 | } 12 | 13 | export function isMangaPage(res: unknown): res is MangaPage { 14 | return (res as MangaPage).inLibrary == false; 15 | } 16 | 17 | export function isMangaLog(res: unknown): res is LogChapterNew|LogChapterRead|LogMangaNewMetadata|LogChapterError { 18 | const x = res as LogChapterNew|LogChapterRead|LogMangaNewMetadata|LogChapterError; 19 | if(x.message === 'log_chapter_new') return true; 20 | if(x.message === 'log_chapter_read') return true; 21 | if(x.message === 'log_manga_metadata') return true; 22 | if(x.message === 'log_chapter_error') return true; 23 | return false; 24 | 25 | } 26 | 27 | export function isLogChapterError(res: unknown): res is LogChapterError { 28 | return (res as LogChapterError).message === 'log_chapter_error'; 29 | } 30 | 31 | export function isLogChapterNew(res: unknown): res is LogChapterNew { 32 | return (res as LogChapterNew).message === 'log_chapter_new'; 33 | } 34 | 35 | export function isLogChapterRead(res: unknown): res is LogChapterRead { 36 | return (res as LogChapterRead).message === 'log_chapter_read'; 37 | } 38 | 39 | export function isLogMangaNewMetadata(res: unknown): res is LogMangaNewMetadata { 40 | return (res as LogMangaNewMetadata).message === 'log_manga_metadata'; 41 | } 42 | -------------------------------------------------------------------------------- /packages/api/src/db/settings.ts: -------------------------------------------------------------------------------- 1 | import { Database } from '@api/db'; 2 | import { resolve } from 'path'; 3 | import { env } from 'process'; 4 | import type { mirrorsLangsType } from '@i18n'; 5 | 6 | const defaultSettings = { 7 | langs: ['en', 'fr', 'de', 'pt', 'pt-br', 'es', 'es-la', 'ru', 'tr', 'ja', 'id', 'zh', 'zh-hk', 'xx'] as mirrorsLangsType[], 8 | cache: { 9 | age : { 10 | max: 1000 * 60 * 60 * 24 * 7, // 1 week 11 | enabled: true, 12 | }, 13 | size: { 14 | max: 3500000000, // 3.5 GB in bytes 15 | enabled: true, 16 | }, 17 | logs: { 18 | enabled: true, 19 | max: 30, 20 | }, 21 | }, 22 | library: { 23 | waitBetweenUpdates: 1000 * 60 * 60 * 6, // 6 hours 24 | logs: { 25 | enabled: true, 26 | max: 100, 27 | }, 28 | }, 29 | }; 30 | 31 | export default class SettingsDatabase extends Database { 32 | static #instance:SettingsDatabase; 33 | constructor() { 34 | if(typeof env.USER_DATA === 'undefined') throw Error('USER_DATA is not defined'); 35 | super(resolve(env.USER_DATA, 'settings_db.json'), defaultSettings); 36 | } 37 | static getInstance(): SettingsDatabase { 38 | if (!this.#instance) { 39 | this.#instance = new this(); 40 | } 41 | return this.#instance; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/api/src/db/tokens.ts: -------------------------------------------------------------------------------- 1 | import { Database } from '@api/db'; 2 | import crypto from 'crypto'; 3 | import { resolve } from 'path'; 4 | import { env } from 'process'; 5 | 6 | type RefreshToken = { 7 | token: string, 8 | expire: number 9 | master?: boolean 10 | } 11 | 12 | type AuthorizedToken = { 13 | token: string, 14 | expire: number 15 | parent: string 16 | master?: boolean 17 | } 18 | type Tokens = { 19 | refreshTokens: RefreshToken[], 20 | authorizedTokens: AuthorizedToken[] 21 | } 22 | 23 | export default class TokenDatabase extends Database { 24 | static #instance: TokenDatabase; 25 | 26 | constructor(tokens: { accessToken: string, refreshToken: string }) { 27 | super(resolve(env.USER_DATA, 'access_db.json'), { authorizedTokens: [], refreshTokens: [] }); 28 | // remove expired or master tokens 29 | this.data.authorizedTokens = this.data.authorizedTokens.filter(t => t.expire > Date.now() && !t.master); 30 | this.data.refreshTokens = this.data.refreshTokens.filter(t => t.expire > Date.now() && !t.master); 31 | // add new tokens 32 | const refresh = { token: tokens.refreshToken, expire: Date.now() + 1000 * 60 * 60 * 24 * 7, master: true }; 33 | this.data.refreshTokens.push(refresh); 34 | const authorized = { token: tokens.accessToken, expire: Date.now() + 1000 * 60 * 60 * 24 * 7, parent: refresh.token, master: true }; 35 | this.data.authorizedTokens.push(authorized); 36 | // save 37 | this.write(); 38 | } 39 | 40 | static getInstance(tokens?: { accessToken: string, refreshToken: string }): TokenDatabase { 41 | if (!this.#instance) { 42 | if(!tokens) throw Error('getInstance requires constructor tokens'); 43 | this.#instance = new this(tokens); 44 | } 45 | return this.#instance; 46 | } 47 | 48 | isExpired(token: RefreshToken) { 49 | return token.expire < Date.now(); 50 | } 51 | areParent(parent:RefreshToken, child:AuthorizedToken) { 52 | return parent.token === child.parent; 53 | } 54 | 55 | isValidAccessToken(token: string) { 56 | const tok = this.findAccessToken(token); 57 | if(!tok) return false; 58 | return tok.expire > Date.now(); 59 | } 60 | 61 | async generateAccess(refresh: RefreshToken, master = false) { 62 | const token = crypto.randomBytes(32).toString('hex'); 63 | const in5minutes = Date.now() + (5 * 60 * 1000); 64 | const authorized = { token, expire: in5minutes, parent: refresh.token, master }; 65 | await this.addAccessToken(authorized); 66 | return authorized; 67 | } 68 | async generateRefresh(master = false) { 69 | const token = crypto.randomBytes(32).toString('hex'); 70 | const in7days = Date.now() + (7 * 24 * 60 * 60 * 1000); 71 | const refresh = { token, expire: in7days, master }; 72 | await this.addRefreshToken(refresh); 73 | return refresh; 74 | } 75 | 76 | findAccessToken(token: string) { 77 | return this.data.authorizedTokens.find(t => t.token === token); 78 | } 79 | 80 | findRefreshToken(token: string) { 81 | return this.data.refreshTokens.find(t => t.token === token); 82 | } 83 | 84 | removeAccessToken(access: AuthorizedToken) { 85 | this.data.authorizedTokens = this.data.authorizedTokens.filter(t => t.token !== access.token); 86 | } 87 | 88 | removeRefreshToken(refresh: RefreshToken) { 89 | this.data.authorizedTokens = this.data.authorizedTokens.filter(t => t.parent !== refresh.token); 90 | this.data.refreshTokens = this.data.refreshTokens.filter(t => t.token !== refresh.token); 91 | } 92 | 93 | addAccessToken(token: AuthorizedToken) { 94 | this.data.authorizedTokens.push(token); 95 | return this.write(); 96 | } 97 | 98 | addRefreshToken(token: RefreshToken) { 99 | this.data.refreshTokens.push(token); 100 | return this.write(); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /packages/api/src/db/uuids.ts: -------------------------------------------------------------------------------- 1 | import { Database } from '@api/db'; 2 | import type { mirrorsLangsType } from '@i18n'; 3 | import { resolve } from 'path'; 4 | import { env } from 'process'; 5 | import { v5 as uuidv5 } from 'uuid'; 6 | 7 | export type uuid = { 8 | mirror: { name: string, version: number }; 9 | langs: mirrorsLangsType[], 10 | /** 11 | * chapter url 12 | * 13 | * @important if chapters share the same url the same uuid will be generated 14 | * @workaround append the chapter number/index/some other identifier at the end of the url 15 | */ 16 | url: string 17 | id: string 18 | } 19 | 20 | type uuids = { 21 | ids: uuid[]; 22 | } 23 | 24 | const defaultSettings = { 25 | ids: [], 26 | }; 27 | 28 | export default class uuidDatabase extends Database { 29 | readonly #NAMESPACE = 'af68caec-20c3-495a-90ff-0350710bc7a3'; 30 | #pending: number; 31 | static #instance: uuidDatabase; 32 | 33 | private constructor() { 34 | if(typeof env.USER_DATA === 'undefined') throw Error('USER_DATA is not defined'); 35 | super(resolve(env.USER_DATA, 'uuid_db.json'), defaultSettings); 36 | this.#pending = 0; 37 | setInterval(async () => { 38 | if(this.#pending > 0) { 39 | await this.write(); 40 | this.#pending = 0; 41 | } 42 | }, 1000 * 60); 43 | } 44 | 45 | static getInstance(): uuidDatabase { 46 | if (!this.#instance) { 47 | this.#instance = new this(); 48 | } 49 | return this.#instance; 50 | } 51 | 52 | #find(id: Omit) { 53 | return this.data.ids.find(i => i.mirror === id.mirror && i.langs.some(l => id.langs.includes(l)) && i.url === id.url); 54 | } 55 | 56 | #save(id: Omit) { 57 | const uuid = uuidv5(id.mirror + id.url, this.#NAMESPACE); 58 | // update uuid if it already exists 59 | const search = this.data.ids.findIndex(i => i.id === uuid); 60 | if(search > -1) this.data.ids[search] = { ...id, id: uuid, langs: Array.from(new Set(this.data.ids[search].langs.concat(id.langs))) }; 61 | else this.data.ids.push({id: uuid, ...id}); 62 | this.#pending++; 63 | return uuid; 64 | } 65 | 66 | generate(uuid: Omit): string 67 | generate(uuid: uuid, force:true):string 68 | generate(uuid: Omit | uuid, force?:boolean):string { 69 | const find = this.#find(uuid); 70 | if(find && find.id) return find.id; 71 | if(force && (uuid as uuid).id) return this.#force(uuid as uuid & { id: string }); 72 | return this.#save(uuid); 73 | } 74 | 75 | #force(uuid: uuid & { id: string }):string { 76 | // update uuid if it already exists 77 | const search = this.data.ids.findIndex(i => i.id === uuid.id); 78 | if(search > -1) this.data.ids[search] = { ...this.data.ids[search], url: uuid.url,langs: Array.from(new Set(this.data.ids[search].langs.concat(uuid.langs))) }; 79 | else this.data.ids.push(uuid); 80 | this.#pending++; 81 | return uuid.id; 82 | } 83 | 84 | remove(id: string) { 85 | this.data.ids = this.data.ids.filter(i => i.id !== id); 86 | this.#pending++; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /packages/api/src/env.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | declare global { 3 | namespace NodeJS { 4 | interface ProcessEnv { 5 | ELECTRON_RUN_AS_NODE?: string; 6 | /** Server's login */ 7 | LOGIN:string, 8 | /** Server's password */ 9 | PASSWORD:string, 10 | /** Server's port */ 11 | PORT:string, 12 | /** 13 | * Server's url 14 | * 15 | * ONLY USE WHEN `env.SSL = 'app'` 16 | * 17 | * @example "https://localhost" 18 | * @example "https://my-server.com" 19 | */ 20 | HOSTNAME?: string, 21 | /** 22 | * SSL MODE 23 | * 24 | * - Provide your own SSL certificate and key: `env.SSL = 'provided'` 25 | * - needs `env.CERT` and `env.KEY` 26 | * - Generate a self-signed certificate and use it: `env.SSL = 'app'` 27 | * - needs `env.HOSTNAME` 28 | * - No SSL (http): `env.SSL = 'false'` 29 | */ 30 | SSL: 'false' | 'app' | 'provided', 31 | /** 32 | * SSL certificate string or path 33 | * 34 | * path is prefered when API is standalone 35 | */ 36 | CERT?: string, 37 | /** 38 | * SSL key string or path 39 | * 40 | * path is prefered when API is standalone 41 | */ 42 | KEY?: string, 43 | /** 44 | * Path to stored data (config, cache, certificates, etc.) 45 | * 46 | * optional: default is `__dirname/user_data` 47 | * @example "C:\\Users\\user\\config\\electron-mangas-reader" 48 | * @example "/home/user/config/electron-mangas-reader" 49 | */ 50 | USER_DATA: string, 51 | 52 | /** Path to the HTML template */ 53 | VIEW?:string 54 | /** 55 | * Path to downloaded manga's chapters 56 | * 57 | * @example "C:\\Users\\user\\pictures" 58 | * @example "/home/user/pictures" 59 | */ 60 | DOWNLOAD_DATA: string, 61 | /** `"production"` or `"development"` (optional) */ 62 | MODE?: string, 63 | /** show console outputs */ 64 | VERBOSE?: string 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /packages/api/src/models/abstracts/selfhosted.ts: -------------------------------------------------------------------------------- 1 | import Mirror from '@api/models/abstracts'; 2 | import type { MirrorConstructor } from '@api/models/types/constructor'; 3 | 4 | type options = { 5 | login?: string | null, 6 | password?: string | null, 7 | host?: string | null, 8 | port?: number | null, 9 | protocol: 'http' | 'https', 10 | markAsRead: boolean 11 | } 12 | 13 | export class SelfHosted extends Mirror { 14 | selfhosted = true; 15 | constructor(opts: MirrorConstructor) { 16 | super(opts); 17 | } 18 | 19 | public get enabled(): boolean { 20 | if(this.credentialsRequired) { 21 | const { enabled, host, port, password, login} = this.options; 22 | if(enabled && host && port && password && login && this.isOnline) return true; 23 | return false; 24 | } else { 25 | const { enabled, host, port } = this.options; 26 | if (enabled && host && port) return true; 27 | return false; 28 | } 29 | } 30 | 31 | set enabled(val: boolean) { 32 | this.options.enabled = val; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /packages/api/src/models/fallenangels.ts: -------------------------------------------------------------------------------- 1 | import { MyMangaReaderCMS } from '@api/models/abstracts/mymangareadercms'; 2 | import icon from '@api/models/icons/fallenangels.png'; 3 | 4 | class FallenAngels extends MyMangaReaderCMS { 5 | constructor() { 6 | super({ 7 | version: 1, 8 | isDead: false, 9 | host: 'https://manga.fascans.com', 10 | name: 'fallenangels', 11 | displayName: 'Fallen Angels', 12 | langs: ['en'], 13 | icon, 14 | manga_page_appended_string: 'Manga ', 15 | meta: { 16 | speed: 0.3, 17 | quality: 0.4, 18 | popularity: 0.3, 19 | }, 20 | options: { 21 | cache:true, 22 | enabled: true, 23 | }, 24 | requestLimits: { 25 | time: 400, 26 | concurrent: 1, 27 | }, 28 | }); 29 | } 30 | } 31 | 32 | const fa = new FallenAngels(); 33 | export default fa; 34 | -------------------------------------------------------------------------------- /packages/api/src/models/icons/amr-importer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiPaix/Fukayo/f9bb5ba83d7f10a35f849a10c5513dbf814bfb94/packages/api/src/models/icons/amr-importer.png -------------------------------------------------------------------------------- /packages/api/src/models/icons/fallenangels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiPaix/Fukayo/f9bb5ba83d7f10a35f849a10c5513dbf814bfb94/packages/api/src/models/icons/fallenangels.png -------------------------------------------------------------------------------- /packages/api/src/models/icons/komga-importer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiPaix/Fukayo/f9bb5ba83d7f10a35f849a10c5513dbf814bfb94/packages/api/src/models/icons/komga-importer.png -------------------------------------------------------------------------------- /packages/api/src/models/icons/komga.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiPaix/Fukayo/f9bb5ba83d7f10a35f849a10c5513dbf814bfb94/packages/api/src/models/icons/komga.png -------------------------------------------------------------------------------- /packages/api/src/models/icons/mangadex-importer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiPaix/Fukayo/f9bb5ba83d7f10a35f849a10c5513dbf814bfb94/packages/api/src/models/icons/mangadex-importer.png -------------------------------------------------------------------------------- /packages/api/src/models/icons/mangadex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiPaix/Fukayo/f9bb5ba83d7f10a35f849a10c5513dbf814bfb94/packages/api/src/models/icons/mangadex.png -------------------------------------------------------------------------------- /packages/api/src/models/icons/mangafox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiPaix/Fukayo/f9bb5ba83d7f10a35f849a10c5513dbf814bfb94/packages/api/src/models/icons/mangafox.png -------------------------------------------------------------------------------- /packages/api/src/models/icons/mangahasu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiPaix/Fukayo/f9bb5ba83d7f10a35f849a10c5513dbf814bfb94/packages/api/src/models/icons/mangahasu.png -------------------------------------------------------------------------------- /packages/api/src/models/icons/scanfr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiPaix/Fukayo/f9bb5ba83d7f10a35f849a10c5513dbf814bfb94/packages/api/src/models/icons/scanfr.png -------------------------------------------------------------------------------- /packages/api/src/models/icons/tachidesk-importer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiPaix/Fukayo/f9bb5ba83d7f10a35f849a10c5513dbf814bfb94/packages/api/src/models/icons/tachidesk-importer.png -------------------------------------------------------------------------------- /packages/api/src/models/icons/tachidesk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiPaix/Fukayo/f9bb5ba83d7f10a35f849a10c5513dbf814bfb94/packages/api/src/models/icons/tachidesk.png -------------------------------------------------------------------------------- /packages/api/src/models/imports/abstracts/index.ts: -------------------------------------------------------------------------------- 1 | import type { ServerToClientEvents } from '@api/server/types'; 2 | import EventEmitter from 'events'; 3 | import type TypedEmitter from 'typed-emitter'; 4 | import { env } from 'process'; 5 | 6 | export default class Importer extends (EventEmitter as new () => TypedEmitter) { 7 | name: string; 8 | url: string; 9 | icon: string; 10 | displayName: string; 11 | constructor(name: string, url: string, displayName: string, icon: string) { 12 | super(); 13 | this.name = name; 14 | this.url = url; 15 | this.displayName = displayName; 16 | this.icon = icon; 17 | } 18 | protected logger(...args: unknown[]) { 19 | const prefix = env.VERBOSE === 'true' ? `${new Date().toLocaleString()} [api] (${this.constructor.name})` : `[api] (\x1b[32m${this.constructor.name}\x1b[0m)`; 20 | if(env.MODE === 'development' || env.VERBOSE === 'true') console.log(prefix ,...args); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/api/src/models/imports/index.ts: -------------------------------------------------------------------------------- 1 | import AMRImporter from '@api/models/imports/all-mangas-reader'; 2 | import type Importer from '@api/models/imports/interfaces'; 3 | import KomgaImporter from '@api/models/imports/komga'; 4 | import MangadexImporter from '@api/models/imports/mangadex'; 5 | import TachideskImporter from '@api/models/imports/tachidesk'; 6 | 7 | const imports:Importer[] = [MangadexImporter, TachideskImporter, KomgaImporter, AMRImporter]; 8 | 9 | export default imports; 10 | -------------------------------------------------------------------------------- /packages/api/src/models/imports/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable semi */ 2 | import type { socketInstance } from '@api/server/types'; 3 | import type { mirrorsLangsType } from '@i18n'; 4 | 5 | export default interface ImporterInterface { 6 | name: string; 7 | url: string; 8 | icon: string; 9 | displayName: string; 10 | get #enabled():boolean 11 | get #login(): string 12 | get #password(): string 13 | get showInfo(): { url: string, name: string, displayName: string, enabled: boolean, icon:string } 14 | getMangas(socket: socketInstance, id:number, langs:mirrorsLangsType[], json?:string):void 15 | } 16 | -------------------------------------------------------------------------------- /packages/api/src/models/imports/komga.ts: -------------------------------------------------------------------------------- 1 | import MangasDB from '@api/db/mangas'; 2 | import mirrors from '@api/models'; 3 | import icon from '@api/models/icons/komga-importer.png'; 4 | import Importer from '@api/models/imports/abstracts'; 5 | import type ImporterInterface from '@api/models/imports/interfaces'; 6 | import type { ImportResults } from '@api/models/imports/types'; 7 | import type Komga from '@api/models/komga'; 8 | import { isErrorMessage } from '@api/models/types/errors'; 9 | import type { socketInstance } from '@api/server/types'; 10 | import type { mirrorsLangsType } from '@i18n'; 11 | 12 | const komga = mirrors.find(m=> m.name === 'komga') as typeof Komga; 13 | 14 | class KomgaImporter extends Importer implements ImporterInterface { 15 | constructor() { 16 | super(komga.name, komga.host, komga.displayName, icon); 17 | } 18 | 19 | get showInfo(): { url: string; name: string; displayName: string; enabled: boolean; icon: string; } { 20 | return { url: komga.host, name: komga.name, displayName: komga.displayName, enabled: komga.enabled, icon }; 21 | } 22 | 23 | async getMangas(socket: socketInstance, id: number, langs: mirrorsLangsType[]) { 24 | let cancel = false; 25 | 26 | const stopListening = () => { 27 | cancel = true; 28 | socket.removeListener('disconnect', stopListening); 29 | socket.removeListener('stopShowImports', stopListening); 30 | }; 31 | 32 | socket.once('disconnect', stopListening); 33 | socket.once('stopShowImports', stopListening); 34 | 35 | const lists = await komga.getLists(); 36 | if(isErrorMessage(lists)) { 37 | socket.emit('showImports', id, lists); 38 | socket.removeAllListeners('showImports'); 39 | return; 40 | } 41 | this.logger('[api]', 'importing', lists.length, 'mangas'); 42 | socket.emit('showImports', id, lists.length); 43 | 44 | const db = await MangasDB.getInstance(); 45 | const indexes = (await db.getIndexes()).filter(m => m.mirror.name === komga.name && m.mirror.version === komga.version); 46 | 47 | const nodb:typeof lists = []; 48 | const indb:ImportResults[] = []; 49 | 50 | for(const m of lists) { 51 | if(cancel) break; 52 | const lookup = indexes.find(f => f.id == m.id); 53 | if(!lookup) { 54 | nodb.push(m); 55 | continue; 56 | } 57 | const res = await db.get({id: lookup.id, langs: lookup.langs }); 58 | if(!res) continue; 59 | indb.push({ 60 | name: res.name, 61 | langs: res.langs, 62 | inLibrary: true, 63 | url: res.url, 64 | covers: res.covers, 65 | mirror: { 66 | name: komga.name, 67 | langs: komga.mirrorInfo.langs, 68 | }, 69 | }); 70 | } 71 | socket.emit('showImports', id, indb.length+nodb.length); 72 | socket.emit('showImports', id, indb); 73 | if(!cancel) { 74 | stopListening(); 75 | komga.getMangasToImport(id, socket, langs, nodb); 76 | } 77 | } 78 | 79 | } 80 | 81 | const komgaImporter = new KomgaImporter(); 82 | export default komgaImporter; 83 | -------------------------------------------------------------------------------- /packages/api/src/models/imports/mangadex.ts: -------------------------------------------------------------------------------- 1 | import MangasDB from '@api/db/mangas'; 2 | import mirrors from '@api/models'; 3 | import icon from '@api/models/icons/mangadex-importer.png'; 4 | import Importer from '@api/models/imports/abstracts'; 5 | import type ImporterInterface from '@api/models/imports/interfaces'; 6 | import type { ImportResults } from '@api/models/imports/types'; 7 | import type MangaDex from '@api/models/mangadex'; 8 | import { isErrorMessage } from '@api/models/types/errors'; 9 | import type { socketInstance } from '@api/server/types'; 10 | import type { mirrorsLangsType } from '@i18n'; 11 | 12 | 13 | const mangadex = mirrors.find(m=> m.name === 'mangadex') as typeof MangaDex; 14 | 15 | class MangadexImporter extends Importer implements ImporterInterface { 16 | constructor() { 17 | super(mangadex.name, mangadex.host, mangadex.displayName, icon); 18 | } 19 | get #enabled():boolean { 20 | if(!mangadex) throw Error('couldnt find mangadex mirror impl.'); 21 | const {login, password} = mangadex.options; 22 | const hasLogin = typeof login === 'string' && login.length > 0; 23 | const hasPassword = typeof password === 'string' && password.length > 0; 24 | return mangadex.enabled && mangadex.loggedIn && hasLogin && hasPassword; 25 | } 26 | 27 | get #login() { 28 | if(!mangadex) throw Error('couldnt find mangadex mirror impl.'); 29 | return mangadex.options.login; 30 | } 31 | 32 | get #password() { 33 | if(!mangadex) throw Error('couldnt find mangadex mirror impl.'); 34 | return mangadex.options.password; 35 | } 36 | 37 | get showInfo(): { url: string; name: string; displayName: string; enabled: boolean; icon: string; } { 38 | return { 39 | url: mangadex.host, 40 | name: mangadex.name, 41 | displayName: mangadex.displayName, 42 | enabled: this.#enabled, 43 | icon: icon, 44 | }; 45 | } 46 | 47 | async getMangas(socket: socketInstance, id:number, langs:mirrorsLangsType[]) { 48 | let cancel = false; 49 | 50 | const stopListening = () => { 51 | cancel = true; 52 | socket.removeListener('disconnect', stopListening); 53 | socket.removeListener('stopShowImports', stopListening); 54 | }; 55 | 56 | socket.once('disconnect', stopListening); 57 | socket.once('stopShowImports', stopListening); 58 | 59 | if(!mangadex) throw Error('couldnt find mangadex mirror impl.'); 60 | const lists = await mangadex.getLists(); 61 | if(isErrorMessage(lists)) { 62 | socket.emit('showImports', id, lists); 63 | socket.removeAllListeners('showImports'); 64 | return; 65 | } 66 | 67 | const db = await MangasDB.getInstance(); 68 | /** this id is a test entry from mangadex: f9c33607-9180-4ba6-b85c-e4b5faee7192 */ 69 | const mangas = Array.from(new Set(lists.map(l => l.relationships.filter(m => m.type === 'manga').map(m=> m.id)).flat())); 70 | const indexes = (await db.getIndexes()).filter(m => m.mirror.name === mangadex.name && m.mirror.version === mangadex.version); 71 | const nodb:string[] = []; 72 | const indb:ImportResults[] = []; 73 | 74 | this.logger('importing', mangas.length, 'mangas'); 75 | 76 | for(const m of mangas) { 77 | const lookup = indexes.find(f => f.id === m); 78 | if(!lookup) { 79 | nodb.push(m); 80 | continue; 81 | } 82 | const res = await db.get({id: lookup.id, langs: lookup.langs }); 83 | if(!res) continue; 84 | indb.push({ 85 | name: res.name, 86 | langs: res.langs, 87 | inLibrary: true, 88 | url: res.url, 89 | covers: res.covers, 90 | mirror: { 91 | name: mangadex.name, 92 | langs: mangadex.mirrorInfo.langs, 93 | }, 94 | }); 95 | } 96 | 97 | socket.emit('showImports', id, nodb.length+indb.length); 98 | if(indb.length) socket.emit('showImports', id, indb); 99 | 100 | if(!cancel) { 101 | stopListening(); 102 | mangadex.getMangasFromList(id, socket, langs, nodb); 103 | } 104 | } 105 | } 106 | 107 | const mangadexImporter = new MangadexImporter(); 108 | export default mangadexImporter; 109 | -------------------------------------------------------------------------------- /packages/api/src/models/imports/tachidesk.ts: -------------------------------------------------------------------------------- 1 | import MangasDB from '@api/db/mangas'; 2 | import mirrors from '@api/models'; 3 | import icon from '@api/models/icons/tachidesk-importer.png'; 4 | import Importer from '@api/models/imports/abstracts'; 5 | import type ImporterInterface from '@api/models/imports/interfaces'; 6 | import type Tachidesk from '@api/models/tachidesk'; 7 | import { isErrorMessage } from '@api/models/types/errors'; 8 | import type { socketInstance } from '@api/server/types'; 9 | import type { mirrorsLangsType } from '@i18n'; 10 | import type { ImportResults } from './types'; 11 | 12 | const tachidesk = mirrors.find(m=> m.name === 'tachidesk') as typeof Tachidesk; 13 | 14 | class TachideskImporter extends Importer implements ImporterInterface { 15 | constructor() { 16 | super(tachidesk.name, tachidesk.host, tachidesk.displayName, icon); 17 | } 18 | 19 | get showInfo(): { url: string; name: string; displayName: string; enabled: boolean; icon: string; } { 20 | return { url: tachidesk.host, name: tachidesk.name, displayName: tachidesk.displayName, enabled: tachidesk.enabled, icon }; 21 | } 22 | async getMangas(socket: socketInstance, id: number, langs: mirrorsLangsType[]) { 23 | let cancel = false; 24 | 25 | const stopListening = () => { 26 | cancel = true; 27 | socket.removeListener('disconnect', stopListening); 28 | socket.removeListener('stopShowImports', stopListening); 29 | }; 30 | 31 | socket.once('disconnect', stopListening); 32 | socket.once('stopShowImports', stopListening); 33 | 34 | const lists = await tachidesk.getLists(); 35 | if(isErrorMessage(lists)) { 36 | socket.emit('showImports', id, lists); 37 | socket.removeAllListeners('showImports'); 38 | return; 39 | } 40 | 41 | this.logger('importing', lists.length, 'mangas'); 42 | socket.emit('showImports', id, lists.length); 43 | 44 | const db = await MangasDB.getInstance(); 45 | const indexes = (await db.getIndexes()).filter(m => m.mirror.name === tachidesk.name && m.mirror.version === tachidesk.version); 46 | 47 | const nodb:typeof lists = []; 48 | const indb:ImportResults[] = []; 49 | 50 | for(const m of lists) { 51 | const lookup = indexes.find(f => f.id == m.id); 52 | if(!lookup) { 53 | nodb.push(m); 54 | continue; 55 | } 56 | const res = await db.get({id: lookup.id, langs: lookup.langs }); 57 | if(!res) continue; 58 | indb.push({ 59 | name: res.name, 60 | langs: res.langs, 61 | inLibrary: true, 62 | url: res.url, 63 | covers: res.covers, 64 | mirror: { 65 | name: tachidesk.name, 66 | langs: tachidesk.mirrorInfo.langs, 67 | }, 68 | }); 69 | } 70 | if(!cancel) { 71 | stopListening(); 72 | tachidesk.getMangasToImport(id, socket, langs, nodb); 73 | } 74 | } 75 | 76 | } 77 | 78 | const tachideskImporter = new TachideskImporter(); 79 | export default tachideskImporter; 80 | -------------------------------------------------------------------------------- /packages/api/src/models/imports/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { mirrorsLangsType } from '@i18n'; 2 | 3 | export type ImportResults = { 4 | mirror: { 5 | name: string, 6 | langs: mirrorsLangsType[], 7 | } 8 | name: string, 9 | langs: mirrorsLangsType[], 10 | url: string, 11 | covers: string[], 12 | inLibrary: boolean, 13 | } 14 | 15 | export type CantImportResults = { 16 | mirror: undefined, 17 | name: string 18 | langs?: mirrorsLangsType[] 19 | } 20 | -------------------------------------------------------------------------------- /packages/api/src/models/index.ts: -------------------------------------------------------------------------------- 1 | import type Mirror from '@api/models/abstracts'; 2 | import fallenangels from '@api/models/fallenangels'; 3 | import type MirrorInterface from '@api/models/interfaces'; 4 | import komga from '@api/models/komga'; 5 | import mangadex from '@api/models/mangadex'; 6 | import mangafox from '@api/models/mangafox'; 7 | import mangahasu from '@api/models/mangahasu'; 8 | import scanfr from '@api/models/scan-fr'; 9 | import tachidesk from '@api/models/tachidesk'; 10 | 11 | /** Every mirrors */ 12 | const mirrors: (Mirror & MirrorInterface)[] = [ 13 | mangafox, 14 | mangahasu, 15 | scanfr, 16 | fallenangels, 17 | komga, 18 | tachidesk, 19 | mangadex, 20 | ]; 21 | 22 | export default mirrors; 23 | -------------------------------------------------------------------------------- /packages/api/src/models/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | import type Importer from '@api/models/imports/abstracts'; 2 | import type Scheduler from '@api/server/scheduler'; 3 | import type { socketInstance } from '@api/server/types'; 4 | import type { mirrorsLangsType } from '@i18n'; 5 | 6 | /** Interface for Mirror classes */ 7 | export default interface MirrorInterface { 8 | /** 9 | * log-in the mirror 10 | */ 11 | login?(): Promise 12 | /** 13 | * Test if url is a manga page 14 | */ 15 | isMangaPage(url:string): boolean; 16 | 17 | /** 18 | * Test if url is a chapter page 19 | */ 20 | isChapterPage(url:string): boolean; 21 | 22 | /** 23 | * Search manga by name 24 | * @param {String} query Search string 25 | * @param {socketInstance} socket user socket 26 | * @param {Number} id request's uid 27 | */ 28 | search(query: string, requestLangs: mirrorsLangsType[] ,socket:socketInstance|Scheduler|Importer, id:number): void; 29 | /** 30 | * Get manga infos (title, authors, tags, chapters, covers, etc..) 31 | * @param url Relative url to the manga page 32 | * @param langs ISO-639-1 language code 33 | * @param socket the request initiator 34 | * @param id arbitrary id 35 | */ 36 | manga(url:string, requestLangs: mirrorsLangsType[], socket:socketInstance|Scheduler, id:number): void; 37 | /** 38 | * Get all images from chapter 39 | * @param link Relative url of chapter page (any page) 40 | * @param lang requested language 41 | * @param socket the request initiator 42 | * @param id arbitrary id 43 | * @param callback callback function to tell the client how many pages to expect 44 | * @param retryIndex If you don't need the whole chapter, you can pass the index of the page you want to start from (0-based) 45 | */ 46 | chapter(link:string, lang:mirrorsLangsType, socket:socketInstance, id:number, callback?: (nbOfPagesToExpect:number)=>void, retryIndex?:number): void; 47 | 48 | markAsRead?(mangaURL:string, lang:mirrorsLangsType, chapterURLs:string[], read:boolean): void; 49 | /** 50 | * 51 | * @param socket the request initiator 52 | * @param id arbitrary id 53 | */ 54 | recommend(requestLangs: mirrorsLangsType[], socket:socketInstance, id:number): void; 55 | // eslint-disable-next-line semi, @typescript-eslint/no-extra-semi 56 | }; 57 | -------------------------------------------------------------------------------- /packages/api/src/models/scan-fr.ts: -------------------------------------------------------------------------------- 1 | import { MyMangaReaderCMS } from '@api/models/abstracts/mymangareadercms'; 2 | import icon from '@api/models/icons/scanfr.png'; 3 | 4 | class scanfr extends MyMangaReaderCMS<{ enabled: boolean}> { 5 | constructor() { 6 | super({ 7 | version: 1, 8 | isDead: false, 9 | host: 'https://www.scan-fr.org', 10 | althost: ['https://.scan-fr.org', 'https://www.scan-fr.cc', 'https://scan-fr.cc'], 11 | name: 'scanfr', 12 | displayName: 'Scan-FR', 13 | langs: ['fr'], 14 | icon, 15 | chapter_selector: 'ul.chapterszozo a[href*=\'/manga/\']', 16 | manga_page_appended_string: 'Manga ', 17 | meta: { 18 | speed: 0.3, 19 | quality: 0.7, 20 | popularity: 0.5, 21 | }, 22 | options: { 23 | cache:true, 24 | enabled: true, 25 | }, 26 | requestLimits: { 27 | time: 400, 28 | concurrent: 1, 29 | }, 30 | }); 31 | } 32 | } 33 | 34 | const scan_fr = new scanfr(); 35 | export default scan_fr; 36 | -------------------------------------------------------------------------------- /packages/api/src/models/types/chapter.ts: -------------------------------------------------------------------------------- 1 | export type ChapterImage = { 2 | /** 0 based index of the page */ 3 | index: number, 4 | /** 5 | * Base64 data string of the page image 6 | * @example "data:image/png;base64,..." 7 | */ 8 | src: string, 9 | /** 10 | * Weither this page is the last one 11 | */ 12 | lastpage: boolean 13 | /** 14 | * height 15 | */ 16 | height: number, 17 | /** 18 | * width 19 | */ 20 | width: number 21 | } 22 | -------------------------------------------------------------------------------- /packages/api/src/models/types/constructor.ts: -------------------------------------------------------------------------------- 1 | import type { mirrorsLangsType } from '@i18n'; 2 | 3 | export type MirrorConstructor, T = S & { enabled: boolean, cache:boolean }> = { 4 | /** 5 | * mirror's implementation version 6 | * 7 | * ⚠️ Mirror version must be incremented ONLY IF the mirror changed all of its mangas/chapter urls. 8 | * or you introduce some changes that breaks previous version of mangas in db 9 | * 10 | */ 11 | version: number, 12 | /** is the mirror dead */ 13 | isDead: boolean, 14 | /** slug name: `az-_` */ 15 | name: string, 16 | /** full name */ 17 | displayName: string, 18 | /** 19 | * hostname without ending slash 20 | * @example 'https://www.mirror.com' 21 | */ 22 | host: string, 23 | /** alternative hostnames were the site can be reached */ 24 | althost?: string[] 25 | /** 26 | * mirror icon (import) 27 | * @example 28 | * import icon from './my-mirror.png'; 29 | * opts.icon = icon; 30 | */ 31 | icon: string 32 | /** 33 | * Languages supported by the mirror 34 | * 35 | * ISO 639-1 codes 36 | */ 37 | langs: mirrorsLangsType[], 38 | /** 39 | * does the mirror treats different languages for the same manga as different entries 40 | * @default true 41 | * @example 42 | * ```js 43 | * // multipleLangsOnSameEntry = false 44 | * manga_from_mangadex = { title: 'A', url: `/manga/xyz`, langs: ['en', 'jp'] } 45 | * 46 | * // multipleLangsOnSameEntry = true 47 | * manga_from_tachidesk = { title: 'B', url: `/manga/yz`, langs: ['en'] } 48 | * manga_from_tachidesk2 = { title: 'B', url: `/manga/xyz`, langs: ['jp'] } 49 | * ``` 50 | */ 51 | entryLanguageHasItsOwnURL?: boolean, 52 | /** crendentials required? */ 53 | credentialsRequired?: boolean; 54 | /** Meta information */ 55 | meta: { 56 | /** 57 | * quality of scans 58 | * 59 | * Number between 0 and 1 60 | */ 61 | quality: number, 62 | /** 63 | * Speed of releases 64 | * 65 | * Number between 0 and 1 66 | */ 67 | speed: number, 68 | /** 69 | * Mirror's popularity 70 | * 71 | * Number between 0 and 1 72 | */ 73 | popularity: number, 74 | } 75 | 76 | /** Requests limits */ 77 | requestLimits: { 78 | /** time in ms between each requests (or each batch of concurrent requests) */ 79 | time: number, 80 | /** number of requests that can be sent at once */ 81 | concurrent: number 82 | } 83 | /** 84 | * 85 | */ 86 | 87 | /** 88 | * Mirror specific option 89 | * @example { adult: true, lowres: false } 90 | */ 91 | options: T 92 | } 93 | -------------------------------------------------------------------------------- /packages/api/src/models/types/errors.ts: -------------------------------------------------------------------------------- 1 | export type ErrorMessage = { 2 | error:string 3 | trace?:string 4 | } 5 | 6 | export type MangaErrorMessage = { 7 | error:'manga_error'|'manga_error_unknown'|'manga_error_invalid_link'|'manga_error_mirror_not_found' 8 | trace?:string 9 | } 10 | 11 | export type SearchErrorMessage = { 12 | mirror: string; 13 | error:'search_error'|'search_error_unknown' 14 | trace?:string 15 | } 16 | 17 | export type ChapterErrorMessage = { 18 | error:'chapter_error'|'chapter_error_unknown'|'chapter_error_invalid_link'|'chapter_error_fetch'|'chapter_error_no_pages' 19 | trace?: string 20 | url?: string 21 | } 22 | export type ChapterImageErrorMessage = { 23 | error:'chapter_error'|'chapter_error_unknown'|'chapter_error_invalid_link'|'chapter_error_fetch'|'chapter_error_no_image'|'chapter_error_mirror_not_found' 24 | trace?: string 25 | url?: string 26 | index: number 27 | lastpage: boolean 28 | } 29 | 30 | export type RecommendErrorMessage = { 31 | mirror: string; 32 | error:'recommend_error'|'recommend_error_unknown' 33 | trace?:string 34 | } 35 | 36 | export type importErrorMessage = { 37 | error:'import_error' 38 | trace?: string | 'importer_not_found' 39 | } 40 | 41 | export function isErrorMessage(x: unknown): x is ErrorMessage { 42 | if(!x) return false; 43 | if(typeof x === 'object') { 44 | return Object.prototype.hasOwnProperty.call(x, 'error'); 45 | } 46 | return false; 47 | } 48 | -------------------------------------------------------------------------------- /packages/api/src/models/types/manga.ts: -------------------------------------------------------------------------------- 1 | import type { mirrorsLangsType } from '@i18n'; 2 | 3 | export interface MangaPage { 4 | /** 5 | * ID of the manga 6 | * @example 'mirror_name/manga_lang/relative-url-of-manga' 7 | */ 8 | id: string 9 | /** 10 | * Relative url of the manga (with leading slash) 11 | * 12 | * @example '/manga/manga-name' 13 | */ 14 | url:string, 15 | /** 16 | * Language of the manga 17 | * 18 | * ISO 639-1 codes 19 | */ 20 | langs: mirrorsLangsType[], 21 | /** Manga's full name */ 22 | name: string, 23 | /** Custom manga's name defined by user */ 24 | displayName?: string, 25 | /** 26 | * Array of covers in base64 data string 27 | * 28 | * @example ["data:image/png;base64,...", "data:image/png;base64,..."] 29 | */ 30 | covers: string[], 31 | /** Summary */ 32 | synopsis?: string, 33 | /** Tags */ 34 | tags:string[], 35 | /** Authors */ 36 | authors: string[], 37 | /** Is the manga saved in db */ 38 | inLibrary: boolean 39 | /** user categories */ 40 | userCategories: string[] 41 | /** publication status */ 42 | status: 'ongoing'|'completed'|'hiatus'|'cancelled'|'unknown', 43 | /** chapters */ 44 | chapters: { 45 | /** 46 | * Chapter's uuid 47 | */ 48 | id: string, 49 | /** 50 | * Chapter's relative URL 51 | * @example '/manga/manga-name/chapter-name' 52 | */ 53 | url: string 54 | /** 55 | * chapter language 56 | */ 57 | lang: mirrorsLangsType 58 | /** fetch date */ 59 | date: number 60 | /** 61 | * Chapter number (float) or position in the list 62 | * 63 | * position is used if the chapter is not numbered (or not available) 64 | */ 65 | number: number 66 | /** Chapter's name */ 67 | name?: string 68 | /** 69 | * Chapter's volume number 70 | */ 71 | volume?: number 72 | /** 73 | * Scanlator's name 74 | * 75 | * to use if mirror can provide the same chapter from multiple scanlators 76 | */ 77 | group?: string 78 | /** 79 | * Chapter's read status 80 | * 81 | * reset to false if the chapter is reloaded without being in the library 82 | */ 83 | read: boolean 84 | }[] 85 | /** mirror name */ 86 | mirror: { 87 | name: string, 88 | /** mirror's implementation version `Integer` */ 89 | version: number 90 | } 91 | } 92 | 93 | export interface MangaInDB extends MangaPage { 94 | chapters : (MangaPage['chapters'][0])[] 95 | meta : { 96 | /** the last read chapter id */ 97 | lastReadChapterId?: string, 98 | /** last time chapters list has been updated */ 99 | lastUpdate: number, 100 | /** notify user when new chapter is out */ 101 | notify: boolean, 102 | /** should the manga chapters list be updated */ 103 | update: boolean, 104 | /** is the mirror broken */ 105 | broken: boolean, 106 | options: { 107 | webtoon: boolean, 108 | showPageNumber: boolean, 109 | zoomMode: 'auto' | 'stretch-width' | 'stretch-height', 110 | longStrip:boolean, 111 | overlay: boolean, 112 | longStripDirection: 'horizontal'|'vertical' 113 | book: boolean, 114 | bookOffset: boolean, 115 | rtl: boolean, 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /packages/api/src/models/types/search.ts: -------------------------------------------------------------------------------- 1 | import type { mirrorInfo } from '@api/models/types/shared'; 2 | import type { mirrorsLangsType } from '@i18n'; 3 | 4 | export type SearchResult = { 5 | /** 6 | * id of the manga 7 | */ 8 | id:string, 9 | /** 10 | * Relative url of the manga (with leading slash) 11 | * 12 | * @example '/manga/manga-name' 13 | */ 14 | url: string, 15 | /** 16 | * Language of the manga 17 | * 18 | * ISO 639-1 codes 19 | */ 20 | langs: mirrorsLangsType[], 21 | /** Manga's full name */ 22 | name: string, 23 | /** 24 | * Array of covers in base64 data string 25 | * 26 | * @example ["data:image/png;base64,...", "data:image/png;base64,..."] 27 | */ 28 | covers:string[], 29 | /** Summary */ 30 | synopsis?: string, 31 | /** Mirror's information */ 32 | mirrorinfo:mirrorInfo, 33 | /** is the manga in the library */ 34 | inLibrary: boolean, 35 | /** Latest release */ 36 | last_release?: { 37 | /** Chapter's name */ 38 | name?:string, 39 | /** Chapter's Volume */ 40 | volume?: number, 41 | /** 42 | * Chapter's Number 43 | * @import use `chapter.name` if chapter is not numbered 44 | */ 45 | chapter?: number 46 | }, 47 | } 48 | -------------------------------------------------------------------------------- /packages/api/src/models/types/shared.ts: -------------------------------------------------------------------------------- 1 | import type { mirrorsLangsType } from '@i18n'; 2 | 3 | export type TaskDone = { 4 | done: boolean; 5 | } 6 | /** 7 | * Mirror information 8 | */ 9 | export type mirrorInfo = { 10 | /** mirror's implementation version `Integer`*/ 11 | version: number, 12 | /** is online? */ 13 | isOnline: boolean, 14 | /** isdead */ 15 | isDead: boolean, 16 | /** Mirror's slug */ 17 | name:string, 18 | /** Mirror's full name */ 19 | displayName: string, 20 | /** is mirror logged in? */ 21 | loggedIn?: boolean, 22 | /** 23 | * hostname without ending slash 24 | * @example 'https://www.mirror.com' 25 | */ 26 | host:string, 27 | /** is the site self hosted? */ 28 | selfhosted:boolean, 29 | /** do the site requires credentials */ 30 | credentialsRequired:boolean, 31 | /** 32 | * Whether the mirror is enabled 33 | */ 34 | enabled:boolean, 35 | /** 36 | * The icon in base 64 data string 37 | * @example "data:image/png;base64,..." 38 | */ 39 | icon:string, 40 | /** 41 | * Languages supported by the mirror 42 | * 43 | * ISO 639-1 codes 44 | */ 45 | langs: mirrorsLangsType[], 46 | /** 47 | * does the mirror treats different languages for the same manga as different entries 48 | * @default true 49 | * @example 50 | * ```js 51 | * // multipleLangsOnSameEntry = false 52 | * manga_from_mangadex = { title: 'A', url: `/manga/xyz`, langs: ['en', 'jp'] } 53 | * 54 | * // multipleLangsOnSameEntry = true 55 | * manga_from_tachidesk = { title: 'B', url: `/manga/yz`, langs: ['en'] } 56 | * manga_from_tachidesk2 = { title: 'B', url: `/manga/xyz`, langs: ['jp'] } 57 | * ``` 58 | */ 59 | entryLanguageHasItsOwnURL: boolean, 60 | /** 61 | * Mirror specific option 62 | * @example { adult: true, lowres: false } 63 | */ 64 | options: Record, 65 | /** Meta information */ 66 | meta: { 67 | /** 68 | * quality of scans 69 | * 70 | * Number between 0 and 1 71 | */ 72 | quality: number, 73 | /** 74 | * Speed of releases 75 | * 76 | * Number between 0 and 1 77 | */ 78 | speed: number, 79 | /** 80 | * Mirror's popularity 81 | * 82 | * Number between 0 and 1 83 | */ 84 | popularity: number, 85 | }; 86 | } 87 | -------------------------------------------------------------------------------- /packages/api/src/server/helpers/arrayEquals.ts: -------------------------------------------------------------------------------- 1 | export function arraysEqual(a:T[], b:T[]) { 2 | if (a === b) return true; 3 | if (a == null || b == null) return false; 4 | if (a.length !== b.length) return false; 5 | 6 | // If you don't care about the order of the elements inside 7 | // the array, you should sort both arrays here. 8 | // Please note that calling sort on an array will modify that array. 9 | // you might want to clone your array first. 10 | 11 | for (let i = 0; i < a.length; ++i) { 12 | if (a[i] !== b[i]) return false; 13 | } 14 | return true; 15 | } 16 | -------------------------------------------------------------------------------- /packages/api/src/server/helpers/index.ts: -------------------------------------------------------------------------------- 1 | import Scheduler from '@api/server/scheduler'; 2 | 3 | export function removeAllCacheFiles() { 4 | const files = Scheduler.getAllCacheFiles(); 5 | files.forEach(file => Scheduler.unlinkSyncNoFail(file.filename)); 6 | Scheduler.getInstance().addCacheLog('cache', files.length, files.reduce((acc, f) => acc + f.size, 0)); 7 | } 8 | -------------------------------------------------------------------------------- /packages/api/src/server/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { ClientToServerEvents } from '@api/client/types'; 2 | import type { CantImportResults, ImportResults } from '@api/models/imports/types'; 3 | import type { ChapterImage } from '@api/models/types/chapter'; 4 | import type { ChapterErrorMessage, ChapterImageErrorMessage, importErrorMessage, MangaErrorMessage, RecommendErrorMessage, SearchErrorMessage } from '@api/models/types/errors'; 5 | import type { MangaInDB, MangaPage } from '@api/models/types/manga'; 6 | import type { SearchResult } from '@api/models/types/search'; 7 | import type { TaskDone } from '@api/models/types/shared'; 8 | import type { Socket } from 'socket.io'; 9 | 10 | export type ServerToClientEvents = { 11 | authorized: () => void; 12 | unauthorized: () => void; 13 | token: (acessToken: string) => void; 14 | refreshToken: (acessToken: string) => void; 15 | connectivity: (status: boolean) => void; 16 | loggedIn: (mirrorName:string, status: boolean) => void; 17 | isOnline: (mirrorName: string, status: boolean) => void; 18 | searchInMirrors: (id:number, mangas:SearchResult[]|SearchResult|SearchErrorMessage|TaskDone) => void; 19 | showManga: (id:number, manga:MangaPage|MangaInDB|MangaErrorMessage) =>void 20 | showMangas: (id:number, mangas:(MangaInDB | MangaPage | MangaErrorMessage)[]) =>void 21 | showChapter: (id:number, chapter:ChapterImage|ChapterImageErrorMessage|ChapterErrorMessage) => void; 22 | showRecommend: (id:number, mangas:SearchResult[]|SearchResult|RecommendErrorMessage|TaskDone) => void; 23 | showLibrary: (id:number, manga:MangaInDB[]) => void; 24 | showImports: (id: number, /** number = nb of mangas to expect */ manga:number|importErrorMessage|ImportResults[]|ImportResults|CantImportResults|CantImportResults[]|TaskDone) => void; 25 | finishedMangasUpdate: (nbOfUpdates:number) => void; 26 | startMangasUpdate: () => void; 27 | } 28 | 29 | export type socketInstance = Socket 30 | -------------------------------------------------------------------------------- /packages/api/src/server/types/scheduler.ts: -------------------------------------------------------------------------------- 1 | import type { MangaInDB } from '@api/models/types/manga'; 2 | import type { mirrorsLangsType } from '@i18n'; 3 | 4 | type MangaLogs = { 5 | /** date of log */ 6 | date: number, 7 | /** message */ 8 | message: string, 9 | /** manga id */ 10 | id: string 11 | } 12 | 13 | export type LogChapterError = MangaLogs & { 14 | message: 'log_chapter_error' 15 | /** error message */ 16 | data: string 17 | } 18 | 19 | export type LogChapterNew = MangaLogs & { 20 | message: 'log_chapter_new', 21 | data: MangaInDB['chapters'][0] 22 | } 23 | 24 | export type LogChapterRead = MangaLogs & { 25 | message: 'log_chapter_read', 26 | data: MangaInDB['chapters'][0] 27 | } 28 | 29 | 30 | export type LogMangaNewMetadata = MangaLogs & { 31 | message: 'log_manga_metadata', 32 | data: { 33 | tag: 'name' | 'langs' | 'synopsis' | 'authors' | 'covers' | 'tags' | 'status' 34 | oldVal?: string | string[] | mirrorsLangsType[] 35 | newVal?: string | string[] | mirrorsLangsType[] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/api/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export type ForkEnv = NodeJS.ProcessEnv & { 2 | ELECTRON_RUN_AS_NODE?: string; 3 | /** Server's login */ 4 | LOGIN:string, 5 | /** Server's password */ 6 | PASSWORD:string, 7 | /** Server's port */ 8 | PORT:string, 9 | /** 10 | * Server's url 11 | * 12 | * ONLY USE WHEN `env.SSL = 'app'` 13 | * 14 | * @example "https://localhost" 15 | * @example "https://my-server.com" 16 | */ 17 | HOSTNAME?: string, 18 | /** 19 | * SSL MODE 20 | * 21 | * - Provide your own SSL certificate and key: `env.SSL = 'provided'` 22 | * - needs `env.CERT` and `env.KEY` 23 | * - Generate a self-signed certificate and use it: `env.SSL = 'app'` 24 | * - needs `env.HOSTNAME` 25 | * - No SSL (http): `env.SSL = 'false'` 26 | */ 27 | SSL: 'false' | 'app' | 'provided', 28 | /** 29 | * SSL certificate string or path 30 | * 31 | * path is prefered when API is standalone 32 | */ 33 | CERT?: string, 34 | /** 35 | * SSL key string or path 36 | * 37 | * path is prefered when API is standalone 38 | */ 39 | KEY?: string, 40 | /** 41 | * Path to stored data (config, cache, certificates, etc.) 42 | * 43 | * optional: default is `__dirname/user_data` 44 | * @example "C:\\Users\\user\\config\\electron-mangas-reader" 45 | * @example "/home/user/config/electron-mangas-reader" 46 | */ 47 | USER_DATA: string, 48 | /** Path to the HTML template */ 49 | VIEW?:string 50 | /** 51 | * Path to downloaded manga's chapters 52 | * 53 | * @example "C:\\Users\\user\\pictures" 54 | * @example "/home/user/pictures" 55 | */ 56 | DOWNLOAD_DATA: string, 57 | /** `"production"` or `"development"` (optional) */ 58 | MODE?: string, 59 | } 60 | -------------------------------------------------------------------------------- /packages/api/src/utils/standalone.ts: -------------------------------------------------------------------------------- 1 | import { env } from 'process'; 2 | import fs from 'fs'; 3 | import { resolve } from 'path'; 4 | 5 | /** 6 | * Function to check if the environment is valid. 7 | */ 8 | export function verify() { 9 | const t = types(); 10 | if(t.length) { 11 | t.forEach((key) => console.log(`${key.name} ${key.type}`)); 12 | process.exit(1); 13 | } 14 | } 15 | 16 | function types() { 17 | const types = []; 18 | if(!env.LOGIN) types.push({ name: 'LOGIN', type: 'is missing' }); 19 | else if(typeof env.LOGIN !== 'string') types.push({ name: 'LOGIN', type: 'is not a string' }); 20 | else if(env.LOGIN.length < 3) types.push({ name: 'LOGIN', type: 'is too short' }); 21 | 22 | if(!env.PASSWORD) types.push({ name: 'PASSWORD', type: 'is missing' }); 23 | else if(typeof env.PASSWORD !== 'string') types.push({ name: 'PASSWORD', type: 'is not a string' }); 24 | else if(env.PASSWORD.length < 6) types.push({ name: 'PASSWORD', type: 'is too short (min 6 characters)' }); 25 | 26 | if(!env.PORT) types.push({ name: 'PORT', type: 'is missing' }); 27 | else if(typeof env.PORT !== 'string') types.push({ name: 'PORT', type: 'is not a string' }); 28 | else { 29 | const port = parseInt(env.PORT); 30 | if(isNaN(port)) types.push({ name: 'PORT', type: 'is not a number' }); 31 | else if(port < 1024 || port > 65535) types.push({ name: 'PORT', type: 'is out of range (1024-65535)' }); 32 | } 33 | 34 | if(env.VIEW) { 35 | if(typeof env.VIEW !== 'string') types.push({ name: 'VIEW', type: 'is not a string' }); 36 | else if(!fs.existsSync(resolve(env.VIEW))) types.push({ name: 'VIEW', type: 'is not a valid path' }); 37 | else if(!fs.statSync(resolve(env.VIEW)).isDirectory()) types.push({ name: 'VIEW', type: 'is not a directory' }); 38 | } 39 | 40 | if(!env.USER_DATA) { 41 | const newPath = resolve(__dirname, 'user_data'); 42 | env.USER_DATA = newPath; 43 | fs.mkdirSync(resolve(__dirname, 'user_data')); 44 | } 45 | 46 | if(!env.DOWNLOAD_DATA) { 47 | const newPath = resolve(__dirname, 'downloads'); 48 | env.DOWNLOAD_DATA = newPath; 49 | fs.mkdirSync(resolve(__dirname, 'downloads')); 50 | } 51 | else if(typeof env.DOWNLOAD_DATA !== 'string') types.push({ name: 'DOWNLOAD_DATA', type: 'is not a string' }); 52 | else if(!fs.existsSync(env.DOWNLOAD_DATA)) types.push({ name: 'DOWNLOAD_DATA', type: 'folder does not exist' }); 53 | else if(!fs.lstatSync(env.DOWNLOAD_DATA).isDirectory()) types.push({ name: 'DOWNLOAD_DATA', type: 'is not a folder' }); 54 | 55 | if(env.MODE) { 56 | if(typeof env.MODE !== 'string') types.push({ name: 'MODE', type: 'is not a string' }); 57 | else if(env.MODE !== 'production' && env.MODE !== 'development') types.push({ name: 'MODE', type: 'has an invalid value (production or development)' }); 58 | } 59 | 60 | if(!env.SSL) types.push({ name: 'SSL', type: 'missing' }); 61 | else if(typeof env.SSL !== 'string') types.push({ name: 'SSL', type: 'is not a string' }); 62 | else if(env.SSL !== 'app' && env.SSL !== 'provided' && env.SSL !== 'false') types.push({ name: 'SSL', type: 'has an invalid value ("app", "provided" or "false")' }); 63 | else if(env.SSL === 'app') { 64 | if(!env.HOSTNAME) types.push({ name: 'HOSTNAME', type: 'is missing' }); 65 | } 66 | else if(env.SSL === 'provided') { 67 | if(!env.CERT) types.push({ name: 'CERT', type: 'is missing' }); 68 | if(!env.KEY) types.push({ name: 'KEY', type: 'is missing' }); 69 | if(typeof env.CERT === 'string' && typeof env.KEY === 'string') { 70 | if(!fs.existsSync(env.CERT)) types.push({ name: 'CERT', type: 'file does not exist' }); 71 | if(!fs.existsSync(env.KEY)) types.push({ name: 'KEY', type: 'file does not exist' }); 72 | if(!fs.lstatSync(env.CERT).isFile()) types.push({ name: 'CERT', type: 'is not a file' }); 73 | if(!fs.lstatSync(env.KEY).isFile()) types.push({ name: 'KEY', type: 'is not a file' }); 74 | } 75 | else { 76 | if(typeof env.CERT !== 'string') types.push({ name: 'CERT', type: 'is not a string' }); 77 | if(typeof env.KEY !== 'string') types.push({ name: 'KEY', type: 'is not a string' }); 78 | } 79 | } 80 | return types; 81 | } 82 | -------------------------------------------------------------------------------- /packages/api/src/utils/steno.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Steno 3 | * https://github.com/typicode/steno 4 | * @license MIT 5 | * @sponsor https://github.com/sponsors/typicode 6 | */ 7 | 8 | 9 | /** 10 | * 11 | * The MIT License (MIT) 12 | * 13 | * Copyright (c) 2021 typicode 14 | * 15 | * Permission is hereby granted, free of charge, to any person obtaining a copy 16 | * of this software and associated documentation files (the "Software"), to deal 17 | * in the Software without restriction, including without limitation the rights 18 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | * copies of the Software, and to permit persons to whom the Software is 20 | * furnished to do so, subject to the following conditions: 21 | * 22 | * The above copyright notice and this permission notice shall be included in all 23 | * copies or substantial portions of the Software. 24 | * 25 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 31 | * SOFTWARE. 32 | */ 33 | import fs from 'fs'; 34 | import path from 'path'; 35 | 36 | // Returns a temporary file 37 | // Example: for /some/file will return /some/.file.tmp 38 | function getTempFilename(file: string): string { 39 | return path.join(path.dirname(file), '.' + path.basename(file) + '.tmp'); 40 | } 41 | 42 | type Resolve = () => void 43 | type Reject = (error: Error) => void 44 | 45 | export class Writer { 46 | #filename: string; 47 | #tempFilename: string; 48 | #locked = false; 49 | #prev: [Resolve, Reject] | null = null; 50 | #next: [Resolve, Reject] | null = null; 51 | #nextPromise: Promise | null = null; 52 | #nextData: string | null = null; 53 | 54 | get isFree() { 55 | return !this.#locked && !this.#nextData; 56 | } 57 | // File is locked, add data for later 58 | #add(data: string): Promise { 59 | // Only keep most recent data 60 | this.#nextData = data; 61 | 62 | // Create a singleton promise to resolve all next promises once next data is written 63 | this.#nextPromise ||= new Promise((resolve, reject) => { 64 | this.#next = [resolve, reject]; 65 | }); 66 | 67 | // Return a promise that will resolve at the same time as next promise 68 | return new Promise((resolve, reject) => { 69 | this.#nextPromise?.then(resolve).catch(reject); 70 | }); 71 | } 72 | 73 | // File isn't locked, write data 74 | async #write(data: string): Promise { 75 | // Lock file 76 | this.#locked = true; 77 | try { 78 | // Atomic write 79 | await fs.promises.writeFile(this.#tempFilename, data, 'utf-8'); 80 | await fs.promises.rename(this.#tempFilename, this.#filename); 81 | 82 | // Call resolve 83 | this.#prev?.[0](); 84 | } catch (err) { 85 | // Call reject 86 | this.#prev?.[1](err as Error); 87 | throw err; 88 | } finally { 89 | // Unlock file 90 | this.#locked = false; 91 | 92 | this.#prev = this.#next; 93 | this.#next = this.#nextPromise = null; 94 | 95 | if (this.#nextData !== null) { 96 | const nextData = this.#nextData; 97 | this.#nextData = null; 98 | await this.write(nextData); 99 | } 100 | } 101 | } 102 | 103 | constructor(filename: string) { 104 | this.#filename = filename; 105 | this.#tempFilename = getTempFilename(filename); 106 | } 107 | 108 | async write(data: string): Promise { 109 | return this.#locked ? this.#add(data) : this.#write(data); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /packages/api/src/utils/types/crawler.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosRequestConfig } from 'axios'; 2 | 3 | export interface ClusterJob extends AxiosRequestConfig { 4 | url: string 5 | /** The CSS selector to wait for */ 6 | waitForSelector?: string; 7 | /** Cookies */ 8 | cookies?: { name: string, value: string, domain: string, path: string }[] 9 | /** referer */ 10 | referer?: string; 11 | /** type */ 12 | type?: 'html'|'json'|'string' 13 | } 14 | -------------------------------------------------------------------------------- /packages/api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext", 4 | "target": "esnext", 5 | "sourceMap": false, 6 | "moduleResolution": "Node", 7 | "resolveJsonModule": true, 8 | "skipLibCheck": true, 9 | "strict": true, 10 | "isolatedModules": false, 11 | "allowSyntheticDefaultImports": true, 12 | "esModuleInterop": true, 13 | "declaration": true, 14 | "experimentalDecorators": true, 15 | "types" : ["node"], 16 | "baseUrl": ".", 17 | "typeRoots": ["./env.d.ts"], 18 | "paths": { 19 | "@renderer/*": [ 20 | "../renderer/src/*" 21 | ], 22 | "@api/*" : [ 23 | "../api/src/*" 24 | ], 25 | "@main/*" : [ 26 | "../main/src/*" 27 | ], 28 | "@preload/*" : [ 29 | "../preload/src/*" 30 | ], 31 | "@assets/*" : [ 32 | "../renderer/assets/*" 33 | ], 34 | "@buildResources/*" : [ 35 | "../../buildResources/*" 36 | ], 37 | "@i18n" : [ 38 | "../i18n/src" 39 | ], 40 | "@i18n/*" : [ 41 | "../i18n/src/*" 42 | ] 43 | }, 44 | }, 45 | "include": [ 46 | "src/**/*.ts", 47 | "../../types/**/*.d.ts", 48 | "src/env.d.ts" 49 | ], 50 | "exclude": [ 51 | "**/*.spec.ts", 52 | "**/*.test.ts" 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /packages/api/vite.config.js: -------------------------------------------------------------------------------- 1 | import { builtinModules } from 'module'; 2 | import { join } from 'path'; 3 | import { node } from '../../.electron-vendors.cache.json'; 4 | 5 | const PACKAGE_ROOT = __dirname; 6 | 7 | const externals = [ 8 | ...builtinModules.filter(m => !m.startsWith('_')), 9 | 'express', 10 | 'morgan', 11 | 'socket.io', 12 | 'puppeteer', 13 | 'puppeteer-cluster', 14 | 'puppeteer-extra', 15 | 'puppeteer-extra-plugin-stealth', 16 | 'puppeteer-extra-plugin-adblocker-no-vulnerabilities', 17 | '@puppeteer/browsers', 18 | 'systeminformation', 19 | 'axios', 20 | 'cheerio', 21 | 'file-type', 22 | 'socket.io-client', 23 | 'socket.io', 24 | 'filenamify', 25 | 'user-agents', 26 | 'form-data', 27 | 'connect-history-api-fallback', 28 | 'openid-client', 29 | 'electron-devtools-installer', 30 | 'image-size', 31 | ]; 32 | 33 | 34 | /** 35 | * @type {import('vite').UserConfig} 36 | * @see https://vitejs.dev/config/ 37 | */ 38 | const config = { 39 | mode: process.env.MODE, 40 | root: PACKAGE_ROOT, 41 | envDir: process.cwd(), 42 | resolve: { 43 | alias: { 44 | '@renderer': join(PACKAGE_ROOT, '..', 'renderer', 'src'), 45 | '@api': join(PACKAGE_ROOT, '..', 'api', 'src'), 46 | '@main': join(PACKAGE_ROOT, '..', 'main', 'src'), 47 | '@preload': join(PACKAGE_ROOT, '..', 'preload', 'src'), 48 | '@assets': join(PACKAGE_ROOT, '..', 'renderer', 'assets'), 49 | '@buildResources': join(PACKAGE_ROOT, '..', '..', 'buildResources'), 50 | '@i18n': join(PACKAGE_ROOT, '..', 'i18n', 'src'), 51 | }, 52 | }, 53 | build: { 54 | target: `node${node}`, 55 | outDir: 'dist', 56 | assetsDir: '.', 57 | minify: 'terser', 58 | lib: { 59 | formats: ['cjs'], 60 | entry: './src/index.ts', 61 | name: 'api', 62 | fileName: (format) => `index.${format}.js`, 63 | }, 64 | rollupOptions: { 65 | onwarn: (warning) => { 66 | if(warning.code === 'EVAL') return; 67 | }, 68 | external: externals, 69 | output:{ 70 | manualChunks(id) { 71 | const split = id.split('/'); 72 | if(split[split.length -2] === 'icons') { 73 | return 'icon.'+[split[split.length -1]]; 74 | } 75 | }, 76 | }, 77 | }, 78 | emptyOutDir: true, 79 | reportCompressedSize: false, 80 | }, 81 | }; 82 | 83 | export default config; 84 | -------------------------------------------------------------------------------- /packages/i18n/src/findLocale.ts: -------------------------------------------------------------------------------- 1 | import { appLangs } from '@i18n'; 2 | 3 | export function findAppLocale(lang:string) { 4 | const locale = appLangs.find(locale => { 5 | if (locale === lang) { 6 | return true; 7 | } 8 | const regex = new RegExp(`${locale}-.*`); 9 | return regex.test(lang); 10 | }); 11 | return locale ? locale : 'en'; 12 | } 13 | -------------------------------------------------------------------------------- /packages/i18n/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { appLangsType, mirrorsLangsType } from '@i18n/availableLangs'; 2 | import { appLangs, mirrorsLang } from '@i18n/availableLangs'; 3 | import { findAppLocale } from '@i18n/findLocale'; 4 | import { BC47_TO_ISO639_1, ISO3166_1_ALPHA2_TO_ISO639_1 } from '@i18n/isoConvert'; 5 | import { importLocale, loadLocale } from '@i18n/loadLocale'; 6 | 7 | export { appLangs, appLangsType, mirrorsLang, mirrorsLangsType, findAppLocale, importLocale, loadLocale, ISO3166_1_ALPHA2_TO_ISO639_1, BC47_TO_ISO639_1 }; 8 | -------------------------------------------------------------------------------- /packages/i18n/src/loadLocale.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/consistent-type-imports */ 2 | import type { appLangsType } from '@i18n'; 3 | import type { ConfigType } from 'dayjs'; //=> this is just because we need ILocale interface which isn't exported 4 | import type { QuasarLanguage } from 'quasar'; 5 | 6 | /* eslint-disable @typescript-eslint/no-unused-vars */ 7 | type useless = ConfigType 8 | 9 | export async function loadLocale(locale: appLangsType) { 10 | const json = await import(`../locales/${locale}.json`) as { default : typeof import('../locales/en.json') }; 11 | return json.default; 12 | } 13 | 14 | 15 | export async function importLocale(locale: appLangsType) { 16 | const quasarLangs = import.meta.glob('../../../node_modules/quasar/lang/*.mjs'); 17 | const dayJSLangs = import.meta.glob('../../../node_modules/dayjs/esm/locale/*.js'); 18 | 19 | let quasar: { default: QuasarLanguage } | undefined; 20 | let dayjs: { default: ILocale } | undefined; 21 | 22 | try { 23 | quasar = await quasarLangs[ `../../../node_modules/quasar/lang/${locale}.mjs` ]() as unknown as { default: QuasarLanguage }; 24 | } catch(err) { 25 | quasar = await quasarLangs[ '../../../node_modules/quasar/lang/en-US.mjs' ]() as unknown as { default: QuasarLanguage }; 26 | } 27 | 28 | try { 29 | dayjs = await dayJSLangs[ `../../../node_modules/dayjs/esm/locale/${ locale }.js` ]() as { default: ILocale }; 30 | } catch(err) { 31 | dayjs = await dayJSLangs[ '../../../node_modules/dayjs/esm/locale/en.js' ]() as { default: ILocale }; 32 | } 33 | 34 | if(!dayjs || !quasar) throw Error('couldn\'t load locales'); 35 | 36 | return { 37 | dayjs: dayjs.default, 38 | quasar: quasar.default, 39 | }; 40 | 41 | } 42 | -------------------------------------------------------------------------------- /packages/i18n/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions":{ 3 | "module":"esnext", 4 | "target":"esnext", 5 | "sourceMap":false, 6 | "moduleResolution":"Node", 7 | "skipLibCheck":true, 8 | "strict":true, 9 | "isolatedModules":false, 10 | "allowSyntheticDefaultImports":true, 11 | "types":[ 12 | "node" 13 | ], 14 | "resolveJsonModule":true, 15 | "baseUrl":".", 16 | "paths":{ 17 | "@renderer/*":[ 18 | "../renderer/src/*" 19 | ], 20 | "@api/*":[ 21 | "../api/src/*" 22 | ], 23 | "@main/*":[ 24 | "../main/src/*" 25 | ], 26 | "@preload/*":[ 27 | "../preload/src/*" 28 | ], 29 | "@assets/*":[ 30 | "../renderer/assets/*" 31 | ], 32 | "@buildResources/*":[ 33 | "../../buildResources/*" 34 | ], 35 | "@i18n" : [ 36 | "../i18n/src" 37 | ], 38 | "@i18n/*" : [ 39 | "../i18n/src/*" 40 | ] 41 | } 42 | }, 43 | "include":[ 44 | "src/**/*.ts", 45 | "../../types/**/*.d.ts" 46 | ], 47 | "exclude":[ 48 | "**/*.spec.ts", 49 | "**/*.test.ts" 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /packages/i18n/vite.config.js: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { chrome } from '../../.electron-vendors.cache.json'; 3 | import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite'; 4 | 5 | const PACKAGE_ROOT = __dirname; 6 | 7 | /** 8 | * @type {import('vite').UserConfig} 9 | * @see https://vitejs.dev/config/ 10 | */ 11 | const config = { 12 | mode: process.env.MODE, 13 | root: PACKAGE_ROOT, 14 | envDir: process.cwd(), 15 | plugins: [ 16 | VueI18nPlugin({ strictMessage: false }), 17 | ], 18 | resolve: { 19 | alias: { 20 | '@renderer': join(PACKAGE_ROOT, '..', 'renderer', 'src'), 21 | '@api': join(PACKAGE_ROOT, '..', 'api', 'src'), 22 | '@main': join(PACKAGE_ROOT, '..', 'main', 'src'), 23 | '@preload': join(PACKAGE_ROOT, '..', 'preload', 'src'), 24 | '@assets': join(PACKAGE_ROOT, '..', 'renderer', 'assets'), 25 | '@buildResources': join(PACKAGE_ROOT, '..', '..', 'buildResources'), 26 | '@i18n': join(PACKAGE_ROOT, '..', 'i18n', 'src'), 27 | 'vue-i18n': 'vue-i18n/dist/vue-i18n.runtime.esm-bundler.js', 28 | }, 29 | }, 30 | build: { 31 | target: `chrome${chrome}`, 32 | outDir: 'dist', 33 | assetsDir: '.', 34 | minify: 'terser', 35 | lib: { 36 | formats: ['cjs'], 37 | entry: './src/index.ts', 38 | name: 'i18n', 39 | fileName: (format) => `index.${format}.js`, 40 | }, 41 | rollupOptions: { 42 | output: { 43 | manualChunks(id) { 44 | if (id.includes('/node_modules/')) { 45 | const modules = ['quasar', '@quasar', 'dayjs']; 46 | const chunk = modules.find((module) => id.includes(`/node_modules/${module}`)); 47 | return chunk ? `vendor-${chunk}` : 'vendor'; 48 | } 49 | }, 50 | }, 51 | }, 52 | emptyOutDir: true, 53 | reportCompressedSize: false, 54 | }, 55 | }; 56 | 57 | export default config; 58 | -------------------------------------------------------------------------------- /packages/main/src/index.ts: -------------------------------------------------------------------------------- 1 | import { restoreOrCreateWindow } from '@main/mainWindow'; 2 | import { app } from 'electron'; 3 | import Ready from './appReady'; 4 | import './security-restrictions'; 5 | 6 | if(app.commandLine.hasSwitch('server')) { 7 | app.commandLine.appendSwitch('disable-gpu'); 8 | app.commandLine.appendSwitch('headless'); 9 | } 10 | 11 | /** 12 | * Prevent multiple instances 13 | */ 14 | const isSingleInstance = app.requestSingleInstanceLock(); 15 | if (!isSingleInstance) { 16 | app.quit(); 17 | process.exit(0); 18 | } 19 | app.on('second-instance', () => restoreOrCreateWindow); 20 | 21 | /** 22 | * Shout down background process if all windows was closed 23 | */ 24 | app.on('window-all-closed', () => { 25 | // for macOS 26 | if (process.platform == 'darwin') app.dock.hide(); 27 | }); 28 | 29 | /** 30 | * @see https://www.electronjs.org/docs/v14-x-y/api/app#event-activate-macos Event: 'activate' 31 | */ 32 | app.on('activate', () => restoreOrCreateWindow); 33 | 34 | /** Ignore SSL errors */ 35 | app.commandLine.appendSwitch('ignore-certificate-errors'); 36 | 37 | app.whenReady().then(() => new Ready()); 38 | -------------------------------------------------------------------------------- /packages/main/src/mainWindow.ts: -------------------------------------------------------------------------------- 1 | import icon from '@buildResources/icon_128.png'; 2 | import { BrowserWindow, globalShortcut, nativeImage } from 'electron'; 3 | import { join } from 'path'; 4 | import { URL } from 'url'; 5 | 6 | async function createWindow(bounds?: Electron.Rectangle) { 7 | const browserWindow = new BrowserWindow({ 8 | roundedCorners: true, 9 | show: false, // Use 'ready-to-show' event to show window 10 | x: bounds?.x, 11 | y: bounds?.y, 12 | height: bounds?.height, 13 | width: bounds?.width, 14 | webPreferences: { 15 | webviewTag: false, // The webview tag is not recommended. Consider alternatives like iframe or Electron's BrowserView. https://www.electronjs.org/docs/latest/api/webview-tag#warning 16 | preload: join(__dirname, '../../preload/dist/index.cjs'), 17 | }, 18 | icon: import.meta.env.MODE === 'test' ? join(__dirname, '../../../buildResources/icon_128.png') : nativeImage.createFromDataURL(icon), 19 | }); 20 | 21 | /** 22 | * Do not allow page refreshes in production 23 | */ 24 | if(import.meta.env.PROD) { 25 | browserWindow.on('focus', () => { 26 | globalShortcut.register('CommandOrControl+R', () => { /** */ }); 27 | globalShortcut.register('CommandOrControl+Shift+I', () => { /** */ }); 28 | globalShortcut.register('F5', () => { /** */ }); 29 | }); 30 | 31 | browserWindow.on('blur', () => { 32 | globalShortcut.unregisterAll(); 33 | }); 34 | } 35 | 36 | /** 37 | * If you install `show: true` then it can cause issues when trying to close the window. 38 | * Use `show: false` and listener events `ready-to-show` to fix these issues. 39 | * 40 | * @see https://github.com/electron/electron/issues/25012 41 | */ 42 | browserWindow.on('ready-to-show', () => { 43 | browserWindow?.setMenuBarVisibility(false); 44 | browserWindow?.show(); 45 | 46 | if (import.meta.env.DEV) { 47 | browserWindow?.webContents.openDevTools(); 48 | } 49 | }); 50 | 51 | /** 52 | * URL for main window. 53 | * Vite dev server for development. 54 | * `file://../renderer/index.html` for production and test 55 | */ 56 | const pageUrl = import.meta.env.DEV && import.meta.env.VITE_DEV_SERVER_URL !== undefined 57 | ? import.meta.env.VITE_DEV_SERVER_URL 58 | : new URL('../renderer/dist/index.html', 'file://' + __dirname).toString(); 59 | 60 | 61 | await browserWindow.loadURL(pageUrl); 62 | 63 | return browserWindow; 64 | } 65 | 66 | /** 67 | * Restore existing BrowserWindow or Create new BrowserWindow 68 | */ 69 | export async function restoreOrCreateWindow(bounds?: Electron.Rectangle) { 70 | let window = BrowserWindow.getAllWindows().find(w => !w.isDestroyed()); 71 | 72 | if (window === undefined) { 73 | window = await createWindow(bounds); 74 | } 75 | 76 | if (window.isMinimized()) { 77 | window.restore(); 78 | } 79 | 80 | window.focus(); 81 | return window; 82 | } 83 | 84 | /** hide window instead of closing it */ 85 | export async function hideWindow() { 86 | const window = BrowserWindow.getAllWindows().find(w => !w.isDestroyed()); 87 | if (window) { 88 | window.hide(); 89 | } 90 | } 91 | 92 | /** show window instead of reloading it */ 93 | export async function showWindow() { 94 | const window = BrowserWindow.getAllWindows().find(w => !w.isDestroyed()); 95 | if (window) { 96 | window.show(); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /packages/main/tests/unit.spec.ts: -------------------------------------------------------------------------------- 1 | import type {MaybeMocked} from 'vitest'; 2 | import {beforeEach, expect, test, vi} from 'vitest'; 3 | import {restoreOrCreateWindow} from '../src/mainWindow'; 4 | 5 | import {BrowserWindow} from 'electron'; 6 | 7 | /** 8 | * Mock real electron BrowserWindow API 9 | */ 10 | vi.mock('electron', () => { 11 | 12 | const bw = vi.fn() as MaybeMocked; 13 | // @ts-expect-error It's work in runtime, but I Haven't idea how to fix this type error 14 | bw.getAllWindows = vi.fn(() => bw.mock.instances); 15 | bw.prototype.loadURL = vi.fn(); 16 | bw.prototype.on = vi.fn(); 17 | bw.prototype.destroy = vi.fn(); 18 | bw.prototype.isDestroyed = vi.fn(); 19 | bw.prototype.isMinimized = vi.fn(); 20 | bw.prototype.focus = vi.fn(); 21 | bw.prototype.restore = vi.fn(); 22 | 23 | return {BrowserWindow: bw}; 24 | }); 25 | 26 | 27 | beforeEach(() => { 28 | vi.clearAllMocks(); 29 | }); 30 | 31 | 32 | test('Should create new window', async () => { 33 | const {mock} = vi.mocked(BrowserWindow); 34 | expect(mock.instances).toHaveLength(0); 35 | 36 | await restoreOrCreateWindow(); 37 | expect(mock.instances).toHaveLength(1); 38 | expect(mock.instances[0].loadURL).toHaveBeenCalledOnce(); 39 | expect(mock.instances[0].loadURL).toHaveBeenCalledWith(expect.stringMatching(/index\.html$/)); 40 | }); 41 | 42 | 43 | test('Should restore existing window', async () => { 44 | const {mock} = vi.mocked(BrowserWindow); 45 | 46 | // Create Window and minimize it 47 | await restoreOrCreateWindow(); 48 | expect(mock.instances).toHaveLength(1); 49 | const appWindow = vi.mocked(mock.instances[0]); 50 | appWindow.isMinimized.mockReturnValueOnce(true); 51 | 52 | await restoreOrCreateWindow(); 53 | expect(mock.instances).toHaveLength(1); 54 | expect(appWindow.restore).toHaveBeenCalledOnce(); 55 | }); 56 | 57 | 58 | test('Should create new window if previous was destroyed', async () => { 59 | const {mock} = vi.mocked(BrowserWindow); 60 | 61 | // Create Window and destroy it 62 | await restoreOrCreateWindow(); 63 | expect(mock.instances).toHaveLength(1); 64 | const appWindow = vi.mocked(mock.instances[0]); 65 | appWindow.isDestroyed.mockReturnValueOnce(true); 66 | 67 | await restoreOrCreateWindow(); 68 | expect(mock.instances).toHaveLength(2); 69 | }); 70 | -------------------------------------------------------------------------------- /packages/main/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext", 4 | "target": "esnext", 5 | "sourceMap": false, 6 | "moduleResolution": "Node", 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "isolatedModules": false, 10 | "allowSyntheticDefaultImports": true, 11 | "types" : ["node"], 12 | "resolveJsonModule": true, 13 | "baseUrl": ".", 14 | "paths": { 15 | "@renderer/*": [ 16 | "../renderer/src/*" 17 | ], 18 | "@api/*" : [ 19 | "../api/src/*" 20 | ], 21 | "@main/*" : [ 22 | "../main/src/*" 23 | ], 24 | "@preload/*" : [ 25 | "../preload/src/*" 26 | ], 27 | "@assets/*" : [ 28 | "../renderer/assets/*" 29 | ], 30 | "@buildResources/*" : [ 31 | "../../buildResources/*" 32 | ], 33 | "@i18n" : [ 34 | "../i18n/src" 35 | ], 36 | "@i18n/*" : [ 37 | "../i18n/src/*" 38 | ] 39 | }, 40 | }, 41 | "include": [ 42 | "src/**/*.ts", 43 | "../../types/**/*.d.ts" 44 | ], 45 | "exclude": [ 46 | "**/*.spec.ts", 47 | "**/*.test.ts" 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /packages/main/vite.config.js: -------------------------------------------------------------------------------- 1 | import { builtinModules } from 'module'; 2 | import { join } from 'path'; 3 | import { node } from '../../.electron-vendors.cache.json'; 4 | 5 | const PACKAGE_ROOT = __dirname; 6 | 7 | 8 | /** 9 | * @type {import('vite').UserConfig} 10 | * @see https://vitejs.dev/config/ 11 | */ 12 | const config = { 13 | mode: process.env.MODE, 14 | root: PACKAGE_ROOT, 15 | envDir: process.cwd(), 16 | resolve: { 17 | alias: { 18 | '@renderer': join(PACKAGE_ROOT, '..', 'renderer', 'src'), 19 | '@api': join(PACKAGE_ROOT, '..', 'api', 'src'), 20 | '@main': join(PACKAGE_ROOT, '..', 'main', 'src'), 21 | '@preload': join(PACKAGE_ROOT, '..', 'preload', 'src'), 22 | '@assets': join(PACKAGE_ROOT, '..', 'renderer', 'assets'), 23 | '@buildResources': join(PACKAGE_ROOT, '..', '..', 'buildResources'), 24 | '@i18n': join(PACKAGE_ROOT, '..', 'i18n', 'src'), 25 | }, 26 | }, 27 | build: { 28 | sourcemap: 'inline', 29 | target: `node${node}`, 30 | outDir: 'dist', 31 | assetsDir: '.', 32 | minify: process.env.MODE !== 'development', 33 | lib: { 34 | entry: './src/index.ts', 35 | name: 'main', 36 | formats: ['cjs'], 37 | fileName: (format) => `index.${format}.js`, 38 | }, 39 | rollupOptions: { 40 | external: [ 41 | 'electron', 42 | 'electron-devtools-installer', 43 | ...builtinModules.filter(m => !m.startsWith('_')), 44 | ], 45 | output: { 46 | entryFileNames: '[name].cjs', 47 | manualChunks(id) { 48 | if (id.includes('/node_modules/')) { 49 | const path = id.split('/node_modules/')[1]; 50 | const subpath = path.split('/')[0]; 51 | return `${subpath}/index`; 52 | } 53 | }, 54 | }, 55 | }, 56 | emptyOutDir: true, 57 | reportCompressedSize: false, 58 | }, 59 | }; 60 | 61 | export default config; 62 | -------------------------------------------------------------------------------- /packages/preload/exposedInMainWorld.d.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | /** 3 | * Expose Environment versions. 4 | * @example 5 | * console.log( window.versions ) 6 | */ 7 | readonly versions: NodeJS.ProcessVersions; 8 | /** 9 | * Expose the user data path. 10 | * intended to be used in a vuex store plugin 11 | */ 12 | readonly getPath: (path: import('./src/config').Paths) => Promise; 13 | readonly apiServer: { 14 | startServer: (payload: import('../api/src/app/types').startPayload) => Promise; 15 | stopServer: () => Promise; 16 | copyImageToClipboard: (string: string) => Promise; 17 | toggleFullScreen: () => void; 18 | onFullScreen(cb: (fullscreen: boolean) => void): Electron.IpcRenderer 19 | getEnv: string; 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /packages/preload/src/apiServer.ts: -------------------------------------------------------------------------------- 1 | import type { startPayload } from '@api/app/types/index'; 2 | import { ipcRenderer } from 'electron'; 3 | 4 | export async function startServer(payload:startPayload) { 5 | return ipcRenderer.invoke('start-server', payload); 6 | } 7 | 8 | export async function stopServer() { 9 | return ipcRenderer.invoke('stop-server'); 10 | } 11 | 12 | export function copyImageToClipboard(string:string) { 13 | return ipcRenderer.invoke('copy-image-to-clipboard', string); 14 | } 15 | 16 | export function toggleFullScreen() { 17 | return ipcRenderer.invoke('toggle-fullscreen'); 18 | } 19 | 20 | export function onFullScreen(cb: (fullscreen: boolean) => void) { 21 | return ipcRenderer.on('fullscreen', (event, fullscreen) => cb(fullscreen)); 22 | } 23 | -------------------------------------------------------------------------------- /packages/preload/src/config.ts: -------------------------------------------------------------------------------- 1 | import { ipcRenderer } from 'electron'; 2 | 3 | export type Paths = 4 | 'userData'| 'desktop' | 'documents' | 'downloads' | 'music' | 'pictures' | 'videos' 5 | 6 | export async function getPath(path: Paths): Promise { 7 | return ipcRenderer.invoke('get-path', path); 8 | } 9 | -------------------------------------------------------------------------------- /packages/preload/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module preload 3 | */ 4 | 5 | import { copyImageToClipboard, onFullScreen, startServer, stopServer, toggleFullScreen } from '@preload/apiServer'; 6 | import { getPath } from '@preload/config'; 7 | import { contextBridge } from 'electron'; 8 | 9 | 10 | /** 11 | * The "Main World" is the JavaScript context that your main renderer code runs in. 12 | * By default, the page you load in your renderer executes code in this world. 13 | * 14 | * @see https://www.electronjs.org/docs/api/context-bridge 15 | */ 16 | 17 | /** 18 | * After analyzing the `exposeInMainWorld` calls, 19 | * `packages/preload/exposedInMainWorld.d.ts` file will be generated. 20 | * It contains all interfaces. 21 | * `packages/preload/exposedInMainWorld.d.ts` file is required for TS is `renderer` 22 | * 23 | * @see https://github.com/cawa-93/dts-for-context-bridge 24 | */ 25 | 26 | /** 27 | * Expose Environment versions. 28 | * @example 29 | * console.log( window.versions ) 30 | */ 31 | contextBridge.exposeInMainWorld('versions', process.versions); 32 | 33 | /** 34 | * Expose the user data path. 35 | * intended to be used in a vuex store plugin 36 | */ 37 | contextBridge.exposeInMainWorld('getPath', getPath); 38 | contextBridge.exposeInMainWorld('apiServer', { startServer, stopServer, getEnv: import.meta.env.MODE, copyImageToClipboard, toggleFullScreen, onFullScreen }); 39 | -------------------------------------------------------------------------------- /packages/preload/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext", 4 | "target": "esnext", 5 | "sourceMap": false, 6 | "moduleResolution": "Node", 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "isolatedModules": true, 10 | 11 | "types" : ["node"], 12 | 13 | "baseUrl": ".", 14 | "paths": { 15 | "@renderer/*": [ 16 | "../renderer/src/*" 17 | ], 18 | "@api/*" : [ 19 | "../api/src/*" 20 | ], 21 | "@main/*" : [ 22 | "../main/src/*" 23 | ], 24 | "@preload/*" : [ 25 | "../preload/src/*" 26 | ], 27 | "@assets/*" : [ 28 | "../renderer/assets/*" 29 | ], 30 | "@buildResources/*" : [ 31 | "../../buildResources/*" 32 | ], 33 | "@i18n" : [ 34 | "../i18n/src" 35 | ], 36 | "@i18n/*" : [ 37 | "../i18n/src/*" 38 | ] 39 | } 40 | }, 41 | "include": [ 42 | "src/**/*.ts", 43 | "../../types/**/*.d.ts" 44 | ], 45 | "exclude": [ 46 | "**/*.spec.ts", 47 | "**/*.test.ts" 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /packages/preload/vite.config.js: -------------------------------------------------------------------------------- 1 | import { builtinModules } from 'module'; 2 | import { join } from 'path'; 3 | import { chrome } from '../../.electron-vendors.cache.json'; 4 | 5 | const PACKAGE_ROOT = __dirname; 6 | 7 | /** 8 | * @type {import('vite').UserConfig} 9 | * @see https://vitejs.dev/config/ 10 | */ 11 | const config = { 12 | mode: process.env.MODE, 13 | root: PACKAGE_ROOT, 14 | envDir: process.cwd(), 15 | resolve: { 16 | alias: { 17 | '@renderer': join(PACKAGE_ROOT, '..', 'renderer', 'src'), 18 | '@api': join(PACKAGE_ROOT, '..', 'api', 'src'), 19 | '@main': join(PACKAGE_ROOT, '..', 'main', 'src'), 20 | '@preload': join(PACKAGE_ROOT, '..', 'preload', 'src'), 21 | '@assets': join(PACKAGE_ROOT, '..', 'renderer', 'assets'), 22 | '@buildResources': join(PACKAGE_ROOT, '..', '..', 'buildResources'), 23 | '@i18n': join(PACKAGE_ROOT, '..', 'i18n', 'src'), 24 | }, 25 | }, 26 | build: { 27 | sourcemap: 'inline', 28 | target: `chrome${chrome}`, 29 | outDir: 'dist', 30 | assetsDir: '.', 31 | minify: process.env.MODE !== 'development', 32 | lib: { 33 | name: 'preload', 34 | entry: 'src/index.ts', 35 | formats: ['cjs'], 36 | }, 37 | rollupOptions: { 38 | external: [ 39 | 'electron', 40 | ...builtinModules.flatMap(p => [p, `node:${p}`]), 41 | ], 42 | output: { 43 | entryFileNames: '[name].cjs', 44 | }, 45 | }, 46 | emptyOutDir: true, 47 | reportCompressedSize: false, 48 | }, 49 | }; 50 | 51 | export default config; 52 | -------------------------------------------------------------------------------- /packages/renderer/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": false, 5 | "vue/setup-compiler-macros": true 6 | }, 7 | "rules": { 8 | "vue/no-v-html": "off" 9 | }, 10 | "extends": [ 11 | /** @see https://eslint.vuejs.org/rules/ */ 12 | "plugin:vue/vue3-recommended" 13 | ], 14 | "parserOptions": { 15 | "parser": "@typescript-eslint/parser", 16 | "ecmaVersion": 12, 17 | "sourceType": "module" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/renderer/assets/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiPaix/Fukayo/f9bb5ba83d7f10a35f849a10c5513dbf814bfb94/packages/renderer/assets/404.png -------------------------------------------------------------------------------- /packages/renderer/assets/404.svg: -------------------------------------------------------------------------------- 1 | 2 | 10 | 12 | 16 | 23 | 24 | 28 | 35 | 42 | 49 | 50 | 55 | 60 | 61 | -------------------------------------------------------------------------------- /packages/renderer/assets/404_portrait.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiPaix/Fukayo/f9bb5ba83d7f10a35f849a10c5513dbf814bfb94/packages/renderer/assets/404_portrait.png -------------------------------------------------------------------------------- /packages/renderer/assets/404_portrait.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | 18 | 28 | 31 | 32 | 36 | 43 | 44 | 48 | 55 | 62 | 69 | 70 | 83 | 96 | 97 | -------------------------------------------------------------------------------- /packages/renderer/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiPaix/Fukayo/f9bb5ba83d7f10a35f849a10c5513dbf814bfb94/packages/renderer/assets/favicon.ico -------------------------------------------------------------------------------- /packages/renderer/assets/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/renderer/assets/icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiPaix/Fukayo/f9bb5ba83d7f10a35f849a10c5513dbf814bfb94/packages/renderer/assets/icon_128.png -------------------------------------------------------------------------------- /packages/renderer/assets/icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiPaix/Fukayo/f9bb5ba83d7f10a35f849a10c5513dbf814bfb94/packages/renderer/assets/icon_16.png -------------------------------------------------------------------------------- /packages/renderer/assets/icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiPaix/Fukayo/f9bb5ba83d7f10a35f849a10c5513dbf814bfb94/packages/renderer/assets/icon_32.png -------------------------------------------------------------------------------- /packages/renderer/assets/icon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiPaix/Fukayo/f9bb5ba83d7f10a35f849a10c5513dbf814bfb94/packages/renderer/assets/icon_64.png -------------------------------------------------------------------------------- /packages/renderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Fukayo 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /packages/renderer/src/components/explore/CarouselSlide.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 65 | -------------------------------------------------------------------------------- /packages/renderer/src/components/explore/GroupCard.vue: -------------------------------------------------------------------------------- 1 | 84 | 137 | -------------------------------------------------------------------------------- /packages/renderer/src/components/explore/GroupMenu.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 135 | -------------------------------------------------------------------------------- /packages/renderer/src/components/helpers/login.ts: -------------------------------------------------------------------------------- 1 | /** Check login validity: not empty */ 2 | export const isLoginValid = (login:string|null) => { 3 | return login !== null && login.length > 0; 4 | }; 5 | 6 | /** 7 | * Check password validity: 8 | * not null, not empty, at least 6 characters 9 | * @param password password to check 10 | */ 11 | export const isPasswordValid = (password:string|null) => { 12 | return password !== null && password.length >= 6; 13 | }; 14 | 15 | export const passwordHint = (password:string|null) => { 16 | // 8 chars, at least a symbol, a number and with upper and lower case chars 17 | const regex = /^(?=.*\d)(?=.*[!@#$%^&*])(?=.*[a-z])(?=.*[A-Z]).{8,}$/; 18 | if(password === null || password.length < 6) return 'setup.password_hints.default'; 19 | if(regex.test(password)) return 'setup.password_hints.strong'; 20 | if(password.length >= 8) return 'setup.password_hints.average'; 21 | return 'setup.password_hints.weak'; 22 | }; 23 | 24 | /** 25 | * Check the port validity: 26 | * 27 | * number between 1024 and 65535 28 | * @param port port to check 29 | */ 30 | export const isPortValid = (port:number) => { 31 | return port >= 1024 && port <= 65535; 32 | }; 33 | 34 | /** 35 | * Check the url validity: 36 | * 37 | * url with protocol and hostname 38 | * @param hostname url to check 39 | */ 40 | export const isHostNameValid = (hostname:string|null) => { 41 | if(hostname === null) return false; 42 | if(!hostname.startsWith('https://')) return false; 43 | hostname = hostname.replace('https://', ''); 44 | if(hostname.length < 1) return false; 45 | if(hostname.includes(' ')) return false; 46 | return true; 47 | }; 48 | 49 | export const hostNameHint = (hostname:string|null) => { 50 | if(hostname === null || hostname.length < 1) return 'setup.address_errors.length'; 51 | if(!hostname.startsWith('https://')) return 'setup.address_errors.protocol'; 52 | if(hostname.replace('https://', '').length < 1) return 'setup.address_errors.length'; 53 | if(hostname.includes(' ')) return 'setup.address_errors.space'; 54 | return ''; 55 | }; 56 | 57 | /** 58 | * Check the SSL certificate validity 59 | * @param cert certificate to check 60 | */ 61 | export const isProvidedCertificateValid = (cert:string|null) => { 62 | if(!cert) return false; 63 | return cert.startsWith('-----BEGIN CERTIFICATE-----') 64 | && (cert.endsWith('-----END CERTIFICATE-----') || cert.endsWith('-----END CERTIFICATE-----\n') ); 65 | }; 66 | 67 | /** 68 | * Check the SSL key validity 69 | * @param key key to check 70 | */ 71 | export const isProvidedKeyValid = (key:string|null) => { 72 | if(!key) return false; 73 | return key.startsWith('-----BEGIN PRIVATE KEY-----') 74 | && (key.endsWith('-----END PRIVATE KEY-----') || key.endsWith('-----END PRIVATE KEY-----\n') ); 75 | }; 76 | 77 | export const certifColor = (cert:string|null) => { 78 | if(cert === null) return 'grey-8'; 79 | if(isProvidedCertificateValid(cert)) return 'positive'; 80 | return 'negative'; 81 | }; 82 | 83 | export const keyColor = (key:string|null) => { 84 | if(key === null) return 'grey-8'; 85 | if(isProvidedKeyValid(key)) return 'positive'; 86 | return 'negative'; 87 | }; 88 | -------------------------------------------------------------------------------- /packages/renderer/src/components/helpers/routePusher.ts: -------------------------------------------------------------------------------- 1 | import type { mirrorsLangsType } from '@i18n'; 2 | import type { RouteParamsRaw } from 'vue-router'; 3 | 4 | export type mangaRoute = { 5 | id: string, 6 | lang: mirrorsLangsType 7 | mirror: string, 8 | } 9 | 10 | export type readerRoute = { 11 | mirror: string, 12 | lang: mirrorsLangsType, 13 | id: string, 14 | chapterId: string, 15 | } 16 | 17 | export type searchRoute = { 18 | q: string, 19 | langs?: mirrorsLangsType[], 20 | } 21 | 22 | export type settingsRoute = { 23 | tab: 'general' | 'sources' | 'languages' | 'files' 24 | } 25 | 26 | export function routeTypeHelper(routeName: 'search', params: searchRoute): {name: 'search', params: searchRoute } 27 | export function routeTypeHelper(routeName: 'settings', params: settingsRoute): {name: 'reader', params: settingsRoute } 28 | export function routeTypeHelper(routeName: 'reader', params: readerRoute): {name: 'reader', params: readerRoute } 29 | export function routeTypeHelper(routeName: 'manga', params: mangaRoute): {name: 'manga', params: mangaRoute } 30 | export function routeTypeHelper(routeName: string, params: RouteParamsRaw) { 31 | return { name: routeName, params }; 32 | } 33 | -------------------------------------------------------------------------------- /packages/renderer/src/components/helpers/socket.ts: -------------------------------------------------------------------------------- 1 | import type { LoginAuth, SocketClientConstructor } from '@api/client/types'; 2 | import { socketManager } from '@renderer/socketClient'; 3 | 4 | export function useSocket(settings:SocketClientConstructor, auth?: LoginAuth) { 5 | return socketManager(settings).connect(auth); 6 | } 7 | -------------------------------------------------------------------------------- /packages/renderer/src/components/helpers/toggleFullScreen.ts: -------------------------------------------------------------------------------- 1 | import { AppFullscreen, Notify } from 'quasar'; 2 | import { ref } from 'vue'; 3 | 4 | const isElectron = typeof window.apiServer !== 'undefined' ? true : false; 5 | 6 | export const isFullScreen = ref(false); 7 | 8 | export const focusMode = ref(false); 9 | 10 | export async function toggleFullScreen():Promise { 11 | if(isElectron) return window.apiServer.toggleFullScreen(); 12 | else return AppFullscreen.toggle(); 13 | } 14 | 15 | export function notify(message: string) { 16 | Notify.create({ 17 | icon: 'info', 18 | color: 'orange-7', 19 | position: 'top', 20 | message, 21 | html: true, 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /packages/renderer/src/components/helpers/transformIMGurl.ts: -------------------------------------------------------------------------------- 1 | import type { useStore as useSettingsStore } from '@renderer/stores/settings'; 2 | 3 | /** make sure we get the right url */ 4 | export function transformIMGurl(url: string, settings:ReturnType) { 5 | const isElectron = typeof window.apiServer !== 'undefined' ? true : false; 6 | // return the url as is if it's external (http, https) 7 | if(url.startsWith('http') || url.startsWith('https')) return url; 8 | // remove leading slash if it's present 9 | if(url.startsWith('/')) url = url.substring(1); 10 | // in dev mode the protocol and port of the file server are different from the current page 11 | if(isElectron) return `${settings.server.ssl === 'false' ? 'http' : 'https'}://127.0.0.1:${settings.server.port}/${url}?token=${settings.server.accessToken}`; 12 | return `${settings.server.ssl === 'false' ? 'http' : 'https'}://${location.hostname}:${settings.server.port}/${url}?token=${settings.server.accessToken}`; 13 | } 14 | -------------------------------------------------------------------------------- /packages/renderer/src/components/helpers/typechecker.ts: -------------------------------------------------------------------------------- 1 | import type { CantImportResults } from '@api/models/imports/types'; 2 | import type { ChapterImage } from '@api/models/types/chapter'; 3 | import type { ChapterErrorMessage, ChapterImageErrorMessage, importErrorMessage, RecommendErrorMessage, SearchErrorMessage } from '@api/models/types/errors'; 4 | import type { SearchResult } from '@api/models/types/search'; 5 | import type { TaskDone } from '@api/models/types/shared'; 6 | export { isManga, isMangaInDB } from '@api/db/helpers'; 7 | 8 | export function isChapterImage(res: ChapterImage | ChapterImageErrorMessage | ChapterErrorMessage): res is ChapterImage { 9 | if(!res) return false; 10 | return (res as ChapterImage).index !== undefined && (res as ChapterImage).src !== undefined && (res as ChapterImage).lastpage !== undefined; 11 | } 12 | 13 | export function isChapterErrorMessage(res: ChapterImage | ChapterImageErrorMessage | ChapterErrorMessage): res is ChapterErrorMessage { 14 | if(!res) return false; 15 | return (res as ChapterImage).index === undefined && (res as ChapterImageErrorMessage).error !== undefined; 16 | } 17 | 18 | export function isChapterImageErrorMessage(res: ChapterImage | ChapterImageErrorMessage | ChapterErrorMessage): res is ChapterImageErrorMessage { 19 | if(!res) return false; 20 | return (res as ChapterImage).src === undefined && (res as ChapterImageErrorMessage).error !== undefined && (res as ChapterImageErrorMessage).index !== undefined; 21 | } 22 | 23 | export function isSearchResult(res: SearchResult | SearchResult[] | SearchErrorMessage | RecommendErrorMessage | TaskDone): res is SearchResult { 24 | return (res as SearchResult).url !== undefined; 25 | } 26 | 27 | export function isTaskDone(res: SearchResult | SearchResult[] | SearchErrorMessage | RecommendErrorMessage | TaskDone | unknown): res is TaskDone { 28 | return (res as TaskDone).done !== undefined; 29 | } 30 | 31 | export function isImportErrorMessage(res:unknown) : res is importErrorMessage { 32 | return (res as importErrorMessage).error !== undefined; 33 | } 34 | 35 | export function isCantImport(res: unknown) : res is CantImportResults { 36 | return (res as CantImportResults).mirror === undefined; 37 | } 38 | -------------------------------------------------------------------------------- /packages/renderer/src/components/library/@types/index.ts: -------------------------------------------------------------------------------- 1 | import type { MangaInDB } from '@api/models/types/manga'; 2 | import type { mirrorsLangsType } from '@i18n'; 3 | 4 | export type MangaInDBwithLabel = { 5 | id: string, 6 | mirror: string, 7 | langs:mirrorsLangsType[], 8 | name: string, 9 | displayName?: string, 10 | tags: string[], 11 | authors: string[], 12 | url: string, 13 | synopsis?:string, 14 | userCategories: string[], 15 | unread: number, 16 | status: MangaInDB['status'] 17 | /** is the mirror dead? */ 18 | dead:boolean, 19 | /** is the entry broken? */ 20 | broken: boolean, 21 | meta: MangaInDB['meta'], 22 | chapters: (MangaInDB['chapters'][0] & { 23 | label: string | number; 24 | value: number; 25 | })[] 26 | }; 27 | 28 | export type MangaGroup = { 29 | name: string; 30 | mangas: MangaInDBwithLabel[]; 31 | covers: string[]; 32 | unread:number; 33 | }; 34 | -------------------------------------------------------------------------------- /packages/renderer/src/components/library/MirrorChips.vue: -------------------------------------------------------------------------------- 1 | 61 | 97 | -------------------------------------------------------------------------------- /packages/renderer/src/components/library/migrate/MigrateButton.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 45 | -------------------------------------------------------------------------------- /packages/renderer/src/components/reader/NavOverlay.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 93 | 107 | 108 | -------------------------------------------------------------------------------- /packages/renderer/src/components/reader/helpers/index.ts: -------------------------------------------------------------------------------- 1 | import type { MangaPage } from '@api/models/types/manga'; 2 | import type { appLangsType } from '@i18n'; 3 | import type en from '@i18n/../locales/en.json'; 4 | import type { useI18n } from 'vue-i18n'; 5 | 6 | export function chapterLabel(number:number, name?:string) { 7 | if(name) return `${number} - ${name}`; 8 | return number; 9 | } 10 | 11 | export function isMouseEvent(event:KeyboardEvent|MouseEvent):event is MouseEvent { 12 | return typeof (event as KeyboardEvent).key === 'undefined'; 13 | } 14 | 15 | 16 | export function formatChapterInfoToString(isKomgaTryingItsBest:boolean, $t: Translate['t'], chap?:MangaPage['chapters'][number]|null) { 17 | if(!chap) return ''; 18 | let str = ''; 19 | if(chap.volume) str += `${$t('mangas.volume')} ${chap.volume}`; 20 | if((!isKomgaTryingItsBest && chap.volume !== undefined ) || (isKomgaTryingItsBest && chap.number > -1 && typeof chap.volume !== 'undefined')) { 21 | str += ' - '; 22 | } 23 | if(!isKomgaTryingItsBest || (isKomgaTryingItsBest && chap.number > -1 && chap.volume === undefined)) { 24 | str += `${$t('mangas.chapter')} ${chap.number}`; 25 | } 26 | return str; 27 | } 28 | 29 | type Translate = ReturnType>; 30 | -------------------------------------------------------------------------------- /packages/renderer/src/components/settings/App.vue: -------------------------------------------------------------------------------- 1 | 42 | 90 | -------------------------------------------------------------------------------- /packages/renderer/src/components/settings/fileSystem.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 88 | -------------------------------------------------------------------------------- /packages/renderer/src/components/settings/helpers/checkReaderSettings.ts: -------------------------------------------------------------------------------- 1 | 2 | import { useStore as useStoreSettings } from '@renderer/stores/settings'; 3 | 4 | const settings = useStoreSettings(); 5 | 6 | /** make sure options are compatible with each others */ 7 | export async function checkSettingsCompatibilty(key: keyof typeof settings.reader) { 8 | 9 | if(key === 'book' && settings.reader.book) { 10 | if(settings.reader.zoomMode === 'stretch-height' && settings.reader.longStripDirection === 'vertical') settings.reader.zoomMode = 'auto'; 11 | if(settings.reader.webtoon) settings.reader.webtoon = false; 12 | if((!settings.reader.longStrip && settings.reader.book) || (settings.reader.longStrip && settings.reader.longStripDirection === 'vertical')) { 13 | if(settings.reader.zoomMode === 'stretch-height') settings.reader.zoomMode = 'auto'; 14 | } 15 | } 16 | 17 | if(key === 'longStrip' && !settings.reader.longStrip) { 18 | settings.reader.webtoon = false; 19 | } 20 | 21 | if(key === 'webtoon' && settings.reader.webtoon) { 22 | if(settings.reader.zoomMode === 'stretch-height') settings.reader.zoomMode = 'auto'; 23 | } 24 | 25 | if(key === 'longStripDirection' && settings.reader.longStripDirection === 'vertical') { 26 | if(settings.reader.book && settings.reader.zoomMode === 'stretch-height') settings.reader.zoomMode = 'auto'; 27 | } 28 | 29 | if(key === 'longStripDirection' && settings.reader.longStripDirection === 'horizontal') { 30 | if(settings.reader.zoomMode === 'stretch-width') settings.reader.zoomMode = 'auto'; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/renderer/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | readonly VITE_APP_VERSION: string 5 | // more env variables... 6 | } 7 | 8 | interface ImportMeta { 9 | readonly env: ImportMetaEnv 10 | } 11 | -------------------------------------------------------------------------------- /packages/renderer/src/index.ts: -------------------------------------------------------------------------------- 1 | import Root from '@renderer/App.vue'; 2 | import { createApp } from 'vue'; 3 | import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'; 4 | 5 | // LocalStorage 6 | import { piniaLocalStorage } from '@renderer/stores/localStorage'; 7 | import { createPinia } from 'pinia'; 8 | const pinia = createPinia(); 9 | pinia.use(piniaLocalStorage); 10 | 11 | // Router 12 | const router = createRouter({ 13 | history: typeof window.apiServer === 'undefined' ? createWebHistory() : createWebHashHistory(), 14 | routes: [ 15 | { 16 | name: 'home', 17 | path: '/', 18 | component: () => import('@renderer/components/library/App.vue'), 19 | }, 20 | { 21 | name: 'search', 22 | path: '/search', 23 | component: () => import('@renderer/components/search/App.vue'), 24 | props: route => ({ query: route.query.q, langs: route.query.langs }), 25 | }, 26 | { 27 | name: 'manga', 28 | path: '/manga/:mirror/:id/:lang', 29 | component: () => import('@renderer/components/manga/App.vue'), 30 | props: true, 31 | }, 32 | { 33 | name: 'reader', 34 | path: '/manga/:mirror/:id/:lang/read/:chapterId', 35 | component: () => import('@renderer/components/reader/App.vue'), 36 | props: true, 37 | }, 38 | { 39 | name: 'explore', 40 | path: '/explore', 41 | component: () => import('@renderer/components/explore/App.vue'), 42 | }, 43 | { 44 | name: 'explore_mirror', 45 | path: '/explore/:mirror', 46 | component: () => import('@renderer/components/explore/SourceExplore.vue'), 47 | }, 48 | { 49 | name: 'settings', 50 | path: '/settings/:tab?', 51 | component: () => import('@renderer/components/settings/App.vue'), 52 | }, 53 | { 54 | name: 'import', 55 | path: '/import', 56 | component: () => import('@renderer/components/import/App.vue'), 57 | }, 58 | { 59 | name: 'logs', 60 | path: '/logs', 61 | component: () => import('@renderer/components/logs/App.vue'), 62 | }, 63 | ], 64 | }); 65 | 66 | // Quasar 67 | import { Dialog, Loading, Notify, Quasar, AppFullscreen } from 'quasar'; 68 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 69 | /** @ts-ignore */ 70 | import('@fontsource/roboto'); 71 | import('@quasar/extras/material-icons/material-icons.css'); 72 | import('@quasar/extras/material-icons-outlined/material-icons-outlined.css'); 73 | import('@quasar/extras/material-icons-round/material-icons-round.css'); 74 | import('quasar/dist/quasar.css'); 75 | 76 | const QuasarConfig = { 77 | plugins: { Dialog, Notify, Loading, AppFullscreen }, 78 | config: { 79 | brand: { 80 | primary: '#3d75ad', 81 | secondary: '#4da89f', 82 | accent: '#9C27B0', 83 | dark: '#1d1d1d', 84 | positive: '#3b9c52', 85 | negative: '#b53645', 86 | info: '#61c1d4', 87 | warning: '#dbb54d', 88 | }, 89 | }, 90 | }; 91 | 92 | // localization 93 | import { findAppLocale } from '@i18n'; 94 | import { setupI18n } from '@renderer/locales'; 95 | import dayjs from 'dayjs'; 96 | 97 | const lang = findAppLocale(navigator.language); 98 | 99 | // init 100 | const App = createApp(Root); 101 | App.provide('dayJS', dayjs); 102 | App.use(setupI18n({ locale: lang })); 103 | App.use(Quasar, QuasarConfig); 104 | App.use(pinia); 105 | App.use(router); 106 | App.mount('#app'); 107 | -------------------------------------------------------------------------------- /packages/renderer/src/locales/index.ts: -------------------------------------------------------------------------------- 1 | import type en from '@i18n/../locales/en.json'; 2 | import type { appLangsType } from '@i18n'; 3 | import { findAppLocale, importLocale, loadLocale } from '@i18n'; 4 | import dayjs from 'dayjs'; 5 | import dayjslocalizedformat from 'dayjs/plugin/localizedFormat'; 6 | import dayjsrelative from 'dayjs/plugin/relativeTime'; 7 | import { Quasar } from 'quasar'; 8 | import type { I18n, I18nOptions } from 'vue-i18n'; 9 | import { createI18n } from 'vue-i18n'; 10 | 11 | type MessageSchema = typeof en 12 | 13 | export function setupI18n(options:I18nOptions<{ message: MessageSchema }, appLangsType>) { 14 | if(!options.globalInjection) options.globalInjection = true; 15 | const lang = findAppLocale(navigator.language); 16 | const i18n = createI18n<[MessageSchema], appLangsType>({ 17 | locale: lang, 18 | }); 19 | 20 | setI18nLanguage(i18n, lang); 21 | return i18n; 22 | } 23 | 24 | export async function setI18nLanguage(i18n:I18n, locale:appLangsType) { 25 | 26 | const messages = await loadLocale(locale); 27 | i18n.global.locale = locale; 28 | i18n.global.setLocaleMessage(locale, messages); 29 | i18n.global.warnHtmlInMessage = 'off'; 30 | const imported = await importLocale(locale); 31 | Quasar.lang.set(imported.quasar); 32 | dayjs.locale(imported.dayjs); 33 | dayjs.extend(dayjsrelative); 34 | dayjs.extend(dayjslocalizedformat); 35 | 36 | } 37 | -------------------------------------------------------------------------------- /packages/renderer/src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import type { DefineComponent } from 'vue'; 3 | // eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/no-explicit-any 4 | const component: DefineComponent<{}, {}, any>; 5 | export default component; 6 | } 7 | -------------------------------------------------------------------------------- /packages/renderer/src/socketClient.ts: -------------------------------------------------------------------------------- 1 | import socket from '@api/client'; 2 | import type { SocketClientConstructor } from '@api/client/types'; 3 | 4 | let sock:socket|null = null; 5 | 6 | export function socketManager(settings: SocketClientConstructor) { 7 | if(sock) return sock; 8 | sock = new socket(settings); 9 | return sock; 10 | } 11 | -------------------------------------------------------------------------------- /packages/renderer/src/stores/history/index.ts: -------------------------------------------------------------------------------- 1 | import type { MangaInDB, MangaPage } from '@api/models/types/manga'; 2 | import { defineStore } from 'pinia'; 3 | 4 | export const useHistoryStore = defineStore('history', { 5 | state: () => { 6 | return { 7 | manga: null as null | MangaInDB | MangaPage, 8 | }; 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /packages/renderer/src/stores/localStorage.ts: -------------------------------------------------------------------------------- 1 | import type { PiniaCustomStateProperties, PiniaPluginContext, StateTree } from 'pinia'; 2 | 3 | function iterate(store: StateTree & PiniaCustomStateProperties, local: StateTree & PiniaCustomStateProperties) { 4 | Object.keys(store).forEach(key => { 5 | if (key === '__proto__' || key === 'constructor') return; 6 | if (typeof store[key] === 'object' && store[key] !== null && !Array.isArray(store[key])) { 7 | if(!local[key]) local[key] = {}; 8 | return iterate(store[key], local[key]); 9 | } 10 | if(!local[key]) { 11 | local[key] = store[key]; 12 | } 13 | }); 14 | return local; 15 | } 16 | 17 | export function piniaLocalStorage(context: PiniaPluginContext) { 18 | if(typeof localStorage[context.store.$id] === 'undefined') { 19 | localStorage.setItem(context.store.$id, JSON.stringify(context.store.$state)); 20 | } else { 21 | const local = JSON.parse(localStorage.getItem(context.store.$id) || '{}'); 22 | const store = Object.assign(context.store.$state, {}); 23 | 24 | const merge = iterate(store, local); 25 | context.store.$patch(merge); 26 | } 27 | context.store.$subscribe((mutation) => { 28 | localStorage.setItem(mutation.storeId, JSON.stringify(context.store.$state)); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /packages/renderer/src/stores/settings/index.ts: -------------------------------------------------------------------------------- 1 | import type { Settings } from '@renderer/stores/settings/types'; 2 | import { defineStore } from 'pinia'; 3 | 4 | const defaultSettings:Settings = { 5 | theme: 'dark', 6 | server: { 7 | login: 'admin', 8 | password: null, 9 | port : 4444, 10 | ssl: 'false', 11 | hostname: 'https://127.0.0.1', 12 | cert: null, 13 | key: null, 14 | accessToken: null, 15 | refreshToken: null, 16 | autostart: false, 17 | }, 18 | mangaPage: { 19 | chapters: { 20 | sort: 'ASC', 21 | hideRead: false, 22 | KomgaTryYourBest: [], 23 | scanlators : { 24 | ignore: [], 25 | }, 26 | }, 27 | }, 28 | reader : { 29 | webtoon: false, 30 | showPageNumber: true, 31 | zoomMode: 'auto', 32 | longStrip: true, 33 | longStripDirection: 'vertical', 34 | book: false, 35 | bookOffset: false, 36 | overlay: true, 37 | rtl: false, 38 | }, 39 | readerGlobal: { 40 | preloadNext: true, 41 | pinRightDrawer: true, 42 | }, 43 | library: { 44 | showUnread: true, 45 | sort: 'AZ', 46 | firstTimer: 1, 47 | }, 48 | }; 49 | 50 | export const useStore = defineStore('settings', { 51 | state: () => defaultSettings, 52 | }); 53 | -------------------------------------------------------------------------------- /packages/renderer/src/stores/settings/types.ts: -------------------------------------------------------------------------------- 1 | import type { MangaInDB } from '@api/models/types/manga'; 2 | 3 | type ServerSettings = { 4 | /** user's login */ 5 | login: string, 6 | /** user's password */ 7 | password: string|null, 8 | /** application port */ 9 | port: number, 10 | /** 11 | * - 'false' = no ssl 12 | * - 'provided' = user's provided cert and keys 13 | * - 'app' = generated cert 14 | */ 15 | ssl: 'false'|'provided'|'app' 16 | /** 17 | * hostname (required for https) 18 | */ 19 | hostname: string, 20 | /** ssl cert */ 21 | cert: string|null 22 | /** ssl key */ 23 | key: string|null 24 | /** API access token */ 25 | accessToken: string|null 26 | /** API refresh token */ 27 | refreshToken: string|null 28 | /** Automatically start server if credentials are filled */ 29 | autostart: boolean 30 | } 31 | 32 | type MangaPageSettings = { 33 | /** chapters settings */ 34 | chapters : { 35 | /** sort asc-desc */ 36 | sort: 'ASC' | 'DESC', 37 | /** hide read chapters */ 38 | hideRead: boolean 39 | /** enable komga experimental parsing */ 40 | KomgaTryYourBest: string[], 41 | /** list of ignored scanlators */ 42 | scanlators : { 43 | ignore: { 44 | /** manga id */ 45 | id: string, 46 | /** scanlator's name */ 47 | name: string 48 | }[] 49 | } 50 | } 51 | } 52 | 53 | type ReaderSettings = MangaInDB['meta']['options'] 54 | 55 | type LibrarySettings = { 56 | /** Only display entries with unread chapters */ 57 | showUnread: boolean, 58 | /** 59 | * Library page's entries sorting 60 | * - AZ 61 | * - ZA 62 | * - unread = most unread first 63 | * - read = most read first 64 | */ 65 | sort: 'AZ' | 'ZA' | 'unread' | 'read', 66 | /** 67 | * Displays the step by step guide 68 | * - 1 = step 1 69 | * - 2 = step 2 70 | * - 3 = complete 71 | */ 72 | firstTimer: 1 | 2 | 3 73 | } 74 | 75 | export type Settings = { 76 | /** color scheme */ 77 | theme: 'dark' | 'light' 78 | /** server settings */ 79 | server: ServerSettings 80 | /** Manga Page settings */ 81 | mangaPage: MangaPageSettings 82 | /** Reader's settings */ 83 | reader: ReaderSettings, 84 | /** Reader's global settings */ 85 | readerGlobal: { 86 | preloadNext: boolean, 87 | pinRightDrawer: boolean, 88 | } 89 | /** Library settings */ 90 | library: LibrarySettings, 91 | } 92 | -------------------------------------------------------------------------------- /packages/renderer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext", 4 | "target": "esnext", 5 | "sourceMap": false, 6 | "moduleResolution": "Node", 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "isolatedModules": true, 10 | "resolveJsonModule": true, 11 | "allowSyntheticDefaultImports": true, 12 | "types" : ["node"], 13 | "jsx": "preserve", 14 | "baseUrl": ".", 15 | "paths": { 16 | "@renderer/*": [ 17 | "../renderer/src/*" 18 | ], 19 | "@api/*" : [ 20 | "../api/src/*" 21 | ], 22 | "@main/*" : [ 23 | "../main/src/*" 24 | ], 25 | "@preload/*" : [ 26 | "../preload/src/*" 27 | ], 28 | "@assets/*" : [ 29 | "../renderer/assets/*" 30 | ], 31 | "@buildResources/*" : [ 32 | "../../buildResources/*" 33 | ], 34 | "@i18n" : [ 35 | "../i18n/src" 36 | ], 37 | "@i18n/*" : [ 38 | "../i18n/src/*" 39 | ] 40 | }, 41 | "lib": ["ESNext", "dom", "dom.iterable"] 42 | }, 43 | 44 | "include": [ 45 | "src/**/*.vue", 46 | "src/**/*.ts", 47 | "src/**/*.tsx", 48 | "types/**/*.d.ts", 49 | "../../types/**/*.d.ts", 50 | "../preload/exposedInMainWorld.d.ts" 51 | , "src/shims-vue.d.ts" ], 52 | "exclude": [ 53 | "**/*.spec.ts", 54 | "**/*.test.ts", 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /packages/renderer/vite.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | import vueI18n from '@intlify/unplugin-vue-i18n/vite'; 4 | import { quasar, transformAssetUrls } from '@quasar/vite-plugin'; 5 | import vue from '@vitejs/plugin-vue'; 6 | import { builtinModules } from 'module'; 7 | import { join, resolve } from 'path'; 8 | import { chrome } from '../../.electron-vendors.cache.json'; 9 | import packageJson from '../../package.json'; 10 | 11 | process.env.VITE_APP_VERSION = packageJson.version; 12 | const PACKAGE_ROOT = __dirname; 13 | 14 | /** 15 | * @type {import('vite').UserConfig} 16 | * @see https://vitejs.dev/config/ 17 | */ 18 | const config = { 19 | mode: process.env.MODE, 20 | root: PACKAGE_ROOT, 21 | base: '', 22 | resolve: { 23 | alias: { 24 | '@renderer': join(PACKAGE_ROOT, '..', 'renderer', 'src'), 25 | '@api': join(PACKAGE_ROOT, '..', 'api', 'src'), 26 | '@main': join(PACKAGE_ROOT, '..', 'main', 'src'), 27 | '@preload': join(PACKAGE_ROOT, '..', 'preload', 'src'), 28 | '@assets': join(PACKAGE_ROOT, '..', 'renderer', 'assets'), 29 | '@buildResources': join(PACKAGE_ROOT, '..', '..', 'buildResources'), 30 | '@i18n': join(PACKAGE_ROOT, '..', 'i18n', 'src'), 31 | }, 32 | }, 33 | plugins: [ 34 | vue({template: transformAssetUrls}), 35 | vueI18n({ 36 | include: join(PACKAGE_ROOT, '..', 'i18n', 'locales') + '/**', 37 | strictMessage: false, 38 | }), 39 | quasar(), 40 | ], 41 | server: { 42 | fs: { 43 | strict: true, 44 | }, 45 | }, 46 | build: { 47 | minify: 'terser', 48 | sourcemap: true, 49 | target: `chrome${chrome}`, 50 | outDir: 'dist', 51 | assetsDir: 'assets', 52 | rollupOptions: { 53 | input: resolve('index.html'), 54 | external: [ 55 | ...builtinModules.flatMap(p => [p, `node:${p}`]), 56 | 'filenamify', 57 | ], 58 | output: { 59 | manualChunks(id) { 60 | if (id.includes('/node_modules/')) { 61 | const modules = ['quasar', '@quasar', 'vue', '@vue', 'dayjs', '@intlify', '@vueuse', 'vue-i18n']; 62 | const chunk = modules.find((module) => id.includes(`/node_modules/${module}`)); 63 | if(chunk === 'quasar') { 64 | if(id.includes('/quasar/lang/')) return `vendor-${chunk}.locales`; 65 | } 66 | 67 | if(chunk === '@vue') { 68 | if(id.includes('/devtools-api/lib/')) return `vendor-${chunk}.devtools`; 69 | if(id.includes('/@vue/runtime-')) return `vendor-${chunk}.runtimes`; 70 | } 71 | return chunk ? `vendor-${chunk}` : 'vendor'; 72 | } 73 | }, 74 | }, 75 | }, 76 | emptyOutDir: true, 77 | reportCompressedSize: false, 78 | }, 79 | test: { 80 | environment: 'happy-dom', 81 | }, 82 | }; 83 | 84 | export default config; 85 | -------------------------------------------------------------------------------- /scripts/release.js: -------------------------------------------------------------------------------- 1 | 2 | const {readFileSync, writeFileSync} = require('fs'); 3 | const util = require('util'); 4 | const exec = util.promisify(require('child_process').exec); 5 | 6 | 7 | /** @type {import('simple-git').default} */ 8 | const simpleGit = require('simple-git'); 9 | 10 | 11 | 12 | 13 | const args = process.argv.slice(2); 14 | 15 | (async () => { 16 | 17 | try { 18 | await exec('git --version'); 19 | } catch(e) { 20 | console.log('git binary not found'); 21 | process.exit(1); 22 | } 23 | 24 | try { 25 | await exec('npm --version'); 26 | } catch(e) { 27 | console.log('npm binary not found'); 28 | process.exit(1); 29 | } 30 | 31 | try { 32 | const git = simpleGit(); 33 | const branch = await git.branch(); 34 | if(branch.current !== 'beta') throw new Error('Not on beta branch'); 35 | if (args.length === 0) throw new Error('script must be run with one of these arguments --major, --minor or --patch'); 36 | const {latest} = await git.tags({'v*-beta': null}); 37 | if(!latest) throw new Error('Couldn\'t find latest tag'); 38 | const res = await git.log(); 39 | const latestTagCommit = res.all.find(r => r.refs.includes(`tag: ${latest}`)); 40 | if(!latestTagCommit) throw new Error('Couldn\'t find latest tag in git history'); 41 | const {total} = await git.log({from: latestTagCommit.hash, to: 'HEAD'}); 42 | if(!total) throw new Error('Couldn\'t calculate number of commits since latest tag'); 43 | 44 | const semantic = latest.replace('v', '').replace('-beta', '').split('.').map(v => parseInt(v)); 45 | if(args[0] === '--major') { 46 | semantic[0]++; 47 | semantic[1] = 0; 48 | semantic[2] = 0; 49 | } 50 | else if(args[0] === '--minor') { 51 | semantic[1]++; 52 | semantic[2] = 0; 53 | } 54 | else if(args[0] === '--patch') semantic[2] = semantic[2] + total + 1; 55 | else return console.log('invalid arg'); 56 | 57 | const oldVersion = latest.replace('-beta', '').replace('v', ''); 58 | const newVersion = `${semantic.join('.')}`; 59 | const newPackage = readFileSync('./package.json').toString().replace(`"version": "${oldVersion}",`, `"version": "${newVersion}",`); 60 | if(args[1] === '--dry') return console.log(`Would update package.json from ${oldVersion} to ${newVersion}`); 61 | writeFileSync('./package.json', newPackage); 62 | const {stderr} = await exec('npm install'); 63 | if(stderr) return console.log(stderr); 64 | await git.add(['./package.json', './package-lock.json']); 65 | await git.commit(`release: beta ${newVersion}`, ['./package.json', './package-lock.json']); 66 | await git.push(); 67 | } catch(e) { 68 | console.error(e); 69 | } 70 | })(); 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /scripts/update-electron-vendors.js: -------------------------------------------------------------------------------- 1 | const {writeFile} = require('fs/promises'); 2 | const {execSync} = require('child_process'); 3 | const electron = require('electron'); 4 | const path = require('path'); 5 | 6 | /** 7 | * Returns versions of electron vendors 8 | * The performance of this feature is very poor and can be improved 9 | * @see https://github.com/electron/electron/issues/28006 10 | * 11 | * @returns {NodeJS.ProcessVersions} 12 | */ 13 | function getVendors() { 14 | const output = execSync(`${electron} -p "JSON.stringify(process.versions)"`, { 15 | env: {'ELECTRON_RUN_AS_NODE': '1'}, 16 | encoding: 'utf-8', 17 | }); 18 | 19 | return JSON.parse(output); 20 | } 21 | 22 | function updateVendors() { 23 | const electronRelease = getVendors(); 24 | 25 | const nodeMajorVersion = electronRelease.node.split('.')[0]; 26 | const chromeMajorVersion = electronRelease.v8.split('.')[0] + electronRelease.v8.split('.')[1]; 27 | 28 | const browserslistrcPath = path.resolve(process.cwd(), '.browserslistrc'); 29 | 30 | return Promise.all([ 31 | writeFile('./.electron-vendors.cache.json', 32 | JSON.stringify({ 33 | chrome: chromeMajorVersion, 34 | node: nodeMajorVersion, 35 | }, null, 2) + '\n', 36 | ), 37 | 38 | writeFile(browserslistrcPath, `Chrome ${chromeMajorVersion}\n`, 'utf8'), 39 | ]); 40 | } 41 | 42 | updateVendors().catch(err => { 43 | console.error(err); 44 | process.exit(1); 45 | }); 46 | -------------------------------------------------------------------------------- /tests/e2e.spec.ts: -------------------------------------------------------------------------------- 1 | import { randomBytes } from 'crypto'; 2 | import type { ElectronApplication } from 'playwright'; 3 | import { _electron as electron } from 'playwright'; 4 | import { afterAll, beforeAll, expect, test } from 'vitest'; 5 | 6 | let electronApp: ElectronApplication; 7 | 8 | beforeAll(async () => { 9 | electronApp = await electron.launch({args: ['.']}); 10 | await electronApp.waitForEvent('window'); 11 | }); 12 | 13 | afterAll(async () => { 14 | await electronApp.close(); 15 | }); 16 | 17 | test('Main window state', async () => { 18 | const windowState: { isVisible: boolean; isDevToolsOpened: boolean; isCrashed: boolean } 19 | = await electronApp.evaluate(({BrowserWindow}) => { 20 | const mainWindow = BrowserWindow.getAllWindows()[0]; 21 | 22 | const getState = () => ({ 23 | isVisible: mainWindow.isVisible(), 24 | isDevToolsOpened: mainWindow.webContents.isDevToolsOpened(), 25 | isCrashed: mainWindow.webContents.isCrashed(), 26 | }); 27 | 28 | return new Promise((resolve) => { 29 | if (mainWindow.isVisible()) { 30 | resolve(getState()); 31 | } else 32 | mainWindow.once('ready-to-show', () => setTimeout(() => resolve(getState()), 0)); 33 | }); 34 | }); 35 | 36 | expect(windowState.isCrashed, 'App was crashed').toBeFalsy(); 37 | expect(windowState.isVisible, 'Main window was not visible').toBeTruthy(); 38 | expect(windowState.isDevToolsOpened, 'DevTools was opened').toBeFalsy(); 39 | }); 40 | 41 | test('Main window web content', async () => { 42 | const page = await electronApp.firstWindow(); 43 | // on startup the app tries to directly connect the user to the server 44 | await page.waitForSelector('#app'); 45 | const root = await page.$('#app', {strict: true}); 46 | expect(root).to.not.be.null; 47 | if(!root) return; 48 | expect((await root.innerHTML()).trim(), 'Window content was empty').not.equal(''); 49 | }); 50 | 51 | 52 | test('Preload versions', async () => { 53 | const page = await electronApp.firstWindow(); 54 | expect(page.evaluate).to.be.a.toBeDefined(); 55 | const exposedVersions = await page.evaluate(() => globalThis.versions); 56 | const expectedVersions = await electronApp.evaluate(() => process.versions); 57 | expect(exposedVersions).toBeDefined(); 58 | expect(exposedVersions).to.deep.equal(expectedVersions); 59 | }); 60 | 61 | test('Preload apiServer', async () => { 62 | const page = await electronApp.firstWindow(); 63 | 64 | const exposed = await page.evaluate(() => globalThis.apiServer); 65 | 66 | expect(exposed).to.haveOwnProperty('startServer'); 67 | expect(exposed).to.haveOwnProperty('stopServer'); 68 | expect(exposed).to.haveOwnProperty('getEnv'); 69 | 70 | const startType = await page.evaluate(() => typeof globalThis.apiServer.startServer); 71 | const stopType = await page.evaluate(() => typeof globalThis.apiServer.stopServer); 72 | const getEnvType = await page.evaluate(() => typeof globalThis.apiServer.getEnv); 73 | 74 | expect(startType).toEqual('function'); 75 | expect(stopType).toEqual('function'); 76 | expect(getEnvType).toEqual('string'); 77 | 78 | const env = await page.evaluate(() => globalThis.apiServer.getEnv); 79 | expect(env).toEqual('production'); 80 | }); 81 | 82 | test('Server setup', async () => { 83 | const page = await electronApp.firstWindow(); 84 | const randomLogin = randomBytes(6).toString('hex'); 85 | const randomPassword = randomBytes(10).toString('hex'); 86 | 87 | await page.locator('input[name="login"]' ).fill(randomLogin); 88 | await page.locator('input[name="password"]').fill(randomPassword); 89 | await page.locator('input[name="port"]').fill('3000'); 90 | await page.locator('#no-ssl').click(); 91 | await page.locator('button[type=submit]').click(); 92 | 93 | }); 94 | 95 | test('Server is running', async () => { 96 | const page = await electronApp.firstWindow(); 97 | await page.waitForSelector('main'); 98 | const library = await page.$('main'); 99 | expect(library).to.not.be.null; 100 | }); 101 | -------------------------------------------------------------------------------- /types/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /** 4 | * Describes all existing environment variables and their types. 5 | * Required for Code completion and type checking 6 | * 7 | * Note: To prevent accidentally leaking env variables to the client, only variables prefixed with `VITE_` are exposed to your Vite-processed code 8 | * 9 | * @see https://github.com/vitejs/vite/blob/cab55b32de62e0de7d7789e8c2a1f04a8eae3a3f/packages/vite/types/importMeta.d.ts#L62-L69 Base Interface 10 | * @see https://vitejs.dev/guide/env-and-mode.html#env-files Vite Env Variables Doc 11 | */ 12 | interface ImportMetaEnv { 13 | 14 | /** 15 | * The value of the variable is set in scripts/watch.js and depend on packages/main/vite.config.js 16 | */ 17 | readonly VITE_DEV_SERVER_URL: undefined | string; 18 | /** 19 | * The value of the variable is set in scripts/watch.js and depend on packages/main/vite.config.js 20 | */ 21 | readonly VITE_DEV_PORT: undefined | string; 22 | } 23 | 24 | interface ImportMeta { 25 | readonly env: ImportMetaEnv 26 | } 27 | -------------------------------------------------------------------------------- /vetur.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('vls').VeturConfig} */ 2 | module.exports = { 3 | settings: { 4 | 'vetur.useWorkspaceDependencies': true, 5 | 'vetur.experimental.templateInterpolationService': true, 6 | }, 7 | projects: [ 8 | { 9 | root: './packages/renderer', 10 | tsconfig: './tsconfig.json', 11 | snippetFolder: './.vscode/vetur/snippets', 12 | globalComponents: [ 13 | './src/components/**/*.vue', 14 | ], 15 | }, 16 | { 17 | root: './packages/main', 18 | tsconfig: './tsconfig.json', 19 | }, 20 | { 21 | root: './packages/preload', 22 | tsconfig: './tsconfig.json', 23 | }, 24 | { 25 | root: './packages/api', 26 | tsconfig: './tsconfig.json', 27 | }, 28 | ], 29 | }; 30 | -------------------------------------------------------------------------------- /vitest.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Config for global end-to-end tests 3 | * placed in project root tests folder 4 | * @type {import('vite').UserConfig} 5 | * @see https://vitest.dev/config/ 6 | */ 7 | const config = { 8 | test: { 9 | /** 10 | * By default, vitest search test files in all packages. 11 | * For e2e tests have sense search only is project root tests folder 12 | */ 13 | include: ['./tests/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], 14 | 15 | /** 16 | * A default timeout of 5000ms is sometimes not enough for playwright. 17 | */ 18 | testTimeout: 30_000, 19 | hookTimeout: 30_000, 20 | }, 21 | }; 22 | 23 | export default config; 24 | -------------------------------------------------------------------------------- /wiki/Home.md: -------------------------------------------------------------------------------- 1 | Welcome to the Fukayo wiki! -------------------------------------------------------------------------------- /wiki/_Sidebar.md: -------------------------------------------------------------------------------- 1 | | [![image](https://user-images.githubusercontent.com/26584973/203880803-f2f49cb3-0932-4376-b9fc-3f55cf645ad7.png)](home) | [![image](https://user-images.githubusercontent.com/26584973/203880716-5cf6b582-0c96-4f91-a247-741e0004c1e0.png)](home-fr) 2 | |----|----| 3 | 4 | *** 5 | 6 | ## 👨‍💻 Fukayo for devs 7 | * **[🤔 Meta](meta)** 8 | * **[⚙️ Setup](setup)** 9 | * **[📋 Requirements](setup-requirements)** 10 | * **[😵‍💫 Known Issues](setup-issues)** 11 | * **[🕸️ Add a new source](mirrors)** 12 | * **[📋 What you'll need](mirrors#what-youll-need)** 13 | * **[🎛️ What you might need](mirrors#what-you-might-need)** 14 | * **[🧰 Tools](mirrors#tools)** 15 | * **[👨‍💻️ Code: Step by step](mirrors#step-by-step-tutorial)** -------------------------------------------------------------------------------- /wiki/locale/fr/Home-fr.md: -------------------------------------------------------------------------------- 1 | Bienvenue sur le Wiki de Fukayo! -------------------------------------------------------------------------------- /wiki/locale/fr/_Sidebar.md: -------------------------------------------------------------------------------- 1 | | [![image](https://user-images.githubusercontent.com/26584973/203880803-f2f49cb3-0932-4376-b9fc-3f55cf645ad7.png)](home) | [![image](https://user-images.githubusercontent.com/26584973/203880716-5cf6b582-0c96-4f91-a247-741e0004c1e0.png)](home-fr) 2 | |----|----| 3 | 4 | *** 5 | 6 | ## 👨‍💻 Fukayo pour les devs 7 | * **[🤔 Meta](meta-fr)** 8 | * **[⚙️ Configuration](setup-fr)** 9 | * **[📋 Nécessaire](setup-requirements-fr)** 10 | * **[😵‍💫 Problème connus](setup-issues-fr)** 11 | * **[🕸️ Ajouter une nouvelle source](mirrors-fr)** 12 | * **[📋 Nécessaire](mirrors-fr#ce-quil-nous-faut)** 13 | * **[🎛️ Nécessaire (peut-être)](mirrors-fr#ce-quil-nous-faudra-peut-être)** 14 | * **[🧰 Outils](mirrors-fr#outils)** 15 | * **[👨‍💻️ Code: pas à pas](mirrors-fr#pas-à-pas)** -------------------------------------------------------------------------------- /wiki/locale/fr/meta-fr.md: -------------------------------------------------------------------------------- 1 | # Meta 2 | ## Commits 3 | ### Utiliser les [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) 4 | #### [liste des scopes disponible](https://github.com/JiPaix/Fukayo/blob/731e55cb3780ed30d93def29705cf7db1094b672/.vscode/settings.json#L25-L41) 5 | Exemples: 6 | - `fix(api): that didn't do this` 7 | - `feat(renderer): add that in here` 8 | - `chore(deps): update this` 9 | 10 | **Les non-devs doivent être en capacité de comprendre le message de commit** 11 | Si vous avez besoin de rentrer dans le détail technique utiliser le body du commit: 12 | ```text 13 | fix(mirrors): mangadex 14 | 15 | fix infinite loop in recommend() 16 | replaced XXX by YYY 17 | ``` 18 | ## Pull requests 19 | - Les titres de PRs doivent ressembler a des commits 20 | - Ne pas ouvrir de PR sur la branche `main` 21 | ## CHANGELOG 22 | Le Changelog est automatiquement généré en utilisant les messages des commits. 23 | # Overview 24 | ## Structure des dossiers 25 | ### i18n 26 | - Dossier: `/packages/i18n/src` 27 | - Détection des locales/async loader, convertisseur `BC47`, `3166-1 ALPHA2` vers `639-1` (custom) 28 | ### Main 29 | - Dossier: `/packages/main/src` 30 | - contient le code d'Electron, les fenêtres, icone de la bare des tâches... 31 | ### API 32 | - Dossier: `/packages/api/src` 33 | - databases, sources, web server, socket server, file server.. etc. 34 | ### Renderer 35 | - Dossier: `/packages/renderer/src` 36 | - Les vues, le router et les stores. -------------------------------------------------------------------------------- /wiki/locale/fr/setup-fr.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ## Installion des dépendances 4 | ```cmd 5 | npm install 6 | ``` 7 | 8 | ## Démarrer Fukayo 9 | ### Rechargement à chaud (Hot Module Reload) 10 | L'application est recharger à chaques changements 11 | ```cmd 12 | npm run watch 13 | ``` 14 | ### Build et start 15 | Build tous les packages et démarrer electron. 16 | ```cmd 17 | npm run build 18 | npm run start 19 | ``` 20 | ### Compilation 21 | Crée un executable. 22 | Les fichiers sont dans le dossier `$PROJECT_ROOT/dist` 23 | ```cmd 24 | run npm compile 25 | ``` 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /wiki/locale/fr/setup-issues-fr.md: -------------------------------------------------------------------------------- 1 | # Problèmes connus 2 | ## Toutes les plateformes 3 | Le rechargements des modules à chaud (**H**ot **M**odule **R**eload) ne fonctionne que pour les changements à l'interieur du dossier renderer. 4 | Les autres changements nécessite de redémarrer l'application. 5 | 6 | ## Windows 7 | Dans certains cas le HMR crash. 8 | Utiliser [build](setup-fr.md#build-et-start) à la place 9 | 10 | ## Linux 11 | Si vous utiliser `nvm` and que vous ne pouvez pas commit à cause d'une erreur `npx command not found` ou `npx commande introuvable`: 12 | Editez le fichier `.git/hooks/pre-coomit` avec le contenu suivants **après avoir fait** `npm install` 13 | ```sh 14 | #!/bin/sh 15 | export NVM_DIR="$HOME/.nvm" 16 | [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" 17 | npx nano-staged 18 | ``` -------------------------------------------------------------------------------- /wiki/locale/fr/setup-requirements-fr.md: -------------------------------------------------------------------------------- 1 | # Nécessaire à l'installation 2 | ## GIT 3 | Téléchargeable pour windows [ici](https://github.com/git-for-windows/git/releases/latest) 4 | 5 | ## Un environement de bureau 6 | Peu importe votre système d'exploitation Fukayo à besoin d'un environnement de bureau durant le developpement. 7 | 8 | ## NodeJS 9 | Version `>=16.13` -------------------------------------------------------------------------------- /wiki/meta.md: -------------------------------------------------------------------------------- 1 | # Meta 2 | ## Commits 3 | ### Use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) 4 | #### [list of available scopes](https://github.com/JiPaix/Fukayo/blob/731e55cb3780ed30d93def29705cf7db1094b672/.vscode/settings.json#L25-L41) 5 | Examples: 6 | - `fix(api): that didn't do this` 7 | - `feat(renderer): add that in here` 8 | - `chore(deps): update this` 9 | 10 | **Non-devs must be able to grasp what the commit is about** 11 | if you need to get technical please use the commit body: 12 | ```text 13 | fix(mirrors): mangadex 14 | 15 | fix infinite loop in recommend() 16 | replaced XXX by YYY 17 | ``` 18 | ## Pull requests 19 | - Treat PRs titles as commits 20 | - Do not open PR on branch `main` 21 | ## CHANGELOG 22 | Changelogs are automatically generated using commits messages. 23 | # Overview 24 | ## Folder Structure 25 | ### i18n 26 | - Location: `/packages/i18n/src` 27 | - locales detector/async loader, convert `BC47`, `3166-1 ALPHA2` to `639-1` (custom) 28 | ### Main 29 | - Location: `/packages/main/src` 30 | - contains electron's code, windows, systray icon.. 31 | ### API 32 | - Location: `/packages/api/src` 33 | - databases, mirrors, web server, socket server, file server.. etc. 34 | ### Renderer 35 | - Location: `/packages/renderer/src` 36 | - Views, router, stores -------------------------------------------------------------------------------- /wiki/setup-issues.md: -------------------------------------------------------------------------------- 1 | # Known issues 2 | ## All plateforms 3 | Hot module reload ONLY watches changes in renderer package. 4 | Changes to other packages requires to restart the app. 5 | 6 | ## Windows 7 | Hot module reload (HMR) randomly crashes in some cases. 8 | use [build and start](setup.md#build-and-start) instead 9 | 10 | ## Linux 11 | If you are using `nvm` and cannot commit because of error `npx command not found`: 12 | edit the file located in `.git/hooks/pre-commit` with this content **after you've run** `npm install` 13 | ```sh 14 | #!/bin/sh 15 | export NVM_DIR="$HOME/.nvm" 16 | [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" 17 | npx nano-staged 18 | ``` -------------------------------------------------------------------------------- /wiki/setup-requirements.md: -------------------------------------------------------------------------------- 1 | # Setup requirements 2 | ## GIT 3 | Windows users can get it [here](https://github.com/git-for-windows/git/releases/latest) 4 | 5 | ## Desktop Environment 6 | Regardless of your OS, Fukayo needs a desktop environment during development. 7 | 8 | ## NodeJS 9 | Version `>=16.13` -------------------------------------------------------------------------------- /wiki/setup.md: -------------------------------------------------------------------------------- 1 | # Setup 2 | 3 | ## Install dependencies 4 | ```cmd 5 | npm install 6 | ``` 7 | 8 | ## Start Fukayo 9 | ### Hot module reload 10 | The application is reloaded whenever changes occures. 11 | ```cmd 12 | npm run watch 13 | ``` 14 | ### Build and start 15 | Build all packages and run electron. 16 | ```cmd 17 | npm run build 18 | npm run start 19 | ``` 20 | ### Compile 21 | Creates an executable. 22 | Files are located in the `$PROJECT_ROOT/dist` folder 23 | ```cmd 24 | run npm compile 25 | ``` 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiPaix/Fukayo/f9bb5ba83d7f10a35f849a10c5513dbf814bfb94/workflow.png --------------------------------------------------------------------------------