├── .github ├── ISSUE_TEMPLATE │ ├── 🐛-bug-report.md │ └── 💡-feature-request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── publish.yml │ └── test-pr.yml ├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── .yarnrc.yml ├── CHANGELOG.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── LOCALIZATION.md ├── README.md ├── assets ├── bpo.png ├── screenshot.png └── trans_flag.png ├── index.html ├── package.json ├── public └── bpo.png ├── src-tauri ├── Cargo.lock ├── Cargo.toml ├── bin │ ├── BPO-steam │ └── BPO-steam-x86_64-unknown-linux-gnu ├── build.rs ├── icons │ ├── 128x128.png │ ├── 128x128@2x.png │ ├── 32x32.png │ ├── Square107x107Logo.png │ ├── Square142x142Logo.png │ ├── Square150x150Logo.png │ ├── Square284x284Logo.png │ ├── Square30x30Logo.png │ ├── Square310x310Logo.png │ ├── Square44x44Logo.png │ ├── Square71x71Logo.png │ ├── Square89x89Logo.png │ ├── StoreLogo.png │ ├── icon.icns │ ├── icon.ico │ └── icon.png ├── rust-toolchain.toml ├── src │ ├── commands │ │ ├── database.rs │ │ ├── logging.rs │ │ ├── metadata.rs │ │ ├── mod.rs │ │ └── scrapers │ │ │ ├── fitgirl.rs │ │ │ ├── mod.rs │ │ │ └── rezi.rs │ ├── main.rs │ ├── migrations │ │ ├── 1_down.sql │ │ └── 1_up.sql │ ├── paths.rs │ └── startup.rs └── tauri.conf.json ├── src ├── Main.svelte ├── Typings.d.ts ├── locale │ ├── i18n.ts │ ├── lang │ │ ├── af.json │ │ ├── ar.json │ │ ├── ba.json │ │ ├── cz.json │ │ ├── de.json │ │ ├── en.json │ │ ├── eo.json │ │ ├── hr.json │ │ ├── lt.json │ │ ├── pl.json │ │ ├── pt-BR.json │ │ └── sr.json │ ├── languages.json │ └── locales.ts ├── main.ts ├── routes │ ├── Browse.svelte │ ├── Library.svelte │ ├── Preferences.svelte │ └── modals │ │ ├── NewGame.svelte │ │ └── Toast.svelte ├── scripts │ ├── Browse.ts │ ├── Library.ts │ ├── Main.ts │ └── Preferences.ts ├── styles │ ├── Global.scss │ ├── _Browse.scss │ ├── _Library.scss │ ├── _Modal.scss │ └── _Preferences.scss └── vite-env.d.ts ├── svelte.config.js ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts └── yarn.lock /.github/ISSUE_TEMPLATE/🐛-bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41B Bug Report" 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | 7 | --- 8 | 9 | **What bug did you encounter?**
10 | Please provide a clear and concise description of the bug. 11 | 12 | **Please provide the steps to reproduce the bug.**
13 | Example: 14 | 1. Go to '...' 15 | 2. Click on '...' 16 | 3. Scroll down to '...' 17 | 4. See error when you do '...' 18 | 19 | **What did you expect to happen?**
20 | Please provide a clear and concise description of what you expected to happen when you encountered the bug. 21 | 22 | **Screenshots:**
23 | If applicable, please provide screenshots in order to help further explain the issue(s) you've encountered. 24 | 25 | **Please specify your operating system and its version.**
26 | - OS: [e.g. Windows 10] 27 | - Version: [e.g. 22H2] 28 | 29 | **Additional Information:**
30 | If applicable, please provide any additional information relating to your bug report. 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/💡-feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F4A1 Feature Request" 3 | about: Suggest an idea for this project 4 | title: "[FEATURE]" 5 | labels: enhancement 6 | 7 | --- 8 | 9 | **Is your feature request related to a problem?**
10 | If so, please provide a clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **What feature would you like to be added?**
13 | Please provide a clear and concise description of the feature you'd like to be added. 14 | 15 | **Additional Information:**
16 | If applicable, please provide any additional information relating to your feature request, such as screenshots or examples of the mentioned feature in other software. 17 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **What changes were made?** 2 | Explain what changes you've implemented with this PR. 3 | 4 | **Does this PR solve an issue?** 5 | If so, please provide a link to the issue this PR resolved. 6 | 7 | **Does this code introduce any issues or warnings?** 8 | If any issues or warnings are generated, please list them here. 9 | 10 | **Which platforms were you able to test this code on?** 11 | Please select all the platforms that apply. 12 | 13 | - [ ] Linux 14 | - [ ] Windows 15 | - [ ] MacOS 16 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # Build Test Action 2 | # ----------------- 3 | # - Builds the app for all platforms 4 | # Runs on ubuntu-latest, macos-latest and windows-latest 5 | # - Publishes the app to GitHub Releases 6 | 7 | name: Publish Release 8 | on: [workflow_dispatch] 9 | 10 | jobs: 11 | create-release: 12 | permissions: write-all 13 | runs-on: ubuntu-20.04 14 | outputs: 15 | release_id: ${{ steps.create-release.outputs.result }} 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: setup node 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: 16 23 | - name: get version 24 | run: echo "PACKAGE_VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_ENV 25 | - name: create release 26 | id: create-release 27 | uses: actions/github-script@v6 28 | with: 29 | script: | 30 | const { data } = await github.rest.repos.createRelease({ 31 | owner: context.repo.owner, 32 | repo: context.repo.repo, 33 | tag_name: `${process.env.PACKAGE_VERSION}`, 34 | name: `Release v${process.env.PACKAGE_VERSION}`, 35 | body: 'Take a look at the assets to download and install this app.', 36 | draft: true, 37 | prerelease: false 38 | }) 39 | 40 | return data.id 41 | 42 | build-tauri: 43 | needs: create-release 44 | strategy: 45 | fail-fast: false 46 | matrix: 47 | platform: [macos-latest, ubuntu-20.04, windows-latest] 48 | 49 | runs-on: ${{ matrix.platform }} 50 | steps: 51 | - uses: actions/checkout@v3 52 | - name: setup node 53 | uses: actions/setup-node@v3 54 | with: 55 | node-version: 16 56 | - name: install Rust stable 57 | uses: dtolnay/rust-toolchain@stable 58 | - name: install dependencies (ubuntu only) 59 | if: matrix.platform == 'ubuntu-20.04' 60 | run: | 61 | sudo apt-get update 62 | sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf 63 | - name: install app dependencies and build it 64 | run: yarn && yarn build 65 | - uses: tauri-apps/tauri-action@v0 66 | env: 67 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 68 | TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} 69 | TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} 70 | with: 71 | releaseId: ${{ needs.create-release.outputs.release_id }} 72 | includeUpdaterJson: true 73 | 74 | publish-release: 75 | runs-on: ubuntu-20.04 76 | needs: [create-release, build-tauri] 77 | 78 | steps: 79 | - name: publish release 80 | id: publish-release 81 | uses: actions/github-script@v6 82 | env: 83 | release_id: ${{ needs.create-release.outputs.release_id }} 84 | with: 85 | script: | 86 | github.rest.repos.updateRelease({ 87 | owner: context.repo.owner, 88 | repo: context.repo.repo, 89 | release_id: process.env.release_id, 90 | draft: false, 91 | prerelease: false 92 | }) 93 | -------------------------------------------------------------------------------- /.github/workflows/test-pr.yml: -------------------------------------------------------------------------------- 1 | # Build Test Action 2 | # ----------------- 3 | # - Builds the app as debug to test if it compiles 4 | # Runs on ubuntu-latest 5 | # Runs the command "yarn tauri build --bundle none" 6 | # Runs on every Push to the main branch 7 | 8 | name: PR Build Test 9 | 10 | on: 11 | pull_request: 12 | push: 13 | branches: [main] 14 | 15 | jobs: 16 | test: 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | platform: [ubuntu-20.04] 21 | 22 | runs-on: ${{ matrix.platform }} 23 | steps: 24 | - uses: actions/checkout@v3 25 | - name: Setup node 26 | uses: actions/setup-node@v3 27 | with: 28 | node-version: 20 29 | - name: Install Rust stable 30 | uses: dtolnay/rust-toolchain@stable 31 | - name: Install dependencies 32 | if: matrix.platform == 'ubuntu-20.04' 33 | run: | 34 | sudo apt-get update 35 | sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf 36 | - name: Install node deps 37 | if: steps.cache-node.outputs.cache-hit != 'true' 38 | run: npm i 39 | 40 | - name: Run svelte-check 41 | run: npm run check 42 | 43 | - name: Install rust deps 44 | if: steps.cache-rust.outputs.cache-hit != 'true' 45 | run: | 46 | cd src-tauri 47 | cargo check --no-default-features 48 | cd .. 49 | 50 | - name: Run tests on Rust 51 | run: npm run rust:test 52 | 53 | - name: Cache the rust crates 54 | id: cache-rust 55 | uses: actions/cache@v3 56 | with: 57 | path: | 58 | ~/.cargo/bin/ 59 | ~/.cargo/registry/index/ 60 | ~/.cargo/registry/cache/ 61 | ~/.cargo/git/db/ 62 | src-tauri/target/ 63 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 64 | 65 | - name: Get npm cache directory 66 | id: npm-cache-dir 67 | shell: bash 68 | run: echo "dir=$(npm config get cache)" >> ${GITHUB_OUTPUT} 69 | 70 | - name: Cache the node packages 71 | id: cache-node 72 | uses: actions/cache@v3 73 | with: 74 | path: ${{ steps.npm-cache-dir.outputs.dir }} 75 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 76 | restore-keys: | 77 | ${{ runner.os }}-node- 78 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | src-tauri/target 3 | dist/ 4 | src-tauri/.sentry-native 5 | .yarn 6 | *.lockb -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "useTabs": false, 4 | "semi": true, 5 | "singleQuote": true, 6 | "bracketSpacing": true, 7 | "bracketSameLine": false, 8 | "arrowParens": "always", 9 | "svelteStrictMode": true, 10 | "svelteIndentScriptAndStyle": true 11 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "editor.formatOnSave": true, 4 | "editor.formatOnPaste": true, 5 | "editor.formatOnType": true, 6 | "svelte.plugin.svelte.format.enable": true, 7 | "[svelte]": { 8 | "editor.defaultFormatter": "svelte.svelte-vscode" 9 | }, 10 | "i18n-ally.localesPaths": ["src/locale/lang"], 11 | "i18n-ally.keystyle": "nested", 12 | "typescript.tsdk": "node_modules/typescript/lib", 13 | "rust-analyzer.linkedProjects": ["./src-tauri/Cargo.toml"], 14 | "rust-analyzer.showUnlinkedFileNotification": false, 15 | "[rust]": { 16 | "editor.defaultFormatter": "rust-lang.rust-analyzer" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules -------------------------------------------------------------------------------- /CHANGELOG.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Hello World", 3 | "body": "Lorem Ipsum" 4 | } 5 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Black Pearl Origin Code of Conduct 2 | 3 | ## **Our Pledge** 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | **Examples of behavior that contributes to creating a positive environment include:** 10 | 11 | - Using welcoming and inclusive language 12 | - Being respectful of differing viewpoints and experiences 13 | - Gracefully accepting constructive criticism 14 | - Focusing on what is best for the community 15 | - Showing empathy towards other community members 16 | 17 | **Examples of unacceptable behavior by participants include:** 18 | 19 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | - Trolling, insulting/derogatory comments, and personal or political attacks 21 | - Public or private harassment 22 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | - Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## **Our Responsibilities** 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team by [making a ticket in the official Discord server](https://discord.gg/3VxVbWaeY6). All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate given the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the Contributor Covenant, version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html. 44 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to BPO 2 | 3 | The following are a few ways you can contribute to the project, along with a guideline to follow when writing code or making a commit. 4 | 5 | ## **With code** 6 | 7 | --- 8 | 9 | Contributing to this project is easy and appreciated. 10 | 11 | You will need [git](https://git-scm.com) for contributing. 12 | 13 | 1. [Fork the repo](https://github.com/BlackPearlOrigin/blackpearlorigin/fork) 14 | 2. Create a new branch `git checkout -b branch-name` 15 | 3. Commit your changes and set commit message `git commit -m "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce rhoncus."` 16 | 4. Push your changes `git push -u origin branch-name` 17 | 5. Open a new pull request 18 | 19 | ## **Style guidelines** 20 | 21 | --- 22 | 23 | Most of these here are taken from the Atom guidelines (they're really good) 24 | 25 | ### **Git commit messages** 26 | 27 | - Use the present tense ("Add feature", not "Added feature") 28 | - Use the imperative mood ("Move cursor to...", not "Moves cursor to") 29 | - Limit the first line to 72 characters or less 30 | - Describe the additions on the next line 31 | - When changing documentation, prefix `[ci skip]` on the commit message 32 | 33 | ### **TypeScript guidelines** 34 | 35 | All of our code is styled with [Prettier](https://prettier.io). 36 | 37 | - Prefer using the spread syntax `{...someObj}` instead `Object.assign()` 38 | - Use different cases: 39 | - camelCase for constants, variables and functions 40 | - PascalCase for classes 41 | - Inline exports when possible 42 | 43 | ```ts 44 | // Use this: 45 | export const functionName = (): void => { 46 | // ... 47 | } 48 | 49 | // Not this: 50 | const functionName = (): void => { 51 | // ... 52 | } 53 | export functionName; 54 | ``` 55 | 56 | - Use arrow functions when possible 57 | 58 | ```ts 59 | // Use this: 60 | const functionName = (): void => { 61 | // ... 62 | }; 63 | 64 | // Not this: 65 | function functionName(): void { 66 | // ... 67 | } 68 | ``` 69 | 70 | ### **Rust guidelines** 71 | 72 | - Use different cases: 73 | - `snake_case` for functions and variables 74 | - `PascalCase` for structs and enums 75 | - `SCREAMING_SNAKE_CASE` for constants 76 | 77 | ### **Documentation guidelines** 78 | 79 | - Use [JSDoc](https://jsdoc.app) 80 | - Use [Markdown](https://www.markdownguide.org/) 81 | - Reference types in documentation using `{}` 82 | - When making a function, use this 83 | - If it invokes a Rust function, use `Typescript Function -> Rust Function` 84 | - If it's only a TypeScript function, use `Typescript Function` 85 | 86 | Example: 87 | 88 | ```ts 89 | /* 90 | * Typescript Function 91 | * - Adds 2 + 2 92 | * 93 | * @returns {number} the number added 94 | */ 95 | const addNum = (): number => 2 + 2; 96 | ``` 97 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2022, Black Pearl Origin and it's contributors 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /LOCALIZATION.md: -------------------------------------------------------------------------------- 1 | # Localization 2 | 3 | Black Pearl Origin has full localization support and any number of languages can be added and changed natively. 4 | 5 | ### Guidelines / Tips for Adding Translations 6 | 7 | - Only add translations for things you are 100% sure about. You don't have to translate everything, partially translating files is fine as well. 8 | - Quality > quantity. 9 | - If you are unsure about a translation, leave it blank. It is better to have a blank translation than a wrong one. 10 | - Feel free to join our [Discord](https://discord.gg/WpBr3hJVf5) if you have any questions! 11 | 12 | ## How to Translate 13 | 14 | There are two ways of doing it:
15 | 16 | - ### Using POEditor 17 |
18 | 19 | 1. Create an account on [POEditor](https://poeditor.com) 20 | 2. Join [our project](https://poeditor.com/join/project/GMut4xJe7I) on it 21 | 3. Search for the language you'd like to translate. If it isn't listed, feel free to ask for it to be added via [Discord](https://discord.gg/WpBr3hJVf5) 22 | 4. Start translating! 23 |
24 | 25 | - ### Using Github 26 |
27 | 28 | 1. [Fork the repo](https://github.com/BlackPearlOrigin/blackpearlorigin/fork) 29 | 2. Create a branch `git checkout -b klingon-translation` 30 | 3. Go to `src/locale/lang` 31 | 4. Create a new file named after the [2-letter ISO code (ISO-639-1)](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) 32 | 5. Copy the `en.json` file into it 33 | 6. Edit the file but not the keys (Example: `loadingText`) 34 | 7. Push the changes into your fork adhering to [CONTRIBUTING.md](./CONTRIBUTING.md) 35 | 8. Open a PR. 36 | 37 |
38 |
39 | 40 | ## Credits: 41 | 42 | ohvii: Swedish translation 43 | despair: French translation 44 | plaga: Hungarian translation 45 | superweird7 and MasterSwords: Arabic translation 46 | GooUckd: Estonian and Finnish translation 47 | Q99: Bosnian and Serbian translation 48 | Mirza Čustović: Croatian translation 49 | N3kowarrior: Czech translation 50 | Rafo: Dutch translation 51 | Brisolo32: Portuguese (Brazil) and Esperanto translation 52 | zun1: German translation 53 | xDal-Lio: Italian translation 54 | Lol123zv: Latvian and Russian translation 55 | SteinScanner and Mr Mango: Polish translation 56 | Sup3r: Portuguese (Portugal) translation 57 | AlexanderMaxRanabel: Turkish translation 58 | SoulStyle: Romanian translation 59 | TeeNam: Vietnamese translation (To be finished) 60 | lyubomir501: Bulgarian translation 61 | Sweeflyboy: Afrikaans translation 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | > [!WARNING] 3 | > BPO has not been developed in a long while and can be considered discontinued, please move on onto other launchers, like [Hydra](https://github.com/hydralauncher/hydra) 4 | 5 | 6 | 7 | ![Svelte](https://img.shields.io/badge/Svelte-4A4A55?style=for-the-badge&logo=svelte&logoColor=FF3E00 'Svelte') ![Tauri](https://img.shields.io/badge/Tauri-4A4A55?style=for-the-badge&logo=tauri&logoColor=00D1B2 'Tauri') 8 | 9 | ![Version](https://img.shields.io/badge/Version-1.1.0-blue?style=for-the-badge) ![AUR](https://img.shields.io/aur/version/black-pearl-origin?style=for-the-badge) ![Build Status](https://img.shields.io/badge/Status-Beta-green?style=for-the-badge) ![License](https://img.shields.io/badge/License-BSD--3--Clause-blue?style=for-the-badge) 10 | 11 | ![Discord](https://img.shields.io/discord/1116707367246123019?label=Discord&logo=discord&logoColor=white&style=for-the-badge) 12 | 13 | Black Pearl Origin is a fork of the Project Black Pearl, founded and maintained by the former PBP lead developers. It was forked due to a fallout with one of the owners in PBP. 14 | 15 | ## What is Black Pearl Origin? 16 | 17 | **Black Pearl Origin** (or BPO) is a [FOSS](https://en.wikipedia.org/wiki/Free_and_open-source_software) project that aims to unify game sources in one place by utilizing extensions made by the community. It is aimed to provide a convenient way of dealing with games sourced from all sorts of websites and provides a store system powered by a powerful extension ecosystem. 18 | 19 | 20 | 21 | ## What is the current state of the project? 22 | 23 | BPO is currently in beta. You can check [the to-do list](https://github.com/orgs/BlackPearlOrigin/projects/4/views/1) to see what features are planned or currently in development. 24 | 25 | ## How can I contribute? 26 | 27 | We welcome any contributions to the project, be it code, translations, or just general feedback. You can check out the [CONTRIBUTING.md](./CONTRIBUTING.md) file for more information on how to contribute via code. 28 | Please remember that translations are managed differently than code contributions as mentioned below. 29 | 30 | ## Translations 31 | 32 | Black Pearl Origin supports full localization. Instructions to help translate the project can be found in [LOCALIZATION.md](./LOCALIZATION.md). 33 | 34 | ## Support 35 | 36 | You are always welcome to join our [Discord](https://discord.gg/WpBr3hJVf5) server to get help or just to hang out with us! 37 | 38 | ## Credit 39 | 40 | Special thanks to the developers of the [Stremio Addon SDK](https://github.com/Stremio/stremio-addon-sdk) for allowing us to use their code as a base for our Plugin SDK. 41 | -------------------------------------------------------------------------------- /assets/bpo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackPearlOrigin/blackpearlorigin/56c14327656b9ca2b9300b5fc9db58ba0224f09f/assets/bpo.png -------------------------------------------------------------------------------- /assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackPearlOrigin/blackpearlorigin/56c14327656b9ca2b9300b5fc9db58ba0224f09f/assets/screenshot.png -------------------------------------------------------------------------------- /assets/trans_flag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackPearlOrigin/blackpearlorigin/56c14327656b9ca2b9300b5fc9db58ba0224f09f/assets/trans_flag.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Black Pearl Origin 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blackpearlorigin", 3 | "private": true, 4 | "version": "1.2.1", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "check": "svelte-check --tsconfig ./tsconfig.json", 11 | "tauri": "tauri", 12 | "build:prod": "tauri build -b none", 13 | "build:debug": "tauri build --debug", 14 | "rust:test": "pwd && cd src-tauri/ && cargo test --no-default-features" 15 | }, 16 | "dependencies": { 17 | "@tauri-apps/api": "^1.1.0", 18 | "@zerodevx/svelte-toast": "^0.9.3", 19 | "npm": "^9.6.3", 20 | "svelte-i18n": "^3.6.0", 21 | "svelte-modals": "^1.2.0", 22 | "svelte-navigator": "^3.2.2", 23 | "zod": "^3.21.4" 24 | }, 25 | "devDependencies": { 26 | "@sveltejs/vite-plugin-svelte": "^1.0.1", 27 | "@tauri-apps/cli": "^1.1.0", 28 | "@tsconfig/svelte": "^3.0.0", 29 | "@types/node": "^18.7.10", 30 | "sass": "^1.57.1", 31 | "svelte": "^3.49.0", 32 | "svelte-check": "^2.8.0", 33 | "svelte-ionicons": "^0.4.4", 34 | "svelte-preprocess": "^4.10.7", 35 | "svelte-simple-modal": "^1.4.5", 36 | "tslib": "^2.4.0", 37 | "typescript": "^4.6.4", 38 | "vite": "^3.2.7" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /public/bpo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackPearlOrigin/blackpearlorigin/56c14327656b9ca2b9300b5fc9db58ba0224f09f/public/bpo.png -------------------------------------------------------------------------------- /src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "blackpearlorigin" 3 | version = "1.2.1" 4 | description = "Unify your game sources in one place by using modules made by the community. " 5 | authors = ["zun1uwu", "infinity-plus", "Brisolo32", "Contributors of Black Pearl Origin"] 6 | license = "BSD-3-Clause" 7 | repository = "https://github.com/BlackPearlOrigin/blackpearlorigin" 8 | edition = "2021" 9 | rust-version = "1.57" 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [build-dependencies] 14 | tauri-build = { version = "1.1", features = [] } 15 | 16 | [dependencies] 17 | serde_json = "1.0" 18 | rfd = "0.10.0" 19 | rusqlite = { version = "0.28.0", features = ["bundled"] } 20 | rusqlite_migration = "1.0.1" 21 | execute = "0.2.11" 22 | reqwest = { version = "0.11.13", features = ["blocking", "json"] } 23 | serde = { version = "1.0", features = ["derive"] } 24 | tauri = { version = "1.2", features = ["api-all", "macos-private-api", "system-tray", "updater"] } 25 | uuid = { version = "1.2.2", features = ["v4", "fast-rng"] } 26 | lazy_static = "1.4.0" 27 | anyhow = "1.0.71" 28 | sevenz-rust = { version = "0.2" } 29 | log = "0.4.20" 30 | env_logger = "0.10.1" 31 | rayon = "1.8.0" 32 | scraper = "0.18.1" 33 | 34 | [features] 35 | # by default Tauri runs in production mode 36 | # when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL 37 | default = ["custom-protocol"] 38 | # this feature is used used for production builds where `devPath` points to the filesystem 39 | # DO NOT remove this 40 | custom-protocol = ["tauri/custom-protocol"] 41 | -------------------------------------------------------------------------------- /src-tauri/bin/BPO-steam: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackPearlOrigin/blackpearlorigin/56c14327656b9ca2b9300b5fc9db58ba0224f09f/src-tauri/bin/BPO-steam -------------------------------------------------------------------------------- /src-tauri/bin/BPO-steam-x86_64-unknown-linux-gnu: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackPearlOrigin/blackpearlorigin/56c14327656b9ca2b9300b5fc9db58ba0224f09f/src-tauri/bin/BPO-steam-x86_64-unknown-linux-gnu -------------------------------------------------------------------------------- /src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackPearlOrigin/blackpearlorigin/56c14327656b9ca2b9300b5fc9db58ba0224f09f/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackPearlOrigin/blackpearlorigin/56c14327656b9ca2b9300b5fc9db58ba0224f09f/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackPearlOrigin/blackpearlorigin/56c14327656b9ca2b9300b5fc9db58ba0224f09f/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackPearlOrigin/blackpearlorigin/56c14327656b9ca2b9300b5fc9db58ba0224f09f/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackPearlOrigin/blackpearlorigin/56c14327656b9ca2b9300b5fc9db58ba0224f09f/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackPearlOrigin/blackpearlorigin/56c14327656b9ca2b9300b5fc9db58ba0224f09f/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackPearlOrigin/blackpearlorigin/56c14327656b9ca2b9300b5fc9db58ba0224f09f/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackPearlOrigin/blackpearlorigin/56c14327656b9ca2b9300b5fc9db58ba0224f09f/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackPearlOrigin/blackpearlorigin/56c14327656b9ca2b9300b5fc9db58ba0224f09f/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackPearlOrigin/blackpearlorigin/56c14327656b9ca2b9300b5fc9db58ba0224f09f/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackPearlOrigin/blackpearlorigin/56c14327656b9ca2b9300b5fc9db58ba0224f09f/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackPearlOrigin/blackpearlorigin/56c14327656b9ca2b9300b5fc9db58ba0224f09f/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackPearlOrigin/blackpearlorigin/56c14327656b9ca2b9300b5fc9db58ba0224f09f/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackPearlOrigin/blackpearlorigin/56c14327656b9ca2b9300b5fc9db58ba0224f09f/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackPearlOrigin/blackpearlorigin/56c14327656b9ca2b9300b5fc9db58ba0224f09f/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackPearlOrigin/blackpearlorigin/56c14327656b9ca2b9300b5fc9db58ba0224f09f/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /src-tauri/rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" -------------------------------------------------------------------------------- /src-tauri/src/commands/database.rs: -------------------------------------------------------------------------------- 1 | use crate::paths; 2 | use rusqlite::{params, Connection}; 3 | use std::{fs, path::PathBuf}; 4 | use uuid::Uuid; 5 | 6 | use super::logging::log_info; 7 | 8 | #[derive(serde::Serialize)] 9 | pub struct Game { 10 | pub id: i64, 11 | pub name: String, 12 | pub exe_path: String, 13 | pub description: String, 14 | pub image: String, 15 | } 16 | 17 | pub fn copy_image(image_path: PathBuf) -> PathBuf { 18 | let uuid = Uuid::new_v4().simple().to_string(); 19 | 20 | log_info(&format!("Generated the following (simple) UUID: {}", uuid)); 21 | 22 | let file_name = match image_path.extension() { 23 | Some(extension) => format!("{}.{}", uuid, extension.to_string_lossy()), 24 | None => uuid, 25 | }; 26 | 27 | let new_path = paths::get_bpo().join("images").join(file_name); 28 | fs::copy(image_path, &new_path).expect("Copying image failed"); 29 | 30 | new_path 31 | } 32 | 33 | #[tauri::command] 34 | pub fn save_to_db( 35 | title: String, 36 | exe_path: String, 37 | description: String, 38 | image: String, 39 | ) -> Result<(), String> { 40 | let image_path: PathBuf = if image.as_str() == "None" { 41 | "None".into() 42 | } else { 43 | copy_image(image.into()) 44 | }; 45 | 46 | // Establish a connection to the database file (library.db) 47 | let db_path = paths::get_db(); 48 | let mut connection = Connection::open(db_path).map_err(|e| e.to_string())?; 49 | 50 | // Declare the query to execute in the sqlite file 51 | let query = "INSERT INTO games (name, executable, description, image) VALUES (?, ?, ?, ?)"; 52 | let transaction = connection.transaction().map_err(|e| e.to_string())?; 53 | let params = params![title, exe_path, description, image_path.to_string_lossy()]; 54 | 55 | transaction 56 | .execute(query, params) 57 | .map_err(|e| e.to_string())?; 58 | 59 | transaction.commit().map_err(|e| e.to_string())?; 60 | 61 | log_info(&format!("Saved game with name \"{}\" to the DB", title)); 62 | Ok(()) 63 | } 64 | 65 | #[tauri::command] 66 | pub fn get_from_db() -> Result, String> { 67 | // Establish a connection to the database file (library.db) 68 | let db_path = paths::get_db(); 69 | let connection = Connection::open(db_path).map_err(|e| e.to_string())?; 70 | 71 | // Declare the query to execute in the sqlite file 72 | let query = "SELECT * FROM games"; 73 | let mut stmt = connection.prepare(query).map_err(|e| e.to_string())?; 74 | let mut rows = stmt.query([]).map_err(|e| e.to_string())?; 75 | 76 | let mut games = vec![]; 77 | while let Ok(Some(row)) = rows.next() { 78 | games.push(Game { 79 | id: row.get(0).map_err(|e| e.to_string())?, 80 | name: row.get(1).map_err(|e| e.to_string())?, 81 | exe_path: row.get(2).map_err(|e| e.to_string())?, 82 | description: row.get(3).map_err(|e| e.to_string())?, 83 | image: row.get(4).map_err(|e| e.to_string())?, 84 | }); 85 | } 86 | 87 | log_info(&format!("Got {} game(s) from DB", games.len())); 88 | Ok(games) 89 | } 90 | 91 | #[tauri::command] 92 | pub fn edit_in_db( 93 | id: i64, 94 | name: String, 95 | executable: String, 96 | description: String, 97 | image: String, 98 | ) -> Result<(), String> { 99 | let db_path = paths::get_bpo().join("library.db"); 100 | let mut connection = Connection::open(db_path).map_err(|e| e.to_string())?; 101 | 102 | // copy new image to location 103 | let image_path: PathBuf = if image.as_str() == "None" { 104 | "None".into() 105 | } else { 106 | copy_image(image.into()) 107 | }; 108 | 109 | let query = 110 | "UPDATE games SET name = ?, executable = ?, description = ?, image = ? WHERE id = ?"; 111 | let transaction = connection.transaction().map_err(|e| e.to_string())?; 112 | let params = params![ 113 | name, 114 | executable, 115 | description, 116 | image_path.to_string_lossy(), 117 | id 118 | ]; 119 | 120 | transaction 121 | .execute(query, params) 122 | .map_err(|e| e.to_string())?; 123 | 124 | transaction.commit().map_err(|e| e.to_string())?; 125 | Ok(()) 126 | } 127 | 128 | #[tauri::command] 129 | pub fn delete_from_db(id: i64) -> Result<(), String> { 130 | let db_path = paths::get_bpo().join("library.db"); 131 | let mut connection = Connection::open(db_path).map_err(|e| e.to_string())?; 132 | 133 | let query = "DELETE FROM games WHERE id = ?;"; 134 | let tx = connection.transaction().map_err(|e| e.to_string())?; 135 | tx.execute(query, params![id]).map_err(|e| e.to_string())?; 136 | tx.commit().map_err(|e| e.to_string())?; 137 | 138 | log_info(&format!("Deleted game with id: {}", id)); 139 | Ok(()) 140 | } 141 | 142 | #[tauri::command] 143 | pub fn wipe_library() -> Result<(), String> { 144 | let db_path = paths::get_bpo().join("library.db"); 145 | let mut connection = Connection::open(db_path).map_err(|e| e.to_string())?; 146 | 147 | let query = "DELETE FROM games;"; 148 | let tx = connection.transaction().map_err(|e| e.to_string())?; 149 | tx.execute(query, []).map_err(|e| e.to_string())?; 150 | tx.commit().map_err(|e| e.to_string())?; 151 | 152 | log_info("Wiped the entire library"); 153 | Ok(()) 154 | } 155 | -------------------------------------------------------------------------------- /src-tauri/src/commands/logging.rs: -------------------------------------------------------------------------------- 1 | use log::{error, info, warn}; 2 | 3 | #[tauri::command] 4 | pub fn log_error(msg: &str) { 5 | error!("{msg}") 6 | } 7 | 8 | #[tauri::command] 9 | pub fn log_warn(msg: &str) { 10 | warn!("{msg}") 11 | } 12 | 13 | #[tauri::command] 14 | pub fn log_info(msg: &str) { 15 | info!("{msg}") 16 | } 17 | -------------------------------------------------------------------------------- /src-tauri/src/commands/metadata.rs: -------------------------------------------------------------------------------- 1 | use crate::paths; 2 | use reqwest::blocking::Client; 3 | use uuid::Uuid; 4 | 5 | use super::logging::log_info; 6 | 7 | #[derive(serde::Deserialize, serde::Serialize)] 8 | pub struct GameMeta { 9 | name: String, 10 | id: u32, 11 | cover_url: String, 12 | summary: String, 13 | } 14 | 15 | #[tauri::command] 16 | pub fn get_game_metadata(name: String) -> Result, String> { 17 | let url = "https://igdb-api.onrender.com/api/v1/game/"; 18 | 19 | let client = Client::builder() 20 | .danger_accept_invalid_certs(true) 21 | .build() 22 | .map_err(|e| format!("Failed to build request client: {e}"))?; 23 | 24 | let response = client 25 | .get(url.to_string() + &name) 26 | .send() 27 | .and_then(|resp| resp.error_for_status()) 28 | .map_err(|e| format!("Failed to send request: {e}"))?; 29 | 30 | log_info(&format!("Response: {:?}", response)); 31 | 32 | let game_meta: Vec = response 33 | .json() 34 | .map_err(|e| format!("Failed to parse response: {e}"))?; 35 | 36 | Ok(game_meta) 37 | } 38 | 39 | #[tauri::command] 40 | pub fn download_image(url: String) -> Result { 41 | let response = reqwest::blocking::get(url) 42 | .and_then(|resp| resp.error_for_status()) 43 | .map_err(|err| format!("Failed to send request: {}", err))?; 44 | 45 | let image = response 46 | .bytes() 47 | .map_err(|err| format!("Failed to get image bytes: {}", err))?; 48 | 49 | let uuid = Uuid::new_v4(); 50 | 51 | let image_path = paths::get_bpo() 52 | .join("images") 53 | .join(format!("{}.jpg", uuid.simple())); // Extension is hardcoded for now 54 | 55 | // Write the image to the images folder and return the path 56 | std::fs::write(image_path.clone(), image) 57 | .map_err(|err| format!("Failed to write image: {}", err))?; 58 | 59 | Ok(image_path.to_str().unwrap().to_string()) 60 | } 61 | 62 | #[cfg(test)] 63 | mod tests { 64 | use super::*; 65 | use crate::startup; 66 | 67 | fn setup() { 68 | startup::init(); 69 | } 70 | 71 | #[test] 72 | fn test_download_img() { 73 | setup(); 74 | 75 | let url = "https://picsum.photos/200"; 76 | let image_path = download_image(url.to_string()).unwrap(); 77 | 78 | assert!(image_path.contains("images")); 79 | assert!(image_path.contains(".jpg")); 80 | 81 | // cleanup 82 | std::fs::remove_file(image_path).unwrap(); 83 | std::fs::remove_dir_all(paths::get_bpo()).unwrap(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src-tauri/src/commands/mod.rs: -------------------------------------------------------------------------------- 1 | use rfd::FileDialog; 2 | use std::{process, thread, time::Instant}; 3 | 4 | use std::fs; 5 | #[cfg(target_family = "unix")] 6 | use std::os::unix::fs::PermissionsExt; 7 | use std::path::PathBuf; 8 | 9 | use self::logging::{log_error, log_info}; 10 | 11 | pub mod database; 12 | pub mod logging; 13 | pub mod metadata; 14 | pub mod scrapers; 15 | 16 | #[tauri::command] 17 | // Opens a file dialog that prompts the user for an executable 18 | // Returns the filepath as a string 19 | pub fn file_dialog() -> Option { 20 | log_info("Executable dialog opened"); 21 | 22 | // Prompt the user to select a file from their computer as an input 23 | let dialog = FileDialog::new() 24 | .add_filter("Executables", &["exe", "com", "cmd", "bat", "sh"]) 25 | .set_directory("/") 26 | .pick_file(); 27 | 28 | dialog.map(|p| p.to_string_lossy().to_string()) 29 | } 30 | 31 | #[tauri::command] 32 | // Opens a file dialog that prompts the user for an image 33 | pub fn image_dialog() -> Option { 34 | log_info("Image dialog opened"); 35 | 36 | // Prompt the user to select a file from their computer as an input 37 | // For error handling, you can use if- and match statements 38 | let dialog = rfd::FileDialog::new() 39 | .add_filter( 40 | "Images", 41 | &["png", "jpg", "jpeg", "gif", "bmp", "ico", "webp"], 42 | ) 43 | .pick_file(); 44 | 45 | dialog.map(|p| p.to_string_lossy().to_string()) 46 | } 47 | 48 | #[cfg(target_family = "unix")] 49 | fn ensure_executable(target: PathBuf) { 50 | let perms = fs::Permissions::from_mode(0o770); 51 | fs::set_permissions(target, perms).unwrap(); 52 | } 53 | 54 | #[tauri::command] 55 | // This function is ran everytime the user clicks "Run" on a library entry 56 | pub fn run_game(path: String) { 57 | let mut command = process::Command::new(path.clone()); 58 | 59 | #[cfg(target_family = "unix")] 60 | ensure_executable(PathBuf::from(path)); 61 | 62 | let start_time = Instant::now(); 63 | 64 | thread::spawn(move || { 65 | let mut child = match command.spawn() { 66 | Ok(child) => child, 67 | Err(_) => return, 68 | }; 69 | 70 | if let Ok(code) = child.wait() { 71 | if code.success() { 72 | let final_time = Instant::now() - start_time; 73 | log_info(&format!("Game ran for {} second(s)", final_time.as_secs())); 74 | } else if let Some(c) = code.code() { 75 | log_error(&format!("Game exited with error: {}", c)); 76 | } 77 | } else { 78 | println!("failed to wait for child process (wtf)"); 79 | } 80 | }); 81 | } 82 | -------------------------------------------------------------------------------- /src-tauri/src/commands/scrapers/fitgirl.rs: -------------------------------------------------------------------------------- 1 | use crate::commands::scrapers::rezi::Item; 2 | use rayon::prelude::*; 3 | use reqwest::header::CONTENT_TYPE; 4 | use scraper::{Html, Selector}; 5 | 6 | use crate::commands::logging::log_error; 7 | 8 | #[tauri::command] 9 | pub fn search_fitgirl(query: &str) -> Option> { 10 | let url = format!("https://www.fitgirl-repacks.site?s={}", query); 11 | let client = reqwest::blocking::Client::new(); 12 | let response = match client 13 | .get(url) 14 | .header(CONTENT_TYPE, "text/html") 15 | // Chrome on Windows UA 16 | .send() 17 | .map_err(|e| e.to_string()) 18 | { 19 | Ok(r) => r, 20 | Err(e) => { 21 | log_error(&e); 22 | return None; 23 | } 24 | }; 25 | 26 | let text = match response.text().map_err(|e| e.to_string()) { 27 | Ok(t) => t, 28 | Err(e) => { 29 | log_error(&e); 30 | return None; 31 | } 32 | }; 33 | 34 | let document = Html::parse_document(&text); 35 | let a_selector = Selector::parse(".entry-title a").unwrap(); 36 | 37 | // get all links and iter over them, making a new request, yada yada yada 38 | let links = document 39 | .select(&a_selector) 40 | .map(|element| element.value().attr("href").unwrap().to_string()) 41 | .collect::>(); 42 | 43 | let items = links 44 | .par_iter() 45 | .map(|link| parse_link(link.to_string()).unwrap_or(String::new())) 46 | .collect::>(); 47 | 48 | let titles = document 49 | .select(&a_selector) 50 | .map(|element| element.inner_html()) 51 | .collect::>(); 52 | 53 | // build the response vector 54 | let mut res: Vec = vec![]; 55 | 56 | for i in 0..items.len() { 57 | res.push(Item { 58 | scraper: "FitGirl".to_string(), 59 | name: titles[i].to_string(), 60 | links: vec![items[i].to_string()], 61 | }); 62 | } 63 | 64 | Some(res) 65 | } 66 | 67 | pub fn parse_link(link: String) -> Option { 68 | let client = reqwest::blocking::Client::new(); 69 | let response = match client 70 | .get(link) 71 | .header(CONTENT_TYPE, "text/html") 72 | .send() 73 | .map_err(|e| e.to_string()) 74 | { 75 | Ok(r) => r, 76 | Err(e) => { 77 | log_error(&e); 78 | return Some(String::new()); 79 | } 80 | }; 81 | 82 | let text = match response.text().map_err(|e| e.to_string()) { 83 | Ok(t) => t, 84 | Err(e) => { 85 | log_error(&e); 86 | return Some(String::new()); 87 | } 88 | }; 89 | 90 | let document = Html::parse_document(&text); 91 | let magnet_selector = Selector::parse(".entry-content li > a").unwrap(); 92 | 93 | if let Some(magnet_link) = document.select(&magnet_selector).nth(1) { 94 | if let Some(href) = magnet_link.value().attr("href") { 95 | return Some(href.to_string()); 96 | } 97 | } 98 | 99 | None 100 | } 101 | 102 | #[cfg(test)] 103 | pub mod fg_tests { 104 | use super::*; 105 | 106 | #[test] 107 | fn test_scraper() { 108 | let res = search_fitgirl("Terraria"); 109 | println!("{:?}", res); 110 | assert_eq!(res.is_some(), true); 111 | } 112 | 113 | #[test] 114 | fn test_parse_link() { 115 | let res = parse_link("https://www.fitgirl-repacks.site/terraria/".to_string()); 116 | let expected_out = "magnet:?xt=urn:btih:D131BF"; 117 | 118 | assert_eq!(res.is_some(), true); 119 | assert_eq!(res.unwrap().starts_with(expected_out), true); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src-tauri/src/commands/scrapers/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod fitgirl; 2 | pub mod rezi; 3 | -------------------------------------------------------------------------------- /src-tauri/src/commands/scrapers/rezi.rs: -------------------------------------------------------------------------------- 1 | use crate::commands::logging::log_error; 2 | use reqwest::header::{AUTHORIZATION, CONTENT_TYPE}; 3 | 4 | #[derive(serde::Serialize, Debug)] 5 | pub struct Item { 6 | pub scraper: String, 7 | pub name: String, 8 | pub links: Vec, 9 | } 10 | 11 | #[derive(serde::Serialize)] 12 | struct Payload { 13 | q: String, 14 | limit: i32, 15 | } 16 | 17 | #[derive(serde::Deserialize)] 18 | struct Response { 19 | hits: Vec, 20 | } 21 | 22 | #[derive(serde::Deserialize)] 23 | struct Hit { 24 | link: String, 25 | title: String, 26 | } 27 | 28 | #[tauri::command] 29 | pub fn search_rezi(query: &str) -> Option> { 30 | let payload = Payload { 31 | q: query.to_owned(), 32 | limit: 16, 33 | }; 34 | 35 | let payload_str = match serde_json::to_string(&payload).map_err(|e| e.to_string()) { 36 | Ok(str) => str, 37 | Err(e) => { 38 | log_error(&e); 39 | return None; 40 | } 41 | }; 42 | 43 | let client = reqwest::blocking::Client::new(); 44 | 45 | let response = match client 46 | .post("https://search.rezi.one/indexes/rezi/search") 47 | .header( 48 | AUTHORIZATION, 49 | "Bearer e2a1974678b37386fef69bb3638a1fb36263b78a8be244c04795ada0fa250d3d", 50 | ) 51 | .header(CONTENT_TYPE, "application/json") 52 | .body(payload_str) 53 | .send() 54 | .map_err(|e| e.to_string()) 55 | { 56 | Ok(r) => r, 57 | Err(e) => { 58 | log_error(&e); 59 | return None; 60 | } 61 | }; 62 | 63 | let text = match response.text().map_err(|e| e.to_string()) { 64 | Ok(t) => t, 65 | Err(e) => { 66 | log_error(&e); 67 | return None; 68 | } 69 | }; 70 | 71 | let result_json: Response = match serde_json::from_str(&text).map_err(|e| e.to_string()) { 72 | Ok(j) => j, 73 | Err(e) => { 74 | log_error(&e); 75 | return None; 76 | } 77 | }; 78 | 79 | let mut items: Vec = vec![]; 80 | 81 | for hit in result_json.hits { 82 | let links = vec![hit.link]; 83 | 84 | let res = Item { 85 | scraper: "Rezi".to_string(), 86 | name: hit.title, 87 | links, 88 | }; 89 | 90 | items.push(res); 91 | } 92 | 93 | Some(items) 94 | } 95 | 96 | #[cfg(test)] 97 | pub mod rezi_tests { 98 | use super::*; 99 | 100 | #[test] 101 | fn test_search_rezi() { 102 | let res = search_rezi("terraria"); 103 | assert!(res.unwrap().len() > 0); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr( 2 | all(not(debug_assertions), target_os = "windows"), 3 | windows_subsystem = "windows" 4 | )] 5 | 6 | mod commands; 7 | mod paths; 8 | mod startup; 9 | 10 | fn main() { 11 | env_logger::init(); 12 | 13 | // Create the usual directories if they don't exist. 14 | startup::init(); 15 | 16 | // This object is the initial tauri window 17 | // Tauri commands that can be called from the frontend are to be invoked below 18 | tauri::Builder::default() 19 | // Invoke your commands here 20 | .invoke_handler(tauri::generate_handler![ 21 | commands::file_dialog, 22 | commands::image_dialog, 23 | commands::run_game, 24 | commands::logging::log_error, 25 | commands::logging::log_warn, 26 | commands::logging::log_info, 27 | commands::database::save_to_db, 28 | commands::database::get_from_db, 29 | commands::database::edit_in_db, 30 | commands::database::delete_from_db, 31 | commands::database::wipe_library, 32 | commands::metadata::get_game_metadata, 33 | commands::metadata::download_image, 34 | commands::scrapers::rezi::search_rezi, 35 | commands::scrapers::fitgirl::search_fitgirl 36 | ]) 37 | .build(tauri::generate_context!()) 38 | .expect("error while running tauri application") 39 | .run(|_, _| {}); 40 | } 41 | -------------------------------------------------------------------------------- /src-tauri/src/migrations/1_down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS games; -------------------------------------------------------------------------------- /src-tauri/src/migrations/1_up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS games ( 2 | id INTEGER PRIMARY KEY, 3 | name TEXT NOT NULL, 4 | executable TEXT NOT NULL, 5 | description TEXT, 6 | image TEXT 7 | ); -------------------------------------------------------------------------------- /src-tauri/src/paths.rs: -------------------------------------------------------------------------------- 1 | use std::{path::PathBuf, process}; 2 | use tauri::api::path::local_data_dir; 3 | 4 | use crate::commands::logging::log_error; 5 | 6 | pub fn get_bpo() -> PathBuf { 7 | let local_dir = match local_data_dir() { 8 | Some(dir) => dir, 9 | None => { 10 | log_error("Failed to get local data dir"); 11 | process::exit(0) 12 | } 13 | }; 14 | 15 | let identifier = "io.github.blackpearlorigin"; 16 | local_dir.join(identifier) 17 | } 18 | 19 | pub fn get_db() -> PathBuf { 20 | get_bpo().join("library.db") 21 | } 22 | 23 | #[cfg(test)] 24 | mod tests { 25 | use super::*; 26 | use crate::{paths, startup}; 27 | 28 | fn setup() { 29 | startup::init(); 30 | } 31 | 32 | #[test] 33 | fn test_init() { 34 | startup::init(); 35 | } 36 | 37 | #[test] 38 | fn test_get_bpo() { 39 | setup(); 40 | let bpo = get_bpo(); 41 | assert!(bpo.exists()); 42 | std::fs::remove_dir_all(paths::get_bpo()).unwrap(); 43 | } 44 | 45 | #[test] 46 | fn test_get_db() { 47 | setup(); 48 | let db = get_db(); 49 | assert!(db.exists()); 50 | 51 | std::fs::remove_dir_all(paths::get_bpo()).unwrap(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src-tauri/src/startup.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | commands::logging::{log_error, log_info}, 3 | paths::get_bpo, 4 | }; 5 | use lazy_static::lazy_static; 6 | use rusqlite::{Connection, Result}; 7 | use rusqlite_migration::{Migrations, M}; 8 | use std::{fs, io::Write, path}; 9 | 10 | // Define migrations. These are applied atomically. 11 | lazy_static! { 12 | static ref MIGRATIONS: Migrations<'static> = 13 | Migrations::new(vec![ 14 | M::up(include_str!("migrations/1_up.sql")).down(include_str!("migrations/1_down.sql")), 15 | // In the future, if the need to change the schema arises, put 16 | // migrations below. 17 | ]); 18 | } 19 | 20 | fn setup_database(gamedb_path: &path::PathBuf) -> Result<(), rusqlite_migration::Error> { 21 | let mut conn = Connection::open(gamedb_path)?; 22 | 23 | // Update the database schema, atomically 24 | MIGRATIONS.to_latest(&mut conn) 25 | } 26 | 27 | pub fn init() { 28 | let bpo_path = get_bpo(); 29 | 30 | // Create default folders 31 | let folders = vec!["plugins", "queries", "images"]; 32 | for folder in folders { 33 | create_folder(&bpo_path.join(folder)); 34 | } 35 | 36 | let gamedb_path = bpo_path.join("library.db"); 37 | let configfile_path = bpo_path.join("config.json"); 38 | 39 | if !configfile_path.exists() { 40 | let mut file = match fs::File::create(&configfile_path) { 41 | Ok(file) => { 42 | log_info(&format!( 43 | "Successfully created file {}", 44 | &configfile_path.display() 45 | )); 46 | 47 | file 48 | } 49 | Err(e) => { 50 | panic!("[ERROR] Error while creating config file: {}", e); 51 | } 52 | }; 53 | 54 | if file.write_all(br#"{ "currentLang": "en", "updater": false, "enabledScrapers": { "rezi": true, "fitgirl": true } }"#).is_err() { 55 | log_error("Failed to write config file"); 56 | } 57 | } 58 | 59 | if !gamedb_path.exists() { 60 | if let Err(e) = fs::File::create(&gamedb_path) { 61 | log_error(&format!("Error while creating config file: {}", e)); 62 | } else { 63 | log_info(&format!( 64 | "Successfully created file {}", 65 | &gamedb_path.display() 66 | )); 67 | } 68 | } 69 | 70 | if let Err(e) = setup_database(&gamedb_path) { 71 | panic!("[ERROR] Error while creating database: {}", e) 72 | } else { 73 | log_info(&format!( 74 | "Successfully created database {}", 75 | &gamedb_path.display() 76 | )); 77 | } 78 | 79 | // Simplified function for creating directories 80 | fn create_folder(path: &path::PathBuf) { 81 | if let Err(e) = fs::create_dir_all(path) { 82 | log_error(&format!( 83 | "Error while creating folder {}: {}", 84 | &path.display(), 85 | e 86 | )); 87 | log_info("Your data may not be saved"); 88 | } else { 89 | log_info(&format!("Created folder {}", &path.display())); 90 | } 91 | } 92 | 93 | println!("Welcome to Black Pearl Origin!") 94 | } 95 | -------------------------------------------------------------------------------- /src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "beforeDevCommand": "npm run dev", 4 | "beforeBuildCommand": "npm run build", 5 | "devPath": "http://localhost:1420", 6 | "distDir": "../dist", 7 | "withGlobalTauri": false 8 | }, 9 | "package": { 10 | "productName": "Black Pearl Origin", 11 | "version": "1.2.1" 12 | }, 13 | "tauri": { 14 | "macOSPrivateApi": true, 15 | "allowlist": { 16 | "all": true, 17 | "fs": { 18 | "all": false, 19 | "readFile": true, 20 | "writeFile": true, 21 | "exists": true, 22 | "createDir": true, 23 | "readDir": false, 24 | "removeDir": false, 25 | "removeFile": true, 26 | "renameFile": false, 27 | "copyFile": false, 28 | "scope": ["$APPLOCALDATA/**/*"] 29 | }, 30 | "path": { 31 | "all": true 32 | }, 33 | "http": { 34 | "scope": [ 35 | "https://api.github.com/repos/*", 36 | "https://raw.githubusercontent.com/*" 37 | ] 38 | }, 39 | "protocol": { 40 | "all": false, 41 | "asset": true, 42 | "assetScope": ["$APPLOCALDATA/**/*", "/**/*"] 43 | } 44 | }, 45 | "systemTray": { 46 | "iconPath": "icons/icon.png", 47 | "iconAsTemplate": true 48 | }, 49 | "bundle": { 50 | "active": true, 51 | "category": "DeveloperTool", 52 | "copyright": "", 53 | "deb": { 54 | "depends": [] 55 | }, 56 | "externalBin": [], 57 | "icon": [ 58 | "icons/32x32.png", 59 | "icons/128x128.png", 60 | "icons/128x128@2x.png", 61 | "icons/icon.icns", 62 | "icons/icon.ico" 63 | ], 64 | "identifier": "io.github.blackpearlorigin", 65 | "longDescription": "", 66 | "macOS": { 67 | "entitlements": null, 68 | "exceptionDomain": "", 69 | "frameworks": [], 70 | "providerShortName": null, 71 | "signingIdentity": null 72 | }, 73 | "resources": [], 74 | "shortDescription": "", 75 | "targets": "all", 76 | "windows": { 77 | "certificateThumbprint": null, 78 | "digestAlgorithm": "sha256", 79 | "timestampUrl": "" 80 | } 81 | }, 82 | "updater": { 83 | "endpoints": [ 84 | "https://github.com/BlackPearlOrigin/updater/blob/main/endpoint.json" 85 | ], 86 | "active": true, 87 | "dialog": false, 88 | "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDVFQjVBOTU5OUY0NTgxMDkKUldRSmdVV2ZXYW0xWG1RK01ieFd1VFhPaXNFMmhuWGdPOElBNWZldWttQThVaForQTczdHlUZU4K", 89 | "windows": { 90 | "installMode": "passive" 91 | } 92 | }, 93 | "windows": [ 94 | { 95 | "fullscreen": false, 96 | "height": 720, 97 | "resizable": true, 98 | "title": "Black Pearl Origin", 99 | "width": 1280 100 | } 101 | ] 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Main.svelte: -------------------------------------------------------------------------------- 1 | 39 | 40 | 41 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 |
55 |
56 |
57 | branding 58 |
59 | 60 | 66 | 72 | 78 |
79 | 80 | 81 | 82 | 83 | 84 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 |
103 |
104 | 105 | -------------------------------------------------------------------------------- /src/Typings.d.ts: -------------------------------------------------------------------------------- 1 | // I should really put these on a typings file 2 | // Maybe it will be global, idk i might have to search 3 | // But for the meanwhile this works 4 | // - Brisolo32 5 | 6 | export interface Config { 7 | currentLang: string; 8 | } 9 | 10 | interface Links { 11 | link: string; 12 | } 13 | 14 | export interface SearchedGame { 15 | name: string; 16 | links: Links[]; 17 | scraper: string; 18 | } 19 | 20 | export interface Game { 21 | id: number; 22 | name: string; 23 | exe_path: string; 24 | description: string; 25 | image: string; 26 | } 27 | 28 | export interface IGDBData { 29 | cover_url: string; 30 | id: number; 31 | name: string; 32 | summary: string; 33 | } 34 | 35 | export interface ScraperResponseEntry { 36 | links: string[]; 37 | name: string; 38 | scraper: string; 39 | } 40 | 41 | export interface Config { 42 | currentLang: string; 43 | cssUrl: string; 44 | updater: boolean; 45 | enabledScrapers: { 46 | [key: string]: boolean; 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /src/locale/i18n.ts: -------------------------------------------------------------------------------- 1 | import { derived, writable } from 'svelte/store'; 2 | export const dict = writable(); 3 | export const locale = writable('en'); 4 | 5 | const localizedDict = derived([dict, locale], ([$dict, $locale]) => { 6 | if (!$dict || !$locale) return; 7 | return $dict[$locale]; 8 | }); 9 | 10 | const getMessageFromLocalizedDict = (id: string, localizedDict: any) => { 11 | const splitId = id.split('.'); 12 | let message = { ...localizedDict }; 13 | splitId.forEach((partialId: string | number) => { 14 | message = message[partialId]; 15 | }); 16 | 17 | return message; 18 | }; 19 | 20 | const createMessageFormatter = (localizedDict: any) => (id: any) => 21 | getMessageFromLocalizedDict(id, localizedDict); 22 | 23 | export const t = derived(localizedDict, ($localizedDict) => { 24 | return createMessageFormatter($localizedDict); 25 | }); 26 | -------------------------------------------------------------------------------- /src/locale/lang/af.json: -------------------------------------------------------------------------------- 1 | { 2 | "browseText": "Blaai", 3 | "libraryText": "Versameling", 4 | "prefsText": "Voorkeure", 5 | "loadingText": "Laai", 6 | "languageText": "Taal", 7 | "preferences": { 8 | "wipeLibrary": "Uitvee versameling", 9 | "saveText": "Stoor", 10 | "availablePlugins": "Beskikbaar inproppe", 11 | "comingSoon": "Binnekort beskikbaar...", 12 | "pluginCard": { 13 | "version": "Weergawe:", 14 | "author": "Outeur:", 15 | "desc": "Beskrywing:" 16 | }, 17 | "resetToDefault": "Reset to default", 18 | "suppressUpdater": "Suppress updater" 19 | }, 20 | "library": { 21 | "run": "Uitvoer", 22 | "searchGame": "Soek" 23 | }, 24 | "browse": { 25 | "selectPlugin": "Kies n inprop", 26 | "nothingFound": "Niks gevind nie", 27 | "downloadText": "Aflaai", 28 | "search": "Soek" 29 | }, 30 | "modals": { 31 | "newGame": { 32 | "gameTitle": "Titel", 33 | "addExec": "Voeg uitvoorbare leer by", 34 | "none": "Niks", 35 | "addImg": "Voeg beeld by", 36 | "desc": "Beskrywing", 37 | "done": "Klaar", 38 | "autoFetch": "Kry metadata" 39 | }, 40 | "fetchMeta": { 41 | "selectGame": "Kies n speletjie" 42 | }, 43 | "updater": { 44 | "updateAvailable": "Nuwe opdatering beskikbaar", 45 | "reboot": "Herbegin binne 5 sekonde..." 46 | } 47 | }, 48 | "themeText": "Temas" 49 | } 50 | -------------------------------------------------------------------------------- /src/locale/lang/ar.json: -------------------------------------------------------------------------------- 1 | { 2 | "browseText": "تصفح", 3 | "libraryText": "المكتبة", 4 | "prefsText": "الإعدادات", 5 | "loadingText": "تحميل", 6 | "languageText": "اللغة", 7 | "preferences": { 8 | "wipeLibrary": "امسح المكتبة", 9 | "saveText": "إحفظ", 10 | "availablePlugins": "الإضافات المتوفرة", 11 | "comingSoon": "قريبا...", 12 | "pluginCard": { 13 | "version": "الإصدار:", 14 | "author": "المؤلف:", 15 | "desc": "الوصف:" 16 | }, 17 | "resetToDefault": "Reset to default", 18 | "suppressUpdater": "Suppress updater" 19 | }, 20 | "library": { 21 | "run": "تشغيل", 22 | "searchGame": "إبحث" 23 | }, 24 | "browse": { 25 | "selectPlugin": "إختر إضافة", 26 | "nothingFound": "لم يتم العثور على شيء", 27 | "downloadText": "تحميل", 28 | "search": "إبحث" 29 | }, 30 | "modals": { 31 | "newGame": { 32 | "gameTitle": "العنوان", 33 | "addExec": "أضف", 34 | "none": "لا شيء", 35 | "addImg": "أضف صورة", 36 | "desc": "الوصف", 37 | "done": "انتهى", 38 | "autoFetch": "إجلب البيانات الوصفية" 39 | }, 40 | "fetchMeta": { 41 | "selectGame": "إختر لعبة" 42 | }, 43 | "updater": { 44 | "updateAvailable": "تحديث جديد متوفر", 45 | "reboot": "اعادت التشغيل في ٥ ثواني..." 46 | } 47 | }, 48 | "themeText": "الأشكال" 49 | } 50 | -------------------------------------------------------------------------------- /src/locale/lang/ba.json: -------------------------------------------------------------------------------- 1 | { 2 | "browseText": "Pretraga", 3 | "libraryText": "Datoteke", 4 | "prefsText": "Postavke", 5 | "loadingText": "Učitavanje", 6 | "languageText": "Jezik", 7 | "preferences": { 8 | "wipeLibrary": "Izbriši sve datoteke", 9 | "saveText": "Spasi", 10 | "availablePlugins": "Dostupni dodaci", 11 | "comingSoon": "Dolazi uskoro...", 12 | "pluginCard": { 13 | "version": "Verzija:", 14 | "author": "Autor:", 15 | "desc": "Opis:" 16 | }, 17 | "resetToDefault": "Reset to default", 18 | "suppressUpdater": "Suppress updater" 19 | }, 20 | "library": { 21 | "run": "Pokreni", 22 | "searchGame": "Traži" 23 | }, 24 | "browse": { 25 | "selectPlugin": "Odaberi dodatak", 26 | "nothingFound": "Ništa nije pronađeno", 27 | "downloadText": "Preuzmi", 28 | "search": "Traži" 29 | }, 30 | "modals": { 31 | "newGame": { 32 | "gameTitle": "Naziv igre", 33 | "addExec": "Dodaj .exe igre", 34 | "none": "Ništa", 35 | "addImg": "Dodaj sliku", 36 | "desc": "Opis", 37 | "done": "Završi", 38 | "autoFetch": "Nađi podatke o igri" 39 | }, 40 | "fetchMeta": { 41 | "selectGame": "Odaberi igru" 42 | }, 43 | "updater": { 44 | "updateAvailable": "Dostupna je nova verzija", 45 | "reboot": "Restartiranje programa za 5 sekundi..." 46 | } 47 | }, 48 | "themeText": "Pozadine" 49 | } 50 | -------------------------------------------------------------------------------- /src/locale/lang/cz.json: -------------------------------------------------------------------------------- 1 | { 2 | "browseText": "Hledat", 3 | "libraryText": "Knihovna", 4 | "prefsText": "Preference", 5 | "loadingText": "Načítá se", 6 | "languageText": "Jazyk", 7 | "preferences": { 8 | "wipeLibrary": "Smazat knihovnu", 9 | "saveText": "Uložit", 10 | "availablePlugins": "Dostupné pluginy", 11 | "comingSoon": "Již brzy...", 12 | "pluginCard": { 13 | "version": "Verze:", 14 | "author": "Autor:", 15 | "desc": "Popis:" 16 | }, 17 | "resetToDefault": "Obnovit do základního nastavení", 18 | "suppressUpdater": "Potlačit aktualizátor" 19 | }, 20 | "library": { 21 | "run": "Načíst", 22 | "searchGame": "Hledat" 23 | }, 24 | "browse": { 25 | "nothingFound": "Nic nebylo nalezeno", 26 | "downloadText": "Stáhnout (DDL)", 27 | "downloadTextMagnet": "Stáhnout (Magnet)", 28 | "search": "Hledat" 29 | }, 30 | "modals": { 31 | "newGame": { 32 | "gameTitle": "Název hry", 33 | "addExec": "Přidat spustitelný soubor", 34 | "none": "Bez", 35 | "addImg": "Přidat obrázek", 36 | "desc": "Popis", 37 | "done": "Hotovo", 38 | "autoFetch": "Načíst metadata" 39 | }, 40 | "fetchMeta": { 41 | "selectGame": "Vyberte hru." 42 | }, 43 | "updater": { 44 | "updateAvailable": "Dostupný nový update.", 45 | "reboot": "Restart za 5 sekund..." 46 | } 47 | }, 48 | "themeText": "Témata" 49 | } 50 | -------------------------------------------------------------------------------- /src/locale/lang/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "browseText": "Stöbern", 3 | "libraryText": "Bibliothek", 4 | "prefsText": "Einstellungen", 5 | "loadingText": "Lädt", 6 | "languageText": "Sprache", 7 | "preferences": { 8 | "installPlugin": "Ein Plug-in installieren", 9 | "wipeLibrary": "Bibliothek löschen", 10 | "saveText": "Speichern", 11 | "availablePlugins": "Verfügbare Plug-ins", 12 | "comingSoon": "Kommt bald...", 13 | "pluginCard": { 14 | "version": "Version:", 15 | "author": "Autor:", 16 | "desc": "Beschreibung:" 17 | }, 18 | "resetToDefault": "Zurücksetzen", 19 | "suppressUpdater": "Updater stummschalten" 20 | }, 21 | "library": { 22 | "run": "Ausführen", 23 | "searchGame": "Suchen" 24 | }, 25 | "browse": { 26 | "selectPlugin": "Plug-in auswählen", 27 | "nothingFound": "Nichts gefunden", 28 | "downloadText": "Herunterladen", 29 | "search": "Suchen" 30 | }, 31 | "modals": { 32 | "newGame": { 33 | "gameTitle": "Titel", 34 | "addExec": "Anwendung hinzufügen", 35 | "none": "Ohne", 36 | "addImg": "Bild hinzufügen", 37 | "desc": "Beschreibung", 38 | "done": "Fertig", 39 | "autoFetch": "Metadaten erhalten" 40 | }, 41 | "fetchMeta": { 42 | "selectGame": "Spiel auswählen" 43 | }, 44 | "updater": { 45 | "updateAvailable": "Neues Update verfügbar", 46 | "reboot": "Neustart in 5 Sekunden..." 47 | } 48 | }, 49 | "pluginText": "Plug-ins", 50 | "themeText": "Thema" 51 | } 52 | -------------------------------------------------------------------------------- /src/locale/lang/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "browseText": "Browse", 3 | "libraryText": "Library", 4 | "prefsText": "Preferences", 5 | "loadingText": "Loading", 6 | "languageText": "Language", 7 | "preferences": { 8 | "wipeLibrary": "Wipe library", 9 | "saveText": "Save", 10 | "availablePlugins": "Available plugins", 11 | "comingSoon": "Coming Soon...", 12 | "pluginCard": { 13 | "version": "Version:", 14 | "author": "Author:", 15 | "desc": "Description:" 16 | }, 17 | "resetToDefault": "Reset to default", 18 | "suppressUpdater": "Suppress updater" 19 | }, 20 | "library": { 21 | "run": "Run", 22 | "searchGame": "Search" 23 | }, 24 | "browse": { 25 | "selectPlugin": "Select a plugin", 26 | "nothingFound": "Nothing Found", 27 | "downloadText": "Download", 28 | "downloadTextMagnet": "Download (Magnet)", 29 | "search": "Search" 30 | }, 31 | "modals": { 32 | "newGame": { 33 | "gameTitle": "Title", 34 | "addExec": "Add executable", 35 | "none": "None", 36 | "addImg": "Add image", 37 | "desc": "Description", 38 | "done": "Done", 39 | "autoFetch": "Fetch metadata" 40 | }, 41 | "fetchMeta": { 42 | "selectGame": "Select a game" 43 | }, 44 | "updater": { 45 | "updateAvailable": "New update available.", 46 | "reboot": "Rebooting in 5 seconds" 47 | } 48 | }, 49 | "themeText": "Themes" 50 | } 51 | -------------------------------------------------------------------------------- /src/locale/lang/eo.json: -------------------------------------------------------------------------------- 1 | { 2 | "browseText": "Foliumi", 3 | "libraryText": "Biblioteko", 4 | "prefsText": "Preferoj", 5 | "loadingText": "Ŝarĝante", 6 | "languageText": "Lingvo", 7 | "preferences": { 8 | "wipeLibrary": "Viŝu bibliotekon", 9 | "saveText": "Ŝpari", 10 | "availablePlugins": "Haveblaj kromprogramon", 11 | "comingSoon": "Baldaŭ...", 12 | "pluginCard": { 13 | "version": "Versio:", 14 | "author": "Aŭtoro:", 15 | "desc": "Priskribo:" 16 | }, 17 | "resetToDefault": "Reset to default", 18 | "suppressUpdater": "Suppress updater" 19 | }, 20 | "library": { 21 | "run": "Kuri", 22 | "searchGame": "Serĉu" 23 | }, 24 | "browse": { 25 | "selectPlugin": "Elektu kromprogramon", 26 | "nothingFound": "Neniuj ludoj trovitaj", 27 | "downloadText": "Elŝuto", 28 | "search": "Serĉu" 29 | }, 30 | "modals": { 31 | "newGame": { 32 | "gameTitle": "Titolo", 33 | "addExec": "Aldonu rueblan", 34 | "none": "Neniu", 35 | "addImg": "Aldonu bildo", 36 | "desc": "Priskribo", 37 | "done": "Farita", 38 | "autoFetch": "Elpeti metadatenojn" 39 | }, 40 | "fetchMeta": { 41 | "selectGame": "Elektu ludo" 42 | }, 43 | "updater": { 44 | "updateAvailable": "Nova ĝisdatigo disponebla", 45 | "reboot": "Rekomenco post 5 sekundoj..." 46 | } 47 | }, 48 | "themeText": "Temoj" 49 | } 50 | -------------------------------------------------------------------------------- /src/locale/lang/hr.json: -------------------------------------------------------------------------------- 1 | { 2 | "browseText": "Pretražuj", 3 | "libraryText": "Datoteke", 4 | "prefsText": "Postavke", 5 | "loadingText": "Učitavanje", 6 | "languageText": "Jezik", 7 | "preferences": { 8 | "wipeLibrary": "Izbriši sve datoteke", 9 | "saveText": "Spasi", 10 | "availablePlugins": "Dostupna proširenja", 11 | "comingSoon": "Dolazi uskoro...", 12 | "pluginCard": { 13 | "version": "Verzija:", 14 | "author": "Autor:", 15 | "desc": "Opis:" 16 | }, 17 | "resetToDefault": "Reset to default", 18 | "suppressUpdater": "Suppress updater" 19 | }, 20 | "library": { 21 | "run": "Pokreni", 22 | "searchGame": "Pretraži" 23 | }, 24 | "browse": { 25 | "selectPlugin": "Odaberi proširenje", 26 | "nothingFound": "Nije pronađena nijedna igra", 27 | "downloadText": "Preuzmi", 28 | "search": "Pretraži" 29 | }, 30 | "modals": { 31 | "newGame": { 32 | "gameTitle": "Naslov igre", 33 | "addExec": "Dodaj komandu", 34 | "none": "Ništa", 35 | "addImg": "Dodaj sliku", 36 | "desc": "Opis", 37 | "done": "Završi", 38 | "autoFetch": "Dobavi podatke o igri" 39 | }, 40 | "fetchMeta": { 41 | "selectGame": "Odaberi igru" 42 | }, 43 | "updater": { 44 | "updateAvailable": "Dostupna je nova verzija", 45 | "reboot": "Ponovno pokretanje za 5 sekundi..." 46 | } 47 | }, 48 | "themeText": "Pozadine" 49 | } 50 | -------------------------------------------------------------------------------- /src/locale/lang/lt.json: -------------------------------------------------------------------------------- 1 | { 2 | "browseText": "Naršyti", 3 | "libraryText": "Biblioteka", 4 | "prefsText": "Pasirinkimai", 5 | "loadingText": "Kraunama", 6 | "languageText": "Kalba", 7 | "preferences": { 8 | "wipeLibrary": "Išvalyti biblioteka", 9 | "saveText": "Išsaugoti", 10 | "availablePlugins": "Pasiekiami priedai", 11 | "comingSoon": "Netrukus pasirodys...", 12 | "pluginCard": { 13 | "version": "Versija:", 14 | "author": "Autorius:", 15 | "desc": "Aprašymas:" 16 | }, 17 | "resetToDefault": "Reset to default", 18 | "suppressUpdater": "Suppress updater" 19 | }, 20 | "library": { 21 | "run": "Atidaryti", 22 | "searchGame": "Ieškoti" 23 | }, 24 | "browse": { 25 | "selectPlugin": "Pasirinkti priedą", 26 | "nothingFound": "Nieko nerasta", 27 | "downloadText": "Atsiųsti", 28 | "search": "Ieškoti" 29 | }, 30 | "modals": { 31 | "newGame": { 32 | "gameTitle": "Pavadinimas", 33 | "addExec": "Pridėti Paleisties failą", 34 | "none": "Nė vienas", 35 | "addImg": "Pridėti nuotrauka", 36 | "desc": "Aprašymas", 37 | "done": "Baigta", 38 | "autoFetch": "Gauti metadatą" 39 | }, 40 | "fetchMeta": { 41 | "selectGame": "Pasirinkite žaidimą" 42 | }, 43 | "updater": { 44 | "updateAvailable": "Pasiekiamas naujas atnaujinimas", 45 | "reboot": "Persikraunama po 5 sekundžių..." 46 | } 47 | }, 48 | "themeText": "Temos" 49 | } 50 | -------------------------------------------------------------------------------- /src/locale/lang/pl.json: -------------------------------------------------------------------------------- 1 | { 2 | "browseText": "Przeglądaj", 3 | "libraryText": "Biblioteka", 4 | "prefsText": "Preferencje", 5 | "loadingText": "Ładowanie", 6 | "languageText": "Język", 7 | "preferences": { 8 | "wipeLibrary": "Wyczyść biblioteke", 9 | "saveText": "Zapisz", 10 | "availablePlugins": "Dostępne rozszerzenia", 11 | "comingSoon": "Wkrótce...", 12 | "pluginCard": { 13 | "version": "Wersja:", 14 | "author": "Autor:", 15 | "desc": "Opis:" 16 | }, 17 | "resetToDefault": "Reset to default", 18 | "suppressUpdater": "Suppress updater" 19 | }, 20 | "library": { 21 | "run": "Start", 22 | "searchGame": "Wyszukaj" 23 | }, 24 | "browse": { 25 | "selectPlugin": "Zaznacz rozszerzenie", 26 | "nothingFound": "Nic nie znaleziono", 27 | "downloadText": "Pobierz", 28 | "search": "Wyszukaj" 29 | }, 30 | "modals": { 31 | "newGame": { 32 | "gameTitle": "Tytuł", 33 | "addExec": "Dodaj plik wykonawczy", 34 | "none": "Brak", 35 | "addImg": "Dodaj obraz", 36 | "desc": "Opis", 37 | "done": "Gotowe", 38 | "autoFetch": "Pobierz metadane" 39 | }, 40 | "fetchMeta": { 41 | "selectGame": "Zaznacz gre" 42 | }, 43 | "updater": { 44 | "updateAvailable": "Dostępna nowa aktualizacja", 45 | "reboot": "Restart za 5 sekund..." 46 | } 47 | }, 48 | "themeText": "Style" 49 | } 50 | -------------------------------------------------------------------------------- /src/locale/lang/pt-BR.json: -------------------------------------------------------------------------------- 1 | { 2 | "browseText": "Buscar", 3 | "libraryText": "Biblioteca", 4 | "prefsText": "Configurações", 5 | "loadingText": "Carregando", 6 | "languageText": "Idioma", 7 | "preferences": { 8 | "wipeLibrary": "Apagar biblioteca", 9 | "saveText": "Salvar", 10 | "availablePlugins": "Plugins disponiveis", 11 | "comingSoon": "Em breve...", 12 | "pluginCard": { 13 | "version": "Versão:", 14 | "author": "Autor:", 15 | "desc": "Descrição:" 16 | }, 17 | "resetToDefault": "Restaurar ao padrão", 18 | "suppressUpdater": "Desativar atualizador" 19 | }, 20 | "library": { 21 | "run": "Executar", 22 | "searchGame": "Buscar" 23 | }, 24 | "browse": { 25 | "selectPlugin": "Selecione um Plugin", 26 | "nothingFound": "Nada foi encontrado", 27 | "downloadText": "Download", 28 | "search": "Buscar" 29 | }, 30 | "modals": { 31 | "newGame": { 32 | "gameTitle": "Título", 33 | "addExec": "Adicionar executavel", 34 | "none": "Nada", 35 | "addImg": "Adicionar Imagem", 36 | "desc": "Descrição", 37 | "done": "OK", 38 | "autoFetch": "Busque metadados" 39 | }, 40 | "fetchMeta": { 41 | "selectGame": "Selecione um jogo" 42 | }, 43 | "updater": { 44 | "updateAvailable": "Nova atualização disponivel", 45 | "reboot": "Reiniciando em 5 segundos..." 46 | } 47 | }, 48 | "themeText": "Temas" 49 | } 50 | -------------------------------------------------------------------------------- /src/locale/lang/sr.json: -------------------------------------------------------------------------------- 1 | { 2 | "browseText": "Претрага", 3 | "libraryText": "Датотеке", 4 | "prefsText": "Поставке", 5 | "loadingText": "Учитавање", 6 | "languageText": "Језик", 7 | "preferences": { 8 | "wipeLibrary": "Избриши датотеку", 9 | "saveText": "Сачувај", 10 | "availablePlugins": "Доступни додаци", 11 | "comingSoon": "Долази ускоро...", 12 | "pluginCard": { 13 | "version": "Верзија:", 14 | "author": "Аутор:", 15 | "desc": "Опис:" 16 | }, 17 | "resetToDefault": "Reset to default", 18 | "suppressUpdater": "Suppress updater" 19 | }, 20 | "library": { 21 | "run": "Покрени", 22 | "searchGame": "Претражи" 23 | }, 24 | "browse": { 25 | "selectPlugin": "Одабери додатак", 26 | "nothingFound": "Ниједна игра није пронађена", 27 | "downloadText": "Преузми", 28 | "search": "Претражи" 29 | }, 30 | "modals": { 31 | "newGame": { 32 | "gameTitle": "Наслов игре", 33 | "addExec": "Додај извршну датотеку", 34 | "none": "Ништа", 35 | "addImg": "Додај слику", 36 | "desc": "Опис", 37 | "done": "Заврши", 38 | "autoFetch": "Пренеси податке о игри" 39 | }, 40 | "fetchMeta": { 41 | "selectGame": "Одабери игру" 42 | }, 43 | "updater": { 44 | "updateAvailable": "Доступна је нова верзија", 45 | "reboot": "Поновно покретање за 5 секунди..." 46 | } 47 | }, 48 | "themeText": "Позадине" 49 | } 50 | -------------------------------------------------------------------------------- /src/locale/languages.json: -------------------------------------------------------------------------------- 1 | [ 2 | "English (US)", 3 | "Português (Brasil)", 4 | "Deutsch", 5 | "العربية", 6 | "Hrvatski", 7 | "Bosanski", 8 | "Српски", 9 | "Lietuvių", 10 | "Afrikaans", 11 | "Esperanto", 12 | "Polski", 13 | "Čeština" 14 | ] 15 | -------------------------------------------------------------------------------- /src/locale/locales.ts: -------------------------------------------------------------------------------- 1 | import portugueseBR from './lang/pt-BR.json'; 2 | import englishUS from './lang/en.json'; 3 | import germanDE from './lang/de.json'; 4 | import arabicAR from './lang/ar.json'; 5 | import croatianHR from './lang/hr.json'; 6 | import bosnianBA from './lang/ba.json'; 7 | import serbianSR from './lang/sr.json'; 8 | import lithuaninanLT from './lang/lt.json'; 9 | import afrikaansAF from './lang/af.json'; 10 | import esperantoEO from './lang/eo.json'; 11 | import polishPL from './lang/pl.json'; 12 | import czechCZ from './lang/cz.json'; 13 | 14 | export default { 15 | en: englishUS, 16 | 'pt-BR': portugueseBR, 17 | de: germanDE, 18 | ar: arabicAR, 19 | hr: croatianHR, 20 | ba: bosnianBA, 21 | sr: serbianSR, 22 | lt: lithuaninanLT, 23 | af: afrikaansAF, 24 | eo: esperantoEO, 25 | pl: polishPL, 26 | cz: czechCZ, 27 | }; 28 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | //! DO NOT FUCKING EDIT THIS FILE 2 | import './styles/Global.scss'; 3 | import Main from './Main.svelte'; 4 | 5 | const app = new Main({ 6 | target: document.getElementById('app'), 7 | }); 8 | 9 | export default app; 10 | -------------------------------------------------------------------------------- /src/routes/Browse.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 26 | 27 | 32 | 39 | 40 |
41 |
42 | 52 | 53 | 58 | {#if searchData.length === 0} 59 |

{$t('browse.nothingFound')}

60 | {/if} 61 | {#each searchData as ScraperResponse} 62 | {#each ScraperResponse as Response} 63 |
64 |

{Response.name}

65 | {#each Response.links as url} 66 | 67 | 68 | {url.link.toString().includes('magnet:') 69 | ? $t('browse.downloadTextMagnet') 70 | : $t('browse.downloadText')} 71 | 72 | {Response.scraper} 73 | {/each} 74 |
75 | {/each} 76 | {/each} 77 |
78 |
79 | -------------------------------------------------------------------------------- /src/routes/Library.svelte: -------------------------------------------------------------------------------- 1 | 69 | 70 |
71 |
72 |
73 | 74 | 77 | 83 |
84 | 85 | 86 | 87 | 88 | 89 |
90 | {#await games then data} 91 | {#each getFilteredGames(data, query) as game} 92 |
93 |
94 | 113 | 122 |
123 |
124 | {/each} 125 | 126 |
127 | 128 | 133 | {#if gameModalOpened} 134 | 135 | {gameOnModal.name} 136 | 137 | 138 | {gameOnModal.name} 147 | 148 | 156 |

157 | {gameOnModal.description} 158 |

159 | 160 |
161 | 171 | 185 |
186 | {/if} 187 |
188 |
189 | {:catch error} 190 |

{error.message}

191 | {/await} 192 |
193 |
194 |
195 | -------------------------------------------------------------------------------- /src/routes/Preferences.svelte: -------------------------------------------------------------------------------- 1 | 41 | 42 |
43 |
44 |
45 |
46 |
47 | {$t('themeText')} 48 | 49 |
50 | 51 |
52 | 58 | 59 | 65 |
66 |
67 | 68 |
69 |
70 | Updater 71 | 72 |
73 | 74 |
75 | 76 |

{$t('preferences.suppressUpdater')}

77 |
78 |
79 | 80 |
81 |
82 | {$t('languageText')} 83 | 84 |
85 |
86 | 93 |
94 |
95 | 96 |
97 |
98 | Save 99 | 100 |
101 | 102 |
103 | 117 |
118 |
119 | 120 |
121 |
122 | Scrapers 123 | 124 |
125 |
126 | 130 |

Rezi

131 |
132 |
133 | 137 |

FitGirl

138 |
139 |
140 |
141 |
142 |
143 | -------------------------------------------------------------------------------- /src/routes/modals/NewGame.svelte: -------------------------------------------------------------------------------- 1 | 68 | 69 | 195 | -------------------------------------------------------------------------------- /src/routes/modals/Toast.svelte: -------------------------------------------------------------------------------- 1 | 29 | 30 |
31 |

{$t('modals.updater.updateAvailable')}

32 | 35 |
36 | -------------------------------------------------------------------------------- /src/scripts/Browse.ts: -------------------------------------------------------------------------------- 1 | import { invoke } from '@tauri-apps/api/tauri'; 2 | import { log } from './Main'; 3 | import type { ScraperResponseEntry } from 'src/Typings'; 4 | import { getConfig } from './Main'; 5 | 6 | /** 7 | * Typescript Function -> Rust Function 8 | * - Runs the search function passing the 9 | * same arguments from the TS function to the Rust 10 | * function 11 | * 12 | * @param {string} query 13 | * @returns {Promise} Array of SearchedGame 14 | */ 15 | export const searchGame = async ( 16 | query: string 17 | ): Promise => { 18 | if (query === '') { 19 | log(1, 'No query entered'); 20 | return null; 21 | } 22 | 23 | const config = await getConfig(); 24 | const enabledScrapers = config.enabledScrapers; 25 | 26 | const results = []; 27 | 28 | switch (true) { 29 | case enabledScrapers.rezi: 30 | const reziData = await invoke('search_rezi', { 31 | query: `${query}`, 32 | }).catch((e) => { 33 | log(0, `Failed to search game. Error: ${e}`); 34 | }); 35 | results.push(reziData); 36 | 37 | case enabledScrapers.fitgirl: 38 | const fgData = await invoke('search_fitgirl', { 39 | query: `${query}`, 40 | }).catch((e) => { 41 | log(0, `Failed to search game. Error: ${e}`); 42 | }); 43 | results.push(fgData); 44 | } 45 | 46 | if (results.length === 0) { 47 | log(1, 'No results found'); 48 | return null; 49 | } 50 | 51 | return results as ScraperResponseEntry[][]; 52 | }; 53 | -------------------------------------------------------------------------------- /src/scripts/Library.ts: -------------------------------------------------------------------------------- 1 | import { invoke } from '@tauri-apps/api/tauri'; 2 | import type { Game, IGDBData } from 'src/Typings'; 3 | 4 | /** 5 | * Typescript Function -> Rust Function 6 | * - Starts the game on the path argument 7 | * 8 | * @param {string} path 9 | * @returns {Promise} Nothing 10 | */ 11 | export const runGame = async (path: string): Promise => 12 | await invoke('run_game', { path: path }).catch(() => { 13 | invoke('log', { 14 | logLevel: 0, 15 | logMessage: 'Failed to invoke function "run_game"', 16 | }); 17 | }); 18 | 19 | /** 20 | * Typescript Function -> Rust Function 21 | * - Deletes the game from the DB using the id 22 | * 23 | * @param {number} id 24 | * @returns {Promise} Nothing 25 | */ 26 | export const deleteGame = async (id: number): Promise => { 27 | await invoke('delete_from_db', { id: id }).catch(() => { 28 | invoke('log', { 29 | logLevel: 0, 30 | logMessage: 'Failed delete from db', 31 | }); 32 | }); 33 | }; 34 | 35 | /** 36 | * Typescript Function -> Rust Function 37 | * - Gets all games from the DB and returns it 38 | * as an Array of objects 39 | * 40 | * @returns {Promise} An array of games object 41 | */ 42 | export const getGames = async (): Promise => { 43 | const games = await invoke('get_from_db') 44 | .then((data) => { 45 | return data; 46 | }) 47 | .catch((error) => { 48 | invoke('log', { 49 | logLevel: 0, 50 | logMessage: 'Failed to get games', 51 | }); 52 | return error; 53 | }); 54 | return games; 55 | }; 56 | 57 | /** 58 | * Typescript Function -> Rust Function 59 | * - Saves game on DB 60 | * 61 | * @param title 62 | * @param executablePath 63 | * @param description 64 | * @param imagePath 65 | * @returns {Promise} Nothing 66 | */ 67 | export const saveData = async ( 68 | title: string, 69 | executablePath: string, 70 | description: string, 71 | imagePath: string 72 | ): Promise => { 73 | await invoke('save_to_db', { 74 | title: title, 75 | exePath: executablePath, 76 | description: description, 77 | image: imagePath, 78 | }).catch((error) => { 79 | invoke('log', { 80 | logLevel: 0, 81 | logMessage: 'Failed to save data: ' + error, 82 | }); 83 | }); 84 | }; 85 | 86 | /** 87 | * Typescript Function -> Rust Function 88 | * - Edits games in DB based on the id 89 | * 90 | * @param {number} id 91 | * @param {string} title 92 | * @param {string} executablePath 93 | * @param {string} description 94 | * @param {string} imagePath 95 | * @returns {Promise} Nothing 96 | */ 97 | export const editData = async ( 98 | id: number, 99 | title: string, 100 | executablePath: string, 101 | description: string, 102 | imagePath: string 103 | ): Promise => { 104 | await invoke('edit_in_db', { 105 | id: id, 106 | name: title, 107 | executable: executablePath, 108 | description: description, 109 | image: imagePath, 110 | }).catch((error) => { 111 | invoke('log', { 112 | logLevel: 0, 113 | logMessage: 'Failed to edit game: ' + error, 114 | }); 115 | }); 116 | }; 117 | 118 | /** 119 | * Typescript Function 120 | * - Filters games based on an array given and query params 121 | * 122 | * @param games 123 | * @param gameToSearch 124 | * @returns {Game[]} An array of type Game[] 125 | */ 126 | export const getFilteredGames = ( 127 | games: Game[], 128 | gameToSearch?: string 129 | ): Game[] => { 130 | if (typeof gameToSearch !== 'undefined') { 131 | return games.filter((game) => { 132 | return game.name.toLowerCase().includes(gameToSearch.toLowerCase()); 133 | }); 134 | } else { 135 | return games; 136 | } 137 | }; 138 | 139 | /** 140 | * Typescript Functiom 141 | * - Runs the function inside the params 142 | * 143 | * @param {VoidFunction} operation 144 | * @returns {Promise} Nothing 145 | */ 146 | export const operationHandler = async (operation: () => void): Promise => 147 | operation(); 148 | 149 | export const getGameMetadata = async (gameName: string) => { 150 | const gameMeta: unknown = await invoke('get_game_metadata', { 151 | name: gameName, 152 | }); 153 | 154 | return gameMeta as IGDBData[]; 155 | }; 156 | 157 | export const downloadImage = async (imageURL: string) => { 158 | const imageFile = await invoke('download_image', { url: imageURL }); 159 | 160 | return imageFile as string; 161 | }; 162 | -------------------------------------------------------------------------------- /src/scripts/Main.ts: -------------------------------------------------------------------------------- 1 | import { readTextFile, BaseDirectory } from '@tauri-apps/api/fs'; 2 | import { invoke } from '@tauri-apps/api/tauri'; 3 | import { locale } from '../locale/i18n'; 4 | import { switchTheme } from './Preferences'; 5 | import type { Config } from 'src/Typings'; 6 | 7 | // TS Function 8 | // - Gets the current locale from config.json 9 | export async function getConfig() { 10 | const config: string = await readTextFile('config.json', { 11 | dir: BaseDirectory.AppLocalData, 12 | }).catch(() => { 13 | log(0, 'Failed to read config.json'); 14 | return ''; 15 | }); 16 | 17 | let configParsed = JSON.parse(config); 18 | return configParsed as Config; 19 | } 20 | 21 | // TS Function 22 | // - Loads the current locale 23 | export async function loadLocale() { 24 | const config = await getConfig(); 25 | locale.set(config.currentLang); 26 | switchTheme(config.cssUrl); 27 | } 28 | 29 | // TS Function -> Rust Function 30 | // - Logs a message to the Rust backend 31 | export function log(logLevel: number, logMessage: string) { 32 | switch (logLevel) { 33 | case 0: 34 | invoke('log_error', { 35 | msg: `From TS: ${logMessage}`, 36 | }); 37 | break; 38 | case 1: 39 | invoke('log_warn', { 40 | msg: `From TS: ${logMessage}`, 41 | }); 42 | break; 43 | case 2: 44 | invoke('log_info', { 45 | msg: `From TS: ${logMessage}`, 46 | }); 47 | break; 48 | default: 49 | invoke('log_info', { 50 | msg: `From TS: ${logMessage}`, 51 | }); 52 | break; 53 | } 54 | } 55 | 56 | // Defines a function that checks if the same string is empty 57 | export const isEmpty = (string: string) => { 58 | return string === undefined || string.length === 0 || !string.trim(); 59 | }; 60 | -------------------------------------------------------------------------------- /src/scripts/Preferences.ts: -------------------------------------------------------------------------------- 1 | import { invoke } from '@tauri-apps/api/tauri'; 2 | import { ask, message } from '@tauri-apps/api/dialog'; 3 | import { BaseDirectory, writeTextFile } from '@tauri-apps/api/fs'; 4 | import { isEmpty } from './Main'; 5 | import { log } from './Main'; 6 | 7 | /** 8 | * Typescript Function -> Rust Function 9 | * - Opens a pop-up window, then if the user selects yes, 10 | * wipes the library 11 | * 12 | * @returns {Promise} Nothing 13 | */ 14 | export const wipeLibrary = async (): Promise => { 15 | const areYouSure = await ask( 16 | 'Are you sure? This action can not be undone.', 17 | 'Library Deletion' 18 | ); 19 | if (areYouSure) { 20 | invoke('wipe_library'); 21 | await message('Library successfully deleted', 'Library Deletion'); 22 | } 23 | }; 24 | 25 | /** 26 | * Typescript Function 27 | * - Saves the selected language to config.json 28 | * 29 | * @param {string} lang 30 | */ 31 | export const saveData = async ( 32 | lang: string, 33 | updaterToggle: boolean, 34 | cssUrl: string, 35 | selectedScrapers: { [key: string]: boolean } 36 | ): Promise => { 37 | let dataObj = { 38 | currentLang: lang, 39 | updater: updaterToggle, 40 | cssUrl: cssUrl, 41 | enabledScrapers: selectedScrapers, 42 | }; 43 | 44 | switchTheme(cssUrl); 45 | 46 | let dataObjString = JSON.stringify(dataObj); 47 | 48 | await writeTextFile('config.json', dataObjString, { 49 | dir: BaseDirectory.AppLocalData, 50 | }) 51 | .catch((e) => { 52 | log(0, `Failed to write config. Error: ${e}`); 53 | 54 | return ''; 55 | }) 56 | .then(() => { 57 | log(2, 'Successfully wrote config file'); 58 | }); 59 | }; 60 | 61 | export const loadData = async (): Promise => {}; 62 | 63 | /* 64 | * Typescript Function 65 | * - Theme switcher poggers 66 | */ 67 | export const switchTheme = (cssUrl: string) => { 68 | if (isEmpty(cssUrl)) return; 69 | 70 | let head = document.getElementsByTagName('head')[0]; 71 | // let link = document.createElement('link'); 72 | 73 | let link = getOrCreateElement('custom-stylesheet'); 74 | 75 | link.setAttribute('href', cssUrl); 76 | link.setAttribute('type', 'text/css'); 77 | link.setAttribute('rel', 'stylesheet'); 78 | 79 | head.appendChild(link); 80 | }; 81 | 82 | export const resetTheme = async () => { 83 | let head = document.getElementsByTagName('head')[0]; 84 | let child = document.getElementById('custom-stylesheet'); 85 | 86 | // now we kill the children 87 | head.removeChild(child); 88 | }; 89 | 90 | const getOrCreateElement = (id: string) => { 91 | // Check if the element already exists in the DOM 92 | const element = document.getElementById(id); 93 | if (element) { 94 | // The element exists, return a reference to it 95 | return element; 96 | } else { 97 | // The element doesn't exist, create a new one 98 | const newElement = document.createElement('link'); 99 | newElement.setAttribute('id', id); 100 | return newElement; 101 | } 102 | }; 103 | -------------------------------------------------------------------------------- /src/styles/Global.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap'); 2 | @import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap'); 3 | 4 | @import 'Browse', 'Library', 'Modal', 'Preferences'; 5 | 6 | :root { 7 | font-family: 'Montserrat'; 8 | font-size: 16px; 9 | line-height: 24px; 10 | font-weight: 400; 11 | 12 | font-synthesis: none; 13 | text-rendering: optimizeLegibility; 14 | -webkit-font-smoothing: antialiased; 15 | -moz-osx-font-smoothing: grayscale; 16 | -webkit-text-size-adjust: 100%; 17 | 18 | --toastBackground: #171717; 19 | } 20 | 21 | :focus { 22 | outline: none; 23 | } 24 | 25 | body { 26 | background-size: 100%; 27 | background-color: #171717; 28 | color: rgb(255, 255, 255); 29 | overflow-x: hidden; 30 | width: 100%; 31 | height: 100vh; 32 | margin-top: 0; 33 | margin-bottom: 0; 34 | } 35 | 36 | .sidenav { 37 | height: 100%; 38 | width: 190px; 39 | position: fixed; 40 | z-index: 1; 41 | top: 0; 42 | left: 0; 43 | overflow-x: hidden; 44 | padding-top: 20px; 45 | background-color: #101010; 46 | 47 | .menu-item { 48 | margin: 3px; 49 | margin-bottom: 10px; 50 | 51 | .menu-button { 52 | height: 30px; 53 | max-height: 30px; 54 | font-family: 'Montserrat', monospace; 55 | padding: 4px; 56 | text-decoration: none; 57 | font-size: 17px; 58 | color: #fff; 59 | transition: all 0.25s; 60 | border-style: none; 61 | 62 | svg { 63 | position: relative; 64 | top: 4px; 65 | padding-left: 10px; 66 | } 67 | 68 | .link { 69 | display: inline-block; 70 | width: 100px; 71 | height: 38px; 72 | padding-left: 5px; 73 | font-family: 'Montserrat', monospace; 74 | text-decoration: none; 75 | font-size: 17px; 76 | color: #fff; 77 | } 78 | 79 | &:hover { 80 | margin: inherit; 81 | cursor: pointer; 82 | } 83 | } 84 | } 85 | 86 | .branding { 87 | display: inline-flex; 88 | margin: 0px; 89 | width: 100%; 90 | 91 | justify-content: center; 92 | align-items: center; 93 | 94 | img { 95 | margin-bottom: 25px; 96 | } 97 | } 98 | } 99 | 100 | .main { 101 | margin-left: 176px; 102 | font-size: 16px; 103 | margin-right: 15px; 104 | } 105 | -------------------------------------------------------------------------------- /src/styles/_Browse.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --accent-color: #00ff00; 3 | } 4 | 5 | .search { 6 | display: flex; 7 | height: 69px; 8 | 9 | input { 10 | margin: 0 0.5rem 2rem 0; 11 | color: #dddddd; 12 | font-size: 1rem; 13 | padding: 8px 8px 8px 8px; 14 | background-color: #121212; 15 | border-radius: 4px; 16 | border: 1px solid #323232; 17 | box-shadow: 0px 2px 8px #00000055; 18 | 19 | flex-grow: 1; 20 | 21 | &[type='text']:focus { 22 | outline-style: none; 23 | } 24 | 25 | &[type='text']::placeholder { 26 | color: #dddddd; 27 | } 28 | } 29 | 30 | button { 31 | margin: 0 8px 2rem 0; 32 | padding: 8px 32px; 33 | background-color: #121212; 34 | border: 1px solid #323232; 35 | border-radius: 4px; 36 | box-shadow: 0px 2px 8px #00000055; 37 | 38 | cursor: pointer; 39 | 40 | i { 41 | color: #dddddd; 42 | font-size: 1.2rem; 43 | } 44 | 45 | &:hover { 46 | text-shadow: none; 47 | } 48 | } 49 | } 50 | 51 | .noresults { 52 | display: flex; 53 | text-align: center; 54 | height: auto; 55 | justify-content: center; 56 | font-weight: 100; 57 | color: #cccccc; 58 | } 59 | 60 | .game { 61 | display: block; 62 | 63 | border-style: solid; 64 | border-width: 1px; 65 | border-color: #323232; 66 | background-color: #0b0e10; 67 | margin: 10px 8px 0px 2px; 68 | padding: 5px 5px 20px 8px; 69 | border-radius: 4px; 70 | 71 | line-height: 10px; 72 | 73 | p { 74 | margin-left: 6px; 75 | margin-top: 8px; 76 | font-size: 19px; 77 | font-weight: 700; 78 | line-height: 1.4rem; 79 | } 80 | 81 | a { 82 | text-decoration: none; 83 | 84 | border-style: solid; 85 | background-color: inherit; 86 | 87 | color: #dddddd; 88 | margin: 0 5px; 89 | margin-bottom: 100px; 90 | padding: 4px 10px 4px 10px; 91 | border-radius: 4px; 92 | border-color: #323232; 93 | border-width: 1px; 94 | } 95 | 96 | #source { 97 | float: right; 98 | margin-right: 15px; 99 | font-size: 0.8rem; 100 | 101 | position: relative; 102 | top: 6px; 103 | } 104 | } 105 | 106 | .main { 107 | margin-left: 179px; 108 | } 109 | -------------------------------------------------------------------------------- /src/styles/_Library.scss: -------------------------------------------------------------------------------- 1 | * { 2 | transition: all 0.25s; 3 | } 4 | 5 | .top { 6 | display: flex; 7 | justify-content: space-between; 8 | 9 | .search-bar { 10 | width: 100%; 11 | color: #dddddd; 12 | font-size: 1rem; 13 | margin: 0 0 32px 0; 14 | padding: 8px; 15 | background-color: #121212; 16 | border-style: solid; 17 | border-width: 1px; 18 | border-color: #323232; 19 | border-radius: 4px; 20 | box-shadow: 0px 2px 8px #00000055; 21 | 22 | &[type='text']:focus { 23 | outline-style: none; 24 | } 25 | 26 | &[type='text']::placeholder { 27 | color: #dddddd; 28 | } 29 | } 30 | 31 | button { 32 | background-color: #121212; 33 | border-color: #323232; 34 | border-style: solid; 35 | border-width: 1px; 36 | border-radius: 4px; 37 | box-shadow: 0px 2px 8px #00000055; 38 | 39 | margin-bottom: 2rem; 40 | margin-top: 0rem; 41 | margin-right: 8px; 42 | color: #dddddd; 43 | font-size: 16px; 44 | cursor: pointer; 45 | transition: all 0.25s; 46 | 47 | svg { 48 | position: relative; 49 | top: 2px; 50 | } 51 | 52 | padding: 0px 32px; 53 | } 54 | 55 | .search-bar:hover, 56 | button:hover { 57 | background-color: #181818; 58 | } 59 | } 60 | 61 | .game-panel { 62 | transition: all 0.25s; 63 | display: inline-block; 64 | padding: 8px 8px 8px 8px; 65 | margin: 8px 0px 8px 0px; 66 | 67 | .game-text { 68 | display: flex; 69 | flex-direction: column; 70 | padding-top: 8px; 71 | padding-left: 10px; 72 | width: 203px; 73 | 74 | button { 75 | padding: 0; 76 | margin: 0; 77 | background-color: inherit; 78 | border: none; 79 | 80 | &:hover { 81 | text-shadow: none; 82 | } 83 | 84 | img { 85 | width: 100px; 86 | height: auto; 87 | transition: all 0.25s; 88 | align-self: center; 89 | margin-bottom: 1rem; 90 | filter: drop-shadow(0px 0px 16px #000000aa); 91 | } 92 | 93 | p { 94 | color: #dddddd; 95 | margin: 0; 96 | font-weight: 400; 97 | font-size: 20px; 98 | font-family: 'Montserrat'; 99 | word-wrap: break-word; 100 | text-align: center; 101 | } 102 | } 103 | } 104 | 105 | .gm-flex { 106 | display: flex; 107 | 108 | .game-modal { 109 | box-shadow: 0px 0px 32px #00000055; 110 | background-color: #101010; 111 | border-color: #101010; 112 | margin: 0; 113 | margin-left: auto; 114 | height: 100vh; 115 | border-width: 1px; 116 | 117 | &::backdrop { 118 | background-color: inherit; 119 | } 120 | 121 | &[open] { 122 | color: #dddddd; 123 | display: flex; 124 | flex-direction: column; 125 | width: 300px; 126 | } 127 | 128 | #game-image { 129 | width: 100px; 130 | height: auto; 131 | margin: auto; 132 | margin-top: 16px; 133 | } 134 | 135 | #game-name { 136 | font-size: 1.5rem; 137 | font-weight: 800; 138 | text-align: center; 139 | } 140 | 141 | #game-desc { 142 | flex-grow: 2; 143 | margin-left: 0.5rem; 144 | margin-right: 0.5rem; 145 | font-size: 0.9rem; 146 | line-height: 17px; 147 | font-weight: 400; 148 | text-align: center; 149 | } 150 | 151 | #execute { 152 | margin-left: 3rem; 153 | margin-right: 3rem; 154 | margin-top: 1rem; 155 | font-size: 1.3rem; 156 | font-family: 'JetBrains Mono', monospace; 157 | font-weight: 900; 158 | border-style: solid; 159 | border-width: 1px; 160 | border-color: #323232; 161 | border-radius: 4px; 162 | background-color: #121212; 163 | color: #dddddd; 164 | padding: 8px 16px; 165 | 166 | &:hover { 167 | background-color: #181818; 168 | } 169 | } 170 | 171 | .buttons { 172 | flex-direction: row; 173 | 174 | .game-button-delete, 175 | .game-button-run { 176 | border: none; 177 | background-color: inherit; 178 | font-size: 2rem; 179 | cursor: pointer; 180 | } 181 | 182 | .game-button-delete { 183 | float: right; 184 | color: #fff; 185 | } 186 | 187 | .game-button-run { 188 | color: #fff; 189 | } 190 | } 191 | } 192 | } 193 | } 194 | 195 | .main { 196 | margin-left: 183px; 197 | } 198 | -------------------------------------------------------------------------------- /src/styles/_Modal.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --accent-color: #00ff00; 3 | } 4 | 5 | .error { 6 | color: red; 7 | margin-bottom: 8px; 8 | margin-top: 0; 9 | } 10 | 11 | .modal-main { 12 | dialog[open].fetch-meta { 13 | display: flex; 14 | flex-direction: column; 15 | 16 | padding: 15px; 17 | max-width: 350px; 18 | 19 | border: 1px solid #323232; 20 | border-radius: 4px; 21 | 22 | background-color: #0b0d0e; 23 | 24 | span { 25 | color: #dddddd; 26 | font-family: 'Montserrat'; 27 | font-weight: 500; 28 | 29 | &::after { 30 | content: ':'; 31 | } 32 | } 33 | 34 | button { 35 | text-align: center; 36 | transition: all 0.25s; 37 | font-family: 'Montserrat'; 38 | background-color: #0b0d0e; 39 | color: #dddddd; 40 | border: 1px solid #0b0d0e; 41 | border-radius: 4px; 42 | padding: 10px; 43 | 44 | &:hover { 45 | cursor: pointer; 46 | border-color: #0b0d0e; 47 | background-color: #2d3739; 48 | } 49 | } 50 | } 51 | 52 | .newgame { 53 | display: flex; 54 | flex-grow: 1; 55 | flex-direction: column; 56 | 57 | button.fetch-meta { 58 | transition: all 0.25s; 59 | 60 | color: #dddddd; 61 | background-color: #0b0d0e; 62 | border-color: #323232; 63 | border-style: solid; 64 | border-width: 1px; 65 | border-radius: 4px; 66 | margin: 15px 0; 67 | 68 | font-size: 15px; 69 | 70 | &:hover { 71 | cursor: pointer; 72 | background-color: #161616; 73 | } 74 | } 75 | 76 | .ng-button { 77 | margin-top: 15px; 78 | padding: 8px 8px 8px 8px; 79 | } 80 | 81 | input, 82 | textarea { 83 | color: #dddddd; 84 | background-color: #0b0d0e; 85 | border-color: #323232; 86 | border-style: solid; 87 | border-width: 1px; 88 | border-radius: 4px; 89 | padding: 8px 8px 8px 8px; 90 | 91 | outline-style: none; 92 | font-family: 'Montserrat'; 93 | font-size: 15px; 94 | 95 | &::placeholder { 96 | color: #fff; 97 | font-size: 15px; 98 | } 99 | } 100 | 101 | .show-path { 102 | color: #dddddd; 103 | display: flex; 104 | flex-direction: row; 105 | 106 | .ng-button { 107 | transition: all 0.25s; 108 | 109 | color: #dddddd; 110 | background-color: #0b0d0e; 111 | border-color: #323232; 112 | border-style: solid; 113 | border-width: 1px; 114 | border-radius: 4px; 115 | //margin-bottom: 16px; 116 | //margin-top: 16px; 117 | margin: 0 0 10px; 118 | margin-right: 10px; 119 | 120 | font-size: 15px; 121 | 122 | &:hover { 123 | cursor: pointer; 124 | background-color: #161616; 125 | } 126 | } 127 | 128 | .image-add { 129 | height: 38px; 130 | } 131 | 132 | img { 133 | border-style: solid; 134 | border-width: 1px; 135 | border-radius: 4px; 136 | padding: 5px; 137 | background-color: #0b0d0e; 138 | border-color: #323232; 139 | margin-bottom: 8px; 140 | } 141 | } 142 | } 143 | 144 | .done-btn { 145 | display: flex; 146 | flex-flow: row-reverse; 147 | 148 | button { 149 | background-color: #0b0d0e; 150 | border: 1px solid #323232; 151 | border-radius: 4px; 152 | font-size: 1rem; 153 | color: #dddddd; 154 | padding: 8px 16px; 155 | 156 | &:hover { 157 | cursor: pointer; 158 | background-color: #161616; 159 | } 160 | } 161 | } 162 | } 163 | 164 | .toast { 165 | button { 166 | background-color: #121212; 167 | border-color: #323232; 168 | border-style: solid; 169 | border-width: 1px; 170 | border-radius: 4px; 171 | box-shadow: 0px 2px 8px #00000055; 172 | 173 | padding: 5px 15px; 174 | margin-bottom: 10px; 175 | 176 | transform: translateX(50%); 177 | 178 | color: #dddddd; 179 | font-size: 16px; 180 | cursor: pointer; 181 | transition: all 0.25s; 182 | } 183 | 184 | p { 185 | margin-top: 10px; 186 | margin-bottom: 10px; 187 | margin-left: 5px; 188 | } 189 | } 190 | 191 | .title-el { 192 | margin: 0px 0 10px 0; 193 | } 194 | 195 | .main { 196 | padding-left: 5px; 197 | } 198 | -------------------------------------------------------------------------------- /src/styles/_Preferences.scss: -------------------------------------------------------------------------------- 1 | .section { 2 | display: flex; 3 | flex-direction: row; 4 | justify-content: space-around; 5 | flex-wrap: wrap; 6 | 7 | margin: 0px 10px 0px 10px; 8 | 9 | .plugin-card { 10 | display: flex; 11 | flex-direction: column; 12 | padding: 5px 0px; 13 | margin-bottom: 20px; 14 | 15 | width: 300px; 16 | height: 188px; 17 | 18 | background: #101010; 19 | box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); 20 | border-radius: 4px; 21 | 22 | .header { 23 | box-sizing: border-box; 24 | 25 | display: flex; 26 | flex-direction: row; 27 | justify-content: center; 28 | align-items: center; 29 | padding: 8px 100px; 30 | //gap: 10px; 31 | 32 | width: 300px; 33 | height: 25px; 34 | 35 | border-bottom: 1px solid #dddddd; 36 | 37 | flex: none; 38 | order: 0; 39 | flex-grow: 0; 40 | 41 | svg { 42 | position: relative; 43 | top: -1px; 44 | padding-left: 5px; 45 | } 46 | } 47 | 48 | .buttons { 49 | display: flex; 50 | flex-direction: column; 51 | 52 | align-items: center; 53 | justify-content: center; 54 | 55 | width: 300px; 56 | height: 170px; 57 | 58 | input { 59 | background: #121212; 60 | color: #dddddd; 61 | border: 1px solid #323232; 62 | border-radius: 4px; 63 | 64 | padding: 7px; 65 | font-size: 1rem; 66 | } 67 | 68 | button { 69 | background: #121212; 70 | color: #dddddd; 71 | border: 1px solid #323232; 72 | border-radius: 4px; 73 | 74 | font-size: 16px; 75 | margin: 10px 5px; 76 | padding: 8px 32px; 77 | } 78 | 79 | .cube { 80 | position: relative; 81 | top: 3px; 82 | } 83 | 84 | .bin { 85 | position: relative; 86 | top: 2px; 87 | } 88 | 89 | select { 90 | appearance: none; 91 | background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAAHCAYAAAD9NeaIAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA+hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNS1jMDIxIDc5LjE1NDkxMSwgMjAxMy8xMC8yOS0xMTo0NzoxNiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgKE1hY2ludG9zaCkiIHhtcDpDcmVhdGVEYXRlPSIyMDE1LTA0LTE3VDE3OjEyOjQyKzAyOjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAxNS0wNC0yMFQxNzoxNjoyNCswMjowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAxNS0wNC0yMFQxNzoxNjoyNCswMjowMCIgZGM6Zm9ybWF0PSJpbWFnZS9wbmciIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6RTU4MjBDRURERjVCMTFFNEEzN0FCODBEM0I5MTExMjkiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6RTU4MjBDRUVERjVCMTFFNEEzN0FCODBEM0I5MTExMjkiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo2RUVFRDJCNkREQzMxMUU0QTM3QUI4MEQzQjkxMTEyOSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpFNTgyMENFQ0RGNUIxMUU0QTM3QUI4MEQzQjkxMTEyOSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PuShL/sAAABeSURBVHjaYszOzjZnYGDYCcT8DMSBv0AcP2XKlKVEqmdgAuKTQOwOxB+JtQCIibYAZgkDkRaRZQGyJYQsItsCdEtwWUSRBdgsQbeIYgtAgAWHOMwiJSBezkAhAAgwAJSTG/DI0S9VAAAAAElFTkSuQmCC') 92 | no-repeat right #121212; 93 | 94 | padding: 7px 35px 7px 7px; 95 | 96 | font-size: 1rem; 97 | margin: 8px 0px; 98 | color: #dddddd; 99 | background-color: #121212; 100 | border-style: solid; 101 | border-width: 1px; 102 | border-radius: 4px; 103 | border-color: #323232; 104 | } 105 | } 106 | 107 | .selector { 108 | display: flex; 109 | flex-direction: row; 110 | 111 | margin: 5px 15px; 112 | 113 | align-items: center; 114 | width: 300px; 115 | 116 | p { 117 | padding-left: 10px; 118 | height: 20px; 119 | margin: 0; 120 | 121 | position: relative; 122 | top: -3px; 123 | } 124 | 125 | [type='checkbox'] { 126 | appearance: none; 127 | position: relative; 128 | 129 | top: -2px; 130 | 131 | background-color: #121212; 132 | border-style: solid; 133 | border-width: 1px; 134 | border-radius: 4px; 135 | border-color: #323232; 136 | 137 | padding: 10px; 138 | 139 | &:checked { 140 | appearance: none; 141 | background-color: invert($color: #121212); 142 | } 143 | } 144 | 145 | &#first { 146 | margin-top: 15px; 147 | } 148 | } 149 | 150 | .checkbox { 151 | display: flex; 152 | flex-direction: row; 153 | 154 | align-items: center; 155 | justify-content: center; 156 | 157 | width: 300px; 158 | height: 170px; 159 | 160 | p { 161 | padding-left: 10px; 162 | position: relative; 163 | } 164 | 165 | [type='checkbox'] { 166 | appearance: none; 167 | position: relative; 168 | 169 | top: -2px; 170 | 171 | background-color: #121212; 172 | border-style: solid; 173 | border-width: 1px; 174 | border-radius: 4px; 175 | border-color: #323232; 176 | 177 | padding: 10px; 178 | 179 | &:checked { 180 | appearance: none; 181 | background-color: invert($color: #121212); 182 | } 183 | } 184 | } 185 | } 186 | 187 | label { 188 | font-size: 1.2rem; 189 | } 190 | 191 | .save-button { 192 | padding: 7px; 193 | font-size: 1rem; 194 | margin: 25px 0px 8px; 195 | color: #dddddd; 196 | } 197 | } 198 | 199 | .main { 200 | margin: 10px 25px 0 176px; 201 | } 202 | 203 | // SCSS to display plugins as cards 204 | .cards { 205 | display: flex; 206 | flex-direction: row; 207 | flex-wrap: wrap; 208 | justify-content: center; 209 | align-items: center; 210 | margin: 0 auto; 211 | padding: 0; 212 | list-style: none; 213 | } 214 | 215 | // SCSS to display a single plugin card 216 | .card { 217 | display: flex; 218 | flex-direction: row; 219 | justify-content: center; 220 | align-items: center; 221 | margin: 20px 0 1.5rem; 222 | padding: 0; 223 | width: 300px; 224 | height: 150px; 225 | //background: #101010; 226 | background: #121212; 227 | box-shadow: 0px 0px 20px rgba(0, 0, 0, 0.25); 228 | border-radius: 4px; 229 | // Gap between cards 230 | &:not(:last-child) { 231 | margin-right: 1.5rem; 232 | } 233 | 234 | .card-left { 235 | flex-grow: 1; 236 | padding-left: 15px; 237 | height: 85%; 238 | 239 | p.card-header { 240 | display: flex; 241 | flex-direction: row; 242 | justify-content: flex-start; 243 | font-weight: bold; 244 | align-items: center; 245 | padding: 0px 30px 2px 0; 246 | width: 87%; 247 | height: 25px; 248 | margin: 0 0 0 0; 249 | 250 | border-bottom: 1px solid white; 251 | 252 | a { 253 | position: relative; 254 | top: 2px; 255 | left: 7px; 256 | 257 | text-decoration: none; 258 | color: #dddddd; 259 | &:visited, 260 | &:hover, 261 | &:active { 262 | color: #dddddd; 263 | } 264 | } 265 | } 266 | 267 | .card-footer { 268 | display: flex; 269 | flex-direction: column; 270 | padding-top: 10px; 271 | 272 | .author { 273 | font-size: 0.8rem; 274 | } 275 | 276 | .version { 277 | font-size: 0.8rem; 278 | } 279 | 280 | .desc { 281 | font-size: 0.8rem; 282 | width: 230px; 283 | } 284 | } 285 | } 286 | 287 | .buttons { 288 | display: flex; 289 | flex-direction: column; 290 | width: 0px; 291 | align-items: center; 292 | 293 | button { 294 | position: relative; 295 | top: -54px; 296 | left: -25px; 297 | 298 | background-color: inherit; 299 | border-style: none; 300 | color: #dddddd; 301 | opacity: 0.6; 302 | padding: 0; 303 | margin: 0; 304 | 305 | cursor: pointer; 306 | } 307 | } 308 | } 309 | 310 | @media (hover: hover) { 311 | .card:hover .buttons > button { 312 | opacity: 1; 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import sveltePreprocess from 'svelte-preprocess'; 2 | 3 | export default { 4 | // Consult https://github.com/sveltejs/svelte-preprocess 5 | // for more information about preprocessors 6 | preprocess: sveltePreprocess(), 7 | }; 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/svelte/tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "useDefineForClassFields": true, 6 | "module": "ESNext", 7 | "resolveJsonModule": true, 8 | "baseUrl": ".", 9 | /** 10 | * Typecheck JS in `.svelte` and `.js` files by default. 11 | * Disable checkJs if you'd like to use dynamic types in JS. 12 | * Note that setting allowJs false does not prevent the use 13 | * of JS in `.svelte` files. 14 | */ 15 | "allowJs": true, 16 | "checkJs": true, 17 | "isolatedModules": true 18 | }, 19 | "include": [ 20 | "src/**/*.d.ts", 21 | "src/**/*.ts", 22 | "src/**/*.js", 23 | "src/**/*.svelte" 24 | ], 25 | "references": [{ "path": "./tsconfig.node.json" }] 26 | } 27 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node" 6 | }, 7 | "include": ["vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import { svelte } from '@sveltejs/vite-plugin-svelte'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [svelte()], 7 | 8 | // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` 9 | // prevent vite from obscuring rust errors 10 | clearScreen: false, 11 | // tauri expects a fixed port, fail if that port is not available 12 | server: { 13 | port: 1420, 14 | strictPort: true, 15 | }, 16 | // to make use of `TAURI_DEBUG` and other env variables 17 | // https://tauri.studio/v1/api/config#buildconfig.beforedevcommand 18 | envPrefix: ['VITE_', 'TAURI_'], 19 | build: { 20 | // Tauri supports es2021 21 | target: ['es2021', 'chrome100', 'safari13'], 22 | // don't minify for debug builds 23 | minify: !process.env.TAURI_DEBUG ? 'esbuild' : false, 24 | // produce sourcemaps for debug builds 25 | sourcemap: !!process.env.TAURI_DEBUG, 26 | }, 27 | }); 28 | --------------------------------------------------------------------------------