mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-19 09:13:57 +00:00
Compare commits
22 Commits
v3.0.5
...
github/for
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba9232b821 | ||
|
|
a432306b1d | ||
|
|
69d96cc290 | ||
|
|
8f4919615f | ||
|
|
a25a960235 | ||
|
|
c754710171 | ||
|
|
bf6ce2b465 | ||
|
|
3adc8662dc | ||
|
|
2a6346cb69 | ||
|
|
455016c1a7 | ||
|
|
e0ec79b105 | ||
|
|
ba7e4c979d | ||
|
|
a54983c339 | ||
|
|
b754b1e052 | ||
|
|
79763b6072 | ||
|
|
3ff15d2d61 | ||
|
|
50303251a2 | ||
|
|
012f872f60 | ||
|
|
e9f68977fe | ||
|
|
c5d4db0a1e | ||
|
|
3b02a3c43f | ||
|
|
ab7625a314 |
@@ -1,4 +1,5 @@
|
|||||||
MAIN_VITE_API_URL=API_URL
|
MAIN_VITE_API_URL=API_URL
|
||||||
MAIN_VITE_AUTH_URL=AUTH_URL
|
MAIN_VITE_AUTH_URL=AUTH_URL
|
||||||
MAIN_VITE_STEAMGRIDDB_API_KEY=YOUR_API_KEY
|
MAIN_VITE_STEAMGRIDDB_API_KEY=YOUR_API_KEY
|
||||||
RENDERER_VITE_INTERCOM_APP_ID=YOUR_APP_ID
|
MAIN_VITE_SENTRY_DSN=YOUR_SENTRY_DSN
|
||||||
|
SENTRY_AUTH_TOKEN=
|
||||||
|
|||||||
10
.github/workflows/build.yml
vendored
10
.github/workflows/build.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
|||||||
- name: Install Node.js
|
- name: Install Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.18.0
|
node-version: 20.11.1
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: yarn
|
run: yarn
|
||||||
@@ -43,8 +43,8 @@ jobs:
|
|||||||
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_STAGING_API_URL }}
|
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_STAGING_API_URL }}
|
||||||
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }}
|
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }}
|
||||||
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }}
|
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }}
|
||||||
MAIN_VITE_ANALYTICS_API_URL: ${{ vars.MAIN_VITE_ANALYTICS_API_URL }}
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }}
|
MAIN_VITE_SENTRY_DSN: ${{ vars.MAIN_VITE_SENTRY_DSN }}
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build Windows
|
- name: Build Windows
|
||||||
@@ -54,8 +54,8 @@ jobs:
|
|||||||
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_STAGING_API_URL }}
|
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_STAGING_API_URL }}
|
||||||
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }}
|
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }}
|
||||||
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }}
|
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }}
|
||||||
MAIN_VITE_ANALYTICS_API_URL: ${{ vars.MAIN_VITE_ANALYTICS_API_URL }}
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }}
|
MAIN_VITE_SENTRY_DSN: ${{ vars.MAIN_VITE_SENTRY_DSN }}
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Create artifact
|
- name: Create artifact
|
||||||
|
|||||||
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
|||||||
- name: Install Node.js
|
- name: Install Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.18.0
|
node-version: 20.11.1
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: yarn
|
run: yarn
|
||||||
|
|||||||
14
.github/workflows/release.yml
vendored
14
.github/workflows/release.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
|||||||
- name: Install Node.js
|
- name: Install Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.18.0
|
node-version: 20.11.1
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: yarn
|
run: yarn
|
||||||
@@ -45,9 +45,10 @@ jobs:
|
|||||||
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }}
|
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }}
|
||||||
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_AUTH_URL }}
|
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_AUTH_URL }}
|
||||||
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_CHECKOUT_URL }}
|
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_CHECKOUT_URL }}
|
||||||
MAIN_VITE_ANALYTICS_API_URL: ${{ vars.MAIN_VITE_ANALYTICS_API_URL }}
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }}
|
MAIN_VITE_SENTRY_DSN: ${{ vars.MAIN_VITE_SENTRY_DSN }}
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build Windows
|
- name: Build Windows
|
||||||
if: matrix.os == 'windows-latest'
|
if: matrix.os == 'windows-latest'
|
||||||
run: yarn build:win
|
run: yarn build:win
|
||||||
@@ -55,9 +56,10 @@ jobs:
|
|||||||
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }}
|
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }}
|
||||||
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_AUTH_URL }}
|
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_AUTH_URL }}
|
||||||
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_CHECKOUT_URL }}
|
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_CHECKOUT_URL }}
|
||||||
MAIN_VITE_ANALYTICS_API_URL: ${{ vars.MAIN_VITE_ANALYTICS_API_URL }}
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }}
|
MAIN_VITE_SENTRY_DSN: ${{ vars.MAIN_VITE_SENTRY_DSN }}
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Create artifact
|
- name: Create artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
@@ -75,7 +77,7 @@ jobs:
|
|||||||
dist/*.pacman
|
dist/*.pacman
|
||||||
|
|
||||||
- name: Release
|
- name: Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v1
|
||||||
with:
|
with:
|
||||||
draft: true
|
draft: true
|
||||||
files: |
|
files: |
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,4 +9,5 @@ out
|
|||||||
*.log*
|
*.log*
|
||||||
.env
|
.env
|
||||||
.vite
|
.vite
|
||||||
|
sentry.properties
|
||||||
ludusavi/
|
ludusavi/
|
||||||
@@ -7,7 +7,6 @@ extraResources:
|
|||||||
- hydra-download-manager
|
- hydra-download-manager
|
||||||
- seeds
|
- seeds
|
||||||
- from: node_modules/create-desktop-shortcuts/src/windows.vbs
|
- from: node_modules/create-desktop-shortcuts/src/windows.vbs
|
||||||
- from: resources/achievement.wav
|
|
||||||
files:
|
files:
|
||||||
- "!**/.vscode/*"
|
- "!**/.vscode/*"
|
||||||
- "!src/*"
|
- "!src/*"
|
||||||
|
|||||||
@@ -6,9 +6,16 @@ import {
|
|||||||
externalizeDepsPlugin,
|
externalizeDepsPlugin,
|
||||||
} from "electron-vite";
|
} from "electron-vite";
|
||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
|
import { sentryVitePlugin } from "@sentry/vite-plugin";
|
||||||
import { vanillaExtractPlugin } from "@vanilla-extract/vite-plugin";
|
import { vanillaExtractPlugin } from "@vanilla-extract/vite-plugin";
|
||||||
import svgr from "vite-plugin-svgr";
|
import svgr from "vite-plugin-svgr";
|
||||||
|
|
||||||
|
const sentryPlugin = sentryVitePlugin({
|
||||||
|
authToken: process.env.SENTRY_AUTH_TOKEN,
|
||||||
|
org: "hydra-launcher",
|
||||||
|
project: "hydra-launcher",
|
||||||
|
});
|
||||||
|
|
||||||
export default defineConfig(({ mode }) => {
|
export default defineConfig(({ mode }) => {
|
||||||
loadEnv(mode);
|
loadEnv(mode);
|
||||||
|
|
||||||
@@ -28,7 +35,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
"@shared": resolve("src/shared"),
|
"@shared": resolve("src/shared"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [externalizeDepsPlugin(), swcPlugin()],
|
plugins: [externalizeDepsPlugin(), swcPlugin(), sentryPlugin],
|
||||||
},
|
},
|
||||||
preload: {
|
preload: {
|
||||||
plugins: [externalizeDepsPlugin()],
|
plugins: [externalizeDepsPlugin()],
|
||||||
@@ -44,7 +51,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
"@shared": resolve("src/shared"),
|
"@shared": resolve("src/shared"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [svgr(), react(), vanillaExtractPlugin()],
|
plugins: [svgr(), react(), vanillaExtractPlugin(), sentryPlugin],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
21
package.json
21
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hydralauncher",
|
"name": "hydralauncher",
|
||||||
"version": "3.0.5",
|
"version": "3.0.4",
|
||||||
"description": "Hydra",
|
"description": "Hydra",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": "Los Broxas",
|
"author": "Los Broxas",
|
||||||
@@ -36,9 +36,9 @@
|
|||||||
"@electron-toolkit/utils": "^3.0.0",
|
"@electron-toolkit/utils": "^3.0.0",
|
||||||
"@fontsource/noto-sans": "^5.0.22",
|
"@fontsource/noto-sans": "^5.0.22",
|
||||||
"@hookform/resolvers": "^3.9.0",
|
"@hookform/resolvers": "^3.9.0",
|
||||||
"@intercom/messenger-js-sdk": "^0.0.14",
|
|
||||||
"@primer/octicons-react": "^19.9.0",
|
"@primer/octicons-react": "^19.9.0",
|
||||||
"@reduxjs/toolkit": "^2.2.3",
|
"@reduxjs/toolkit": "^2.2.3",
|
||||||
|
"@sentry/electron": "^5.1.0",
|
||||||
"@vanilla-extract/css": "^1.14.2",
|
"@vanilla-extract/css": "^1.14.2",
|
||||||
"@vanilla-extract/dynamic": "^2.1.1",
|
"@vanilla-extract/dynamic": "^2.1.1",
|
||||||
"@vanilla-extract/recipes": "^0.5.2",
|
"@vanilla-extract/recipes": "^0.5.2",
|
||||||
@@ -51,25 +51,25 @@
|
|||||||
"color.js": "^1.2.0",
|
"color.js": "^1.2.0",
|
||||||
"create-desktop-shortcuts": "^1.11.0",
|
"create-desktop-shortcuts": "^1.11.0",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"dexie": "^4.0.9",
|
"dexie": "^4.0.8",
|
||||||
"electron-log": "^5.2.0",
|
"electron-log": "^5.2.0",
|
||||||
"electron-updater": "^6.3.9",
|
"electron-updater": "^6.3.9",
|
||||||
"file-type": "^19.6.0",
|
|
||||||
"flexsearch": "^0.7.43",
|
"flexsearch": "^0.7.43",
|
||||||
"i18next": "^23.11.2",
|
"i18next": "^23.11.2",
|
||||||
"i18next-browser-languagedetector": "^7.2.1",
|
"i18next-browser-languagedetector": "^7.2.1",
|
||||||
|
"icojs": "^0.19.4",
|
||||||
"jsdom": "^24.0.0",
|
"jsdom": "^24.0.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"knex": "^3.1.0",
|
"knex": "^3.1.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
|
"lottie-react": "^2.4.0",
|
||||||
"parse-torrent": "^11.0.17",
|
"parse-torrent": "^11.0.17",
|
||||||
"piscina": "^4.7.0",
|
"piscina": "^4.5.1",
|
||||||
"react-hook-form": "^7.53.0",
|
"react-hook-form": "^7.53.0",
|
||||||
"react-i18next": "^14.1.0",
|
"react-i18next": "^14.1.0",
|
||||||
"react-loading-skeleton": "^3.4.0",
|
"react-loading-skeleton": "^3.4.0",
|
||||||
"react-redux": "^9.1.1",
|
"react-redux": "^9.1.1",
|
||||||
"react-router-dom": "^6.22.3",
|
"react-router-dom": "^6.22.3",
|
||||||
"sound-play": "^1.1.0",
|
|
||||||
"sudo-prompt": "^9.2.1",
|
"sudo-prompt": "^9.2.1",
|
||||||
"tar": "^7.4.3",
|
"tar": "^7.4.3",
|
||||||
"typeorm": "^0.3.20",
|
"typeorm": "^0.3.20",
|
||||||
@@ -79,11 +79,12 @@
|
|||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^19.5.0",
|
"@commitlint/cli": "^19.3.0",
|
||||||
"@commitlint/config-conventional": "^19.5.0",
|
"@commitlint/config-conventional": "^19.2.2",
|
||||||
"@electron-toolkit/eslint-config-prettier": "^2.0.0",
|
"@electron-toolkit/eslint-config-prettier": "^2.0.0",
|
||||||
"@electron-toolkit/eslint-config-ts": "^2.0.0",
|
"@electron-toolkit/eslint-config-ts": "^1.0.1",
|
||||||
"@electron-toolkit/tsconfig": "^1.0.1",
|
"@electron-toolkit/tsconfig": "^1.0.1",
|
||||||
|
"@sentry/vite-plugin": "^2.20.1",
|
||||||
"@swc/core": "^1.4.16",
|
"@swc/core": "^1.4.16",
|
||||||
"@types/auto-launch": "^5.0.5",
|
"@types/auto-launch": "^5.0.5",
|
||||||
"@types/color": "^3.0.6",
|
"@types/color": "^3.0.6",
|
||||||
@@ -95,7 +96,6 @@
|
|||||||
"@types/parse-torrent": "^5.8.7",
|
"@types/parse-torrent": "^5.8.7",
|
||||||
"@types/react": "^18.2.48",
|
"@types/react": "^18.2.48",
|
||||||
"@types/react-dom": "^18.2.18",
|
"@types/react-dom": "^18.2.18",
|
||||||
"@types/sound-play": "^1.1.3",
|
|
||||||
"@types/user-agents": "^1.0.4",
|
"@types/user-agents": "^1.0.4",
|
||||||
"@vanilla-extract/vite-plugin": "^4.0.7",
|
"@vanilla-extract/vite-plugin": "^4.0.7",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
@@ -110,7 +110,6 @@
|
|||||||
"prettier": "^3.2.4",
|
"prettier": "^3.2.4",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"sass-embedded": "^1.80.6",
|
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"vite": "^5.0.12",
|
"vite": "^5.0.12",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
libtorrent
|
libtorrent
|
||||||
cx_Freeze == 7.2.3
|
cx_Freeze
|
||||||
cx_Logging; sys_platform == 'win32'
|
cx_Logging; sys_platform == 'win32'
|
||||||
pywin32; sys_platform == 'win32'
|
pywin32; sys_platform == 'win32'
|
||||||
psutil
|
psutil
|
||||||
|
|||||||
BIN
resources/achievement-sound.mp3
Normal file
BIN
resources/achievement-sound.mp3
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -26,7 +26,8 @@
|
|||||||
"game_has_no_executable": "Game has no executable selected",
|
"game_has_no_executable": "Game has no executable selected",
|
||||||
"sign_in": "Sign in",
|
"sign_in": "Sign in",
|
||||||
"friends": "Friends",
|
"friends": "Friends",
|
||||||
"need_help": "Need help?"
|
"aria_view_profile": "View profile",
|
||||||
|
"resize_sidebar": "Resize sidebar"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Search games",
|
"search": "Search games",
|
||||||
@@ -36,7 +37,9 @@
|
|||||||
"search_results": "Search results",
|
"search_results": "Search results",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"version_available_install": "Version {{version}} available. Click here to restart and install.",
|
"version_available_install": "Version {{version}} available. Click here to restart and install.",
|
||||||
"version_available_download": "Version {{version}} available. Click here to download."
|
"version_available_download": "Version {{version}} available. Click here to download.",
|
||||||
|
"back": "Back",
|
||||||
|
"clear_search": "Clear search"
|
||||||
},
|
},
|
||||||
"bottom_panel": {
|
"bottom_panel": {
|
||||||
"no_downloads_in_progress": "No downloads in progress",
|
"no_downloads_in_progress": "No downloads in progress",
|
||||||
@@ -133,6 +136,7 @@
|
|||||||
"warning": "Warning:",
|
"warning": "Warning:",
|
||||||
"hydra_needs_to_remain_open": "for this download, Hydra needs to remain open util it's completed. If Hydra closes before completing, you will lose your progress.",
|
"hydra_needs_to_remain_open": "for this download, Hydra needs to remain open util it's completed. If Hydra closes before completing, you will lose your progress.",
|
||||||
"achievements": "Achievements",
|
"achievements": "Achievements",
|
||||||
|
"achievement": "Achievement",
|
||||||
"achievements_count": "Achievements {{unlockedCount}}/{{achievementsCount}}",
|
"achievements_count": "Achievements {{unlockedCount}}/{{achievementsCount}}",
|
||||||
"cloud_save": "Cloud save",
|
"cloud_save": "Cloud save",
|
||||||
"cloud_save_description": "Save your progress in the cloud and continue playing on any device",
|
"cloud_save_description": "Save your progress in the cloud and continue playing on any device",
|
||||||
@@ -359,13 +363,13 @@
|
|||||||
},
|
},
|
||||||
"achievement": {
|
"achievement": {
|
||||||
"achievement_unlocked": "Achievement unlocked",
|
"achievement_unlocked": "Achievement unlocked",
|
||||||
|
"achievement_locked": "Achievement locked",
|
||||||
"user_achievements": "{{displayName}}'s Achievements",
|
"user_achievements": "{{displayName}}'s Achievements",
|
||||||
"your_achievements": "Your Achievements",
|
"your_achievements": "Your Achievements",
|
||||||
"unlocked_at": "Unlocked at:",
|
"unlocked_at": "Unlocked at:",
|
||||||
"subscription_needed": "A Hydra Cloud subscription is required to see this content",
|
"subscription_needed": "A Hydra Cloud subscription is required to see this content",
|
||||||
"new_achievements_unlocked": "Unlocked {{achievementCount}} new achievements from {{gameCount}} games",
|
"new_achievements_unlocked": "Unlocked {{achievementCount}} new achievements from {{gameCount}} games",
|
||||||
"achievement_progress": "{{unlockedCount}}/{{totalCount}} achievements",
|
"aria_achievement_summary": "{{userDisplayName}} achievements for {{gameTitle}}, {{userAchievementCount}} unlocked of {{userTotalAchievementCount}}, {{percentage}} completed"
|
||||||
"achievements_unlocked_for_game": "Unlocked {{achievementCount}} new achievements for {{gameTitle}}"
|
|
||||||
},
|
},
|
||||||
"tour": {
|
"tour": {
|
||||||
"subscription_tour_title": "Hydra Cloud Subscription",
|
"subscription_tour_title": "Hydra Cloud Subscription",
|
||||||
|
|||||||
@@ -25,8 +25,7 @@
|
|||||||
"queued": "{{title}} (En cola)",
|
"queued": "{{title}} (En cola)",
|
||||||
"game_has_no_executable": "El juego no tiene un ejecutable seleccionado",
|
"game_has_no_executable": "El juego no tiene un ejecutable seleccionado",
|
||||||
"sign_in": "Iniciar sesión",
|
"sign_in": "Iniciar sesión",
|
||||||
"friends": "Amigos",
|
"friends": "Amigos"
|
||||||
"need_help": "¿Necesitas ayuda?"
|
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Buscar juegos",
|
"search": "Buscar juegos",
|
||||||
|
|||||||
@@ -26,7 +26,8 @@
|
|||||||
"game_has_no_executable": "Jogo não possui executável selecionado",
|
"game_has_no_executable": "Jogo não possui executável selecionado",
|
||||||
"sign_in": "Login",
|
"sign_in": "Login",
|
||||||
"friends": "Amigos",
|
"friends": "Amigos",
|
||||||
"need_help": "Precisa de ajuda?"
|
"aria_view_profile": "Ver perfil",
|
||||||
|
"resize_sidebar": "Redimensionar barra lateral"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Buscar jogos",
|
"search": "Buscar jogos",
|
||||||
@@ -36,7 +37,9 @@
|
|||||||
"settings": "Ajustes",
|
"settings": "Ajustes",
|
||||||
"home": "Início",
|
"home": "Início",
|
||||||
"version_available_install": "Versão {{version}} disponível. Clique aqui para reiniciar e instalar.",
|
"version_available_install": "Versão {{version}} disponível. Clique aqui para reiniciar e instalar.",
|
||||||
"version_available_download": "Versão {{version}} disponível. Clique aqui para fazer o download."
|
"version_available_download": "Versão {{version}} disponível. Clique aqui para fazer o download.",
|
||||||
|
"back": "Voltar",
|
||||||
|
"clear_search": "Limpar busca"
|
||||||
},
|
},
|
||||||
"bottom_panel": {
|
"bottom_panel": {
|
||||||
"no_downloads_in_progress": "Sem downloads em andamento",
|
"no_downloads_in_progress": "Sem downloads em andamento",
|
||||||
@@ -129,6 +132,7 @@
|
|||||||
"warning": "Aviso:",
|
"warning": "Aviso:",
|
||||||
"hydra_needs_to_remain_open": "para este download, o Hydra precisa ficar aberto até a conclusão. Caso o Hydra encerre antes da conclusão, perderá seu progresso.",
|
"hydra_needs_to_remain_open": "para este download, o Hydra precisa ficar aberto até a conclusão. Caso o Hydra encerre antes da conclusão, perderá seu progresso.",
|
||||||
"achievements": "Conquistas",
|
"achievements": "Conquistas",
|
||||||
|
"achievement": "Conquista",
|
||||||
"achievements_count": "Conquistas ({{unlockedCount}}/{{achievementsCount}})",
|
"achievements_count": "Conquistas ({{unlockedCount}}/{{achievementsCount}})",
|
||||||
"cloud_save": "Salvamento em nuvem",
|
"cloud_save": "Salvamento em nuvem",
|
||||||
"cloud_save_description": "Mantenha seu progresso na nuvem e continue de onde parou em qualquer dispositivo",
|
"cloud_save_description": "Mantenha seu progresso na nuvem e continue de onde parou em qualquer dispositivo",
|
||||||
@@ -357,13 +361,13 @@
|
|||||||
},
|
},
|
||||||
"achievement": {
|
"achievement": {
|
||||||
"achievement_unlocked": "Conquista desbloqueada",
|
"achievement_unlocked": "Conquista desbloqueada",
|
||||||
|
"achievement_locked": "Conquista bloqueada",
|
||||||
"your_achievements": "Suas Conquistas",
|
"your_achievements": "Suas Conquistas",
|
||||||
"user_achievements": "Conquistas de {{displayName}}",
|
"user_achievements": "Conquistas de {{displayName}}",
|
||||||
"unlocked_at": "Desbloqueado em:",
|
"unlocked_at": "Desbloqueado em:",
|
||||||
"subscription_needed": "Você precisa de uma assinatura Hydra Cloud para visualizar este conteúdo",
|
"subscription_needed": "Você precisa de uma assinatura Hydra Cloud para visualizar este conteúdo",
|
||||||
"new_achievements_unlocked": "{{achievementCount}} novas conquistas de {{gameCount}} jogos",
|
"new_achievements_unlocked": "{{achievementCount}} novas conquistas de {{gameCount}} jogos",
|
||||||
"achievement_progress": "{{unlockedCount}}/{{totalCount}} conquistas",
|
"aria_achievement_summary": "Conquistas de {{userDisplayName}} em {{gameTitle}}, {{userAchievementCount}} desbloqueadas de {{userTotalAchievementCount}}, {{percentage}} concluídas"
|
||||||
"achievements_unlocked_for_game": "Desbloqueadas {{achievementCount}} novas conquistas em {{gameTitle}}"
|
|
||||||
},
|
},
|
||||||
"tour": {
|
"tour": {
|
||||||
"subscription_tour_title": "Assinatura Hydra Cloud",
|
"subscription_tour_title": "Assinatura Hydra Cloud",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"featured": "Рекомендованное",
|
"featured": "Рекомендованное",
|
||||||
"surprise_me": "Удиви меня",
|
"surprise_me": "Удиви меня",
|
||||||
"no_results": "Ничего не найдено",
|
"no_results": "Ничего не найдено",
|
||||||
"hot": "Сейчас в топе",
|
"hot": "Сейчас жарко",
|
||||||
"start_typing": "Начинаю вводить текст для поиска...",
|
"start_typing": "Начинаю вводить текст для поиска...",
|
||||||
"weekly": "📅 Лучшие игры недели"
|
"weekly": "📅 Лучшие игры недели"
|
||||||
},
|
},
|
||||||
@@ -24,8 +24,7 @@
|
|||||||
"queued": "{{title}} (В очереди)",
|
"queued": "{{title}} (В очереди)",
|
||||||
"game_has_no_executable": "Файл запуска игры не выбран",
|
"game_has_no_executable": "Файл запуска игры не выбран",
|
||||||
"sign_in": "Войти",
|
"sign_in": "Войти",
|
||||||
"friends": "Друзья",
|
"friends": "Друзья"
|
||||||
"need_help": "Нужна помощь?"
|
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"search": "Поиск",
|
"search": "Поиск",
|
||||||
|
|||||||
@@ -19,10 +19,6 @@ export const seedsPath = app.isPackaged
|
|||||||
? path.join(process.resourcesPath, "seeds")
|
? path.join(process.resourcesPath, "seeds")
|
||||||
: path.join(__dirname, "..", "..", "seeds");
|
: path.join(__dirname, "..", "..", "seeds");
|
||||||
|
|
||||||
export const achievementSoundPath = app.isPackaged
|
|
||||||
? path.join(process.resourcesPath, "achievement.wav")
|
|
||||||
: path.join(__dirname, "..", "..", "resources", "achievement.wav");
|
|
||||||
|
|
||||||
export const backupsPath = path.join(app.getPath("userData"), "Backups");
|
export const backupsPath = path.join(app.getPath("userData"), "Backups");
|
||||||
|
|
||||||
export const appVersion = app.getVersion();
|
export const appVersion = app.getVersion();
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
|
import * as Sentry from "@sentry/electron/main";
|
||||||
|
|
||||||
import { userAuthRepository } from "@main/repository";
|
import { userAuthRepository } from "@main/repository";
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
@@ -9,6 +10,8 @@ const getSessionHash = async (_event: Electron.IpcMainInvokeEvent) => {
|
|||||||
if (!auth) return null;
|
if (!auth) return null;
|
||||||
const payload = jwt.decode(auth.accessToken) as jwt.JwtPayload;
|
const payload = jwt.decode(auth.accessToken) as jwt.JwtPayload;
|
||||||
|
|
||||||
|
Sentry.setContext("sessionId", payload.sessionId);
|
||||||
|
|
||||||
return payload.sessionId;
|
return payload.sessionId;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
import * as Sentry from "@sentry/electron/main";
|
||||||
import {
|
import {
|
||||||
DownloadManager,
|
DownloadManager,
|
||||||
HydraApi,
|
HydraApi,
|
||||||
@@ -28,6 +29,9 @@ const signOut = async (_event: Electron.IpcMainInvokeEvent) => {
|
|||||||
gamesPlaytime.clear();
|
gamesPlaytime.clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* Removes user from Sentry */
|
||||||
|
Sentry.setUser(null);
|
||||||
|
|
||||||
/* Cancels any ongoing downloads */
|
/* Cancels any ongoing downloads */
|
||||||
DownloadManager.cancelDownload();
|
DownloadManager.cancelDownload();
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import parseTorrent from "parse-torrent";
|
|
||||||
import type { StartGameDownloadPayload } from "@types";
|
import type { StartGameDownloadPayload } from "@types";
|
||||||
import { DownloadManager, HydraApi, logger } from "@main/services";
|
import { DownloadManager, HydraApi, logger } from "@main/services";
|
||||||
|
|
||||||
@@ -9,7 +9,6 @@ import { createGame } from "@main/services/library-sync";
|
|||||||
import { steamUrlBuilder } from "@shared";
|
import { steamUrlBuilder } from "@shared";
|
||||||
import { dataSource } from "@main/data-source";
|
import { dataSource } from "@main/data-source";
|
||||||
import { DownloadQueue, Game } from "@main/entity";
|
import { DownloadQueue, Game } from "@main/entity";
|
||||||
import { HydraAnalytics } from "@main/services/hydra-analytics";
|
|
||||||
|
|
||||||
const startGameDownload = async (
|
const startGameDownload = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
@@ -91,17 +90,6 @@ const startGameDownload = async (
|
|||||||
logger.error("Failed to create game download", err);
|
logger.error("Failed to create game download", err);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (uri.startsWith("magnet:")) {
|
|
||||||
try {
|
|
||||||
const { infoHash } = await parseTorrent(payload.uri);
|
|
||||||
if (infoHash) {
|
|
||||||
HydraAnalytics.postDownload(infoHash).catch(() => {});
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
logger.error("Failed to parse torrent", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await DownloadManager.cancelDownload(updatedGame!.id);
|
await DownloadManager.cancelDownload(updatedGame!.id);
|
||||||
await DownloadManager.startDownload(updatedGame!);
|
await DownloadManager.startDownload(updatedGame!);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { app, BrowserWindow, net, protocol } from "electron";
|
import { app, BrowserWindow, net, protocol } from "electron";
|
||||||
|
import { init } from "@sentry/electron/main";
|
||||||
import updater from "electron-updater";
|
import updater from "electron-updater";
|
||||||
import i18n from "i18next";
|
import i18n from "i18next";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
@@ -25,6 +26,12 @@ autoUpdater.logger = logger;
|
|||||||
const gotTheLock = app.requestSingleInstanceLock();
|
const gotTheLock = app.requestSingleInstanceLock();
|
||||||
if (!gotTheLock) app.quit();
|
if (!gotTheLock) app.quit();
|
||||||
|
|
||||||
|
if (import.meta.env.MAIN_VITE_SENTRY_DSN) {
|
||||||
|
init({
|
||||||
|
dsn: import.meta.env.MAIN_VITE_SENTRY_DSN,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
app.commandLine.appendSwitch("--no-sandbox");
|
app.commandLine.appendSwitch("--no-sandbox");
|
||||||
|
|
||||||
i18n.init({
|
i18n.init({
|
||||||
@@ -98,6 +105,7 @@ app.whenReady().then(async () => {
|
|||||||
WindowManager.createMainWindow();
|
WindowManager.createMainWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WindowManager.createNotificationWindow();
|
||||||
WindowManager.createSystemTray(userPreferences?.language || "en");
|
WindowManager.createSystemTray(userPreferences?.language || "en");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import type { AchievementFile, UnlockedAchievement } from "@types";
|
|||||||
import { achievementsLogger } from "../logger";
|
import { achievementsLogger } from "../logger";
|
||||||
import { Cracker } from "@shared";
|
import { Cracker } from "@shared";
|
||||||
import { IsNull, Not } from "typeorm";
|
import { IsNull, Not } from "typeorm";
|
||||||
import { publishCombinedNewAchievementNotification } from "../notifications";
|
import { WindowManager } from "../window-manager";
|
||||||
|
|
||||||
const fileStats: Map<string, number> = new Map();
|
const fileStats: Map<string, number> = new Map();
|
||||||
const fltFiles: Map<string, Set<string>> = new Map();
|
const fltFiles: Map<string, Set<string>> = new Map();
|
||||||
@@ -249,12 +249,11 @@ export class AchievementWatcherManager {
|
|||||||
0
|
0
|
||||||
);
|
);
|
||||||
|
|
||||||
if (totalNewAchievements > 0) {
|
WindowManager.notificationWindow?.webContents.send(
|
||||||
publishCombinedNewAchievementNotification(
|
"on-combined-achievements-unlocked",
|
||||||
totalNewAchievements,
|
totalNewGamesWithAchievements,
|
||||||
totalNewGamesWithAchievements
|
totalNewAchievements
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
this.hasFinishedMergingWithRemote = true;
|
this.hasFinishedMergingWithRemote = true;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,12 +8,11 @@ import { HydraApi } from "../hydra-api";
|
|||||||
import { getUnlockedAchievements } from "@main/events/user/get-unlocked-achievements";
|
import { getUnlockedAchievements } from "@main/events/user/get-unlocked-achievements";
|
||||||
import { Game } from "@main/entity";
|
import { Game } from "@main/entity";
|
||||||
import { achievementsLogger } from "../logger";
|
import { achievementsLogger } from "../logger";
|
||||||
import { publishNewAchievementNotification } from "../notifications";
|
|
||||||
|
|
||||||
const saveAchievementsOnLocal = async (
|
const saveAchievementsOnLocal = async (
|
||||||
objectId: string,
|
objectId: string,
|
||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
achievements: UnlockedAchievement[],
|
achievements: any[],
|
||||||
sendUpdateEvent: boolean
|
sendUpdateEvent: boolean
|
||||||
) => {
|
) => {
|
||||||
return gameAchievementRepository
|
return gameAchievementRepository
|
||||||
@@ -83,8 +82,6 @@ export const mergeAchievements = async (
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const mergedLocalAchievements = unlockedAchievements.concat(newAchievements);
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
newAchievements.length &&
|
newAchievements.length &&
|
||||||
publishNotification &&
|
publishNotification &&
|
||||||
@@ -102,7 +99,7 @@ export const mergeAchievements = async (
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.filter((achievement) => Boolean(achievement))
|
.filter((achievement) => achievement)
|
||||||
.map((achievement) => {
|
.map((achievement) => {
|
||||||
return {
|
return {
|
||||||
displayName: achievement!.displayName,
|
displayName: achievement!.displayName,
|
||||||
@@ -110,15 +107,16 @@ export const mergeAchievements = async (
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
publishNewAchievementNotification({
|
WindowManager.notificationWindow?.webContents.send(
|
||||||
achievements: achievementsInfo,
|
"on-achievement-unlocked",
|
||||||
unlockedAchievementCount: mergedLocalAchievements.length,
|
game.objectID,
|
||||||
totalAchievementCount: achievementsData.length,
|
game.shop,
|
||||||
gameTitle: game.title,
|
achievementsInfo
|
||||||
gameIcon: game.iconUrl,
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const mergedLocalAchievements = unlockedAchievements.concat(newAchievements);
|
||||||
|
|
||||||
if (game.remoteId) {
|
if (game.remoteId) {
|
||||||
await HydraApi.put("/profile/games/achievements", {
|
await HydraApi.put("/profile/games/achievements", {
|
||||||
id: game.remoteId,
|
id: game.remoteId,
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
import { userSubscriptionRepository } from "@main/repository";
|
|
||||||
import axios from "axios";
|
|
||||||
import { appVersion } from "@main/constants";
|
|
||||||
|
|
||||||
export class HydraAnalytics {
|
|
||||||
private static instance = axios.create({
|
|
||||||
baseURL: import.meta.env.MAIN_VITE_ANALYTICS_API_URL,
|
|
||||||
headers: { "User-Agent": `Hydra Launcher v${appVersion}` },
|
|
||||||
});
|
|
||||||
|
|
||||||
private static async hasActiveSubscription() {
|
|
||||||
const userSubscription = await userSubscriptionRepository.findOne({
|
|
||||||
where: { id: 1 },
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
userSubscription?.expiresAt && userSubscription.expiresAt > new Date()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static async postDownload(hash: string) {
|
|
||||||
const hasSubscription = await this.hasActiveSubscription();
|
|
||||||
|
|
||||||
return this.instance
|
|
||||||
.post("/track", {
|
|
||||||
event: "download",
|
|
||||||
attributes: {
|
|
||||||
hash,
|
|
||||||
hasSubscription,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((response) => response.data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -24,7 +24,6 @@ export class Ludusavi {
|
|||||||
workerData: {
|
workerData: {
|
||||||
binaryPath: this.binaryPath,
|
binaryPath: this.binaryPath,
|
||||||
},
|
},
|
||||||
maxThreads: 1,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
static async getConfig() {
|
static async getConfig() {
|
||||||
|
|||||||
67
src/main/services/notifications.ts
Normal file
67
src/main/services/notifications.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { Notification, nativeImage } from "electron";
|
||||||
|
import { t } from "i18next";
|
||||||
|
import { parseICO } from "icojs";
|
||||||
|
import trayIcon from "@resources/tray-icon.png?asset";
|
||||||
|
import { Game } from "@main/entity";
|
||||||
|
import { gameRepository, userPreferencesRepository } from "@main/repository";
|
||||||
|
|
||||||
|
const getGameIconNativeImage = async (gameId: number) => {
|
||||||
|
try {
|
||||||
|
const game = await gameRepository.findOne({
|
||||||
|
where: {
|
||||||
|
id: gameId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!game?.iconUrl) return undefined;
|
||||||
|
|
||||||
|
const images = await parseICO(
|
||||||
|
Buffer.from(game.iconUrl.split("base64,")[1], "base64")
|
||||||
|
);
|
||||||
|
|
||||||
|
const highResIcon = images.find((image) => image.width >= 128);
|
||||||
|
if (!highResIcon) return undefined;
|
||||||
|
|
||||||
|
return nativeImage.createFromBuffer(Buffer.from(highResIcon.buffer));
|
||||||
|
} catch (err) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const publishDownloadCompleteNotification = async (game: Game) => {
|
||||||
|
const userPreferences = await userPreferencesRepository.findOne({
|
||||||
|
where: { id: 1 },
|
||||||
|
});
|
||||||
|
|
||||||
|
const icon = await getGameIconNativeImage(game.id);
|
||||||
|
|
||||||
|
if (userPreferences?.downloadNotificationsEnabled) {
|
||||||
|
new Notification({
|
||||||
|
title: t("download_complete", {
|
||||||
|
ns: "notifications",
|
||||||
|
}),
|
||||||
|
body: t("game_ready_to_install", {
|
||||||
|
ns: "notifications",
|
||||||
|
title: game.title,
|
||||||
|
}),
|
||||||
|
icon,
|
||||||
|
}).show();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const publishNotificationUpdateReadyToInstall = async (
|
||||||
|
version: string
|
||||||
|
) => {
|
||||||
|
new Notification({
|
||||||
|
title: t("new_update_available", {
|
||||||
|
ns: "notifications",
|
||||||
|
version,
|
||||||
|
}),
|
||||||
|
body: t("restart_to_install_update", {
|
||||||
|
ns: "notifications",
|
||||||
|
}),
|
||||||
|
icon: trayIcon,
|
||||||
|
}).show();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const publishNewFriendRequestNotification = async () => {};
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
import { Notification, app } from "electron";
|
|
||||||
import { t } from "i18next";
|
|
||||||
import trayIcon from "@resources/tray-icon.png?asset";
|
|
||||||
import { Game } from "@main/entity";
|
|
||||||
import { userPreferencesRepository } from "@main/repository";
|
|
||||||
import fs from "node:fs";
|
|
||||||
import axios from "axios";
|
|
||||||
import path from "node:path";
|
|
||||||
import sound from "sound-play";
|
|
||||||
import { achievementSoundPath } from "@main/constants";
|
|
||||||
import icon from "@resources/icon.png?asset";
|
|
||||||
import { NotificationOptions, toXmlString } from "./xml";
|
|
||||||
import { logger } from "../logger";
|
|
||||||
|
|
||||||
async function downloadImage(url: string | null) {
|
|
||||||
if (!url) return undefined;
|
|
||||||
if (!url.startsWith("http")) return undefined;
|
|
||||||
|
|
||||||
const fileName = url.split("/").pop()!;
|
|
||||||
const outputPath = path.join(app.getPath("temp"), fileName);
|
|
||||||
const writer = fs.createWriteStream(outputPath);
|
|
||||||
|
|
||||||
const response = await axios.get(url, {
|
|
||||||
responseType: "stream",
|
|
||||||
});
|
|
||||||
|
|
||||||
response.data.pipe(writer);
|
|
||||||
|
|
||||||
return new Promise<string | undefined>((resolve) => {
|
|
||||||
writer.on("finish", () => {
|
|
||||||
resolve(outputPath);
|
|
||||||
});
|
|
||||||
writer.on("error", () => {
|
|
||||||
logger.error("Failed to download image", { url });
|
|
||||||
resolve(undefined);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export const publishDownloadCompleteNotification = async (game: Game) => {
|
|
||||||
const userPreferences = await userPreferencesRepository.findOne({
|
|
||||||
where: { id: 1 },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (userPreferences?.downloadNotificationsEnabled) {
|
|
||||||
new Notification({
|
|
||||||
title: t("download_complete", {
|
|
||||||
ns: "notifications",
|
|
||||||
}),
|
|
||||||
body: t("game_ready_to_install", {
|
|
||||||
ns: "notifications",
|
|
||||||
title: game.title,
|
|
||||||
}),
|
|
||||||
icon: await downloadImage(game.iconUrl),
|
|
||||||
}).show();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const publishNotificationUpdateReadyToInstall = async (
|
|
||||||
version: string
|
|
||||||
) => {
|
|
||||||
new Notification({
|
|
||||||
title: t("new_update_available", {
|
|
||||||
ns: "notifications",
|
|
||||||
version,
|
|
||||||
}),
|
|
||||||
body: t("restart_to_install_update", {
|
|
||||||
ns: "notifications",
|
|
||||||
}),
|
|
||||||
icon: trayIcon,
|
|
||||||
}).show();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const publishNewFriendRequestNotification = async () => {};
|
|
||||||
|
|
||||||
export const publishCombinedNewAchievementNotification = async (
|
|
||||||
achievementCount,
|
|
||||||
gameCount
|
|
||||||
) => {
|
|
||||||
const options: NotificationOptions = {
|
|
||||||
title: t("achievement_unlocked", { ns: "achievement" }),
|
|
||||||
body: t("new_achievements_unlocked", {
|
|
||||||
ns: "achievement",
|
|
||||||
gameCount,
|
|
||||||
achievementCount,
|
|
||||||
}),
|
|
||||||
icon,
|
|
||||||
silent: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
new Notification({
|
|
||||||
...options,
|
|
||||||
toastXml: toXmlString(options),
|
|
||||||
}).show();
|
|
||||||
|
|
||||||
if (process.platform !== "linux") {
|
|
||||||
sound.play(achievementSoundPath);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const publishNewAchievementNotification = async (info: {
|
|
||||||
achievements: { displayName: string; iconUrl: string }[];
|
|
||||||
unlockedAchievementCount: number;
|
|
||||||
totalAchievementCount: number;
|
|
||||||
gameTitle: string;
|
|
||||||
gameIcon: string | null;
|
|
||||||
}) => {
|
|
||||||
const partialOptions =
|
|
||||||
info.achievements.length > 1
|
|
||||||
? {
|
|
||||||
title: t("achievements_unlocked_for_game", {
|
|
||||||
ns: "achievement",
|
|
||||||
gameTitle: info.gameTitle,
|
|
||||||
achievementCount: info.achievements.length,
|
|
||||||
}),
|
|
||||||
body: info.achievements.map((a) => a.displayName).join(", "),
|
|
||||||
icon: (await downloadImage(info.gameIcon)) ?? icon,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
title: t("achievement_unlocked", { ns: "achievement" }),
|
|
||||||
body: info.achievements[0].displayName,
|
|
||||||
icon: (await downloadImage(info.achievements[0].iconUrl)) ?? icon,
|
|
||||||
};
|
|
||||||
|
|
||||||
const options: NotificationOptions = {
|
|
||||||
...partialOptions,
|
|
||||||
silent: true,
|
|
||||||
progress: {
|
|
||||||
value: info.unlockedAchievementCount / info.totalAchievementCount,
|
|
||||||
valueOverride: t("achievement_progress", {
|
|
||||||
ns: "achievement",
|
|
||||||
unlockedCount: info.unlockedAchievementCount,
|
|
||||||
totalCount: info.totalAchievementCount,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
new Notification({
|
|
||||||
...options,
|
|
||||||
toastXml: toXmlString(options),
|
|
||||||
}).show();
|
|
||||||
|
|
||||||
if (process.platform !== "linux") {
|
|
||||||
sound.play(achievementSoundPath);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
export interface NotificationOptions {
|
|
||||||
title: string;
|
|
||||||
body?: string;
|
|
||||||
icon: string;
|
|
||||||
duration?: "short" | "long";
|
|
||||||
silent?: boolean;
|
|
||||||
progress?: {
|
|
||||||
status?: string;
|
|
||||||
value: number;
|
|
||||||
valueOverride: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function escape(string: string) {
|
|
||||||
return string.replace(/[<>&'"]/g, (match) => {
|
|
||||||
switch (match) {
|
|
||||||
case "<":
|
|
||||||
return "<";
|
|
||||||
case ">":
|
|
||||||
return ">";
|
|
||||||
case "&":
|
|
||||||
return "&";
|
|
||||||
case "'":
|
|
||||||
return "'";
|
|
||||||
case '"':
|
|
||||||
return """;
|
|
||||||
default:
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function addAttributeOrTrim(name: string, value: string) {
|
|
||||||
return value ? `${name}="${value}" ` : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toXmlString(options: NotificationOptions) {
|
|
||||||
let template =
|
|
||||||
"<toast " +
|
|
||||||
`displayTimestamp="${new Date().toISOString()}" ` +
|
|
||||||
`scenario="default" ` +
|
|
||||||
`duration="${options.duration ?? "short"}" ` +
|
|
||||||
`activationType="protocol" ` +
|
|
||||||
">";
|
|
||||||
|
|
||||||
//Visual
|
|
||||||
template += `<visual><binding template="ToastGeneric">`;
|
|
||||||
if (options.icon)
|
|
||||||
template += `<image placement="appLogoOverride" src="${options.icon}" hint-crop="none"/>`;
|
|
||||||
template +=
|
|
||||||
`<text><![CDATA[${options.title}]]></text>` +
|
|
||||||
`<text><![CDATA[${options.body}]]></text>`;
|
|
||||||
|
|
||||||
//Progress bar
|
|
||||||
if (options.progress) {
|
|
||||||
template +=
|
|
||||||
"<progress " +
|
|
||||||
`value="${options.progress.value}" ` +
|
|
||||||
`status="" ` +
|
|
||||||
addAttributeOrTrim(
|
|
||||||
"valueStringOverride",
|
|
||||||
escape(options.progress.valueOverride)
|
|
||||||
) +
|
|
||||||
"/>";
|
|
||||||
}
|
|
||||||
template += "</binding></visual>";
|
|
||||||
|
|
||||||
//Actions
|
|
||||||
template += "<actions>";
|
|
||||||
template += "</actions>";
|
|
||||||
|
|
||||||
//Audio
|
|
||||||
template += "<audio " + `silent="true" ` + `loop="false" ` + "/>";
|
|
||||||
|
|
||||||
//EOF
|
|
||||||
template += "</toast>";
|
|
||||||
|
|
||||||
return template;
|
|
||||||
}
|
|
||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
userAuthRepository,
|
userAuthRepository,
|
||||||
userSubscriptionRepository,
|
userSubscriptionRepository,
|
||||||
} from "@main/repository";
|
} from "@main/repository";
|
||||||
|
import * as Sentry from "@sentry/electron/main";
|
||||||
import { UserNotLoggedInError } from "@shared";
|
import { UserNotLoggedInError } from "@shared";
|
||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
|
|
||||||
@@ -38,6 +39,8 @@ export const getUserData = () => {
|
|||||||
await userSubscriptionRepository.delete({ id: 1 });
|
await userSubscriptionRepository.delete({ id: 1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Sentry.setUser({ id: me.id, username: me.username });
|
||||||
|
|
||||||
return me;
|
return me;
|
||||||
})
|
})
|
||||||
.catch(async (err) => {
|
.catch(async (err) => {
|
||||||
@@ -56,7 +59,6 @@ export const getUserData = () => {
|
|||||||
id: loggedUser.userId,
|
id: loggedUser.userId,
|
||||||
username: "",
|
username: "",
|
||||||
bio: "",
|
bio: "",
|
||||||
email: null,
|
|
||||||
profileVisibility: "PUBLIC" as ProfileVisibility,
|
profileVisibility: "PUBLIC" as ProfileVisibility,
|
||||||
subscription: loggedUser.subscription
|
subscription: loggedUser.subscription
|
||||||
? {
|
? {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import UserAgent from "user-agents";
|
|||||||
|
|
||||||
export class WindowManager {
|
export class WindowManager {
|
||||||
public static mainWindow: Electron.BrowserWindow | null = null;
|
public static mainWindow: Electron.BrowserWindow | null = null;
|
||||||
|
public static notificationWindow: Electron.BrowserWindow | null = null;
|
||||||
|
|
||||||
private static loadMainWindowURL(hash = "") {
|
private static loadMainWindowURL(hash = "") {
|
||||||
// HMR for renderer base on electron-vite cli.
|
// HMR for renderer base on electron-vite cli.
|
||||||
@@ -38,6 +39,21 @@ export class WindowManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static loadNotificationWindowURL() {
|
||||||
|
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
|
||||||
|
this.notificationWindow?.loadURL(
|
||||||
|
`${process.env["ELECTRON_RENDERER_URL"]}#/achievement-notification`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.notificationWindow?.loadFile(
|
||||||
|
path.join(__dirname, "../renderer/index.html"),
|
||||||
|
{
|
||||||
|
hash: "achievement-notification",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static createMainWindow() {
|
public static createMainWindow() {
|
||||||
if (this.mainWindow) return;
|
if (this.mainWindow) return;
|
||||||
|
|
||||||
@@ -47,7 +63,7 @@ export class WindowManager {
|
|||||||
minWidth: 1024,
|
minWidth: 1024,
|
||||||
minHeight: 540,
|
minHeight: 540,
|
||||||
backgroundColor: "#1c1c1c",
|
backgroundColor: "#1c1c1c",
|
||||||
titleBarStyle: process.platform === "linux" ? "default" : "hidden",
|
titleBarStyle: process.platform === "win32" ? "hidden" : "default",
|
||||||
...(process.platform === "linux" ? { icon } : {}),
|
...(process.platform === "linux" ? { icon } : {}),
|
||||||
trafficLightPosition: { x: 16, y: 16 },
|
trafficLightPosition: { x: 16, y: 16 },
|
||||||
titleBarOverlay: {
|
titleBarOverlay: {
|
||||||
@@ -85,10 +101,6 @@ export class WindowManager {
|
|||||||
return callback(details);
|
return callback(details);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (details.url.includes("intercom.io")) {
|
|
||||||
return callback(details);
|
|
||||||
}
|
|
||||||
|
|
||||||
const headers = {
|
const headers = {
|
||||||
"access-control-allow-origin": ["*"],
|
"access-control-allow-origin": ["*"],
|
||||||
"access-control-allow-methods": ["GET, POST, PUT, DELETE, OPTIONS"],
|
"access-control-allow-methods": ["GET, POST, PUT, DELETE, OPTIONS"],
|
||||||
@@ -139,6 +151,32 @@ export class WindowManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static createNotificationWindow() {
|
||||||
|
this.notificationWindow = new BrowserWindow({
|
||||||
|
transparent: true,
|
||||||
|
maximizable: false,
|
||||||
|
autoHideMenuBar: true,
|
||||||
|
minimizable: false,
|
||||||
|
focusable: false,
|
||||||
|
skipTaskbar: true,
|
||||||
|
frame: false,
|
||||||
|
width: 350,
|
||||||
|
height: 104,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
webPreferences: {
|
||||||
|
preload: path.join(__dirname, "../preload/index.mjs"),
|
||||||
|
sandbox: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.notificationWindow.setIgnoreMouseEvents(true);
|
||||||
|
// this.notificationWindow.setVisibleOnAllWorkspaces(true, {
|
||||||
|
// visibleOnFullScreen: true,
|
||||||
|
// });
|
||||||
|
this.notificationWindow.setAlwaysOnTop(true, "screen-saver", 1);
|
||||||
|
this.loadNotificationWindowURL();
|
||||||
|
}
|
||||||
|
|
||||||
public static openAuthWindow() {
|
public static openAuthWindow() {
|
||||||
if (this.mainWindow) {
|
if (this.mainWindow) {
|
||||||
const authWindow = new BrowserWindow({
|
const authWindow = new BrowserWindow({
|
||||||
|
|||||||
2
src/main/vite-env.d.ts
vendored
2
src/main/vite-env.d.ts
vendored
@@ -3,8 +3,8 @@
|
|||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
readonly MAIN_VITE_STEAMGRIDDB_API_KEY: string;
|
readonly MAIN_VITE_STEAMGRIDDB_API_KEY: string;
|
||||||
readonly MAIN_VITE_API_URL: string;
|
readonly MAIN_VITE_API_URL: string;
|
||||||
readonly MAIN_VITE_ANALYTICS_API_URL: string;
|
|
||||||
readonly MAIN_VITE_AUTH_URL: string;
|
readonly MAIN_VITE_AUTH_URL: string;
|
||||||
|
readonly MAIN_VITE_SENTRY_DSN: string;
|
||||||
readonly MAIN_VITE_CHECKOUT_URL: string;
|
readonly MAIN_VITE_CHECKOUT_URL: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,25 +16,15 @@ export const backupGame = ({
|
|||||||
preview?: boolean;
|
preview?: boolean;
|
||||||
winePrefix?: string;
|
winePrefix?: string;
|
||||||
}) => {
|
}) => {
|
||||||
return new Promise((resolve, reject) => {
|
const args = ["backup", title, "--api", "--force"];
|
||||||
const args = ["backup", title, "--api", "--force"];
|
|
||||||
|
|
||||||
if (preview) args.push("--preview");
|
if (preview) args.push("--preview");
|
||||||
if (backupPath) args.push("--path", backupPath);
|
if (backupPath) args.push("--path", backupPath);
|
||||||
if (winePrefix) args.push("--wine-prefix", winePrefix);
|
if (winePrefix) args.push("--wine-prefix", winePrefix);
|
||||||
|
|
||||||
cp.execFile(
|
const result = cp.execFileSync(binaryPath, args);
|
||||||
binaryPath,
|
|
||||||
args,
|
|
||||||
(err: cp.ExecFileException | null, stdout: string) => {
|
|
||||||
if (err) {
|
|
||||||
return reject(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
return resolve(JSON.parse(stdout) as LudusaviBackup);
|
return JSON.parse(result.toString("utf-8")) as LudusaviBackup;
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const restoreBackup = (backupPath: string) => {
|
export const restoreBackup = (backupPath: string) => {
|
||||||
|
|||||||
@@ -51,6 +51,35 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
getGameStats: (objectId: string, shop: GameShop) =>
|
getGameStats: (objectId: string, shop: GameShop) =>
|
||||||
ipcRenderer.invoke("getGameStats", objectId, shop),
|
ipcRenderer.invoke("getGameStats", objectId, shop),
|
||||||
getTrendingGames: () => ipcRenderer.invoke("getTrendingGames"),
|
getTrendingGames: () => ipcRenderer.invoke("getTrendingGames"),
|
||||||
|
onAchievementUnlocked: (
|
||||||
|
cb: (
|
||||||
|
objectId: string,
|
||||||
|
shop: GameShop,
|
||||||
|
achievements?: { displayName: string; iconUrl: string }[]
|
||||||
|
) => void
|
||||||
|
) => {
|
||||||
|
const listener = (
|
||||||
|
_event: Electron.IpcRendererEvent,
|
||||||
|
objectId: string,
|
||||||
|
shop: GameShop,
|
||||||
|
achievements?: { displayName: string; iconUrl: string }[]
|
||||||
|
) => cb(objectId, shop, achievements);
|
||||||
|
ipcRenderer.on("on-achievement-unlocked", listener);
|
||||||
|
return () =>
|
||||||
|
ipcRenderer.removeListener("on-achievement-unlocked", listener);
|
||||||
|
},
|
||||||
|
onCombinedAchievementsUnlocked: (
|
||||||
|
cb: (gameCount: number, achievementsCount: number) => void
|
||||||
|
) => {
|
||||||
|
const listener = (
|
||||||
|
_event: Electron.IpcRendererEvent,
|
||||||
|
gameCount: number,
|
||||||
|
achievementCount: number
|
||||||
|
) => cb(gameCount, achievementCount);
|
||||||
|
ipcRenderer.on("on-combined-achievements-unlocked", listener);
|
||||||
|
return () =>
|
||||||
|
ipcRenderer.removeListener("on-combined-achievements-unlocked", listener);
|
||||||
|
},
|
||||||
onUpdateAchievements: (
|
onUpdateAchievements: (
|
||||||
objectId: string,
|
objectId: string,
|
||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<title>Hydra</title>
|
<title>Hydra</title>
|
||||||
<meta
|
<meta
|
||||||
http-equiv="Content-Security-Policy"
|
http-equiv="Content-Security-Policy"
|
||||||
content="default-src 'self'; script-src *; style-src 'self' 'unsafe-inline'; img-src 'self' data: local: *; media-src 'self' local: data: *; connect-src *; font-src *;"
|
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: local: *; media-src 'self' local: data: *;"
|
||||||
/>
|
/>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -126,9 +126,3 @@ export const titleBar = style({
|
|||||||
zIndex: "4",
|
zIndex: "4",
|
||||||
borderBottom: `1px solid ${vars.color.border}`,
|
borderBottom: `1px solid ${vars.color.border}`,
|
||||||
} as ComplexStyleRule);
|
} as ComplexStyleRule);
|
||||||
|
|
||||||
export const cloudText = style({
|
|
||||||
background: "linear-gradient(270deg, #16B195 50%, #3E62C0 100%)",
|
|
||||||
backgroundClip: "text",
|
|
||||||
color: "transparent",
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { useCallback, useContext, useEffect, useRef } from "react";
|
import { useCallback, useContext, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
import { Sidebar, BottomPanel, Header, Toast } from "@renderer/components";
|
import { Sidebar, BottomPanel, Header, Toast } from "@renderer/components";
|
||||||
|
|
||||||
import Intercom from "@intercom/messenger-js-sdk";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useAppDispatch,
|
useAppDispatch,
|
||||||
useAppSelector,
|
useAppSelector,
|
||||||
@@ -31,17 +29,16 @@ import { UserFriendModal } from "./pages/shared-modals/user-friend-modal";
|
|||||||
import { downloadSourcesWorker } from "./workers";
|
import { downloadSourcesWorker } from "./workers";
|
||||||
import { repacksContext } from "./context";
|
import { repacksContext } from "./context";
|
||||||
import { logger } from "./logger";
|
import { logger } from "./logger";
|
||||||
|
import { SubscriptionTourModal } from "./pages/shared-modals/subscription-tour-modal";
|
||||||
|
|
||||||
|
interface TourModals {
|
||||||
|
subscriptionModal?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AppProps {
|
export interface AppProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(import.meta.env);
|
|
||||||
|
|
||||||
Intercom({
|
|
||||||
app_id: import.meta.env.RENDERER_VITE_INTERCOM_APP_ID,
|
|
||||||
});
|
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
const contentRef = useRef<HTMLDivElement>(null);
|
const contentRef = useRef<HTMLDivElement>(null);
|
||||||
const { updateLibrary, library } = useLibrary();
|
const { updateLibrary, library } = useLibrary();
|
||||||
@@ -62,13 +59,8 @@ export function App() {
|
|||||||
hideFriendsModal,
|
hideFriendsModal,
|
||||||
} = useUserDetails();
|
} = useUserDetails();
|
||||||
|
|
||||||
const {
|
const { userDetails, fetchUserDetails, updateUserDetails, clearUserDetails } =
|
||||||
userDetails,
|
useUserDetails();
|
||||||
hasActiveSubscription,
|
|
||||||
fetchUserDetails,
|
|
||||||
updateUserDetails,
|
|
||||||
clearUserDetails,
|
|
||||||
} = useUserDetails();
|
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
@@ -85,6 +77,9 @@ export function App() {
|
|||||||
|
|
||||||
const { showSuccessToast } = useToast();
|
const { showSuccessToast } = useToast();
|
||||||
|
|
||||||
|
const [showSubscritionTourModal, setShowSubscritionTourModal] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Promise.all([window.electron.getUserPreferences(), updateLibrary()]).then(
|
Promise.all([window.electron.getUserPreferences(), updateLibrary()]).then(
|
||||||
([preferences]) => {
|
([preferences]) => {
|
||||||
@@ -130,6 +125,16 @@ export function App() {
|
|||||||
});
|
});
|
||||||
}, [fetchUserDetails, syncFriendRequests, updateUserDetails, dispatch]);
|
}, [fetchUserDetails, syncFriendRequests, updateUserDetails, dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const tourModalsString = window.localStorage.getItem("tourModals") || "{}";
|
||||||
|
|
||||||
|
const tourModals = JSON.parse(tourModalsString) as TourModals;
|
||||||
|
|
||||||
|
if (!tourModals.subscriptionModal) {
|
||||||
|
setShowSubscritionTourModal(true);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const onSignIn = useCallback(() => {
|
const onSignIn = useCallback(() => {
|
||||||
fetchUserDetails().then((response) => {
|
fetchUserDetails().then((response) => {
|
||||||
if (response) {
|
if (response) {
|
||||||
@@ -217,9 +222,7 @@ export function App() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
new MutationObserver(() => {
|
new MutationObserver(() => {
|
||||||
const modal = document.body.querySelector(
|
const modal = document.body.querySelector("[role=dialog]");
|
||||||
"[role=dialog]:not([data-intercom-frame='true'])"
|
|
||||||
);
|
|
||||||
|
|
||||||
dispatch(toggleDraggingDisabled(Boolean(modal)));
|
dispatch(toggleDraggingDisabled(Boolean(modal)));
|
||||||
}).observe(document.body, {
|
}).observe(document.body, {
|
||||||
@@ -277,6 +280,14 @@ export function App() {
|
|||||||
});
|
});
|
||||||
}, [indexRepacks]);
|
}, [indexRepacks]);
|
||||||
|
|
||||||
|
const handleCloseSubscriptionTourModal = () => {
|
||||||
|
setShowSubscritionTourModal(false);
|
||||||
|
window.localStorage.setItem(
|
||||||
|
"tourModals",
|
||||||
|
JSON.stringify({ subscriptionModal: true } as TourModals)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const handleToastClose = useCallback(() => {
|
const handleToastClose = useCallback(() => {
|
||||||
dispatch(closeToast());
|
dispatch(closeToast());
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
@@ -285,12 +296,7 @@ export function App() {
|
|||||||
<>
|
<>
|
||||||
{window.electron.platform === "win32" && (
|
{window.electron.platform === "win32" && (
|
||||||
<div className={styles.titleBar}>
|
<div className={styles.titleBar}>
|
||||||
<h4>
|
<h4>Hydra</h4>
|
||||||
Hydra
|
|
||||||
{hasActiveSubscription && (
|
|
||||||
<span className={styles.cloudText}> Cloud</span>
|
|
||||||
)}
|
|
||||||
</h4>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -301,6 +307,11 @@ export function App() {
|
|||||||
onClose={handleToastClose}
|
onClose={handleToastClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<SubscriptionTourModal
|
||||||
|
visible={showSubscritionTourModal && false}
|
||||||
|
onClose={handleCloseSubscriptionTourModal}
|
||||||
|
/>
|
||||||
|
|
||||||
{userDetails && (
|
{userDetails && (
|
||||||
<UserFriendModal
|
<UserFriendModal
|
||||||
visible={isFriendsModalVisible}
|
visible={isFriendsModalVisible}
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 59 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 229 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 68 KiB |
725
src/renderer/src/assets/lottie/cloud.json
Normal file
725
src/renderer/src/assets/lottie/cloud.json
Normal file
@@ -0,0 +1,725 @@
|
|||||||
|
{
|
||||||
|
"v": "5.12.1",
|
||||||
|
"fr": 30,
|
||||||
|
"ip": 0,
|
||||||
|
"op": 60,
|
||||||
|
"w": 400,
|
||||||
|
"h": 400,
|
||||||
|
"nm": "Cloud",
|
||||||
|
"ddd": 0,
|
||||||
|
"assets": [],
|
||||||
|
"layers": [
|
||||||
|
{
|
||||||
|
"ddd": 0,
|
||||||
|
"ind": 2,
|
||||||
|
"ty": 4,
|
||||||
|
"nm": "Layer 6",
|
||||||
|
"sr": 1,
|
||||||
|
"ks": {
|
||||||
|
"o": { "a": 0, "k": 100, "ix": 11 },
|
||||||
|
"r": { "a": 0, "k": 0, "ix": 10 },
|
||||||
|
"p": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{
|
||||||
|
"i": { "x": 0.667, "y": 1 },
|
||||||
|
"o": { "x": 0.333, "y": 0 },
|
||||||
|
"t": 0,
|
||||||
|
"s": [322.789, 202.565, 0],
|
||||||
|
"to": [-1.5, -0.167, 0],
|
||||||
|
"ti": [0, 0, 0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"i": { "x": 0.667, "y": 1 },
|
||||||
|
"o": { "x": 0.333, "y": 0 },
|
||||||
|
"t": 30,
|
||||||
|
"s": [313.789, 201.565, 0],
|
||||||
|
"to": [0, 0, 0],
|
||||||
|
"ti": [-1.5, -0.167, 0]
|
||||||
|
},
|
||||||
|
{ "t": 60, "s": [322.789, 202.565, 0] }
|
||||||
|
],
|
||||||
|
"ix": 2,
|
||||||
|
"l": 2
|
||||||
|
},
|
||||||
|
"a": { "a": 0, "k": [0, 0, 0], "ix": 1, "l": 2 },
|
||||||
|
"s": { "a": 0, "k": [100, 100, 100], "ix": 6, "l": 2 }
|
||||||
|
},
|
||||||
|
"ao": 0,
|
||||||
|
"shapes": [
|
||||||
|
{
|
||||||
|
"ty": "gr",
|
||||||
|
"it": [
|
||||||
|
{
|
||||||
|
"ind": 0,
|
||||||
|
"ty": "sh",
|
||||||
|
"ix": 1,
|
||||||
|
"ks": {
|
||||||
|
"a": 0,
|
||||||
|
"k": {
|
||||||
|
"i": [
|
||||||
|
[0, -38.564],
|
||||||
|
[38.564, 0],
|
||||||
|
[0, 38.564],
|
||||||
|
[-38.564, 0]
|
||||||
|
],
|
||||||
|
"o": [
|
||||||
|
[0, 38.564],
|
||||||
|
[-38.564, 0],
|
||||||
|
[0, -38.564],
|
||||||
|
[38.564, 0]
|
||||||
|
],
|
||||||
|
"v": [
|
||||||
|
[69.827, 0],
|
||||||
|
[0, 69.827],
|
||||||
|
[-69.827, 0],
|
||||||
|
[0, -69.827]
|
||||||
|
],
|
||||||
|
"c": true
|
||||||
|
},
|
||||||
|
"ix": 2
|
||||||
|
},
|
||||||
|
"nm": "Path 1",
|
||||||
|
"mn": "ADBE Vector Shape - Group",
|
||||||
|
"hd": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": "fl",
|
||||||
|
"c": {
|
||||||
|
"a": 0,
|
||||||
|
"k": [0.839215686275, 0.854901960784, 0.933333333333, 1],
|
||||||
|
"ix": 4
|
||||||
|
},
|
||||||
|
"o": { "a": 0, "k": 100, "ix": 5 },
|
||||||
|
"r": 1,
|
||||||
|
"bm": 0,
|
||||||
|
"nm": "Fill 1",
|
||||||
|
"mn": "ADBE Vector Graphic - Fill",
|
||||||
|
"hd": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": "tr",
|
||||||
|
"p": { "a": 0, "k": [0, 0], "ix": 2 },
|
||||||
|
"a": { "a": 0, "k": [0, 0], "ix": 1 },
|
||||||
|
"s": { "a": 0, "k": [100, 100], "ix": 3 },
|
||||||
|
"r": { "a": 0, "k": 0, "ix": 6 },
|
||||||
|
"o": { "a": 0, "k": 100, "ix": 7 },
|
||||||
|
"sk": { "a": 0, "k": 0, "ix": 4 },
|
||||||
|
"sa": { "a": 0, "k": 0, "ix": 5 },
|
||||||
|
"nm": "Transform"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nm": "Group 1",
|
||||||
|
"np": 2,
|
||||||
|
"cix": 2,
|
||||||
|
"bm": 0,
|
||||||
|
"ix": 1,
|
||||||
|
"mn": "ADBE Vector Group",
|
||||||
|
"hd": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ip": 0,
|
||||||
|
"op": 270,
|
||||||
|
"st": 0,
|
||||||
|
"ct": 1,
|
||||||
|
"bm": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ddd": 0,
|
||||||
|
"ind": 3,
|
||||||
|
"ty": 4,
|
||||||
|
"nm": "Layer 5",
|
||||||
|
"sr": 1,
|
||||||
|
"ks": {
|
||||||
|
"o": { "a": 0, "k": 100, "ix": 11 },
|
||||||
|
"r": { "a": 0, "k": 0, "ix": 10 },
|
||||||
|
"p": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{
|
||||||
|
"i": { "x": 0.667, "y": 1 },
|
||||||
|
"o": { "x": 0.333, "y": 0 },
|
||||||
|
"t": 0,
|
||||||
|
"s": [243.704, 202.565, 0],
|
||||||
|
"to": [-1.667, 0, 0],
|
||||||
|
"ti": [0, 0, 0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"i": { "x": 0.667, "y": 1 },
|
||||||
|
"o": { "x": 0.333, "y": 0 },
|
||||||
|
"t": 30,
|
||||||
|
"s": [233.704, 202.565, 0],
|
||||||
|
"to": [0, 0, 0],
|
||||||
|
"ti": [-1.667, 0, 0]
|
||||||
|
},
|
||||||
|
{ "t": 60, "s": [243.704, 202.565, 0] }
|
||||||
|
],
|
||||||
|
"ix": 2,
|
||||||
|
"l": 2
|
||||||
|
},
|
||||||
|
"a": { "a": 0, "k": [0, 0, 0], "ix": 1, "l": 2 },
|
||||||
|
"s": { "a": 0, "k": [100, 100, 100], "ix": 6, "l": 2 }
|
||||||
|
},
|
||||||
|
"ao": 0,
|
||||||
|
"shapes": [
|
||||||
|
{
|
||||||
|
"ty": "gr",
|
||||||
|
"it": [
|
||||||
|
{
|
||||||
|
"ind": 0,
|
||||||
|
"ty": "sh",
|
||||||
|
"ix": 1,
|
||||||
|
"ks": {
|
||||||
|
"a": 0,
|
||||||
|
"k": {
|
||||||
|
"i": [
|
||||||
|
[0, -38.564],
|
||||||
|
[38.564, 0],
|
||||||
|
[0, 38.564],
|
||||||
|
[-38.564, 0]
|
||||||
|
],
|
||||||
|
"o": [
|
||||||
|
[0, 38.564],
|
||||||
|
[-38.564, 0],
|
||||||
|
[0, -38.564],
|
||||||
|
[38.564, 0]
|
||||||
|
],
|
||||||
|
"v": [
|
||||||
|
[69.827, 0],
|
||||||
|
[0, 69.827],
|
||||||
|
[-69.827, 0],
|
||||||
|
[0, -69.827]
|
||||||
|
],
|
||||||
|
"c": true
|
||||||
|
},
|
||||||
|
"ix": 2
|
||||||
|
},
|
||||||
|
"nm": "Path 1",
|
||||||
|
"mn": "ADBE Vector Shape - Group",
|
||||||
|
"hd": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": "fl",
|
||||||
|
"c": {
|
||||||
|
"a": 0,
|
||||||
|
"k": [0.839215686275, 0.854901960784, 0.933333333333, 1],
|
||||||
|
"ix": 4
|
||||||
|
},
|
||||||
|
"o": { "a": 0, "k": 100, "ix": 5 },
|
||||||
|
"r": 1,
|
||||||
|
"bm": 0,
|
||||||
|
"nm": "Fill 1",
|
||||||
|
"mn": "ADBE Vector Graphic - Fill",
|
||||||
|
"hd": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": "tr",
|
||||||
|
"p": { "a": 0, "k": [0, 0], "ix": 2 },
|
||||||
|
"a": { "a": 0, "k": [0, 0], "ix": 1 },
|
||||||
|
"s": { "a": 0, "k": [100, 100], "ix": 3 },
|
||||||
|
"r": { "a": 0, "k": 0, "ix": 6 },
|
||||||
|
"o": { "a": 0, "k": 100, "ix": 7 },
|
||||||
|
"sk": { "a": 0, "k": 0, "ix": 4 },
|
||||||
|
"sa": { "a": 0, "k": 0, "ix": 5 },
|
||||||
|
"nm": "Transform"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nm": "Group 1",
|
||||||
|
"np": 2,
|
||||||
|
"cix": 2,
|
||||||
|
"bm": 0,
|
||||||
|
"ix": 1,
|
||||||
|
"mn": "ADBE Vector Group",
|
||||||
|
"hd": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ip": 0,
|
||||||
|
"op": 270,
|
||||||
|
"st": 0,
|
||||||
|
"ct": 1,
|
||||||
|
"bm": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ddd": 0,
|
||||||
|
"ind": 4,
|
||||||
|
"ty": 4,
|
||||||
|
"nm": "Layer 4",
|
||||||
|
"sr": 1,
|
||||||
|
"ks": {
|
||||||
|
"o": { "a": 0, "k": 100, "ix": 11 },
|
||||||
|
"r": { "a": 0, "k": 0, "ix": 10 },
|
||||||
|
"p": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{
|
||||||
|
"i": { "x": 0.667, "y": 1 },
|
||||||
|
"o": { "x": 0.333, "y": 0 },
|
||||||
|
"t": 0,
|
||||||
|
"s": [260.681, 151.053, 0],
|
||||||
|
"to": [1.333, -1.333, 0],
|
||||||
|
"ti": [0, 0, 0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"i": { "x": 0.667, "y": 1 },
|
||||||
|
"o": { "x": 0.333, "y": 0 },
|
||||||
|
"t": 30,
|
||||||
|
"s": [268.681, 143.053, 0],
|
||||||
|
"to": [0, 0, 0],
|
||||||
|
"ti": [1.333, -1.333, 0]
|
||||||
|
},
|
||||||
|
{ "t": 60, "s": [260.681, 151.053, 0] }
|
||||||
|
],
|
||||||
|
"ix": 2,
|
||||||
|
"l": 2
|
||||||
|
},
|
||||||
|
"a": { "a": 0, "k": [0, 0, 0], "ix": 1, "l": 2 },
|
||||||
|
"s": { "a": 0, "k": [100, 100, 100], "ix": 6, "l": 2 }
|
||||||
|
},
|
||||||
|
"ao": 0,
|
||||||
|
"shapes": [
|
||||||
|
{
|
||||||
|
"ty": "gr",
|
||||||
|
"it": [
|
||||||
|
{
|
||||||
|
"ind": 0,
|
||||||
|
"ty": "sh",
|
||||||
|
"ix": 1,
|
||||||
|
"ks": {
|
||||||
|
"a": 0,
|
||||||
|
"k": {
|
||||||
|
"i": [
|
||||||
|
[0, -38.564],
|
||||||
|
[38.564, 0],
|
||||||
|
[0, 38.564],
|
||||||
|
[-38.564, 0]
|
||||||
|
],
|
||||||
|
"o": [
|
||||||
|
[0, 38.564],
|
||||||
|
[-38.564, 0],
|
||||||
|
[0, -38.564],
|
||||||
|
[38.564, 0]
|
||||||
|
],
|
||||||
|
"v": [
|
||||||
|
[69.827, 0],
|
||||||
|
[0, 69.827],
|
||||||
|
[-69.827, 0],
|
||||||
|
[0, -69.827]
|
||||||
|
],
|
||||||
|
"c": true
|
||||||
|
},
|
||||||
|
"ix": 2
|
||||||
|
},
|
||||||
|
"nm": "Path 1",
|
||||||
|
"mn": "ADBE Vector Shape - Group",
|
||||||
|
"hd": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": "fl",
|
||||||
|
"c": {
|
||||||
|
"a": 0,
|
||||||
|
"k": [0.839215686275, 0.854901960784, 0.933333333333, 1],
|
||||||
|
"ix": 4
|
||||||
|
},
|
||||||
|
"o": { "a": 0, "k": 100, "ix": 5 },
|
||||||
|
"r": 1,
|
||||||
|
"bm": 0,
|
||||||
|
"nm": "Fill 1",
|
||||||
|
"mn": "ADBE Vector Graphic - Fill",
|
||||||
|
"hd": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": "tr",
|
||||||
|
"p": { "a": 0, "k": [0, 0], "ix": 2 },
|
||||||
|
"a": { "a": 0, "k": [0, 0], "ix": 1 },
|
||||||
|
"s": { "a": 0, "k": [100, 100], "ix": 3 },
|
||||||
|
"r": { "a": 0, "k": 0, "ix": 6 },
|
||||||
|
"o": { "a": 0, "k": 100, "ix": 7 },
|
||||||
|
"sk": { "a": 0, "k": 0, "ix": 4 },
|
||||||
|
"sa": { "a": 0, "k": 0, "ix": 5 },
|
||||||
|
"nm": "Transform"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nm": "Group 1",
|
||||||
|
"np": 2,
|
||||||
|
"cix": 2,
|
||||||
|
"bm": 0,
|
||||||
|
"ix": 1,
|
||||||
|
"mn": "ADBE Vector Group",
|
||||||
|
"hd": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ip": 0,
|
||||||
|
"op": 270,
|
||||||
|
"st": 0,
|
||||||
|
"ct": 1,
|
||||||
|
"bm": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ddd": 0,
|
||||||
|
"ind": 5,
|
||||||
|
"ty": 4,
|
||||||
|
"nm": "Layer 3",
|
||||||
|
"sr": 1,
|
||||||
|
"ks": {
|
||||||
|
"o": { "a": 0, "k": 100, "ix": 11 },
|
||||||
|
"r": { "a": 0, "k": 0, "ix": 10 },
|
||||||
|
"p": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{
|
||||||
|
"i": { "x": 0.667, "y": 1 },
|
||||||
|
"o": { "x": 0.333, "y": 0 },
|
||||||
|
"t": 0,
|
||||||
|
"s": [162.135, 206.563, 0],
|
||||||
|
"to": [-0.833, -0.167, 0],
|
||||||
|
"ti": [0, 0, 0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"i": { "x": 0.667, "y": 1 },
|
||||||
|
"o": { "x": 0.333, "y": 0 },
|
||||||
|
"t": 30,
|
||||||
|
"s": [157.135, 205.563, 0],
|
||||||
|
"to": [0, 0, 0],
|
||||||
|
"ti": [-0.833, -0.167, 0]
|
||||||
|
},
|
||||||
|
{ "t": 60, "s": [162.135, 206.563, 0] }
|
||||||
|
],
|
||||||
|
"ix": 2,
|
||||||
|
"l": 2
|
||||||
|
},
|
||||||
|
"a": { "a": 0, "k": [0, 0, 0], "ix": 1, "l": 2 },
|
||||||
|
"s": { "a": 0, "k": [100, 100, 100], "ix": 6, "l": 2 }
|
||||||
|
},
|
||||||
|
"ao": 0,
|
||||||
|
"shapes": [
|
||||||
|
{
|
||||||
|
"ty": "gr",
|
||||||
|
"it": [
|
||||||
|
{
|
||||||
|
"ind": 0,
|
||||||
|
"ty": "sh",
|
||||||
|
"ix": 1,
|
||||||
|
"ks": {
|
||||||
|
"a": 0,
|
||||||
|
"k": {
|
||||||
|
"i": [
|
||||||
|
[0, -36.66],
|
||||||
|
[36.66, 0],
|
||||||
|
[0, 36.66],
|
||||||
|
[-36.66, 0]
|
||||||
|
],
|
||||||
|
"o": [
|
||||||
|
[0, 36.66],
|
||||||
|
[-36.66, 0],
|
||||||
|
[0, -36.66],
|
||||||
|
[36.66, 0]
|
||||||
|
],
|
||||||
|
"v": [
|
||||||
|
[66.378, 0],
|
||||||
|
[0, 66.378],
|
||||||
|
[-66.378, 0],
|
||||||
|
[0, -66.378]
|
||||||
|
],
|
||||||
|
"c": true
|
||||||
|
},
|
||||||
|
"ix": 2
|
||||||
|
},
|
||||||
|
"nm": "Path 1",
|
||||||
|
"mn": "ADBE Vector Shape - Group",
|
||||||
|
"hd": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": "fl",
|
||||||
|
"c": {
|
||||||
|
"a": 0,
|
||||||
|
"k": [0.839215686275, 0.854901960784, 0.933333333333, 1],
|
||||||
|
"ix": 4
|
||||||
|
},
|
||||||
|
"o": { "a": 0, "k": 100, "ix": 5 },
|
||||||
|
"r": 1,
|
||||||
|
"bm": 0,
|
||||||
|
"nm": "Fill 1",
|
||||||
|
"mn": "ADBE Vector Graphic - Fill",
|
||||||
|
"hd": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": "tr",
|
||||||
|
"p": { "a": 0, "k": [0, 0], "ix": 2 },
|
||||||
|
"a": { "a": 0, "k": [0, 0], "ix": 1 },
|
||||||
|
"s": { "a": 0, "k": [100, 100], "ix": 3 },
|
||||||
|
"r": { "a": 0, "k": 0, "ix": 6 },
|
||||||
|
"o": { "a": 0, "k": 100, "ix": 7 },
|
||||||
|
"sk": { "a": 0, "k": 0, "ix": 4 },
|
||||||
|
"sa": { "a": 0, "k": 0, "ix": 5 },
|
||||||
|
"nm": "Transform"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nm": "Group 1",
|
||||||
|
"np": 2,
|
||||||
|
"cix": 2,
|
||||||
|
"bm": 0,
|
||||||
|
"ix": 1,
|
||||||
|
"mn": "ADBE Vector Group",
|
||||||
|
"hd": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ip": 0,
|
||||||
|
"op": 270,
|
||||||
|
"st": 0,
|
||||||
|
"ct": 1,
|
||||||
|
"bm": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ddd": 0,
|
||||||
|
"ind": 6,
|
||||||
|
"ty": 4,
|
||||||
|
"nm": "Layer 2",
|
||||||
|
"sr": 1,
|
||||||
|
"ks": {
|
||||||
|
"o": { "a": 0, "k": 100, "ix": 11 },
|
||||||
|
"r": { "a": 0, "k": 0, "ix": 10 },
|
||||||
|
"p": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{
|
||||||
|
"i": { "x": 0.667, "y": 1 },
|
||||||
|
"o": { "x": 0.333, "y": 0 },
|
||||||
|
"t": 0,
|
||||||
|
"s": [180.178, 132.225, 0],
|
||||||
|
"to": [-0.5, -2.333, 0],
|
||||||
|
"ti": [0, 0, 0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"i": { "x": 0.667, "y": 1 },
|
||||||
|
"o": { "x": 0.333, "y": 0 },
|
||||||
|
"t": 30,
|
||||||
|
"s": [177.178, 118.225, 0],
|
||||||
|
"to": [0, 0, 0],
|
||||||
|
"ti": [-0.5, -2.333, 0]
|
||||||
|
},
|
||||||
|
{ "t": 60, "s": [180.178, 132.225, 0] }
|
||||||
|
],
|
||||||
|
"ix": 2,
|
||||||
|
"l": 2
|
||||||
|
},
|
||||||
|
"a": { "a": 0, "k": [0, 0, 0], "ix": 1, "l": 2 },
|
||||||
|
"s": { "a": 0, "k": [100, 100, 100], "ix": 6, "l": 2 }
|
||||||
|
},
|
||||||
|
"ao": 0,
|
||||||
|
"shapes": [
|
||||||
|
{
|
||||||
|
"ty": "gr",
|
||||||
|
"it": [
|
||||||
|
{
|
||||||
|
"ind": 0,
|
||||||
|
"ty": "sh",
|
||||||
|
"ix": 1,
|
||||||
|
"ks": {
|
||||||
|
"a": 0,
|
||||||
|
"k": {
|
||||||
|
"i": [
|
||||||
|
[0, -50.068],
|
||||||
|
[50.068, 0],
|
||||||
|
[0, 50.068],
|
||||||
|
[-50.068, 0]
|
||||||
|
],
|
||||||
|
"o": [
|
||||||
|
[0, 50.068],
|
||||||
|
[-50.068, 0],
|
||||||
|
[0, -50.068],
|
||||||
|
[50.068, 0]
|
||||||
|
],
|
||||||
|
"v": [
|
||||||
|
[90.655, 0],
|
||||||
|
[0, 90.655],
|
||||||
|
[-90.655, 0],
|
||||||
|
[0, -90.655]
|
||||||
|
],
|
||||||
|
"c": true
|
||||||
|
},
|
||||||
|
"ix": 2
|
||||||
|
},
|
||||||
|
"nm": "Path 1",
|
||||||
|
"mn": "ADBE Vector Shape - Group",
|
||||||
|
"hd": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": "fl",
|
||||||
|
"c": {
|
||||||
|
"a": 0,
|
||||||
|
"k": [0.839215686275, 0.854901960784, 0.933333333333, 1],
|
||||||
|
"ix": 4
|
||||||
|
},
|
||||||
|
"o": { "a": 0, "k": 100, "ix": 5 },
|
||||||
|
"r": 1,
|
||||||
|
"bm": 0,
|
||||||
|
"nm": "Fill 1",
|
||||||
|
"mn": "ADBE Vector Graphic - Fill",
|
||||||
|
"hd": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": "tr",
|
||||||
|
"p": { "a": 0, "k": [0, 0], "ix": 2 },
|
||||||
|
"a": { "a": 0, "k": [0, 0], "ix": 1 },
|
||||||
|
"s": { "a": 0, "k": [100, 100], "ix": 3 },
|
||||||
|
"r": { "a": 0, "k": 0, "ix": 6 },
|
||||||
|
"o": { "a": 0, "k": 100, "ix": 7 },
|
||||||
|
"sk": { "a": 0, "k": 0, "ix": 4 },
|
||||||
|
"sa": { "a": 0, "k": 0, "ix": 5 },
|
||||||
|
"nm": "Transform"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nm": "Group 1",
|
||||||
|
"np": 2,
|
||||||
|
"cix": 2,
|
||||||
|
"bm": 0,
|
||||||
|
"ix": 1,
|
||||||
|
"mn": "ADBE Vector Group",
|
||||||
|
"hd": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ip": 0,
|
||||||
|
"op": 270,
|
||||||
|
"st": 0,
|
||||||
|
"ct": 1,
|
||||||
|
"bm": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ddd": 0,
|
||||||
|
"ind": 7,
|
||||||
|
"ty": 4,
|
||||||
|
"nm": "Layer 1",
|
||||||
|
"sr": 1,
|
||||||
|
"ks": {
|
||||||
|
"o": { "a": 0, "k": 100, "ix": 11 },
|
||||||
|
"r": { "a": 0, "k": 0, "ix": 10 },
|
||||||
|
"p": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{
|
||||||
|
"i": { "x": 0.667, "y": 1 },
|
||||||
|
"o": { "x": 0.333, "y": 0 },
|
||||||
|
"t": 0,
|
||||||
|
"s": [95.756, 208.288, 0],
|
||||||
|
"to": [-1.167, 0, 0],
|
||||||
|
"ti": [0, 0, 0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"i": { "x": 0.667, "y": 1 },
|
||||||
|
"o": { "x": 0.333, "y": 0 },
|
||||||
|
"t": 30,
|
||||||
|
"s": [88.756, 208.288, 0],
|
||||||
|
"to": [0, 0, 0],
|
||||||
|
"ti": [-1.167, 0, 0]
|
||||||
|
},
|
||||||
|
{ "t": 60, "s": [95.756, 208.288, 0] }
|
||||||
|
],
|
||||||
|
"ix": 2,
|
||||||
|
"l": 2
|
||||||
|
},
|
||||||
|
"a": { "a": 0, "k": [0, 0, 0], "ix": 1, "l": 2 },
|
||||||
|
"s": { "a": 0, "k": [100, 100, 100], "ix": 6, "l": 2 }
|
||||||
|
},
|
||||||
|
"ao": 0,
|
||||||
|
"shapes": [
|
||||||
|
{
|
||||||
|
"ty": "gr",
|
||||||
|
"it": [
|
||||||
|
{
|
||||||
|
"ind": 0,
|
||||||
|
"ty": "sh",
|
||||||
|
"ix": 1,
|
||||||
|
"ks": {
|
||||||
|
"a": 0,
|
||||||
|
"k": {
|
||||||
|
"i": [
|
||||||
|
[0, -35.403],
|
||||||
|
[35.403, 0],
|
||||||
|
[0, 35.403],
|
||||||
|
[-35.403, 0]
|
||||||
|
],
|
||||||
|
"o": [
|
||||||
|
[0, 35.403],
|
||||||
|
[-35.403, 0],
|
||||||
|
[0, -35.403],
|
||||||
|
[35.403, 0]
|
||||||
|
],
|
||||||
|
"v": [
|
||||||
|
[64.103, 0],
|
||||||
|
[0, 64.103],
|
||||||
|
[-64.103, 0],
|
||||||
|
[0, -64.103]
|
||||||
|
],
|
||||||
|
"c": true
|
||||||
|
},
|
||||||
|
"ix": 2
|
||||||
|
},
|
||||||
|
"nm": "Path 1",
|
||||||
|
"mn": "ADBE Vector Shape - Group",
|
||||||
|
"hd": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": "fl",
|
||||||
|
"c": {
|
||||||
|
"a": 0,
|
||||||
|
"k": [0.839215686275, 0.854901960784, 0.933333333333, 1],
|
||||||
|
"ix": 4
|
||||||
|
},
|
||||||
|
"o": { "a": 0, "k": 100, "ix": 5 },
|
||||||
|
"r": 1,
|
||||||
|
"bm": 0,
|
||||||
|
"nm": "Fill 1",
|
||||||
|
"mn": "ADBE Vector Graphic - Fill",
|
||||||
|
"hd": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": "tr",
|
||||||
|
"p": { "a": 0, "k": [0, 0], "ix": 2 },
|
||||||
|
"a": { "a": 0, "k": [0, 0], "ix": 1 },
|
||||||
|
"s": { "a": 0, "k": [100, 100], "ix": 3 },
|
||||||
|
"r": { "a": 0, "k": 0, "ix": 6 },
|
||||||
|
"o": { "a": 0, "k": 100, "ix": 7 },
|
||||||
|
"sk": { "a": 0, "k": 0, "ix": 4 },
|
||||||
|
"sa": { "a": 0, "k": 0, "ix": 5 },
|
||||||
|
"nm": "Transform"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nm": "Group 1",
|
||||||
|
"np": 2,
|
||||||
|
"cix": 2,
|
||||||
|
"bm": 0,
|
||||||
|
"ix": 1,
|
||||||
|
"mn": "ADBE Vector Group",
|
||||||
|
"hd": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ip": 0,
|
||||||
|
"op": 270,
|
||||||
|
"st": 0,
|
||||||
|
"ct": 1,
|
||||||
|
"bm": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ddd": 0,
|
||||||
|
"ind": 8,
|
||||||
|
"ty": 3,
|
||||||
|
"nm": "Null 1",
|
||||||
|
"parent": 6,
|
||||||
|
"sr": 1,
|
||||||
|
"ks": {
|
||||||
|
"o": { "a": 0, "k": 0, "ix": 11 },
|
||||||
|
"r": { "a": 0, "k": 0, "ix": 10 },
|
||||||
|
"p": { "a": 0, "k": [19.822, 67.775, 0], "ix": 2, "l": 2 },
|
||||||
|
"a": { "a": 0, "k": [0, 0, 0], "ix": 1, "l": 2 },
|
||||||
|
"s": { "a": 0, "k": [100, 100, 100], "ix": 6, "l": 2 }
|
||||||
|
},
|
||||||
|
"ao": 0,
|
||||||
|
"ip": 0,
|
||||||
|
"op": 270,
|
||||||
|
"st": 0,
|
||||||
|
"bm": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"markers": [],
|
||||||
|
"props": {}
|
||||||
|
}
|
||||||
1735
src/renderer/src/assets/lottie/flame.json
Normal file
1735
src/renderer/src/assets/lottie/flame.json
Normal file
File diff suppressed because it is too large
Load Diff
1054
src/renderer/src/assets/lottie/settings.json
Normal file
1054
src/renderer/src/assets/lottie/settings.json
Normal file
File diff suppressed because it is too large
Load Diff
928
src/renderer/src/assets/lottie/stars.json
Normal file
928
src/renderer/src/assets/lottie/stars.json
Normal file
@@ -0,0 +1,928 @@
|
|||||||
|
{
|
||||||
|
"v": "4.8.0",
|
||||||
|
"meta": { "g": "LottieFiles AE 3.5.6", "a": "", "k": "", "d": "", "tc": "" },
|
||||||
|
"fr": 60,
|
||||||
|
"ip": 0,
|
||||||
|
"op": 120,
|
||||||
|
"w": 300,
|
||||||
|
"h": 300,
|
||||||
|
"nm": "Comp 1",
|
||||||
|
"ddd": 0,
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"id": "comp_0",
|
||||||
|
"layers": [
|
||||||
|
{
|
||||||
|
"ddd": 0,
|
||||||
|
"ind": 1,
|
||||||
|
"ty": 5,
|
||||||
|
"nm": "3",
|
||||||
|
"sr": 1,
|
||||||
|
"ks": {
|
||||||
|
"o": { "a": 0, "k": 100, "ix": 11 },
|
||||||
|
"r": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{
|
||||||
|
"i": { "x": [0.055], "y": [1] },
|
||||||
|
"o": { "x": [0.333], "y": [0] },
|
||||||
|
"t": 0,
|
||||||
|
"s": [0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"i": { "x": [0.055], "y": [1] },
|
||||||
|
"o": { "x": [0.333], "y": [0] },
|
||||||
|
"t": 30,
|
||||||
|
"s": [8]
|
||||||
|
},
|
||||||
|
{ "t": 60, "s": [0] }
|
||||||
|
],
|
||||||
|
"ix": 10
|
||||||
|
},
|
||||||
|
"p": { "a": 0, "k": [930, 525, 0], "ix": 2 },
|
||||||
|
"a": { "a": 0, "k": [16.605, -23.904, 0], "ix": 1 },
|
||||||
|
"s": { "a": 0, "k": [170, 170, 100], "ix": 6 }
|
||||||
|
},
|
||||||
|
"ao": 0,
|
||||||
|
"hasMask": true,
|
||||||
|
"masksProperties": [
|
||||||
|
{
|
||||||
|
"inv": false,
|
||||||
|
"mode": "a",
|
||||||
|
"pt": {
|
||||||
|
"a": 0,
|
||||||
|
"k": {
|
||||||
|
"i": [
|
||||||
|
[0, 0],
|
||||||
|
[0, 0],
|
||||||
|
[0, 0],
|
||||||
|
[0, 0],
|
||||||
|
[0, 0],
|
||||||
|
[0, 0]
|
||||||
|
],
|
||||||
|
"o": [
|
||||||
|
[0, 0],
|
||||||
|
[0, 0],
|
||||||
|
[0, 0],
|
||||||
|
[0, 0],
|
||||||
|
[0, 0],
|
||||||
|
[0, 0]
|
||||||
|
],
|
||||||
|
"v": [
|
||||||
|
[14.987, -34.426],
|
||||||
|
[9.105, -30.309],
|
||||||
|
[9.987, -22.073],
|
||||||
|
[17.487, -16.779],
|
||||||
|
[24.105, -23.544],
|
||||||
|
[22.193, -30.603]
|
||||||
|
],
|
||||||
|
"c": true
|
||||||
|
},
|
||||||
|
"ix": 1
|
||||||
|
},
|
||||||
|
"o": { "a": 0, "k": 100, "ix": 3 },
|
||||||
|
"x": { "a": 0, "k": 0, "ix": 4 },
|
||||||
|
"nm": "Mask 1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ef": [
|
||||||
|
{
|
||||||
|
"ty": 21,
|
||||||
|
"nm": "Fill",
|
||||||
|
"np": 9,
|
||||||
|
"mn": "ADBE Fill",
|
||||||
|
"ix": 1,
|
||||||
|
"en": 1,
|
||||||
|
"ef": [
|
||||||
|
{
|
||||||
|
"ty": 10,
|
||||||
|
"nm": "Fill Mask",
|
||||||
|
"mn": "ADBE Fill-0001",
|
||||||
|
"ix": 1,
|
||||||
|
"v": { "a": 0, "k": 0, "ix": 1 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": 7,
|
||||||
|
"nm": "All Masks",
|
||||||
|
"mn": "ADBE Fill-0007",
|
||||||
|
"ix": 2,
|
||||||
|
"v": { "a": 0, "k": 0, "ix": 2 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": 2,
|
||||||
|
"nm": "Color",
|
||||||
|
"mn": "ADBE Fill-0002",
|
||||||
|
"ix": 3,
|
||||||
|
"v": {
|
||||||
|
"a": 0,
|
||||||
|
"k": [0.992156863213, 0.880375564098, 0.128396704793, 1],
|
||||||
|
"ix": 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": 7,
|
||||||
|
"nm": "Invert",
|
||||||
|
"mn": "ADBE Fill-0006",
|
||||||
|
"ix": 4,
|
||||||
|
"v": { "a": 0, "k": 0, "ix": 4 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": 0,
|
||||||
|
"nm": "Horizontal Feather",
|
||||||
|
"mn": "ADBE Fill-0003",
|
||||||
|
"ix": 5,
|
||||||
|
"v": { "a": 0, "k": 0, "ix": 5 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": 0,
|
||||||
|
"nm": "Vertical Feather",
|
||||||
|
"mn": "ADBE Fill-0004",
|
||||||
|
"ix": 6,
|
||||||
|
"v": { "a": 0, "k": 0, "ix": 6 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": 0,
|
||||||
|
"nm": "Opacity",
|
||||||
|
"mn": "ADBE Fill-0005",
|
||||||
|
"ix": 7,
|
||||||
|
"v": { "a": 0, "k": 1, "ix": 7 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"t": {
|
||||||
|
"d": {
|
||||||
|
"k": [
|
||||||
|
{
|
||||||
|
"s": {
|
||||||
|
"s": 40,
|
||||||
|
"f": "SegoeUIEmoji",
|
||||||
|
"t": "✨",
|
||||||
|
"j": 0,
|
||||||
|
"tr": 0,
|
||||||
|
"lh": 48,
|
||||||
|
"ls": 0,
|
||||||
|
"fc": [1, 1, 1]
|
||||||
|
},
|
||||||
|
"t": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"p": {},
|
||||||
|
"m": { "g": 1, "a": { "a": 0, "k": [0, 0], "ix": 2 } },
|
||||||
|
"a": []
|
||||||
|
},
|
||||||
|
"ip": 0,
|
||||||
|
"op": 123,
|
||||||
|
"st": 0,
|
||||||
|
"bm": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ddd": 0,
|
||||||
|
"ind": 2,
|
||||||
|
"ty": 5,
|
||||||
|
"nm": "2",
|
||||||
|
"sr": 1,
|
||||||
|
"ks": {
|
||||||
|
"o": { "a": 0, "k": 100, "ix": 11 },
|
||||||
|
"r": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{
|
||||||
|
"i": { "x": [0.055], "y": [1] },
|
||||||
|
"o": { "x": [0.333], "y": [0] },
|
||||||
|
"t": 0,
|
||||||
|
"s": [0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"i": { "x": [0.055], "y": [1] },
|
||||||
|
"o": { "x": [0.333], "y": [0] },
|
||||||
|
"t": 30,
|
||||||
|
"s": [-8]
|
||||||
|
},
|
||||||
|
{ "t": 60, "s": [0] }
|
||||||
|
],
|
||||||
|
"ix": 10
|
||||||
|
},
|
||||||
|
"p": { "a": 0, "k": [960, 540, 0], "ix": 2 },
|
||||||
|
"a": { "a": 0, "k": [31.912, -13.397, 0], "ix": 1 },
|
||||||
|
"s": { "a": 0, "k": [170, 170, 100], "ix": 6 }
|
||||||
|
},
|
||||||
|
"ao": 0,
|
||||||
|
"hasMask": true,
|
||||||
|
"masksProperties": [
|
||||||
|
{
|
||||||
|
"inv": false,
|
||||||
|
"mode": "a",
|
||||||
|
"pt": {
|
||||||
|
"a": 0,
|
||||||
|
"k": {
|
||||||
|
"i": [
|
||||||
|
[0, 0],
|
||||||
|
[0, 0],
|
||||||
|
[0, 0],
|
||||||
|
[0, 0],
|
||||||
|
[0, 0],
|
||||||
|
[0, 0],
|
||||||
|
[0, 0],
|
||||||
|
[0, 0],
|
||||||
|
[0, 0]
|
||||||
|
],
|
||||||
|
"o": [
|
||||||
|
[0, 0],
|
||||||
|
[0, 0],
|
||||||
|
[0, 0],
|
||||||
|
[0, 0],
|
||||||
|
[0, 0],
|
||||||
|
[0, 0],
|
||||||
|
[0, 0],
|
||||||
|
[0, 0],
|
||||||
|
[0, 0]
|
||||||
|
],
|
||||||
|
"v": [
|
||||||
|
[31.31, -34.72],
|
||||||
|
[24.546, -22.514],
|
||||||
|
[16.605, -16.485],
|
||||||
|
[17.046, -11.338],
|
||||||
|
[21.163, -7.073],
|
||||||
|
[27.487, -0.309],
|
||||||
|
[33.663, 10.133],
|
||||||
|
[47.634, -1.926],
|
||||||
|
[51.31, -12.073]
|
||||||
|
],
|
||||||
|
"c": true
|
||||||
|
},
|
||||||
|
"ix": 1
|
||||||
|
},
|
||||||
|
"o": { "a": 0, "k": 100, "ix": 3 },
|
||||||
|
"x": { "a": 0, "k": 0, "ix": 4 },
|
||||||
|
"nm": "Mask 1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ef": [
|
||||||
|
{
|
||||||
|
"ty": 21,
|
||||||
|
"nm": "Fill",
|
||||||
|
"np": 9,
|
||||||
|
"mn": "ADBE Fill",
|
||||||
|
"ix": 1,
|
||||||
|
"en": 1,
|
||||||
|
"ef": [
|
||||||
|
{
|
||||||
|
"ty": 10,
|
||||||
|
"nm": "Fill Mask",
|
||||||
|
"mn": "ADBE Fill-0001",
|
||||||
|
"ix": 1,
|
||||||
|
"v": { "a": 0, "k": 0, "ix": 1 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": 7,
|
||||||
|
"nm": "All Masks",
|
||||||
|
"mn": "ADBE Fill-0007",
|
||||||
|
"ix": 2,
|
||||||
|
"v": { "a": 0, "k": 0, "ix": 2 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": 2,
|
||||||
|
"nm": "Color",
|
||||||
|
"mn": "ADBE Fill-0002",
|
||||||
|
"ix": 3,
|
||||||
|
"v": {
|
||||||
|
"a": 0,
|
||||||
|
"k": [0.992156863213, 0.880375564098, 0.128396704793, 1],
|
||||||
|
"ix": 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": 7,
|
||||||
|
"nm": "Invert",
|
||||||
|
"mn": "ADBE Fill-0006",
|
||||||
|
"ix": 4,
|
||||||
|
"v": { "a": 0, "k": 0, "ix": 4 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": 0,
|
||||||
|
"nm": "Horizontal Feather",
|
||||||
|
"mn": "ADBE Fill-0003",
|
||||||
|
"ix": 5,
|
||||||
|
"v": { "a": 0, "k": 0, "ix": 5 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": 0,
|
||||||
|
"nm": "Vertical Feather",
|
||||||
|
"mn": "ADBE Fill-0004",
|
||||||
|
"ix": 6,
|
||||||
|
"v": { "a": 0, "k": 0, "ix": 6 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": 0,
|
||||||
|
"nm": "Opacity",
|
||||||
|
"mn": "ADBE Fill-0005",
|
||||||
|
"ix": 7,
|
||||||
|
"v": { "a": 0, "k": 1, "ix": 7 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"t": {
|
||||||
|
"d": {
|
||||||
|
"k": [
|
||||||
|
{
|
||||||
|
"s": {
|
||||||
|
"s": 40,
|
||||||
|
"f": "SegoeUIEmoji",
|
||||||
|
"t": "✨",
|
||||||
|
"j": 0,
|
||||||
|
"tr": 0,
|
||||||
|
"lh": 48,
|
||||||
|
"ls": 0,
|
||||||
|
"fc": [1, 1, 1]
|
||||||
|
},
|
||||||
|
"t": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"p": {},
|
||||||
|
"m": { "g": 1, "a": { "a": 0, "k": [0, 0], "ix": 2 } },
|
||||||
|
"a": []
|
||||||
|
},
|
||||||
|
"ip": 0,
|
||||||
|
"op": 123,
|
||||||
|
"st": 0,
|
||||||
|
"bm": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ddd": 0,
|
||||||
|
"ind": 3,
|
||||||
|
"ty": 5,
|
||||||
|
"nm": "✨",
|
||||||
|
"sr": 1,
|
||||||
|
"ks": {
|
||||||
|
"o": { "a": 0, "k": 100, "ix": 11 },
|
||||||
|
"r": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{
|
||||||
|
"i": { "x": [0.055], "y": [1] },
|
||||||
|
"o": { "x": [0.333], "y": [0] },
|
||||||
|
"t": 0,
|
||||||
|
"s": [0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"i": { "x": [0.055], "y": [1] },
|
||||||
|
"o": { "x": [0.333], "y": [0] },
|
||||||
|
"t": 30,
|
||||||
|
"s": [8]
|
||||||
|
},
|
||||||
|
{ "t": 60, "s": [0] }
|
||||||
|
],
|
||||||
|
"ix": 10
|
||||||
|
},
|
||||||
|
"p": { "a": 0, "k": [935, 560, 0], "ix": 2 },
|
||||||
|
"a": { "a": 0, "k": [14.973, -6.64, 0], "ix": 1 },
|
||||||
|
"s": { "a": 0, "k": [170, 170, 100], "ix": 6 }
|
||||||
|
},
|
||||||
|
"ao": 0,
|
||||||
|
"hasMask": true,
|
||||||
|
"masksProperties": [
|
||||||
|
{
|
||||||
|
"inv": false,
|
||||||
|
"mode": "a",
|
||||||
|
"pt": {
|
||||||
|
"a": 0,
|
||||||
|
"k": {
|
||||||
|
"i": [
|
||||||
|
[0, 0],
|
||||||
|
[0, 0],
|
||||||
|
[0, 0],
|
||||||
|
[0, 0],
|
||||||
|
[0, 0],
|
||||||
|
[0, 0],
|
||||||
|
[0, 0]
|
||||||
|
],
|
||||||
|
"o": [
|
||||||
|
[0, 0],
|
||||||
|
[0, 0],
|
||||||
|
[0, 0],
|
||||||
|
[0, 0],
|
||||||
|
[0, 0],
|
||||||
|
[0, 0],
|
||||||
|
[0, 0]
|
||||||
|
],
|
||||||
|
"v": [
|
||||||
|
[13.957, -17.514],
|
||||||
|
[2.928, -9.132],
|
||||||
|
[2.487, 1.603],
|
||||||
|
[14.105, 7.339],
|
||||||
|
[21.605, -0.161],
|
||||||
|
[22.193, -5.161],
|
||||||
|
[17.34, -10.014]
|
||||||
|
],
|
||||||
|
"c": true
|
||||||
|
},
|
||||||
|
"ix": 1
|
||||||
|
},
|
||||||
|
"o": { "a": 0, "k": 100, "ix": 3 },
|
||||||
|
"x": { "a": 0, "k": 0, "ix": 4 },
|
||||||
|
"nm": "Mask 1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ef": [
|
||||||
|
{
|
||||||
|
"ty": 21,
|
||||||
|
"nm": "Fill",
|
||||||
|
"np": 9,
|
||||||
|
"mn": "ADBE Fill",
|
||||||
|
"ix": 1,
|
||||||
|
"en": 1,
|
||||||
|
"ef": [
|
||||||
|
{
|
||||||
|
"ty": 10,
|
||||||
|
"nm": "Fill Mask",
|
||||||
|
"mn": "ADBE Fill-0001",
|
||||||
|
"ix": 1,
|
||||||
|
"v": { "a": 0, "k": 0, "ix": 1 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": 7,
|
||||||
|
"nm": "All Masks",
|
||||||
|
"mn": "ADBE Fill-0007",
|
||||||
|
"ix": 2,
|
||||||
|
"v": { "a": 0, "k": 0, "ix": 2 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": 2,
|
||||||
|
"nm": "Color",
|
||||||
|
"mn": "ADBE Fill-0002",
|
||||||
|
"ix": 3,
|
||||||
|
"v": {
|
||||||
|
"a": 0,
|
||||||
|
"k": [0.992156863213, 0.880375564098, 0.128396704793, 1],
|
||||||
|
"ix": 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": 7,
|
||||||
|
"nm": "Invert",
|
||||||
|
"mn": "ADBE Fill-0006",
|
||||||
|
"ix": 4,
|
||||||
|
"v": { "a": 0, "k": 0, "ix": 4 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": 0,
|
||||||
|
"nm": "Horizontal Feather",
|
||||||
|
"mn": "ADBE Fill-0003",
|
||||||
|
"ix": 5,
|
||||||
|
"v": { "a": 0, "k": 0, "ix": 5 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": 0,
|
||||||
|
"nm": "Vertical Feather",
|
||||||
|
"mn": "ADBE Fill-0004",
|
||||||
|
"ix": 6,
|
||||||
|
"v": { "a": 0, "k": 0, "ix": 6 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": 0,
|
||||||
|
"nm": "Opacity",
|
||||||
|
"mn": "ADBE Fill-0005",
|
||||||
|
"ix": 7,
|
||||||
|
"v": { "a": 0, "k": 1, "ix": 7 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"t": {
|
||||||
|
"d": {
|
||||||
|
"k": [
|
||||||
|
{
|
||||||
|
"s": {
|
||||||
|
"s": 40,
|
||||||
|
"f": "SegoeUIEmoji",
|
||||||
|
"t": "✨",
|
||||||
|
"j": 0,
|
||||||
|
"tr": 0,
|
||||||
|
"lh": 48,
|
||||||
|
"ls": 0,
|
||||||
|
"fc": [1, 1, 1]
|
||||||
|
},
|
||||||
|
"t": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"p": {},
|
||||||
|
"m": { "g": 1, "a": { "a": 0, "k": [0, 0], "ix": 2 } },
|
||||||
|
"a": []
|
||||||
|
},
|
||||||
|
"ip": 0,
|
||||||
|
"op": 123,
|
||||||
|
"st": 0,
|
||||||
|
"bm": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fonts": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"fName": "SegoeUIEmoji",
|
||||||
|
"fFamily": "Segoe UI Emoji",
|
||||||
|
"fStyle": "Regular",
|
||||||
|
"ascent": 74.0234375
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"layers": [
|
||||||
|
{
|
||||||
|
"ddd": 0,
|
||||||
|
"ind": 1,
|
||||||
|
"ty": 0,
|
||||||
|
"nm": "botão",
|
||||||
|
"refId": "comp_0",
|
||||||
|
"sr": 1,
|
||||||
|
"ks": {
|
||||||
|
"o": { "a": 0, "k": 100, "ix": 11 },
|
||||||
|
"r": { "a": 0, "k": 0, "ix": 10 },
|
||||||
|
"p": { "a": 0, "k": [155, 154, 0], "ix": 2 },
|
||||||
|
"a": { "a": 0, "k": [960, 540, 0], "ix": 1 },
|
||||||
|
"s": { "a": 0, "k": [100, 100, 100], "ix": 6 }
|
||||||
|
},
|
||||||
|
"ao": 0,
|
||||||
|
"ef": [
|
||||||
|
{
|
||||||
|
"ty": 25,
|
||||||
|
"nm": "Drop Shadow",
|
||||||
|
"np": 8,
|
||||||
|
"mn": "ADBE Drop Shadow",
|
||||||
|
"ix": 1,
|
||||||
|
"en": 1,
|
||||||
|
"ef": [
|
||||||
|
{
|
||||||
|
"ty": 2,
|
||||||
|
"nm": "Shadow Color",
|
||||||
|
"mn": "ADBE Drop Shadow-0001",
|
||||||
|
"ix": 1,
|
||||||
|
"v": {
|
||||||
|
"a": 0,
|
||||||
|
"k": [1, 0.829733371735, 0.414901971817, 1],
|
||||||
|
"ix": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": 0,
|
||||||
|
"nm": "Opacity",
|
||||||
|
"mn": "ADBE Drop Shadow-0002",
|
||||||
|
"ix": 2,
|
||||||
|
"v": {
|
||||||
|
"a": 1,
|
||||||
|
"k": [
|
||||||
|
{
|
||||||
|
"i": { "x": [0], "y": [1] },
|
||||||
|
"o": { "x": [0.333], "y": [0] },
|
||||||
|
"t": 0,
|
||||||
|
"s": [127.5]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"i": { "x": [0], "y": [1] },
|
||||||
|
"o": { "x": [0.333], "y": [0] },
|
||||||
|
"t": 15,
|
||||||
|
"s": [204]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"i": { "x": [0], "y": [1] },
|
||||||
|
"o": { "x": [0.333], "y": [0] },
|
||||||
|
"t": 30,
|
||||||
|
"s": [127.5]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"i": { "x": [0], "y": [1] },
|
||||||
|
"o": { "x": [0.333], "y": [0] },
|
||||||
|
"t": 45,
|
||||||
|
"s": [204]
|
||||||
|
},
|
||||||
|
{ "t": 70, "s": [76.5] }
|
||||||
|
],
|
||||||
|
"ix": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": 0,
|
||||||
|
"nm": "Direction",
|
||||||
|
"mn": "ADBE Drop Shadow-0003",
|
||||||
|
"ix": 3,
|
||||||
|
"v": { "a": 0, "k": 135, "ix": 3 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": 0,
|
||||||
|
"nm": "Distance",
|
||||||
|
"mn": "ADBE Drop Shadow-0004",
|
||||||
|
"ix": 4,
|
||||||
|
"v": { "a": 0, "k": 0, "ix": 4 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": 0,
|
||||||
|
"nm": "Softness",
|
||||||
|
"mn": "ADBE Drop Shadow-0005",
|
||||||
|
"ix": 5,
|
||||||
|
"v": { "a": 0, "k": 40, "ix": 5 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ty": 7,
|
||||||
|
"nm": "Shadow Only",
|
||||||
|
"mn": "ADBE Drop Shadow-0006",
|
||||||
|
"ix": 6,
|
||||||
|
"v": { "a": 0, "k": 0, "ix": 6 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"w": 1920,
|
||||||
|
"h": 1080,
|
||||||
|
"ip": 0,
|
||||||
|
"op": 120,
|
||||||
|
"st": 0,
|
||||||
|
"bm": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"markers": [],
|
||||||
|
"chars": [
|
||||||
|
{
|
||||||
|
"ch": "✨",
|
||||||
|
"size": 40,
|
||||||
|
"style": "Regular",
|
||||||
|
"w": 137.3,
|
||||||
|
"data": {
|
||||||
|
"shapes": [
|
||||||
|
{
|
||||||
|
"ty": "gr",
|
||||||
|
"it": [
|
||||||
|
{
|
||||||
|
"ind": 0,
|
||||||
|
"ty": "sh",
|
||||||
|
"ix": 1,
|
||||||
|
"ks": {
|
||||||
|
"a": 0,
|
||||||
|
"k": {
|
||||||
|
"i": [
|
||||||
|
[0.423, 1.042],
|
||||||
|
[0, 0],
|
||||||
|
[0.7, 0],
|
||||||
|
[0.293, -0.618],
|
||||||
|
[0, 0],
|
||||||
|
[1.041, -0.488],
|
||||||
|
[0, 0],
|
||||||
|
[0, -0.684],
|
||||||
|
[-0.652, -0.293],
|
||||||
|
[0, 0],
|
||||||
|
[-0.423, -1.041],
|
||||||
|
[0, 0],
|
||||||
|
[-0.716, 0],
|
||||||
|
[-0.293, 0.619],
|
||||||
|
[0, 0],
|
||||||
|
[-1.042, 0.488],
|
||||||
|
[0, 0],
|
||||||
|
[0, 0.684],
|
||||||
|
[0.618, 0.293],
|
||||||
|
[0, 0]
|
||||||
|
],
|
||||||
|
"o": [
|
||||||
|
[0, 0],
|
||||||
|
[-0.326, -0.618],
|
||||||
|
[-0.7, 0],
|
||||||
|
[0, 0],
|
||||||
|
[-0.456, 1.009],
|
||||||
|
[0, 0],
|
||||||
|
[-0.652, 0.293],
|
||||||
|
[0, 0.684],
|
||||||
|
[0, 0],
|
||||||
|
[1.074, 0.456],
|
||||||
|
[0, 0],
|
||||||
|
[0.293, 0.619],
|
||||||
|
[0.716, 0],
|
||||||
|
[0, 0],
|
||||||
|
[0.455, -1.009],
|
||||||
|
[0, 0],
|
||||||
|
[0.618, -0.293],
|
||||||
|
[0, -0.684],
|
||||||
|
[0, 0],
|
||||||
|
[-1.074, -0.455]
|
||||||
|
],
|
||||||
|
"v": [
|
||||||
|
[47.119, -68.994],
|
||||||
|
[43.799, -76.562],
|
||||||
|
[42.261, -77.49],
|
||||||
|
[40.771, -76.562],
|
||||||
|
[37.402, -68.994],
|
||||||
|
[35.156, -66.748],
|
||||||
|
[30.908, -64.893],
|
||||||
|
[29.932, -63.428],
|
||||||
|
[30.908, -61.963],
|
||||||
|
[35.156, -60.107],
|
||||||
|
[37.402, -57.861],
|
||||||
|
[40.771, -50.244],
|
||||||
|
[42.285, -49.316],
|
||||||
|
[43.799, -50.244],
|
||||||
|
[47.119, -57.861],
|
||||||
|
[49.365, -60.107],
|
||||||
|
[53.662, -61.963],
|
||||||
|
[54.59, -63.428],
|
||||||
|
[53.662, -64.893],
|
||||||
|
[49.365, -66.748]
|
||||||
|
],
|
||||||
|
"c": true
|
||||||
|
},
|
||||||
|
"ix": 2
|
||||||
|
},
|
||||||
|
"nm": "✨",
|
||||||
|
"mn": "ADBE Vector Shape - Group",
|
||||||
|
"hd": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ind": 1,
|
||||||
|
"ty": "sh",
|
||||||
|
"ix": 2,
|
||||||
|
"ks": {
|
||||||
|
"a": 0,
|
||||||
|
"k": {
|
||||||
|
"i": [
|
||||||
|
[1.334, 3.223],
|
||||||
|
[0, 0],
|
||||||
|
[1.204, 0.423],
|
||||||
|
[1.204, -0.423],
|
||||||
|
[0.618, -1.237],
|
||||||
|
[0, 0],
|
||||||
|
[3.125, -1.432],
|
||||||
|
[0, 0],
|
||||||
|
[0.423, -1.221],
|
||||||
|
[-0.423, -1.221],
|
||||||
|
[-1.27, -0.618],
|
||||||
|
[0, 0],
|
||||||
|
[-1.335, -3.223],
|
||||||
|
[0, 0],
|
||||||
|
[-1.205, -0.407],
|
||||||
|
[-1.205, 0.407],
|
||||||
|
[-0.619, 1.27],
|
||||||
|
[0, 0],
|
||||||
|
[-3.125, 1.433],
|
||||||
|
[0, 0],
|
||||||
|
[-0.423, 1.221],
|
||||||
|
[0.423, 1.221],
|
||||||
|
[1.27, 0.619],
|
||||||
|
[0, 0]
|
||||||
|
],
|
||||||
|
"o": [
|
||||||
|
[0, 0],
|
||||||
|
[-0.619, -1.237],
|
||||||
|
[-1.205, -0.423],
|
||||||
|
[-1.205, 0.423],
|
||||||
|
[0, 0],
|
||||||
|
[-1.367, 3.223],
|
||||||
|
[0, 0],
|
||||||
|
[-1.27, 0.619],
|
||||||
|
[-0.423, 1.221],
|
||||||
|
[0.423, 1.221],
|
||||||
|
[0, 0],
|
||||||
|
[3.157, 1.433],
|
||||||
|
[0, 0],
|
||||||
|
[0.618, 1.27],
|
||||||
|
[1.204, 0.407],
|
||||||
|
[1.204, -0.407],
|
||||||
|
[0, 0],
|
||||||
|
[1.367, -3.223],
|
||||||
|
[0, 0],
|
||||||
|
[1.27, -0.618],
|
||||||
|
[0.423, -1.221],
|
||||||
|
[-0.423, -1.221],
|
||||||
|
[0, 0],
|
||||||
|
[-3.158, -1.432]
|
||||||
|
],
|
||||||
|
"v": [
|
||||||
|
[95.605, -50.83],
|
||||||
|
[85.498, -74.658],
|
||||||
|
[82.764, -77.148],
|
||||||
|
[79.15, -77.148],
|
||||||
|
[76.416, -74.658],
|
||||||
|
[66.357, -50.83],
|
||||||
|
[59.619, -43.848],
|
||||||
|
[46.875, -38.086],
|
||||||
|
[44.336, -35.327],
|
||||||
|
[44.336, -31.665],
|
||||||
|
[46.875, -28.906],
|
||||||
|
[59.619, -23.145],
|
||||||
|
[66.357, -16.162],
|
||||||
|
[76.416, 7.666],
|
||||||
|
[79.15, 10.181],
|
||||||
|
[82.764, 10.181],
|
||||||
|
[85.498, 7.666],
|
||||||
|
[95.605, -16.162],
|
||||||
|
[102.344, -23.145],
|
||||||
|
[115.088, -28.906],
|
||||||
|
[117.627, -31.665],
|
||||||
|
[117.627, -35.327],
|
||||||
|
[115.088, -38.086],
|
||||||
|
[102.344, -43.848]
|
||||||
|
],
|
||||||
|
"c": true
|
||||||
|
},
|
||||||
|
"ix": 2
|
||||||
|
},
|
||||||
|
"nm": "✨",
|
||||||
|
"mn": "ADBE Vector Shape - Group",
|
||||||
|
"hd": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ind": 2,
|
||||||
|
"ty": "sh",
|
||||||
|
"ix": 3,
|
||||||
|
"ks": {
|
||||||
|
"a": 0,
|
||||||
|
"k": {
|
||||||
|
"i": [
|
||||||
|
[-1.367, -0.651],
|
||||||
|
[0, 0],
|
||||||
|
[0, -0.928],
|
||||||
|
[0.813, -0.423],
|
||||||
|
[0, 0],
|
||||||
|
[0.586, -1.399],
|
||||||
|
[0, 0],
|
||||||
|
[0.895, 0],
|
||||||
|
[0.391, 0.846],
|
||||||
|
[0, 0],
|
||||||
|
[1.334, 0.652],
|
||||||
|
[0, 0],
|
||||||
|
[0, 0.928],
|
||||||
|
[-0.814, 0.423],
|
||||||
|
[0, 0],
|
||||||
|
[-0.586, 1.4],
|
||||||
|
[0, 0],
|
||||||
|
[-0.896, 0],
|
||||||
|
[-0.391, -0.846],
|
||||||
|
[0, 0]
|
||||||
|
],
|
||||||
|
"o": [
|
||||||
|
[0, 0],
|
||||||
|
[0.813, 0.423],
|
||||||
|
[0, 0.928],
|
||||||
|
[0, 0],
|
||||||
|
[-1.335, 0.652],
|
||||||
|
[0, 0],
|
||||||
|
[-0.391, 0.846],
|
||||||
|
[-0.896, 0],
|
||||||
|
[0, 0],
|
||||||
|
[-0.586, -1.399],
|
||||||
|
[0, 0],
|
||||||
|
[-0.814, -0.423],
|
||||||
|
[0, -0.928],
|
||||||
|
[0, 0],
|
||||||
|
[1.334, -0.651],
|
||||||
|
[0, 0],
|
||||||
|
[0.391, -0.846],
|
||||||
|
[0.895, 0],
|
||||||
|
[0, 0],
|
||||||
|
[0.553, 1.4]
|
||||||
|
],
|
||||||
|
"v": [
|
||||||
|
[44.385, -16.943],
|
||||||
|
[49.854, -14.404],
|
||||||
|
[51.074, -12.378],
|
||||||
|
[49.854, -10.352],
|
||||||
|
[44.385, -7.812],
|
||||||
|
[41.504, -4.736],
|
||||||
|
[37.158, 5.713],
|
||||||
|
[35.229, 6.982],
|
||||||
|
[33.301, 5.713],
|
||||||
|
[28.955, -4.736],
|
||||||
|
[26.074, -7.812],
|
||||||
|
[20.605, -10.352],
|
||||||
|
[19.385, -12.378],
|
||||||
|
[20.605, -14.404],
|
||||||
|
[26.074, -16.943],
|
||||||
|
[28.955, -20.02],
|
||||||
|
[33.301, -30.469],
|
||||||
|
[35.229, -31.738],
|
||||||
|
[37.158, -30.469],
|
||||||
|
[41.504, -20.02]
|
||||||
|
],
|
||||||
|
"c": true
|
||||||
|
},
|
||||||
|
"ix": 2
|
||||||
|
},
|
||||||
|
"nm": "✨",
|
||||||
|
"mn": "ADBE Vector Shape - Group",
|
||||||
|
"hd": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nm": "✨",
|
||||||
|
"np": 6,
|
||||||
|
"cix": 2,
|
||||||
|
"bm": 0,
|
||||||
|
"ix": 1,
|
||||||
|
"mn": "ADBE Vector Group",
|
||||||
|
"hd": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"fFamily": "Segoe UI Emoji"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
23
src/renderer/src/components/avatar/avatar.css.ts
Normal file
23
src/renderer/src/components/avatar/avatar.css.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { style } from "@vanilla-extract/css";
|
||||||
|
|
||||||
|
import { vars } from "../../theme.css";
|
||||||
|
|
||||||
|
export const profileAvatar = style({
|
||||||
|
borderRadius: "4px",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
backgroundColor: vars.color.background,
|
||||||
|
border: `solid 1px ${vars.color.border}`,
|
||||||
|
cursor: "pointer",
|
||||||
|
color: vars.color.muted,
|
||||||
|
position: "relative",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const profileAvatarImage = style({
|
||||||
|
height: "100%",
|
||||||
|
width: "100%",
|
||||||
|
objectFit: "cover",
|
||||||
|
overflow: "hidden",
|
||||||
|
borderRadius: "4px",
|
||||||
|
});
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
@use "../../scss/globals.scss";
|
|
||||||
|
|
||||||
.profile-avatar {
|
|
||||||
border-radius: 4px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
background-color: globals.$background-color;
|
|
||||||
border: solid 1px globals.$border-color;
|
|
||||||
cursor: pointer;
|
|
||||||
color: globals.$muted-color;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&__image {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
overflow: hidden;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { PersonIcon } from "@primer/octicons-react";
|
import { PersonIcon } from "@primer/octicons-react";
|
||||||
|
|
||||||
import "./avatar.scss";
|
import * as styles from "./avatar.css";
|
||||||
|
|
||||||
export interface AvatarProps
|
export interface AvatarProps
|
||||||
extends Omit<
|
extends Omit<
|
||||||
@@ -16,9 +16,14 @@ export interface AvatarProps
|
|||||||
|
|
||||||
export function Avatar({ size, alt, src, ...props }: AvatarProps) {
|
export function Avatar({ size, alt, src, ...props }: AvatarProps) {
|
||||||
return (
|
return (
|
||||||
<div className="profile-avatar" style={{ width: size, height: size }}>
|
<div className={styles.profileAvatar} style={{ width: size, height: size }}>
|
||||||
{src ? (
|
{src ? (
|
||||||
<img className="profile-avatar__image" alt={alt} src={src} {...props} />
|
<img
|
||||||
|
className={styles.profileAvatarImage}
|
||||||
|
alt={alt}
|
||||||
|
src={src}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<PersonIcon size={size * 0.7} />
|
<PersonIcon size={size * 0.7} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
13
src/renderer/src/components/badge/badge.css.ts
Normal file
13
src/renderer/src/components/badge/badge.css.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { style } from "@vanilla-extract/css";
|
||||||
|
|
||||||
|
import { SPACING_UNIT } from "../../theme.css";
|
||||||
|
|
||||||
|
export const badge = style({
|
||||||
|
color: "#c0c1c7",
|
||||||
|
fontSize: "10px",
|
||||||
|
padding: `${SPACING_UNIT / 2}px ${SPACING_UNIT}px`,
|
||||||
|
border: "solid 1px #c0c1c7",
|
||||||
|
borderRadius: "4px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
});
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
@use "../../scss/globals.scss";
|
|
||||||
|
|
||||||
.badge {
|
|
||||||
color: globals.$muted-color;
|
|
||||||
font-size: 10px;
|
|
||||||
padding: calc(globals.$spacing-unit / 2) globals.$spacing-unit;
|
|
||||||
border: solid 1px globals.$muted-color;
|
|
||||||
border-radius: 4px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import * as styles from "./badge.css";
|
||||||
import "./badge.scss";
|
|
||||||
|
|
||||||
export interface BadgeProps {
|
export interface BadgeProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -8,7 +7,7 @@ export interface BadgeProps {
|
|||||||
|
|
||||||
export function Badge({ children }: BadgeProps) {
|
export function Badge({ children }: BadgeProps) {
|
||||||
return (
|
return (
|
||||||
<div className="badge">
|
<div className={styles.badge}>
|
||||||
<span>{children}</span>
|
<span>{children}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
25
src/renderer/src/components/bottom-panel/bottom-panel.css.ts
Normal file
25
src/renderer/src/components/bottom-panel/bottom-panel.css.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { style } from "@vanilla-extract/css";
|
||||||
|
|
||||||
|
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||||
|
|
||||||
|
export const bottomPanel = style({
|
||||||
|
width: "100%",
|
||||||
|
borderTop: `solid 1px ${vars.color.border}`,
|
||||||
|
backgroundColor: vars.color.background,
|
||||||
|
padding: `${SPACING_UNIT / 2}px ${SPACING_UNIT * 2}px`,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
transition: "all ease 0.2s",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
position: "relative",
|
||||||
|
zIndex: vars.zIndex.bottomPanel,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const downloadsButton = style({
|
||||||
|
color: vars.color.body,
|
||||||
|
borderBottom: "1px solid transparent",
|
||||||
|
":hover": {
|
||||||
|
borderBottom: `1px solid ${vars.color.body}`,
|
||||||
|
cursor: "pointer",
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
@use "../../scss/globals.scss";
|
|
||||||
|
|
||||||
.bottom-panel {
|
|
||||||
width: "100%";
|
|
||||||
border-top: solid 1px globals.$border-color;
|
|
||||||
background-color: globals.$background-color;
|
|
||||||
padding: calc(globals.$spacing-unit / 2) calc(globals.$spacing-unit * 2);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
transition: all ease 0.2s;
|
|
||||||
justify-content: space-between;
|
|
||||||
position: relative;
|
|
||||||
z-index: globals.$bottom-panel-z-index;
|
|
||||||
|
|
||||||
&__downloads-button {
|
|
||||||
color: globals.$body-color;
|
|
||||||
border-bottom: solid 1px transparent;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-bottom: solid 1px globals.$body-color;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
|
|
||||||
import { useDownload, useUserDetails } from "@renderer/hooks";
|
import { useDownload, useUserDetails } from "@renderer/hooks";
|
||||||
|
|
||||||
import "./bottom-panel.scss";
|
import * as styles from "./bottom-panel.css";
|
||||||
|
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { VERSION_CODENAME } from "@renderer/constants";
|
import { VERSION_CODENAME } from "@renderer/constants";
|
||||||
@@ -72,10 +72,10 @@ export function BottomPanel() {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="bottom-panel">
|
<footer className={styles.bottomPanel}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="bottom-panel__downloads-button"
|
className={styles.downloadsButton}
|
||||||
onClick={() => navigate("/downloads")}
|
onClick={() => navigate("/downloads")}
|
||||||
>
|
>
|
||||||
<small>{status}</small>
|
<small>{status}</small>
|
||||||
|
|||||||
69
src/renderer/src/components/button/button.css.ts
Normal file
69
src/renderer/src/components/button/button.css.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { style, styleVariants } from "@vanilla-extract/css";
|
||||||
|
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||||
|
|
||||||
|
const base = style({
|
||||||
|
padding: `${SPACING_UNIT}px ${SPACING_UNIT * 2}px`,
|
||||||
|
backgroundColor: vars.color.muted,
|
||||||
|
borderRadius: "8px",
|
||||||
|
border: "solid 1px transparent",
|
||||||
|
transition: "all ease 0.2s",
|
||||||
|
cursor: "pointer",
|
||||||
|
minHeight: "40px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: `${SPACING_UNIT}px`,
|
||||||
|
":active": {
|
||||||
|
opacity: vars.opacity.active,
|
||||||
|
},
|
||||||
|
":disabled": {
|
||||||
|
opacity: vars.opacity.disabled,
|
||||||
|
cursor: "not-allowed",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const button = styleVariants({
|
||||||
|
primary: [
|
||||||
|
base,
|
||||||
|
{
|
||||||
|
":hover": {
|
||||||
|
backgroundColor: "#DADBE1",
|
||||||
|
},
|
||||||
|
":disabled": {
|
||||||
|
backgroundColor: vars.color.muted,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outline: [
|
||||||
|
base,
|
||||||
|
{
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
border: `solid 1px ${vars.color.border}`,
|
||||||
|
color: vars.color.muted,
|
||||||
|
":hover": {
|
||||||
|
backgroundColor: "rgba(255, 255, 255, 0.1)",
|
||||||
|
},
|
||||||
|
":disabled": {
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
dark: [
|
||||||
|
base,
|
||||||
|
{
|
||||||
|
backgroundColor: vars.color.darkBackground,
|
||||||
|
color: "#c0c1c7",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
danger: [
|
||||||
|
base,
|
||||||
|
{
|
||||||
|
borderColor: "transparent",
|
||||||
|
backgroundColor: "#a31533",
|
||||||
|
color: "#c0c1c7",
|
||||||
|
":hover": {
|
||||||
|
backgroundColor: "#b3203f",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
@use "../../scss/globals.scss";
|
|
||||||
|
|
||||||
.button {
|
|
||||||
padding: globals.$spacing-unit globals.$spacing-unit * 2;
|
|
||||||
background-color: globals.$muted-color;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: solid 1px transparent;
|
|
||||||
transition: all ease 0.2s;
|
|
||||||
cursor: pointer;
|
|
||||||
min-height: 40px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: globals.$spacing-unit;
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
opacity: globals.$active-opacity;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
opacity: globals.$disabled-opacity;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--primary {
|
|
||||||
&:hover {
|
|
||||||
background-color: #dadbe1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
background-color: globals.$muted-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&--outline {
|
|
||||||
background-color: transparent;
|
|
||||||
border: solid 1px globals.$border-color;
|
|
||||||
color: globals.$muted-color;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&--dark {
|
|
||||||
background-color: globals.$dark-background-color;
|
|
||||||
color: globals.$muted-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--danger {
|
|
||||||
border-color: transparent;
|
|
||||||
background-color: globals.$danger-color;
|
|
||||||
color: globals.$muted-color;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: #b3203f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
import cn from "classnames";
|
import cn from "classnames";
|
||||||
|
import * as styles from "./button.css";
|
||||||
import "./button.scss";
|
|
||||||
|
|
||||||
export interface ButtonProps
|
export interface ButtonProps
|
||||||
extends React.DetailedHTMLProps<
|
extends React.DetailedHTMLProps<
|
||||||
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
HTMLButtonElement
|
HTMLButtonElement
|
||||||
> {
|
> {
|
||||||
theme?: "primary" | "outline" | "dark" | "danger";
|
theme?: keyof typeof styles.button;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Button({
|
export function Button({
|
||||||
@@ -19,7 +18,7 @@ export function Button({
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={cn("button", `button--${theme}`, className)}
|
className={cn(styles.button[theme], className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -65,6 +65,24 @@ export function Header({ onSearch, onClear, search }: HeaderProps) {
|
|||||||
navigate(-1);
|
navigate(-1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.onkeydown = (event: KeyboardEvent) => {
|
||||||
|
const { key, ctrlKey } = event;
|
||||||
|
if (!isFocused && ctrlKey && key === "k") {
|
||||||
|
focusInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFocused && key === "Escape" && inputRef.current) {
|
||||||
|
inputRef.current.blur();
|
||||||
|
handleBlur();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.onkeydown = null;
|
||||||
|
};
|
||||||
|
}, [isFocused]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<header
|
<header
|
||||||
@@ -81,6 +99,7 @@ export function Header({ onSearch, onClear, search }: HeaderProps) {
|
|||||||
})}
|
})}
|
||||||
onClick={handleBackButtonClick}
|
onClick={handleBackButtonClick}
|
||||||
disabled={location.key === "default"}
|
disabled={location.key === "default"}
|
||||||
|
title={t("back")}
|
||||||
>
|
>
|
||||||
<ArrowLeftIcon />
|
<ArrowLeftIcon />
|
||||||
</button>
|
</button>
|
||||||
@@ -100,6 +119,8 @@ export function Header({ onSearch, onClear, search }: HeaderProps) {
|
|||||||
type="button"
|
type="button"
|
||||||
className={styles.actionButton}
|
className={styles.actionButton}
|
||||||
onClick={focusInput}
|
onClick={focusInput}
|
||||||
|
tabIndex={-1}
|
||||||
|
title={t("search")}
|
||||||
>
|
>
|
||||||
<SearchIcon />
|
<SearchIcon />
|
||||||
</button>
|
</button>
|
||||||
@@ -121,6 +142,7 @@ export function Header({ onSearch, onClear, search }: HeaderProps) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={onClear}
|
onClick={onClear}
|
||||||
className={styles.actionButton}
|
className={styles.actionButton}
|
||||||
|
title={t("clear_search")}
|
||||||
>
|
>
|
||||||
<XIcon />
|
<XIcon />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
18
src/renderer/src/components/sidebar/download-icon.tsx
Normal file
18
src/renderer/src/components/sidebar/download-icon.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import Lottie from "lottie-react";
|
||||||
|
|
||||||
|
import downloadingAnimation from "@renderer/assets/lottie/downloading.json";
|
||||||
|
|
||||||
|
export interface DownloadIconProps {
|
||||||
|
isDownloading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DownloadIcon({ isDownloading }: DownloadIconProps) {
|
||||||
|
return (
|
||||||
|
<Lottie
|
||||||
|
animationData={downloadingAnimation}
|
||||||
|
loop={isDownloading}
|
||||||
|
autoplay={isDownloading}
|
||||||
|
style={{ width: 16 }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
import {
|
import { AppsIcon, GearIcon, HomeIcon } from "@primer/octicons-react";
|
||||||
AppsIcon,
|
|
||||||
DownloadIcon,
|
import { DownloadIcon } from "./download-icon";
|
||||||
GearIcon,
|
|
||||||
HomeIcon,
|
|
||||||
} from "@primer/octicons-react";
|
|
||||||
|
|
||||||
export const routes = [
|
export const routes = [
|
||||||
{
|
{
|
||||||
@@ -19,7 +16,9 @@ export const routes = [
|
|||||||
{
|
{
|
||||||
path: "/downloads",
|
path: "/downloads",
|
||||||
nameKey: "downloads",
|
nameKey: "downloads",
|
||||||
render: () => <DownloadIcon />,
|
render: (isDownloading: boolean) => (
|
||||||
|
<DownloadIcon isDownloading={isDownloading} />
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/settings",
|
path: "/settings",
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ export function SidebarProfile() {
|
|||||||
type="button"
|
type="button"
|
||||||
className={styles.profileButton}
|
className={styles.profileButton}
|
||||||
onClick={handleProfileClick}
|
onClick={handleProfileClick}
|
||||||
|
aria-label={t("aria_view_profile")}
|
||||||
>
|
>
|
||||||
<div className={styles.profileButtonContent}>
|
<div className={styles.profileButtonContent}>
|
||||||
<Avatar
|
<Avatar
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ export const sidebar = recipe({
|
|||||||
borderRight: `solid 1px ${vars.color.border}`,
|
borderRight: `solid 1px ${vars.color.border}`,
|
||||||
position: "relative",
|
position: "relative",
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
justifyContent: "space-between",
|
|
||||||
},
|
},
|
||||||
variants: {
|
variants: {
|
||||||
resizing: {
|
resizing: {
|
||||||
@@ -125,28 +124,3 @@ export const section = style({
|
|||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
paddingBottom: `${SPACING_UNIT}px`,
|
paddingBottom: `${SPACING_UNIT}px`,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const helpButton = style({
|
|
||||||
color: vars.color.muted,
|
|
||||||
padding: `${SPACING_UNIT}px ${SPACING_UNIT * 2}px`,
|
|
||||||
gap: "9px",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
cursor: "pointer",
|
|
||||||
borderTop: `solid 1px ${vars.color.border}`,
|
|
||||||
transition: "background-color ease 0.1s",
|
|
||||||
":hover": {
|
|
||||||
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const helpButtonIcon = style({
|
|
||||||
background: "linear-gradient(0deg, #16B195 50%, #3E62C0 100%)",
|
|
||||||
width: "24px",
|
|
||||||
height: "24px",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
color: "#fff",
|
|
||||||
borderRadius: "50%",
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -5,12 +5,7 @@ import { useLocation, useNavigate } from "react-router-dom";
|
|||||||
import type { LibraryGame } from "@types";
|
import type { LibraryGame } from "@types";
|
||||||
|
|
||||||
import { TextField } from "@renderer/components";
|
import { TextField } from "@renderer/components";
|
||||||
import {
|
import { useDownload, useLibrary, useToast } from "@renderer/hooks";
|
||||||
useDownload,
|
|
||||||
useLibrary,
|
|
||||||
useToast,
|
|
||||||
useUserDetails,
|
|
||||||
} from "@renderer/hooks";
|
|
||||||
|
|
||||||
import { routes } from "./routes";
|
import { routes } from "./routes";
|
||||||
|
|
||||||
@@ -20,9 +15,6 @@ import { buildGameDetailsPath } from "@renderer/helpers";
|
|||||||
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||||
import { SidebarProfile } from "./sidebar-profile";
|
import { SidebarProfile } from "./sidebar-profile";
|
||||||
import { sortBy } from "lodash-es";
|
import { sortBy } from "lodash-es";
|
||||||
import { CommentDiscussionIcon } from "@primer/octicons-react";
|
|
||||||
|
|
||||||
import { show, update } from "@intercom/messenger-js-sdk";
|
|
||||||
|
|
||||||
const SIDEBAR_MIN_WIDTH = 200;
|
const SIDEBAR_MIN_WIDTH = 200;
|
||||||
const SIDEBAR_INITIAL_WIDTH = 250;
|
const SIDEBAR_INITIAL_WIDTH = 250;
|
||||||
@@ -50,20 +42,6 @@ export function Sidebar() {
|
|||||||
return sortBy(library, (game) => game.title);
|
return sortBy(library, (game) => game.title);
|
||||||
}, [library]);
|
}, [library]);
|
||||||
|
|
||||||
const { userDetails, hasActiveSubscription } = useUserDetails();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (userDetails) {
|
|
||||||
update({
|
|
||||||
name: userDetails.displayName,
|
|
||||||
Username: userDetails.username,
|
|
||||||
Email: userDetails.email,
|
|
||||||
"Subscription expiration date": userDetails?.subscription?.expiresAt,
|
|
||||||
"Payment status": userDetails?.subscription?.status,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [userDetails, hasActiveSubscription]);
|
|
||||||
|
|
||||||
const { lastPacket, progress } = useDownload();
|
const { lastPacket, progress } = useDownload();
|
||||||
|
|
||||||
const { showWarningToast } = useToast();
|
const { showWarningToast } = useToast();
|
||||||
@@ -72,6 +50,10 @@ export function Sidebar() {
|
|||||||
updateLibrary();
|
updateLibrary();
|
||||||
}, [lastPacket?.game.id, updateLibrary]);
|
}, [lastPacket?.game.id, updateLibrary]);
|
||||||
|
|
||||||
|
const isDownloading = sortedLibrary.some(
|
||||||
|
(game) => game.status === "active" && game.progress !== 1
|
||||||
|
);
|
||||||
|
|
||||||
const sidebarRef = useRef<HTMLElement>(null);
|
const sidebarRef = useRef<HTMLElement>(null);
|
||||||
|
|
||||||
const cursorPos = useRef({ x: 0 });
|
const cursorPos = useRef({ x: 0 });
|
||||||
@@ -86,6 +68,26 @@ export function Sidebar() {
|
|||||||
sidebarRef.current?.clientWidth || SIDEBAR_INITIAL_WIDTH;
|
sidebarRef.current?.clientWidth || SIDEBAR_INITIAL_WIDTH;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleKeyDown: React.KeyboardEventHandler<HTMLButtonElement> = (
|
||||||
|
event
|
||||||
|
) => {
|
||||||
|
const { key } = event;
|
||||||
|
|
||||||
|
if (key === "ArrowRight") {
|
||||||
|
setSidebarWidth((prevWidth) =>
|
||||||
|
prevWidth < SIDEBAR_INITIAL_WIDTH
|
||||||
|
? SIDEBAR_INITIAL_WIDTH
|
||||||
|
: SIDEBAR_MAX_WIDTH
|
||||||
|
);
|
||||||
|
} else if (key === "ArrowLeft") {
|
||||||
|
setSidebarWidth((prevWidth) =>
|
||||||
|
prevWidth > SIDEBAR_INITIAL_WIDTH
|
||||||
|
? SIDEBAR_INITIAL_WIDTH
|
||||||
|
: SIDEBAR_MIN_WIDTH
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleFilter: React.ChangeEventHandler<HTMLInputElement> = (event) => {
|
const handleFilter: React.ChangeEventHandler<HTMLInputElement> = (event) => {
|
||||||
setFilteredLibrary(
|
setFilteredLibrary(
|
||||||
sortedLibrary.filter((game) =>
|
sortedLibrary.filter((game) =>
|
||||||
@@ -188,95 +190,84 @@ export function Sidebar() {
|
|||||||
maxWidth: sidebarWidth,
|
maxWidth: sidebarWidth,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<SidebarProfile />
|
||||||
style={{ display: "flex", flexDirection: "column", overflow: "hidden" }}
|
|
||||||
>
|
|
||||||
<SidebarProfile />
|
|
||||||
|
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<section className={styles.section}>
|
<section className={styles.section}>
|
||||||
<ul className={styles.menu}>
|
<ul className={styles.menu}>
|
||||||
{routes.map(({ nameKey, path, render }) => (
|
{routes.map(({ nameKey, path, render }) => (
|
||||||
<li
|
<li
|
||||||
key={nameKey}
|
key={nameKey}
|
||||||
className={styles.menuItem({
|
className={styles.menuItem({
|
||||||
active: location.pathname === path,
|
active: location.pathname === path,
|
||||||
})}
|
})}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.menuItemButton}
|
||||||
|
onClick={() => handleSidebarItemClick(path)}
|
||||||
>
|
>
|
||||||
<button
|
{render(isDownloading)}
|
||||||
type="button"
|
<span>{t(nameKey)}</span>
|
||||||
className={styles.menuItemButton}
|
</button>
|
||||||
onClick={() => handleSidebarItemClick(path)}
|
</li>
|
||||||
>
|
))}
|
||||||
{render()}
|
</ul>
|
||||||
<span>{t(nameKey)}</span>
|
</section>
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className={styles.section}>
|
<section className={styles.section}>
|
||||||
<small className={styles.sectionTitle}>{t("my_library")}</small>
|
<small className={styles.sectionTitle}>{t("my_library")}</small>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
ref={filterRef}
|
ref={filterRef}
|
||||||
placeholder={t("filter")}
|
placeholder={t("filter")}
|
||||||
onChange={handleFilter}
|
onChange={handleFilter}
|
||||||
theme="dark"
|
theme="dark"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ul className={styles.menu}>
|
<ul className={styles.menu}>
|
||||||
{filteredLibrary.map((game) => (
|
{filteredLibrary.map((game) => (
|
||||||
<li
|
<li
|
||||||
key={game.id}
|
key={game.id}
|
||||||
className={styles.menuItem({
|
className={styles.menuItem({
|
||||||
active:
|
active:
|
||||||
location.pathname ===
|
location.pathname === `/game/${game.shop}/${game.objectID}`,
|
||||||
`/game/${game.shop}/${game.objectID}`,
|
muted: game.status === "removed",
|
||||||
muted: game.status === "removed",
|
})}
|
||||||
})}
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.menuItemButton}
|
||||||
|
onClick={(event) => handleSidebarGameClick(event, game)}
|
||||||
|
aria-label={game.title}
|
||||||
>
|
>
|
||||||
<button
|
{game.iconUrl ? (
|
||||||
type="button"
|
<img
|
||||||
className={styles.menuItemButton}
|
className={styles.gameIcon}
|
||||||
onClick={(event) => handleSidebarGameClick(event, game)}
|
src={game.iconUrl}
|
||||||
>
|
alt={game.title}
|
||||||
{game.iconUrl ? (
|
loading="lazy"
|
||||||
<img
|
/>
|
||||||
className={styles.gameIcon}
|
) : (
|
||||||
src={game.iconUrl}
|
<SteamLogo className={styles.gameIcon} />
|
||||||
alt={game.title}
|
)}
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<SteamLogo className={styles.gameIcon} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<span className={styles.menuItemButtonLabel}>
|
<span className={styles.menuItemButtonLabel}>
|
||||||
{getGameTitle(game)}
|
{getGameTitle(game)}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{hasActiveSubscription && (
|
|
||||||
<button type="button" className={styles.helpButton} onClick={show}>
|
|
||||||
<div className={styles.helpButtonIcon}>
|
|
||||||
<CommentDiscussionIcon size={14} />
|
|
||||||
</div>
|
|
||||||
<span>{t("need_help")}</span>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.handle}
|
className={styles.handle}
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
title={t("resize_sidebar")}
|
||||||
/>
|
/>
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
|
|||||||
10
src/renderer/src/declaration.d.ts
vendored
10
src/renderer/src/declaration.d.ts
vendored
@@ -66,6 +66,16 @@ declare global {
|
|||||||
searchGameRepacks: (query: string) => Promise<GameRepack[]>;
|
searchGameRepacks: (query: string) => Promise<GameRepack[]>;
|
||||||
getGameStats: (objectId: string, shop: GameShop) => Promise<GameStats>;
|
getGameStats: (objectId: string, shop: GameShop) => Promise<GameStats>;
|
||||||
getTrendingGames: () => Promise<TrendingGame[]>;
|
getTrendingGames: () => Promise<TrendingGame[]>;
|
||||||
|
onAchievementUnlocked: (
|
||||||
|
cb: (
|
||||||
|
objectId: string,
|
||||||
|
shop: GameShop,
|
||||||
|
achievements?: { displayName: string; iconUrl: string }[]
|
||||||
|
) => void
|
||||||
|
) => () => Electron.IpcRenderer;
|
||||||
|
onCombinedAchievementsUnlocked: (
|
||||||
|
cb: (gameCount: number, achievementCount: number) => void
|
||||||
|
) => () => Electron.IpcRenderer;
|
||||||
onUpdateAchievements: (
|
onUpdateAchievements: (
|
||||||
objectId: string,
|
objectId: string,
|
||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
|
|||||||
@@ -10,22 +10,12 @@ export interface HowLongToBeatEntry {
|
|||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CatalogueCache {
|
|
||||||
id?: number;
|
|
||||||
category: string;
|
|
||||||
games: { objectId: string; shop: GameShop }[];
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
expiresAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const db = new Dexie("Hydra");
|
export const db = new Dexie("Hydra");
|
||||||
|
|
||||||
db.version(5).stores({
|
db.version(4).stores({
|
||||||
repacks: `++id, title, uris, fileSize, uploadDate, downloadSourceId, repacker, createdAt, updatedAt`,
|
repacks: `++id, title, uris, fileSize, uploadDate, downloadSourceId, repacker, createdAt, updatedAt`,
|
||||||
downloadSources: `++id, url, name, etag, downloadCount, status, createdAt, updatedAt`,
|
downloadSources: `++id, url, name, etag, downloadCount, status, createdAt, updatedAt`,
|
||||||
howLongToBeatEntries: `++id, categories, [shop+objectId], createdAt, updatedAt`,
|
howLongToBeatEntries: `++id, categories, [shop+objectId], createdAt, updatedAt`,
|
||||||
catalogueCache: `++id, category, games, createdAt, updatedAt, expiresAt`,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const downloadSourcesTable = db.table("downloadSources");
|
export const downloadSourcesTable = db.table("downloadSources");
|
||||||
@@ -34,6 +24,4 @@ export const howLongToBeatEntriesTable = db.table<HowLongToBeatEntry>(
|
|||||||
"howLongToBeatEntries"
|
"howLongToBeatEntries"
|
||||||
);
|
);
|
||||||
|
|
||||||
export const catalogueCacheTable = db.table<CatalogueCache>("catalogueCache");
|
|
||||||
|
|
||||||
db.open();
|
db.open();
|
||||||
|
|||||||
0
src/renderer/src/hooks/use-friendship.ts
Normal file
0
src/renderer/src/hooks/use-friendship.ts
Normal file
@@ -6,6 +6,8 @@ import { Provider } from "react-redux";
|
|||||||
import LanguageDetector from "i18next-browser-languagedetector";
|
import LanguageDetector from "i18next-browser-languagedetector";
|
||||||
import { HashRouter, Route, Routes } from "react-router-dom";
|
import { HashRouter, Route, Routes } from "react-router-dom";
|
||||||
|
|
||||||
|
import * as Sentry from "@sentry/electron/renderer";
|
||||||
|
|
||||||
import "@fontsource/noto-sans/400.css";
|
import "@fontsource/noto-sans/400.css";
|
||||||
import "@fontsource/noto-sans/500.css";
|
import "@fontsource/noto-sans/500.css";
|
||||||
import "@fontsource/noto-sans/700.css";
|
import "@fontsource/noto-sans/700.css";
|
||||||
@@ -17,7 +19,9 @@ import { App } from "./app";
|
|||||||
import { store } from "./store";
|
import { store } from "./store";
|
||||||
|
|
||||||
import resources from "@locales";
|
import resources from "@locales";
|
||||||
|
import { AchievementNotification } from "./pages/achievements/notification/achievement-notification";
|
||||||
|
|
||||||
|
import "./workers";
|
||||||
import { RepacksContextProvider } from "./context";
|
import { RepacksContextProvider } from "./context";
|
||||||
import { SuspenseWrapper } from "./components";
|
import { SuspenseWrapper } from "./components";
|
||||||
|
|
||||||
@@ -34,6 +38,8 @@ const Achievements = React.lazy(
|
|||||||
() => import("./pages/achievements/achievements")
|
() => import("./pages/achievements/achievements")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Sentry.init({});
|
||||||
|
|
||||||
i18n
|
i18n
|
||||||
.use(LanguageDetector)
|
.use(LanguageDetector)
|
||||||
.use(initReactI18next)
|
.use(initReactI18next)
|
||||||
@@ -91,6 +97,10 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
|||||||
element={<SuspenseWrapper Component={Achievements} />}
|
element={<SuspenseWrapper Component={Achievements} />}
|
||||||
/>
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route
|
||||||
|
path="/achievement-notification"
|
||||||
|
Component={AchievementNotification}
|
||||||
|
/>
|
||||||
</Routes>
|
</Routes>
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
</RepacksContextProvider>
|
</RepacksContextProvider>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { average } from "color.js";
|
|||||||
import Color from "color";
|
import Color from "color";
|
||||||
import { Link } from "@renderer/components";
|
import { Link } from "@renderer/components";
|
||||||
import { ComparedAchievementList } from "./compared-achievement-list";
|
import { ComparedAchievementList } from "./compared-achievement-list";
|
||||||
|
import { TFunction } from "i18next/typescript/t";
|
||||||
|
|
||||||
interface UserInfo {
|
interface UserInfo {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -39,10 +40,35 @@ interface AchievementSummaryProps {
|
|||||||
isComparison?: boolean;
|
isComparison?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ariaLabelSummary = (
|
||||||
|
t: TFunction,
|
||||||
|
gameTitle: string,
|
||||||
|
user: UserInfo
|
||||||
|
): string => {
|
||||||
|
return t("aria_achievement_summary", {
|
||||||
|
userDisplayName: user.displayName,
|
||||||
|
gameTitle: gameTitle,
|
||||||
|
userAchievementCount: user.unlockedAchievementCount,
|
||||||
|
userTotalAchievementCount: user.totalAchievementCount,
|
||||||
|
percentage: formatDownloadProgress(
|
||||||
|
user.unlockedAchievementCount / user.totalAchievementCount
|
||||||
|
),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const ariaLabelAchievement = (
|
||||||
|
t: TFunction,
|
||||||
|
achievement: UserAchievement
|
||||||
|
): string => {
|
||||||
|
return `${
|
||||||
|
achievement.unlocked ? t("achievement_unlocked") : t("achievement_locked")
|
||||||
|
}, ${achievement.displayName}, ${achievement.description}`;
|
||||||
|
};
|
||||||
|
|
||||||
function AchievementSummary({ user, isComparison }: AchievementSummaryProps) {
|
function AchievementSummary({ user, isComparison }: AchievementSummaryProps) {
|
||||||
const { t } = useTranslation("achievement");
|
const { t } = useTranslation("achievement");
|
||||||
const { userDetails, hasActiveSubscription } = useUserDetails();
|
const { userDetails, hasActiveSubscription } = useUserDetails();
|
||||||
const { handleClickOpenCheckout } = useContext(gameDetailsContext);
|
const { handleClickOpenCheckout, gameTitle } = useContext(gameDetailsContext);
|
||||||
|
|
||||||
const getProfileImage = (
|
const getProfileImage = (
|
||||||
user: Pick<UserInfo, "profileImageUrl" | "displayName">
|
user: Pick<UserInfo, "profileImageUrl" | "displayName">
|
||||||
@@ -124,6 +150,8 @@ function AchievementSummary({ user, isComparison }: AchievementSummaryProps) {
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
padding: `${SPACING_UNIT}px`,
|
padding: `${SPACING_UNIT}px`,
|
||||||
}}
|
}}
|
||||||
|
role="region"
|
||||||
|
aria-label={ariaLabelSummary(t, gameTitle, user)}
|
||||||
>
|
>
|
||||||
{getProfileImage(user)}
|
{getProfileImage(user)}
|
||||||
<div
|
<div
|
||||||
@@ -178,7 +206,12 @@ function AchievementList({ achievements }: AchievementListProps) {
|
|||||||
return (
|
return (
|
||||||
<ul className={styles.list}>
|
<ul className={styles.list}>
|
||||||
{achievements.map((achievement, index) => (
|
{achievements.map((achievement, index) => (
|
||||||
<li key={index} className={styles.listItem} style={{ display: "flex" }}>
|
<li
|
||||||
|
key={index}
|
||||||
|
className={styles.listItem}
|
||||||
|
style={{ display: "flex" }}
|
||||||
|
aria-label={ariaLabelAchievement(t, achievement)}
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
className={styles.listItemImage({
|
className={styles.listItemImage({
|
||||||
unlocked: achievement.unlocked,
|
unlocked: achievement.unlocked,
|
||||||
@@ -273,6 +306,7 @@ export function AchievementsContent({
|
|||||||
src={steamUrlBuilder.libraryHero(objectId)}
|
src={steamUrlBuilder.libraryHero(objectId)}
|
||||||
style={{ display: "none" }}
|
style={{ display: "none" }}
|
||||||
alt={gameTitle}
|
alt={gameTitle}
|
||||||
|
className={styles.heroImage}
|
||||||
onLoad={handleHeroLoad}
|
onLoad={handleHeroLoad}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,31 @@ export const hero = style({
|
|||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
position: "relative",
|
position: "relative",
|
||||||
transition: "all ease 0.2s",
|
transition: "all ease 0.2s",
|
||||||
|
"@media": {
|
||||||
|
"(min-width: 1250px)": {
|
||||||
|
height: "350px",
|
||||||
|
minHeight: "350px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const heroImage = style({
|
||||||
|
width: "100%",
|
||||||
|
height: `${HERO_HEIGHT}px`,
|
||||||
|
minHeight: `${HERO_HEIGHT}px`,
|
||||||
|
objectFit: "cover",
|
||||||
|
objectPosition: "top",
|
||||||
|
transition: "all ease 0.2s",
|
||||||
|
position: "absolute",
|
||||||
|
zIndex: "0",
|
||||||
|
filter: "blur(5px)",
|
||||||
|
"@media": {
|
||||||
|
"(min-width: 1250px)": {
|
||||||
|
objectPosition: "center",
|
||||||
|
height: "350px",
|
||||||
|
minHeight: "350px",
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const heroContent = style({
|
export const heroContent = style({
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { recipe } from "@vanilla-extract/recipes";
|
||||||
|
import { vars } from "../../../theme.css";
|
||||||
|
import { keyframes, style } from "@vanilla-extract/css";
|
||||||
|
|
||||||
|
const animationIn = keyframes({
|
||||||
|
"0%": { transform: `translateY(-240px)` },
|
||||||
|
"100%": { transform: "translateY(0)" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const animationOut = keyframes({
|
||||||
|
"0%": { transform: `translateY(0)` },
|
||||||
|
"100%": { transform: "translateY(-240px)" },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const container = recipe({
|
||||||
|
base: {
|
||||||
|
marginTop: "24px",
|
||||||
|
marginLeft: "24px",
|
||||||
|
animationDuration: "1.0s",
|
||||||
|
height: "60px",
|
||||||
|
display: "flex",
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
closing: {
|
||||||
|
true: {
|
||||||
|
animationName: animationOut,
|
||||||
|
transform: "translateY(-240px)",
|
||||||
|
},
|
||||||
|
false: {
|
||||||
|
animationName: animationIn,
|
||||||
|
transform: "translateY(0)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const content = style({
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
gap: "8px",
|
||||||
|
alignItems: "center",
|
||||||
|
background: vars.color.background,
|
||||||
|
paddingRight: "8px",
|
||||||
|
});
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import achievementSound from "@renderer/assets/audio/achievement.wav";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import * as styles from "./achievement-notification.css";
|
||||||
|
|
||||||
|
interface AchievementInfo {
|
||||||
|
displayName: string;
|
||||||
|
iconUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NOTIFICATION_TIMEOUT = 4000;
|
||||||
|
|
||||||
|
export function AchievementNotification() {
|
||||||
|
const { t } = useTranslation("achievement");
|
||||||
|
|
||||||
|
const [isClosing, setIsClosing] = useState(false);
|
||||||
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
|
||||||
|
const [achievements, setAchievements] = useState<AchievementInfo[]>([]);
|
||||||
|
const [currentAchievement, setCurrentAchievement] =
|
||||||
|
useState<AchievementInfo | null>(null);
|
||||||
|
|
||||||
|
const achievementAnimation = useRef(-1);
|
||||||
|
const closingAnimation = useRef(-1);
|
||||||
|
const visibleAnimation = useRef(-1);
|
||||||
|
|
||||||
|
const playAudio = useCallback(() => {
|
||||||
|
const audio = new Audio(achievementSound);
|
||||||
|
audio.volume = 0.2;
|
||||||
|
audio.play();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribe = window.electron.onCombinedAchievementsUnlocked(
|
||||||
|
(gameCount, achievementCount) => {
|
||||||
|
if (gameCount === 0 || achievementCount === 0) return;
|
||||||
|
|
||||||
|
setAchievements([
|
||||||
|
{
|
||||||
|
displayName: t("new_achievements_unlocked", {
|
||||||
|
gameCount,
|
||||||
|
achievementCount,
|
||||||
|
}),
|
||||||
|
iconUrl:
|
||||||
|
"https://avatars.githubusercontent.com/u/164102380?s=400&u=01a13a7b4f0c642f7e547b8e1d70440ea06fa750&v=4",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
playAudio();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe();
|
||||||
|
};
|
||||||
|
}, [playAudio]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribe = window.electron.onAchievementUnlocked(
|
||||||
|
(_object, _shop, achievements) => {
|
||||||
|
if (!achievements || !achievements.length) return;
|
||||||
|
|
||||||
|
setAchievements((ach) => ach.concat(achievements));
|
||||||
|
|
||||||
|
playAudio();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe();
|
||||||
|
};
|
||||||
|
}, [playAudio]);
|
||||||
|
|
||||||
|
const hasAchievementsPending = achievements.length > 0;
|
||||||
|
|
||||||
|
const startAnimateClosing = useCallback(() => {
|
||||||
|
cancelAnimationFrame(closingAnimation.current);
|
||||||
|
cancelAnimationFrame(visibleAnimation.current);
|
||||||
|
cancelAnimationFrame(achievementAnimation.current);
|
||||||
|
|
||||||
|
setIsClosing(true);
|
||||||
|
|
||||||
|
const zero = performance.now();
|
||||||
|
closingAnimation.current = requestAnimationFrame(
|
||||||
|
function animateClosing(time) {
|
||||||
|
if (time - zero <= 1000) {
|
||||||
|
closingAnimation.current = requestAnimationFrame(animateClosing);
|
||||||
|
} else {
|
||||||
|
setIsVisible(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasAchievementsPending) {
|
||||||
|
setIsClosing(false);
|
||||||
|
setIsVisible(true);
|
||||||
|
|
||||||
|
let zero = performance.now();
|
||||||
|
cancelAnimationFrame(closingAnimation.current);
|
||||||
|
cancelAnimationFrame(visibleAnimation.current);
|
||||||
|
cancelAnimationFrame(achievementAnimation.current);
|
||||||
|
achievementAnimation.current = requestAnimationFrame(
|
||||||
|
function animateLock(time) {
|
||||||
|
if (time - zero > NOTIFICATION_TIMEOUT) {
|
||||||
|
zero = performance.now();
|
||||||
|
setAchievements((ach) => ach.slice(1));
|
||||||
|
}
|
||||||
|
achievementAnimation.current = requestAnimationFrame(animateLock);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
startAnimateClosing();
|
||||||
|
}
|
||||||
|
}, [hasAchievementsPending]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (achievements.length) {
|
||||||
|
setCurrentAchievement(achievements[0]);
|
||||||
|
}
|
||||||
|
}, [achievements]);
|
||||||
|
|
||||||
|
if (!isVisible || !currentAchievement) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container({ closing: isClosing })}>
|
||||||
|
<div className={styles.content}>
|
||||||
|
<img
|
||||||
|
src={currentAchievement.iconUrl}
|
||||||
|
alt={currentAchievement.displayName}
|
||||||
|
style={{ flex: 1, width: "60px" }}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<p>{t("achievement_unlocked")}</p>
|
||||||
|
<p>{currentAchievement.displayName}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -83,10 +83,8 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible) {
|
getGameBackupPreview();
|
||||||
getGameBackupPreview();
|
}, [getGameBackupPreview]);
|
||||||
}
|
|
||||||
}, [getGameBackupPreview, visible]);
|
|
||||||
|
|
||||||
const backupStateLabel = useMemo(() => {
|
const backupStateLabel = useMemo(() => {
|
||||||
if (uploadingBackup) {
|
if (uploadingBackup) {
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ export function GallerySlider() {
|
|||||||
direction: "left",
|
direction: "left",
|
||||||
})}
|
})}
|
||||||
aria-label={t("previous_screenshot")}
|
aria-label={t("previous_screenshot")}
|
||||||
tabIndex={0}
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
<ChevronLeftIcon size={36} />
|
<ChevronLeftIcon size={36} />
|
||||||
</button>
|
</button>
|
||||||
@@ -153,7 +153,7 @@ export function GallerySlider() {
|
|||||||
direction: "right",
|
direction: "right",
|
||||||
})}
|
})}
|
||||||
aria-label={t("next_screenshot")}
|
aria-label={t("next_screenshot")}
|
||||||
tabIndex={0}
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
<ChevronRightIcon size={36} />
|
<ChevronRightIcon size={36} />
|
||||||
</button>
|
</button>
|
||||||
@@ -169,6 +169,7 @@ export function GallerySlider() {
|
|||||||
})}
|
})}
|
||||||
onClick={() => setMediaIndex(i)}
|
onClick={() => setMediaIndex(i)}
|
||||||
aria-label={t("open_screenshot", { number: i + 1 })}
|
aria-label={t("open_screenshot", { number: i + 1 })}
|
||||||
|
onFocus={() => setMediaIndex(i)}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={media.thumbnail}
|
src={media.thumbnail}
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ import * as styles from "./game-details.css";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { cloudSyncContext, gameDetailsContext } from "@renderer/context";
|
import { cloudSyncContext, gameDetailsContext } from "@renderer/context";
|
||||||
import { steamUrlBuilder } from "@shared";
|
import { steamUrlBuilder } from "@shared";
|
||||||
|
import Lottie from "lottie-react";
|
||||||
|
|
||||||
import cloudIconAnimated from "@renderer/assets/icons/cloud-animated.gif";
|
import cloudAnimation from "@renderer/assets/lottie/cloud.json";
|
||||||
import { useUserDetails } from "@renderer/hooks";
|
import { useUserDetails } from "@renderer/hooks";
|
||||||
|
|
||||||
const HERO_ANIMATION_THRESHOLD = 25;
|
const HERO_ANIMATION_THRESHOLD = 25;
|
||||||
@@ -164,9 +165,10 @@ export function GameDetailsContent() {
|
|||||||
position: "relative",
|
position: "relative",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img
|
<Lottie
|
||||||
src={cloudIconAnimated}
|
animationData={cloudAnimation}
|
||||||
alt="Cloud icon"
|
loop={false}
|
||||||
|
autoplay
|
||||||
style={{ width: 26, position: "absolute", top: -3 }}
|
style={{ width: 26, position: "absolute", top: -3 }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ import type { GameRepack, GameShop, Steam250Game } from "@types";
|
|||||||
import { Button, ConfirmationModal } from "@renderer/components";
|
import { Button, ConfirmationModal } from "@renderer/components";
|
||||||
import { buildGameDetailsPath } from "@renderer/helpers";
|
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||||
|
|
||||||
import starsIconAnimated from "@renderer/assets/icons/stars-animated.gif";
|
import starsAnimation from "@renderer/assets/lottie/stars.json";
|
||||||
|
|
||||||
|
import Lottie from "lottie-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { SkeletonTheme } from "react-loading-skeleton";
|
import { SkeletonTheme } from "react-loading-skeleton";
|
||||||
import { GameDetailsSkeleton } from "./game-details-skeleton";
|
import { GameDetailsSkeleton } from "./game-details-skeleton";
|
||||||
@@ -193,15 +194,15 @@ export default function GameDetails() {
|
|||||||
<div
|
<div
|
||||||
style={{ width: 16, height: 16, position: "relative" }}
|
style={{ width: 16, height: 16, position: "relative" }}
|
||||||
>
|
>
|
||||||
<img
|
<Lottie
|
||||||
src={starsIconAnimated}
|
animationData={starsAnimation}
|
||||||
alt="Stars animation"
|
|
||||||
style={{
|
style={{
|
||||||
width: 70,
|
width: 70,
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: -28,
|
top: -28,
|
||||||
left: -27,
|
left: -27,
|
||||||
}}
|
}}
|
||||||
|
loop={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{t("next_suggestion")}
|
{t("next_suggestion")}
|
||||||
|
|||||||
@@ -1,9 +1,4 @@
|
|||||||
import {
|
import { GearIcon, PlayIcon, PlusCircleIcon } from "@primer/octicons-react";
|
||||||
DownloadIcon,
|
|
||||||
GearIcon,
|
|
||||||
PlayIcon,
|
|
||||||
PlusCircleIcon,
|
|
||||||
} from "@primer/octicons-react";
|
|
||||||
import { Button } from "@renderer/components";
|
import { Button } from "@renderer/components";
|
||||||
import { useDownload, useLibrary } from "@renderer/hooks";
|
import { useDownload, useLibrary } from "@renderer/hooks";
|
||||||
import { useContext, useState } from "react";
|
import { useContext, useState } from "react";
|
||||||
@@ -11,6 +6,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import * as styles from "./hero-panel-actions.css";
|
import * as styles from "./hero-panel-actions.css";
|
||||||
|
|
||||||
import { gameDetailsContext } from "@renderer/context";
|
import { gameDetailsContext } from "@renderer/context";
|
||||||
|
import { DownloadIcon } from "@renderer/components/sidebar/download-icon";
|
||||||
|
|
||||||
export function HeroPanelActions() {
|
export function HeroPanelActions() {
|
||||||
const [toggleLibraryGameDisabled, setToggleLibraryGameDisabled] =
|
const [toggleLibraryGameDisabled, setToggleLibraryGameDisabled] =
|
||||||
@@ -129,7 +125,7 @@ export function HeroPanelActions() {
|
|||||||
disabled={isGameDownloading || !repacks.length}
|
disabled={isGameDownloading || !repacks.length}
|
||||||
className={styles.heroPanelAction}
|
className={styles.heroPanelAction}
|
||||||
>
|
>
|
||||||
<DownloadIcon />
|
<DownloadIcon isDownloading={false} />
|
||||||
{t("download")}
|
{t("download")}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ export function HowLongToBeatSection({
|
|||||||
<li
|
<li
|
||||||
key={category.title}
|
key={category.title}
|
||||||
className={styles.howLongToBeatCategory}
|
className={styles.howLongToBeatCategory}
|
||||||
|
aria-label={`${category.title}, ${getDuration(
|
||||||
|
category.duration
|
||||||
|
)}`}
|
||||||
>
|
>
|
||||||
<p
|
<p
|
||||||
className={styles.howLongToBeatCategoryLabel}
|
className={styles.howLongToBeatCategoryLabel}
|
||||||
|
|||||||
@@ -194,6 +194,10 @@ export function Sidebar() {
|
|||||||
})}
|
})}
|
||||||
className={styles.listItem}
|
className={styles.listItem}
|
||||||
title={achievement.description}
|
title={achievement.description}
|
||||||
|
aria-label={`
|
||||||
|
${t("achievement")} ${index + 1},
|
||||||
|
${achievement.displayName}
|
||||||
|
`}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
className={styles.listItemImage({
|
className={styles.listItemImage({
|
||||||
|
|||||||
@@ -68,10 +68,3 @@ export const buttonsList = style({
|
|||||||
padding: "0",
|
padding: "0",
|
||||||
gap: `${SPACING_UNIT}px`,
|
gap: `${SPACING_UNIT}px`,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const flameIcon = style({
|
|
||||||
width: "30px",
|
|
||||||
top: "-10px",
|
|
||||||
left: "-5px",
|
|
||||||
position: "absolute",
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
@@ -7,28 +7,21 @@ import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
|
|||||||
import { Button, GameCard, Hero } from "@renderer/components";
|
import { Button, GameCard, Hero } from "@renderer/components";
|
||||||
import type { Steam250Game, CatalogueEntry } from "@types";
|
import type { Steam250Game, CatalogueEntry } from "@types";
|
||||||
|
|
||||||
import flameIconStatic from "@renderer/assets/icons/flame-static.png";
|
import starsAnimation from "@renderer/assets/lottie/stars.json";
|
||||||
import flameIconAnimated from "@renderer/assets/icons/flame-animated.gif";
|
import flameAnimation from "@renderer/assets/lottie/flame.json";
|
||||||
import starsIconAnimated from "@renderer/assets/icons/stars-animated.gif";
|
|
||||||
|
|
||||||
import * as styles from "./home.css";
|
import * as styles from "./home.css";
|
||||||
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
||||||
|
import Lottie, { type LottieRefCurrentProps } from "lottie-react";
|
||||||
import { buildGameDetailsPath } from "@renderer/helpers";
|
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||||
import { CatalogueCategory } from "@shared";
|
import { CatalogueCategory } from "@shared";
|
||||||
import { catalogueCacheTable, db } from "@renderer/dexie";
|
|
||||||
import { add } from "date-fns";
|
|
||||||
|
|
||||||
const categoryCacheDurationInSeconds = {
|
|
||||||
[CatalogueCategory.Hot]: 60 * 60 * 2,
|
|
||||||
[CatalogueCategory.Weekly]: 60 * 60 * 24,
|
|
||||||
[CatalogueCategory.Achievements]: 60 * 60 * 24,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const { t } = useTranslation("home");
|
const { t } = useTranslation("home");
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [animateFlame, setAnimateFlame] = useState(false);
|
const flameAnimationRef = useRef<LottieRefCurrentProps>(null);
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [randomGame, setRandomGame] = useState<Steam250Game | null>(null);
|
const [randomGame, setRandomGame] = useState<Steam250Game | null>(null);
|
||||||
|
|
||||||
@@ -44,43 +37,19 @@ export default function Home() {
|
|||||||
[CatalogueCategory.Achievements]: [],
|
[CatalogueCategory.Achievements]: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const getCatalogue = useCallback(async (category: CatalogueCategory) => {
|
const getCatalogue = useCallback((category: CatalogueCategory) => {
|
||||||
try {
|
setCurrentCatalogueCategory(category);
|
||||||
const catalogueCache = await catalogueCacheTable
|
setIsLoading(true);
|
||||||
.where("expiresAt")
|
|
||||||
.above(new Date())
|
|
||||||
.and((cache) => cache.category === category)
|
|
||||||
.first();
|
|
||||||
|
|
||||||
setCurrentCatalogueCategory(category);
|
window.electron
|
||||||
setIsLoading(true);
|
.getCatalogue(category)
|
||||||
|
.then((catalogue) => {
|
||||||
if (catalogueCache)
|
setCatalogue((prev) => ({ ...prev, [category]: catalogue }));
|
||||||
return setCatalogue((prev) => ({
|
})
|
||||||
...prev,
|
.catch(() => {})
|
||||||
[category]: catalogueCache.games,
|
.finally(() => {
|
||||||
}));
|
setIsLoading(false);
|
||||||
|
|
||||||
const catalogue = await window.electron.getCatalogue(category);
|
|
||||||
|
|
||||||
db.transaction("rw", catalogueCacheTable, async () => {
|
|
||||||
await catalogueCacheTable.where("category").equals(category).delete();
|
|
||||||
|
|
||||||
await catalogueCacheTable.add({
|
|
||||||
category,
|
|
||||||
games: catalogue,
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
expiresAt: add(new Date(), {
|
|
||||||
seconds: categoryCacheDurationInSeconds[category],
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
setCatalogue((prev) => ({ ...prev, [category]: catalogue }));
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const getRandomGame = useCallback(() => {
|
const getRandomGame = useCallback(() => {
|
||||||
@@ -119,13 +88,13 @@ export default function Home() {
|
|||||||
|
|
||||||
const handleMouseEnterCategory = (category: CatalogueCategory) => {
|
const handleMouseEnterCategory = (category: CatalogueCategory) => {
|
||||||
if (category === CatalogueCategory.Hot) {
|
if (category === CatalogueCategory.Hot) {
|
||||||
setAnimateFlame(true);
|
flameAnimationRef?.current?.play();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseLeaveCategory = (category: CatalogueCategory) => {
|
const handleMouseLeaveCategory = (category: CatalogueCategory) => {
|
||||||
if (category === CatalogueCategory.Hot) {
|
if (category === CatalogueCategory.Hot) {
|
||||||
setAnimateFlame(false);
|
flameAnimationRef?.current?.stop();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -154,17 +123,17 @@ export default function Home() {
|
|||||||
<div
|
<div
|
||||||
style={{ width: 16, height: 16, position: "relative" }}
|
style={{ width: 16, height: 16, position: "relative" }}
|
||||||
>
|
>
|
||||||
<img
|
<Lottie
|
||||||
src={flameIconStatic}
|
lottieRef={flameAnimationRef}
|
||||||
alt="Flame icon"
|
animationData={flameAnimation}
|
||||||
className={styles.flameIcon}
|
loop={false}
|
||||||
style={{ display: animateFlame ? "none" : "block" }}
|
autoplay={false}
|
||||||
/>
|
style={{
|
||||||
<img
|
width: 30,
|
||||||
src={flameIconAnimated}
|
top: -10,
|
||||||
alt="Flame animation"
|
left: -5,
|
||||||
className={styles.flameIcon}
|
position: "absolute",
|
||||||
style={{ display: animateFlame ? "block" : "none" }}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -181,10 +150,10 @@ export default function Home() {
|
|||||||
disabled={!randomGame}
|
disabled={!randomGame}
|
||||||
>
|
>
|
||||||
<div style={{ width: 16, height: 16, position: "relative" }}>
|
<div style={{ width: 16, height: 16, position: "relative" }}>
|
||||||
<img
|
<Lottie
|
||||||
src={starsIconAnimated}
|
animationData={starsAnimation}
|
||||||
alt="Stars animation"
|
|
||||||
style={{ width: 70, position: "absolute", top: -28, left: -27 }}
|
style={{ width: 70, position: "absolute", top: -28, left: -27 }}
|
||||||
|
loop={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{t("surprise_me")}
|
{t("surprise_me")}
|
||||||
@@ -194,9 +163,10 @@ export default function Home() {
|
|||||||
<h2 style={{ display: "flex", gap: SPACING_UNIT }}>
|
<h2 style={{ display: "flex", gap: SPACING_UNIT }}>
|
||||||
{currentCatalogueCategory === CatalogueCategory.Hot && (
|
{currentCatalogueCategory === CatalogueCategory.Hot && (
|
||||||
<div style={{ width: 24, height: 24, position: "relative" }}>
|
<div style={{ width: 24, height: 24, position: "relative" }}>
|
||||||
<img
|
<Lottie
|
||||||
src={flameIconAnimated}
|
animationData={flameAnimation}
|
||||||
alt="Flame animation"
|
loop={false}
|
||||||
|
autoplay
|
||||||
style={{
|
style={{
|
||||||
width: 40,
|
width: 40,
|
||||||
top: -10,
|
top: -10,
|
||||||
|
|||||||
@@ -254,7 +254,6 @@ export function ProfileHero() {
|
|||||||
if (gameRunning)
|
if (gameRunning)
|
||||||
return {
|
return {
|
||||||
...gameRunning,
|
...gameRunning,
|
||||||
objectId: gameRunning.objectID,
|
|
||||||
sessionDurationInSeconds: gameRunning.sessionDurationInMillis / 1000,
|
sessionDurationInSeconds: gameRunning.sessionDurationInMillis / 1000,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -331,7 +330,7 @@ export function ProfileHero() {
|
|||||||
<Link
|
<Link
|
||||||
to={buildGameDetailsPath({
|
to={buildGameDetailsPath({
|
||||||
...currentGame,
|
...currentGame,
|
||||||
objectId: currentGame.objectId,
|
objectId: currentGame.objectID,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{currentGame.title}
|
{currentGame.title}
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Button, Modal } from "../../components";
|
||||||
|
import { SPACING_UNIT } from "../../theme.css";
|
||||||
|
|
||||||
|
export interface UserFriendsModalProps {
|
||||||
|
visible: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SubscriptionTourModal = ({
|
||||||
|
visible,
|
||||||
|
onClose,
|
||||||
|
}: UserFriendsModalProps) => {
|
||||||
|
const { t } = useTranslation("tour");
|
||||||
|
|
||||||
|
const handleSubscribeClick = () => {
|
||||||
|
window.electron.openCheckout().finally(onClose);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
visible={visible}
|
||||||
|
title={t("subscription_tour_title")}
|
||||||
|
onClose={onClose}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
|
justifyContent: "space-around",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h2>Hydra Cloud</h2>
|
||||||
|
<ul style={{ margin: "0", padding: "0" }}>
|
||||||
|
<li style={{ margin: `${SPACING_UNIT}px ${SPACING_UNIT * 2}px` }}>
|
||||||
|
{t("cloud_saving")}
|
||||||
|
</li>
|
||||||
|
<li style={{ margin: `${SPACING_UNIT}px ${SPACING_UNIT * 2}px` }}>
|
||||||
|
{t("cloud_achievements")}
|
||||||
|
</li>
|
||||||
|
<li style={{ margin: `${SPACING_UNIT}px ${SPACING_UNIT * 2}px` }}>
|
||||||
|
{t("show_and_compare_achievements")}
|
||||||
|
</li>
|
||||||
|
<li style={{ margin: `${SPACING_UNIT}px ${SPACING_UNIT * 2}px` }}>
|
||||||
|
{t("animated_profile_banner")}
|
||||||
|
</li>
|
||||||
|
<li style={{ margin: `${SPACING_UNIT}px ${SPACING_UNIT * 2}px` }}>
|
||||||
|
{t("animated_profile_picture")}
|
||||||
|
</li>
|
||||||
|
<li style={{ margin: `${SPACING_UNIT}px ${SPACING_UNIT * 2}px` }}>
|
||||||
|
{t("premium_support")}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button onClick={handleSubscribeClick}>{t("subscribe_now")}</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -101,7 +101,6 @@ export const UserFriendModalAddFriend = ({
|
|||||||
>
|
>
|
||||||
{isAddingFriend ? t("sending") : t("add")}
|
{isAddingFriend ? t("sending") : t("add")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={handleClickSeeProfile}
|
onClick={handleClickSeeProfile}
|
||||||
disabled={isAddingFriend}
|
disabled={isAddingFriend}
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
$background-color: #1c1c1c;
|
|
||||||
$dark-background-color: #151515;
|
|
||||||
|
|
||||||
$muted-color: #c0c1c7;
|
|
||||||
$body-color: #8e919b;
|
|
||||||
|
|
||||||
$border-color: #424244;
|
|
||||||
$success-color: #1c9749;
|
|
||||||
$danger-color: #e11d48;
|
|
||||||
$warning-color: #ffc107;
|
|
||||||
|
|
||||||
$disabled-opacity: 0.5;
|
|
||||||
$active-opacity: 0.7;
|
|
||||||
|
|
||||||
$spacing-unit: 8px;
|
|
||||||
|
|
||||||
$toast-z-index: 5;
|
|
||||||
$bottom-panel-z-index: 3;
|
|
||||||
$title-bar-z-index: 4;
|
|
||||||
$backdrop-z-index: 4;
|
|
||||||
$modal-z-index: 5;
|
|
||||||
8
src/renderer/src/vite-env.d.ts
vendored
8
src/renderer/src/vite-env.d.ts
vendored
@@ -1,10 +1,2 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
/// <reference types="vite-plugin-svgr/client" />
|
/// <reference types="vite-plugin-svgr/client" />
|
||||||
|
|
||||||
interface ImportMetaEnv {
|
|
||||||
readonly RENDERER_VITE_INTERCOM_APP_ID: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ImportMeta {
|
|
||||||
readonly env: ImportMetaEnv;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -55,9 +55,6 @@ export const removeDuplicateSpaces = (name: string) =>
|
|||||||
|
|
||||||
export const replaceDotsWithSpace = (name: string) => name.replace(/\./g, " ");
|
export const replaceDotsWithSpace = (name: string) => name.replace(/\./g, " ");
|
||||||
|
|
||||||
export const replaceNbspWithSpace = (name: string) =>
|
|
||||||
name.replace(new RegExp(String.fromCharCode(160), "g"), " ");
|
|
||||||
|
|
||||||
export const replaceUnderscoreWithSpace = (name: string) =>
|
export const replaceUnderscoreWithSpace = (name: string) =>
|
||||||
name.replace(/_/g, " ");
|
name.replace(/_/g, " ");
|
||||||
|
|
||||||
@@ -72,7 +69,6 @@ export const formatName = pipe<string>(
|
|||||||
removeSpecialEditionFromName,
|
removeSpecialEditionFromName,
|
||||||
replaceUnderscoreWithSpace,
|
replaceUnderscoreWithSpace,
|
||||||
replaceDotsWithSpace,
|
replaceDotsWithSpace,
|
||||||
replaceNbspWithSpace,
|
|
||||||
(str) => str.replace(/DIRECTOR'S CUT/g, ""),
|
(str) => str.replace(/DIRECTOR'S CUT/g, ""),
|
||||||
removeSymbolsFromName,
|
removeSymbolsFromName,
|
||||||
removeDuplicateSpaces,
|
removeDuplicateSpaces,
|
||||||
|
|||||||
@@ -245,7 +245,6 @@ export interface Subscription {
|
|||||||
export interface UserDetails {
|
export interface UserDetails {
|
||||||
id: string;
|
id: string;
|
||||||
username: string;
|
username: string;
|
||||||
email: string | null;
|
|
||||||
displayName: string;
|
displayName: string;
|
||||||
profileImageUrl: string | null;
|
profileImageUrl: string | null;
|
||||||
backgroundImageUrl: string | null;
|
backgroundImageUrl: string | null;
|
||||||
@@ -258,7 +257,6 @@ export interface UserProfile {
|
|||||||
id: string;
|
id: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
profileImageUrl: string | null;
|
profileImageUrl: string | null;
|
||||||
email: string | null;
|
|
||||||
backgroundImageUrl: string | null;
|
backgroundImageUrl: string | null;
|
||||||
profileVisibility: ProfileVisibility;
|
profileVisibility: ProfileVisibility;
|
||||||
libraryGames: UserGame[];
|
libraryGames: UserGame[];
|
||||||
@@ -375,4 +373,4 @@ export interface ComparedAchievements {
|
|||||||
export * from "./steam.types";
|
export * from "./steam.types";
|
||||||
export * from "./real-debrid.types";
|
export * from "./real-debrid.types";
|
||||||
export * from "./ludusavi.types";
|
export * from "./ludusavi.types";
|
||||||
export * from "./how-long-to-beat.types";
|
export * from "./howlongtobeat.types";
|
||||||
|
|||||||
Reference in New Issue
Block a user