Compare commits

..

114 Commits

Author SHA1 Message Date
Nate
ee0e314b29 css fix 2025-01-18 18:45:54 -03:00
Nate
d50bb137e6 fixed everything 2025-01-18 15:17:52 -03:00
Nate
1bbf3b27bf added "as vars;" + vars.$ 2025-01-18 14:13:57 -03:00
Nate
3c9d036efd @import to @use 2025-01-18 13:48:00 -03:00
Nate
e97a6fe51a font syntax fix 2025-01-18 13:14:59 -03:00
Eight
ead094de01 Merge branch 'main' into feature/migration-to-scss 2025-01-18 13:08:30 -03:00
Nate
855a646d23 syntax fix 2025-01-17 22:42:14 -03:00
Nate
8192e5d8f1 full migration to scss 2025-01-17 20:16:57 -03:00
Nate
2bd4b69926 full migration to scss 2025-01-17 20:14:54 -03:00
Nate
62c6071395 full migration to scss 2025-01-17 20:03:20 -03:00
Nate
d1750fff59 full migration to scss 2025-01-17 20:00:00 -03:00
Nate
138244d5aa full migration to scss 2025-01-17 19:58:16 -03:00
Nate
d038398750 full migration to scss 2025-01-17 19:43:41 -03:00
Nate
b0eb7c16cd migration to scss 2025-01-17 19:26:27 -03:00
Nate
691dba26af migration to scss
all tsx files adjusted
added root vars on _variables.scss
2025-01-17 18:52:23 -03:00
Nate
ad330bd7a3 Merge branch 'feature/migration-to-scss' of https://github.com/hydralauncher/hydra into feature/migration-to-scss 2025-01-17 18:51:32 -03:00
Hachi-R
fe8f1b44db lint 2025-01-17 16:34:48 -03:00
Hachi-R
b9c072e7ac feat: integrate monaco editor 2025-01-17 16:34:06 -03:00
Nate
50df38856d migration to scss
no .tsx changes were made, yet
2025-01-17 14:11:35 -03:00
Chubby Granny Chaser
8a30e946e3 Merge pull request #1405 from vitorRibeiro7/hotfix-game-minimun-specs-accordion
Some checks failed
Release / build (ubuntu-latest) (push) Has been cancelled
Release / build (windows-latest) (push) Has been cancelled
fix: fix css bug on requirements details style
2025-01-17 16:09:17 +00:00
Hachi-R
f5395305eb Merge branch 'feature/migration-to-scss' of https://github.com/hydralauncher/hydra into feature/migration-to-scss 2025-01-17 12:37:33 -03:00
Hachi-R
3f29a78593 fix: show editor devtools in dev 2025-01-17 12:29:03 -03:00
Eight
71f3409275 Merge branch 'main' into feature/migration-to-scss 2025-01-17 12:27:30 -03:00
vitorRibeiro7
cd871ec359 refactor: add non re-render rules to useEffect 2025-01-17 12:25:35 -03:00
Hachi-R
c4f5d17b40 refactor: remove redundant condition 2025-01-17 12:25:00 -03:00
Chubby Granny Chaser
548b7c3f41 Merge branch 'main' into hotfix-game-minimun-specs-accordion 2025-01-17 15:18:28 +00:00
Hachi-R
686ec61a99 Merge branch 'main' into feature/migration-to-scss 2025-01-17 12:10:00 -03:00
Hachi-R
fb63ec864c feat: add editor window 2025-01-17 12:00:59 -03:00
vitorRibeiro7
bbcdb42708 ench: add dynamic height 2025-01-17 11:55:03 -03:00
vitorRibeiro7
e9d541498e fix: reset overflow hidden 2025-01-17 11:54:14 -03:00
vitorRibeiro7
4e34f41ee0 ench: remove maxHeight on sidebar section 2025-01-17 11:33:44 -03:00
vitorRibeiro7
049c27cdb7 fix; revert minHeight 2025-01-17 11:32:28 -03:00
Hachi-R
e07297fc53 lint 2025-01-17 10:30:35 -03:00
Hachi-R
c17839ae97 feat: add aparence tab to settings page 2025-01-17 10:27:59 -03:00
vitorRibeiro7
2ba653429f fix: fix css bug on requirements details style 2025-01-16 18:20:08 -03:00
Zamitto
9574e39d75 Merge pull request #1404 from hydrasources/patch-11
Some checks are pending
Release / build (ubuntu-latest) (push) Waiting to run
Release / build (windows-latest) (push) Waiting to run
Update RU translation.json
2025-01-16 15:42:38 -03:00
hydrasources
8ebb5edfbc Update translation.json 2025-01-16 21:29:28 +03:00
Zamitto
69787ee068 Merge pull request #1402 from hydralauncher/feat/manage-account-buttons
Some checks are pending
Release / build (ubuntu-latest) (push) Waiting to run
Release / build (windows-latest) (push) Waiting to run
feat: manage account buttons
2025-01-16 13:45:24 -03:00
Zamitto
d1fa4895e4 feat: adjust spacing 2025-01-16 12:58:09 -03:00
Hachi-R
395f77e17c refactor: change notifications header to paragraph 2025-01-16 12:18:25 -03:00
Zamitto
5d0e825880 fix: i18n 2025-01-16 11:57:28 -03:00
Zamitto
b06339d362 feat: handle refreshToken failure 2025-01-16 11:55:13 -03:00
Zamitto
3d0bf11359 feat: update text 2025-01-16 11:53:10 -03:00
Zamitto
2346a5bf86 feat: add renewal info 2025-01-16 11:32:59 -03:00
Hachi-R
f6707a5c84 lint 2025-01-16 11:30:24 -03:00
Hachi-R
ee4c564698 Merge branch 'main' into feature/migration-to-scss 2025-01-16 11:30:09 -03:00
Zamitto
81cb73c243 feat: code suggestion 2025-01-16 11:22:10 -03:00
Zamitto
923f7d7e80 feat: review 2025-01-16 09:48:08 -03:00
Zamitto
153ab05174 feat: remove unused string 2025-01-16 09:47:24 -03:00
Zamitto
ff0ef74066 feat: update icon order 2025-01-16 09:38:28 -03:00
Zamitto
bc2ee2dc4c feat: texts and layout 2025-01-16 01:12:34 -03:00
Zamitto
d866face54 feat: add account updated listener 2025-01-16 00:21:44 -03:00
Zamitto
44fd971c95 feat: refactor open auth 2025-01-15 23:56:37 -03:00
Zamitto
9941460c60 feat: code review 2025-01-15 17:13:36 -03:00
Zamitto
15f721ac39 feat: use Avatar component and remove non null assertion 2025-01-15 17:11:02 -03:00
Zamitto
56fabb2881 fix: hook dependencies 2025-01-15 17:08:07 -03:00
Zamitto
ffd3e37b48 feat: refactor 2025-01-15 16:57:41 -03:00
Zamitto
c4378c0ffc feat: update user details on settings account tab 2025-01-15 16:29:36 -03:00
Zamitto
af4fcb8f06 feat: manage account buttons 2025-01-15 15:49:11 -03:00
Zamitto
d4be5b8c66 Merge pull request #1386 from hydrasources/patch-10
Some checks failed
Release / build (ubuntu-latest) (push) Has been cancelled
Release / build (windows-latest) (push) Has been cancelled
Update RU translation.json
2025-01-07 13:48:35 -03:00
Zamitto
21a88b889f chore: run prettier 2025-01-07 13:45:31 -03:00
Zamitto
a0a3697516 fix: missing comma 2025-01-07 13:08:13 -03:00
Zamitto
317434f663 fix: lint error 2025-01-07 13:02:20 -03:00
hydrasources
cac2a7a70e Update translation.json 2025-01-07 18:53:34 +03:00
Chubby Granny Chaser
11700b7c16 Merge pull request #1378 from Shisuiicaro/Feature/Datanodes-Hoster
Some checks are pending
Release / build (ubuntu-latest) (push) Waiting to run
Release / build (windows-latest) (push) Waiting to run
Feat: Datanodes Hoster
2025-01-07 08:40:45 +00:00
Chubby Granny Chaser
2407be0fb2 Merge branch 'main' into Feature/Datanodes-Hoster 2025-01-06 20:43:37 +00:00
Shisuys
2a31c32cda Update datanodes.ts 2025-01-05 17:32:09 -03:00
Zamitto
7a7f270482 Merge pull request #1384 from mikropsoft/main
Some checks failed
Release / build (ubuntu-latest) (push) Has been cancelled
Release / build (windows-latest) (push) Has been cancelled
Update TR Locales
2025-01-05 15:47:44 -03:00
Zamitto
ab2d8c351b Removing excessive new lines 2025-01-05 15:37:25 -03:00
𝗦𝗵𝗟𝗲𝗿𝗣
87acdea5ab Update TR Locales 2025-01-05 21:08:52 +03:00
Eight
385db5c936 Merge pull request #1301 from hydralauncher/feature/reset-achievements
Some checks failed
Release / build (ubuntu-latest) (push) Has been cancelled
Release / build (windows-latest) (push) Has been cancelled
feat: add reset achievements modal
2025-01-03 19:41:24 -03:00
Hachi-R
cade56bb12 feat: disable reset achievements button if user is not logged in 2025-01-03 19:15:31 -03:00
Hachi-R
3efb1425b9 lint 2025-01-03 18:50:13 -03:00
Hachi-R
2df57b071d feat: disable reset achievement button if has no achievements 2025-01-03 18:50:02 -03:00
Hachi-R
5061695500 lint 2025-01-03 18:22:25 -03:00
Hachi-R
190ddeb46e refactor: improve logging for deleted game achievements 2025-01-03 18:22:13 -03:00
Hachi-R
e6d76a5dbe lint 2025-01-03 17:56:39 -03:00
Hachi-R
2ddda4e4d2 refactor: remove error logging 2025-01-03 17:56:13 -03:00
Hachi-R
ef3bf98903 feat: add success and error toast 2025-01-03 17:36:55 -03:00
Hachi-R
b68fe300ba refactor: rename state variable for clarity 2025-01-03 17:24:37 -03:00
Hachi-R
29ba0cca85 fix: update button disabled state logic 2025-01-03 17:21:09 -03:00
Shisuys
82b1d710c2 Datanodes support 2025-01-03 16:58:37 -03:00
Hachi-R
93b86f8c6c refactor: improve reset achievements handling and modal state management 2025-01-03 12:38:06 -03:00
Hachi-R
8cf549ff05 refactor: enhance logging in resetGameAchievement 2025-01-03 12:16:32 -03:00
Hachi-R
257a71d626 fix: change console.info to console.log 2025-01-02 09:37:58 -03:00
Hachi-R
f3d617a13a feat: log response after deleting game achievements 2025-01-02 09:37:23 -03:00
Hachi-R
9672e649e4 feat: log deleted achievement files 2025-01-02 09:36:09 -03:00
Hachi-R
e2f798c627 refactor: simplify resetGameAchievements by replacing Promise.all with a for loop 2025-01-02 09:35:05 -03:00
Hachi-R
52c159fe51 fix: replace console.error with achievementsLogger.error 2025-01-02 09:34:28 -03:00
Hachi-R
9849fbb31c refactor: change ResetAchievementsModalProps to use Readonly type for better immutability 2025-01-02 06:36:55 -03:00
Hachi-R
addc2a74d3 lint 2025-01-02 06:28:45 -03:00
Hachi-R
10766526c5 refactor: streamline resetGameAchievements with a single try catch 2025-01-02 06:28:32 -03:00
Hachi-R
bfdc2787d4 feat: remove hame achievements from remote db 2025-01-02 06:14:56 -03:00
Hachi-R
c60cd4bee4 Merge remote-tracking branch 'origin/main' into feature/reset-achievements 2025-01-02 05:28:57 -03:00
Zamitto
59bc23bbd8 Merge pull request #1367 from hydralauncher/fix/issues
Some checks failed
Release / build (ubuntu-latest) (push) Has been cancelled
Release / build (windows-latest) (push) Has been cancelled
fix: issues
2025-01-01 20:32:05 -03:00
Zamitto
16b50fc22b feat: remove launch options temporarily 2025-01-01 20:19:57 -03:00
Zamitto
720a7aa2a0 chore: bump version 2025-01-01 19:53:48 -03:00
Zamitto
5c2bafcfe8 Merge pull request #1371 from hydralauncher/feature/adding-sentry
Feature/adding sentry
2025-01-01 19:48:17 -03:00
Chubby Granny Chaser
c30c685ee4 fix: fixing translation 2025-01-01 21:50:26 +00:00
Chubby Granny Chaser
fba86002d1 feat: adding translation for button 2025-01-01 21:38:28 +00:00
Chubby Granny Chaser
a121ef77c0 feat: adding translation for button 2025-01-01 21:32:22 +00:00
Chubby Granny Chaser
bd653be071 Merge branch 'main' into fix/issues 2025-01-01 21:31:36 +00:00
Zamitto
297ca5a190 Merge pull request #1369 from 7ROBE/patch-6
Some checks are pending
Release / build (ubuntu-latest) (push) Waiting to run
Release / build (windows-latest) (push) Waiting to run
Update ar translation.json
2025-01-01 10:31:04 -03:00
7ROBE
6278600b98 Update translation.json 2025-01-01 11:11:41 +03:00
Zamitto
1226483deb fix: open game with parameters 2024-12-31 19:38:17 -03:00
Zamitto
0661cbd661 fix: steamGenres 2024-12-31 19:32:41 -03:00
Zamitto
ad204e3879 fix: issues 2024-12-31 14:06:44 -03:00
Hachi-R
afcfcbf482 refactor: clean up reset game achievements logic 2024-12-17 13:55:45 -03:00
Hachi-R
ac6eb247df feat: implement reset game achievements functionality 2024-12-17 13:15:55 -03:00
Hachi-R
47a5f4d327 feat: add reset achievements modal 2024-12-17 11:10:25 -03:00
Chubby Granny Chaser
cedb61cb38 feat: removing insert custom styles 2024-12-16 16:21:02 +00:00
Chubby Granny Chaser
a292164a55 feat: adding demo theme composer 2024-11-08 17:05:38 +00:00
bumyy
4b59a007f4 feat: migration to scss 2024-11-08 13:31:40 -03:00
bumyy
c9e99d3852 feat: migrated to scss 2024-11-07 20:23:03 -03:00
216 changed files with 5924 additions and 4836 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "hydralauncher", "name": "hydralauncher",
"version": "3.1.4", "version": "3.1.5",
"description": "Hydra", "description": "Hydra",
"main": "./out/main/index.js", "main": "./out/main/index.js",
"author": "Los Broxas", "author": "Los Broxas",
@@ -36,6 +36,7 @@
"@electron-toolkit/utils": "^3.0.0", "@electron-toolkit/utils": "^3.0.0",
"@fontsource/noto-sans": "^5.1.0", "@fontsource/noto-sans": "^5.1.0",
"@hookform/resolvers": "^3.9.1", "@hookform/resolvers": "^3.9.1",
"@monaco-editor/react": "^4.6.0",
"@primer/octicons-react": "^19.9.0", "@primer/octicons-react": "^19.9.0",
"@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-dropdown-menu": "^2.1.2",
"@reduxjs/toolkit": "^2.2.3", "@reduxjs/toolkit": "^2.2.3",
@@ -47,13 +48,13 @@
"auto-launch": "^5.0.6", "auto-launch": "^5.0.6",
"axios": "^1.7.9", "axios": "^1.7.9",
"better-sqlite3": "^11.7.0", "better-sqlite3": "^11.7.0",
"check-disk-space": "^3.4.0",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"color": "^4.2.3", "color": "^4.2.3",
"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.10", "dexie": "^4.0.10",
"diskusage": "^1.2.0",
"electron-log": "^5.2.4", "electron-log": "^5.2.4",
"electron-updater": "^6.3.9", "electron-updater": "^6.3.9",
"file-type": "^19.6.0", "file-type": "^19.6.0",

View File

