├── .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 |  
8 |
9 |    
10 |
11 | 
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 |
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 |
43 |
44 |
45 |
46 |
51 |
52 |
53 |
58 | {#if searchData.length === 0}
59 |
{$t('browse.nothingFound')}
60 | {/if}
61 | {#each searchData as ScraperResponse}
62 | {#each ScraperResponse as Response}
63 |
75 | {/each}
76 | {/each}
77 |
78 |
79 |
--------------------------------------------------------------------------------
/src/routes/Library.svelte:
--------------------------------------------------------------------------------
1 |
69 |
70 |
71 |
72 |
84 |
85 |
86 |
87 |
88 |
89 |
90 | {#await games then data}
91 | {#each getFilteredGames(data, query) as game}
92 |
93 |
94 |
102 |
111 | {game.name}
112 |
113 |
122 |
123 |
124 | {/each}
125 |
126 |
127 |
128 |
133 | {#if gameModalOpened}
134 |
135 | {gameOnModal.name}
136 |
137 |
138 |
147 |
148 | {$t('library.run')}
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 |
50 |
51 |
52 |
58 |
59 | {$t('preferences.resetToDefault')}
65 |
66 |
67 |
68 |
69 |
73 |
74 |
75 |
76 |
{$t('preferences.suppressUpdater')}
77 |
78 |
79 |
80 |
81 |
85 |
86 |
87 | {#each languages as languageCode, i}
88 |
89 | {languageNames[i]}
90 |
91 | {/each}
92 |
93 |
94 |
95 |
96 |
97 |
101 |
102 |
103 |
115 | {$t('preferences.saveText')}
116 |
117 |
118 |
119 |
120 |
121 |
125 |
132 |
133 |
137 |
FitGirl
138 |
139 |
140 |
141 |
142 |
143 |
--------------------------------------------------------------------------------
/src/routes/modals/NewGame.svelte:
--------------------------------------------------------------------------------
1 |
68 |
69 |
70 |
71 |
78 |
79 |
80 |
{$t('modals.newGame.addExec')}
83 |
84 |
85 |
90 |
91 |
92 |
93 |
{$t('modals.newGame.addImg')}
96 |
97 |
102 | {#if imageSelected && imagePath != 'None'}
103 |
104 | {/if}
105 |
106 |
112 |
113 |
158 | {$t('modals.newGame.autoFetch')}
159 |
160 |
161 |
162 |
163 | {$t('modals.fetchMeta.selectGame')}
164 | {#each gameMetadata as gameMeta, i}
165 |
179 | {gameMeta.name}
180 |
181 | {/each}
182 |
183 |
184 |
185 |
186 |
191 | {$t('modals.newGame.done')}
192 |
193 |
194 |
195 |
--------------------------------------------------------------------------------
/src/routes/modals/Toast.svelte:
--------------------------------------------------------------------------------
1 |
29 |
30 |
31 |
{$t('modals.updater.updateAvailable')}
32 |
Download
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 |
--------------------------------------------------------------------------------