@@ -4,399 +4,414 @@
"successfully_signed_in": "تم تسجيل الدخول بنجاح" "successfully_signed_in": "تم تسجيل الدخول بنجاح"
}, },
"home": { "home": {
"featured": ميّز", "featured": ُتَمَيِّز",
"surprise_me": "فاجئني", "surprise_me": َاجِئْنِي",
"no_results": م يتم العثور على نتائج", "no_results": َمْ يُعْثَرْ عَلَى نَتائِج",
"start_typing": "بدء الكتابة للبحث...", "start_typing": "اِبْدَأْ بِالْكِتَابَةِ لِلْبَحْثِ...",
"hot": "الأكثر رواجا الآن", "hot": "اَلْأَكْثَرُ شُيُوعًا الْآن",
"weekly": "📅 أفضل ألعاب الأسبوع", "weekly": "📅 أَفْضَلُ أَلْعَابِ الْأُسْبُوعِ",
"achievements": "🏆 ألعاب للتغلب عليها" "achievements": "🏆 أَلْعَابٌ لِلتَّغَلُّبِ عَلَيْهَا"
}, },
"sidebar": { "sidebar": {
"catalogue": "قائمة الألعاب", "catalogue": "الْفِهْرِسُ",
"downloads": "التنزيلات", "downloads": "التَّنْزِيلَاتُ",
"settings": "إعدادات", "settings": "الإعْدَادَاتُ",
"my_library": كتبتي", "my_library": َكْتَبَتِي",
"downloading_metadata": "{{title}} (جارٍ تنزيل البيانات الوصفية...)", "downloading_metadata": "{{title}} (جَارٍ تَنْزِيلُ الْبَيَانَاتِ الْوَصْفِيَّةِ...)",
"paused": "{{title}} (متوقف مؤقتًا)", "paused": "{{title}} (مُوْقَفٌ)",
"downloading": "{{title}} ({{percentage}} - جاري التنزيل...)", "downloading": "{{title}} ({{percentage}} - جَارٍ التَّنْزِيلُ...)",
"filter": "بحث في المكتبة", "filter": "تَصْفِيَةُ الْمَكْتَبَةِ",
"home": "الرئيسية", "home": "الرَّئِيسِيَّةُ",
"queued": "{{title}} (في قائمة الانتظار)", "queued": "{{title}} (فِي الْانْتِظَارِ)",
"game_has_no_executable": "لم يتم تحديد اللعبة القابلة للتنفيذ", "game_has_no_executable": "اللُّعْبَةُ لَيْسَ لَدَيْهَا مِلَفٌّ تَنْفِيذِيٌّ مُحَدَّدٌ",
"sign_in": سجيل الدخول", "sign_in": َسْجِيلُ الدُّخُولِ",
"friends": "أصدقاء", "friends": "الْأَصْدِقَاءُ",
"need_help": "بحاجة الى مساعدة؟" "need_help": "هَلْ تَحْتَاجُ إِلَى مُسَاعَدَةٍ؟"
}, },
"header": { "header": {
"search": "ابحث عن الألعاب", "search": "بَحْثُ الْأَلْعَابِ",
"home": "الرئيسية", "home": "الرَّئِيسِيَّةُ",
"catalogue": "قائمة الألعاب", "catalogue": "الْفِهْرِسُ",
"downloads": "التنزيلات", "downloads": "التَّنْزِيلَاتُ",
"search_results": "نتائج البحث", "search_results": َتائِجُ الْبَحْثِ",
"settings": "إعدادات", "settings": "الإعْدَادَاتُ",
"version_available_install": "إصدار {{version}} متاح. ", "version_available_install": "الْإِصْدَارُ {{version}} مَتَوَفِّرٌ. انْقُرْ هُنَا لِإِعَادَةِ التَّشْغِيلِ وَالتَّثْبِيتِ.",
"version_available_download": "إصدار {{version}} متاح. " "version_available_download": "الْإِصْدَارُ {{version}} مَتَوَفِّرٌ. انْقُرْ هُنَا لِلتَّنْزِيلِ."
}, },
"bottom_panel": { "bottom_panel": {
"no_downloads_in_progress": "لا توجد تنزيلات قيد التقدم", "no_downloads_in_progress": َا تَوْجَدُ تَنْزِيلَاتٌ جَارِيَةٌ",
"downloading_metadata": "جارٍ التنزيل {{title}} البيانات الوصفية...", "downloading_metadata": َارٍ تَنْزِيلُ الْبَيَانَاتِ الْوَصْفِيَّةِ لِـ {{title}}...",
"downloading": "جارٍ التنزيل {{title}} ({{percentage}} مكتملة) - الانتهاء {{eta}} - {{speed}}", "downloading": َارٍ تَنْزِيلُ {{title}}... ({{percentage}} مَكْتُومٌ) - الِاكْتِمَالُ {{eta}} - {{speed}}",
"calculating_eta": "جارٍ التنزيل {{title}} ({{percentage}} مكتمل) - حساب الوقت المتبقي...", "calculating_eta": َارٍ تَنْزِيلُ {{title}}... ({{percentage}} مَكْتُومٌ) - جَارٍ حِسَابُ الْوَقْتِ الْمُتَبَقِّي...",
"checking_files": "التحقق {{title}} ملفات…({{percentage}} مكتمل)" "checking_files": "جَارٍ التَّحَقُّقُ مِنْ مَلَفَّاتِ {{title}}... ({{percentage}} مَكْتُومٌ)"
}, },
"catalogue": { "catalogue": {
"next_page": "الصفحة التالية", "search": "تَصْفِيَةٌ...",
"previous_page": "الصفحة السابقة" "developers": "الْمُطَوِّرُونَ",
"genres": "الْأَنْوَاعُ",
"tags": "الْعَلَامَاتُ",
"publishers": "النَّاشِرُونَ",
"download_sources": "مَصَادِرُ التَّنْزِيلِ",
"result_count": "{{resultCount}} نَتائِجُ",
"filter_count": "{{filterCount}} مَتَوَفِّرٌ",
"clear_filters": "مَسْحُ {{filterCount}} الْمُخْتَارَةِ"
}, },
"game_details": { "game_details": {
"open_download_options": "افتح خيارات التنزيل", "open_download_options": "فَتْحُ خِيَارَاتِ التَّنْزِيلِ",
"download_options_zero": "{{count}} خيارات التنزيل", "download_options_zero": "لَا تَوْجَدُ خِيَارَاتُ تَنْزِيلٍ",
"updated_at": "تم التحديث {{updated_at}}", "download_options_one": "{{count}} خِيَارُ تَنْزِيلٍ",
"install": "ثَبَّتَ", "download_options_other": "{{count}} خِيَارَاتُ تَنْزِيلٍ",
"resume": "استئناف", "updated_at": "تَمَّ التَّحْدِيثُ فِي {{updated_at}}",
"pause": "إيقاف", "install": "تَثْبِيتٌ",
"cancel": "إلغاء", "resume": "اسْتِئْنَافٌ",
"remove": زالة", "pause": ِيقَافٌ",
"space_left_on_disk": "{{space}} متبقية على القرص", "cancel": "إِلْغَاءٌ",
"eta": "الوقت المتبقي {{eta}}", "remove": "إِزَالَةٌ",
"calculating_eta": "جارٍ حساب الوقت المتبقي…", "space_left_on_disk": "{{space}} مُتَبَقٍّ عَلَى الْقُرْصِ",
"downloading_metadata": "جارٍ تنزيل البيانات الوصفية…", "eta": "الِاكْتِمَالُ {{eta}}",
"filter": "إعادة حزم التصفية", "calculating_eta": "جَارٍ حِسَابُ الْوَقْتِ الْمُتَبَقِّي...",
"requirements": "متطلبات النظام", "downloading_metadata": "جَارٍ تَنْزِيلُ الْبَيَانَاتِ الْوَصْفِيَّةِ...",
"minimum": "الحد الأدنى", "filter": "تَصْفِيَةُ الْإِصْدَارَاتِ الْمُعَادِ تَغْلِيفُهَا",
"recommended": "مُستَحسَن", "requirements": "مُتَطَلَّبَاتُ النِّظَامِ",
"paused": "متوقف مؤقتًا", "minimum": "الْأَدْنَى",
"release_date": "صدر بتاريخ {{date}}", "recommended": "الْمُوَصَّى بِهِ",
"publisher": "نشرت من قبل {{publisher}}", "paused": "مُوْقَفٌ",
"hours": "ساعات", "release_date": "تَمَّ الْإِصْدَارُ فِي {{date}}",
"minutes": "دقائق", "publisher": "نُشِرَ بِوَاسِطَةِ {{publisher}}",
"amount_hours": "{{amount}} ساعات", "hours": َاعَاتٌ",
"amount_minutes": "{{amount}} دقائق", "minutes": "دَقَائِقُ",
"accuracy": "{{accuracy}}٪ دقة", "amount_hours": "{{amount}} سَاعَاتٌ",
"add_to_library": "أضف إلى المكتبة", "amount_minutes": "{{amount}} دَقَائِقُ",
"remove_from_library": "إزالة من المكتبة", "accuracy": "دِقَّةٌ {{accuracy}}%",
"no_downloads": "لا التنزيلات المتاحة", "add_to_library": "إِضَافَةٌ إِلَى الْمَكْتَبَةِ",
"play_time": "تم اللعب لمدة {{amount}}", "remove_from_library": "إِزَالَةٌ مِنَ الْمَكْتَبَةِ",
"last_time_played": "لعبت آخر مرة {{period}}", "no_downloads": "لَا تَوْجَدُ تَنْزِيلَاتٌ مَتَوَفِّرَةٌ",
"not_played_yet": "أنت لم تلعب {{title}} حتى الآن", "play_time": "لُعِبَ لِمُدَّةِ {{amount}}",
"next_suggestion": "الاقتراح التالي", "last_time_played": "آخِرُ مَرَّةٍ لُعِبَتْ {{period}}",
"play": "لعب", "not_played_yet": "لَمْ تَلْعَبْ {{title}} بَعْدُ",
"deleting": "جارٍ حذف المثبت…", "next_suggestion": "الِاقْتِرَاحُ التَّالِي",
"close": "إغلاق", "play": "لَعِبٌ",
"playing_now": "قيداللعب الآن", "deleting": "جَارٍ حَذْفُ الْمُثَبِّتِ...",
"change": "تغيير", "close": "إِغْلَاقٌ",
"repacks_modal_description": "اختر الحزمة التي تريد تنزيلها", "playing_now": "جَارِي اللَّعِبُ الْآن",
"select_folder_hint": "لتغيير المجلد الافتراضي، انتقل إلى <0>إعدادات</0>", "change": "تَغْيِيرٌ",
"download_now": "قم بالتنزيل الآن", "repacks_modal_description": "اخْتَرِ الْإِصْدَارَ الْمُعَادَ تَغْلِيفُهُ الَّذِي تُرِيدُ تَنْزِيلَهُ",
"no_shop_details": "لا يمكن استرداد تفاصيل المتجر.", "select_folder_hint": "لِتَغْيِيرِ الْمَجَلَّدِ الافْتِرَاضِيِّ، اذْهَبْ إِلَى <0>الإعْدَادَاتِ</0>",
"download_options": "خيارات التنزيل", "download_now": "تَنْزِيلٌ الْآن",
"download_path": "مسار التحميل", "no_shop_details": "لَمْ يَتَمَكَّنْ مِنْ اسْتِرْدَادِ تَفَاصِيلِ الْمَتْجَرِ.",
"previous_screenshot": "لقطة الشاشة السابقة", "download_options": "خِيَارَاتُ التَّنْزِيلِ",
"next_screenshot": "لقطة الشاشة التالية", "download_path": "مَسَارُ التَّنْزِيلِ",
"screenshot": قطة الشاشة {{number}}", "previous_screenshot": َقْطَةُ الشَّاشَةِ السَّابِقَةُ",
"open_screenshot": "فتح لقطة الشاشة {{number}}", "next_screenshot": "لَقْطَةُ الشَّاشَةِ التَّالِيَةُ",
"download_settings": "تحميل الإعدادات", "screenshot": "لَقْطَةُ الشَّاشَةِ {{number}}",
"downloader": "أداة التنزيل", "open_screenshot": "فَتْحُ لَقْطَةِ الشَّاشَةِ {{number}}",
"select_executable": "يختار", "download_settings": "إعْدَادَاتُ التَّنْزِيلِ",
"no_executable_selected": "لم يتم تحديد أي ملف قابل للتنفيذ", "downloader": "الْمُنَزِّلُ",
"open_folder": "افتح المجلد", "select_executable": "تَحْدِيدٌ",
"open_download_location": "انظر الملفات التي تم تنزيلها", "no_executable_selected": "لَمْ يُحَدَّدْ مِلَفٌّ تَنْفِيذِيٌّ",
"create_shortcut": "إنشاء اختصار سطح المكتب", "open_folder": "فَتْحُ الْمَجَلَّدِ",
"clear": "واضح", "open_download_location": "مُشَاهَدَةُ الْمَلَفَّاتِ الْمُنَزَّلَةِ",
"remove_files": "إزالة الملفات", "create_shortcut": "إِنْشَاءُ طَرِيقٍ مُخْتَصَرٍ عَلَى سَطْحِ الْمَكْتَبِ",
"remove_from_library_title": "هل أنت متأكد؟", "clear": "مَسْحٌ",
"remove_from_library_description": "سيتم إزالة هذا {{game}} من مكتبتك", "remove_files": "إِزَالَةُ الْمَلَفَّاتِ",
"options": "خيارات", "remove_from_library_title": "هَلْ أَنْتَ مُتَأَكِّدٌ؟",
"executable_section_title": "قابل للتنفيذ", "remove_from_library_description": "سَيُؤَدِّي هَذَا إِلَى إِزَالَةِ {{game}} مِنْ مَكْتَبَتِكَ",
"executable_section_description": "مسار الملف الذي سيتم تنفيذه عند النقر فوق \"تشغيل\".", "options": "خِيَارَاتٌ",
"downloads_secion_title": "التنزيلات", "executable_section_title": "الْمِلَفُّ التَّنْفِيذِيُّ",
"downloads_section_description": "تحقق من التحديثات أو الإصدارات الأخرى من هذه اللعبة", "executable_section_description": "مَسَارُ الْمِلَفِّ الَّذِي سَيَتِمُّ تَنْفِيذُهُ عِنْدَ النَّقْرِ عَلَى \"لَعِبٌ\"",
"danger_zone_section_title": "منطقة الخطر", "downloads_secion_title": "التَّنْزِيلَاتُ",
"danger_zone_section_description": "قم بإزالة هذه اللعبة من مكتبتك أو الملفات التي تم تنزيلها بواسطة Hydra", "downloads_section_description": "تَحَقَّقْ مِنَ التَّحْدِيثَاتِ أَوِ الْإِصْدَارَاتِ الْأُخْرَى لِهَذِهِ اللُّعْبَةِ",
"download_in_progress": "التنزيل قيد التقدم", "danger_zone_section_title": "مِنْطَقَةُ الْخَطَرِ",
"download_paused": "تم إيقاف التنزيل مؤقتًا", "danger_zone_section_description": "إِزَالَةُ هَذِهِ اللُّعْبَةِ مِنْ مَكْتَبَتِكَ أَوِ الْمَلَفَّاتِ الْمُنَزَّلَةِ بِوَاسِطَةِ Hydra",
"last_downloaded_option": "آخر خيار تم تنزيله", "download_in_progress": "جَارٍ التَّنْزِيلُ",
"create_shortcut_success": "تم إنشاء الاختصار بنجاح", "download_paused": "التَّنْزِيلُ مُوْقَفٌ",
"create_shortcut_error": "حدث خطأ أثناء إنشاء الاختصار", "last_downloaded_option": "خِيَارُ التَّنْزِيلِ الْأَخِيرُ",
"nsfw_content_title": "تحتوي هذه اللعبة على محتوى غير مناسب", "create_shortcut_success": "تَمَّ إِنْشَاءُ الطَّرِيقِ الْمُخْتَصَرِ بِنَجَاحٍ",
"nsfw_content_description": "{{title}} يحتوي على محتوى قد لا يكون مناسبًا لجميع الأعمار. ", "create_shortcut_error": "خَطَأٌ فِي إِنْشَاءِ الطَّرِيقِ الْمُخْتَصَرِ",
"allow_nsfw_content": "اسمح", "nsfw_content_title": "هَذِهِ اللُّعْبَةُ تَحْتَوِي عَلَى مُحْتَوًى غَيْرِ لَائِقٍ",
"refuse_nsfw_content": "عُد", "nsfw_content_description": "{{title}} تَحْتَوِي عَلَى مُحْتَوًى قَدْ لَا يَكُونُ مُنَاسِبًا لِجَمِيعِ الْأَعْمَارِ. هَلْ أَنْتَ مُتَأَكِّدٌ مِنْ أَنَّكَ تُرِيدُ الْمُتَابَعَةَ؟",
"stats": "احصائيات", "allow_nsfw_content": "الْمُتَابَعَةُ",
"download_count": "التنزيلات", "refuse_nsfw_content": "الرُّجُوعُ",
"player_count": "اللاعبين النشطين", "stats": "الإحْصَائِيَّاتُ",
"download_error": "خيار التنزيل هذا غير متوفر", "download_count": "التَّنْزِيلَاتُ",
"download": "تحميل", "player_count": "اللَّاعِبُونَ النَّشِطُونَ",
"executable_path_in_use": "قابل للتنفيذ قيد الاستخدام بالفعل بواسطة \"{{game}}\"", "download_error": "هَذَا خِيَارُ التَّنْزِيلِ غَيْرُ مَتَوَفِّرٍ",
"warning": حذير:", "download": َنْزِيلٌ",
"hydra_needs_to_remain_open": "لإجراء هذا التنزيل، يجب أن يظل Hydra مفتوحًا حتى اكتماله. ", "executable_path_in_use": "الْمِلَفُّ التَّنْفِيذِيُّ مُسْتَخْدَمٌ بِوَاسِطَةِ \"{{game}}\"",
"achievements": "الإنجازات", "warning": "تَنْبِيهٌ:",
"achievements_count": "الإنجازات {{unlockedCount}}/{{achievementsCount}}", "hydra_needs_to_remain_open": "لِهَذَا التَّنْزِيلِ، يَجِبُ أَنْ يَبْقَى Hydra مَفْتُوحًا حَتَّى يَتِمَّ الِاكْتِمَالُ. إِذَا أُغْلِقَ Hydra قَبْلَ الِاكْتِمَالِ، سَتَفْقِدُ تَقَدُّمَكَ.",
"cloud_save": "حفظ السحابة", "achievements": "الإِنْجَازَاتُ",
"cloud_save_description": "احفظ تقدمك في السحابة واستمر في اللعب على أي جهاز", "achievements_count": "الإِنْجَازَاتُ {{unlockedCount}}/{{achievementsCount}}",
"backups": "النسخ الاحتياطية", "cloud_save": "حِفْظٌ سَحَابِيٌّ",
"install_backup": "ثَبَّتَ", "cloud_save_description": "احْفَظْ تَقَدُّمَكَ فِي السَّحَابَةِ وَاسْتَمِرَّ فِي اللَّعِبِ عَلَى أَيِّ جِهَازٍ",
"delete_backup": "يمسح", "backups": "الْنُسَخُ الِاحْتِيَاطِيَّةُ",
"create_backup": "نسخة احتياطية جديدة", "install_backup": "تَثْبِيتٌ",
"last_backup_date": "آخر نسخة احتياطية قيد التشغيل {{date}}", "delete_backup": "حَذْفٌ",
"no_backup_preview": "لم يتم العثور على ألعاب محفوظة لهذا العنوان", "create_backup": "نُسْخَةٌ احْتِيَاطِيَّةٌ جَدِيدَةٌ",
"restoring_backup": "استعادة النسخة الاحتياطية ({{progress}} مكتمل)…", "last_backup_date": "آخِرُ نُسْخَةٍ احْتِيَاطِيَّةٍ فِي {{date}}",
"uploading_backup": "جارٍ تحميل النسخة الاحتياطية…", "no_backup_preview": "لَمْ يُعْثَرْ عَلَى أَيِّ أَلْعَابٍ مَحْفُوظَةٍ لِهَذَا الْعُنْوَانِ",
"no_backups": "لم تقم بإنشاء أي نسخ احتياطية لهذه اللعبة حتى الآن", "restoring_backup": "جَارٍ اسْتِعَادَةُ النُّسْخَةِ الِاحْتِيَاطِيَّةِ ({{progress}} مَكْتُومٌ)...",
"backup_uploaded": "تم تحميل النسخة الاحتياطية", "uploading_backup": "جَارٍ رَفْعُ النُّسْخَةِ الِاحْتِيَاطِيَّةِ...",
"backup_deleted": "تم حذف النسخة الاحتياطية", "no_backups": "لَمْ تَقُمْ بِإِنْشَاءِ أَيِّ نُسَخٍ احْتِيَاطِيَّةٍ لِهَذِهِ اللُّعْبَةِ بَعْدُ",
"backup_restored": مت استعادة النسخة الاحتياطية", "backup_uploaded": َمَّ رَفْعُ النُّسْخَةِ الِاحْتِيَاطِيَّةِ",
"see_all_achievements": "شاهد جميع الإنجازات", "backup_deleted": "تَمَّ حَذْفُ النُّسْخَةِ الِاحْتِيَاطِيَّةِ",
"sign_in_to_see_achievements": "قم بتسجيل الدخول لرؤية الإنجازات", "backup_restored": "تَمَّ اسْتِعَادَةُ النُّسْخَةِ الِاحْتِيَاطِيَّةِ",
"mapping_method_automatic": "تلقائي", "see_all_achievements": "عَرْضُ جَمِيعِ الإِنْجَازَاتِ",
"mapping_method_manual": "يدوي", "sign_in_to_see_achievements": "سَجِّلِ الدُّخُولَ لِعَرْضِ الإِنْجَازَاتِ",
"mapping_method_label": "طريقة رسم الخرائط", "mapping_method_automatic": "آلِيٌّ",
"files_automatically_mapped": "تم تعيين الملفات تلقائيًا", "mapping_method_manual": "يَدَوِيٌّ",
"no_backups_created": "لم يتم إنشاء نسخ احتياطية لهذه اللعبة", "mapping_method_label": "طَرِيقَةُ التَّحْدِيدِ",
"manage_files": "إدارة الملفات", "files_automatically_mapped": "تَمَّ تَحْدِيدُ الْمَلَفَّاتِ تِلْقَائِيًّا",
"loading_save_preview": "جارٍ البحث عن حفظ الألعاب...", "no_backups_created": "لَمْ تُنْشَأْ أَيُّ نُسَخٍ احْتِيَاطِيَّةٍ لِهَذِهِ اللُّعْبَةِ",
"wine_prefix": "بادئة النبيذ", "manage_files": "إِدَارَةُ الْمَلَفَّاتِ",
"wine_prefix_description": "بادئة Wine المستخدمة لتشغيل هذه اللعبة", "loading_save_preview": "جَارٍ الْبَحْثُ عَنْ أَلْعَابٍ مَحْفُوظَةٍ...",
"no_download_option_info": "لا توجد معلومات متاحة", "wine_prefix": "بَادِئَةُ Wine",
"backup_deletion_failed": "فشل في حذف النسخة الاحتياطية", "wine_prefix_description": "بَادِئَةُ Wine الْمُسْتَخْدَمَةُ لِتَشْغِيلِ هَذِهِ اللُّعْبَةِ",
"max_number_of_artifacts_reached": "تم الوصول إلى الحد الأقصى لعدد النسخ الاحتياطية لهذه اللعبة", "launch_options": "خِيَارَاتُ الْإِطْلَاقِ",
"achievements_not_sync": "لا تتم مزامنة إنجازاتك", "launch_options_description": "يُمْكِنُ لِلْمُسْتَخْدِمِينَ الْمُتَقَدِّمِينَ إِدْخَالُ تَعْدِيلَاتٍ عَلَى خِيَارَاتِ الْإِطْلَاقِ",
"manage_files_description": "إدارة الملفات التي سيتم نسخها احتياطيًا واستعادتها", "launch_options_placeholder": "لَمْ يُحَدَّدْ أَيُّ مُعَامِلٍ",
"select_folder": "حدد المجلد", "no_download_option_info": "لَا تَوْجَدُ مَعْلُومَاتٌ مَتَوَفِّرَةٌ",
"backup_from": "نسخة احتياطية من {{date}}", "backup_deletion_failed": "فَشَلَ فِي حَذْفِ النُّسْخَةِ الِاحْتِيَاطِيَّةِ",
"custom_backup_location_set": "تعيين موقع النسخ الاحتياطي المخصص", "max_number_of_artifacts_reached": "تَمَّ بَلُوغُ الْعَدَدِ الْأَقْصَى لِلنُّسَخِ الِاحْتِيَاطِيَّةِ لِهَذِهِ اللُّعْبَةِ",
"no_directory_selected": "لم يتم تحديد أي دليل", "achievements_not_sync": "تَعَرَّفْ عَلَى كَيْفِيَّةِ مَزْجِ إِنْجَازَاتِكَ",
"download_options_one": "{{count}} خيار التنزيل", "manage_files_description": "إِدَارَةُ الْمَلَفَّاتِ الَّتِي سَيَتِمُّ نَسْخُهَا احْتِيَاطِيًّا وَاسْتِعَادَتُهَا",
"download_options_two": "{{count}} خيارات التنزيل", "select_folder": "تَحْدِيدُ الْمَجَلَّدِ",
"download_options_few": "{{count}} خيارات التنزيل", "backup_from": "نُسْخَةٌ احْتِيَاطِيَّةٌ مِنْ {{date}}",
"download_options_many": "{{count}} خيارات التنزيل", "custom_backup_location_set": "تَمَّ تَحْدِيدُ مَوْقِعِ النُّسْخَةِ الِاحْتِيَاطِيَّةِ الْمُخَصَّصِ",
"download_options_other": "{{count}} خيارات التنزيل" "no_directory_selected": "لَمْ يُحَدَّدْ أَيُّ دَلِيلٍ"
}, },
"activation": { "activation": {
"title": فعيل Hydra", "title": َفْعِيلُ Hydra",
"installation_id": عرف التثبيت:", "installation_id": ُعَرِّفُ التَّثْبِيتِ:",
"enter_activation_code": دخل رمز التفعيل الخاص بك", "enter_activation_code": َدْخِلْ رَمْزَ التَّفْعِيلِ",
"message": ذا كنت لا تعرف أين تطلب هذا، فلا ينبغي أن يكون لديك هذا.", "message": ِذَا كُنْتَ لَا تَعْرِفُ أَيْنَ تَطْلُبُ هَذَا، فَلا يَجِبُ أَنْ تَكُونَ لَدَيْكَ.",
"activate": "فعل", "activate": "تَفْعِيلٌ",
"loading": "تحميل…" "loading": "جَارٍ التَّحْمِيلُ..."
}, },
"downloads": { "downloads": {
"resume": "استئناف", "resume": "اسْتِئْنَافٌ",
"pause": "إيقاف مؤقت", "pause": ِيقَافٌ",
"eta": "الوقت المتبقي {{eta}}", "eta": "الِاكْتِمَالُ {{eta}}",
"paused": توقف مؤقتًا", "paused": ُوْقَفٌ",
"verifying": "جارٍ التحقق…", "verifying": َارٍ التَّحَقُّقُ...",
"completed": كتمل", "completed": َكْتُومٌ",
"removed": م يتم تحميلها", "removed": َمْ يُنَزَّلْ",
"cancel": لغاء", "cancel": ِلْغَاءٌ",
"filter": صفية الألعاب التي تم تنزيلها", "filter": َصْفِيَةُ الْأَلْعَابِ الْمُنَزَّلَةِ",
"remove": زالة", "remove": ِزَالَةٌ",
"downloading_metadata": "جارٍ تنزيل البيانات الوصفية…", "downloading_metadata": َارٍ تَنْزِيلُ الْبَيَانَاتِ الْوَصْفِيَّةِ...",
"deleting": "جارٍ حذف المثبت…", "deleting": َارٍ حَذْفُ الْمُثَبِّتِ...",
"delete": "إزالة المثبت", "delete": "حَذْفُ الْمُثَبِّتِ",
"delete_modal_title": "هل أنت متأكد؟", "delete_modal_title": "هَلْ أَنْتَ مُتَأَكِّدٌ؟",
"delete_modal_description": يؤدي هذا إلى إزالة كافة ملفات التثبيت من جهاز الكمبيوتر الخاص بك", "delete_modal_description": َيُؤَدِّي هَذَا إِلَى إِزَالَةِ جَمِيعِ مَلَفَّاتِ التَّثْبِيتِ مِنْ حَاسُوبِكَ",
"install": "ثَبَّتَ", "install": "تَثْبِيتٌ",
"download_in_progress": "في تَقَدم", "download_in_progress": "جَارٍ التَّنْفِيذُ",
"queued_downloads": "التنزيلات في قائمة الانتظار", "queued_downloads": "التَّنْزِيلَاتُ فِي الْانْتِظَارِ",
"downloads_completed": كتمل", "downloads_completed": َكْتُومٌ",
"queued": "في قائمة الانتظار", "queued": ِي الْانْتِظَارِ",
"no_downloads_title": "هذا فارغ", "no_downloads_title": "فَرَاغٌ تَامٌ",
"no_downloads_description": م تقم بتنزيل أي شيء باستخدام Hydra بعد، ولكن لم يفت الأوان بعد للبدء.", "no_downloads_description": َمْ تَقُمْ بِتَنْزِيلِ أَيِّ شَيْءٍ بِاسْتِخْدَامِ Hydra بَعْدُ، لَكِنَّهُ لَيْسَ مُتَأَخِّرًا لِلْبَدْءِ.",
"checking_files": "جارٍ فحص الملفات…" "checking_files": َارٍ التَّحَقُّقُ مِنَ الْمَلَفَّاتِ...",
"seeding": "الْبَذْرُ",
"stop_seeding": "إِيقَافُ الْبَذْرِ",
"resume_seeding": "اسْتِئْنَافُ الْبَذْرِ",
"options": "إِدَارَةٌ"
}, },
"settings": { "settings": {
"downloads_path": سار التنزيلات", "downloads_path": َسَارُ التَّنْزِيلَاتِ",
"change": حديث", "change": َحْدِيثٌ",
"notifications": "إشعارات", "notifications": "الإِشْعَارَاتُ",
"enable_download_notifications": ند اكتمال التنزيل", "enable_download_notifications": ِنْدَ اكْتِمَالِ التَّنْزِيلِ",
"enable_repack_list_notifications": ند إضافة حزمة جديدة", "enable_repack_list_notifications": ِنْدَ إِضَافَةِ إِصْدَارٍ مُعَادٍ تَغْلِيفِهِ جَدِيدٍ",
"real_debrid_api_token_label": مز Real-Debrid API", "real_debrid_api_token_label": َمْزُ واجهة برمجة التطبيقات Real-Debrid",
"quit_app_instead_hiding": "لا تخفي Hydra عند الإغلاق", "quit_app_instead_hiding": "لا تُخْفِ Hydra عِنْدَ الإِغْلَاقِ",
"launch_with_system": "قم بتشغيل Hydra عند بدء تشغيل النظام", "launch_with_system": "تَشْغِيلُ Hydra عِنْدَ بَدْءِ النِّظَامِ",
"general": "عام", "general": َامٌ",
"behavior": لوك", "behavior": ُلُوكٌ",
"download_sources": "تحميل المصادر", "download_sources": "مَصَادِرُ التَّنْزِيلِ",
"language": "لغة", "language": "اللُّغَةُ",
"real_debrid_api_token": مز API", "real_debrid_api_token": َمْزُ واجهة برمجة التطبيقات",
"enable_real_debrid": مكين ريال ديبريد", "enable_real_debrid": َمْكِينُ Real-Debrid",
"real_debrid_description": "Real-Debrid هو برنامج تنزيل غير مقيد يسمح لك بتنزيل الملفات بسرعة، ولا يقتصر ذلك إلا على سرعة الإنترنت لديك.", "real_debrid_description": "Real-Debrid هُوَ مُنَزِّلٌ غَيْرُ مَقْيُودٍ يَتِيحُ لَكَ تَنْزِيلَ الْمَلَفَّاتِ بِسُرْعَةٍ، مَحْدُودٌ فَقَطْ بِسُرْعَةِ الْإِنْتَرْنِتِ لَدَيْكَ.",
"real_debrid_invalid_token": مز API غير صالح", "real_debrid_invalid_token": َمْزُ واجهة برمجة التطبيقات غَيْرُ صَالِحٍ",
"real_debrid_api_token_hint": مكنك الحصول على رمز API الخاص بك <0>هنا</0>", "real_debrid_api_token_hint": ُمْكِنُكَ الْحُصُولُ عَلَى رَمْزِ واجهة برمجة التطبيقات <0>هُنَا</0>",
"real_debrid_free_account_error": "الحساب \"{{username}}\" هو حساب مجاني. يرجى الاشتراك في Real-Debrid", "real_debrid_free_account_error": "الْحِسَابُ \"{{username}}\" هُوَ حِسَابٌ مَجَّانِيٌّ. يَرْجَى الِاشْتِرَاكُ فِي Real-Debrid",
"real_debrid_linked_message": "حساب \"{{username}}\"مرتبط", "real_debrid_linked_message": "تَمَّ رَبْطُ الْحِسَابِ \"{{username}}\"",
"save_changes": فظ التغييرات", "save_changes": ِفْظُ التَّغْيِيرَاتِ",
"changes_saved": م حفظ التغييرات بنجاح", "changes_saved": َمَّ حِفْظُ التَّغْيِيرَاتِ بِنَجَاحٍ",
"download_sources_description": تقوم Hydra بجلب روابط التنزيل من هذه المصادر. ", "download_sources_description": َتَقُومُ Hydra بِجَلْبِ رَوَابِطِ التَّنْزِيلِ مِنْ هَذِهِ الْمَصَادِرِ. يَجِبُ أَنْ يَكُونَ عُنْوَانُ URL لِلْمَصْدَرِ رَابِطًا مُبَاشِرًا إِلَى مِلَفٍّ .json يَحْتَوِي عَلَى رَوَابِطِ التَّنْزِيلِ.",
"validate_download_source": "التحقق من صحة", "validate_download_source": "تَصْدِيقٌ",
"remove_download_source": زالة", "remove_download_source": ِزَالَةٌ",
"add_download_source": "أضف المصدر", "add_download_source": "إِضَافَةُ مَصْدَرٍ",
"download_count_zero": "{{countFormatted}} خيارات التنزيل", "download_count_zero": "لَا تَوْجَدُ خِيَارَاتُ تَنْزِيلٍ",
"download_source_url": "تنزيل عنوان URL المصدر", "download_count_one": "{{countFormatted}} خِيَارُ تَنْزِيلٍ",
"add_download_source_description": "أدخل عنوان URL لملف .json", "download_count_other": "{{countFormatted}} خِيَارَاتُ تَنْزِيلٍ",
"download_source_up_to_date": "محدث", "download_source_url": "عُنْوَانُ مَصْدَرِ التَّنْزِيلِ",
"download_source_errored": "خطأ", "add_download_source_description": "أَدْخِلْ عُنْوَانَ URL لِمِلَفٍّ .json",
"sync_download_sources": "مصادر المزامنة", "download_source_up_to_date": "مُحَدَّثٌ",
"removed_download_source": "تمت إزالة مصدر التنزيل", "download_source_errored": "خَطَأٌ",
"added_download_source": "تمت إضافة مصدر التنزيل", "sync_download_sources": "مَزْجُ الْمَصَادِرِ",
"download_sources_synced": تم مزامنة جميع مصادر التنزيل", "removed_download_source": َمَّ إِزَالَةُ مَصْدَرِ التَّنْزِيلِ",
"insert_valid_json_url": "أدخل عنوان URL صالحًا لـ JSON", "added_download_source": "تَمَّتْ إِضَافَةُ مَصْدَرِ التَّنْزِيلِ",
"found_download_option_zero": "وجد {{countFormatted}} خيارات التنزيل", "download_sources_synced": "تَمَّ مَزْجُ جَمِيعِ مَصَادِرِ التَّنْزِيلِ",
"import": "يستورد", "insert_valid_json_url": "أَدْخِلْ عُنْوَانَ JSON صَالِحًا",
"public": "عام", "found_download_option_zero": "لَمْ يُعْثَرْ عَلَى خِيَارِ تَنْزِيلٍ",
"private": "خاص", "found_download_option_one": "عُثِرَ عَلَى {{countFormatted}} خِيَارِ تَنْزِيلٍ",
"friends_only": "الأصدقاء فقط", "found_download_option_other": "عُثِرَ عَلَى {{countFormatted}} خِيَارَاتِ تَنْزِيلٍ",
"privacy": "خصوصية", "import": "اسْتِيرَادٌ",
"profile_visibility": "رؤية الملف الشخصي", "public": "عَامٌ",
"profile_visibility_description": "اختر من يمكنه رؤية ملفك الشخصي ومكتبتك", "private": "خَاصٌ",
"required_field": "هذه الخانة مطلوبه", "friends_only": "الْأَصْدِقَاءُ فَقَطْ",
"source_already_exists": "تمت إضافة هذا المصدر بالفعل", "privacy": "الْخُصُوصِيَّةُ",
"must_be_valid_url": "يجب أن يكون المصدر عنوان URL صالحًا", "profile_visibility": "رُؤْيَةُ الْمَلَفِّ الشَّخْصِيِّ",
"blocked_users": "المستخدمين المحظورين", "profile_visibility_description": "اخْتَرْ مَنْ يُمْكِنُهُ رُؤْيَةُ مَلَفِّكَ الشَّخْصِيِّ وَمَكْتَبَتِكَ",
"user_unblocked": "تم إلغاء حظر المستخدم", "required_field": "هَذَا الْحَقْلُ مَطْلُوبٌ",
"enable_achievement_notifications": "عندما يتم فتح الإنجاز", "source_already_exists": "تَمَّتْ إِضَافَةُ هَذَا الْمَصْدَرِ مِنْ قَبْلُ",
"launch_minimized": "تم تصغير إطلاق Hydra", "must_be_valid_url": "يَجِبُ أَنْ يَكُونَ الْمَصْدَرُ عُنْوَانَ URL صَالِحًا",
"disable_nsfw_alert": "تعطيل تنبيه NSFW", "blocked_users": "الْمُسْتَخْدِمُونَ الْمَحْظُورُونَ",
"show_hidden_achievement_description": "إظهار وصف الإنجازات المخفية قبل فتحها", "user_unblocked": "تَمَّ إِزَالَةُ حَظْرِ الْمُسْتَخْدِمِ",
"download_count_one": "{{countFormatted}} خيار التنزيل", "enable_achievement_notifications": "عِنْدَ فَتْحِ إِنْجَازٍ",
"download_count_two": "{{countFormatted}} خيارات التنزيل", "launch_minimized": "تَشْغِيلُ Hydra مُصَغَّرًا",
"download_count_few": "{{countFormatted}} خيارات التنزيل", "disable_nsfw_alert": "تَعْطِيلُ تَنْبِيهِ الْمُحْتَوَى غَيْرِ اللَّائِقِ",
"download_count_many": "{{countFormatted}} خيارات التنزيل", "seed_after_download_complete": "الْبَذْرُ بَعْدَ اكْتِمَالِ التَّنْزِيلِ",
"download_count_other": "{{countFormatted}} خيارات التنزيل", "show_hidden_achievement_description": "إِظْهَارُ وَصْفِ الإِنْجَازَاتِ الْمَخْفِيَّةِ قَبْلَ فَتْحِهَا"
"found_download_option_one": "وجد {{countFormatted}} خيار التنزيل",
"found_download_option_two": "وجد {{countFormatted}} خيارات التنزيل",
"found_download_option_few": "وجد {{countFormatted}} خيارات التنزيل",
"found_download_option_many": "وجد {{countFormatted}} خيارات التنزيل",
"found_download_option_other": "وجد {{countFormatted}} خيارات التنزيل"
}, },
"notifications": { "notifications": {
"download_complete": "اكتمل التنزيل", "download_complete": "اكْتِمَالُ التَّنْزِيلِ",
"game_ready_to_install": "{{title}} جاهز للتثبيت", "game_ready_to_install": "{{title}} جَاهِزٌ لِلتَّثْبِيتِ",
"repack_list_updated": م تحديث قائمة إعادة التعبئة", "repack_list_updated": َمَّ تَحْدِيثُ قَائِمَةِ الإِصْدَارَاتِ الْمُعَادَةِ تَغْلِيفُهَا",
"new_update_available": "إصدار {{version}} متاح", "repack_count_one": "{{count}} إِصْدَارٌ مُعَادٌ تَغْلِيفُهُ أُضِيفَ",
"restart_to_install_update": "أعد تشغيل Hydra لتثبيت التحديث", "repack_count_other": "{{count}} إِصْدَارَاتٌ مُعَادَةٌ تَغْلِيفُهَا أُضِيفَتْ",
"notification_achievement_unlocked_title": "تم فتح الإنجاز لـ {{game}}", "new_update_available": "الْإِصْدَارُ {{version}} مَتَوَفِّرٌ",
"notification_achievement_unlocked_body": "{{achievement}} وغيرها {{count}} تم فتحها", "restart_to_install_update": "أَعِدْ تَشْغِيلَ Hydra لِتَثْبِيتِ التَّحْدِيثِ",
"repack_count_zero": "{{count}} تمت إضافة العبوات", "notification_achievement_unlocked_title": "تَمَّ فَتْحُ إِنْجَازٍ لِـ {{game}}",
"repack_count_one": "{{count}} تمت إضافة أعد حزم", "notification_achievement_unlocked_body": "{{achievement}} وَ{{count}} أُخْرَى تَمَّ فَتْحُهَا"
"repack_count_two": "{{count}} تمت إضافة العبوات",
"repack_count_few": "{{count}} تمت إضافة العبوات",
"repack_count_many": "{{count}} تمت إضافة العبوات",
"repack_count_other": "{{count}} تمت إضافة العبوات"
}, },
"system_tray": { "system_tray": {
"open": "افتح Hydra", "open": "فَتْحُ Hydra",
"quit": "خروج" "quit": "الْخُرُوجُ"
}, },
"game_card": { "game_card": {
"no_downloads": "لا توجد تنزيلات متاحة" "no_downloads": َا تَوْجَدُ تَنْزِيلَاتٌ مَتَوَفِّرَةٌ"
}, },
"binary_not_found_modal": { "binary_not_found_modal": {
"title": "البرامج غير مثبتة", "title": "الْبَرَامِجُ غَيْرُ مُثَبَّتَةٍ",
"description": م يتم العثور على الملفات التنفيذية الخاصة بـ Wine أو Lutris على نظامك", "description": َمْ يُعْثَرْ عَلَى مَلَفَّاتٍ تَنْفِيذِيَّةٍ لِـ Wine أَوْ Lutris عَلَى نِظَامِكَ",
"instructions": حقق من الطريقة الصحيحة لتثبيت أي منها على توزيعة Linux لديك حتى تعمل اللعبة بشكل طبيعي" "instructions": َحَقَّقْ مِنَ الطَّرِيقَةِ الصَّحِيحَةِ لِتَثْبِيتِ أَيٍّ مِنْهُمَا عَلَى تَوْزِيعَةِ Linux لَدَيْكَ لِتَعْمَلَ اللُّعْبَةُ بِشَكْلٍ طَبِيعِيٍّ"
}, },
"modal": { "modal": {
"close": ر الإغلاق" "close": ِرُّ الإِغْلَاقِ"
}, },
"forms": { "forms": {
"toggle_password_visibility": بديل رؤية كلمة المرور" "toggle_password_visibility": َبْدِيلُ رُؤْيَةِ كَلِمَةِ الْمَرُورِ"
}, },
"user_profile": { "user_profile": {
"amount_hours": "{{amount}} ساعات", "amount_hours": "{{amount}} سَاعَاتٌ",
"amount_minutes": "{{amount}} دقائق", "amount_minutes": "{{amount}} دَقَائِقُ",
"last_time_played": "لعبت آخر مرة {{period}}", "last_time_played": "آخِرُ مَرَّةٍ لُعِبَتْ {{period}}",
"activity": "النشاط الأخير", "activity": "النَّشَاطُ الْأَخِيرُ",
"library": "مكتبة", "library": "الْمَكْتَبَةُ",
"total_play_time": جمالي وقت اللعب", "total_play_time": ِجْمَالِيُّ وَقْتِ اللَّعِبِ",
"no_recent_activity_title": "هممم... لا شيء هنا", "no_recent_activity_title": "هَمَمْ... لَا شَيْءَ هُنَا",
"no_recent_activity_description": م تلعب أي مباراة مؤخرًا. ", "no_recent_activity_description": َمْ تَلْعَبْ أَيَّ أَلْعَابٍ مُؤَخَّرًا. حَانَ الْوَقْتُ لِتَغْيِيرِ ذَلِكَ!",
"display_name": "اسم العرض", "display_name": "اسْمُ الْعَرْضِ",
"saving": "توفير", "saving": "جَارٍ الْحِفْظُ",
"save": "يحفظ", "save": "حِفْظٌ",
"edit_profile": حرير الملف الشخصي", "edit_profile": َحْرِيرُ الْمَلَفِّ الشَّخْصِيِّ",
"saved_successfully": م الحفظ بنجاح", "saved_successfully": َمَّ الْحِفْظُ بِنَجَاحٍ",
"try_again": "من فضلك، حاول مرة أخرى", "try_again": "الرَّجَاءُ الْمُحَاوَلَةُ مَرَّةً أُخْرَى",
"sign_out_modal_title": "هل أنت متأكد؟", "sign_out_modal_title": "هَلْ أَنْتَ مُتَأَكِّدٌ؟",
"cancel": لغاء", "cancel": ِلْغَاءٌ",
"successfully_signed_out": م تسجيل الخروج بنجاح", "successfully_signed_out": َمَّ تَسْجِيلُ الْخُرُوجِ بِنَجَاحٍ",
"sign_out": سجيل الخروج", "sign_out": َسْجِيلُ الْخُرُوجِ",
"playing_for": "اللعب من أجل {{amount}}", "playing_for": "جَارِي اللَّعِبُ لِمُدَّةِ {{amount}}",
"sign_out_modal_text": كتبتك مرتبطة بحسابك الحالي. ", "sign_out_modal_text": َكْتَبَتُكَ مُرْتَبِطَةٌ بِحِسَابِكَ الْحَالِيِّ. عِنْدَ تَسْجِيلِ الْخُرُوجِ، لَنْ تَكُونَ مَكْتَبَتُكَ مَرْئِيَّةً بَعْدَ الْآنِ، وَلَنْ يَتِمَّ حِفْظُ أَيِّ تَقَدُّمٍ. هَلْ تُرِيدُ الْمُتَابَعَةَ مَعَ تَسْجِيلِ الْخُرُوجِ؟",
"add_friends": "أضف أصدقاء", "add_friends": "إِضَافَةُ الْأَصْدِقَاءِ",
"add": "يضيف", "add": "إِضَافَةٌ",
"friend_code": مز الصديق", "friend_code": َمْزُ الصَّدِيقِ",
"see_profile": "انظر الملف الشخصي", "see_profile": "رُؤْيَةُ الْمَلَفِّ الشَّخْصِيِّ",
"sending": "إرسال", "sending": "جَارٍ الْإِرْسَالُ",
"friend_request_sent": م إرسال طلب الصداقة", "friend_request_sent": َمَّ إِرْسَالُ طَلَبِ الصَّدَاقَةِ",
"friends": "أصدقاء", "friends": "الْأَصْدِقَاءُ",
"friends_list": "قائمة الأصدقاء", "friends_list": َائِمَةُ الْأَصْدِقَاءِ",
"user_not_found": "لم يتم العثور على المستخدم", "user_not_found": "الْمُسْتَخْدِمُ غَيْرُ مَوْجُودٍ",
"block_user": ظر المستخدم", "block_user": َظْرُ الْمُسْتَخْدِمِ",
"add_friend": ضافة صديق", "add_friend": ِضَافَةُ صَدِيقٍ",
"request_sent": م إرسال الطلب", "request_sent": َمَّ إِرْسَالُ الطَّلَبِ",
"request_received": م استلام الطلب", "request_received": َمَّ اسْتِقْبَالُ الطَّلَبِ",
"accept_request": بول الطلب", "accept_request": َبُولُ الطَّلَبِ",
"ignore_request": جاهل الطلب", "ignore_request": َجَاهُلُ الطَّلَبِ",
"cancel_request": لغاء الطلب", "cancel_request": ِلْغَاءُ الطَّلَبِ",
"undo_friendship": "التراجع عن الصداقة", "undo_friendship": "إِلْغَاءُ الصَّدَاقَةِ",
"request_accepted": م قبول الطلب", "request_accepted": َمَّ قَبُولُ الطَّلَبِ",
"user_blocked_successfully": م حظر المستخدم بنجاح", "user_blocked_successfully": َمَّ حَظْرُ الْمُسْتَخْدِمِ بِنَجَاحٍ",
"user_block_modal_text": "هذا سوف يمنع {{displayName}}", "user_block_modal_text": "سَيُؤَدِّي هَذَا إِلَى حَظْرِ {{displayName}}",
"blocked_users": "المستخدمين المحظورين", "blocked_users": "الْمُسْتَخْدِمُونَ الْمَحْظُورُونَ",
"unblock": لغاء الحظر", "unblock": ِزَالَةُ الْحَظْرِ",
"no_friends_added": يس لديك أي أصدقاء مضافين", "no_friends_added": َيْسَ لَدَيْكَ أَصْدِقَاءٌ مُضَافُونَ",
"pending": يد الانتظار", "pending": َيْدُ الْانْتِظَارِ",
"no_pending_invites": يس لديك أي دعوات معلقة", "no_pending_invites": َيْسَ لَدَيْكَ دَعَوَاتٌ قَيْدُ الْانْتِظَارِ",
"no_blocked_users": يس لديك أي مستخدمين محظورين", "no_blocked_users": َيْسَ لَدَيْكَ مُسْتَخْدِمُونَ مَحْظُورُونَ",
"friend_code_copied": م نسخ رمز الصديق", "friend_code_copied": َمَّ نَسْخُ رَمْزِ الصَّدِيقِ",
"undo_friendship_modal_text": يؤدي هذا إلى التراجع عن صداقتك معه {{displayName}}", "undo_friendship_modal_text": َيُؤَدِّي هَذَا إِلَى إِلْغَاءِ صَدَاقَتِكَ مَعَ {{displayName}}",
"privacy_hint": ضبط من يمكنه رؤية هذا، انتقل إلى <0>إعدادات</0>", "privacy_hint": ِتَعْدِيلِ مَنْ يُمْكِنُهُ رُؤْيَةُ هَذَا، اذْهَبْ إِلَى <0>الإعْدَادَاتِ</0>",
"locked_profile": "هذا الملف الشخصي خاص", "locked_profile": "هَذَا الْمَلَفُّ الشَّخْصِيُّ خَاصٌّ",
"image_process_failure": شل أثناء معالجة الصورة", "image_process_failure": َشَلَ أَثْنَاءَ مُعَالَجَةِ الصُّورَةِ",
"required_field": "هذه الخانة مطلوبه", "required_field": "هَذَا الْحَقْلُ مَطْلُوبٌ",
"displayname_min_length": جب أن يتكون اسم العرض من 3 أحرف على الأقل", "displayname_min_length": َجِبُ أَنْ يَكُونَ اسْمُ الْعَرْضِ عَلَى الْأَقَلِّ 3 أَحْرُفٍ",
"displayname_max_length": جب ألا يزيد طول اسم العرض عن 50 حرفًا", "displayname_max_length": َجِبُ أَنْ يَكُونَ اسْمُ الْعَرْضِ عَلَى الْأَكْثَرِ 50 حَرْفًا",
"report_profile": "الإبلاغ عن هذا الملف الشخصي", "report_profile": "تَقْرِيرٌ عَنْ هَذَا الْمَلَفِّ الشَّخْصِيِّ",
"report_reason": ماذا تقوم بالإبلاغ عن هذا الملف الشخصي؟", "report_reason": ِمَاذَا تُقَدِّمُ تَقْرِيرًا عَنْ هَذَا الْمَلَفِّ الشَّخْصِيِّ؟",
"report_description": علومات إضافية", "report_description": َعْلُومَاتٌ إِضَافِيَّةٌ",
"report_description_placeholder": علومات إضافية", "report_description_placeholder": َعْلُومَاتٌ إِضَافِيَّةٌ",
"report": قرير", "report": َقْرِيرٌ",
"report_reason_hate": طاب الكراهية", "report_reason_hate": ِطَابُ الْكُرْهِ",
"report_reason_sexual_content": "المحتوى الجنسي", "report_reason_sexual_content": "مُحْتَوًى جِنْسِيٌّ",
"report_reason_violence": نف", "report_reason_violence": ُنْفٌ",
"report_reason_spam": سائل إلكترونية مزعجة", "report_reason_spam": َاسِلَةٌ عَشْوَائِيَّةٌ",
"profile_reported": "تم الإبلاغ عن الملف الشخصي", "report_reason_other": "آخَرُ",
"your_friend_code": "رمز صديقك:", "profile_reported": "تَمَّ تَقْرِيرُ الْمَلَفِّ الشَّخْصِيِّ",
"upload_banner": "تحميل لافتة", "your_friend_code": "رَمْزُ صَدِيقِكَ:",
"uploading_banner": "جارٍ تحميل البانر…", "upload_banner": "رَفْعُ لَافِتَةٍ",
"background_image_updated": "تم تحديث صورة الخلفية", "uploading_banner": "جَارٍ رَفْعُ اللَّافِتَةِ...",
"report_reason_zero": "آخر", "background_image_updated": "تَمَّ تَحْدِيثُ صُورَةِ الْخَلْفِيَّةِ",
"report_reason_one": "لماذا تقوم بالإبلاغ عن هذا الملف الشخصي؟", "stats": "الإحْصَائِيَّاتُ",
"report_reason_two": "آخر", "achievements": "الإِنْجَازَاتُ",
"report_reason_few": "آخر", "games": "الْأَلْعَابُ",
"report_reason_many": "آخر", "top_percentile": "الْأَفْضَلُ {{percentile}}%",
"report_reason_other": "آخر" "ranking_updated_weekly": "التَّرْتِيبُ يُحَدَّثُ أُسْبُوعِيًّا",
"playing": "جَارِي اللَّعِبُ {{game}}",
"achievements_unlocked": "الإِنْجَازَاتُ الْمَفْتُوحَةُ",
"earned_points": "النَّقَاطُ الْمَكْسُوبَةُ",
"show_achievements_on_profile": "عَرْضُ إِنْجَازَاتِكَ عَلَى مَلَفِّكَ الشَّخْصِيِّ",
"show_points_on_profile": "عَرْضُ النَّقَاطِ الْمَكْسُوبَةِ عَلَى مَلَفِّكَ الشَّخْصِيِّ"
}, },
"achievement": { "achievement": {
"achievement_unlocked": "تم فتح الإنجاز", "achievement_unlocked": "إِنْجَازٌ مَفْتُوحٌ",
"user_achievements": "{{displayName}}إنجازات", "user_achievements": "إِنْجَازَاتُ {{displayName}}",
"your_achievements": نجازاتك", "your_achievements": ِنْجَازَاتُكَ",
"unlocked_at": "مقفلة في: {{date}}", "unlocked_at": "تَمَّ الْفَتْحُ فِي: {{date}}",
"subscription_needed": "مطلوب اشتراك Hydra Cloud لرؤية هذا المحتوى", "subscription_needed": "يَحْتَاجُ اشْتِرَاكُ Hydra Cloud لِرُؤْيَةِ هَذَا الْمُحْتَوَى",
"new_achievements_unlocked": "مفتوح {{achievementCount}} انجازات جديدة من {{gameCount}} ألعاب", "new_achievements_unlocked": "تَمَّ فَتْحُ {{achievementCount}} إِنْجَازَاتٍ جَدِيدَةٍ مِنْ {{gameCount}} أَلْعَابٍ",
"achievement_progress": "{{unlockedCount}}/{{totalCount}} الإنجازات", "achievement_progress": "{{unlockedCount}}/{{totalCount}} إِنْجَازَاتٍ",
"achievements_unlocked_for_game": "مفتوح {{achievementCount}} انجازات جديدة ل {{gameTitle}}" "achievements_unlocked_for_game": "تَمَّ فَتْحُ {{achievementCount}} إِنْجَازَاتٍ جَدِيدَةٍ لِـ {{gameTitle}}",
"hidden_achievement_tooltip": "هَذَا إِنْجَازٌ مَخْفِيٌّ",
"achievement_earn_points": "اكْسِبْ {{points}} نَقَاطًا بِهَذَا الإِنْجَازِ",
"earned_points": "النَّقَاطُ الْمَكْسُوبَةُ:",
"available_points": "النَّقَاطُ الْمُتَوَفِّرَةُ:",
"how_to_earn_achievements_points": "كَيْفَ تَكْسِبُ نَقَاطَ الإِنْجَازَاتِ؟"
}, },
"hydra_cloud": { "hydra_cloud": {
"subscription_tour_title": "اشتراك Hydra كلاود", "subscription_tour_title": "اشْتِرَاكُ Hydra Cloud",
"subscribe_now": "اشترك الآن", "subscribe_now": "اشْتَرِكِ الْآنَ",
"cloud_saving": "الحفظ السحابي", "cloud_saving": "حِفْظٌ سَحَابِيٌّ",
"cloud_achievements": "احفظ إنجازاتك على السحابة", "cloud_achievements": "حِفْظُ إِنْجَازَاتِكَ فِي السَّحَابَةِ",
"animated_profile_picture": "صور شخصية متحركة", "animated_profile_picture": ُورُ الْمَلَفِّ الشَّخْصِيِّ الْمُتَحَرِّكَةِ",
"premium_support": "دعم متميز", "premium_support": "الدَّعْمُ الْمُتَقَدِّمُ",
"show_and_compare_achievements": رض ومقارنة إنجازاتك مع المستخدمين الآخرين", "show_and_compare_achievements": َرْضٌ وَمُقَارَنَةُ إِنْجَازَاتِكَ مَعَ مُسْتَخْدِمِينَ آخَرِينَ",
"animated_profile_banner": "لافتة الملف الشخصي المتحركة" "animated_profile_banner": َافِتَةُ الْمَلَفِّ الشَّخْصِيِّ الْمُتَحَرِّكَةِ",
"hydra_cloud": "Hydra Cloud",
"hydra_cloud_feature_found": "لَقَدْ اكْتَشَفْتَ مِيزَةً مِنْ Hydra Cloud!",
"learn_more": "تَعَلَّمْ أَكْثَرَ"
} }
} }

View File

@@ -58,7 +58,7 @@
}, },
"game_details": { "game_details": {
"launch_options": "Опции за стартиране", "launch_options": "Опции за стартиране",
"launch_options_description": "Напредналите потребители могат да въведат модификации на своите опции за стартиране", "launch_options_description": "Напредналите потребители могат да въведат модификации на своите опции за стартиране (экспериментальный)",
"launch_options_placeholder": "Няма зададен параметър", "launch_options_placeholder": "Няма зададен параметър",
"open_download_options": "Варианти за изтегляне", "open_download_options": "Варианти за изтегляне",
"download_options_zero": "Няма варианти за изтегляне", "download_options_zero": "Няма варианти за изтегляне",

View File

@@ -168,7 +168,7 @@
"wine_prefix": "Wine Prefix", "wine_prefix": "Wine Prefix",
"wine_prefix_description": "The Wine prefix used to run this game", "wine_prefix_description": "The Wine prefix used to run this game",
"launch_options": "Launch Options", "launch_options": "Launch Options",
"launch_options_description": "Advanced users may choose to enter modifications to their launch options", "launch_options_description": "Advanced users may choose to enter modifications to their launch options (experimental feature)",
"launch_options_placeholder": "No parameter specified", "launch_options_placeholder": "No parameter specified",
"no_download_option_info": "No information available", "no_download_option_info": "No information available",
"backup_deletion_failed": "Failed to delete backup", "backup_deletion_failed": "Failed to delete backup",
@@ -178,7 +178,13 @@
"select_folder": "Select folder", "select_folder": "Select folder",
"backup_from": "Backup from {{date}}", "backup_from": "Backup from {{date}}",
"custom_backup_location_set": "Custom backup location set", "custom_backup_location_set": "Custom backup location set",
"no_directory_selected": "No directory selected" "no_directory_selected": "No directory selected",
"no_write_permission": "Cannot download into this directory. Click here to learn more.",
"reset_achievements": "Reset achievements",
"reset_achievements_description": "This will reset all achievements for {{game}}",
"reset_achievements_title": "Are you sure?",
"reset_achievements_success": "Achievements successfully reset",
"reset_achievements_error": "Failed to reset achievements"
}, },
"activation": { "activation": {
"title": "Activate Hydra", "title": "Activate Hydra",
@@ -274,7 +280,23 @@
"launch_minimized": "Launch Hydra minimized", "launch_minimized": "Launch Hydra minimized",
"disable_nsfw_alert": "Disable NSFW alert", "disable_nsfw_alert": "Disable NSFW alert",
"seed_after_download_complete": "Seed after download complete", "seed_after_download_complete": "Seed after download complete",
"show_hidden_achievement_description": "Show hidden achievements description before unlocking them" "show_hidden_achievement_description": "Show hidden achievements description before unlocking them",
"account": "Account",
"no_users_blocked": "You have no blocked users",
"subscription_active_until": "Your Hydra Cloud is active until {{date}}",
"manage_subscription": "Manage subscription",
"update_email": "Update email",
"update_password": "Update password",
"current_email": "Current email:",
"no_email_account": "You have not set an email yet",
"account_data_updated_successfully": "Account data updated successfully",
"renew_subscription": "Renew Hydra Cloud",
"subscription_expired_at": "Your subscription expired at {{date}}",
"no_subscription": "Enjoy Hydra in the best possible way",
"become_subscriber": "Be Hydra Cloud",
"subscription_renew_cancelled": "Automatic renewal is disabled",
"subscription_renews_on": "Your subscription renews on {{date}}",
"bill_sent_until": "Your next bill will be sent until this day"
}, },
"notifications": { "notifications": {
"download_complete": "Download complete", "download_complete": "Download complete",

View File

@@ -7,7 +7,7 @@
"featured": "Destaques", "featured": "Destaques",
"hot": "Populares", "hot": "Populares",
"weekly": "📅 Mais baixados da semana", "weekly": "📅 Mais baixados da semana",
"achievements": "🏆 Pra platinar", "achievements": "🏆 Para platinar",
"surprise_me": "Surpreenda-me", "surprise_me": "Surpreenda-me",
"no_results": "Nenhum resultado encontrado", "no_results": "Nenhum resultado encontrado",
"start_typing": "Comece a digitar para pesquisar…" "start_typing": "Comece a digitar para pesquisar…"
@@ -156,7 +156,7 @@
"wine_prefix": "Prefixo Wine", "wine_prefix": "Prefixo Wine",
"wine_prefix_description": "O prefixo Wine que foi utilizado para instalar o jogo", "wine_prefix_description": "O prefixo Wine que foi utilizado para instalar o jogo",
"launch_options": "Opções de Inicialização", "launch_options": "Opções de Inicialização",
"launch_options_description": "Usuários avançados podem adicionar opções de inicialização no jogo", "launch_options_description": "Usuários avançados podem adicionar opções de inicialização no jogo (experimental)",
"launch_options_placeholder": "Nenhum parâmetro informado", "launch_options_placeholder": "Nenhum parâmetro informado",
"no_download_option_info": "Sem informações disponíveis", "no_download_option_info": "Sem informações disponíveis",
"backup_deletion_failed": "Falha ao apagar backup", "backup_deletion_failed": "Falha ao apagar backup",
@@ -167,7 +167,12 @@
"select_folder": "Selecione a pasta", "select_folder": "Selecione a pasta",
"manage_files_description": "Gerencie quais arquivos serão feitos backup", "manage_files_description": "Gerencie quais arquivos serão feitos backup",
"clear": "Limpar", "clear": "Limpar",
"no_directory_selected": "Nenhum diretório selecionado" "no_directory_selected": "Nenhum diretório selecionado",
"reset_achievements": "Resetar conquistas",
"reset_achievements_description": "Isso irá resetar todas as conquistas de {{game}}",
"reset_achievements_title": "Tem certeza?",
"reset_achievements_success": "Conquistas resetadas com sucesso",
"reset_achievements_error": "Falha ao resetar conquistas"
}, },
"activation": { "activation": {
"title": "Ativação", "title": "Ativação",
@@ -263,7 +268,23 @@
"launch_minimized": "Iniciar o Hydra minimizado", "launch_minimized": "Iniciar o Hydra minimizado",
"disable_nsfw_alert": "Desativar alerta de conteúdo inapropriado", "disable_nsfw_alert": "Desativar alerta de conteúdo inapropriado",
"seed_after_download_complete": "Semear após a conclusão do download", "seed_after_download_complete": "Semear após a conclusão do download",
"show_hidden_achievement_description": "Mostrar descrição de conquistas ocultas antes de debloqueá-las" "show_hidden_achievement_description": "Mostrar descrição de conquistas ocultas antes de debloqueá-las",
"account": "Conta",
"no_users_blocked": "Você não bloqueou nenhum usuário",
"subscription_active_until": "Sua assinatura Hydra Cloud ficará ativa até {{date}}",
"manage_subscription": "Gerenciar assinatura",
"update_email": "Atualizar email",
"update_password": "Atualizar senha",
"current_email": "Email atual:",
"no_email_account": "Você ainda não adicionou um email a sua conta",
"account_data_updated_successfully": "Dados da conta atualizados com sucesso",
"renew_subscription": "Renovar Hydra Cloud",
"subscription_expired_at": "Sua assinatura expirou em {{date}}",
"no_subscription": "Aproveite o Hydra da melhor forma possível",
"become_subscriber": "Seja Hydra Cloud",
"subscription_renew_cancelled": "A renovação automática está desativada",
"subscription_renews_on": "Sua assinatura renova dia {{date}}",
"bill_sent_until": "Sua próxima cobrança será enviada até esse dia"
}, },
"notifications": { "notifications": {
"download_complete": "Download concluído", "download_complete": "Download concluído",
@@ -392,7 +413,7 @@
"new_achievements_unlocked": "{{achievementCount}} novas conquistas de {{gameCount}} jogos", "new_achievements_unlocked": "{{achievementCount}} novas conquistas de {{gameCount}} jogos",
"achievement_progress": "{{unlockedCount}}/{{totalCount}} conquistas", "achievement_progress": "{{unlockedCount}}/{{totalCount}} conquistas",
"achievements_unlocked_for_game": "Desbloqueadas {{achievementCount}} novas conquistas em {{gameTitle}}", "achievements_unlocked_for_game": "Desbloqueadas {{achievementCount}} novas conquistas em {{gameTitle}}",
"hidden_achievement_tooltip": "Está é uma conquista oculta", "hidden_achievement_tooltip": "Esta é uma conquista oculta",
"achievement_earn_points": "Ganhe {{points}} pontos com essa conquista", "achievement_earn_points": "Ganhe {{points}} pontos com essa conquista",
"earned_points": "Pontos ganhos:", "earned_points": "Pontos ganhos:",
"available_points": "Pontos disponíveis:", "available_points": "Pontos disponíveis:",

View File

@@ -167,6 +167,9 @@
"loading_save_preview": "Поиск сохранений…", "loading_save_preview": "Поиск сохранений…",
"wine_prefix": "Префикс Wine", "wine_prefix": "Префикс Wine",
"wine_prefix_description": "Префикс Wine, используемый для запуска этой игры", "wine_prefix_description": "Префикс Wine, используемый для запуска этой игры",
"launch_options": "Параметры запуска",
"launch_options_description": "Опытные пользователи могут внести изменения в параметры запуска",
"launch_options_placeholder": "Параметр не указан ",
"no_download_option_info": "Информация недоступна", "no_download_option_info": "Информация недоступна",
"backup_deletion_failed": "Не удалось удалить резервную копию", "backup_deletion_failed": "Не удалось удалить резервную копию",
"max_number_of_artifacts_reached": "Достигнуто максимальное количество резервных копий для этой игры", "max_number_of_artifacts_reached": "Достигнуто максимальное количество резервных копий для этой игры",
@@ -175,7 +178,11 @@
"select_folder": "Выбрать папку", "select_folder": "Выбрать папку",
"backup_from": "Резервная копия от {{date}}", "backup_from": "Резервная копия от {{date}}",
"custom_backup_location_set": "Установлено настраиваемое местоположение резервной копии", "custom_backup_location_set": "Установлено настраиваемое местоположение резервной копии",
"no_directory_selected": "Не выбран каталог" "no_directory_selected": "Не выбран каталог",
"no_write_permission": "Невозможно загрузить в эту директорию. Нажмите здесь, чтобы узнать больше.",
"reset_achievements_title": "Вы уверены?",
"reset_achievements_success": "Достижения успешно сброшены",
"reset_achievements_error": "Не удалось сбросить достижения"
}, },
"activation": { "activation": {
"title": "Активировать Hydra", "title": "Активировать Hydra",
@@ -271,7 +278,23 @@
"source_already_exists": "Этот источник уже добавлен", "source_already_exists": "Этот источник уже добавлен",
"user_unblocked": "Пользователь разблокирован", "user_unblocked": "Пользователь разблокирован",
"seed_after_download_complete": "Раздавать после завершения загрузки", "seed_after_download_complete": "Раздавать после завершения загрузки",
"show_hidden_achievement_description": "Показывать описание скрытых достижений перед их получением" "show_hidden_achievement_description": "Показывать описание скрытых достижений перед их получением",
"account": "Аккаунт",
"no_users_blocked": "У вас нет заблокированных пользователей",
"subscription_active_until": "Ваша подписка на Hydra Cloud активна до {{date}}",
"manage_subscription": "Управлять подпиской",
"update_email": "Обновить электронную почту",
"update_password": "Обновить пароль",
"current_email": "Текущий email:",
"no_email_account": "Вы еще не установили электронную почту",
"account_data_updated_successfully": "Данные учетной записи успешно обновлены",
"renew_subscription": "Обновить подписку Hydra Cloud",
"subscription_expired_at": "Срок действия вашей подписки истек в {{date}}",
"no_subscription": "Наслаждайтесь Hydra по максимуму",
"become_subscriber": "Станьте обладателем Hydra Cloud",
"subscription_renew_cancelled": "Автоматическое продление отключено",
"subscription_renews_on": "Ваша подписка продлевается на {{date}}",
"bill_sent_until": "Ваш следующий счет будет отправлен до этого дня"
}, },
"notifications": { "notifications": {
"download_complete": "Загрузка завершена", "download_complete": "Загрузка завершена",

View File

@@ -1,131 +1,423 @@
{ {
"language_name": "Türkçe", "language_name": "Türkçe",
"app": {
"successfully_signed_in": "Başarıyla giriş yapıldı"
},
"home": { "home": {
"featured": "Öne çıkan", "featured": "Öne Çıkanlar",
"surprise_me": "Şaşırt beni", "surprise_me": "Beni Şaşırt",
"no_results": "Sonuç bulunamadı" "no_results": "Sonuç bulunamadı",
"start_typing": "Aramak için yazmaya başlayın...",
"hot": "Şu anda popüler",
"weekly": "📅 Haftanın en iyi oyunları",
"achievements": "🏆 Tamamlanacak oyunlar"
}, },
"sidebar": { "sidebar": {
"catalogue": "Katalog", "catalogue": "Katalog",
"downloads": "İndirmeler", "downloads": "İndirilenler",
"settings": "Ayarlar", "settings": "Ayarlar",
"my_library": "Kütüphane", "my_library": "Kütüphanem",
"downloading_metadata": "{{title}} (Metadata indiriliyor…)", "downloading_metadata": "{{title}} (Meta verileri indiriliyor…)",
"paused": "{{title}} (Duraklatıldı)", "paused": "{{title}} (Durduruldu)",
"downloading": "{{title}} ({{percentage}} - İndiriliyor…)", "downloading": "{{title}} ({{percentage}} - İndiriliyor…)",
"filter": "Kütüphaneyi filtrele", "filter": "Kütüphaneyi filtrele",
"home": "Ana menü" "home": "Ana Sayfa",
"queued": "{{title}} (Sırada)",
"game_has_no_executable": "Oyun için bir çalıştırılabilir dosya seçilmedi",
"sign_in": "Giriş yap",
"friends": "Arkadaşlar",
"need_help": "Yardıma mı ihtiyacınız var?"
}, },
"header": { "header": {
"search": "Ara", "search": "Oyunları ara",
"home": "Ana menü", "home": "Ana Sayfa",
"catalogue": "Katalog", "catalogue": "Katalog",
"downloads": "İndirmeler", "downloads": "İndirilenler",
"search_results": "Arama sonuçları", "search_results": "Arama sonuçları",
"settings": "Ayarlar" "settings": "Ayarlar",
"version_available_install": "Sürüm {{version}} mevcut. Yüklemek ve yeniden başlatmak için buraya tıklayın.",
"version_available_download": "Sürüm {{version}} mevcut. İndirmek için buraya tıklayın."
}, },
"bottom_panel": { "bottom_panel": {
"no_downloads_in_progress": "İndirilen bir şey yok", "no_downloads_in_progress": "Devam eden indirme yok",
"downloading_metadata": "{{title}} metadatası indiriliyor…", "downloading_metadata": "{{title}} meta verileri indiriliyor…",
"downloading": "{{title}} indiriliyor… ({{percentage}} tamamlandı) - Bitiş {{eta}} - {{speed}}" "downloading": "{{title}} indiriliyor… ({{percentage}} tamamlandı) - Tamamlama: {{eta}} - Hız: {{speed}}",
"calculating_eta": "{{title}} indiriliyor… ({{percentage}} tamamlandı) - Kalan süre hesaplanıyor…",
"checking_files": "{{title}} dosyaları kontrol ediliyor… ({{percentage}} tamamlandı)"
}, },
"catalogue": { "catalogue": {
"next_page": "Sonraki sayfa", "search": "Filtrele…",
"previous_page": "Önceki sayfa" "developers": "Geliştiriciler",
"genres": "Türler",
"tags": "Etiketler",
"publishers": "Yayıncılar",
"download_sources": "İndirme kaynakları",
"result_count": "{{resultCount}} sonuç",
"filter_count": "{{filterCount}} mevcut",
"clear_filters": "{{filterCount}} seçili filtreyi temizle"
}, },
"game_details": { "game_details": {
"open_download_options": "İndirme seçeneklerini aç", "open_download_options": "İndirme seçeneklerini aç",
"download_options_zero": "İndirme seçeneği yok", "download_options_zero": "İndirme seçeneği yok",
"download_options_one": "{{count}} indirme seçeneği", "download_options_one": "{{count}} indirme seçeneği",
"download_options_other": "{{count}} indirme seçeneği", "download_options_other": "{{count}} indirme seçeneği",
"updated_at": "{{updated_at}} güncellendi", "updated_at": "{{updated_at}} tarihinde güncellendi",
"install": "İndir", "install": "Yükle",
"resume": "Devam et", "resume": "Devam et",
"pause": "Duraklat", "pause": "Durdur",
"cancel": "İptal et", "cancel": "İptal et",
"remove": "Sil", "remove": "Kaldır",
"space_left_on_disk": "Diskte {{space}} yer kaldı", "space_left_on_disk": "Diskte {{space}} boş alan kaldı",
"eta": "Bitiş {{eta}}", "eta": "{{eta}} tahmini bitiş",
"downloading_metadata": "Metadata indiriliyor…", "calculating_eta": "Kalan süre hesaplanıyor…",
"filter": "Repackleri filtrele", "downloading_metadata": "Meta veriler indiriliyor…",
"filter": "Paketleri filtrele",
"requirements": "Sistem gereksinimleri", "requirements": "Sistem gereksinimleri",
"minimum": "Minimum", "minimum": "Minimum",
"recommended": "Önerilen", "recommended": "Önerilen",
"release_date": "{{date}} tarihinde çıktı", "paused": "Durduruldu",
"publisher": "{{publisher}} tarihinde yayınlandı", "release_date": "{{date}} tarihinde yayımlandı",
"hours": "saatler", "publisher": "{{publisher}} tarafından yayımlandı",
"minutes": "dakikalar", "hours": "saat",
"minutes": "dakika",
"amount_hours": "{{amount}} saat", "amount_hours": "{{amount}} saat",
"amount_minutes": "{{amount}} dakika", "amount_minutes": "{{amount}} dakika",
"accuracy": "%{{accuracy}} doğruluk", "accuracy": "{{accuracy}}% doğruluk",
"add_to_library": "Kütüphaneye ekle", "add_to_library": "Kütüphaneye ekle",
"remove_from_library": "Kütüphaneden kaldır", "remove_from_library": "Kütüphaneden kaldır",
"no_downloads": "İndirme yok", "no_downloads": "İndirilebilir içerik yok",
"play_time": "{{amount}} oynandı", "play_time": "{{amount}} süre oynandı",
"last_time_played": "Son oynanan {{period}}", "last_time_played": "Son oynama {{period}} önce",
"not_played_yet": "Bu {{title}} h oynanmadı", "not_played_yet": "{{title}} henüz oynanmadı",
"next_suggestion": "Sıradaki öneri", "next_suggestion": "Sonraki öneri",
"play": "Oyna", "play": "Oyna",
"deleting": "Installer siliniyor…", "deleting": "Yükleyici siliniyor…",
"close": "Kapat", "close": "Kapat",
"playing_now": imdi oynanıyor", "playing_now": u anda oynanıyor",
"change": "Değiştir", "change": "Değiştir",
"repacks_modal_description": "İndirmek istediğiiniz repacki seçin", "repacks_modal_description": "İndirmek istediğiniz paketi seçin",
"select_folder_hint": "Varsayılan klasörü değiştirmek için ulaşmanız gereken ayar", "select_folder_hint": "Varsayılan klasörü değiştirmek için <0>Ayarlar</0> bölümüne gidin",
"download_now": "Şimdi" "download_now": "Şimdi indir",
"no_shop_details": "Mağaza bilgileri alınamadı.",
"download_options": "İndirme seçenekleri",
"download_path": "İndirme yolu",
"previous_screenshot": "Önceki ekran görüntüsü",
"next_screenshot": "Sonraki ekran görüntüsü",
"screenshot": "{{number}} ekran görüntüsü",
"open_screenshot": "{{number}} ekran görüntüsünü aç",
"download_settings": "İndirme ayarları",
"downloader": "İndirici",
"select_executable": "Seç",
"no_executable_selected": "Hiçbir çalıştırılabilir dosya seçilmedi",
"open_folder": "Klasörü aç",
"open_download_location": "İndirilen dosyaları gör",
"create_shortcut": "Masaüstü kısayolu oluştur",
"clear": "Temizle",
"remove_files": "Dosyaları kaldır",
"remove_from_library_title": "Emin misiniz?",
"remove_from_library_description": "Bu işlem {{game}} oyununu kütüphanenizden kaldıracaktır",
"options": "Seçenekler",
"executable_section_title": "Çalıştırılabilir dosya",
"executable_section_description": "\"Oyna\" tıklandığında çalıştırılacak dosyanın yolu",
"downloads_secion_title": "İndirmeler",
"downloads_section_description": "Bu oyun için güncellemeleri veya diğer sürümleri kontrol edin",
"danger_zone_section_title": "Tehlike bölgesi",
"danger_zone_section_description": "Bu oyunu kütüphanenizden veya Hydra tarafından indirilen dosyaları kaldırın",
"download_in_progress": "İndirme devam ediyor",
"download_paused": "İndirme durduruldu",
"last_downloaded_option": "Son indirilen seçenek",
"create_shortcut_success": "Kısayol başarıyla oluşturuldu",
"create_shortcut_error": "Kısayol oluşturulurken hata oluştu",
"nsfw_content_title": "Bu oyun uygunsuz içerik içeriyor",
"nsfw_content_description": "{{title}} her yaş için uygun olmayabilecek içeriklere sahiptir. Devam etmek istediğinizden emin misiniz?",
"allow_nsfw_content": "Devam et",
"refuse_nsfw_content": "Geri dön",
"stats": "İstatistikler",
"download_count": "İndirme sayısı",
"player_count": "Aktif oyuncular",
"download_error": "Bu indirme seçeneği mevcut değil",
"download": "İndir",
"executable_path_in_use": "\"{{game}}\" tarafından kullanılan çalıştırılabilir dosya",
"warning": "Uyarı:",
"hydra_needs_to_remain_open": "Bu indirmenin tamamlanması için Hydra açık kalmalıdır. Eğer Hydra kapanırsa, ilerleme kaydedilmez.",
"achievements": "Başarılar",
"achievements_count": "Başarılar {{unlockedCount}}/{{achievementsCount}}",
"cloud_save": "Bulut kaydı",
"cloud_save_description": "İlerlemenizi buluta kaydedin ve herhangi bir cihazda oynamaya devam edin",
"backups": "Yedekler",
"install_backup": "Yükle",
"delete_backup": "Sil",
"create_backup": "Yeni yedek oluştur",
"last_backup_date": "{{date}} tarihindeki son yedek",
"no_backup_preview": "Bu oyun için kayıtlı oyun bulunamadı",
"restoring_backup": "Yedek geri yükleniyor ({{progress}} tamamlandı)…",
"uploading_backup": "Yedek yükleniyor…",
"no_backups": "Bu oyun için henüz bir yedek oluşturmadınız",
"backup_uploaded": "Yedek yüklendi",
"backup_deleted": "Yedek silindi",
"backup_restored": "Yedek geri yüklendi",
"see_all_achievements": "Tüm başarıları gör",
"sign_in_to_see_achievements": "Başarıları görmek için giriş yapın",
"mapping_method_automatic": "Otomatik",
"mapping_method_manual": "Manuel",
"mapping_method_label": "Eşleme yöntemi",
"files_automatically_mapped": "Dosyalar otomatik olarak eşlendi",
"no_backups_created": "Bu oyun için yedek oluşturulmadı",
"manage_files": "Dosyaları yönet",
"loading_save_preview": "Kayıtlı oyunlar aranıyor…",
"wine_prefix": "Wine Prefix",
"wine_prefix_description": "Bu oyunu çalıştırmak için kullanılan Wine Prefix",
"launch_options": "Başlatma Seçenekleri",
"launch_options_description": "İleri düzey kullanıcılar, başlatma seçeneklerine değişiklikler girebilir (deneysel özellik)",
"launch_options_placeholder": "Belirtilen bir parametre yok",
"no_download_option_info": "Bilgi mevcut değil",
"backup_deletion_failed": "Yedek silinemedi",
"max_number_of_artifacts_reached": "Bu oyun için maksimum yedek sayısına ulaşıldı",
"achievements_not_sync": "Başarılarınızı senkronize etmeyi öğrenin",
"manage_files_description": "Hangi dosyaların yedeklenip geri yükleneceğini yönetin",
"select_folder": "Klasör seç",
"backup_from": "{{date}} tarihinden yedek",
"custom_backup_location_set": "Özel yedekleme konumu ayarlandı",
"no_directory_selected": "Bir dizin seçilmedi",
"no_write_permission": "Bu dizine indirme yapılamaz. Daha fazla bilgi için buraya tıklayın.",
"reset_achievements": "Başarıları sıfırla",
"reset_achievements_description": "Bu işlem {{game}} için tüm başarıları sıfırlar",
"reset_achievements_title": "Emin misiniz?",
"reset_achievements_success": "Başarılar başarıyla sıfırlandı",
"reset_achievements_error": "Başarılar sıfırlanamadı"
}, },
"activation": { "activation": {
"title": "Hydra'yı aktif et", "title": "Hydra'yı Aktive Et",
"installation_id": "Kurulum ID'si:", "installation_id": "Kurulum Kimliği:",
"enter_activation_code": "Aktifleştirme kodunuzu girin", "enter_activation_code": "Aktivasyon kodunuzu girin",
"message": "Bunu nerede soracağınızı bilmiyorsanız, buna sahip olmamanız gerekiyor.", "message": "Bunu nereden soracağınızı bilmiyorsanız, bu sizin için olmamalı.",
"activate": "Aktif et", "activate": "Aktive Et",
"loading": "Yükleniyor…" "loading": "Yükleniyor…"
}, },
"downloads": { "downloads": {
"resume": "Devam et", "resume": "Devam Et",
"pause": "Duraklat", "pause": "Duraklat",
"eta": "Bitiş {{eta}}", "eta": "Tamamlama {{eta}}",
"paused": "Duraklatıldı", "paused": "Duraklatıldı",
"verifying": "Doğrulanıyor…", "verifying": "Doğrulanıyor…",
"completed": "Tamamlandı", "completed": "Tamamlandı",
"cancel": ptal et", "removed": ndirilmedi",
"filter": "Yüklü oyunları filtrele", "cancel": "İptal Et",
"filter": "İndirilen oyunları filtrele",
"remove": "Kaldır", "remove": "Kaldır",
"downloading_metadata": "Metadata indiriliyor…", "downloading_metadata": "Metadata indiriliyor…",
"deleting": "Installer siliniyor…", "deleting": "Yükleyici siliniyor…",
"delete": "Installer'ı sil", "delete": "Yükleyiciyi kaldır",
"delete_modal_title": "Emin misiniz?", "delete_modal_title": "Emin misiniz?",
"delete_modal_description": "Bu bilgisayarınızdan tüm kurulum dosyalarını silecek", "delete_modal_description": "Bu işlem, tüm kurulum dosyalarını bilgisayarınızdan kaldıracaktır",
"install": "Kur" "install": "Kur",
"download_in_progress": "Devam ediyor",
"queued_downloads": "Sıradaki indirmeler",
"downloads_completed": "Tamamlananlar",
"queued": "Sırada",
"no_downloads_title": "Bomboş",
"no_downloads_description": "Henüz Hydra ile hiçbir şey indirmediniz, ancak başlamak için asla geç değil.",
"checking_files": "Dosyalar kontrol ediliyor…",
"seeding": "Paylaşılıyor",
"stop_seeding": "Paylaşımı durdur",
"resume_seeding": "Paylaşımı sürdür",
"options": "Yönet"
}, },
"settings": { "settings": {
"downloads_path": "İndirme yolu", "downloads_path": "İndirme yolu",
"change": "Güncelle", "change": "Güncelle",
"notifications": "Bildirimler", "notifications": "Bildirimler",
"enable_download_notifications": "Bir indirme bittiğinde", "enable_download_notifications": "Bir indirme tamamlandığında",
"enable_repack_list_notifications": "Yeni bir repack eklendiğinde" "enable_repack_list_notifications": "Yeni bir repack eklendiğinde",
"real_debrid_api_token_label": "Real-Debrid API anahtarı",
"quit_app_instead_hiding": "Hydra'yı kapatırken gizlemeyin",
"launch_with_system": "Hydra'yı sistem başlatıldığında çalıştır",
"general": "Genel",
"behavior": "Davranış",
"download_sources": "İndirme kaynakları",
"language": "Dil",
"real_debrid_api_token": "API Anahtarı",
"enable_real_debrid": "Real-Debrid'i Etkinleştir",
"real_debrid_description": "Real-Debrid, yalnızca internet hızınızla sınırlı olarak hızlı dosya indirmenizi sağlayan sınırsız bir indirici.",
"real_debrid_invalid_token": "Geçersiz API anahtarı",
"real_debrid_api_token_hint": "API anahtarınızı <0>buradan</0> alabilirsiniz",
"real_debrid_free_account_error": "\"{{username}}\" hesabı ücretsiz bir hesaptır. Lütfen Real-Debrid abonesi olun",
"real_debrid_linked_message": "\"{{username}}\" hesabı bağlandı",
"save_changes": "Değişiklikleri Kaydet",
"changes_saved": "Değişiklikler başarıyla kaydedildi",
"download_sources_description": "Hydra, indirme bağlantılarını bu kaynaklardan alacak. Kaynak URL, indirme bağlantılarını içeren bir .json dosyasına doğrudan bir bağlantı olmalıdır.",
"validate_download_source": "Doğrula",
"remove_download_source": "Kaldır",
"add_download_source": "Kaynak ekle",
"download_count_zero": "İndirme seçeneği yok",
"download_count_one": "{{countFormatted}} indirme seçeneği",
"download_count_other": "{{countFormatted}} indirme seçeneği",
"download_source_url": "İndirme kaynağı URL'si",
"add_download_source_description": ".json dosyasının URL'sini girin",
"download_source_up_to_date": "Güncel",
"download_source_errored": "Hatalı",
"sync_download_sources": "Kaynakları senkronize et",
"removed_download_source": "İndirme kaynağı kaldırıldı",
"added_download_source": "İndirme kaynağı eklendi",
"download_sources_synced": "Tüm indirme kaynakları senkronize edildi",
"insert_valid_json_url": "Geçerli bir JSON URL'si girin",
"found_download_option_zero": "Hiçbir indirme seçeneği bulunamadı",
"found_download_option_one": "{{countFormatted}} indirme seçeneği bulundu",
"found_download_option_other": "{{countFormatted}} indirme seçeneği bulundu",
"import": "İçe aktar",
"public": "Herkese açık",
"private": "Gizli",
"friends_only": "Sadece arkadaşlar",
"privacy": "Gizlilik",
"profile_visibility": "Profil görünürlüğü",
"profile_visibility_description": "Profilinizi ve kütüphanenizi kimlerin görebileceğini seçin",
"required_field": "Bu alan gereklidir",
"source_already_exists": "Bu kaynak zaten eklenmiş",
"must_be_valid_url": "Kaynak geçerli bir URL olmalıdır",
"blocked_users": "Engellenen kullanıcılar",
"user_unblocked": "Kullanıcının engeli kaldırıldı",
"enable_achievement_notifications": "Bir başarı kilidi açıldığında",
"launch_minimized": "Hydra'yı küçültülmüş başlat",
"disable_nsfw_alert": "NSFW uyarısını devre dışı bırak",
"seed_after_download_complete": "İndirme tamamlandıktan sonra paylaş",
"show_hidden_achievement_description": "Gizli başarııklamalarını kilitlenmeden önce göster"
}, },
"notifications": { "notifications": {
"download_complete": "İndirme tamamlandı", "download_complete": "İndirme tamamlandı",
"game_ready_to_install": "{{title}} kuruluma hazır", "game_ready_to_install": "{{title}} kurulmaya hazır",
"repack_list_updated": "Repack listesi güncellendi", "repack_list_updated": "Repack listesi güncellendi",
"repack_count_one": "{{count}} yeni repack eklendi", "repack_count_one": "{{count}} repack eklendi",
"repack_count_other": "{{count}} yeni repack eklendi" "repack_count_other": "{{count}} repack eklendi",
"new_update_available": "Sürüm {{version}} mevcut",
"restart_to_install_update": "Güncellemeyi yüklemek için Hydra'yı yeniden başlatın",
"notification_achievement_unlocked_title": "{{game}} için başarı kilidi açıldı",
"notification_achievement_unlocked_body": "{{achievement}} ve diğer {{count}} başarılar açıldı"
}, },
"system_tray": { "system_tray": {
"open": "Hydra'yı aç", "open": "Hydra'yı Aç",
"quit": ık" "quit": ık"
}, },
"game_card": { "game_card": {
"no_downloads": "İndirme mevcut değil" "no_downloads": "İndirilebilir içerik bulunmuyor"
}, },
"binary_not_found_modal": { "binary_not_found_modal": {
"title": "Programlar yüklü değil", "title": "Programlar Yüklü Değil",
"description": "Sisteminizde Wine veya Lutris çalıştırılabiliri bulunamadı", "description": "Wine veya Lutris çalıştırılabilir dosyaları sisteminizde bulunamadı",
"instructions": "Oyunları düzgün şekilde çalıştırmak için Linux distronuza bunlardan birini nasıl yükleyebileceğinize bakın" "instructions": "Oyunun normal çalışabilmesi için bunlardan herhangi birini Linux dağıtımınıza uygun şekilde nasıl kuracağınızı kontrol edin"
}, },
"modal": { "modal": {
"close": "Kapat tuşu" "close": "Kapat düğmesi"
},
"forms": {
"toggle_password_visibility": "Şifre görünürlüğünü değiştir"
},
"user_profile": {
"amount_hours": "{{amount}} saat",
"amount_minutes": "{{amount}} dakika",
"last_time_played": "Son oynanma {{period}}",
"activity": "Son Etkinlik",
"library": "Kütüphane",
"total_play_time": "Toplam oynama süresi",
"no_recent_activity_title": "Hmmm… burada bir şey yok",
"no_recent_activity_description": "Son zamanlarda hiç oyun oynamamışsınız. Bunu değiştirmenin zamanı geldi!",
"display_name": "Görünen isim",
"saving": "Kaydediliyor",
"save": "Kaydet",
"edit_profile": "Profili Düzenle",
"saved_successfully": "Başarıyla kaydedildi",
"try_again": "Lütfen tekrar deneyin",
"sign_out_modal_title": "Emin misiniz?",
"cancel": "İptal",
"successfully_signed_out": "Başarıyla çıkış yapıldı",
"sign_out": ıkış yap",
"playing_for": "{{amount}} oynanıyor",
"sign_out_modal_text": "Kütüphaneniz mevcut hesabınıza bağlı. Çıkış yaptığınızda kütüphaneniz görünür olmayacak ve herhangi bir ilerleme kaydedilmeyecek. Çıkışa devam etmek istiyor musunuz?",
"add_friends": "Arkadaş Ekle",
"add": "Ekle",
"friend_code": "Arkadaş kodu",
"see_profile": "Profili gör",
"sending": "Gönderiliyor",
"friend_request_sent": "Arkadaşlık isteği gönderildi",
"friends": "Arkadaşlar",
"friends_list": "Arkadaş listesi",
"user_not_found": "Kullanıcı bulunamadı",
"block_user": "Kullanıcıyı engelle",
"add_friend": "Arkadaş ekle",
"request_sent": "İstek gönderildi",
"request_received": "İstek alındı",
"accept_request": "İsteği kabul et",
"ignore_request": "İsteği yok say",
"cancel_request": "İsteği iptal et",
"undo_friendship": "Arkadaşlığı sonlandır",
"request_accepted": "İstek kabul edildi",
"user_blocked_successfully": "Kullanıcı başarıyla engellendi",
"user_block_modal_text": "Bu işlem {{displayName}} adlı kullanıcıyı engelleyecek",
"blocked_users": "Engellenen kullanıcılar",
"unblock": "Engeli kaldır",
"no_friends_added": "Hiç arkadaş eklemediniz",
"pending": "Bekliyor",
"no_pending_invites": "Bekleyen davetiniz yok",
"no_blocked_users": "Engellenmiş kullanıcı yok",
"friend_code_copied": "Arkadaş kodu kopyalandı",
"undo_friendship_modal_text": "Bu işlem {{displayName}} ile arkadaşlığınızı sonlandıracak",
"privacy_hint": "Bunu kimin görebileceğini ayarlamak için <0>Ayarlar</0> bölümüne gidin",
"locked_profile": "Bu profil gizli",
"image_process_failure": "Görüntü işleme başarısız oldu",
"required_field": "Bu alan gerekli",
"displayname_min_length": "Görünen isim en az 3 karakter uzunluğunda olmalıdır",
"displayname_max_length": "Görünen isim en fazla 50 karakter uzunluğunda olabilir",
"report_profile": "Bu profili bildir",
"report_reason": "Bu profili neden bildiriyorsunuz?",
"report_description": "Ek bilgi",
"report_description_placeholder": "Ek bilgi",
"report": "Bildir",
"report_reason_hate": "Nefret söylemi",
"report_reason_sexual_content": "Cinsel içerik",
"report_reason_violence": "Şiddet",
"report_reason_spam": "Spam",
"report_reason_other": "Diğer",
"profile_reported": "Profil bildirildi",
"your_friend_code": "Arkadaş kodunuz:",
"upload_banner": "Afiş yükle",
"uploading_banner": "Afiş yükleniyor…",
"background_image_updated": "Arka plan görüntüsü güncellendi",
"stats": "İstatistikler",
"achievements": "Başarılar",
"games": "Oyunlar",
"top_percentile": "En üst {{percentile}}%",
"ranking_updated_weekly": "Sıralama haftalık olarak güncellenir",
"playing": "{{game}} oynanıyor",
"achievements_unlocked": "Başarılar açıldı",
"earned_points": "Kazanılan puanlar",
"show_achievements_on_profile": "Başarılarınızı profilinizde gösterin",
"show_points_on_profile": "Kazandığınız puanları profilinizde gösterin"
},
"achievement": {
"achievement_unlocked": "Başarııldı",
"user_achievements": "{{displayName}}'in Başarıları",
"your_achievements": "Başarılarınız",
"unlocked_at": "Açılma zamanı: {{date}}",
"subscription_needed": "Bu içeriği görmek için bir Hydra Cloud aboneliği gereklidir",
"new_achievements_unlocked": "{{gameCount}} oyundan {{achievementCount}} yeni başarııldı",
"achievement_progress": "{{unlockedCount}}/{{totalCount}} başarı",
"achievements_unlocked_for_game": "{{gameTitle}} oyunu için {{achievementCount}} yeni başarııldı",
"hidden_achievement_tooltip": "Bu gizli bir başarıdır",
"achievement_earn_points": "Bu başarı ile {{points}} puan kazanın",
"earned_points": "Kazanılan puanlar:",
"available_points": "Mevcut puanlar:",
"how_to_earn_achievements_points": "Başarı puanları nasıl kazanılır?"
},
"hydra_cloud": {
"subscription_tour_title": "Hydra Cloud Aboneliği",
"subscribe_now": "Şimdi abone olun",
"cloud_saving": "Bulut kaydetme",
"cloud_achievements": "Başarılarınızı buluta kaydedin",
"animated_profile_picture": "Animasyonlu profil resimleri",
"premium_support": "Premium Destek",
"show_and_compare_achievements": "Başarılarınızı diğer kullanıcılarla karşılaştırın ve gösterin",
"animated_profile_banner": "Animasyonlu profil afişi",
"hydra_cloud": "Hydra Cloud",
"hydra_cloud_feature_found": "Bir Hydra Cloud özelliği keşfettiniz!",
"learn_more": "Daha Fazla Bilgi Edinin"
} }
} }

View File

@@ -0,0 +1,7 @@
import { registerEvent } from "../register-event";
import { WindowManager } from "@main/services";
const openEditorWindow = async (_event: Electron.IpcMainInvokeEvent) =>
WindowManager.openEditorWindow();
registerEvent("openEditorWindow", openEditorWindow);

View File

@@ -9,6 +9,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;
if (!payload) return null;
return payload.sessionId; return payload.sessionId;
}; };

View File

@@ -1,7 +1,24 @@
import i18next from "i18next";
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import { WindowManager } from "@main/services"; import { HydraApi, WindowManager } from "@main/services";
import { AuthPage } from "@shared";
const openAuthWindow = async (_event: Electron.IpcMainInvokeEvent) => const openAuthWindow = async (
WindowManager.openAuthWindow(); _event: Electron.IpcMainInvokeEvent,
page: AuthPage
) => {
const searchParams = new URLSearchParams({
lng: i18next.language,
});
if ([AuthPage.UpdateEmail, AuthPage.UpdatePassword].includes(page)) {
const { accessToken } = await HydraApi.refreshToken().catch(() => {
return { accessToken: "" };
});
searchParams.set("token", accessToken);
}
WindowManager.openAuthWindow(page, searchParams);
};
registerEvent("openAuthWindow", openAuthWindow); registerEvent("openAuthWindow", openAuthWindow);

View File

@@ -1,7 +1,7 @@
import { HydraApi } from "@main/services"; import { HydraApi } from "@main/services";
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import type { GameArtifact, GameShop } from "@types"; import type { GameArtifact, GameShop } from "@types";
import { SubscriptionRequiredError } from "@shared"; import { SubscriptionRequiredError, UserNotLoggedInError } from "@shared";
const getGameArtifacts = async ( const getGameArtifacts = async (
_event: Electron.IpcMainInvokeEvent, _event: Electron.IpcMainInvokeEvent,
@@ -22,6 +22,10 @@ const getGameArtifacts = async (
return []; return [];
} }
if (err instanceof UserNotLoggedInError) {
return [];
}
throw err; throw err;
}); });
}; };

View File

@@ -0,0 +1,15 @@
import fs from "node:fs";
import { registerEvent } from "../register-event";
const checkFolderWritePermission = async (
_event: Electron.IpcMainInvokeEvent,
path: string
) =>
new Promise((resolve) => {
fs.access(path, fs.constants.W_OK, (err) => {
resolve(!err);
});
});
registerEvent("checkFolderWritePermission", checkFolderWritePermission);

View File

@@ -1,10 +1,10 @@
import checkDiskSpace from "check-disk-space"; import disk from "diskusage";
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
const getDiskFreeSpace = async ( const getDiskFreeSpace = async (
_event: Electron.IpcMainInvokeEvent, _event: Electron.IpcMainInvokeEvent,
path: string path: string
) => checkDiskSpace(path); ) => disk.check(path);
registerEvent("getDiskFreeSpace", getDiskFreeSpace); registerEvent("getDiskFreeSpace", getDiskFreeSpace);

View File

@@ -1,9 +0,0 @@
export const parseLaunchOptions = (params: string | null): string[] => {
if (params == null || params == "") {
return [];
}
const paramsSplit = params.split(" ");
return paramsSplit;
};

View File

@@ -11,6 +11,7 @@ import "./catalogue/get-trending-games";
import "./catalogue/get-publishers"; import "./catalogue/get-publishers";
import "./catalogue/get-developers"; import "./catalogue/get-developers";
import "./hardware/get-disk-free-space"; import "./hardware/get-disk-free-space";
import "./hardware/check-folder-write-permission";
import "./library/add-game-to-library"; import "./library/add-game-to-library";
import "./library/create-game-shortcut"; import "./library/create-game-shortcut";
import "./library/close-game"; import "./library/close-game";
@@ -27,9 +28,12 @@ import "./library/verify-executable-path";
import "./library/remove-game"; import "./library/remove-game";
import "./library/remove-game-from-library"; import "./library/remove-game-from-library";
import "./library/select-game-wine-prefix"; import "./library/select-game-wine-prefix";
import "./library/reset-game-achievements";
import "./misc/open-checkout"; import "./misc/open-checkout";
import "./misc/open-external"; import "./misc/open-external";
import "./misc/show-open-dialog"; import "./misc/show-open-dialog";
import "./misc/get-features";
import "./misc/show-item-in-folder";
import "./torrenting/cancel-game-download"; import "./torrenting/cancel-game-download";
import "./torrenting/pause-game-download"; import "./torrenting/pause-game-download";
import "./torrenting/resume-game-download"; import "./torrenting/resume-game-download";
@@ -45,6 +49,7 @@ import "./user-preferences/authenticate-real-debrid";
import "./download-sources/put-download-source"; import "./download-sources/put-download-source";
import "./auth/sign-out"; import "./auth/sign-out";
import "./auth/open-auth-window"; import "./auth/open-auth-window";
import "./aparence/open-editor-window";
import "./auth/get-session-hash"; import "./auth/get-session-hash";
import "./user/get-user"; import "./user/get-user";
import "./user/get-blocked-users"; import "./user/get-blocked-users";
@@ -71,7 +76,6 @@ import "./cloud-save/delete-game-artifact";
import "./cloud-save/select-game-backup-path"; import "./cloud-save/select-game-backup-path";
import "./notifications/publish-new-repacks-notification"; import "./notifications/publish-new-repacks-notification";
import { isPortableVersion } from "@main/helpers"; import { isPortableVersion } from "@main/helpers";
import "./misc/show-item-in-folder";
ipcMain.handle("ping", () => "pong"); ipcMain.handle("ping", () => "pong");
ipcMain.handle("getVersion", () => appVersion); ipcMain.handle("getVersion", () => appVersion);

View File

@@ -2,9 +2,7 @@ import { gameRepository } from "@main/repository";
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import { shell } from "electron"; import { shell } from "electron";
import { spawn } from "child_process";
import { parseExecutablePath } from "../helpers/parse-executable-path"; import { parseExecutablePath } from "../helpers/parse-executable-path";
import { parseLaunchOptions } from "../helpers/parse-launch-options";
const openGame = async ( const openGame = async (
_event: Electron.IpcMainInvokeEvent, _event: Electron.IpcMainInvokeEvent,
@@ -12,21 +10,15 @@ const openGame = async (
executablePath: string, executablePath: string,
launchOptions: string | null launchOptions: string | null
) => { ) => {
// TODO: revisit this for launchOptions
const parsedPath = parseExecutablePath(executablePath); const parsedPath = parseExecutablePath(executablePath);
const parsedParams = parseLaunchOptions(launchOptions);
await gameRepository.update( await gameRepository.update(
{ id: gameId }, { id: gameId },
{ executablePath: parsedPath, launchOptions } { executablePath: parsedPath, launchOptions }
); );
if (process.platform === "linux" || process.platform === "darwin") { shell.openPath(parsedPath);
shell.openPath(parsedPath);
}
if (process.platform === "win32") {
spawn(parsedPath, parsedParams, { shell: false, detached: true });
}
}; };
registerEvent("openGame", openGame); registerEvent("openGame", openGame);

View File

@@ -0,0 +1,56 @@
import { gameAchievementRepository, gameRepository } from "@main/repository";
import { registerEvent } from "../register-event";
import { findAchievementFiles } from "@main/services/achievements/find-achivement-files";
import fs from "fs";
import { achievementsLogger, HydraApi, WindowManager } from "@main/services";
import { getUnlockedAchievements } from "../user/get-unlocked-achievements";
const resetGameAchievements = async (
_event: Electron.IpcMainInvokeEvent,
gameId: number
) => {
try {
const game = await gameRepository.findOne({ where: { id: gameId } });
if (!game) return;
const achievementFiles = findAchievementFiles(game);
if (achievementFiles.length) {
for (const achievementFile of achievementFiles) {
achievementsLogger.log(`deleting ${achievementFile.filePath}`);
await fs.promises.rm(achievementFile.filePath);
}
}
await gameAchievementRepository.update(
{ objectId: game.objectID },
{
unlockedAchievements: null,
}
);
await HydraApi.delete(`/profile/games/achievements/${game.remoteId}`).then(
() =>
achievementsLogger.log(
`Deleted achievements from ${game.remoteId} - ${game.objectID} - ${game.title}`
)
);
const gameAchievements = await getUnlockedAchievements(
game.objectID,
game.shop,
true
);
WindowManager.mainWindow?.webContents.send(
`on-update-achievements-${game.objectID}-${game.shop}`,
gameAchievements
);
} catch (error) {
achievementsLogger.error(error);
throw error;
}
};
registerEvent("resetGameAchievements", resetGameAchievements);

View File

@@ -0,0 +1,8 @@
import { registerEvent } from "../register-event";
import { HydraApi } from "@main/services";
const getFeatures = async (_event: Electron.IpcMainInvokeEvent) => {
return HydraApi.get<string[]>("/features", null, { needsAuth: false });
};
registerEvent("getFeatures", getFeatures);

View File

@@ -1,16 +1,10 @@
import { shell } from "electron"; import { shell } from "electron";
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import { import { userAuthRepository } from "@main/repository";
userAuthRepository,
userPreferencesRepository,
} from "@main/repository";
import { HydraApi } from "@main/services"; import { HydraApi } from "@main/services";
const openCheckout = async (_event: Electron.IpcMainInvokeEvent) => { const openCheckout = async (_event: Electron.IpcMainInvokeEvent) => {
const [userAuth, userPreferences] = await Promise.all([ const userAuth = await userAuthRepository.findOne({ where: { id: 1 } });
userAuthRepository.findOne({ where: { id: 1 } }),
userPreferencesRepository.findOne({ where: { id: 1 } }),
]);
if (!userAuth) { if (!userAuth) {
return; return;
@@ -22,7 +16,6 @@ const openCheckout = async (_event: Electron.IpcMainInvokeEvent) => {
const params = new URLSearchParams({ const params = new URLSearchParams({
token: paymentToken, token: paymentToken,
lng: userPreferences?.language || "en",
}); });
shell.openExternal( shell.openExternal(

View File

@@ -8,7 +8,7 @@ import {
} from "@main/repository"; } from "@main/repository";
import { publishDownloadCompleteNotification } from "../notifications"; import { publishDownloadCompleteNotification } from "../notifications";
import type { DownloadProgress } from "@types"; import type { DownloadProgress } from "@types";
import { GofileApi, QiwiApi } from "../hosters"; import { GofileApi, QiwiApi, DatanodesApi } from "../hosters";
import { PythonRPC } from "../python-rpc"; import { PythonRPC } from "../python-rpc";
import { import {
LibtorrentPayload, LibtorrentPayload,
@@ -277,6 +277,16 @@ export class DownloadManager {
save_path: game.downloadPath!, save_path: game.downloadPath!,
}; };
} }
case Downloader.Datanodes: {
const downloadUrl = await DatanodesApi.getDownloadUrl(game.uri!);
return {
action: "start",
game_id: game.id,
url: downloadUrl,
save_path: game.downloadPath!,
};
}
case Downloader.Torrent: case Downloader.Torrent:
return { return {
action: "start", action: "start",

View File

@@ -0,0 +1,47 @@
import axios, { AxiosResponse } from "axios";
export class DatanodesApi {
private static readonly session = axios.create({});
public static async getDownloadUrl(downloadUrl: string): Promise<string> {
const parsedUrl = new URL(downloadUrl);
const pathSegments = parsedUrl.pathname.split("/");
const fileCode = decodeURIComponent(pathSegments[1]);
const fileName = decodeURIComponent(pathSegments[pathSegments.length - 1]);
const payload = new URLSearchParams({
op: "download2",
id: fileCode,
rand: "",
referer: "https://datanodes.to/download",
method_free: "Free Download >>",
method_premium: "",
adblock_detected: "",
});
const response: AxiosResponse = await this.session.post(
"https://datanodes.to/download",
payload,
{
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Cookie: `lang=english; file_name=${fileName}; file_code=${fileCode};`,
Host: "datanodes.to",
Origin: "https://datanodes.to",
Referer: "https://datanodes.to/download",
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
},
maxRedirects: 0,
validateStatus: (status: number) => status === 302 || status < 400,
}
);
if (response.status === 302) {
return response.headers["location"];
}
return "";
}
}

View File

@@ -1,2 +1,3 @@
export * from "./gofile"; export * from "./gofile";
export * from "./qiwi"; export * from "./qiwi";
export * from "./datanodes";

View File

@@ -215,38 +215,42 @@ export class HydraApi {
} }
} }
public static async refreshToken() {
const { accessToken, expiresIn } = await this.instance
.post<{ accessToken: string; expiresIn: number }>(`/auth/refresh`, {
refreshToken: this.userAuth.refreshToken,
})
.then((response) => response.data);
const tokenExpirationTimestamp =
Date.now() +
this.secondsToMilliseconds(expiresIn) -
this.EXPIRATION_OFFSET_IN_MS;
this.userAuth.authToken = accessToken;
this.userAuth.expirationTimestamp = tokenExpirationTimestamp;
logger.log(
"Token refreshed. New expiration:",
this.userAuth.expirationTimestamp
);
userAuthRepository.upsert(
{
id: 1,
accessToken,
tokenExpirationTimestamp,
},
["id"]
);
return { accessToken, expiresIn };
}
private static async revalidateAccessTokenIfExpired() { private static async revalidateAccessTokenIfExpired() {
const now = new Date(); if (this.userAuth.expirationTimestamp < Date.now()) {
if (this.userAuth.expirationTimestamp < now.getTime()) {
try { try {
const response = await this.instance.post(`/auth/refresh`, { await this.refreshToken();
refreshToken: this.userAuth.refreshToken,
});
const { accessToken, expiresIn } = response.data;
const tokenExpirationTimestamp =
now.getTime() +
this.secondsToMilliseconds(expiresIn) -
this.EXPIRATION_OFFSET_IN_MS;
this.userAuth.authToken = accessToken;
this.userAuth.expirationTimestamp = tokenExpirationTimestamp;
logger.log(
"Token refreshed. New expiration:",
this.userAuth.expirationTimestamp
);
userAuthRepository.upsert(
{
id: 1,
accessToken,
tokenExpirationTimestamp,
},
["id"]
);
} catch (err) { } catch (err) {
this.handleUnauthorizedError(err); this.handleUnauthorizedError(err);
} }
@@ -261,7 +265,7 @@ export class HydraApi {
}; };
} }
private static handleUnauthorizedError = (err) => { private static readonly handleUnauthorizedError = (err) => {
if (err instanceof AxiosError && err.response?.status === 401) { if (err instanceof AxiosError && err.response?.status === 401) {
logger.error( logger.error(
"401 - Current credentials:", "401 - Current credentials:",

View File

@@ -9,7 +9,7 @@ import {
shell, shell,
} from "electron"; } from "electron";
import { is } from "@electron-toolkit/utils"; import { is } from "@electron-toolkit/utils";
import i18next, { t } from "i18next"; import { t } from "i18next";
import path from "node:path"; import path from "node:path";
import icon from "@resources/icon.png?asset"; import icon from "@resources/icon.png?asset";
import trayIcon from "@resources/tray-icon.png?asset"; import trayIcon from "@resources/tray-icon.png?asset";
@@ -17,6 +17,7 @@ import { gameRepository, userPreferencesRepository } from "@main/repository";
import { IsNull, Not } from "typeorm"; import { IsNull, Not } from "typeorm";
import { HydraApi } from "./hydra-api"; import { HydraApi } from "./hydra-api";
import UserAgent from "user-agents"; import UserAgent from "user-agents";
import { AuthPage } from "@shared";
export class WindowManager { export class WindowManager {
public static mainWindow: Electron.BrowserWindow | null = null; public static mainWindow: Electron.BrowserWindow | null = null;
@@ -64,7 +65,10 @@ export class WindowManager {
this.mainWindow.webContents.session.webRequest.onBeforeSendHeaders( this.mainWindow.webContents.session.webRequest.onBeforeSendHeaders(
(details, callback) => { (details, callback) => {
if (details.webContentsId !== this.mainWindow?.webContents.id) { if (
details.webContentsId !== this.mainWindow?.webContents.id ||
details.url.includes("chatwoot")
) {
return callback(details); return callback(details);
} }
@@ -81,15 +85,11 @@ export class WindowManager {
this.mainWindow.webContents.session.webRequest.onHeadersReceived( this.mainWindow.webContents.session.webRequest.onHeadersReceived(
(details, callback) => { (details, callback) => {
if (details.webContentsId !== this.mainWindow?.webContents.id) { if (
return callback(details); details.webContentsId !== this.mainWindow?.webContents.id ||
} details.url.includes("featurebase") ||
details.url.includes("chatwoot")
if (details.url.includes("featurebase")) { ) {
return callback(details);
}
if (details.url.includes("chatwoot")) {
return callback(details); return callback(details);
} }
@@ -143,7 +143,7 @@ export class WindowManager {
}); });
} }
public static openAuthWindow() { public static openAuthWindow(page: AuthPage, searchParams: URLSearchParams) {
if (this.mainWindow) { if (this.mainWindow) {
const authWindow = new BrowserWindow({ const authWindow = new BrowserWindow({
width: 600, width: 600,
@@ -165,12 +165,8 @@ export class WindowManager {
if (!app.isPackaged) authWindow.webContents.openDevTools(); if (!app.isPackaged) authWindow.webContents.openDevTools();
const searchParams = new URLSearchParams({
lng: i18next.language,
});
authWindow.loadURL( authWindow.loadURL(
`${import.meta.env.MAIN_VITE_AUTH_URL}/?${searchParams.toString()}` `${import.meta.env.MAIN_VITE_AUTH_URL}${page}?${searchParams.toString()}`
); );
authWindow.once("ready-to-show", () => { authWindow.once("ready-to-show", () => {
@@ -182,11 +178,64 @@ export class WindowManager {
authWindow.close(); authWindow.close();
HydraApi.handleExternalAuth(url); HydraApi.handleExternalAuth(url);
return;
}
if (url.startsWith("hydralauncher://update-account")) {
authWindow.close();
WindowManager.mainWindow?.webContents.send("on-account-updated");
} }
}); });
} }
} }
public static openEditorWindow() {
if (this.mainWindow) {
const editorWindow = new BrowserWindow({
width: 600,
height: 720,
minWidth: 600,
minHeight: 720,
backgroundColor: "#1c1c1c",
titleBarStyle: process.platform === "linux" ? "default" : "hidden",
...(process.platform === "linux" ? { icon } : {}),
trafficLightPosition: { x: 16, y: 16 },
titleBarOverlay: {
symbolColor: "#DADBE1",
color: "#151515",
height: 34,
},
parent: this.mainWindow,
modal: true,
show: false,
maximizable: true,
resizable: true,
minimizable: true,
webPreferences: {
sandbox: false,
preload: path.join(__dirname, "../preload/index.mjs"),
},
});
editorWindow.removeMenu();
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
editorWindow.loadURL(`${process.env["ELECTRON_RENDERER_URL"]}#/editor`);
} else {
editorWindow.loadFile(path.join(__dirname, "../renderer/index.html"), {
hash: "editor",
});
}
editorWindow.once("ready-to-show", () => {
editorWindow.show();
});
if (!app.isPackaged) editorWindow.webContents.openDevTools();
}
}
public static redirect(hash: string) { public static redirect(hash: string) {
if (!this.mainWindow) this.createMainWindow(); if (!this.mainWindow) this.createMainWindow();
this.loadMainWindowURL(hash); this.loadMainWindowURL(hash);

View File

@@ -15,7 +15,7 @@ import type {
SeedingStatus, SeedingStatus,
GameAchievement, GameAchievement,
} from "@types"; } from "@types";
import type { CatalogueCategory } from "@shared"; import type { AuthPage, CatalogueCategory } from "@shared";
import type { AxiosProgressEvent } from "axios"; import type { AxiosProgressEvent } from "axios";
contextBridge.exposeInMainWorld("electron", { contextBridge.exposeInMainWorld("electron", {
@@ -130,6 +130,8 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.invoke("deleteGameFolder", gameId), ipcRenderer.invoke("deleteGameFolder", gameId),
getGameByObjectId: (objectId: string) => getGameByObjectId: (objectId: string) =>
ipcRenderer.invoke("getGameByObjectId", objectId), ipcRenderer.invoke("getGameByObjectId", objectId),
resetGameAchievements: (gameId: number) =>
ipcRenderer.invoke("resetGameAchievements", gameId),
onGamesRunning: ( onGamesRunning: (
cb: ( cb: (
gamesRunning: Pick<GameRunning, "id" | "sessionDurationInMillis">[] gamesRunning: Pick<GameRunning, "id" | "sessionDurationInMillis">[]
@@ -150,6 +152,8 @@ contextBridge.exposeInMainWorld("electron", {
/* Hardware */ /* Hardware */
getDiskFreeSpace: (path: string) => getDiskFreeSpace: (path: string) =>
ipcRenderer.invoke("getDiskFreeSpace", path), ipcRenderer.invoke("getDiskFreeSpace", path),
checkFolderWritePermission: (path: string) =>
ipcRenderer.invoke("checkFolderWritePermission", path),
/* Cloud save */ /* Cloud save */
uploadSaveGame: ( uploadSaveGame: (
@@ -226,6 +230,7 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.invoke("showOpenDialog", options), ipcRenderer.invoke("showOpenDialog", options),
showItemInFolder: (path: string) => showItemInFolder: (path: string) =>
ipcRenderer.invoke("showItemInFolder", path), ipcRenderer.invoke("showItemInFolder", path),
getFeatures: () => ipcRenderer.invoke("getFeatures"),
platform: process.platform, platform: process.platform,
/* Auto update */ /* Auto update */
@@ -286,13 +291,19 @@ contextBridge.exposeInMainWorld("electron", {
/* Auth */ /* Auth */
signOut: () => ipcRenderer.invoke("signOut"), signOut: () => ipcRenderer.invoke("signOut"),
openAuthWindow: () => ipcRenderer.invoke("openAuthWindow"), openAuthWindow: (page: AuthPage) =>
ipcRenderer.invoke("openAuthWindow", page),
getSessionHash: () => ipcRenderer.invoke("getSessionHash"), getSessionHash: () => ipcRenderer.invoke("getSessionHash"),
onSignIn: (cb: () => void) => { onSignIn: (cb: () => void) => {
const listener = (_event: Electron.IpcRendererEvent) => cb(); const listener = (_event: Electron.IpcRendererEvent) => cb();
ipcRenderer.on("on-signin", listener); ipcRenderer.on("on-signin", listener);
return () => ipcRenderer.removeListener("on-signin", listener); return () => ipcRenderer.removeListener("on-signin", listener);
}, },
onAccountUpdated: (cb: () => void) => {
const listener = (_event: Electron.IpcRendererEvent) => cb();
ipcRenderer.on("on-account-updated", listener);
return () => ipcRenderer.removeListener("on-account-updated", listener);
},
onSignOut: (cb: () => void) => { onSignOut: (cb: () => void) => {
const listener = (_event: Electron.IpcRendererEvent) => cb(); const listener = (_event: Electron.IpcRendererEvent) => cb();
ipcRenderer.on("on-signout", listener); ipcRenderer.on("on-signout", listener);
@@ -302,4 +313,7 @@ contextBridge.exposeInMainWorld("electron", {
/* Notifications */ /* Notifications */
publishNewRepacksNotification: (newRepacksCount: number) => publishNewRepacksNotification: (newRepacksCount: number) =>
ipcRenderer.invoke("publishNewRepacksNotification", newRepacksCount), ipcRenderer.invoke("publishNewRepacksNotification", newRepacksCount),
/* Editor */
openEditorWindow: () => ipcRenderer.invoke("openEditorWindow"),
}); });

View File

@@ -0,0 +1,21 @@
$spacing-unit: 8px;
$color-background: #1c1c1c;
$color-dark-background: #151515;
$color-muted: #c0c1c7;
$color-body: #8e919b;
$color-border: rgba(255, 255, 255, 0.15);
$color-success: #1c9749;
$color-danger: #e11d48;
$color-warning: #ffc107;
$opacity-disabled: 0.5;
$opacity-active: 0.7;
$size-body: 14px;
$size-small: 12px;
$z-index-toast: 5;
$z-index-bottom-panel: 3;
$z-index-title-bar: 4;
$z-index-backdrop: 4;

View File

@@ -1,134 +0,0 @@
import {
ComplexStyleRule,
createContainer,
globalStyle,
style,
} from "@vanilla-extract/css";
import { SPACING_UNIT, vars } from "./theme.css";
export const appContainer = createContainer();
globalStyle("*", {
boxSizing: "border-box",
});
globalStyle("::-webkit-scrollbar", {
width: "9px",
backgroundColor: vars.color.darkBackground,
});
globalStyle("::-webkit-scrollbar-track", {
backgroundColor: "rgba(255, 255, 255, 0.03)",
});
globalStyle("::-webkit-scrollbar-thumb", {
backgroundColor: "rgba(255, 255, 255, 0.08)",
borderRadius: "24px",
});
globalStyle("::-webkit-scrollbar-thumb:hover", {
backgroundColor: "rgba(255, 255, 255, 0.16)",
});
globalStyle("html, body, #root, main", {
height: "100%",
});
globalStyle("body", {
overflow: "hidden",
userSelect: "none",
fontFamily: "Noto Sans, sans-serif",
fontSize: vars.size.body,
color: vars.color.body,
margin: "0",
});
globalStyle("button", {
padding: "0",
backgroundColor: "transparent",
border: "none",
fontFamily: "inherit",
});
globalStyle("h1, h2, h3, h4, h5, h6, p", {
margin: 0,
});
globalStyle("p", {
lineHeight: "20px",
});
globalStyle("#root, main", {
display: "flex",
});
globalStyle("#root", {
flexDirection: "column",
});
globalStyle("main", {
overflow: "hidden",
});
globalStyle(
"input::-webkit-outer-spin-button, input::-webkit-inner-spin-button",
{
WebkitAppearance: "none",
margin: "0",
}
);
globalStyle("label", {
fontSize: vars.size.body,
});
globalStyle("input[type=number]", {
MozAppearance: "textfield",
});
globalStyle("img", {
WebkitUserDrag: "none",
} as Record<string, string>);
globalStyle("progress[value]", {
WebkitAppearance: "none",
});
export const container = style({
width: "100%",
height: "100%",
overflow: "hidden",
display: "flex",
flexDirection: "column",
containerName: appContainer,
containerType: "inline-size",
});
export const content = style({
overflowY: "auto",
alignItems: "center",
display: "flex",
flexDirection: "column",
position: "relative",
height: "100%",
background: `linear-gradient(0deg, ${vars.color.darkBackground} 50%, ${vars.color.background} 100%)`,
});
export const titleBar = style({
display: "flex",
width: "100%",
height: "35px",
minHeight: "35px",
backgroundColor: vars.color.darkBackground,
alignItems: "center",
padding: `0 ${SPACING_UNIT * 2}px`,
WebkitAppRegion: "drag",
zIndex: "4",
borderBottom: `1px solid ${vars.color.border}`,
} as ComplexStyleRule);
export const cloudText = style({
background: "linear-gradient(270deg, #16B195 50%, #3E62C0 100%)",
backgroundClip: "text",
color: "transparent",
});

136
src/renderer/src/app.scss Normal file
View File

@@ -0,0 +1,136 @@
@use "./scss/variables" as vars;
* {
box-sizing: border-box;
}
::-webkit-scrollbar {
width: 9px;
background-color: vars.$dark-background-color;
}
::-webkit-scrollbar-track {
background-color: rgba(255, 255, 255, 0.03);
}
::-webkit-scrollbar-thumb {
background-color: rgba(255, 255, 255, 0.08);
border-radius: 24px;
}
::-webkit-scrollbar-thumb:hover {
background-color: rgba(255, 255, 255, 0.16);
}
html,
body,
#root,
main {
height: 100%;
}
body {
overflow: hidden;
user-select: none;
font-family: "Noto Sans", sans-serif;
font-size: vars.$body-font-size;
color: vars.$body-color;
margin: 0;
}
button {
padding: 0;
background-color: transparent;
border: none;
font-family: inherit;
}
h1,
h2,
h3,
h4,
h5,
h6,
p {
margin: 0;
}
p {
line-height: 20px;
}
#root,
main {
display: flex;
}
#root {
flex-direction: column;
}
main {
overflow: hidden;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
label {
font-size: vars.$body-font-size;
}
input[type="number"] {
-moz-appearance: textfield;
}
img {
-webkit-user-drag: none;
}
progress[value] {
-webkit-appearance: none;
}
.app-container {
width: 100%;
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
}
.app-container__content {
overflow-y: auto;
align-items: center;
display: flex;
flex-direction: column;
position: relative;
height: 100%;
background: linear-gradient(
0deg,
vars.$dark-background-color 50%,
vars.$background-color 100%
);
}
.app-container__title-bar {
display: flex;
width: 100%;
height: 35px;
min-height: 35px;
background-color: vars.$dark-background-color;
align-items: center;
padding: 0 vars.$spacing-unit * 2;
-webkit-app-region: drag;
z-index: vars.$title-bar-z-index;
border-bottom: 1px solid vars.$border-color;
}
.app-container__cloud-text {
background: linear-gradient(270deg, #16b195 50%, #3e62c0 100%);
background-clip: text;
color: transparent;
}

View File

@@ -12,8 +12,6 @@ import {
useUserDetails, useUserDetails,
} from "@renderer/hooks"; } from "@renderer/hooks";
import * as styles from "./app.css";
import { Outlet, useLocation, useNavigate } from "react-router-dom"; import { Outlet, useLocation, useNavigate } from "react-router-dom";
import { import {
setUserPreferences, setUserPreferences,
@@ -30,6 +28,8 @@ import { downloadSourcesTable } from "./dexie";
import { useSubscription } from "./hooks/use-subscription"; import { useSubscription } from "./hooks/use-subscription";
import { HydraCloudModal } from "./pages/shared-modals/hydra-cloud/hydra-cloud-modal"; import { HydraCloudModal } from "./pages/shared-modals/hydra-cloud/hydra-cloud-modal";
import "./app.scss";
export interface AppProps { export interface AppProps {
children: React.ReactNode; children: React.ReactNode;
} }
@@ -240,11 +240,11 @@ export function App() {
return ( return (
<> <>
{window.electron.platform === "win32" && ( {window.electron.platform === "win32" && (
<div className={styles.titleBar}> <div className="title-bar">
<h4> <h4>
Hydra Hydra
{hasActiveSubscription && ( {hasActiveSubscription && (
<span className={styles.cloudText}> Cloud</span> <span className="title-bar__cloud-text"> Cloud</span>
)} )}
</h4> </h4>
</div> </div>
@@ -275,10 +275,10 @@ export function App() {
<main> <main>
<Sidebar /> <Sidebar />
<article className={styles.container}> <article className="app-container">
<Header /> <Header />
<section ref={contentRef} className={styles.content}> <section ref={contentRef} className="app-container__content">
<Outlet /> <Outlet />
</section> </section>
</article> </article>

View File

@@ -1,14 +1,14 @@
@use "../../scss/globals.scss"; @use "../../scss/variables" as vars;
.profile-avatar { .profile-avatar {
border-radius: 4px; border-radius: 4px;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
background-color: globals.$background-color; background-color: vars.$background-color;
border: solid 1px globals.$border-color; border: solid 1px vars.$border-color;
cursor: pointer; cursor: pointer;
color: globals.$muted-color; color: vars.$muted-color;
position: relative; position: relative;
&__image { &__image {

View File

@@ -1,54 +0,0 @@
import { keyframes } from "@vanilla-extract/css";
import { recipe } from "@vanilla-extract/recipes";
import { SPACING_UNIT, vars } from "../../theme.css";
export const backdropFadeIn = keyframes({
"0%": { backdropFilter: "blur(0px)", backgroundColor: "rgba(0, 0, 0, 0.5)" },
"100%": {
backdropFilter: "blur(2px)",
backgroundColor: "rgba(0, 0, 0, 0.7)",
},
});
export const backdropFadeOut = keyframes({
"0%": { backdropFilter: "blur(2px)", backgroundColor: "rgba(0, 0, 0, 0.7)" },
"100%": {
backdropFilter: "blur(0px)",
backgroundColor: "rgba(0, 0, 0, 0)",
},
});
export const backdrop = recipe({
base: {
animationName: backdropFadeIn,
animationDuration: "0.4s",
backgroundColor: "rgba(0, 0, 0, 0.7)",
position: "absolute",
width: "100%",
height: "100%",
display: "flex",
justifyContent: "center",
alignItems: "center",
zIndex: vars.zIndex.backdrop,
top: "0",
padding: `${SPACING_UNIT * 3}px`,
backdropFilter: "blur(2px)",
transition: "all ease 0.2s",
},
variants: {
closing: {
true: {
animationName: backdropFadeOut,
backdropFilter: "blur(0px)",
backgroundColor: "rgba(0, 0, 0, 0)",
},
},
windows: {
true: {
// SPACING_UNIT * 3 + title bar spacing
paddingTop: `${SPACING_UNIT * 3 + 35}px`,
},
},
},
});

View File

@@ -0,0 +1,50 @@
@use "../../scss/variables" as vars;
.backdrop {
animation-name: backdrop-fade-in;
animation-duration: 0.4s;
background-color: rgba(0, 0, 0, 0.7);
position: absolute;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
z-index: vars.$backdrop-z-index;
top: 0;
padding: calc(vars.$spacing-unit * 3);
backdrop-filter: blur(2px);
transition: all ease 0.2s;
&--closing {
animation-name: backdrop-fade-out;
backdrop-filter: blur(0px);
background-color: rgba(0, 0, 0, 0);
}
&--windows {
padding-top: calc(#{vars.$spacing-unit * 3} + 35);
}
}
@keyframes backdrop-fade-in {
0% {
backdrop-filter: blur(0px);
background-color: rgba(0, 0, 0, 0.5);
}
100% {
backdrop-filter: blur(2px);
background-color: rgba(0, 0, 0, 0.7);
}
}
@keyframes backdrop-fade-out {
0% {
backdrop-filter: blur(2px);
background-color: rgba(0, 0, 0, 0.7);
}
100% {
backdrop-filter: blur(0px);
background-color: rgba(0, 0, 0, 0);
}
}

View File

@@ -1,4 +1,5 @@
import * as styles from "./backdrop.css"; import "./backdrop.scss";
import cn from "classnames";
export interface BackdropProps { export interface BackdropProps {
isClosing?: boolean; isClosing?: boolean;
@@ -8,9 +9,9 @@ export interface BackdropProps {
export function Backdrop({ isClosing = false, children }: BackdropProps) { export function Backdrop({ isClosing = false, children }: BackdropProps) {
return ( return (
<div <div
className={styles.backdrop({ className={cn("backdrop", {
closing: isClosing, "backdrop--closing": isClosing,
windows: window.electron.platform === "win32", "backdrop--windows": window.electron.platform === "win32",
})} })}
> >
{children} {children}

View File

@@ -1,10 +1,10 @@
@use "../../scss/globals.scss"; @use "../../scss/variables" as vars;
.badge { .badge {
color: globals.$muted-color; color: vars.$muted-color;
font-size: 10px; font-size: 10px;
padding: calc(globals.$spacing-unit / 2) globals.$spacing-unit; padding: calc(vars.$spacing-unit / 2) vars.$spacing-unit;
border: solid 1px globals.$muted-color; border: solid 1px vars.$muted-color;
border-radius: 4px; border-radius: 4px;
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -1,23 +1,23 @@
@use "../../scss/globals.scss"; @use "../../scss/variables" as vars;
.bottom-panel { .bottom-panel {
width: 100%; width: 100%;
border-top: solid 1px globals.$border-color; border-top: solid 1px vars.$border-color;
background-color: globals.$background-color; background-color: vars.$background-color;
padding: calc(globals.$spacing-unit / 2) calc(globals.$spacing-unit * 2); padding: calc(vars.$spacing-unit / 2) calc(vars.$spacing-unit * 2);
display: flex; display: flex;
align-items: center; align-items: center;
transition: all ease 0.2s; transition: all ease 0.2s;
justify-content: space-between; justify-content: space-between;
position: relative; position: relative;
z-index: globals.$bottom-panel-z-index; z-index: vars.$bottom-panel-z-index;
&__downloads-button { &__downloads-button {
color: globals.$body-color; color: vars.$body-color;
border-bottom: solid 1px transparent; border-bottom: solid 1px transparent;
&:hover { &:hover {
border-bottom: solid 1px globals.$body-color; border-bottom: solid 1px vars.$body-color;
cursor: pointer; cursor: pointer;
} }
} }

View File

@@ -1,8 +1,8 @@
@use "../../scss/globals.scss"; @use "../../scss/variables" as vars;
.button { .button {
padding: globals.$spacing-unit globals.$spacing-unit * 2; padding: vars.$spacing-unit vars.$spacing-unit * 2;
background-color: globals.$muted-color; background-color: vars.$muted-color;
border-radius: 8px; border-radius: 8px;
border: solid 1px transparent; border: solid 1px transparent;
transition: all ease 0.2s; transition: all ease 0.2s;
@@ -11,14 +11,14 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: globals.$spacing-unit; gap: vars.$spacing-unit;
&:active { &:active {
opacity: globals.$active-opacity; opacity: vars.$active-opacity;
} }
&:disabled { &:disabled {
opacity: globals.$disabled-opacity; opacity: vars.$disabled-opacity;
cursor: not-allowed; cursor: not-allowed;
} }
@@ -28,14 +28,14 @@
} }
&:disabled { &:disabled {
background-color: globals.$muted-color; background-color: vars.$muted-color;
} }
} }
&--outline { &--outline {
background-color: transparent; background-color: transparent;
border: solid 1px globals.$border-color; border: solid 1px vars.$border-color;
color: globals.$muted-color; color: vars.$muted-color;
&:hover { &:hover {
background-color: rgba(255, 255, 255, 0.1); background-color: rgba(255, 255, 255, 0.1);
@@ -47,14 +47,14 @@
} }
&--dark { &--dark {
background-color: globals.$dark-background-color; background-color: vars.$dark-background-color;
color: globals.$muted-color; color: vars.$muted-color;
} }
&--danger { &--danger {
border-color: transparent; border-color: transparent;
background-color: globals.$danger-color; background-color: vars.$danger-color;
color: globals.$muted-color; color: vars.$muted-color;
&:hover { &:hover {
background-color: #b3203f; background-color: #b3203f;

View File

@@ -1,57 +0,0 @@
import { style } from "@vanilla-extract/css";
import { SPACING_UNIT, vars } from "../../theme.css";
import { recipe } from "@vanilla-extract/recipes";
export const checkboxField = style({
display: "flex",
flexDirection: "row",
alignItems: "center",
gap: `${SPACING_UNIT}px`,
cursor: "pointer",
});
export const checkbox = recipe({
base: {
width: "20px",
height: "20px",
borderRadius: "4px",
backgroundColor: vars.color.darkBackground,
display: "flex",
justifyContent: "center",
alignItems: "center",
position: "relative",
transition: "all ease 0.2s",
border: `solid 1px ${vars.color.border}`,
minWidth: "20px",
minHeight: "20px",
color: vars.color.darkBackground,
":hover": {
borderColor: "rgba(255, 255, 255, 0.5)",
},
},
variants: {
checked: {
true: {
backgroundColor: vars.color.muted,
},
},
},
});
export const checkboxInput = style({
width: "100%",
height: "100%",
position: "absolute",
margin: "0",
padding: "0",
opacity: "0",
cursor: "pointer",
});
export const checkboxLabel = style({
cursor: "pointer",
textOverflow: "ellipsis",
overflow: "hidden",
whiteSpace: "nowrap",
});

View File

@@ -0,0 +1,39 @@
@use "../../scss/variables" as vars;
.checkbox-field {
display: flex;
flex-direction: row;
align-items: center;
gap: vars.$spacing-unit;
cursor: pointer;
&__checkbox {
width: 20px;
height: 20px;
border-radius: 4px;
background-color: vars.$dark-background-color;
display: flex;
justify-content: center;
align-items: center;
position: relative;
transition: all ease 0.2s;
border: solid 1px vars.$border-color;
&:hover {
border-color: rgba(255, 255, 255, 0.5);
}
}
&__input {
width: 100%;
height: 100%;
position: absolute;
margin: 0;
padding: 0;
opacity: 0;
cursor: pointer;
}
&__label {
cursor: pointer;
}
}

View File

@@ -1,6 +1,6 @@
import { useId } from "react"; import { useId } from "react";
import * as styles from "./checkbox-field.css";
import { CheckIcon } from "@primer/octicons-react"; import { CheckIcon } from "@primer/octicons-react";
import "./checkbox-field.scss";
export interface CheckboxFieldProps export interface CheckboxFieldProps
extends React.DetailedHTMLProps< extends React.DetailedHTMLProps<
@@ -14,17 +14,19 @@ export function CheckboxField({ label, ...props }: CheckboxFieldProps) {
const id = useId(); const id = useId();
return ( return (
<div className={styles.checkboxField}> <div className="checkbox-field">
<div className={styles.checkbox({ checked: props.checked })}> <div
className={`checkbox-field__checkbox ${props.checked ? "checked" : ""}`}
>
<input <input
id={id} id={id}
type="checkbox" type="checkbox"
className={styles.checkboxInput} className="checkbox-field__input"
{...props} {...props}
/> />
{props.checked && <CheckIcon />} {props.checked && <CheckIcon />}
</div> </div>
<label htmlFor={id} className={styles.checkboxLabel}> <label htmlFor={id} className="checkbox-field__label">
{label} {label}
</label> </label>
</div> </div>

View File

@@ -1,13 +0,0 @@
import { SPACING_UNIT } from "../../theme.css";
import { style } from "@vanilla-extract/css";
export const actions = style({
display: "flex",
alignSelf: "flex-end",
gap: `${SPACING_UNIT * 2}px`,
});
export const descriptionText = style({
fontSize: "16px",
lineHeight: "24px",
});

View File

@@ -0,0 +1,17 @@
@use "../../scss/variables" as vars;
.confirmation-modal {
display: flex;
flex-direction: column;
gap: calc(vars.$spacing-unit * 2);
&__actions {
display: flex;
align-self: flex-end;
gap: calc(vars.$spacing-unit * 2);
}
&__description {
font-size: 16px;
line-height: 24px;
}
}

View File

@@ -1,7 +1,7 @@
import { Button } from "../button/button"; import { Button } from "../button/button";
import { Modal, type ModalProps } from "../modal/modal"; import { Modal, type ModalProps } from "../modal/modal";
import * as styles from "./confirmation-modal.css"; import "./confirmation-modal.scss";
export interface ConfirmationModalProps extends Omit<ModalProps, "children"> { export interface ConfirmationModalProps extends Omit<ModalProps, "children"> {
confirmButtonLabel: string; confirmButtonLabel: string;
@@ -31,10 +31,10 @@ export function ConfirmationModal({
return ( return (
<Modal {...props}> <Modal {...props}>
<div style={{ display: "flex", flexDirection: "column", gap: "16px" }}> <div className="confirmation-modal">
<p className={styles.descriptionText}>{descriptionText}</p> <p className="confirmation-modal__description">{descriptionText}</p>
<div className={styles.actions}> <div className="confirmation-modal__actions">
<Button theme="outline" onClick={handleCancelClick}> <Button theme="outline" onClick={handleCancelClick}>
{cancelButtonLabel} {cancelButtonLabel}
</Button> </Button>

View File

@@ -1,9 +1,9 @@
@use "../../scss/globals.scss"; @use "../../scss/variables" as vars;
.dropdown-menu { .dropdown-menu {
&__content { &__content {
background-color: globals.$dark-background-color; background-color: vars.$dark-background-color;
border: 1px solid globals.$border-color; border: 1px solid vars.$border-color;
border-radius: 6px; border-radius: 6px;
min-width: 200px; min-width: 200px;
flex-direction: column; flex-direction: column;
@@ -20,13 +20,13 @@
padding: 4px 12px; padding: 4px 12px;
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
color: globals.$muted-color; color: vars.$muted-color;
} }
&__separator { &__separator {
width: 100%; width: 100%;
height: 1px; height: 1px;
background-color: globals.$border-color; background-color: vars.$border-color;
} }
&__item { &__item {
@@ -49,12 +49,12 @@
} }
&:not(&__item--disabled) &__item:hover { &:not(&__item--disabled) &__item:hover {
background-color: globals.$background-color; background-color: vars.$background-color;
color: globals.$muted-color; color: vars.$muted-color;
} }
&__item:focus { &__item:focus {
background-color: globals.$background-color; background-color: vars.$background-color;
outline: none; outline: none;
} }

View File

@@ -1,106 +0,0 @@
import { style } from "@vanilla-extract/css";
import { SPACING_UNIT, vars } from "../../theme.css";
export const card = style({
width: "100%",
height: "180px",
boxShadow: "0px 0px 15px 0px #000000",
overflow: "hidden",
borderRadius: "4px",
transition: "all ease 0.2s",
border: `solid 1px ${vars.color.border}`,
cursor: "pointer",
zIndex: "1",
":active": {
opacity: vars.opacity.active,
},
});
export const backdrop = style({
background: "linear-gradient(0deg, rgba(0, 0, 0, 0.7) 50%, transparent 100%)",
width: "100%",
height: "100%",
display: "flex",
justifyContent: "flex-end",
flexDirection: "column",
position: "relative",
});
export const cover = style({
width: "100%",
height: "100%",
objectFit: "cover",
objectPosition: "center",
position: "absolute",
zIndex: "-1",
transition: "all ease 0.2s",
selectors: {
[`${card}:hover &`]: {
transform: "scale(1.05)",
},
},
});
export const content = style({
color: "#DADBE1",
padding: `${SPACING_UNIT}px ${SPACING_UNIT * 2}px`,
display: "flex",
alignItems: "flex-start",
gap: `${SPACING_UNIT}px`,
flexDirection: "column",
transition: "all ease 0.2s",
transform: "translateY(24px)",
selectors: {
[`${card}:hover &`]: {
transform: "translateY(0px)",
},
},
});
export const title = style({
fontSize: "16px",
fontWeight: "bold",
textAlign: "left",
});
export const downloadOptions = style({
display: "flex",
margin: "0",
padding: "0",
gap: `${SPACING_UNIT}px`,
flexWrap: "wrap",
listStyle: "none",
});
export const specifics = style({
display: "flex",
gap: `${SPACING_UNIT * 2}px`,
justifyContent: "center",
});
export const specificsItem = style({
gap: `${SPACING_UNIT}px`,
display: "flex",
color: vars.color.muted,
fontSize: "12px",
alignItems: "flex-end",
});
export const titleContainer = style({
display: "flex",
alignItems: "center",
gap: `${SPACING_UNIT}px`,
color: vars.color.muted,
});
export const shopIcon = style({
width: "20px",
height: "20px",
minWidth: "20px",
});
export const noDownloadsLabel = style({
color: vars.color.body,
fontWeight: "bold",
});

View File

@@ -0,0 +1,102 @@
@use "../../scss/variables" as vars;
.game-card {
width: 100%;
height: 180px;
box-shadow: 0px 0px 15px 0px #000000;
overflow: hidden;
border-radius: 4px;
transition: all ease 0.2s;
border: solid 1px vars.$border-color;
cursor: pointer;
z-index: 1;
&:active {
opacity: vars.$active-opacity;
}
&__backdrop {
background: linear-gradient(0deg, rgba(0, 0, 0, 0.7) 50%, transparent 100%);
width: 100%;
height: 100%;
display: flex;
justify-content: flex-end;
flex-direction: column;
position: relative;
}
&__cover {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
position: absolute;
z-index: -1;
transition: all ease 0.2s;
}
&__content {
color: #dadbe1;
padding: vars.$spacing-unit calc(vars.$spacing-unit * 2);
display: flex;
align-items: flex-start;
gap: vars.$spacing-unit;
flex-direction: column;
transition: all ease 0.2s;
transform: translateY(24px);
}
&__title {
font-size: 16px;
font-weight: bold;
text-align: left;
}
&__download-options {
display: flex;
margin: 0;
padding: 0;
gap: vars.$spacing-unit;
flex-wrap: wrap;
list-style: none;
}
&__specifics {
display: flex;
gap: calc(vars.$spacing-unit * 2);
justify-content: center;
}
&__specifics-item {
gap: vars.$spacing-unit;
display: flex;
color: vars.$muted-color;
font-size: 12px;
align-items: flex-end;
}
&__title-container {
display: flex;
align-items: center;
gap: vars.$spacing-unit;
color: vars.$muted-color;
}
&__shop-icon {
width: 20px;
height: 20px;
min-width: 20px;
}
&__no-download-label {
color: vars.$body-color;
font-weight: bold;
}
&:hover &__cover {
transform: scale(1.05);
}
&:hover &__content {
transform: translateY(0px);
}
}

View File

@@ -3,7 +3,8 @@ import type { GameStats } from "@types";
import SteamLogo from "@renderer/assets/steam-logo.svg?react"; import SteamLogo from "@renderer/assets/steam-logo.svg?react";
import * as styles from "./game-card.css"; import "./game-card.scss";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Badge } from "../badge/badge"; import { Badge } from "../badge/badge";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
@@ -19,7 +20,7 @@ export interface GameCardProps
} }
const shopIcon = { const shopIcon = {
steam: <SteamLogo className={styles.shopIcon} />, steam: <SteamLogo className="game-card__shop-icon" />,
}; };
export function GameCard({ game, ...props }: GameCardProps) { export function GameCard({ game, ...props }: GameCardProps) {
@@ -48,25 +49,25 @@ export function GameCard({ game, ...props }: GameCardProps) {
<button <button
{...props} {...props}
type="button" type="button"
className={styles.card} className="game-card"
onMouseEnter={handleHover} onMouseEnter={handleHover}
> >
<div className={styles.backdrop}> <div className="game-card__backdrop">
<img <img
src={steamUrlBuilder.library(game.objectId)} src={steamUrlBuilder.library(game.objectId)}
alt={game.title} alt={game.title}
className={styles.cover} className="game-card__cover"
loading="lazy" loading="lazy"
/> />
<div className={styles.content}> <div className="game-card__content">
<div className={styles.titleContainer}> <div className="game-card__title-container">
{shopIcon[game.shop]} {shopIcon[game.shop]}
<p className={styles.title}>{game.title}</p> <p className="game-card__title">{game.title}</p>
</div> </div>
{uniqueRepackers.length > 0 ? ( {uniqueRepackers.length > 0 ? (
<ul className={styles.downloadOptions}> <ul className="game-card__download-options">
{uniqueRepackers.map((repacker) => ( {uniqueRepackers.map((repacker) => (
<li key={repacker}> <li key={repacker}>
<Badge>{repacker}</Badge> <Badge>{repacker}</Badge>
@@ -74,17 +75,17 @@ export function GameCard({ game, ...props }: GameCardProps) {
))} ))}
</ul> </ul>
) : ( ) : (
<p className={styles.noDownloadsLabel}>{t("no_downloads")}</p> <p className="game-card__no-download-label">{t("no_downloads")}</p>
)} )}
<div className={styles.specifics}> <div className="game-card__specifics">
<div className={styles.specificsItem}> <div className="game-card__specifics-item">
<DownloadIcon /> <DownloadIcon />
<span> <span>
{stats ? numberFormatter.format(stats.downloadCount) : "…"} {stats ? numberFormatter.format(stats.downloadCount) : "…"}
</span> </span>
</div> </div>
<div className={styles.specificsItem}> <div className="game-card__specifics-item">
<PeopleIcon /> <PeopleIcon />
<span> <span>
{stats ? numberFormatter.format(stats?.playerCount) : "…"} {stats ? numberFormatter.format(stats?.playerCount) : "…"}

View File

@@ -0,0 +1,32 @@
@use "../../scss/variables" as vars;
.auto-update-sub-header {
border-bottom: solid 1px vars.$body-color;
padding: calc(vars.$spacing-unit / 2) calc(vars.$spacing-unit * 3);
&__new-version-link {
display: flex;
align-items: center;
gap: vars.$spacing-unit;
color: #8e919b;
font-size: 12px;
}
&__new-version-icon {
color: vars.$success-color;
}
&__new-version-button {
display: flex;
align-items: center;
justify-content: center;
gap: vars.$spacing-unit;
color: vars.$body-color;
font-size: 12px;
&:hover {
text-decoration: underline;
cursor: pointer;
}
}
}

View File

@@ -2,7 +2,7 @@ import { useTranslation } from "react-i18next";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { SyncIcon } from "@primer/octicons-react"; import { SyncIcon } from "@primer/octicons-react";
import { Link } from "../link/link"; import { Link } from "../link/link";
import * as styles from "./header.css"; import "./auto-update-header.scss";
import type { AppUpdaterEvent } from "@types"; import type { AppUpdaterEvent } from "@types";
export const releasesPageUrl = export const releasesPageUrl =
@@ -45,9 +45,15 @@ export function AutoUpdateSubHeader() {
if (!isAutoInstallAvailable) { if (!isAutoInstallAvailable) {
return ( return (
<header className={styles.subheader}> <header className="auto-update-sub-header">
<Link to={releasesPageUrl} className={styles.newVersionLink}> <Link
<SyncIcon className={styles.newVersionIcon} size={12} /> to={releasesPageUrl}
className="auto-update-sub-header__new-version-link"
>
<SyncIcon
className="auto-update-sub-header__new-version-icon"
size={12}
/>
{t("version_available_download", { version: newVersion })} {t("version_available_download", { version: newVersion })}
</Link> </Link>
</header> </header>
@@ -56,13 +62,16 @@ export function AutoUpdateSubHeader() {
if (isReadyToInstall) { if (isReadyToInstall) {
return ( return (
<header className={styles.subheader}> <header className="auto-update-sub-header">
<button <button
type="button" type="button"
className={styles.newVersionButton} className="auto-update-sub-header__new-version-button"
onClick={handleClickInstallUpdate} onClick={handleClickInstallUpdate}
> >
<SyncIcon className={styles.newVersionIcon} size={12} /> <SyncIcon
className="auto-update-sub-header__new-version-icon"
size={12}
/>
{t("version_available_install", { version: newVersion })} {t("version_available_install", { version: newVersion })}
</button> </button>
</header> </header>

View File

@@ -1,182 +0,0 @@
import type { ComplexStyleRule } from "@vanilla-extract/css";
import { keyframes, style } from "@vanilla-extract/css";
import { recipe } from "@vanilla-extract/recipes";
import { SPACING_UNIT, vars } from "../../theme.css";
export const slideIn = keyframes({
"0%": { transform: "translateX(20px)", opacity: "0" },
"100%": {
transform: "translateX(0)",
opacity: "1",
},
});
export const slideOut = keyframes({
"0%": { transform: "translateX(0px)", opacity: "1" },
"100%": {
transform: "translateX(20px)",
opacity: "0",
},
});
export const header = recipe({
base: {
display: "flex",
justifyContent: "space-between",
alignItems: "center",
gap: `${SPACING_UNIT * 2}px`,
WebkitAppRegion: "drag",
width: "100%",
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 3}px`,
color: vars.color.muted,
borderBottom: `solid 1px ${vars.color.border}`,
backgroundColor: vars.color.darkBackground,
} as ComplexStyleRule,
variants: {
draggingDisabled: {
true: {
WebkitAppRegion: "no-drag",
} as ComplexStyleRule,
},
isWindows: {
true: {
WebkitAppRegion: "no-drag",
} as ComplexStyleRule,
},
},
});
export const search = recipe({
base: {
backgroundColor: vars.color.background,
display: "inline-flex",
transition: "all ease 0.2s",
width: "200px",
alignItems: "center",
borderRadius: "8px",
border: `solid 1px ${vars.color.border}`,
height: "40px",
WebkitAppRegion: "no-drag",
} as ComplexStyleRule,
variants: {
focused: {
true: {
width: "250px",
borderColor: "#DADBE1",
},
false: {
":hover": {
borderColor: "rgba(255, 255, 255, 0.5)",
},
},
},
},
});
export const searchInput = style({
backgroundColor: "transparent",
border: "none",
width: "100%",
height: "100%",
outline: "none",
color: "#DADBE1",
cursor: "default",
fontFamily: "inherit",
textOverflow: "ellipsis",
":focus": {
cursor: "text",
},
});
export const actionButton = style({
color: "inherit",
cursor: "pointer",
transition: "all ease 0.2s",
padding: `${SPACING_UNIT}px`,
":hover": {
color: "#DADBE1",
},
});
export const section = style({
display: "flex",
alignItems: "center",
gap: `${SPACING_UNIT * 2}px`,
height: "100%",
overflow: "hidden",
});
export const backButton = recipe({
base: {
color: vars.color.body,
cursor: "pointer",
WebkitAppRegion: "no-drag",
position: "absolute",
transition: "transform ease 0.2s",
animationDuration: "0.2s",
width: "16px",
height: "16px",
display: "flex",
alignItems: "center",
} as ComplexStyleRule,
variants: {
enabled: {
true: {
animationName: slideIn,
},
false: {
opacity: "0",
pointerEvents: "none",
animationName: slideOut,
},
},
},
});
export const title = recipe({
base: {
transition: "all ease 0.2s",
overflow: "hidden",
textOverflow: "ellipsis",
width: "100%",
},
variants: {
hasBackButton: {
true: {
transform: "translateX(28px)",
width: "calc(100% - 28px)",
},
},
},
});
export const subheader = style({
borderBottom: `solid 1px ${vars.color.border}`,
padding: `${SPACING_UNIT / 2}px ${SPACING_UNIT * 3}px`,
});
export const newVersionButton = style({
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: `${SPACING_UNIT}px`,
color: vars.color.body,
fontSize: "12px",
":hover": {
textDecoration: "underline",
cursor: "pointer",
},
});
export const newVersionLink = style({
display: "flex",
alignItems: "center",
gap: `${SPACING_UNIT}px`,
color: "#8e919b",
fontSize: "12px",
});
export const newVersionIcon = style({
color: vars.color.success,
});

View File

@@ -0,0 +1,145 @@
@use "../../scss/variables" as vars;
.header {
display: flex;
justify-content: space-between;
align-items: center;
gap: calc(vars.$spacing-unit * 2);
-webkit-app-region: drag;
width: 100%;
padding: calc(vars.$spacing-unit * 2) calc(vars.$spacing-unit * 3);
color: vars.$muted-color;
border-bottom: solid 1px vars.$border-color;
background-color: vars.$dark-background-color;
&--dragging-disabled {
-webkit-app-region: no-drag;
}
&--is-windows {
-webkit-app-region: no-drag;
}
&__search {
background-color: vars.$background-color;
display: inline-flex;
transition: all ease 0.2s;
width: 200px;
align-items: center;
border-radius: 8px;
border: solid 1px vars.$border-color;
height: 40px;
-webkit-app-region: no-drag;
&:hover {
border-color: rgba(255, 255, 255, 0.5);
}
&--focused {
width: 250px;
border-color: #dadbe1;
}
}
&__search-input {
background-color: transparent;
border: none;
width: 100%;
height: 100%;
outline: none;
color: #dadbe1;
cursor: default;
font-family: inherit;
text-overflow: ellipsis;
&:focus {
cursor: text;
}
}
&__action-button {
color: inherit;
cursor: pointer;
transition: all ease 0.2s;
padding: vars.$spacing-unit;
&:hover {
color: #dadbe1;
}
}
&__section {
display: flex;
align-items: center;
gap: calc(vars.$spacing-unit * 2);
height: 100%;
overflow: hidden;
}
&__back-button {
color: vars.$body-color;
cursor: pointer;
-webkit-app-region: no-drag;
position: absolute;
transition: transform ease 0.2s;
animation-duration: 0.2s;
width: 16px;
height: 16px;
display: flex;
align-items: center;
opacity: 0;
pointer-events: none;
animation-name: slide-out;
&--enabled {
animation: slide-in;
opacity: 1;
pointer-events: all;
}
}
&__title {
transition: all ease 0.2s;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
&--has-back-button {
transform: translateX(28px);
width: calc(100% - 28px);
}
}
&__new-version-link {
display: flex;
align-items: center;
gap: vars.$spacing-unit;
color: vars.$body-color;
font-size: vars.$new-version-font-size;
}
&__new-version-icon {
color: vars.$success-color;
}
}
@keyframes slide-in {
0% {
transform: translateX(20px);
opacity: 0;
}
100% {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slide-out {
0% {
transform: translateX(0px);
opacity: 1;
}
100% {
transform: translateX(20px);
opacity: 0;
}
}

View File

@@ -5,9 +5,10 @@ import { ArrowLeftIcon, SearchIcon, XIcon } from "@primer/octicons-react";
import { useAppDispatch, useAppSelector } from "@renderer/hooks"; import { useAppDispatch, useAppSelector } from "@renderer/hooks";
import * as styles from "./header.css"; import "./header.scss";
import { AutoUpdateSubHeader } from "./auto-update-sub-header"; import { AutoUpdateSubHeader } from "./auto-update-sub-header";
import { setFilters } from "@renderer/features"; import { setFilters } from "@renderer/features";
import cn from "classnames";
const pathTitle: Record<string, string> = { const pathTitle: Record<string, string> = {
"/": "home", "/": "home",
@@ -75,16 +76,16 @@ export function Header() {
return ( return (
<> <>
<header <header
className={styles.header({ className={cn("header", {
draggingDisabled, "header--dragging-disabled": draggingDisabled,
isWindows: window.electron.platform === "win32", "header--is-windows": window.electron.platform === "win32",
})} })}
> >
<section className={styles.section} style={{ flex: 1 }}> <section className="header__section" style={{ flex: 1 }}>
<button <button
type="button" type="button"
className={styles.backButton({ className={cn("header__back-button", {
enabled: location.key !== "default", "header__back-button--enabled": location.key !== "default",
})} })}
onClick={handleBackButtonClick} onClick={handleBackButtonClick}
disabled={location.key === "default"} disabled={location.key === "default"}
@@ -93,19 +94,23 @@ export function Header() {
</button> </button>
<h3 <h3
className={styles.title({ className={cn("header__title", {
hasBackButton: location.key !== "default", "header__title--has-back-button": location.key !== "default",
})} })}
> >
{title} {title}
</h3> </h3>
</section> </section>
<section className={styles.section}> <section className="header__section">
<div className={styles.search({ focused: isFocused })}> <div
className={cn("header__search", {
"header__search--focused": isFocused,
})}
>
<button <button
type="button" type="button"
className={styles.actionButton} className="header__action-button"
onClick={focusInput} onClick={focusInput}
> >
<SearchIcon /> <SearchIcon />
@@ -117,7 +122,7 @@ export function Header() {
name="search" name="search"
placeholder={t("search")} placeholder={t("search")}
value={searchValue} value={searchValue}
className={styles.searchInput} className="header__search-input"
onChange={(event) => handleSearch(event.target.value)} onChange={(event) => handleSearch(event.target.value)}
onFocus={() => setIsFocused(true)} onFocus={() => setIsFocused(true)}
onBlur={handleBlur} onBlur={handleBlur}
@@ -127,7 +132,7 @@ export function Header() {
<button <button
type="button" type="button"
onClick={() => dispatch(setFilters({ title: "" }))} onClick={() => dispatch(setFilters({ title: "" }))}
className={styles.actionButton} className="header__action-button"
> >
<XIcon /> <XIcon />
</button> </button>

View File

@@ -1,60 +0,0 @@
import { style } from "@vanilla-extract/css";
import { SPACING_UNIT, vars } from "../../theme.css";
export const hero = style({
width: "100%",
height: "280px",
minHeight: "280px",
maxHeight: "280px",
borderRadius: "4px",
color: "#DADBE1",
overflow: "hidden",
boxShadow: "0px 0px 15px 0px #000000",
cursor: "pointer",
border: `solid 1px ${vars.color.border}`,
zIndex: "1",
});
export const heroMedia = style({
objectFit: "cover",
objectPosition: "center",
position: "absolute",
zIndex: "-1",
width: "100%",
height: "100%",
transition: "all ease 0.2s",
imageRendering: "revert",
selectors: {
[`${hero}:hover &`]: {
transform: "scale(1.02)",
},
},
});
export const backdrop = style({
width: "100%",
height: "100%",
background: "linear-gradient(0deg, rgba(0, 0, 0, 0.8) 25%, transparent 100%)",
position: "relative",
display: "flex",
overflow: "hidden",
});
export const description = style({
maxWidth: "700px",
color: vars.color.muted,
textAlign: "left",
lineHeight: "20px",
marginTop: `${SPACING_UNIT * 2}px`,
});
export const content = style({
width: "100%",
height: "100%",
padding: `${SPACING_UNIT * 4}px ${SPACING_UNIT * 3}px`,
gap: `${SPACING_UNIT * 2}px`,
display: "flex",
flexDirection: "column",
justifyContent: "flex-end",
});

View File

@@ -0,0 +1,57 @@
@use "../../scss/variables" as vars;
.hero {
width: 100%;
height: 280px;
min-height: 280px;
max-height: 280px;
border-radius: 4px;
color: #dadbe1;
overflow: hidden;
box-shadow: 0px 0px 15px 0px #000000;
cursor: pointer;
border: solid 1px vars.$border-color;
z-index: 1;
&__media {
object-fit: cover;
object-position: center;
position: absolute;
z-index: -1;
width: 100%;
height: 100%;
transition: all ease 0.2s;
image-rendering: revert;
&:hover {
transform: scale(1.02);
}
}
&__backdrop {
width: 100%;
height: 100%;
background: linear-gradient(0deg, rgba(0, 0, 0, 0.8) 25%, transparent 100%);
position: relative;
display: flex;
overflow: hidden;
}
&__description {
max-width: 700px;
color: vars.$muted-color;
text-align: left;
line-height: 20px;
margin-top: vars.$spacing-unit * 2;
}
&__content {
width: 100%;
height: 100%;
padding: vars.$spacing-unit * 4 vars.$spacing-unit * 3;
gap: vars.$spacing-unit * 2;
display: flex;
flex-direction: column;
justify-content: flex-end;
}
}

View File

@@ -1,9 +1,9 @@
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import * as styles from "./hero.css";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import type { TrendingGame } from "@types"; import type { TrendingGame } from "@types";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Skeleton from "react-loading-skeleton"; import Skeleton from "react-loading-skeleton";
import "./hero.scss";
export function Hero() { export function Hero() {
const [featuredGameDetails, setFeaturedGameDetails] = useState< const [featuredGameDetails, setFeaturedGameDetails] = useState<
@@ -29,7 +29,7 @@ export function Hero() {
}, [i18n.language]); }, [i18n.language]);
if (isLoading) { if (isLoading) {
return <Skeleton className={styles.hero} />; return <Skeleton className="hero" />;
} }
if (featuredGameDetails?.length) { if (featuredGameDetails?.length) {
@@ -37,17 +37,17 @@ export function Hero() {
<button <button
type="button" type="button"
onClick={() => navigate(game.uri)} onClick={() => navigate(game.uri)}
className={styles.hero} className="hero"
key={index} key={index}
> >
<div className={styles.backdrop}> <div className="hero__backdrop">
<img <img
src={game.background} src={game.background}
alt={game.description} alt={game.description}
className={styles.heroMedia} className="hero__media"
/> />
<div className={styles.content}> <div className="hero__content">
{game.logo && ( {game.logo && (
<img <img
src={game.logo} src={game.logo}
@@ -56,7 +56,7 @@ export function Hero() {
loading="eager" loading="eager"
/> />
)} )}
<p className={styles.description}>{game.description}</p> <p className="hero__description">{game.description}</p>
</div> </div>
</div> </div>
</button> </button>

View File

@@ -1,9 +0,0 @@
import { style } from "@vanilla-extract/css";
export const link = style({
textDecoration: "none",
color: "#C0C1C7",
":hover": {
textDecoration: "underline",
},
});

View File

@@ -0,0 +1,10 @@
@use "../../scss/variables" as vars;
.link {
text-decoration: none;
color: vars.$muted-color;
&:hover {
text-decoration: underline;
}
}

View File

@@ -1,6 +1,6 @@
import { Link as ReactRouterDomLink, LinkProps } from "react-router-dom"; import { Link as ReactRouterDomLink, LinkProps } from "react-router-dom";
import cn from "classnames"; import cn from "classnames";
import * as styles from "./link.css"; import "./link.scss";
export function Link({ children, to, className, ...props }: LinkProps) { export function Link({ children, to, className, ...props }: LinkProps) {
const openExternal = (event: React.MouseEvent) => { const openExternal = (event: React.MouseEvent) => {
@@ -12,7 +12,7 @@ export function Link({ children, to, className, ...props }: LinkProps) {
return ( return (
<a <a
href={to} href={to}
className={cn(styles.link, className)} className={cn("link", className)}
onClick={openExternal} onClick={openExternal}
{...props} {...props}
> >
@@ -22,11 +22,7 @@ export function Link({ children, to, className, ...props }: LinkProps) {
} }
return ( return (
<ReactRouterDomLink <ReactRouterDomLink className={cn("link", className)} to={to} {...props}>
className={cn(styles.link, className)}
to={to}
{...props}
>
{children} {children}
</ReactRouterDomLink> </ReactRouterDomLink>
); );

View File

@@ -1,78 +0,0 @@
import { keyframes, style } from "@vanilla-extract/css";
import { recipe } from "@vanilla-extract/recipes";
import { SPACING_UNIT, vars } from "../../theme.css";
export const scaleFadeIn = keyframes({
"0%": { opacity: "0", scale: "0.5" },
"100%": {
opacity: "1",
scale: "1",
},
});
export const scaleFadeOut = keyframes({
"0%": { opacity: "1", scale: "1" },
"100%": {
opacity: "0",
scale: "0.5",
},
});
export const modal = recipe({
base: {
animation: `${scaleFadeIn} 0.2s cubic-bezier(0.33, 1, 0.68, 1) 0s 1 normal none running`,
backgroundColor: vars.color.background,
borderRadius: "4px",
minWidth: "400px",
maxWidth: "600px",
color: vars.color.body,
maxHeight: "100%",
border: `solid 1px ${vars.color.border}`,
overflow: "hidden",
display: "flex",
flexDirection: "column",
},
variants: {
closing: {
true: {
animationName: scaleFadeOut,
opacity: "0",
},
},
large: {
true: {
width: "800px",
maxWidth: "800px",
},
},
},
});
export const modalContent = style({
height: "100%",
overflow: "auto",
padding: `${SPACING_UNIT * 3}px ${SPACING_UNIT * 2}px`,
});
export const modalHeader = style({
display: "flex",
gap: `${SPACING_UNIT}px`,
padding: `${SPACING_UNIT * 2}px`,
borderBottom: `solid 1px ${vars.color.border}`,
justifyContent: "space-between",
alignItems: "center",
});
export const closeModalButton = style({
cursor: "pointer",
transition: "all ease 0.2s",
alignSelf: "flex-start",
":hover": {
opacity: "0.75",
},
});
export const closeModalButtonIcon = style({
color: vars.color.body,
});

View File

@@ -0,0 +1,77 @@
@use "../../scss/variables" as vars;
.modal {
animation: scale-fade-in 0.2s cubic-bezier(0.33, 1, 0.68, 1) 0s 1 normal none
running;
background-color: vars.$background-color;
border-radius: 4px;
min-width: 400px;
max-width: 600px;
color: vars.$body-color;
max-height: 100%;
border: solid 1px vars.$border-color;
overflow: hidden;
display: flex;
flex-direction: column;
&__closing {
animation-name: scale-fade-out;
opacity: 0;
}
&__large {
width: 800px;
max-width: 800px;
}
&__content {
height: 100%;
overflow: auto;
padding: calc(vars.$spacing-unit * 3) calc(vars.$spacing-unit * 2);
}
&__header {
display: flex;
gap: vars.$spacing-unit;
padding: calc(vars.$spacing-unit * 2);
border-bottom: solid 1px vars.$border-color;
justify-content: space-between;
align-items: center;
}
&__close-button {
cursor: pointer;
transition: all ease 0.2s;
align-self: flex-start;
&:hover {
opacity: 0.75;
}
}
&__close-button-icon {
color: vars.$body-color;
}
}
@keyframes scale-fade-in {
0% {
opacity: 0;
scale: 0.5;
}
100% {
opacity: 1;
scale: 1;
}
}
@keyframes scale-fade-out {
0% {
opacity: 1;
scale: 1;
}
100% {
opacity: 0;
scale: 0.5;
}
}

View File

@@ -2,10 +2,11 @@ import { useCallback, useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import { XIcon } from "@primer/octicons-react"; import { XIcon } from "@primer/octicons-react";
import * as styles from "./modal.css"; import "./modal.scss";
import { Backdrop } from "../backdrop/backdrop"; import { Backdrop } from "../backdrop/backdrop";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import cn from "classnames";
export interface ModalProps { export interface ModalProps {
visible: boolean; visible: boolean;
@@ -46,6 +47,12 @@ export function Modal({
}, [onClose]); }, [onClose]);
const isTopMostModal = () => { const isTopMostModal = () => {
if (
document.querySelector(
".featurebase-widget-overlay.featurebase-display-block"
)
)
return false;
const openModals = document.querySelectorAll("[role=dialog]"); const openModals = document.querySelectorAll("[role=dialog]");
return ( return (
@@ -102,14 +109,17 @@ export function Modal({
return createPortal( return createPortal(
<Backdrop isClosing={isClosing}> <Backdrop isClosing={isClosing}>
<div <div
className={styles.modal({ closing: isClosing, large })} className={cn("modal", {
modal__closing: isClosing,
modal__large: large,
})}
role="dialog" role="dialog"
aria-labelledby={title} aria-labelledby={title}
aria-describedby={description} aria-describedby={description}
ref={modalContentRef} ref={modalContentRef}
data-hydra-dialog data-hydra-dialog
> >
<div className={styles.modalHeader}> <div className="modal__header">
<div style={{ display: "flex", gap: 4, flexDirection: "column" }}> <div style={{ display: "flex", gap: 4, flexDirection: "column" }}>
<h3>{title}</h3> <h3>{title}</h3>
{description && <p>{description}</p>} {description && <p>{description}</p>}
@@ -118,13 +128,13 @@ export function Modal({
<button <button
type="button" type="button"
onClick={handleCloseClick} onClick={handleCloseClick}
className={styles.closeModalButton} className="modal__close-button"
aria-label={t("close")} aria-label={t("close")}
> >
<XIcon className={styles.closeModalButtonIcon} size={24} /> <XIcon className="modal__close-button-icon" size={24} />
</button> </button>
</div> </div>
<div className={styles.modalContent}>{children}</div> <div className="modal__content">{children}</div>
</div> </div>
</Backdrop>, </Backdrop>,
document.body document.body

View File

@@ -1,59 +0,0 @@
import { style } from "@vanilla-extract/css";
import { recipe } from "@vanilla-extract/recipes";
import { SPACING_UNIT, vars } from "../../theme.css";
export const select = recipe({
base: {
display: "inline-flex",
transition: "all ease 0.2s",
width: "fit-content",
alignItems: "center",
borderRadius: "8px",
border: `1px solid ${vars.color.border}`,
height: "40px",
minHeight: "40px",
},
variants: {
focused: {
true: {
borderColor: "#DADBE1",
},
false: {
":hover": {
borderColor: "rgba(255, 255, 255, 0.5)",
},
},
},
theme: {
primary: {
backgroundColor: vars.color.darkBackground,
},
dark: {
backgroundColor: vars.color.background,
},
},
},
});
export const option = style({
backgroundColor: vars.color.darkBackground,
borderRight: "4px solid",
borderColor: "transparent",
borderRadius: "8px",
width: "fit-content",
height: "100%",
outline: "none",
color: "#DADBE1",
cursor: "default",
fontFamily: "inherit",
fontSize: vars.size.body,
textOverflow: "ellipsis",
padding: `${SPACING_UNIT}px`,
});
export const label = style({
marginBottom: `${SPACING_UNIT}px`,
display: "block",
color: vars.color.body,
});

View File

@@ -0,0 +1,49 @@
@use "../../scss/variables" as vars;
.select-field {
display: inline-flex;
transition: all ease 0.2s;
width: fit-content;
align-items: center;
border-radius: 8px;
border: 1px solid vars.$border-color;
height: 40px;
min-height: 40px;
&:hover {
border-color: rgba(255, 255, 255, 0.5);
}
&__focused {
border-color: #dadbe1;
}
&__primary {
background-color: vars.$dark-background-color;
}
&__dark {
background-color: vars.$background-color;
}
&__option {
background-color: vars.$dark-background-color;
border-right: 4px solid;
border-color: transparent;
border-radius: 8px;
width: fit-content;
height: 100%;
outline: none;
color: #dadbe1;
cursor: default;
font-family: inherit;
font-size: vars.$body-font-size;
text-overflow: ellipsis;
padding: vars.$spacing-unit;
}
&__label {
margin-bottom: vars.$spacing-unit;
display: block;
color: vars.$body-color;
}
}

View File

@@ -1,13 +1,13 @@
import { useId, useState } from "react"; import { useId, useState } from "react";
import type { RecipeVariants } from "@vanilla-extract/recipes"; import "./select-field.scss";
import * as styles from "./select-field.css"; import cn from "classnames";
export interface SelectProps export interface SelectProps
extends React.DetailedHTMLProps< extends React.DetailedHTMLProps<
React.SelectHTMLAttributes<HTMLSelectElement>, React.SelectHTMLAttributes<HTMLSelectElement>,
HTMLSelectElement HTMLSelectElement
> { > {
theme?: NonNullable<RecipeVariants<typeof styles.select>>["theme"]; theme?: "primary" | "dark";
label?: string; label?: string;
options?: { key: string; value: string; label: string }[]; options?: { key: string; value: string; label: string }[];
} }
@@ -25,16 +25,20 @@ export function SelectField({
return ( return (
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
{label && ( {label && (
<label htmlFor={id} className={styles.label}> <label htmlFor={id} className="select-field__label">
{label} {label}
</label> </label>
)} )}
<div className={styles.select({ focused: isFocused, theme })}> <div
className={cn("select-field", `select-field--${theme}`, {
"select-field__focused": isFocused,
})}
>
<select <select
id={id} id={id}
value={value} value={value}
className={styles.option} className="select-field__option"
onFocus={() => setIsFocused(true)} onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)} onBlur={() => setIsFocused(false)}
onChange={onChange} onChange={onChange}

View File

@@ -1,79 +0,0 @@
import { style } from "@vanilla-extract/css";
import { SPACING_UNIT, vars } from "../../theme.css";
export const profileContainer = style({
position: "relative",
display: "flex",
alignItems: "center",
gap: `${SPACING_UNIT}px`,
padding: `${SPACING_UNIT}px ${SPACING_UNIT * 2}px`,
});
export const profileButton = style({
display: "flex",
cursor: "pointer",
transition: "all ease 0.1s",
color: vars.color.muted,
width: "100%",
overflow: "hidden",
borderRadius: "4px",
padding: `${SPACING_UNIT}px ${SPACING_UNIT}px`,
":hover": {
backgroundColor: "rgba(255, 255, 255, 0.15)",
},
});
export const profileButtonContent = style({
display: "flex",
alignItems: "center",
gap: `${SPACING_UNIT + SPACING_UNIT / 2}px`,
width: "100%",
});
export const profileButtonInformation = style({
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
flex: "1",
minWidth: 0,
});
export const profileButtonTitle = style({
fontWeight: "bold",
fontSize: vars.size.body,
width: "100%",
textAlign: "left",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
});
export const friendsButton = style({
color: vars.color.muted,
cursor: "pointer",
borderRadius: "50%",
width: "40px",
minWidth: "40px",
minHeight: "40px",
height: "40px",
backgroundColor: vars.color.background,
position: "relative",
transition: "all ease 0.3s",
":hover": {
backgroundColor: "rgba(255, 255, 255, 0.15)",
},
});
export const friendsButtonBadge = style({
backgroundColor: vars.color.success,
display: "flex",
justifyContent: "center",
alignItems: "center",
width: "20px",
height: "20px",
borderRadius: "50%",
position: "absolute",
top: "-5px",
right: "-5px",
});

View File

@@ -0,0 +1,79 @@
@use "../../scss/variables" as vars;
.sidebar-profile {
position: relative;
display: flex;
align-items: center;
gap: vars.$spacing-unit;
padding: vars.$spacing-unit vars.$spacing-unit * 2;
&__button {
display: flex;
cursor: pointer;
transition: all ease 0.1s;
color: vars.$muted-color;
width: 100%;
overflow: hidden;
border-radius: 4px;
padding: vars.$spacing-unit vars.$spacing-unit;
&:hover {
background-color: rgba(255, 255, 255, 0.15);
}
}
&__button-content {
display: flex;
align-items: center;
gap: calc(vars.$spacing-unit + vars.$spacing-unit / 2);
width: 100%;
}
&__button-information {
display: flex;
flex-direction: column;
align-items: flex-start;
flex: 1;
min-width: 0;
}
&__button-title {
font-weight: bold;
font-size: 14px;
width: 100%;
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&__friends-button {
color: vars.$muted-color;
cursor: pointer;
border-radius: 50%;
width: 40px;
min-width: 40px;
min-height: 40px;
height: 40px;
background-color: vars.$background-color;
position: relative;
transition: all ease 0.3s;
&:hover {
background-color: rgba(255, 255, 255, 0.15);
}
}
&__friends-button-badge {
background-color: vars.$success-color;
display: flex;
justify-content: center;
align-items: center;
width: 20px;
height: 20px;
border-radius: 50%;
position: absolute;
top: -5px;
right: -5px;
}
}

View File

@@ -1,12 +1,13 @@
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { PeopleIcon } from "@primer/octicons-react"; import { PeopleIcon } from "@primer/octicons-react";
import * as styles from "./sidebar-profile.css";
import { useAppSelector, useUserDetails } from "@renderer/hooks"; import { useAppSelector, useUserDetails } from "@renderer/hooks";
import { useEffect, useMemo } from "react"; import { useEffect, useMemo } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal"; import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
import SteamLogo from "@renderer/assets/steam-logo.svg?react"; import SteamLogo from "@renderer/assets/steam-logo.svg?react";
import { Avatar } from "../avatar/avatar"; import { Avatar } from "../avatar/avatar";
import { AuthPage } from "@shared";
import "./sidebar-profile.scss";
const LONG_POLLING_INTERVAL = 120_000; const LONG_POLLING_INTERVAL = 120_000;
@@ -26,11 +27,11 @@ export function SidebarProfile() {
const handleProfileClick = () => { const handleProfileClick = () => {
if (userDetails === null) { if (userDetails === null) {
window.electron.openAuthWindow(); window.electron.openAuthWindow(AuthPage.SignIn);
return; return;
} }
navigate(`/profile/${userDetails!.id}`); navigate(`/profile/${userDetails.id}`);
}; };
useEffect(() => { useEffect(() => {
@@ -49,14 +50,14 @@ export function SidebarProfile() {
return ( return (
<button <button
type="button" type="button"
className={styles.friendsButton} className="sidebar-profile__friends-button"
onClick={() => onClick={() =>
showFriendsModal(UserFriendModalTab.AddFriend, userDetails.id) showFriendsModal(UserFriendModalTab.AddFriend, userDetails.id)
} }
title={t("friends")} title={t("friends")}
> >
{friendRequestCount > 0 && ( {friendRequestCount > 0 && (
<small className={styles.friendsButtonBadge}> <small className="sidebar-profile__friends-button-badge">
{friendRequestCount > 99 ? "99+" : friendRequestCount} {friendRequestCount > 99 ? "99+" : friendRequestCount}
</small> </small>
)} )}
@@ -84,21 +85,21 @@ export function SidebarProfile() {
}; };
return ( return (
<div className={styles.profileContainer}> <div className="sidebar-profile">
<button <button
type="button" type="button"
className={styles.profileButton} className="sidebar-profile__button"
onClick={handleProfileClick} onClick={handleProfileClick}
> >
<div className={styles.profileButtonContent}> <div className="sidebar-profile__button-content">
<Avatar <Avatar
size={35} size={35}
src={userDetails?.profileImageUrl} src={userDetails?.profileImageUrl}
alt={userDetails?.displayName} alt={userDetails?.displayName}
/> />
<div className={styles.profileButtonInformation}> <div className="sidebar-profile__button-information">
<p className={styles.profileButtonTitle}> <p className="sidebar-profile__button-title">
{userDetails ? userDetails.displayName : t("sign_in")} {userDetails ? userDetails.displayName : t("sign_in")}
</p> </p>

View File

@@ -1,152 +0,0 @@
import { style } from "@vanilla-extract/css";
import { recipe } from "@vanilla-extract/recipes";
import { SPACING_UNIT, vars } from "../../theme.css";
export const sidebar = recipe({
base: {
backgroundColor: vars.color.darkBackground,
color: vars.color.muted,
flexDirection: "column",
display: "flex",
transition: "opacity ease 0.2s",
borderRight: `solid 1px ${vars.color.border}`,
position: "relative",
overflow: "hidden",
justifyContent: "space-between",
},
variants: {
resizing: {
true: {
opacity: vars.opacity.active,
pointerEvents: "none",
},
},
darwin: {
true: {
paddingTop: `${SPACING_UNIT * 6}px`,
},
false: {
paddingTop: `${SPACING_UNIT}px`,
},
},
},
});
export const content = style({
display: "flex",
flexDirection: "column",
padding: `${SPACING_UNIT * 2}px`,
gap: `${SPACING_UNIT * 2}px`,
width: "100%",
overflow: "auto",
});
export const handle = style({
width: "5px",
height: "100%",
cursor: "col-resize",
position: "absolute",
right: "0",
});
export const menu = style({
listStyle: "none",
padding: "0",
margin: "0",
gap: `${SPACING_UNIT / 2}px`,
display: "flex",
flexDirection: "column",
overflow: "hidden",
});
export const menuItem = recipe({
base: {
transition: "all ease 0.1s",
cursor: "pointer",
textWrap: "nowrap",
display: "flex",
color: vars.color.muted,
borderRadius: "4px",
":hover": {
backgroundColor: "rgba(255, 255, 255, 0.15)",
},
},
variants: {
active: {
true: {
backgroundColor: "rgba(255, 255, 255, 0.1)",
},
},
muted: {
true: {
opacity: vars.opacity.disabled,
":hover": {
opacity: "1",
},
},
},
},
});
export const menuItemButton = style({
color: "inherit",
display: "flex",
alignItems: "center",
gap: `${SPACING_UNIT}px`,
cursor: "pointer",
overflow: "hidden",
width: "100%",
padding: `9px ${SPACING_UNIT}px`,
});
export const menuItemButtonLabel = style({
textOverflow: "ellipsis",
overflow: "hidden",
});
export const gameIcon = style({
width: "20px",
height: "20px",
minWidth: "20px",
minHeight: "20px",
borderRadius: "4px",
backgroundSize: "cover",
});
export const sectionTitle = style({
textTransform: "uppercase",
fontWeight: "bold",
});
export const section = style({
gap: `${SPACING_UNIT * 2}px`,
display: "flex",
flexDirection: "column",
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%",
});

View File

@@ -0,0 +1,136 @@
@use "../../scss/variables" as vars;
.sidebar {
background-color: vars.$dark-background-color;
color: vars.$muted-color;
flex-direction: column;
display: flex;
transition: opacity ease 0.2s;
border-right: solid 1px vars.$border-color;
position: relative;
overflow: hidden;
padding-top: vars.$spacing-unit;
&__resizing {
opacity: vars.$active-opacity;
pointer-events: none;
}
&__darwin {
padding-top: calc(vars.$spacing-unit * 6);
}
&__content {
display: flex;
flex-direction: column;
padding: calc(vars.$spacing-unit * 2);
gap: calc(vars.$spacing-unit * 2);
width: 100%;
overflow: auto;
}
&__handle {
width: 5px;
height: 100%;
cursor: col-resize;
position: absolute;
right: 0;
}
&__menu {
list-style: none;
padding: 0;
margin: 0;
gap: calc(vars.$spacing-unit / 2);
display: flex;
flex-direction: column;
overflow: hidden;
}
&__menu-item {
transition: all ease 0.1s;
cursor: pointer;
text-wrap: nowrap;
display: flex;
color: vars.$muted-color;
border-radius: 4px;
&:hover {
background-color: rgba(255, 255, 255, 0.15);
}
&--active {
background-color: rgba(255, 255, 255, 0.1);
}
&--muted {
opacity: vars.$disabled-opacity;
&:hover {
opacity: 1;
}
}
}
&__menu-item-button {
color: inherit;
display: flex;
align-items: center;
gap: vars.$spacing-unit;
cursor: pointer;
overflow: hidden;
width: 100%;
padding: 9px vars.$spacing-unit;
}
&__menu-item-button-label {
text-overflow: ellipsis;
overflow: hidden;
}
&__game-icon {
width: 20px;
height: 20px;
min-width: 20px;
min-height: 20px;
border-radius: 4px;
background-size: cover;
}
&__section-title {
text-transform: uppercase;
font-weight: bold;
}
&__section {
gap: calc(vars.$spacing-unit * 2);
display: flex;
flex-direction: column;
padding-bottom: vars.$spacing-unit;
}
&__help-button {
color: vars.$muted-color;
padding: vars.$spacing-unit calc(vars.$spacing-unit * 2);
gap: 9px;
display: flex;
align-items: center;
cursor: pointer;
border-top: solid 1px vars.$border-color;
transition: background-color ease 0.1s;
&:hover {
background-color: rgba(255, 255, 255, 0.15);
}
}
&__help-button-icon {
background: linear-gradient(0deg, #16b195 50%, #3e62c0 100%);
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
border-radius: 50%;
}
}

View File

@@ -14,12 +14,14 @@ import {
import { routes } from "./routes"; import { routes } from "./routes";
import * as styles from "./sidebar.css"; import "./sidebar.scss";
import { buildGameDetailsPath } from "@renderer/helpers"; 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 cn from "classnames";
import { CommentDiscussionIcon } from "@primer/octicons-react"; import { CommentDiscussionIcon } from "@primer/octicons-react";
const SIDEBAR_MIN_WIDTH = 200; const SIDEBAR_MIN_WIDTH = 200;
@@ -168,9 +170,9 @@ export function Sidebar() {
return ( return (
<aside <aside
ref={sidebarRef} ref={sidebarRef}
className={styles.sidebar({ className={cn("sidebar", {
resizing: isResizing, sidebar__resizing: isResizing,
darwin: window.electron.platform === "darwin", sidebar__darwin: window.electron.platform === "darwin",
})} })}
style={{ style={{
width: sidebarWidth, width: sidebarWidth,
@@ -183,19 +185,19 @@ export function Sidebar() {
> >
<SidebarProfile /> <SidebarProfile />
<div className={styles.content}> <div className="sidebar__content">
<section className={styles.section}> <section className="sidebar__section">
<ul className={styles.menu}> <ul className="sidebar__menu">
{routes.map(({ nameKey, path, render }) => ( {routes.map(({ nameKey, path, render }) => (
<li <li
key={nameKey} key={nameKey}
className={styles.menuItem({ className={cn("sidebar__menu-item", {
active: location.pathname === path, "sidebar__menu-item--active": location.pathname === path,
})} })}
> >
<button <button
type="button" type="button"
className={styles.menuItemButton} className="sidebar__menu-item-button"
onClick={() => handleSidebarItemClick(path)} onClick={() => handleSidebarItemClick(path)}
> >
{render()} {render()}
@@ -206,8 +208,8 @@ export function Sidebar() {
</ul> </ul>
</section> </section>
<section className={styles.section}> <section className="sidebar__section">
<small className={styles.sectionTitle}>{t("my_library")}</small> <small className="sidebar__section-title">{t("my_library")}</small>
<TextField <TextField
ref={filterRef} ref={filterRef}
@@ -216,34 +218,34 @@ export function Sidebar() {
theme="dark" theme="dark"
/> />
<ul className={styles.menu}> <ul className="sidebar__menu">
{filteredLibrary.map((game) => ( {filteredLibrary.map((game) => (
<li <li
key={game.id} key={game.id}
className={styles.menuItem({ className={cn("sidebar__menu-item", {
active: "sidebar__menu-item--active":
location.pathname === location.pathname ===
`/game/${game.shop}/${game.objectID}`, `/game/${game.shop}/${game.objectID}`,
muted: game.status === "removed", "sidebar__menu-item--muted": game.status === "removed",
})} })}
> >
<button <button
type="button" type="button"
className={styles.menuItemButton} className="sidebar__menu-item-button"
onClick={(event) => handleSidebarGameClick(event, game)} onClick={(event) => handleSidebarGameClick(event, game)}
> >
{game.iconUrl ? ( {game.iconUrl ? (
<img <img
className={styles.gameIcon} className="sidebar__game-icon"
src={game.iconUrl} src={game.iconUrl}
alt={game.title} alt={game.title}
loading="lazy" loading="lazy"
/> />
) : ( ) : (
<SteamLogo className={styles.gameIcon} /> <SteamLogo className="sidebar__game-icon" />
)} )}
<span className={styles.menuItemButtonLabel}> <span className="sidebar__menu-item-button-label">
{getGameTitle(game)} {getGameTitle(game)}
</span> </span>
</button> </button>
@@ -257,10 +259,10 @@ export function Sidebar() {
{hasActiveSubscription && ( {hasActiveSubscription && (
<button <button
type="button" type="button"
className={styles.helpButton} className="sidebar__help-button"
data-open-support-chat data-open-support-chat
> >
<div className={styles.helpButtonIcon}> <div className="sidebar__help-button-icon">
<CommentDiscussionIcon size={14} /> <CommentDiscussionIcon size={14} />
</div> </div>
<span>{t("need_help")}</span> <span>{t("need_help")}</span>
@@ -269,7 +271,7 @@ export function Sidebar() {
<button <button
type="button" type="button"
className={styles.handle} className="sidebar__handle"
onMouseDown={handleMouseDown} onMouseDown={handleMouseDown}
/> />
</aside> </aside>

View File

@@ -1,89 +0,0 @@
import { style } from "@vanilla-extract/css";
import { recipe } from "@vanilla-extract/recipes";
import { SPACING_UNIT, vars } from "../../theme.css";
export const textFieldContainer = style({
flex: "1",
gap: `${SPACING_UNIT}px`,
display: "flex",
flexDirection: "column",
});
export const textField = recipe({
base: {
display: "inline-flex",
transition: "all ease 0.2s",
width: "100%",
alignItems: "center",
borderRadius: "8px",
border: `solid 1px ${vars.color.border}`,
height: "40px",
minHeight: "40px",
},
variants: {
theme: {
primary: {
backgroundColor: vars.color.darkBackground,
},
dark: {
backgroundColor: vars.color.background,
},
},
hasError: {
true: {
borderColor: vars.color.danger,
},
},
focused: {
true: {
borderColor: "#DADBE1",
},
false: {
":hover": {
borderColor: "rgba(255, 255, 255, 0.5)",
},
},
},
},
});
export const textFieldInput = recipe({
base: {
backgroundColor: "transparent",
border: "none",
width: "100%",
height: "100%",
outline: "none",
color: "#DADBE1",
cursor: "default",
fontFamily: "inherit",
textOverflow: "ellipsis",
padding: `${SPACING_UNIT}px`,
":focus": {
cursor: "text",
},
},
variants: {
readOnly: {
true: {
textOverflow: "inherit",
},
},
},
});
export const togglePasswordButton = style({
cursor: "pointer",
color: vars.color.muted,
padding: `${SPACING_UNIT}px`,
});
export const textFieldWrapper = style({
display: "flex",
gap: `${SPACING_UNIT}px`,
});
export const errorLabel = style({
color: vars.color.danger,
});

View File

@@ -0,0 +1,75 @@
@use "../../scss/variables" as vars;
.text-field-container {
flex: 1;
gap: vars.$spacing-unit;
display: flex;
flex-direction: column;
}
.text-field {
display: inline-flex;
transition: all ease 0.2s;
width: 100%;
align-items: center;
border-radius: 8px;
border: solid 1px vars.$border-color;
height: 40px;
min-height: 40px;
&__primary {
background-color: vars.$dark-background-color;
}
&__dark {
background-color: vars.$background-color;
}
&__has-error {
border-color: vars.$danger-color;
}
&--focused {
border-color: vars.$search-border-color-focused;
}
&:not(&--focused):hover {
border-color: vars.$search-border-color-hover;
}
&__input {
background-color: transparent;
border: none;
width: 100%;
height: 100%;
outline: none;
color: vars.$search-input-color;
cursor: default;
font-family: inherit;
text-overflow: ellipsis;
padding: vars.$spacing-unit;
&:focus {
cursor: text;
}
&__read-only {
text-overflow: inherit;
}
}
&__toggle-password-button {
cursor: pointer;
color: vars.$muted-color;
padding: vars.$spacing-unit;
}
&__wrapper {
display: flex;
gap: vars.$spacing-unit;
}
&__error-label {
color: vars.$danger-color;
}
}

View File

@@ -1,17 +1,16 @@
import React, { useId, useMemo, useState } from "react"; import React, { useId, useMemo, useState } from "react";
import type { RecipeVariants } from "@vanilla-extract/recipes";
import type { FieldError, FieldErrorsImpl, Merge } from "react-hook-form";
import { EyeClosedIcon, EyeIcon } from "@primer/octicons-react"; import { EyeClosedIcon, EyeIcon } from "@primer/octicons-react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import cn from "classnames";
import * as styles from "./text-field.css"; import "./text-field.scss";
export interface TextFieldProps export interface TextFieldProps
extends React.DetailedHTMLProps< extends React.DetailedHTMLProps<
React.InputHTMLAttributes<HTMLInputElement>, React.InputHTMLAttributes<HTMLInputElement>,
HTMLInputElement HTMLInputElement
> { > {
theme?: NonNullable<RecipeVariants<typeof styles.textField>>["theme"]; theme?: "primary" | "dark";
label?: string | React.ReactNode; label?: string | React.ReactNode;
hint?: string | React.ReactNode; hint?: string | React.ReactNode;
textFieldProps?: React.DetailedHTMLProps< textFieldProps?: React.DetailedHTMLProps<
@@ -23,7 +22,7 @@ export interface TextFieldProps
HTMLDivElement HTMLDivElement
>; >;
rightContent?: React.ReactNode | null; rightContent?: React.ReactNode | null;
error?: FieldError | Merge<FieldError, FieldErrorsImpl<any>> | undefined; error?: string | React.ReactNode;
} }
export const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>( export const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>(
@@ -42,9 +41,7 @@ export const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>(
) => { ) => {
const id = useId(); const id = useId();
const [isFocused, setIsFocused] = useState(false); const [isFocused, setIsFocused] = useState(false);
const [isPasswordVisible, setIsPasswordVisible] = useState(false); const [isPasswordVisible, setIsPasswordVisible] = useState(false);
const { t } = useTranslation("forms"); const { t } = useTranslation("forms");
const showPasswordToggleButton = props.type === "password"; const showPasswordToggleButton = props.type === "password";
@@ -55,9 +52,11 @@ export const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>(
}, [props.type, isPasswordVisible]); }, [props.type, isPasswordVisible]);
const hintContent = useMemo(() => { const hintContent = useMemo(() => {
if (error && error.message) if (error && typeof error === "object" && "message" in error)
return ( return (
<small className={styles.errorLabel}>{error.message as string}</small> <small className="text-field__error-label">
{error.message as string}
</small>
); );
if (hint) return <small>{hint}</small>; if (hint) return <small>{hint}</small>;
@@ -77,22 +76,23 @@ export const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>(
const hasError = !!error; const hasError = !!error;
return ( return (
<div className={styles.textFieldContainer} {...containerProps}> <div className="text-field-container" {...containerProps}>
{label && <label htmlFor={id}>{label}</label>} {label && <label htmlFor={id}>{label}</label>}
<div className={styles.textFieldWrapper}> <div className="text-field__wrapper">
<div <div
className={styles.textField({ className={cn("text-field", `text-field__${theme}`, {
theme, "text-field__has-error": hasError,
hasError, "text-field--focused": isFocused,
focused: isFocused,
})} })}
{...textFieldProps} {...textFieldProps}
> >
<input <input
ref={ref} ref={ref}
id={id} id={id}
className={styles.textFieldInput({ readOnly: props.readOnly })} className={cn("text-field__input", {
"text-field__input__read-only": props.readOnly,
})}
{...props} {...props}
onFocus={handleFocus} onFocus={handleFocus}
onBlur={handleBlur} onBlur={handleBlur}
@@ -102,7 +102,7 @@ export const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>(
{showPasswordToggleButton && ( {showPasswordToggleButton && (
<button <button
type="button" type="button"
className={styles.togglePasswordButton} className="text-field__toggle-password-button"
onClick={() => setIsPasswordVisible(!isPasswordVisible)} onClick={() => setIsPasswordVisible(!isPasswordVisible)}
aria-label={t("toggle_password_visibility")} aria-label={t("toggle_password_visibility")}
> >
@@ -124,4 +124,4 @@ export const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>(
} }
); );
TextField.displayName = "TextField"; TextField.displayName = "TextField";

View File

@@ -1,87 +0,0 @@
import { keyframes, style } from "@vanilla-extract/css";
import { SPACING_UNIT, vars } from "../../theme.css";
import { recipe } from "@vanilla-extract/recipes";
const TOAST_HEIGHT = 80;
export const slideIn = keyframes({
"0%": { transform: `translateY(${TOAST_HEIGHT + SPACING_UNIT * 2}px)` },
"100%": { transform: "translateY(0)" },
});
export const slideOut = keyframes({
"0%": { transform: `translateY(0)` },
"100%": { transform: `translateY(${TOAST_HEIGHT + SPACING_UNIT * 2}px)` },
});
export const toast = recipe({
base: {
animationDuration: "0.2s",
animationTimingFunction: "ease-in-out",
maxHeight: TOAST_HEIGHT,
position: "fixed",
backgroundColor: vars.color.background,
borderRadius: "4px",
border: `solid 1px ${vars.color.border}`,
right: `${SPACING_UNIT * 2}px`,
/* Bottom panel height + 16px */
bottom: `${26 + SPACING_UNIT * 2}px`,
overflow: "hidden",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
zIndex: vars.zIndex.toast,
maxWidth: "500px",
},
variants: {
closing: {
true: {
animationName: slideOut,
transform: `translateY(${TOAST_HEIGHT + SPACING_UNIT * 2}px)`,
},
false: {
animationName: slideIn,
transform: `translateY(0)`,
},
},
},
});
export const toastContent = style({
display: "flex",
gap: `${SPACING_UNIT * 2}px`,
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 2}px`,
justifyContent: "center",
alignItems: "center",
});
export const progress = style({
width: "100%",
height: "5px",
"::-webkit-progress-bar": {
backgroundColor: vars.color.darkBackground,
},
"::-webkit-progress-value": {
backgroundColor: vars.color.muted,
},
});
export const closeButton = style({
color: vars.color.body,
cursor: "pointer",
padding: "0",
margin: "0",
});
export const successIcon = style({
color: vars.color.success,
});
export const errorIcon = style({
color: vars.color.danger,
});
export const warningIcon = style({
color: vars.color.warning,
});

View File

@@ -0,0 +1,92 @@
@use "../../scss/variables" as vars;
@keyframes slideIn {
0% {
transform: translateY(96px);
}
100% {
transform: translateY(0);
}
}
@keyframes slideOut {
0% {
transform: translateY(0);
}
100% {
transform: translateY(96px);
}
}
.toast {
animation-duration: 0.2s;
animation-timing-function: ease-in-out;
max-height: 80px;
position: fixed;
background-color: vars.$background-color;
border-radius: 4px;
border: solid 1px vars.$border-color;
right: vars.$spacing-unit * 2;
bottom: 42px;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: space-between;
z-index: vars.$toast-z-index;
max-width: 500px;
&--closing {
animation-name: slideOut;
transform: translateY(96px);
}
&--opening {
animation-name: slideIn;
transform: translateY(0);
}
&__content {
display: flex;
gap: vars.$spacing-unit * 2;
padding: vars.$spacing-unit * 2;
justify-content: center;
align-items: center;
}
&__progress {
width: 100%;
height: 5px;
&::-webkit-progress-bar {
background-color: vars.$dark-background-color;
}
&::-webkit-progress-value {
background-color: vars.$muted-color;
}
}
&__close-button {
color: vars.$body-color;
cursor: pointer;
padding: 0;
margin: 0;
}
&__icon-container {
display: flex;
gap: var(--spacing-unit);
}
&__success-icon {
color: vars.$success-color;
}
&__error-icon {
color: vars.$danger-color;
}
&__warning-icon {
color: vars.$warning-color;
}
}

View File

@@ -6,8 +6,8 @@ import {
XIcon, XIcon,
} from "@primer/octicons-react"; } from "@primer/octicons-react";
import * as styles from "./toast.css"; import "./toast.scss";
import { SPACING_UNIT } from "@renderer/theme.css"; import cn from "classnames";
export interface ToastProps { export interface ToastProps {
visible: boolean; visible: boolean;
@@ -77,22 +77,28 @@ export function Toast({ visible, message, type, onClose }: ToastProps) {
if (!visible) return null; if (!visible) return null;
return ( return (
<div className={styles.toast({ closing: isClosing })}> <div
<div className={styles.toastContent}> className={cn("toast", {
<div style={{ display: "flex", gap: `${SPACING_UNIT}px` }}> toast__closing: isClosing,
})}
>
<div className="toast__content">
<div className="toast__icon-container">
{type === "success" && ( {type === "success" && (
<CheckCircleFillIcon className={styles.successIcon} /> <CheckCircleFillIcon className="toast__success-icon" />
)} )}
{type === "error" && <XCircleFillIcon className={styles.errorIcon} />} {type === "error" && (
<XCircleFillIcon className="toast__error-icon" />
)}
{type === "warning" && <AlertIcon className={styles.warningIcon} />} {type === "warning" && <AlertIcon className="toast__warning-icon" />}
<span style={{ fontWeight: "bold" }}>{message}</span> <span style={{ fontWeight: "bold" }}>{message}</span>
</div> </div>
<button <button
type="button" type="button"
className={styles.closeButton} className="toast__close-button"
onClick={startAnimateClosing} onClick={startAnimateClosing}
aria-label="Close toast" aria-label="Close toast"
> >
@@ -100,7 +106,7 @@ export function Toast({ visible, message, type, onClose }: ToastProps) {
</button> </button>
</div> </div>
<progress className={styles.progress} value={progress} max={100} /> <progress className="toast__progress" value={progress} max={100} />
</div> </div>
); );
} }

View File

@@ -8,6 +8,7 @@ export const DOWNLOADER_NAME = {
[Downloader.Gofile]: "Gofile", [Downloader.Gofile]: "Gofile",
[Downloader.PixelDrain]: "PixelDrain", [Downloader.PixelDrain]: "PixelDrain",
[Downloader.Qiwi]: "Qiwi", [Downloader.Qiwi]: "Qiwi",
[Downloader.Datanodes]: "Datanodes",
}; };
export const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120; export const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120;

View File

@@ -117,11 +117,7 @@ export function GameDetailsContextProvider({
abortControllerRef.current = abortController; abortControllerRef.current = abortController;
window.electron window.electron
.getGameShopDetails( .getGameShopDetails(objectId, shop, getSteamLanguage(i18n.language))
objectId!,
shop as GameShop,
getSteamLanguage(i18n.language)
)
.then((result) => { .then((result) => {
if (abortController.signal.aborted) return; if (abortController.signal.aborted) return;
@@ -140,14 +136,14 @@ export function GameDetailsContextProvider({
setIsLoading(false); setIsLoading(false);
}); });
window.electron.getGameStats(objectId, shop as GameShop).then((result) => { window.electron.getGameStats(objectId, shop).then((result) => {
if (abortController.signal.aborted) return; if (abortController.signal.aborted) return;
setStats(result); setStats(result);
}); });
if (userDetails) { if (userDetails) {
window.electron window.electron
.getUnlockedAchievements(objectId, shop as GameShop) .getUnlockedAchievements(objectId, shop)
.then((achievements) => { .then((achievements) => {
if (abortController.signal.aborted) return; if (abortController.signal.aborted) return;
setAchievements(achievements); setAchievements(achievements);

View File

@@ -1,4 +1,4 @@
import type { CatalogueCategory } from "@shared"; import type { AuthPage, CatalogueCategory } from "@shared";
import type { import type {
AppUpdaterEvent, AppUpdaterEvent,
Game, Game,
@@ -31,7 +31,7 @@ import type {
CatalogueSearchPayload, CatalogueSearchPayload,
} from "@types"; } from "@types";
import type { AxiosProgressEvent } from "axios"; import type { AxiosProgressEvent } from "axios";
import type { DiskSpace } from "check-disk-space"; import type disk from "diskusage";
declare global { declare global {
declare module "*.svg" { declare module "*.svg" {
@@ -122,7 +122,7 @@ declare global {
) => void ) => void
) => () => Electron.IpcRenderer; ) => () => Electron.IpcRenderer;
onLibraryBatchComplete: (cb: () => void) => () => Electron.IpcRenderer; onLibraryBatchComplete: (cb: () => void) => () => Electron.IpcRenderer;
resetGameAchievements: (gameId: number) => Promise<void>;
/* User preferences */ /* User preferences */
getUserPreferences: () => Promise<UserPreferences | null>; getUserPreferences: () => Promise<UserPreferences | null>;
updateUserPreferences: ( updateUserPreferences: (
@@ -140,7 +140,8 @@ declare global {
) => Promise<{ fingerprint: string }>; ) => Promise<{ fingerprint: string }>;
/* Hardware */ /* Hardware */
getDiskFreeSpace: (path: string) => Promise<DiskSpace>; getDiskFreeSpace: (path: string) => Promise<disk.DiskUsage>;
checkFolderWritePermission: (path: string) => Promise<boolean>;
/* Cloud save */ /* Cloud save */
uploadSaveGame: ( uploadSaveGame: (
@@ -195,6 +196,7 @@ declare global {
options: Electron.OpenDialogOptions options: Electron.OpenDialogOptions
) => Promise<Electron.OpenDialogReturnValue>; ) => Promise<Electron.OpenDialogReturnValue>;
showItemInFolder: (path: string) => Promise<void>; showItemInFolder: (path: string) => Promise<void>;
getFeatures: () => Promise<string[]>;
platform: NodeJS.Platform; platform: NodeJS.Platform;
/* Auto update */ /* Auto update */
@@ -206,9 +208,10 @@ declare global {
/* Auth */ /* Auth */
signOut: () => Promise<void>; signOut: () => Promise<void>;
openAuthWindow: () => Promise<void>; openAuthWindow: (page: AuthPage) => Promise<void>;
getSessionHash: () => Promise<string | null>; getSessionHash: () => Promise<string | null>;
onSignIn: (cb: () => void) => () => Electron.IpcRenderer; onSignIn: (cb: () => void) => () => Electron.IpcRenderer;
onAccountUpdated: (cb: () => void) => () => Electron.IpcRenderer;
onSignOut: (cb: () => void) => () => Electron.IpcRenderer; onSignOut: (cb: () => void) => () => Electron.IpcRenderer;
/* User */ /* User */
@@ -257,6 +260,9 @@ declare global {
/* Notifications */ /* Notifications */
publishNewRepacksNotification: (newRepacksCount: number) => Promise<void>; publishNewRepacksNotification: (newRepacksCount: number) => Promise<void>;
/* Editor */
openEditorWindow: () => Promise<void>;
} }
interface Window { interface Window {

View File

@@ -6,3 +6,4 @@ export * from "./redux";
export * from "./use-user-details"; export * from "./use-user-details";
export * from "./use-format"; export * from "./use-format";
export * from "./use-repacks"; export * from "./use-repacks";
export * from "./use-feature";

View File

@@ -0,0 +1,23 @@
import { useEffect } from "react";
enum Feature {
CheckDownloadWritePermission = "CHECK_DOWNLOAD_WRITE_PERMISSION",
}
export function useFeature() {
useEffect(() => {
window.electron.getFeatures().then((features) => {
localStorage.setItem("features", JSON.stringify(features || []));
});
}, []);
const isFeatureEnabled = (feature: Feature) => {
const features = JSON.parse(localStorage.getItem("features") || "[]");
return features.includes(feature);
};
return {
isFeatureEnabled,
Feature,
};
}

View File

@@ -13,6 +13,7 @@ import type {
UpdateProfileRequest, UpdateProfileRequest,
UserDetails, UserDetails,
} from "@types"; } from "@types";
import * as Sentry from "@sentry/react";
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal"; import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
import { isFuture, isToday } from "date-fns"; import { isFuture, isToday } from "date-fns";
@@ -30,6 +31,8 @@ export function useUserDetails() {
} = useAppSelector((state) => state.userDetails); } = useAppSelector((state) => state.userDetails);
const clearUserDetails = useCallback(async () => { const clearUserDetails = useCallback(async () => {
Sentry.setUser(null);
dispatch(setUserDetails(null)); dispatch(setUserDetails(null));
dispatch(setProfileBackground(null)); dispatch(setProfileBackground(null));
@@ -44,6 +47,12 @@ export function useUserDetails() {
const updateUserDetails = useCallback( const updateUserDetails = useCallback(
async (userDetails: UserDetails) => { async (userDetails: UserDetails) => {
Sentry.setUser({
id: userDetails.id,
username: userDetails.username,
email: userDetails.email ?? undefined,
});
dispatch(setUserDetails(userDetails)); dispatch(setUserDetails(userDetails));
window.localStorage.setItem("userDetails", JSON.stringify(userDetails)); window.localStorage.setItem("userDetails", JSON.stringify(userDetails));
}, },

View File

@@ -30,6 +30,7 @@ const Downloads = React.lazy(() => import("./pages/downloads/downloads"));
const Settings = React.lazy(() => import("./pages/settings/settings")); const Settings = React.lazy(() => import("./pages/settings/settings"));
const Catalogue = React.lazy(() => import("./pages/catalogue/catalogue")); const Catalogue = React.lazy(() => import("./pages/catalogue/catalogue"));
const Profile = React.lazy(() => import("./pages/profile/profile")); const Profile = React.lazy(() => import("./pages/profile/profile"));
const Editor = React.lazy(() => import("./pages/editor/editor"));
const Achievements = React.lazy( const Achievements = React.lazy(
() => import("./pages/achievements/achievements") () => import("./pages/achievements/achievements")
); );
@@ -104,6 +105,11 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
element={<SuspenseWrapper Component={Achievements} />} element={<SuspenseWrapper Component={Achievements} />}
/> />
</Route> </Route>
<Route
path="/editor"
element={<SuspenseWrapper Component={Editor} />}
/>
</Routes> </Routes>
</HashRouter> </HashRouter>
</Provider> </Provider>

View File

@@ -1,11 +1,12 @@
import { useDate } from "@renderer/hooks"; import { useDate } from "@renderer/hooks";
import type { UserAchievement } from "@types"; import type { UserAchievement } from "@types";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import * as styles from "./achievements.css";
import { EyeClosedIcon } from "@primer/octicons-react"; import { EyeClosedIcon } from "@primer/octicons-react";
import HydraIcon from "@renderer/assets/icons/hydra.svg?react"; import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
import { useSubscription } from "@renderer/hooks/use-subscription"; import { useSubscription } from "@renderer/hooks/use-subscription";
import { vars } from "@renderer/theme.css"; import classNames from "classnames";
import "./achievements.scss";
import "../../scss/_variables.scss";
interface AchievementListProps { interface AchievementListProps {
achievements: UserAchievement[]; achievements: UserAchievement[];
@@ -17,16 +18,16 @@ export function AchievementList({ achievements }: AchievementListProps) {
const { formatDateTime } = useDate(); const { formatDateTime } = useDate();
return ( return (
<ul className={styles.list}> <ul className="achievements__list">
{achievements.map((achievement) => ( {achievements.map((achievement) => (
<li <li
key={achievement.name} key={achievement.name}
className={styles.listItem} className="achievements__list-item"
style={{ display: "flex" }} style={{ display: "flex" }}
> >
<img <img
className={styles.listItemImage({ className={classNames("achievements__list-item-image", {
unlocked: achievement.unlocked, "achievements__list-item-image--unlocked": achievement.unlocked,
})} })}
src={achievement.icon} src={achievement.icon}
alt={achievement.displayName} alt={achievement.displayName}
@@ -66,7 +67,7 @@ export function AchievementList({ achievements }: AchievementListProps) {
alignItems: "center", alignItems: "center",
gap: "4px", gap: "4px",
cursor: "pointer", cursor: "pointer",
color: vars.color.warning, color: "var(--warning-color)",
}} }}
title={t("achievement_earn_points", { title={t("achievement_earn_points", {
points: "???", points: "???",

View File

@@ -1,71 +0,0 @@
import { style } from "@vanilla-extract/css";
import { recipe } from "@vanilla-extract/recipes";
import { SPACING_UNIT, vars } from "../../theme.css";
export const panel = style({
width: "100%",
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 3}px`,
backgroundColor: vars.color.background,
display: "flex",
flexDirection: "column",
alignItems: "start",
justifyContent: "space-between",
borderBottom: `solid 1px ${vars.color.border}`,
});
export const content = style({
display: "flex",
gap: `${SPACING_UNIT}px`,
justifyContent: "center",
});
export const actions = style({
display: "flex",
gap: `${SPACING_UNIT}px`,
});
export const downloadDetailsRow = style({
gap: `${SPACING_UNIT}px`,
display: "flex",
color: vars.color.body,
alignItems: "center",
});
export const downloadsLink = style({
color: vars.color.body,
textDecoration: "underline",
});
export const progressBar = recipe({
base: {
position: "absolute",
bottom: "0",
left: "0",
width: "100%",
height: "3px",
transition: "all ease 0.2s",
"::-webkit-progress-bar": {
backgroundColor: "transparent",
},
"::-webkit-progress-value": {
backgroundColor: vars.color.muted,
},
},
variants: {
disabled: {
true: {
opacity: vars.opacity.disabled,
},
},
},
});
export const link = style({
textAlign: "start",
color: vars.color.body,
":hover": {
textDecoration: "underline",
cursor: "pointer",
},
});

View File

@@ -0,0 +1,66 @@
@use "../../scss/variables" as vars;
.achievement-panel {
width: 100%;
padding: #{vars.$spacing-unit * 2} #{vars.$spacing-unit * 3};
background-color: vars.$background-color;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: space-between;
border-bottom: solid 1px vars.$border-color;
&__content {
display: flex;
gap: vars.$spacing-unit;
justify-content: center;
}
&__actions {
display: flex;
gap: vars.$spacing-unit;
}
&__download-details-row {
gap: vars.$spacing-unit;
display: flex;
color: vars.$body-color;
align-items: center;
}
&__downloads-link {
color: vars.$body-color;
text-decoration: underline;
}
&__progress-bar {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 3px;
transition: all ease 0.2s;
&::-webkit-progress-bar {
background-color: transparent;
}
&::-webkit-progress-value {
background-color: vars.$muted-color;
}
&--disabled {
opacity: vars.$disabled-opacity;
}
}
&__link {
text-align: start;
color: vars.$body-color;
&:hover {
text-decoration: underline;
cursor: pointer;
}
}
}

View File

@@ -3,8 +3,8 @@ import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
import { UserAchievement } from "@types"; import { UserAchievement } from "@types";
import { useSubscription } from "@renderer/hooks/use-subscription"; import { useSubscription } from "@renderer/hooks/use-subscription";
import { useUserDetails } from "@renderer/hooks"; import { useUserDetails } from "@renderer/hooks";
import { vars } from "@renderer/theme.css";
import * as styles from "./achievement-panel.css"; import "./achievement-panel.scss";
export interface AchievementPanelProps { export interface AchievementPanelProps {
achievements: UserAchievement[]; achievements: UserAchievement[];
@@ -28,17 +28,17 @@ export function AchievementPanel({ achievements }: AchievementPanelProps) {
if (!hasActiveSubscription) { if (!hasActiveSubscription) {
return ( return (
<div className={styles.panel}> <div className="achievement-panel">
<div className={styles.content}> <div className="achievement-panel__content">
{t("earned_points")} <HydraIcon width={20} height={20} /> {t("earned_points")} <HydraIcon width={20} height={20} />
??? / ??? ??? / ???
</div> </div>
<button <button
type="button" type="button"
onClick={() => showHydraCloudModal("achievements-points")} onClick={() => showHydraCloudModal("achievements-points")}
className={styles.link} className="achievement-panel__link"
> >
<small style={{ color: vars.color.warning }}> <small style={{ color: "#ffc107" }}>
{t("how_to_earn_achievements_points")} {t("how_to_earn_achievements_points")}
</small> </small>
</button> </button>
@@ -47,8 +47,8 @@ export function AchievementPanel({ achievements }: AchievementPanelProps) {
} }
return ( return (
<div className={styles.panel}> <div className="achievement-panel">
<div className={styles.content}> <div className="achievement-panel__content">
{t("earned_points")} <HydraIcon width={20} height={20} /> {t("earned_points")} <HydraIcon width={20} height={20} />
{achievementsPointsEarnedSum} / {achievementsPointsTotal} {achievementsPointsEarnedSum} / {achievementsPointsTotal}
</div> </div>

View File

@@ -8,18 +8,19 @@ import {
formatDownloadProgress, formatDownloadProgress,
} from "@renderer/helpers"; } from "@renderer/helpers";
import { LockIcon, PersonIcon, TrophyIcon } from "@primer/octicons-react"; import { LockIcon, PersonIcon, TrophyIcon } from "@primer/octicons-react";
import { SPACING_UNIT, vars } from "@renderer/theme.css";
import { gameDetailsContext } from "@renderer/context"; import { gameDetailsContext } from "@renderer/context";
import type { ComparedAchievements } from "@types"; import type { ComparedAchievements } from "@types";
import { average } from "color.js"; 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 * as styles from "./achievements.css";
import { AchievementList } from "./achievement-list"; import { AchievementList } from "./achievement-list";
import { AchievementPanel } from "./achievement-panel"; import { AchievementPanel } from "./achievement-panel";
import { ComparedAchievementPanel } from "./compared-achievement-panel"; import { ComparedAchievementPanel } from "./compared-achievement-panel";
import { useSubscription } from "@renderer/hooks/use-subscription"; import { useSubscription } from "@renderer/hooks/use-subscription";
import classNames from "classnames";
import "./achievements.scss";
import "../../scss/_variables.scss";
interface UserInfo { interface UserInfo {
id: string; id: string;
@@ -48,10 +49,10 @@ function AchievementSummary({ user, isComparison }: AchievementSummaryProps) {
user: Pick<UserInfo, "profileImageUrl" | "displayName"> user: Pick<UserInfo, "profileImageUrl" | "displayName">
) => { ) => {
return ( return (
<div className={styles.profileAvatar}> <div className="achievements__profile-avatar">
{user.profileImageUrl ? ( {user.profileImageUrl ? (
<img <img
className={styles.profileAvatar} className="achievements__profile-avatar"
src={user.profileImageUrl} src={user.profileImageUrl}
alt={user.displayName} alt={user.displayName}
/> />
@@ -64,97 +65,38 @@ function AchievementSummary({ user, isComparison }: AchievementSummaryProps) {
if (isComparison && userDetails?.id == user.id && !hasActiveSubscription) { if (isComparison && userDetails?.id == user.id && !hasActiveSubscription) {
return ( return (
<div <div className="achievements__summary achievements__summary--locked">
style={{ <div className="achievements__summary-overlay">
display: "flex",
gap: `${SPACING_UNIT * 2}px`,
alignItems: "center",
position: "relative",
padding: `${SPACING_UNIT}px`,
}}
>
<div
style={{
position: "absolute",
zIndex: 2,
inset: 0,
width: "100%",
height: "100%",
background: "rgba(0, 0, 0, 0.7)",
display: "flex",
alignItems: "center",
flexDirection: "row",
gap: `${SPACING_UNIT}px`,
borderRadius: "4px",
justifyContent: "center",
}}
>
<LockIcon size={24} /> <LockIcon size={24} />
<h3> <h3>
<button <button
className={styles.subscriptionRequiredButton} className="achievements__subscription-required-button"
onClick={() => showHydraCloudModal("achievements")} onClick={() => showHydraCloudModal("achievements")}
> >
{t("subscription_needed")} {t("subscription_needed")}
</button> </button>
</h3> </h3>
</div> </div>
<div <div className="achievements__summary-content achievements__summary-content--blurred">
style={{
display: "flex",
gap: `${SPACING_UNIT * 2}px`,
alignItems: "center",
height: "62px",
position: "relative",
filter: "blur(4px)",
}}
>
{getProfileImage(user)} {getProfileImage(user)}
<h1 style={{ marginBottom: "8px" }}>{user.displayName}</h1> <h1 className="achievements__summary-title">{user.displayName}</h1>
</div> </div>
</div> </div>
); );
} }
return ( return (
<div <div className="achievements__summary">
style={{
display: "flex",
gap: `${SPACING_UNIT * 2}px`,
alignItems: "center",
padding: `${SPACING_UNIT}px`,
}}
>
{getProfileImage(user)} {getProfileImage(user)}
<div <div className="achievements__summary-details">
style={{ <h1 className="achievements__summary-title">{user.displayName}</h1>
display: "flex", <div className="achievements__summary-stats">
flexDirection: "column", <div className="achievements__summary-count">
width: "100%",
}}
>
<h1 style={{ marginBottom: "8px" }}>{user.displayName}</h1>
<div
style={{
display: "flex",
justifyContent: "space-between",
marginBottom: 8,
color: vars.color.muted,
}}
>
<div
style={{
display: "flex",
alignItems: "center",
gap: 8,
}}
>
<TrophyIcon size={13} /> <TrophyIcon size={13} />
<span> <span>
{user.unlockedAchievementCount} / {user.totalAchievementCount} {user.unlockedAchievementCount} / {user.totalAchievementCount}
</span> </span>
</div> </div>
<span> <span>
{formatDownloadProgress( {formatDownloadProgress(
user.unlockedAchievementCount / user.totalAchievementCount user.unlockedAchievementCount / user.totalAchievementCount
@@ -164,7 +106,7 @@ function AchievementSummary({ user, isComparison }: AchievementSummaryProps) {
<progress <progress
max={1} max={1}
value={user.unlockedAchievementCount / user.totalAchievementCount} value={user.unlockedAchievementCount / user.totalAchievementCount}
className={styles.achievementsProgressBar} className="achievements__progress-bar"
/> />
</div> </div>
</div> </div>
@@ -201,9 +143,10 @@ export function AchievementsContent({
setGameColor(backgroundColor); setGameColor(backgroundColor);
}; };
const HERO_HEIGHT = 150;
const onScroll: React.UIEventHandler<HTMLElement> = (event) => { const onScroll: React.UIEventHandler<HTMLElement> = (event) => {
const heroHeight = heroRef.current?.clientHeight ?? styles.HERO_HEIGHT; const heroHeight = heroRef.current?.clientHeight ?? HERO_HEIGHT;
const scrollY = (event.target as HTMLDivElement).scrollTop; const scrollY = (event.target as HTMLDivElement).scrollTop;
if (scrollY >= heroHeight && !isHeaderStuck) { if (scrollY >= heroHeight && !isHeaderStuck) {
@@ -219,10 +162,10 @@ export function AchievementsContent({
user: Pick<UserInfo, "profileImageUrl" | "displayName"> user: Pick<UserInfo, "profileImageUrl" | "displayName">
) => { ) => {
return ( return (
<div className={styles.profileAvatarSmall}> <div className="achievements__profile-avatar-small">
{user.profileImageUrl ? ( {user.profileImageUrl ? (
<img <img
className={styles.profileAvatarSmall} className="achievements__profile-avatar-small"
src={user.profileImageUrl} src={user.profileImageUrl}
alt={user.displayName} alt={user.displayName}
/> />
@@ -236,10 +179,10 @@ export function AchievementsContent({
if (!objectId || !shop || !gameTitle || !userDetails) return null; if (!objectId || !shop || !gameTitle || !userDetails) return null;
return ( return (
<div className={styles.wrapper}> <div className="achievements__wrapper">
<img <img
src={steamUrlBuilder.libraryHero(objectId)} src={steamUrlBuilder.libraryHero(objectId)}
style={{ display: "none" }} className="achievements__hidden-image"
alt={gameTitle} alt={gameTitle}
onLoad={handleHeroLoad} onLoad={handleHeroLoad}
/> />
@@ -247,38 +190,29 @@ export function AchievementsContent({
<section <section
ref={containerRef} ref={containerRef}
onScroll={onScroll} onScroll={onScroll}
className={styles.container} className="achievements__container"
> >
<div <div
className="achievements__gradient-background"
style={{ style={{
display: "flex", background: `linear-gradient(0deg, #1c1c1c 0%, ${gameColor} 100%)`,
flexDirection: "column",
background: `linear-gradient(0deg, ${vars.color.darkBackground} 0%, ${gameColor} 100%)`,
}} }}
> >
<div ref={heroRef} className={styles.hero}> <div ref={heroRef} className="achievements__hero">
<div className={styles.heroContent}> <div className="achievements__hero-content">
<Link <Link
to={buildGameDetailsPath({ shop, objectId, title: gameTitle })} to={buildGameDetailsPath({ shop, objectId, title: gameTitle })}
> >
<img <img
src={steamUrlBuilder.logo(objectId)} src={steamUrlBuilder.logo(objectId)}
className={styles.gameLogo} className="achievements__game-logo"
alt={gameTitle} alt={gameTitle}
/> />
</Link> </Link>
</div> </div>
</div> </div>
<div <div className="achievements__summary-container">
style={{
display: "flex",
flexDirection: "column",
width: "100%",
gap: `${SPACING_UNIT}px`,
padding: `${SPACING_UNIT}px`,
}}
>
<AchievementSummary <AchievementSummary
user={{ user={{
...userDetails, ...userDetails,
@@ -298,24 +232,24 @@ export function AchievementsContent({
</div> </div>
{otherUser && ( {otherUser && (
<div className={styles.tableHeader({ stuck: isHeaderStuck })}> <div
className={classNames("achievements__table-header", {
"achievements__table-header--stuck": isHeaderStuck,
})}
>
<div <div
style={{ className={classNames("achievements__grid-container", {
display: "grid", "achievements__grid-container--no-subscription":
gridTemplateColumns: hasActiveSubscription !hasActiveSubscription,
? "3fr 1fr 1fr" })}
: "3fr 2fr",
gap: `${SPACING_UNIT * 2}px`,
padding: `${SPACING_UNIT}px ${SPACING_UNIT * 3}px`,
}}
> >
<div></div> <div></div>
{hasActiveSubscription && ( {hasActiveSubscription && (
<div style={{ display: "flex", justifyContent: "center" }}> <div className="achievements__profile-center">
{getProfileImage({ ...userDetails })} {getProfileImage({ ...userDetails })}
</div> </div>
)} )}
<div style={{ display: "flex", justifyContent: "center" }}> <div className="achievements__profile-center">
{getProfileImage(otherUser)} {getProfileImage(otherUser)}
</div> </div>
</div> </div>

View File

@@ -1,13 +1,13 @@
import Skeleton from "react-loading-skeleton"; import Skeleton from "react-loading-skeleton";
import * as styles from "./achievements.css"; import "./achievements.scss";
export function AchievementsSkeleton() { export function AchievementsSkeleton() {
return ( return (
<div className={styles.container}> <div className="achievements__container">
<div className={styles.hero}> <div className="achievements__hero">
<Skeleton className={styles.heroImageSkeleton} /> <Skeleton className="achievements__hero-image-skeleton" />
</div> </div>
<div className={styles.heroPanelSkeleton}></div> <div className="achievements__hero-panel-skeleton"></div>
</div> </div>
); );
} }

View File

@@ -1,197 +0,0 @@
import { SPACING_UNIT, vars } from "../../theme.css";
import { style } from "@vanilla-extract/css";
import { recipe } from "@vanilla-extract/recipes";
export const HERO_HEIGHT = 150;
const LOGO_HEIGHT = 100;
const LOGO_MAX_WIDTH = 200;
export const wrapper = style({
display: "flex",
flexDirection: "column",
overflow: "hidden",
width: "100%",
height: "100%",
transition: "all ease 0.3s",
});
export const hero = style({
width: "100%",
height: `${HERO_HEIGHT}px`,
minHeight: `${HERO_HEIGHT}px`,
display: "flex",
flexDirection: "column",
position: "relative",
transition: "all ease 0.2s",
});
export const heroContent = style({
padding: `${SPACING_UNIT * 2}px`,
width: "100%",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
});
export const gameLogo = style({
width: LOGO_MAX_WIDTH,
height: LOGO_HEIGHT,
objectFit: "contain",
transition: "all ease 0.2s",
":hover": {
transform: "scale(1.05)",
},
});
export const container = style({
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
overflow: "auto",
zIndex: "1",
});
export const tableHeader = recipe({
base: {
width: "100%",
backgroundColor: vars.color.darkBackground,
transition: "all ease 0.2s",
borderBottom: `solid 1px ${vars.color.border}`,
position: "sticky",
top: "0",
zIndex: "1",
},
variants: {
stuck: {
true: {
boxShadow: "0px 0px 15px 0px rgba(0, 0, 0, 0.8)",
},
},
},
});
export const list = style({
listStyle: "none",
margin: "0",
display: "flex",
flexDirection: "column",
gap: `${SPACING_UNIT * 2}px`,
padding: `${SPACING_UNIT * 2}px`,
width: "100%",
backgroundColor: vars.color.background,
});
export const listItem = style({
transition: "all ease 0.1s",
color: vars.color.muted,
width: "100%",
overflow: "hidden",
borderRadius: "4px",
padding: `${SPACING_UNIT}px ${SPACING_UNIT}px`,
gap: `${SPACING_UNIT * 2}px`,
alignItems: "center",
textAlign: "left",
":hover": {
backgroundColor: "rgba(255, 255, 255, 0.15)",
textDecoration: "none",
},
});
export const listItemImage = recipe({
base: {
width: "54px",
height: "54px",
borderRadius: "4px",
objectFit: "cover",
},
variants: {
unlocked: {
false: {
filter: "grayscale(100%)",
},
},
},
});
export const achievementsProgressBar = style({
width: "100%",
height: "8px",
transition: "all ease 0.2s",
"::-webkit-progress-bar": {
backgroundColor: "rgba(255, 255, 255, 0.15)",
borderRadius: "4px",
},
"::-webkit-progress-value": {
backgroundColor: vars.color.muted,
borderRadius: "4px",
},
});
export const heroLogoBackdrop = style({
width: "100%",
height: "100%",
position: "absolute",
display: "flex",
flexDirection: "column",
justifyContent: "flex-end",
});
export const heroImageSkeleton = style({
height: "150px",
});
export const heroPanelSkeleton = style({
width: "100%",
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 2}px`,
display: "flex",
alignItems: "center",
backgroundColor: vars.color.background,
height: "72px",
borderBottom: `solid 1px ${vars.color.border}`,
});
export const listItemSkeleton = style({
width: "100%",
overflow: "hidden",
borderRadius: "4px",
padding: `${SPACING_UNIT}px ${SPACING_UNIT}px`,
gap: `${SPACING_UNIT * 2}px`,
});
export const profileAvatar = style({
height: "54px",
width: "54px",
borderRadius: "4px",
display: "flex",
justifyContent: "center",
alignItems: "center",
backgroundColor: vars.color.background,
position: "relative",
objectFit: "cover",
});
export const profileAvatarSmall = style({
height: "32px",
width: "32px",
borderRadius: "4px",
display: "flex",
justifyContent: "center",
alignItems: "center",
backgroundColor: vars.color.background,
position: "relative",
objectFit: "cover",
});
export const subscriptionRequiredButton = style({
textDecoration: "none",
display: "flex",
justifyContent: "center",
width: "100%",
gap: `${SPACING_UNIT / 2}px`,
color: vars.color.body,
cursor: "pointer",
":hover": {
textDecoration: "underline",
},
});

View File

@@ -0,0 +1,188 @@
@use "../../scss/variables" as vars;
.achievements {
&__wrapper {
display: flex;
flex-direction: column;
overflow: hidden;
width: 100%;
height: 100%;
transition: all ease 0.3s;
}
&__hero {
width: 100%;
height: vars.$hero-sub-height;
min-height: vars.$hero-sub-height;
display: flex;
flex-direction: column;
position: relative;
transition: all ease 0.2s;
}
&__hero-content {
padding: #{vars.$spacing-unit * 2};
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
}
&__game-logo {
width: vars.$logo-max-width;
height: vars.$logo-height;
object-fit: contain;
transition: all ease 0.2s;
&:hover {
transform: scale(1.05);
}
}
&__container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
overflow: auto;
z-index: 1;
}
&__table-header {
width: 100%;
background-color: vars.$dark-background-color;
transition: all ease 0.2s;
border-bottom: solid 1px vars.$border-color;
position: sticky;
top: 0;
z-index: 1;
&--stuck {
box-shadow: 0px 0px 15px 0px rgba(0, 0, 0, 0.8);
}
}
&__list {
list-style: none;
margin: 0;
display: flex;
flex-direction: column;
gap: #{vars.$spacing-unit * 2};
padding: #{vars.$spacing-unit * 2};
width: 100%;
background-color: vars.$background-color;
}
&__list-item {
transition: all ease 0.1s;
color: vars.$muted-color;
width: 100%;
overflow: hidden;
border-radius: 4px;
padding: vars.$spacing-unit;
gap: #{vars.$spacing-unit * 2};
align-items: center;
text-align: left;
&:hover {
background-color: rgba(255, 255, 255, 0.15);
text-decoration: none;
}
}
&__list-item-image {
width: 54px;
height: 54px;
border-radius: 4px;
object-fit: cover;
&--unlocked {
filter: grayscale(100%);
}
}
&__achievements-progress-bar {
width: 100%;
height: 8px;
transition: all ease 0.2s;
&::-webkit-progress-bar {
background-color: rgba(255, 255, 255, 0.15);
border-radius: 4px;
}
&::-webkit-progress-value {
background-color: vars.$muted-color;
border-radius: 4px;
}
}
&__hero-logo-backdrop {
width: 100%;
height: 100%;
position: absolute;
display: flex;
flex-direction: column;
justify-content: flex-end;
}
&__hero-image-skeleton {
height: 150px;
}
&__hero-panel-skeleton {
width: 100%;
padding: #{vars.$spacing-unit * 2};
display: flex;
align-items: center;
background-color: vars.$background-color;
height: 72px;
border-bottom: solid 1px vars.$border-color;
}
&__list-item-skeleton {
width: 100%;
overflow: hidden;
border-radius: 4px;
padding: vars.$spacing-unit;
gap: #{vars.$spacing-unit * 2};
}
&__profile-avatar {
height: 54px;
width: 54px;
border-radius: 4px;
display: flex;
justify-content: center;
align-items: center;
background-color: vars.$background-color;
position: relative;
object-fit: cover;
}
&__profile-avatar-small {
height: 32px;
width: 32px;
border-radius: 4px;
display: flex;
justify-content: center;
align-items: center;
background-color: vars.$background-color;
position: relative;
object-fit: cover;
}
&__subscription-required-button {
text-decoration: none;
display: flex;
justify-content: center;
width: 100%;
gap: calc(vars.$spacing-unit / 2);
color: vars.$body-color;
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
}

View File

@@ -3,7 +3,8 @@ import { useAppDispatch, useUserDetails } from "@renderer/hooks";
import type { ComparedAchievements, GameShop } from "@types"; import type { ComparedAchievements, GameShop } from "@types";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { useSearchParams } from "react-router-dom"; import { useSearchParams } from "react-router-dom";
import { vars } from "@renderer/theme.css";
import "./achievements.scss";
import { import {
GameDetailsContextConsumer, GameDetailsContextConsumer,
GameDetailsContextProvider, GameDetailsContextProvider,
@@ -75,10 +76,7 @@ export default function Achievements() {
(otherUserId && comparedAchievements === null); (otherUserId && comparedAchievements === null);
return ( return (
<SkeletonTheme <SkeletonTheme baseColor="var(--background-color)" highlightColor="#444">
baseColor={vars.color.background}
highlightColor="#444"
>
{showSkeleton ? ( {showSkeleton ? (
<AchievementsSkeleton /> <AchievementsSkeleton />
) : ( ) : (

View File

@@ -1,13 +1,14 @@
import type { ComparedAchievements } from "@types"; import type { ComparedAchievements } from "@types";
import * as styles from "./achievements.css";
import { import {
CheckCircleIcon, CheckCircleIcon,
EyeClosedIcon, EyeClosedIcon,
LockIcon, LockIcon,
} from "@primer/octicons-react"; } from "@primer/octicons-react";
import { useDate } from "@renderer/hooks"; import { useDate } from "@renderer/hooks";
import { SPACING_UNIT } from "@renderer/theme.css";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import classNames from "classnames";
import "./achievements.scss";
import "../../scss/_variables.scss";
export interface ComparedAchievementListProps { export interface ComparedAchievementListProps {
achievements: ComparedAchievements; achievements: ComparedAchievements;
@@ -20,11 +21,11 @@ export function ComparedAchievementList({
const { formatDateTime } = useDate(); const { formatDateTime } = useDate();
return ( return (
<ul className={styles.list}> <ul className="achievements__list">
{achievements.achievements.map((achievement, index) => ( {achievements.achievements.map((achievement, index) => (
<li <li
key={index} key={index}
className={styles.listItem} className="achievements__list-item"
style={{ style={{
display: "grid", display: "grid",
gridTemplateColumns: achievement.ownerStat gridTemplateColumns: achievement.ownerStat
@@ -37,12 +38,14 @@ export function ComparedAchievementList({
display: "flex", display: "flex",
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
gap: `${SPACING_UNIT}px`, gap: `var(--spacing-unit)`,
}} }}
> >
<img <img
className={styles.listItemImage({ className={classNames("achievements__list-item-image", {
unlocked: true, "achievements__list-item-image--unlocked":
achievement.ownerStat?.unlocked ||
achievement.targetStat.unlocked,
})} })}
src={achievement.icon} src={achievement.icon}
alt={achievement.displayName} alt={achievement.displayName}
@@ -71,7 +74,7 @@ export function ComparedAchievementList({
whiteSpace: "nowrap", whiteSpace: "nowrap",
display: "flex", display: "flex",
flexDirection: "row", flexDirection: "row",
gap: `${SPACING_UNIT}px`, gap: `var(--spacing-unit)`,
justifyContent: "center", justifyContent: "center",
}} }}
title={formatDateTime(achievement.ownerStat.unlockTime!)} title={formatDateTime(achievement.ownerStat.unlockTime!)}
@@ -82,7 +85,7 @@ export function ComparedAchievementList({
<div <div
style={{ style={{
display: "flex", display: "flex",
padding: `${SPACING_UNIT}px`, padding: `var(--spacing-unit)`,
justifyContent: "center", justifyContent: "center",
}} }}
> >
@@ -97,7 +100,7 @@ export function ComparedAchievementList({
whiteSpace: "nowrap", whiteSpace: "nowrap",
display: "flex", display: "flex",
flexDirection: "row", flexDirection: "row",
gap: `${SPACING_UNIT}px`, gap: `var(--spacing-unit)`,
justifyContent: "center", justifyContent: "center",
}} }}
title={formatDateTime(achievement.targetStat.unlockTime!)} title={formatDateTime(achievement.targetStat.unlockTime!)}
@@ -108,7 +111,7 @@ export function ComparedAchievementList({
<div <div
style={{ style={{
display: "flex", display: "flex",
padding: `${SPACING_UNIT}px`, padding: `var(--spacing-unit)`,
justifyContent: "center", justifyContent: "center",
}} }}
> >

View File

@@ -1,10 +1,12 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import * as styles from "./achievement-panel.css";
import HydraIcon from "@renderer/assets/icons/hydra.svg?react"; import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
import { ComparedAchievements } from "@types"; import { ComparedAchievements } from "@types";
import { SPACING_UNIT } from "@renderer/theme.css";
import { useUserDetails } from "@renderer/hooks"; import { useUserDetails } from "@renderer/hooks";
import classNames from "classnames";
import "./achievement-panel.scss";
import "../../scss/_variables.scss";
export interface ComparedAchievementPanelProps { export interface ComparedAchievementPanelProps {
achievements: ComparedAchievements; achievements: ComparedAchievements;
@@ -18,24 +20,24 @@ export function ComparedAchievementPanel({
return ( return (
<div <div
className={styles.panel} className={classNames("achievement-panel", {
style={{ "achievement-panel--subscribed": hasActiveSubscription,
display: "grid", })}
gridTemplateColumns: hasActiveSubscription ? "3fr 1fr 1fr" : "3fr 2fr",
gap: `${SPACING_UNIT * 2}px`,
}}
> >
<div style={{ display: "flex", gap: `${SPACING_UNIT}px` }}> <div className="achievement-panel__points">
{t("available_points")} <HydraIcon width={20} height={20} />{" "} {t("available_points")}
<HydraIcon width={20} height={20} />
{achievements.achievementsPointsTotal} {achievements.achievementsPointsTotal}
</div> </div>
{hasActiveSubscription && ( {hasActiveSubscription && (
<div className={styles.content}> <div className="achievement-panel__content">
<HydraIcon width={20} height={20} /> <HydraIcon width={20} height={20} />
{achievements.owner.achievementsPointsEarnedSum ?? 0} {achievements.owner.achievementsPointsEarnedSum ?? 0}
</div> </div>
)} )}
<div className={styles.content}>
<div className="achievement-panel__content">
<HydraIcon width={20} height={20} /> <HydraIcon width={20} height={20} />
{achievements.target.achievementsPointsEarnedSum} {achievements.target.achievementsPointsEarnedSum}
</div> </div>

View File

@@ -1,10 +1,10 @@
@use "../../scss/globals.scss"; @use "../../scss/variables" as vars;
.catalogue { .catalogue {
overflow-y: auto; overflow-y: auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: calc(globals.$spacing-unit * 2); gap: calc(vars.$spacing-unit * 2);
width: 100%; width: 100%;
padding: 16px; padding: 16px;
scroll-behavior: smooth; scroll-behavior: smooth;
@@ -13,10 +13,16 @@
width: 270px; width: 270px;
min-width: 270px; min-width: 270px;
max-width: 270px; max-width: 270px;
background-color: globals.$dark-background-color; background-color: vars.$dark-background-color;
border-radius: 4px; border-radius: 4px;
padding: 16px; padding: 16px;
border: 1px solid globals.$border-color; border: 1px solid vars.$border-color;
align-self: flex-start; align-self: flex-start;
} }
&__header {
display: flex;
gap: calc(var(--spacing-unit) * 2);
justify-content: space-between;
}
} }

View File

@@ -9,8 +9,8 @@ import {
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import "./catalogue.scss"; import "./catalogue.scss";
import "../../scss/_variables.scss";
import { SPACING_UNIT, vars } from "@renderer/theme.css";
import { downloadSourcesTable } from "@renderer/dexie"; import { downloadSourcesTable } from "@renderer/dexie";
import { FilterSection } from "./filter-section"; import { FilterSection } from "./filter-section";
import { setFilters, setPage } from "@renderer/features"; import { setFilters, setPage } from "@renderer/features";
@@ -270,13 +270,7 @@ export default function Catalogue() {
</div> </div>
</div> </div>
<div <div className="catalogue__header">
style={{
display: "flex",
gap: SPACING_UNIT * 2,
justifyContent: "space-between",
}}
>
<div <div
style={{ style={{
display: "flex", display: "flex",
@@ -287,8 +281,8 @@ export default function Catalogue() {
> >
{isLoading ? ( {isLoading ? (
<SkeletonTheme <SkeletonTheme
baseColor={vars.color.darkBackground} baseColor="var(--dark-background-color)"
highlightColor={vars.color.background} highlightColor="var(--background-color)"
> >
{Array.from({ length: PAGE_SIZE }).map((_, i) => ( {Array.from({ length: PAGE_SIZE }).map((_, i) => (
<Skeleton <Skeleton
@@ -296,7 +290,7 @@ export default function Catalogue() {
style={{ style={{
height: 105, height: 105,
borderRadius: 4, borderRadius: 4,
border: `solid 1px ${vars.color.border}`, border: `solid 1px var(--border-color)`,
}} }}
/> />
))} ))}

View File

@@ -1,5 +1,5 @@
import { vars } from "@renderer/theme.css";
import { XIcon } from "@primer/octicons-react"; import { XIcon } from "@primer/octicons-react";
import "../../scss/_variables.scss";
interface FilterItemProps { interface FilterItemProps {
filter: string; filter: string;
@@ -13,11 +13,11 @@ export function FilterItem({ filter, orbColor, onRemove }: FilterItemProps) {
style={{ style={{
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
color: vars.color.body, color: "var(--body-color)",
backgroundColor: vars.color.darkBackground, backgroundColor: "var(--dark-background-color",
padding: "6px 12px", padding: "6px 12px",
borderRadius: 4, borderRadius: 4,
border: `solid 1px ${vars.color.border}`, border: `solid 1px var(--border-color)`,
fontSize: 12, fontSize: 12,
}} }}
> >
@@ -35,7 +35,7 @@ export function FilterItem({ filter, orbColor, onRemove }: FilterItemProps) {
type="button" type="button"
onClick={onRemove} onClick={onRemove}
style={{ style={{
color: vars.color.body, color: "var(--body-color)",
marginLeft: 4, marginLeft: 4,
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",

View File

@@ -3,8 +3,8 @@ import { useFormat } from "@renderer/hooks";
import { useCallback, useMemo, useState } from "react"; import { useCallback, useMemo, useState } from "react";
import List from "rc-virtual-list"; import List from "rc-virtual-list";
import { vars } from "@renderer/theme.css";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import "../../scss/_variables.scss";
export interface FilterSectionProps { export interface FilterSectionProps {
title: string; title: string;
@@ -80,7 +80,7 @@ export function FilterSection({
fontSize: 12, fontSize: 12,
marginBottom: 12, marginBottom: 12,
display: "block", display: "block",
color: vars.color.body, color: "var(--body-color)",
cursor: "pointer", cursor: "pointer",
textDecoration: "underline", textDecoration: "underline",
}} }}

View File

@@ -1,7 +1,7 @@
@use "../../scss/globals.scss"; @use "../../scss/variables" as vars;
.game-item { .game-item {
background-color: globals.$dark-background-color; background-color: vars.$dark-background-color;
width: 100%; width: 100%;
color: #fff; color: #fff;
display: flex; display: flex;
@@ -9,9 +9,9 @@
overflow: hidden; overflow: hidden;
position: relative; position: relative;
border-radius: 4px; border-radius: 4px;
border: 1px solid globals.$border-color; border: 1px solid vars.$border-color;
cursor: pointer; cursor: pointer;
gap: calc(globals.$spacing-unit * 2); gap: calc(vars.$spacing-unit * 2);
transition: all ease 0.2s; transition: all ease 0.2s;
&:hover { &:hover {
@@ -22,7 +22,7 @@
width: 200px; width: 200px;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
border-right: 1px solid globals.$border-color; border-right: 1px solid vars.$border-color;
} }
&__details { &__details {
@@ -30,11 +30,11 @@
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
gap: 4px; gap: 4px;
padding: calc(globals.$spacing-unit * 2) 0; padding: calc(vars.$spacing-unit * 2) 0;
} }
&__genres { &__genres {
color: globals.$body-color; color: vars.$body-color;
font-size: 12px; font-size: 12px;
text-align: left; text-align: left;
margin-bottom: 4px; margin-bottom: 4px;
@@ -42,7 +42,7 @@
&__repackers { &__repackers {
display: flex; display: flex;
gap: globals.$spacing-unit; gap: vars.$spacing-unit;
flex-wrap: wrap; flex-wrap: wrap;
} }
} }

View File

@@ -31,11 +31,11 @@ export function GameItem({ game }: GameItemProps) {
const genres = useMemo(() => { const genres = useMemo(() => {
return game.genres?.map((genre) => { return game.genres?.map((genre) => {
const index = steamGenres["en"].findIndex( const index = steamGenres["en"]?.findIndex(
(steamGenre) => steamGenre === genre (steamGenre) => steamGenre === genre
); );
if (steamGenres[language] && steamGenres[language][index]) { if (index && steamGenres[language] && steamGenres[language][index]) {
return steamGenres[language][index]; return steamGenres[language][index];
} }

View File

@@ -1,11 +0,0 @@
import { style } from "@vanilla-extract/css";
import { SPACING_UNIT } from "../../theme.css";
export const deleteActionsButtonsCtn = style({
display: "flex",
width: "100%",
justifyContent: "end",
alignItems: "center",
gap: `${SPACING_UNIT}px`,
});

Some files were not shown because too many files have changed in this diff Show More