mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-12 06:16:17 +00:00
Compare commits
12 Commits
feature/mi
...
fix/using-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
decacfa1d9 | ||
|
|
7c7f621d95 | ||
|
|
032293b339 | ||
|
|
f6eedde976 | ||
|
|
18c6994df6 | ||
|
|
239b29687c | ||
|
|
dfc2dd1c8b | ||
|
|
954037b826 | ||
|
|
09c1170407 | ||
|
|
89f1ce5ead | ||
|
|
392279c4e1 | ||
|
|
8fbe23e61c |
@@ -125,6 +125,10 @@ cd hydra
|
||||
yarn
|
||||
```
|
||||
|
||||
### <a name="install-openssl-11"></a> Instale OpenSSL 1.1
|
||||
|
||||
[OpenSSL 1.1](https://slproweb.com/download/Win64OpenSSL-1_1_1w.exe) é exigido pelo libtorrent em ambientes Windows.
|
||||
|
||||
### <a name="install-python-39"></a> Instale Python 3.9
|
||||
|
||||
Certifique-se de ter o Python 3.9 instalado em sua máquina. Você pode baixá-lo e instalá-lo em [python.org](https://www.python.org/downloads/release/python-3913/).
|
||||
|
||||
@@ -36,7 +36,6 @@
|
||||
"@electron-toolkit/utils": "^3.0.0",
|
||||
"@fontsource/noto-sans": "^5.1.0",
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@primer/octicons-react": "^19.9.0",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
||||
"@reduxjs/toolkit": "^2.2.3",
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
from flask import Flask, request, jsonify
|
||||
import sys, json, urllib.parse, psutil
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
import json
|
||||
import urllib.parse
|
||||
import sys
|
||||
import psutil
|
||||
from torrent_downloader import TorrentDownloader
|
||||
from http_downloader import HttpDownloader
|
||||
from profile_image_processor import ProfileImageProcessor
|
||||
import libtorrent as lt
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# Retrieve command line arguments
|
||||
torrent_port = sys.argv[1]
|
||||
http_port = sys.argv[2]
|
||||
http_port = int(sys.argv[2])
|
||||
rpc_password = sys.argv[3]
|
||||
start_download_payload = sys.argv[4]
|
||||
start_seeding_payload = sys.argv[5]
|
||||
|
||||
downloads = {}
|
||||
# This can be streamed down from Node
|
||||
downloading_game_id = -1
|
||||
|
||||
torrent_session = lt.session({'listen_interfaces': '0.0.0.0:{port}'.format(port=torrent_port)})
|
||||
torrent_session = lt.session({'listen_interfaces': f'0.0.0.0:{torrent_port}'})
|
||||
|
||||
if start_download_payload:
|
||||
initial_download = json.loads(urllib.parse.unquote(start_download_payload))
|
||||
downloading_game_id = initial_download['game_id']
|
||||
|
||||
|
||||
if initial_download['url'].startswith('magnet'):
|
||||
torrent_downloader = TorrentDownloader(torrent_session)
|
||||
downloads[initial_download['game_id']] = torrent_downloader
|
||||
@@ -49,135 +49,142 @@ if start_seeding_payload:
|
||||
except Exception as e:
|
||||
print("Error starting seeding", e)
|
||||
|
||||
def validate_rpc_password():
|
||||
"""Middleware to validate RPC password."""
|
||||
header_password = request.headers.get('x-hydra-rpc-password')
|
||||
if header_password != rpc_password:
|
||||
return jsonify({"error": "Unauthorized"}), 401
|
||||
class RequestHandler(BaseHTTPRequestHandler):
|
||||
def validate_rpc_password(self):
|
||||
header_password = self.headers.get('x-hydra-rpc-password')
|
||||
if header_password != rpc_password:
|
||||
self.send_response(401)
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({"error": "Unauthorized"}).encode('utf-8'))
|
||||
return False
|
||||
return True
|
||||
|
||||
@app.route("/status", methods=["GET"])
|
||||
def status():
|
||||
auth_error = validate_rpc_password()
|
||||
if auth_error:
|
||||
return auth_error
|
||||
def do_GET(self):
|
||||
if self.path == "/status":
|
||||
if not self.validate_rpc_password():
|
||||
return
|
||||
|
||||
downloader = downloads.get(downloading_game_id)
|
||||
if downloader:
|
||||
status = downloads.get(downloading_game_id).get_download_status()
|
||||
return jsonify(status), 200
|
||||
else:
|
||||
return jsonify(None)
|
||||
|
||||
@app.route("/seed-status", methods=["GET"])
|
||||
def seed_status():
|
||||
auth_error = validate_rpc_password()
|
||||
if auth_error:
|
||||
return auth_error
|
||||
|
||||
seed_status = []
|
||||
|
||||
for game_id, downloader in downloads.items():
|
||||
if not downloader:
|
||||
continue
|
||||
|
||||
response = downloader.get_download_status()
|
||||
if response is None:
|
||||
continue
|
||||
|
||||
if response.get('status') == 5:
|
||||
seed_status.append({
|
||||
'gameId': game_id,
|
||||
**response,
|
||||
})
|
||||
|
||||
return jsonify(seed_status), 200
|
||||
|
||||
@app.route("/healthcheck", methods=["GET"])
|
||||
def healthcheck():
|
||||
return "", 200
|
||||
|
||||
@app.route("/process-list", methods=["GET"])
|
||||
def process_list():
|
||||
auth_error = validate_rpc_password()
|
||||
if auth_error:
|
||||
return auth_error
|
||||
|
||||
process_list = [proc.info for proc in psutil.process_iter(['exe', 'pid', 'name'])]
|
||||
return jsonify(process_list), 200
|
||||
|
||||
@app.route("/profile-image", methods=["POST"])
|
||||
def profile_image():
|
||||
auth_error = validate_rpc_password()
|
||||
if auth_error:
|
||||
return auth_error
|
||||
|
||||
data = request.get_json()
|
||||
image_path = data.get('image_path')
|
||||
|
||||
try:
|
||||
processed_image_path, mime_type = ProfileImageProcessor.process_image(image_path)
|
||||
return jsonify({'imagePath': processed_image_path, 'mimeType': mime_type}), 200
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 400
|
||||
|
||||
@app.route("/action", methods=["POST"])
|
||||
def action():
|
||||
global torrent_session
|
||||
global downloading_game_id
|
||||
|
||||
auth_error = validate_rpc_password()
|
||||
if auth_error:
|
||||
return auth_error
|
||||
|
||||
data = request.get_json()
|
||||
action = data.get('action')
|
||||
game_id = data.get('game_id')
|
||||
|
||||
if action == 'start':
|
||||
url = data.get('url')
|
||||
|
||||
existing_downloader = downloads.get(game_id)
|
||||
|
||||
if url.startswith('magnet'):
|
||||
if existing_downloader and isinstance(existing_downloader, TorrentDownloader):
|
||||
existing_downloader.start_download(url, data['save_path'], "")
|
||||
downloader = downloads.get(downloading_game_id)
|
||||
if downloader:
|
||||
status = downloader.get_download_status()
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps(status).encode('utf-8'))
|
||||
else:
|
||||
torrent_downloader = TorrentDownloader(torrent_session)
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps(None).encode('utf-8'))
|
||||
|
||||
elif self.path == "/seed-status":
|
||||
if not self.validate_rpc_password():
|
||||
return
|
||||
|
||||
seed_status = []
|
||||
for game_id, downloader in downloads.items():
|
||||
if not downloader:
|
||||
continue
|
||||
|
||||
response = downloader.get_download_status()
|
||||
if response is None:
|
||||
continue
|
||||
|
||||
if response.get('status') == 5:
|
||||
seed_status.append({
|
||||
'gameId': game_id,
|
||||
**response,
|
||||
})
|
||||
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps(seed_status).encode('utf-8'))
|
||||
|
||||
elif self.path == "/healthcheck":
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
|
||||
elif self.path == "/process-list":
|
||||
if not self.validate_rpc_password():
|
||||
return
|
||||
|
||||
process_list = [proc.info for proc in psutil.process_iter(['exe', 'pid', 'name'])]
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps(process_list).encode('utf-8'))
|
||||
|
||||
def do_POST(self):
|
||||
if not self.validate_rpc_password():
|
||||
return
|
||||
|
||||
content_length = int(self.headers['Content-Length'])
|
||||
post_data = self.rfile.read(content_length)
|
||||
data = json.loads(post_data.decode('utf-8'))
|
||||
|
||||
if self.path == "/profile-image":
|
||||
image_path = data.get('image_path')
|
||||
try:
|
||||
processed_image_path, mime_type = ProfileImageProcessor.process_image(image_path)
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({'imagePath': processed_image_path, 'mimeType': mime_type}).encode('utf-8'))
|
||||
except Exception as e:
|
||||
self.send_response(400)
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({"error": str(e)}).encode('utf-8'))
|
||||
|
||||
elif self.path == "/action":
|
||||
global downloading_game_id
|
||||
action = data.get('action')
|
||||
game_id = data.get('game_id')
|
||||
|
||||
if action == 'start':
|
||||
url = data.get('url')
|
||||
|
||||
existing_downloader = downloads.get(game_id)
|
||||
|
||||
if url.startswith('magnet'):
|
||||
if existing_downloader and isinstance(existing_downloader, TorrentDownloader):
|
||||
existing_downloader.start_download(url, data['save_path'], "")
|
||||
else:
|
||||
torrent_downloader = TorrentDownloader(torrent_session)
|
||||
downloads[game_id] = torrent_downloader
|
||||
torrent_downloader.start_download(url, data['save_path'], "")
|
||||
else:
|
||||
if existing_downloader and isinstance(existing_downloader, HttpDownloader):
|
||||
existing_downloader.start_download(url, data['save_path'], data.get('header'))
|
||||
else:
|
||||
http_downloader = HttpDownloader()
|
||||
downloads[game_id] = http_downloader
|
||||
http_downloader.start_download(url, data['save_path'], data.get('header'))
|
||||
|
||||
downloading_game_id = game_id
|
||||
|
||||
elif action == 'pause':
|
||||
downloader = downloads.get(game_id)
|
||||
if downloader:
|
||||
downloader.pause_download()
|
||||
downloading_game_id = -1
|
||||
elif action == 'cancel':
|
||||
downloader = downloads.get(game_id)
|
||||
if downloader:
|
||||
downloader.cancel_download()
|
||||
elif action == 'resume_seeding':
|
||||
torrent_downloader = TorrentDownloader(torrent_session, lt.torrent_flags.upload_mode)
|
||||
downloads[game_id] = torrent_downloader
|
||||
torrent_downloader.start_download(url, data['save_path'], "")
|
||||
else:
|
||||
if existing_downloader and isinstance(existing_downloader, HttpDownloader):
|
||||
existing_downloader.start_download(url, data['save_path'], data.get('header'))
|
||||
torrent_downloader.start_download(data['url'], data['save_path'], "")
|
||||
elif action == 'pause_seeding':
|
||||
downloader = downloads.get(game_id)
|
||||
if downloader:
|
||||
downloader.cancel_download()
|
||||
else:
|
||||
http_downloader = HttpDownloader()
|
||||
downloads[game_id] = http_downloader
|
||||
http_downloader.start_download(url, data['save_path'], data.get('header'))
|
||||
|
||||
downloading_game_id = game_id
|
||||
self.send_response(400)
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({"error": "Invalid action"}).encode('utf-8'))
|
||||
return
|
||||
|
||||
elif action == 'pause':
|
||||
downloader = downloads.get(game_id)
|
||||
if downloader:
|
||||
downloader.pause_download()
|
||||
downloading_game_id = -1
|
||||
elif action == 'cancel':
|
||||
downloader = downloads.get(game_id)
|
||||
if downloader:
|
||||
downloader.cancel_download()
|
||||
elif action == 'resume_seeding':
|
||||
torrent_downloader = TorrentDownloader(torrent_session, lt.torrent_flags.upload_mode)
|
||||
downloads[game_id] = torrent_downloader
|
||||
torrent_downloader.start_download(data['url'], data['save_path'], "")
|
||||
elif action == 'pause_seeding':
|
||||
downloader = downloads.get(game_id)
|
||||
if downloader:
|
||||
downloader.cancel_download()
|
||||
|
||||
else:
|
||||
return jsonify({"error": "Invalid action"}), 400
|
||||
|
||||
return "", 200
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=int(http_port))
|
||||
|
||||
server = HTTPServer(('0.0.0.0', http_port), RequestHandler)
|
||||
print(f"Server running on port {http_port}")
|
||||
server.serve_forever()
|
||||
|
||||
@@ -4,5 +4,4 @@ cx_Logging; sys_platform == 'win32'
|
||||
pywin32; sys_platform == 'win32'
|
||||
psutil
|
||||
Pillow
|
||||
flask
|
||||
aria2p
|
||||
|
||||
@@ -49,14 +49,14 @@ fs.readdir(dist, async (err, files) => {
|
||||
})
|
||||
);
|
||||
|
||||
if (uploads.length > 0) {
|
||||
for (const upload of uploads) {
|
||||
await fetch(process.env.BUILD_WEBHOOK_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
uploads,
|
||||
upload,
|
||||
branchName: process.env.BRANCH_NAME,
|
||||
version: packageJson.version,
|
||||
githubActor: process.env.GITHUB_ACTOR,
|
||||
|
||||
@@ -1,417 +1,439 @@
|
||||
{
|
||||
"language_name": "اَلْعَرَبِيَّةُ",
|
||||
"language_name": "العربية",
|
||||
"app": {
|
||||
"successfully_signed_in": "تم تسجيل الدخول بنجاح"
|
||||
},
|
||||
"home": {
|
||||
"featured": "مُتَمَيِّز",
|
||||
"surprise_me": "فَاجِئْنِي",
|
||||
"no_results": "لَمْ يُعْثَرْ عَلَى نَتائِج",
|
||||
"start_typing": "اِبْدَأْ بِالْكِتَابَةِ لِلْبَحْثِ...",
|
||||
"hot": "اَلْأَكْثَرُ شُيُوعًا الْآن",
|
||||
"weekly": "📅 أَفْضَلُ أَلْعَابِ الْأُسْبُوعِ",
|
||||
"achievements": "🏆 أَلْعَابٌ لِلتَّغَلُّبِ عَلَيْهَا"
|
||||
"featured": "مميز",
|
||||
"surprise_me": "مفاجئني",
|
||||
"no_results": "لم يتم العثور على نتائج",
|
||||
"start_typing": "ابدأ الكتابة للبحث...",
|
||||
"hot": "الأكثر شيوعًا الآن",
|
||||
"weekly": "📅 أفضل ألعاب الأسبوع",
|
||||
"achievements": "🏆 ألعاب للتغلب عليها"
|
||||
},
|
||||
"sidebar": {
|
||||
"catalogue": "الْفِهْرِسُ",
|
||||
"downloads": "التَّنْزِيلَاتُ",
|
||||
"settings": "الإعْدَادَاتُ",
|
||||
"my_library": "مَكْتَبَتِي",
|
||||
"downloading_metadata": "{{title}} (جَارٍ تَنْزِيلُ الْبَيَانَاتِ الْوَصْفِيَّةِ...)",
|
||||
"paused": "{{title}} (مُوْقَفٌ)",
|
||||
"downloading": "{{title}} ({{percentage}} - جَارٍ التَّنْزِيلُ...)",
|
||||
"filter": "تَصْفِيَةُ الْمَكْتَبَةِ",
|
||||
"home": "الرَّئِيسِيَّةُ",
|
||||
"queued": "{{title}} (فِي الْانْتِظَارِ)",
|
||||
"game_has_no_executable": "اللُّعْبَةُ لَيْسَ لَدَيْهَا مِلَفٌّ تَنْفِيذِيٌّ مُحَدَّدٌ",
|
||||
"sign_in": "تَسْجِيلُ الدُّخُولِ",
|
||||
"friends": "الْأَصْدِقَاءُ",
|
||||
"need_help": "هَلْ تَحْتَاجُ إِلَى مُسَاعَدَةٍ؟"
|
||||
"catalogue": "الكـتالوج",
|
||||
"downloads": "التنزيلات",
|
||||
"settings": "الإعدادات",
|
||||
"my_library": "مكتبتي",
|
||||
"downloading_metadata": "{{title}} (جارٍ تنزيل البيانات الوصفية...)",
|
||||
"paused": "{{title}} (معلّق)",
|
||||
"downloading": "{{title}} ({{percentage}} - جاري التنزيل...)",
|
||||
"filter": "تصفية المكتبة",
|
||||
"home": "الرئيسية",
|
||||
"queued": "{{title}} (في قائمة الانتظار)",
|
||||
"game_has_no_executable": "اللعبة لا تحتوي على ملف تشغيل",
|
||||
"sign_in": "تسجيل الدخول",
|
||||
"friends": "الأصدقاء",
|
||||
"need_help": "تحتاج مساعدة؟"
|
||||
},
|
||||
"header": {
|
||||
"search": "بَحْثُ الْأَلْعَابِ",
|
||||
"home": "الرَّئِيسِيَّةُ",
|
||||
"catalogue": "الْفِهْرِسُ",
|
||||
"downloads": "التَّنْزِيلَاتُ",
|
||||
"search_results": "نَتائِجُ الْبَحْثِ",
|
||||
"settings": "الإعْدَادَاتُ",
|
||||
"version_available_install": "الْإِصْدَارُ {{version}} مَتَوَفِّرٌ. انْقُرْ هُنَا لِإِعَادَةِ التَّشْغِيلِ وَالتَّثْبِيتِ.",
|
||||
"version_available_download": "الْإِصْدَارُ {{version}} مَتَوَفِّرٌ. انْقُرْ هُنَا لِلتَّنْزِيلِ."
|
||||
"search": "ابحث عن الألعاب",
|
||||
"home": "الرئيسية",
|
||||
"catalogue": "الكـتالوج",
|
||||
"downloads": "التنزيلات",
|
||||
"search_results": "نتائج البحث",
|
||||
"settings": "الإعدادات",
|
||||
"version_available_install": "الإصدار {{version}} متوفر. انقر هنا لإعادة التشغيل والتثبيت.",
|
||||
"version_available_download": "الإصدار {{version}} متوفر. انقر هنا للتنزيل."
|
||||
},
|
||||
"bottom_panel": {
|
||||
"no_downloads_in_progress": "لَا تَوْجَدُ تَنْزِيلَاتٌ جَارِيَةٌ",
|
||||
"downloading_metadata": "جَارٍ تَنْزِيلُ الْبَيَانَاتِ الْوَصْفِيَّةِ لِـ {{title}}...",
|
||||
"downloading": "جَارٍ تَنْزِيلُ {{title}}... ({{percentage}} مَكْتُومٌ) - الِاكْتِمَالُ {{eta}} - {{speed}}",
|
||||
"calculating_eta": "جَارٍ تَنْزِيلُ {{title}}... ({{percentage}} مَكْتُومٌ) - جَارٍ حِسَابُ الْوَقْتِ الْمُتَبَقِّي...",
|
||||
"checking_files": "جَارٍ التَّحَقُّقُ مِنْ مَلَفَّاتِ {{title}}... ({{percentage}} مَكْتُومٌ)"
|
||||
"no_downloads_in_progress": "لا توجد تنزيلات قيد التقدم",
|
||||
"downloading_metadata": "جارٍ تنزيل البيانات الوصفية لـ {{title}}...",
|
||||
"downloading": "جارٍ تنزيل {{title}}... ({{percentage}} اكتمال) - الوقت المتبقي {{eta}} - السرعة {{speed}}",
|
||||
"calculating_eta": "جارٍ تنزيل {{title}}... ({{percentage}} اكتمال) - جاري حساب الوقت المتبقي...",
|
||||
"checking_files": "جارٍ فحص ملفات {{title}}... ({{percentage}} اكتمال)"
|
||||
},
|
||||
"catalogue": {
|
||||
"search": "تَصْفِيَةٌ...",
|
||||
"developers": "الْمُطَوِّرُونَ",
|
||||
"genres": "الْأَنْوَاعُ",
|
||||
"tags": "الْعَلَامَاتُ",
|
||||
"publishers": "النَّاشِرُونَ",
|
||||
"download_sources": "مَصَادِرُ التَّنْزِيلِ",
|
||||
"result_count": "{{resultCount}} نَتائِجُ",
|
||||
"filter_count": "{{filterCount}} مَتَوَفِّرٌ",
|
||||
"clear_filters": "مَسْحُ {{filterCount}} الْمُخْتَارَةِ"
|
||||
"search": "تصفية...",
|
||||
"developers": "المطورون",
|
||||
"genres": "الأنواع",
|
||||
"tags": "العلامات",
|
||||
"publishers": "الناشرون",
|
||||
"download_sources": "مصادر التنزيل",
|
||||
"result_count": "{{resultCount}} نتيجة",
|
||||
"filter_count": "{{filterCount}} متاح",
|
||||
"clear_filters": "مسح {{filterCount}} المحددة"
|
||||
},
|
||||
"game_details": {
|
||||
"open_download_options": "فَتْحُ خِيَارَاتِ التَّنْزِيلِ",
|
||||
"download_options_zero": "لَا تَوْجَدُ خِيَارَاتُ تَنْزِيلٍ",
|
||||
"download_options_one": "{{count}} خِيَارُ تَنْزِيلٍ",
|
||||
"download_options_other": "{{count}} خِيَارَاتُ تَنْزِيلٍ",
|
||||
"updated_at": "تَمَّ التَّحْدِيثُ فِي {{updated_at}}",
|
||||
"install": "تَثْبِيتٌ",
|
||||
"resume": "اسْتِئْنَافٌ",
|
||||
"pause": "إِيقَافٌ",
|
||||
"cancel": "إِلْغَاءٌ",
|
||||
"remove": "إِزَالَةٌ",
|
||||
"space_left_on_disk": "{{space}} مُتَبَقٍّ عَلَى الْقُرْصِ",
|
||||
"eta": "الِاكْتِمَالُ {{eta}}",
|
||||
"calculating_eta": "جَارٍ حِسَابُ الْوَقْتِ الْمُتَبَقِّي...",
|
||||
"downloading_metadata": "جَارٍ تَنْزِيلُ الْبَيَانَاتِ الْوَصْفِيَّةِ...",
|
||||
"filter": "تَصْفِيَةُ الْإِصْدَارَاتِ الْمُعَادِ تَغْلِيفُهَا",
|
||||
"requirements": "مُتَطَلَّبَاتُ النِّظَامِ",
|
||||
"minimum": "الْأَدْنَى",
|
||||
"recommended": "الْمُوَصَّى بِهِ",
|
||||
"paused": "مُوْقَفٌ",
|
||||
"release_date": "تَمَّ الْإِصْدَارُ فِي {{date}}",
|
||||
"publisher": "نُشِرَ بِوَاسِطَةِ {{publisher}}",
|
||||
"hours": "سَاعَاتٌ",
|
||||
"minutes": "دَقَائِقُ",
|
||||
"amount_hours": "{{amount}} سَاعَاتٌ",
|
||||
"amount_minutes": "{{amount}} دَقَائِقُ",
|
||||
"accuracy": "دِقَّةٌ {{accuracy}}%",
|
||||
"add_to_library": "إِضَافَةٌ إِلَى الْمَكْتَبَةِ",
|
||||
"remove_from_library": "إِزَالَةٌ مِنَ الْمَكْتَبَةِ",
|
||||
"no_downloads": "لَا تَوْجَدُ تَنْزِيلَاتٌ مَتَوَفِّرَةٌ",
|
||||
"play_time": "لُعِبَ لِمُدَّةِ {{amount}}",
|
||||
"last_time_played": "آخِرُ مَرَّةٍ لُعِبَتْ {{period}}",
|
||||
"not_played_yet": "لَمْ تَلْعَبْ {{title}} بَعْدُ",
|
||||
"next_suggestion": "الِاقْتِرَاحُ التَّالِي",
|
||||
"play": "لَعِبٌ",
|
||||
"deleting": "جَارٍ حَذْفُ الْمُثَبِّتِ...",
|
||||
"close": "إِغْلَاقٌ",
|
||||
"playing_now": "جَارِي اللَّعِبُ الْآن",
|
||||
"change": "تَغْيِيرٌ",
|
||||
"repacks_modal_description": "اخْتَرِ الْإِصْدَارَ الْمُعَادَ تَغْلِيفُهُ الَّذِي تُرِيدُ تَنْزِيلَهُ",
|
||||
"select_folder_hint": "لِتَغْيِيرِ الْمَجَلَّدِ الافْتِرَاضِيِّ، اذْهَبْ إِلَى <0>الإعْدَادَاتِ</0>",
|
||||
"download_now": "تَنْزِيلٌ الْآن",
|
||||
"no_shop_details": "لَمْ يَتَمَكَّنْ مِنْ اسْتِرْدَادِ تَفَاصِيلِ الْمَتْجَرِ.",
|
||||
"download_options": "خِيَارَاتُ التَّنْزِيلِ",
|
||||
"download_path": "مَسَارُ التَّنْزِيلِ",
|
||||
"previous_screenshot": "لَقْطَةُ الشَّاشَةِ السَّابِقَةُ",
|
||||
"next_screenshot": "لَقْطَةُ الشَّاشَةِ التَّالِيَةُ",
|
||||
"screenshot": "لَقْطَةُ الشَّاشَةِ {{number}}",
|
||||
"open_screenshot": "فَتْحُ لَقْطَةِ الشَّاشَةِ {{number}}",
|
||||
"download_settings": "إعْدَادَاتُ التَّنْزِيلِ",
|
||||
"downloader": "الْمُنَزِّلُ",
|
||||
"select_executable": "تَحْدِيدٌ",
|
||||
"no_executable_selected": "لَمْ يُحَدَّدْ مِلَفٌّ تَنْفِيذِيٌّ",
|
||||
"open_folder": "فَتْحُ الْمَجَلَّدِ",
|
||||
"open_download_location": "مُشَاهَدَةُ الْمَلَفَّاتِ الْمُنَزَّلَةِ",
|
||||
"create_shortcut": "إِنْشَاءُ طَرِيقٍ مُخْتَصَرٍ عَلَى سَطْحِ الْمَكْتَبِ",
|
||||
"clear": "مَسْحٌ",
|
||||
"remove_files": "إِزَالَةُ الْمَلَفَّاتِ",
|
||||
"remove_from_library_title": "هَلْ أَنْتَ مُتَأَكِّدٌ؟",
|
||||
"remove_from_library_description": "سَيُؤَدِّي هَذَا إِلَى إِزَالَةِ {{game}} مِنْ مَكْتَبَتِكَ",
|
||||
"options": "خِيَارَاتٌ",
|
||||
"executable_section_title": "الْمِلَفُّ التَّنْفِيذِيُّ",
|
||||
"executable_section_description": "مَسَارُ الْمِلَفِّ الَّذِي سَيَتِمُّ تَنْفِيذُهُ عِنْدَ النَّقْرِ عَلَى \"لَعِبٌ\"",
|
||||
"downloads_secion_title": "التَّنْزِيلَاتُ",
|
||||
"downloads_section_description": "تَحَقَّقْ مِنَ التَّحْدِيثَاتِ أَوِ الْإِصْدَارَاتِ الْأُخْرَى لِهَذِهِ اللُّعْبَةِ",
|
||||
"danger_zone_section_title": "مِنْطَقَةُ الْخَطَرِ",
|
||||
"danger_zone_section_description": "إِزَالَةُ هَذِهِ اللُّعْبَةِ مِنْ مَكْتَبَتِكَ أَوِ الْمَلَفَّاتِ الْمُنَزَّلَةِ بِوَاسِطَةِ Hydra",
|
||||
"download_in_progress": "جَارٍ التَّنْزِيلُ",
|
||||
"download_paused": "التَّنْزِيلُ مُوْقَفٌ",
|
||||
"last_downloaded_option": "خِيَارُ التَّنْزِيلِ الْأَخِيرُ",
|
||||
"create_shortcut_success": "تَمَّ إِنْشَاءُ الطَّرِيقِ الْمُخْتَصَرِ بِنَجَاحٍ",
|
||||
"create_shortcut_error": "خَطَأٌ فِي إِنْشَاءِ الطَّرِيقِ الْمُخْتَصَرِ",
|
||||
"nsfw_content_title": "هَذِهِ اللُّعْبَةُ تَحْتَوِي عَلَى مُحْتَوًى غَيْرِ لَائِقٍ",
|
||||
"nsfw_content_description": "{{title}} تَحْتَوِي عَلَى مُحْتَوًى قَدْ لَا يَكُونُ مُنَاسِبًا لِجَمِيعِ الْأَعْمَارِ. هَلْ أَنْتَ مُتَأَكِّدٌ مِنْ أَنَّكَ تُرِيدُ الْمُتَابَعَةَ؟",
|
||||
"allow_nsfw_content": "الْمُتَابَعَةُ",
|
||||
"refuse_nsfw_content": "الرُّجُوعُ",
|
||||
"stats": "الإحْصَائِيَّاتُ",
|
||||
"download_count": "التَّنْزِيلَاتُ",
|
||||
"player_count": "اللَّاعِبُونَ النَّشِطُونَ",
|
||||
"download_error": "هَذَا خِيَارُ التَّنْزِيلِ غَيْرُ مَتَوَفِّرٍ",
|
||||
"download": "تَنْزِيلٌ",
|
||||
"executable_path_in_use": "الْمِلَفُّ التَّنْفِيذِيُّ مُسْتَخْدَمٌ بِوَاسِطَةِ \"{{game}}\"",
|
||||
"warning": "تَنْبِيهٌ:",
|
||||
"hydra_needs_to_remain_open": "لِهَذَا التَّنْزِيلِ، يَجِبُ أَنْ يَبْقَى Hydra مَفْتُوحًا حَتَّى يَتِمَّ الِاكْتِمَالُ. إِذَا أُغْلِقَ Hydra قَبْلَ الِاكْتِمَالِ، سَتَفْقِدُ تَقَدُّمَكَ.",
|
||||
"achievements": "الإِنْجَازَاتُ",
|
||||
"achievements_count": "الإِنْجَازَاتُ {{unlockedCount}}/{{achievementsCount}}",
|
||||
"cloud_save": "حِفْظٌ سَحَابِيٌّ",
|
||||
"cloud_save_description": "احْفَظْ تَقَدُّمَكَ فِي السَّحَابَةِ وَاسْتَمِرَّ فِي اللَّعِبِ عَلَى أَيِّ جِهَازٍ",
|
||||
"backups": "الْنُسَخُ الِاحْتِيَاطِيَّةُ",
|
||||
"install_backup": "تَثْبِيتٌ",
|
||||
"delete_backup": "حَذْفٌ",
|
||||
"create_backup": "نُسْخَةٌ احْتِيَاطِيَّةٌ جَدِيدَةٌ",
|
||||
"last_backup_date": "آخِرُ نُسْخَةٍ احْتِيَاطِيَّةٍ فِي {{date}}",
|
||||
"no_backup_preview": "لَمْ يُعْثَرْ عَلَى أَيِّ أَلْعَابٍ مَحْفُوظَةٍ لِهَذَا الْعُنْوَانِ",
|
||||
"restoring_backup": "جَارٍ اسْتِعَادَةُ النُّسْخَةِ الِاحْتِيَاطِيَّةِ ({{progress}} مَكْتُومٌ)...",
|
||||
"uploading_backup": "جَارٍ رَفْعُ النُّسْخَةِ الِاحْتِيَاطِيَّةِ...",
|
||||
"no_backups": "لَمْ تَقُمْ بِإِنْشَاءِ أَيِّ نُسَخٍ احْتِيَاطِيَّةٍ لِهَذِهِ اللُّعْبَةِ بَعْدُ",
|
||||
"backup_uploaded": "تَمَّ رَفْعُ النُّسْخَةِ الِاحْتِيَاطِيَّةِ",
|
||||
"backup_deleted": "تَمَّ حَذْفُ النُّسْخَةِ الِاحْتِيَاطِيَّةِ",
|
||||
"backup_restored": "تَمَّ اسْتِعَادَةُ النُّسْخَةِ الِاحْتِيَاطِيَّةِ",
|
||||
"see_all_achievements": "عَرْضُ جَمِيعِ الإِنْجَازَاتِ",
|
||||
"sign_in_to_see_achievements": "سَجِّلِ الدُّخُولَ لِعَرْضِ الإِنْجَازَاتِ",
|
||||
"mapping_method_automatic": "آلِيٌّ",
|
||||
"mapping_method_manual": "يَدَوِيٌّ",
|
||||
"mapping_method_label": "طَرِيقَةُ التَّحْدِيدِ",
|
||||
"files_automatically_mapped": "تَمَّ تَحْدِيدُ الْمَلَفَّاتِ تِلْقَائِيًّا",
|
||||
"no_backups_created": "لَمْ تُنْشَأْ أَيُّ نُسَخٍ احْتِيَاطِيَّةٍ لِهَذِهِ اللُّعْبَةِ",
|
||||
"manage_files": "إِدَارَةُ الْمَلَفَّاتِ",
|
||||
"loading_save_preview": "جَارٍ الْبَحْثُ عَنْ أَلْعَابٍ مَحْفُوظَةٍ...",
|
||||
"wine_prefix": "بَادِئَةُ Wine",
|
||||
"wine_prefix_description": "بَادِئَةُ Wine الْمُسْتَخْدَمَةُ لِتَشْغِيلِ هَذِهِ اللُّعْبَةِ",
|
||||
"launch_options": "خِيَارَاتُ الْإِطْلَاقِ",
|
||||
"launch_options_description": "يُمْكِنُ لِلْمُسْتَخْدِمِينَ الْمُتَقَدِّمِينَ إِدْخَالُ تَعْدِيلَاتٍ عَلَى خِيَارَاتِ الْإِطْلَاقِ",
|
||||
"launch_options_placeholder": "لَمْ يُحَدَّدْ أَيُّ مُعَامِلٍ",
|
||||
"no_download_option_info": "لَا تَوْجَدُ مَعْلُومَاتٌ مَتَوَفِّرَةٌ",
|
||||
"backup_deletion_failed": "فَشَلَ فِي حَذْفِ النُّسْخَةِ الِاحْتِيَاطِيَّةِ",
|
||||
"max_number_of_artifacts_reached": "تَمَّ بَلُوغُ الْعَدَدِ الْأَقْصَى لِلنُّسَخِ الِاحْتِيَاطِيَّةِ لِهَذِهِ اللُّعْبَةِ",
|
||||
"achievements_not_sync": "تَعَرَّفْ عَلَى كَيْفِيَّةِ مَزْجِ إِنْجَازَاتِكَ",
|
||||
"manage_files_description": "إِدَارَةُ الْمَلَفَّاتِ الَّتِي سَيَتِمُّ نَسْخُهَا احْتِيَاطِيًّا وَاسْتِعَادَتُهَا",
|
||||
"select_folder": "تَحْدِيدُ الْمَجَلَّدِ",
|
||||
"backup_from": "نُسْخَةٌ احْتِيَاطِيَّةٌ مِنْ {{date}}",
|
||||
"custom_backup_location_set": "تَمَّ تَحْدِيدُ مَوْقِعِ النُّسْخَةِ الِاحْتِيَاطِيَّةِ الْمُخَصَّصِ",
|
||||
"no_directory_selected": "لَمْ يُحَدَّدْ أَيُّ دَلِيلٍ"
|
||||
"open_download_options": "فتح خيارات التنزيل",
|
||||
"download_options_zero": "لا توجد خيارات تنزيل",
|
||||
"download_options_one": "خيار تنزيل واحد",
|
||||
"download_options_other": "{{count}} خيارات تنزيل",
|
||||
"updated_at": "تم التحديث في {{updated_at}}",
|
||||
"install": "تثبيت",
|
||||
"resume": "استئناف",
|
||||
"pause": "إيقاف مؤقت",
|
||||
"cancel": "إلغاء",
|
||||
"remove": "إزالة",
|
||||
"space_left_on_disk": "{{space}} متبقي على القرص",
|
||||
"eta": "الانتهاء {{eta}}",
|
||||
"calculating_eta": "جارٍ حساب الوقت المتبقي...",
|
||||
"downloading_metadata": "جارٍ تنزيل البيانات الوصفية...",
|
||||
"filter": "تصفية الحزم المعاد تعبئتها",
|
||||
"requirements": "متطلبات النظام",
|
||||
"minimum": "الحد الأدنى",
|
||||
"recommended": "مُوصى به",
|
||||
"paused": "معلّق",
|
||||
"release_date": "تاريخ الإصدار {{date}}",
|
||||
"publisher": "نشر بواسطة {{publisher}}",
|
||||
"hours": "ساعات",
|
||||
"minutes": "دقائق",
|
||||
"amount_hours": "{{amount}} ساعات",
|
||||
"amount_minutes": "{{amount}} دقائق",
|
||||
"accuracy": "دقة {{accuracy}}%",
|
||||
"add_to_library": "إضافة إلى المكتبة",
|
||||
"remove_from_library": "إزالة من المكتبة",
|
||||
"no_downloads": "لا توجد تنزيلات متاحة",
|
||||
"play_time": "لعب لمدة {{amount}}",
|
||||
"last_time_played": "آخر تشغيل {{period}}",
|
||||
"not_played_yet": "لم تلعب {{title}} بعد",
|
||||
"next_suggestion": "الاقتراح التالي",
|
||||
"play": "تشغيل",
|
||||
"deleting": "جارٍ حذف المثبت...",
|
||||
"close": "إغلاق",
|
||||
"playing_now": "يتم التشغيل الآن",
|
||||
"change": "تغيير",
|
||||
"repacks_modal_description": "اختر الحزمة المعاد تعبئتها التي تريد تنزيلها",
|
||||
"select_folder_hint": "لتغيير المجلد الافتراضي، انتقل إلى <0>الإعدادات</0>",
|
||||
"download_now": "تنزيل الآن",
|
||||
"no_shop_details": "تعذر الحصول على تفاصيل المتجر.",
|
||||
"download_options": "خيارات التنزيل",
|
||||
"download_path": "مسار التنزيل",
|
||||
"previous_screenshot": "لقطة الشاشة السابقة",
|
||||
"next_screenshot": "لقطة الشاشة التالية",
|
||||
"screenshot": "لقطة الشاشة {{number}}",
|
||||
"open_screenshot": "فتح لقطة الشاشة {{number}}",
|
||||
"download_settings": "إعدادات التنزيل",
|
||||
"downloader": "أداة التنزيل",
|
||||
"select_executable": "تحديد",
|
||||
"no_executable_selected": "لم يتم تحديد ملف تشغيل",
|
||||
"open_folder": "فتح المجلد",
|
||||
"open_download_location": "عرض الملفات المحملة",
|
||||
"create_shortcut": "إنشاء اختصار على سطح المكتب",
|
||||
"clear": "مسح",
|
||||
"remove_files": "إزالة الملفات",
|
||||
"remove_from_library_title": "هل أنت متأكد؟",
|
||||
"remove_from_library_description": "سيؤدي هذا إلى إزالة {{game}} من مكتبتك",
|
||||
"options": "خيارات",
|
||||
"executable_section_title": "ملف التشغيل",
|
||||
"executable_section_description": "مسار الملف الذي سيتم تشغيله عند النقر على \"تشغيل\"",
|
||||
"downloads_secion_title": "التنزيلات",
|
||||
"downloads_section_description": "تحقق من التحديثات أو الإصدارات الأخرى لهذه اللعبة",
|
||||
"danger_zone_section_title": "منطقة الخطر",
|
||||
"danger_zone_section_description": "إزالة هذه اللعبة من مكتبتك أو الملفات التي تم تنزيلها بواسطة Hydra",
|
||||
"download_in_progress": "تنزيل قيد التقدم",
|
||||
"download_paused": "التنزيل معلق",
|
||||
"last_downloaded_option": "خيار التنزيل الأخير",
|
||||
"create_shortcut_success": "تم إنشاء الاختصار بنجاح",
|
||||
"create_shortcut_error": "خطأ في إنشاء الاختصار",
|
||||
"nsfw_content_title": "هذه اللعبة تحتوي على محتوى غير لائق",
|
||||
"nsfw_content_description": "{{title}} يحتوي على محتوى قد لا يناسب جميع الأعمار. هل تريد المتابعة؟",
|
||||
"allow_nsfw_content": "متابعة",
|
||||
"refuse_nsfw_content": "رجوع",
|
||||
"stats": "الإحصائيات",
|
||||
"download_count": "مرات التنزيل",
|
||||
"player_count": "اللاعبون النشطون",
|
||||
"download_error": "خيار التنزيل هذا غير متاح",
|
||||
"download": "تنزيل",
|
||||
"executable_path_in_use": "مسار التشغيل مستخدم بالفعل بواسطة \"{{game}}\"",
|
||||
"warning": "تحذير:",
|
||||
"hydra_needs_to_remain_open": "لهذا التنزيل، يجب أن يبقى Hydra مفتوحًا حتى اكتماله. إذا أغلق Hydra قبل الاكتمال، ستفقد تقدمك.",
|
||||
"achievements": "الإنجازات",
|
||||
"achievements_count": "الإنجازات {{unlockedCount}}/{{achievementsCount}}",
|
||||
"cloud_save": "حفظ سحابي",
|
||||
"cloud_save_description": "احفظ تقدمك على السحابة واستمر في اللعب من أي جهاز",
|
||||
"backups": "النسخ الاحتياطية",
|
||||
"install_backup": "تثبيت",
|
||||
"delete_backup": "حذف",
|
||||
"create_backup": "نسخة احتياطية جديدة",
|
||||
"last_backup_date": "آخر نسخة احتياطية في {{date}}",
|
||||
"no_backup_preview": "لم يتم العثور على حفظات لهذا العنوان",
|
||||
"restoring_backup": "جارٍ استعادة النسخة الاحتياطية ({{progress}} اكتمال)...",
|
||||
"uploading_backup": "جارٍ رفع النسخة الاحتياطية...",
|
||||
"no_backups": "لم تقم بإنشاء أي نسخ احتياطية لهذه اللعبة بعد",
|
||||
"backup_uploaded": "تم رفع النسخة الاحتياطية",
|
||||
"backup_deleted": "تم حذف النسخة الاحتياطية",
|
||||
"backup_restored": "تم استعادة النسخة الاحتياطية",
|
||||
"see_all_achievements": "عرض جميع الإنجازات",
|
||||
"sign_in_to_see_achievements": "سجل الدخول لعرض الإنجازات",
|
||||
"mapping_method_automatic": "تلقائي",
|
||||
"mapping_method_manual": "يدوي",
|
||||
"mapping_method_label": "طريقة التعيين",
|
||||
"files_automatically_mapped": "تم تعيين الملفات تلقائيًا",
|
||||
"no_backups_created": "لم يتم إنشاء نسخ احتياطية لهذه اللعبة",
|
||||
"manage_files": "إدارة الملفات",
|
||||
"loading_save_preview": "جارٍ البحث عن حفظات الألعاب...",
|
||||
"wine_prefix": "بادئة Wine",
|
||||
"wine_prefix_description": "بادئة Wine المستخدمة لتشغيل هذه اللعبة",
|
||||
"launch_options": "خيارات التشغيل",
|
||||
"launch_options_description": "يمكن للمستخدمين المتقدمين إدخال تعديلات على خيارات التشغيل (ميزة تجريبية)",
|
||||
"launch_options_placeholder": "لم يتم تحديد أي معاملات",
|
||||
"no_download_option_info": "لا توجد معلومات متاحة",
|
||||
"backup_deletion_failed": "فشل حذف النسخة الاحتياطية",
|
||||
"max_number_of_artifacts_reached": "تم الوصول إلى الحد الأقصى لعدد النسخ الاحتياطية لهذه اللعبة",
|
||||
"achievements_not_sync": "تعرف على كيفية مزامنة إنجازاتك",
|
||||
"manage_files_description": "إدارة الملفات التي سيتم نسخها احتياطيًا واستعادتها",
|
||||
"select_folder": "حدد المجلد",
|
||||
"backup_from": "نسخة احتياطية من {{date}}",
|
||||
"custom_backup_location_set": "تم تعيين موقع نسخ احتياطي مخصص",
|
||||
"no_directory_selected": "لم يتم تحديد مجلد",
|
||||
"no_write_permission": "لا يمكن التنزيل إلى هذا المجلد. انقر هنا لمعرفة المزيد.",
|
||||
"reset_achievements": "إعادة تعيين الإنجازات",
|
||||
"reset_achievements_description": "سيؤدي هذا إلى إعادة تعيين جميع إنجازات {{game}}",
|
||||
"reset_achievements_title": "هل أنت متأكد؟",
|
||||
"reset_achievements_success": "تم إعادة تعيين الإنجازات بنجاح",
|
||||
"reset_achievements_error": "فشل إعادة تعيين الإنجازات"
|
||||
},
|
||||
"activation": {
|
||||
"title": "تَفْعِيلُ Hydra",
|
||||
"installation_id": "مُعَرِّفُ التَّثْبِيتِ:",
|
||||
"enter_activation_code": "أَدْخِلْ رَمْزَ التَّفْعِيلِ",
|
||||
"message": "إِذَا كُنْتَ لَا تَعْرِفُ أَيْنَ تَطْلُبُ هَذَا، فَلا يَجِبُ أَنْ تَكُونَ لَدَيْكَ.",
|
||||
"activate": "تَفْعِيلٌ",
|
||||
"loading": "جَارٍ التَّحْمِيلُ..."
|
||||
"title": "تفعيل Hydra",
|
||||
"installation_id": "معرف التثبيت:",
|
||||
"enter_activation_code": "أدخل رمز التفعيل الخاص بك",
|
||||
"message": "إذا كنت لا تعرف أين تطلب هذا، فلا يجب أن يكون لديك هذا.",
|
||||
"activate": "تفعيل",
|
||||
"loading": "جارٍ التحميل..."
|
||||
},
|
||||
"downloads": {
|
||||
"resume": "اسْتِئْنَافٌ",
|
||||
"pause": "إِيقَافٌ",
|
||||
"eta": "الِاكْتِمَالُ {{eta}}",
|
||||
"paused": "مُوْقَفٌ",
|
||||
"verifying": "جَارٍ التَّحَقُّقُ...",
|
||||
"completed": "مَكْتُومٌ",
|
||||
"removed": "لَمْ يُنَزَّلْ",
|
||||
"cancel": "إِلْغَاءٌ",
|
||||
"filter": "تَصْفِيَةُ الْأَلْعَابِ الْمُنَزَّلَةِ",
|
||||
"remove": "إِزَالَةٌ",
|
||||
"downloading_metadata": "جَارٍ تَنْزِيلُ الْبَيَانَاتِ الْوَصْفِيَّةِ...",
|
||||
"deleting": "جَارٍ حَذْفُ الْمُثَبِّتِ...",
|
||||
"delete": "حَذْفُ الْمُثَبِّتِ",
|
||||
"delete_modal_title": "هَلْ أَنْتَ مُتَأَكِّدٌ؟",
|
||||
"delete_modal_description": "سَيُؤَدِّي هَذَا إِلَى إِزَالَةِ جَمِيعِ مَلَفَّاتِ التَّثْبِيتِ مِنْ حَاسُوبِكَ",
|
||||
"install": "تَثْبِيتٌ",
|
||||
"download_in_progress": "جَارٍ التَّنْفِيذُ",
|
||||
"queued_downloads": "التَّنْزِيلَاتُ فِي الْانْتِظَارِ",
|
||||
"downloads_completed": "مَكْتُومٌ",
|
||||
"queued": "فِي الْانْتِظَارِ",
|
||||
"no_downloads_title": "فَرَاغٌ تَامٌ",
|
||||
"no_downloads_description": "لَمْ تَقُمْ بِتَنْزِيلِ أَيِّ شَيْءٍ بِاسْتِخْدَامِ Hydra بَعْدُ، لَكِنَّهُ لَيْسَ مُتَأَخِّرًا لِلْبَدْءِ.",
|
||||
"checking_files": "جَارٍ التَّحَقُّقُ مِنَ الْمَلَفَّاتِ...",
|
||||
"seeding": "الْبَذْرُ",
|
||||
"stop_seeding": "إِيقَافُ الْبَذْرِ",
|
||||
"resume_seeding": "اسْتِئْنَافُ الْبَذْرِ",
|
||||
"options": "إِدَارَةٌ"
|
||||
"resume": "استئناف",
|
||||
"pause": "إيقاف مؤقت",
|
||||
"eta": "الانتهاء {{eta}}",
|
||||
"paused": "معلّق",
|
||||
"verifying": "جارٍ التحقق...",
|
||||
"completed": "مكتمل",
|
||||
"removed": "غير محمل",
|
||||
"cancel": "إلغاء",
|
||||
"filter": "تصفية الألعاب المحملة",
|
||||
"remove": "إزالة",
|
||||
"downloading_metadata": "جارٍ تنزيل البيانات الوصفية...",
|
||||
"deleting": "جارٍ حذف المثبت...",
|
||||
"delete": "إزالة المثبت",
|
||||
"delete_modal_title": "هل أنت متأكد؟",
|
||||
"delete_modal_description": "سيؤدي هذا إلى إزالة جميع ملفات التثبيت من جهازك",
|
||||
"install": "تثبيت",
|
||||
"download_in_progress": "قيد التقدم",
|
||||
"queued_downloads": "التنزيلات في قائمة الانتظار",
|
||||
"downloads_completed": "مكتمل",
|
||||
"queued": "في قائمة الانتظار",
|
||||
"no_downloads_title": "فارغ جدًا",
|
||||
"no_downloads_description": "لم تقم بتنزيل أي شيء باستخدام Hydra بعد، ولكن لم يفت الأوان للبدء.",
|
||||
"checking_files": "جارٍ فحص الملفات...",
|
||||
"seeding": "التوزيع",
|
||||
"stop_seeding": "إيقاف التوزيع",
|
||||
"resume_seeding": "استئناف التوزيع",
|
||||
"options": "إدارة"
|
||||
},
|
||||
"settings": {
|
||||
"downloads_path": "مَسَارُ التَّنْزِيلَاتِ",
|
||||
"change": "تَحْدِيثٌ",
|
||||
"notifications": "الإِشْعَارَاتُ",
|
||||
"enable_download_notifications": "عِنْدَ اكْتِمَالِ التَّنْزِيلِ",
|
||||
"enable_repack_list_notifications": "عِنْدَ إِضَافَةِ إِصْدَارٍ مُعَادٍ تَغْلِيفِهِ جَدِيدٍ",
|
||||
"real_debrid_api_token_label": "رَمْزُ واجهة برمجة التطبيقات Real-Debrid",
|
||||
"quit_app_instead_hiding": "لا تُخْفِ Hydra عِنْدَ الإِغْلَاقِ",
|
||||
"launch_with_system": "تَشْغِيلُ Hydra عِنْدَ بَدْءِ النِّظَامِ",
|
||||
"general": "عَامٌ",
|
||||
"behavior": "سُلُوكٌ",
|
||||
"download_sources": "مَصَادِرُ التَّنْزِيلِ",
|
||||
"language": "اللُّغَةُ",
|
||||
"real_debrid_api_token": "رَمْزُ واجهة برمجة التطبيقات",
|
||||
"enable_real_debrid": "تَمْكِينُ Real-Debrid",
|
||||
"real_debrid_description": "Real-Debrid هُوَ مُنَزِّلٌ غَيْرُ مَقْيُودٍ يَتِيحُ لَكَ تَنْزِيلَ الْمَلَفَّاتِ بِسُرْعَةٍ، مَحْدُودٌ فَقَطْ بِسُرْعَةِ الْإِنْتَرْنِتِ لَدَيْكَ.",
|
||||
"real_debrid_invalid_token": "رَمْزُ واجهة برمجة التطبيقات غَيْرُ صَالِحٍ",
|
||||
"real_debrid_api_token_hint": "يُمْكِنُكَ الْحُصُولُ عَلَى رَمْزِ واجهة برمجة التطبيقات <0>هُنَا</0>",
|
||||
"real_debrid_free_account_error": "الْحِسَابُ \"{{username}}\" هُوَ حِسَابٌ مَجَّانِيٌّ. يَرْجَى الِاشْتِرَاكُ فِي Real-Debrid",
|
||||
"real_debrid_linked_message": "تَمَّ رَبْطُ الْحِسَابِ \"{{username}}\"",
|
||||
"save_changes": "حِفْظُ التَّغْيِيرَاتِ",
|
||||
"changes_saved": "تَمَّ حِفْظُ التَّغْيِيرَاتِ بِنَجَاحٍ",
|
||||
"download_sources_description": "سَتَقُومُ Hydra بِجَلْبِ رَوَابِطِ التَّنْزِيلِ مِنْ هَذِهِ الْمَصَادِرِ. يَجِبُ أَنْ يَكُونَ عُنْوَانُ URL لِلْمَصْدَرِ رَابِطًا مُبَاشِرًا إِلَى مِلَفٍّ .json يَحْتَوِي عَلَى رَوَابِطِ التَّنْزِيلِ.",
|
||||
"validate_download_source": "تَصْدِيقٌ",
|
||||
"remove_download_source": "إِزَالَةٌ",
|
||||
"add_download_source": "إِضَافَةُ مَصْدَرٍ",
|
||||
"download_count_zero": "لَا تَوْجَدُ خِيَارَاتُ تَنْزِيلٍ",
|
||||
"download_count_one": "{{countFormatted}} خِيَارُ تَنْزِيلٍ",
|
||||
"download_count_other": "{{countFormatted}} خِيَارَاتُ تَنْزِيلٍ",
|
||||
"download_source_url": "عُنْوَانُ مَصْدَرِ التَّنْزِيلِ",
|
||||
"add_download_source_description": "أَدْخِلْ عُنْوَانَ URL لِمِلَفٍّ .json",
|
||||
"download_source_up_to_date": "مُحَدَّثٌ",
|
||||
"download_source_errored": "خَطَأٌ",
|
||||
"sync_download_sources": "مَزْجُ الْمَصَادِرِ",
|
||||
"removed_download_source": "تَمَّ إِزَالَةُ مَصْدَرِ التَّنْزِيلِ",
|
||||
"added_download_source": "تَمَّتْ إِضَافَةُ مَصْدَرِ التَّنْزِيلِ",
|
||||
"download_sources_synced": "تَمَّ مَزْجُ جَمِيعِ مَصَادِرِ التَّنْزِيلِ",
|
||||
"insert_valid_json_url": "أَدْخِلْ عُنْوَانَ JSON صَالِحًا",
|
||||
"found_download_option_zero": "لَمْ يُعْثَرْ عَلَى خِيَارِ تَنْزِيلٍ",
|
||||
"found_download_option_one": "عُثِرَ عَلَى {{countFormatted}} خِيَارِ تَنْزِيلٍ",
|
||||
"found_download_option_other": "عُثِرَ عَلَى {{countFormatted}} خِيَارَاتِ تَنْزِيلٍ",
|
||||
"import": "اسْتِيرَادٌ",
|
||||
"public": "عَامٌ",
|
||||
"private": "خَاصٌ",
|
||||
"friends_only": "الْأَصْدِقَاءُ فَقَطْ",
|
||||
"privacy": "الْخُصُوصِيَّةُ",
|
||||
"profile_visibility": "رُؤْيَةُ الْمَلَفِّ الشَّخْصِيِّ",
|
||||
"profile_visibility_description": "اخْتَرْ مَنْ يُمْكِنُهُ رُؤْيَةُ مَلَفِّكَ الشَّخْصِيِّ وَمَكْتَبَتِكَ",
|
||||
"required_field": "هَذَا الْحَقْلُ مَطْلُوبٌ",
|
||||
"source_already_exists": "تَمَّتْ إِضَافَةُ هَذَا الْمَصْدَرِ مِنْ قَبْلُ",
|
||||
"must_be_valid_url": "يَجِبُ أَنْ يَكُونَ الْمَصْدَرُ عُنْوَانَ URL صَالِحًا",
|
||||
"blocked_users": "الْمُسْتَخْدِمُونَ الْمَحْظُورُونَ",
|
||||
"user_unblocked": "تَمَّ إِزَالَةُ حَظْرِ الْمُسْتَخْدِمِ",
|
||||
"enable_achievement_notifications": "عِنْدَ فَتْحِ إِنْجَازٍ",
|
||||
"launch_minimized": "تَشْغِيلُ Hydra مُصَغَّرًا",
|
||||
"disable_nsfw_alert": "تَعْطِيلُ تَنْبِيهِ الْمُحْتَوَى غَيْرِ اللَّائِقِ",
|
||||
"seed_after_download_complete": "الْبَذْرُ بَعْدَ اكْتِمَالِ التَّنْزِيلِ",
|
||||
"show_hidden_achievement_description": "إِظْهَارُ وَصْفِ الإِنْجَازَاتِ الْمَخْفِيَّةِ قَبْلَ فَتْحِهَا"
|
||||
"downloads_path": "مسار التنزيلات",
|
||||
"change": "تحديث",
|
||||
"notifications": "الإشعارات",
|
||||
"enable_download_notifications": "عند اكتمال التنزيل",
|
||||
"enable_repack_list_notifications": "عند إضافة حزمة معاد تعبئتها جديدة",
|
||||
"real_debrid_api_token_label": "رمز واجهة برمجة تطبيقات Real-Debrid",
|
||||
"quit_app_instead_hiding": "لا تخفي Hydra عند الإغلاق",
|
||||
"launch_with_system": "تشغيل Hydra مع بدء النظام",
|
||||
"general": "عام",
|
||||
"behavior": "السلوك",
|
||||
"download_sources": "مصادر التنزيل",
|
||||
"language": "اللغة",
|
||||
"real_debrid_api_token": "رمز API",
|
||||
"enable_real_debrid": "تفعيل Real-Debrid",
|
||||
"real_debrid_description": "Real-Debrid هو أداة تنزيل غير مقيدة تتيح لك تنزيل الملفات بسرعة، مقيدة فقط بسرعة الإنترنت لديك.",
|
||||
"real_debrid_invalid_token": "رمز API غير صالح",
|
||||
"real_debrid_api_token_hint": "يمكنك الحصول على رمز API الخاص بك <0>هنا</0>",
|
||||
"real_debrid_free_account_error": "الحساب \"{{username}}\" هو حساب مجاني. يرجى الاشتراك في Real-Debrid",
|
||||
"real_debrid_linked_message": "تم ربط الحساب \"{{username}}\"",
|
||||
"save_changes": "حفظ التغييرات",
|
||||
"changes_saved": "تم حفظ التغييرات بنجاح",
|
||||
"download_sources_description": "سيقوم Hydra بجلب روابط التنزيل من هذه المصادر. يجب أن يكون عنوان URL المصدر رابطًا مباشرًا لملف .json يحتوي على روابط التنزيل.",
|
||||
"validate_download_source": "تحقق",
|
||||
"remove_download_source": "إزالة",
|
||||
"add_download_source": "إضافة مصدر",
|
||||
"download_count_zero": "لا توجد خيارات تنزيل",
|
||||
"download_count_one": "{{countFormatted}} خيار تنزيل",
|
||||
"download_count_other": "{{countFormatted}} خيارات تنزيل",
|
||||
"download_source_url": "عنوان URL لمصدر التنزيل",
|
||||
"add_download_source_description": "أدخل عنوان URL لملف .json",
|
||||
"download_source_up_to_date": "محدث",
|
||||
"download_source_errored": "خطأ",
|
||||
"sync_download_sources": "مزامنة المصادر",
|
||||
"removed_download_source": "تمت إزالة مصدر التنزيل",
|
||||
"added_download_source": "تمت إضافة مصدر التنزيل",
|
||||
"download_sources_synced": "تمت مزامنة جميع مصادر التنزيل",
|
||||
"insert_valid_json_url": "أدخل عنوان JSON صالح",
|
||||
"found_download_option_zero": "لم يتم العثور على خيارات تنزيل",
|
||||
"found_download_option_one": "تم العثور على {{countFormatted}} خيار تنزيل",
|
||||
"found_download_option_other": "تم العثور على {{countFormatted}} خيارات تنزيل",
|
||||
"import": "استيراد",
|
||||
"public": "عام",
|
||||
"private": "خاص",
|
||||
"friends_only": "الأصدقاء فقط",
|
||||
"privacy": "الخصوصية",
|
||||
"profile_visibility": "رؤية الملف الشخصي",
|
||||
"profile_visibility_description": "اختر من يمكنه رؤية ملفك الشخصي ومكتبتك",
|
||||
"required_field": "هذا الحقل مطلوب",
|
||||
"source_already_exists": "تمت إضافة هذا المصدر مسبقًا",
|
||||
"must_be_valid_url": "يجب أن يكون المصدر عنوان URL صالحًا",
|
||||
"blocked_users": "المستخدمون المحظورون",
|
||||
"user_unblocked": "تم إلغاء حظر المستخدم",
|
||||
"enable_achievement_notifications": "عند فتح إنجاز",
|
||||
"launch_minimized": "تشغيل Hydra مصغرًا",
|
||||
"disable_nsfw_alert": "تعطيل تنبيه المحتوى غير اللائق",
|
||||
"seed_after_download_complete": "التوزيع بعد اكتمال التنزيل",
|
||||
"show_hidden_achievement_description": "عرض وصف الإنجازات المخفية قبل فتحها",
|
||||
"account": "الحساب",
|
||||
"no_users_blocked": "لا يوجد مستخدمون محظورون",
|
||||
"subscription_active_until": "اشتراك Hydra Cloud نشط حتى {{date}}",
|
||||
"manage_subscription": "إدارة الاشتراك",
|
||||
"update_email": "تحديث البريد الإلكتروني",
|
||||
"update_password": "تحديث كلمة المرور",
|
||||
"current_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": {
|
||||
"download_complete": "اكْتِمَالُ التَّنْزِيلِ",
|
||||
"game_ready_to_install": "{{title}} جَاهِزٌ لِلتَّثْبِيتِ",
|
||||
"repack_list_updated": "تَمَّ تَحْدِيثُ قَائِمَةِ الإِصْدَارَاتِ الْمُعَادَةِ تَغْلِيفُهَا",
|
||||
"repack_count_one": "{{count}} إِصْدَارٌ مُعَادٌ تَغْلِيفُهُ أُضِيفَ",
|
||||
"repack_count_other": "{{count}} إِصْدَارَاتٌ مُعَادَةٌ تَغْلِيفُهَا أُضِيفَتْ",
|
||||
"new_update_available": "الْإِصْدَارُ {{version}} مَتَوَفِّرٌ",
|
||||
"restart_to_install_update": "أَعِدْ تَشْغِيلَ Hydra لِتَثْبِيتِ التَّحْدِيثِ",
|
||||
"notification_achievement_unlocked_title": "تَمَّ فَتْحُ إِنْجَازٍ لِـ {{game}}",
|
||||
"notification_achievement_unlocked_body": "{{achievement}} وَ{{count}} أُخْرَى تَمَّ فَتْحُهَا"
|
||||
"download_complete": "اكتمل التنزيل",
|
||||
"game_ready_to_install": "{{title}} جاهز للتثبيت",
|
||||
"repack_list_updated": "تم تحديث قائمة الحزم المعاد تعبئتها",
|
||||
"repack_count_one": "تمت إضافة {{count}} حزمة معاد تعبئتها",
|
||||
"repack_count_other": "تمت إضافة {{count}} حزم معاد تعبئتها",
|
||||
"new_update_available": "الإصدار {{version}} متوفر",
|
||||
"restart_to_install_update": "أعد تشغيل Hydra لتثبيت التحديث",
|
||||
"notification_achievement_unlocked_title": "تم فتح إنجاز لـ {{game}}",
|
||||
"notification_achievement_unlocked_body": "{{achievement}} و {{count}} آخرين تم فتحهم"
|
||||
},
|
||||
"system_tray": {
|
||||
"open": "فَتْحُ Hydra",
|
||||
"quit": "الْخُرُوجُ"
|
||||
"open": "فتح Hydra",
|
||||
"quit": "خروج"
|
||||
},
|
||||
"game_card": {
|
||||
"no_downloads": "لَا تَوْجَدُ تَنْزِيلَاتٌ مَتَوَفِّرَةٌ"
|
||||
"no_downloads": "لا توجد تنزيلات متاحة"
|
||||
},
|
||||
"binary_not_found_modal": {
|
||||
"title": "الْبَرَامِجُ غَيْرُ مُثَبَّتَةٍ",
|
||||
"description": "لَمْ يُعْثَرْ عَلَى مَلَفَّاتٍ تَنْفِيذِيَّةٍ لِـ Wine أَوْ Lutris عَلَى نِظَامِكَ",
|
||||
"instructions": "تَحَقَّقْ مِنَ الطَّرِيقَةِ الصَّحِيحَةِ لِتَثْبِيتِ أَيٍّ مِنْهُمَا عَلَى تَوْزِيعَةِ Linux لَدَيْكَ لِتَعْمَلَ اللُّعْبَةُ بِشَكْلٍ طَبِيعِيٍّ"
|
||||
"title": "البرامج غير مثبتة",
|
||||
"description": "لم يتم العثور على ملفات تشغيل Wine أو Lutris على نظامك",
|
||||
"instructions": "تحقق من الطريقة الصحيحة لتثبيت أي منها على توزيعة لينكس الخاصة بك حتى تعمل اللعبة بشكل طبيعي"
|
||||
},
|
||||
"modal": {
|
||||
"close": "زِرُّ الإِغْلَاقِ"
|
||||
"close": "زر الإغلاق"
|
||||
},
|
||||
"forms": {
|
||||
"toggle_password_visibility": "تَبْدِيلُ رُؤْيَةِ كَلِمَةِ الْمَرُورِ"
|
||||
"toggle_password_visibility": "تبديل رؤية كلمة المرور"
|
||||
},
|
||||
"user_profile": {
|
||||
"amount_hours": "{{amount}} سَاعَاتٌ",
|
||||
"amount_minutes": "{{amount}} دَقَائِقُ",
|
||||
"last_time_played": "آخِرُ مَرَّةٍ لُعِبَتْ {{period}}",
|
||||
"activity": "النَّشَاطُ الْأَخِيرُ",
|
||||
"library": "الْمَكْتَبَةُ",
|
||||
"total_play_time": "إِجْمَالِيُّ وَقْتِ اللَّعِبِ",
|
||||
"no_recent_activity_title": "هَمَمْ... لَا شَيْءَ هُنَا",
|
||||
"no_recent_activity_description": "لَمْ تَلْعَبْ أَيَّ أَلْعَابٍ مُؤَخَّرًا. حَانَ الْوَقْتُ لِتَغْيِيرِ ذَلِكَ!",
|
||||
"display_name": "اسْمُ الْعَرْضِ",
|
||||
"saving": "جَارٍ الْحِفْظُ",
|
||||
"save": "حِفْظٌ",
|
||||
"edit_profile": "تَحْرِيرُ الْمَلَفِّ الشَّخْصِيِّ",
|
||||
"saved_successfully": "تَمَّ الْحِفْظُ بِنَجَاحٍ",
|
||||
"try_again": "الرَّجَاءُ الْمُحَاوَلَةُ مَرَّةً أُخْرَى",
|
||||
"sign_out_modal_title": "هَلْ أَنْتَ مُتَأَكِّدٌ؟",
|
||||
"cancel": "إِلْغَاءٌ",
|
||||
"successfully_signed_out": "تَمَّ تَسْجِيلُ الْخُرُوجِ بِنَجَاحٍ",
|
||||
"sign_out": "تَسْجِيلُ الْخُرُوجِ",
|
||||
"playing_for": "جَارِي اللَّعِبُ لِمُدَّةِ {{amount}}",
|
||||
"sign_out_modal_text": "مَكْتَبَتُكَ مُرْتَبِطَةٌ بِحِسَابِكَ الْحَالِيِّ. عِنْدَ تَسْجِيلِ الْخُرُوجِ، لَنْ تَكُونَ مَكْتَبَتُكَ مَرْئِيَّةً بَعْدَ الْآنِ، وَلَنْ يَتِمَّ حِفْظُ أَيِّ تَقَدُّمٍ. هَلْ تُرِيدُ الْمُتَابَعَةَ مَعَ تَسْجِيلِ الْخُرُوجِ؟",
|
||||
"add_friends": "إِضَافَةُ الْأَصْدِقَاءِ",
|
||||
"add": "إِضَافَةٌ",
|
||||
"friend_code": "رَمْزُ الصَّدِيقِ",
|
||||
"see_profile": "رُؤْيَةُ الْمَلَفِّ الشَّخْصِيِّ",
|
||||
"sending": "جَارٍ الْإِرْسَالُ",
|
||||
"friend_request_sent": "تَمَّ إِرْسَالُ طَلَبِ الصَّدَاقَةِ",
|
||||
"friends": "الْأَصْدِقَاءُ",
|
||||
"friends_list": "قَائِمَةُ الْأَصْدِقَاءِ",
|
||||
"user_not_found": "الْمُسْتَخْدِمُ غَيْرُ مَوْجُودٍ",
|
||||
"block_user": "حَظْرُ الْمُسْتَخْدِمِ",
|
||||
"add_friend": "إِضَافَةُ صَدِيقٍ",
|
||||
"request_sent": "تَمَّ إِرْسَالُ الطَّلَبِ",
|
||||
"request_received": "تَمَّ اسْتِقْبَالُ الطَّلَبِ",
|
||||
"accept_request": "قَبُولُ الطَّلَبِ",
|
||||
"ignore_request": "تَجَاهُلُ الطَّلَبِ",
|
||||
"cancel_request": "إِلْغَاءُ الطَّلَبِ",
|
||||
"undo_friendship": "إِلْغَاءُ الصَّدَاقَةِ",
|
||||
"request_accepted": "تَمَّ قَبُولُ الطَّلَبِ",
|
||||
"user_blocked_successfully": "تَمَّ حَظْرُ الْمُسْتَخْدِمِ بِنَجَاحٍ",
|
||||
"user_block_modal_text": "سَيُؤَدِّي هَذَا إِلَى حَظْرِ {{displayName}}",
|
||||
"blocked_users": "الْمُسْتَخْدِمُونَ الْمَحْظُورُونَ",
|
||||
"unblock": "إِزَالَةُ الْحَظْرِ",
|
||||
"no_friends_added": "لَيْسَ لَدَيْكَ أَصْدِقَاءٌ مُضَافُونَ",
|
||||
"pending": "قَيْدُ الْانْتِظَارِ",
|
||||
"no_pending_invites": "لَيْسَ لَدَيْكَ دَعَوَاتٌ قَيْدُ الْانْتِظَارِ",
|
||||
"no_blocked_users": "لَيْسَ لَدَيْكَ مُسْتَخْدِمُونَ مَحْظُورُونَ",
|
||||
"friend_code_copied": "تَمَّ نَسْخُ رَمْزِ الصَّدِيقِ",
|
||||
"undo_friendship_modal_text": "سَيُؤَدِّي هَذَا إِلَى إِلْغَاءِ صَدَاقَتِكَ مَعَ {{displayName}}",
|
||||
"privacy_hint": "لِتَعْدِيلِ مَنْ يُمْكِنُهُ رُؤْيَةُ هَذَا، اذْهَبْ إِلَى <0>الإعْدَادَاتِ</0>",
|
||||
"locked_profile": "هَذَا الْمَلَفُّ الشَّخْصِيُّ خَاصٌّ",
|
||||
"image_process_failure": "فَشَلَ أَثْنَاءَ مُعَالَجَةِ الصُّورَةِ",
|
||||
"required_field": "هَذَا الْحَقْلُ مَطْلُوبٌ",
|
||||
"displayname_min_length": "يَجِبُ أَنْ يَكُونَ اسْمُ الْعَرْضِ عَلَى الْأَقَلِّ 3 أَحْرُفٍ",
|
||||
"displayname_max_length": "يَجِبُ أَنْ يَكُونَ اسْمُ الْعَرْضِ عَلَى الْأَكْثَرِ 50 حَرْفًا",
|
||||
"report_profile": "تَقْرِيرٌ عَنْ هَذَا الْمَلَفِّ الشَّخْصِيِّ",
|
||||
"report_reason": "لِمَاذَا تُقَدِّمُ تَقْرِيرًا عَنْ هَذَا الْمَلَفِّ الشَّخْصِيِّ؟",
|
||||
"report_description": "مَعْلُومَاتٌ إِضَافِيَّةٌ",
|
||||
"report_description_placeholder": "مَعْلُومَاتٌ إِضَافِيَّةٌ",
|
||||
"report": "تَقْرِيرٌ",
|
||||
"report_reason_hate": "خِطَابُ الْكُرْهِ",
|
||||
"report_reason_sexual_content": "مُحْتَوًى جِنْسِيٌّ",
|
||||
"report_reason_violence": "عُنْفٌ",
|
||||
"report_reason_spam": "رَاسِلَةٌ عَشْوَائِيَّةٌ",
|
||||
"report_reason_other": "آخَرُ",
|
||||
"profile_reported": "تَمَّ تَقْرِيرُ الْمَلَفِّ الشَّخْصِيِّ",
|
||||
"your_friend_code": "رَمْزُ صَدِيقِكَ:",
|
||||
"upload_banner": "رَفْعُ لَافِتَةٍ",
|
||||
"uploading_banner": "جَارٍ رَفْعُ اللَّافِتَةِ...",
|
||||
"background_image_updated": "تَمَّ تَحْدِيثُ صُورَةِ الْخَلْفِيَّةِ",
|
||||
"stats": "الإحْصَائِيَّاتُ",
|
||||
"achievements": "الإِنْجَازَاتُ",
|
||||
"games": "الْأَلْعَابُ",
|
||||
"top_percentile": "الْأَفْضَلُ {{percentile}}%",
|
||||
"ranking_updated_weekly": "التَّرْتِيبُ يُحَدَّثُ أُسْبُوعِيًّا",
|
||||
"playing": "جَارِي اللَّعِبُ {{game}}",
|
||||
"achievements_unlocked": "الإِنْجَازَاتُ الْمَفْتُوحَةُ",
|
||||
"earned_points": "النَّقَاطُ الْمَكْسُوبَةُ",
|
||||
"show_achievements_on_profile": "عَرْضُ إِنْجَازَاتِكَ عَلَى مَلَفِّكَ الشَّخْصِيِّ",
|
||||
"show_points_on_profile": "عَرْضُ النَّقَاطِ الْمَكْسُوبَةِ عَلَى مَلَفِّكَ الشَّخْصِيِّ"
|
||||
"amount_hours": "{{amount}} ساعات",
|
||||
"amount_minutes": "{{amount}} دقائق",
|
||||
"last_time_played": "آخر تشغيل {{period}}",
|
||||
"activity": "النشاط الأخير",
|
||||
"library": "المكتبة",
|
||||
"total_play_time": "إجمالي وقت اللعب",
|
||||
"no_recent_activity_title": "همم... لا شيء هنا",
|
||||
"no_recent_activity_description": "لم تلعب أي ألعاب مؤخرًا. حان الوقت لتغيير ذلك!",
|
||||
"display_name": "اسم العرض",
|
||||
"saving": "جارٍ الحفظ",
|
||||
"save": "حفظ",
|
||||
"edit_profile": "تعديل الملف الشخصي",
|
||||
"saved_successfully": "تم الحفظ بنجاح",
|
||||
"try_again": "يرجى المحاولة مرة أخرى",
|
||||
"sign_out_modal_title": "هل أنت متأكد؟",
|
||||
"cancel": "إلغاء",
|
||||
"successfully_signed_out": "تم تسجيل الخروج بنجاح",
|
||||
"sign_out": "تسجيل الخروج",
|
||||
"playing_for": "يلعب لمدة {{amount}}",
|
||||
"sign_out_modal_text": "مكتبتك مرتبطة بحسابك الحالي. عند تسجيل الخروج، لن تكون مكتبتك مرئية بعد الآن، ولن يتم حفظ أي تقدم. هل تتابع تسجيل الخروج؟",
|
||||
"add_friends": "إضافة أصدقاء",
|
||||
"add": "إضافة",
|
||||
"friend_code": "رمز الصديق",
|
||||
"see_profile": "عرض الملف الشخصي",
|
||||
"sending": "جارٍ الإرسال",
|
||||
"friend_request_sent": "تم إرسال طلب الصداقة",
|
||||
"friends": "الأصدقاء",
|
||||
"friends_list": "قائمة الأصدقاء",
|
||||
"user_not_found": "المستخدم غير موجود",
|
||||
"block_user": "حظر المستخدم",
|
||||
"add_friend": "إضافة صديق",
|
||||
"request_sent": "تم إرسال الطلب",
|
||||
"request_received": "تم استلام الطلب",
|
||||
"accept_request": "قبول الطلب",
|
||||
"ignore_request": "تجاهل الطلب",
|
||||
"cancel_request": "إلغاء الطلب",
|
||||
"undo_friendship": "إلغاء الصداقة",
|
||||
"request_accepted": "تم قبول الطلب",
|
||||
"user_blocked_successfully": "تم حظر المستخدم بنجاح",
|
||||
"user_block_modal_text": "سيؤدي هذا إلى حظر {{displayName}}",
|
||||
"blocked_users": "المستخدمون المحظورون",
|
||||
"unblock": "إلغاء الحظر",
|
||||
"no_friends_added": "ليس لديك أصدقاء مضافون",
|
||||
"pending": "قيد الانتظار",
|
||||
"no_pending_invites": "ليس لديك دعوات معلقة",
|
||||
"no_blocked_users": "ليس لديك مستخدمون محظورون",
|
||||
"friend_code_copied": "تم نسخ رمز الصديق",
|
||||
"undo_friendship_modal_text": "سيؤدي هذا إلى إلغاء صداقتك مع {{displayName}}",
|
||||
"privacy_hint": "لضبط من يمكنه رؤية هذا، انتقل إلى <0>الإعدادات</0>",
|
||||
"locked_profile": "هذا الملف الشخصي خاص",
|
||||
"image_process_failure": "فشل معالجة الصورة",
|
||||
"required_field": "هذا الحقل مطلوب",
|
||||
"displayname_min_length": "يجب أن يكون اسم العرض على الأقل 3 أحرف",
|
||||
"displayname_max_length": "يجب ألا يتجاوز اسم العرض 50 حرفًا",
|
||||
"report_profile": "الإبلاغ عن هذا الملف الشخصي",
|
||||
"report_reason": "لماذا تقوم بالإبلاغ عن هذا الملف الشخصي؟",
|
||||
"report_description": "معلومات إضافية",
|
||||
"report_description_placeholder": "معلومات إضافية",
|
||||
"report": "الإبلاغ",
|
||||
"report_reason_hate": "خطاب كراهية",
|
||||
"report_reason_sexual_content": "محتوى جنسي",
|
||||
"report_reason_violence": "عنف",
|
||||
"report_reason_spam": "بريد عشوائي",
|
||||
"report_reason_other": "أخرى",
|
||||
"profile_reported": "تم الإبلاغ عن الملف الشخصي",
|
||||
"your_friend_code": "رمز صديقك:",
|
||||
"upload_banner": "تحميل بانر",
|
||||
"uploading_banner": "جارٍ تحميل البانر...",
|
||||
"background_image_updated": "تم تحديث صورة الخلفية",
|
||||
"stats": "الإحصائيات",
|
||||
"achievements": "إنجازات",
|
||||
"games": "الألعاب",
|
||||
"top_percentile": "ال{{percentile}}% الأعلى",
|
||||
"ranking_updated_weekly": "يتم تحديث التصنيف أسبوعيًا",
|
||||
"playing": "يلعب {{game}}",
|
||||
"achievements_unlocked": "الإنجازات المفتوحة",
|
||||
"earned_points": "النقاط المكتسبة",
|
||||
"show_achievements_on_profile": "عرض إنجازاتك على ملفك الشخصي",
|
||||
"show_points_on_profile": "عرض نقاطك المكتسبة على ملفك الشخصي"
|
||||
},
|
||||
"achievement": {
|
||||
"achievement_unlocked": "إِنْجَازٌ مَفْتُوحٌ",
|
||||
"user_achievements": "إِنْجَازَاتُ {{displayName}}",
|
||||
"your_achievements": "إِنْجَازَاتُكَ",
|
||||
"unlocked_at": "تَمَّ الْفَتْحُ فِي: {{date}}",
|
||||
"subscription_needed": "يَحْتَاجُ اشْتِرَاكُ Hydra Cloud لِرُؤْيَةِ هَذَا الْمُحْتَوَى",
|
||||
"new_achievements_unlocked": "تَمَّ فَتْحُ {{achievementCount}} إِنْجَازَاتٍ جَدِيدَةٍ مِنْ {{gameCount}} أَلْعَابٍ",
|
||||
"achievement_progress": "{{unlockedCount}}/{{totalCount}} إِنْجَازَاتٍ",
|
||||
"achievements_unlocked_for_game": "تَمَّ فَتْحُ {{achievementCount}} إِنْجَازَاتٍ جَدِيدَةٍ لِـ {{gameTitle}}",
|
||||
"hidden_achievement_tooltip": "هَذَا إِنْجَازٌ مَخْفِيٌّ",
|
||||
"achievement_earn_points": "اكْسِبْ {{points}} نَقَاطًا بِهَذَا الإِنْجَازِ",
|
||||
"earned_points": "النَّقَاطُ الْمَكْسُوبَةُ:",
|
||||
"available_points": "النَّقَاطُ الْمُتَوَفِّرَةُ:",
|
||||
"how_to_earn_achievements_points": "كَيْفَ تَكْسِبُ نَقَاطَ الإِنْجَازَاتِ؟"
|
||||
"achievement_unlocked": "تم فتح الإنجاز",
|
||||
"user_achievements": "إنجازات {{displayName}}",
|
||||
"your_achievements": "إنجازاتك",
|
||||
"unlocked_at": "تم الفتح في: {{date}}",
|
||||
"subscription_needed": "يحتاج إلى اشتراك Hydra Cloud لرؤية هذا المحتوى",
|
||||
"new_achievements_unlocked": "تم فتح {{achievementCount}} إنجازات جديدة من {{gameCount}} ألعاب",
|
||||
"achievement_progress": "{{unlockedCount}}/{{totalCount}} إنجازات",
|
||||
"achievements_unlocked_for_game": "تم فتح {{achievementCount}} إنجازات جديدة لـ {{gameTitle}}",
|
||||
"hidden_achievement_tooltip": "هذا إنجاز مخفي",
|
||||
"achievement_earn_points": "اكسب {{points}} نقطة مع هذا الإنجاز",
|
||||
"earned_points": "النقاط المكتسبة:",
|
||||
"available_points": "النقاط المتاحة:",
|
||||
"how_to_earn_achievements_points": "كيفية كسب نقاط الإنجازات؟"
|
||||
},
|
||||
"hydra_cloud": {
|
||||
"subscription_tour_title": "اشْتِرَاكُ Hydra Cloud",
|
||||
"subscribe_now": "اشْتَرِكِ الْآنَ",
|
||||
"cloud_saving": "حِفْظٌ سَحَابِيٌّ",
|
||||
"cloud_achievements": "حِفْظُ إِنْجَازَاتِكَ فِي السَّحَابَةِ",
|
||||
"animated_profile_picture": "صُورُ الْمَلَفِّ الشَّخْصِيِّ الْمُتَحَرِّكَةِ",
|
||||
"premium_support": "الدَّعْمُ الْمُتَقَدِّمُ",
|
||||
"show_and_compare_achievements": "عَرْضٌ وَمُقَارَنَةُ إِنْجَازَاتِكَ مَعَ مُسْتَخْدِمِينَ آخَرِينَ",
|
||||
"animated_profile_banner": "لَافِتَةُ الْمَلَفِّ الشَّخْصِيِّ الْمُتَحَرِّكَةِ",
|
||||
"subscription_tour_title": "اشتراك Hydra Cloud",
|
||||
"subscribe_now": "اشترك الآن",
|
||||
"cloud_saving": "حفظ سحابي",
|
||||
"cloud_achievements": "احفظ إنجازاتك على السحابة",
|
||||
"animated_profile_picture": "صورة ملف شخصي متحركة",
|
||||
"premium_support": "دعم ممتاز",
|
||||
"show_and_compare_achievements": "اعرض وقارن إنجازاتك مع المستخدمين الآخرين",
|
||||
"animated_profile_banner": "بانر ملف شخصي متحرك",
|
||||
"hydra_cloud": "Hydra Cloud",
|
||||
"hydra_cloud_feature_found": "لَقَدْ اكْتَشَفْتَ مِيزَةً مِنْ Hydra Cloud!",
|
||||
"learn_more": "تَعَلَّمْ أَكْثَرَ"
|
||||
"hydra_cloud_feature_found": "لقد اكتشفت ميزة Hydra Cloud!",
|
||||
"learn_more": "معرفة المزيد"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"featured": "Destaques",
|
||||
"hot": "Populares",
|
||||
"weekly": "📅 Mais baixados da semana",
|
||||
"achievements": "🏆 Para platinar",
|
||||
"achievements": "🏆 Pra platinar",
|
||||
"surprise_me": "Surpreenda-me",
|
||||
"no_results": "Nenhum resultado encontrado",
|
||||
"start_typing": "Comece a digitar para pesquisar…"
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
"featured": "Рекомендации",
|
||||
"surprise_me": "Удиви меня",
|
||||
"no_results": "Ничего не найдено",
|
||||
"hot": "Сейчас в топе",
|
||||
"start_typing": "Начинаю вводить текст для поиска...",
|
||||
"hot": "Сейчас популярно",
|
||||
"start_typing": "Начинаю вводить текст...",
|
||||
"weekly": "📅 Лучшие игры недели",
|
||||
"achievements": "🏆 Игры, в которых нужно победить"
|
||||
},
|
||||
@@ -424,7 +424,7 @@
|
||||
"subscribe_now": "Подпишитесь прямо сейчас",
|
||||
"cloud_saving": "Сохранение в облаке",
|
||||
"cloud_achievements": "Сохраняйте свои достижения в облаке",
|
||||
"animated_profile_picture": "Анимированные фотографии профиля",
|
||||
"animated_profile_picture": "Анимированные аватарки",
|
||||
"premium_support": "Премиальная поддержка",
|
||||
"show_and_compare_achievements": "Показывайте и сравнивайте свои достижения с достижениями других пользователей",
|
||||
"animated_profile_banner": "Анимированный баннер профиля",
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { WindowManager } from "@main/services";
|
||||
|
||||
const openEditorWindow = async (_event: Electron.IpcMainInvokeEvent) =>
|
||||
WindowManager.openEditorWindow();
|
||||
|
||||
registerEvent("openEditorWindow", openEditorWindow);
|
||||
@@ -49,7 +49,6 @@ import "./user-preferences/authenticate-real-debrid";
|
||||
import "./download-sources/put-download-source";
|
||||
import "./auth/sign-out";
|
||||
import "./auth/open-auth-window";
|
||||
import "./aparence/open-editor-window";
|
||||
import "./auth/get-session-hash";
|
||||
import "./user/get-user";
|
||||
import "./user/get-blocked-users";
|
||||
|
||||
@@ -190,52 +190,6 @@ export class WindowManager {
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
if (!this.mainWindow) this.createMainWindow();
|
||||
this.loadMainWindowURL(hash);
|
||||
|
||||
@@ -313,7 +313,4 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
/* Notifications */
|
||||
publishNewRepacksNotification: (newRepacksCount: number) =>
|
||||
ipcRenderer.invoke("publishNewRepacksNotification", newRepacksCount),
|
||||
|
||||
/* Editor */
|
||||
openEditorWindow: () => ipcRenderer.invoke("openEditorWindow"),
|
||||
});
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
$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;
|
||||
134
src/renderer/src/app.css.ts
Normal file
134
src/renderer/src/app.css.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
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",
|
||||
});
|
||||
@@ -1,136 +0,0 @@
|
||||
@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;
|
||||
}
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
useUserDetails,
|
||||
} from "@renderer/hooks";
|
||||
|
||||
import * as styles from "./app.css";
|
||||
|
||||
import { Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||
import {
|
||||
setUserPreferences,
|
||||
@@ -28,8 +30,6 @@ import { downloadSourcesTable } from "./dexie";
|
||||
import { useSubscription } from "./hooks/use-subscription";
|
||||
import { HydraCloudModal } from "./pages/shared-modals/hydra-cloud/hydra-cloud-modal";
|
||||
|
||||
import "./app.scss";
|
||||
|
||||
export interface AppProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
@@ -240,11 +240,11 @@ export function App() {
|
||||
return (
|
||||
<>
|
||||
{window.electron.platform === "win32" && (
|
||||
<div className="title-bar">
|
||||
<div className={styles.titleBar}>
|
||||
<h4>
|
||||
Hydra
|
||||
{hasActiveSubscription && (
|
||||
<span className="title-bar__cloud-text"> Cloud</span>
|
||||
<span className={styles.cloudText}> Cloud</span>
|
||||
)}
|
||||
</h4>
|
||||
</div>
|
||||
@@ -275,10 +275,10 @@ export function App() {
|
||||
<main>
|
||||
<Sidebar />
|
||||
|
||||
<article className="app-container">
|
||||
<article className={styles.container}>
|
||||
<Header />
|
||||
|
||||
<section ref={contentRef} className="app-container__content">
|
||||
<section ref={contentRef} className={styles.content}>
|
||||
<Outlet />
|
||||
</section>
|
||||
</article>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
@use "../../scss/variables" as vars;
|
||||
@use "../../scss/globals.scss";
|
||||
|
||||
.profile-avatar {
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: vars.$background-color;
|
||||
border: solid 1px vars.$border-color;
|
||||
background-color: globals.$background-color;
|
||||
border: solid 1px globals.$border-color;
|
||||
cursor: pointer;
|
||||
color: vars.$muted-color;
|
||||
color: globals.$muted-color;
|
||||
position: relative;
|
||||
|
||||
&__image {
|
||||
|
||||
54
src/renderer/src/components/backdrop/backdrop.css.ts
Normal file
54
src/renderer/src/components/backdrop/backdrop.css.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
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`,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,50 +0,0 @@
|
||||
@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);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import "./backdrop.scss";
|
||||
import cn from "classnames";
|
||||
import * as styles from "./backdrop.css";
|
||||
|
||||
export interface BackdropProps {
|
||||
isClosing?: boolean;
|
||||
@@ -9,9 +8,9 @@ export interface BackdropProps {
|
||||
export function Backdrop({ isClosing = false, children }: BackdropProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn("backdrop", {
|
||||
"backdrop--closing": isClosing,
|
||||
"backdrop--windows": window.electron.platform === "win32",
|
||||
className={styles.backdrop({
|
||||
closing: isClosing,
|
||||
windows: window.electron.platform === "win32",
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
@use "../../scss/variables" as vars;
|
||||
@use "../../scss/globals.scss";
|
||||
|
||||
.badge {
|
||||
color: vars.$muted-color;
|
||||
color: globals.$muted-color;
|
||||
font-size: 10px;
|
||||
padding: calc(vars.$spacing-unit / 2) vars.$spacing-unit;
|
||||
border: solid 1px vars.$muted-color;
|
||||
padding: calc(globals.$spacing-unit / 2) globals.$spacing-unit;
|
||||
border: solid 1px globals.$muted-color;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
@use "../../scss/variables" as vars;
|
||||
@use "../../scss/globals.scss";
|
||||
|
||||
.bottom-panel {
|
||||
width: 100%;
|
||||
border-top: solid 1px vars.$border-color;
|
||||
background-color: vars.$background-color;
|
||||
padding: calc(vars.$spacing-unit / 2) calc(vars.$spacing-unit * 2);
|
||||
border-top: solid 1px globals.$border-color;
|
||||
background-color: globals.$background-color;
|
||||
padding: calc(globals.$spacing-unit / 2) calc(globals.$spacing-unit * 2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: all ease 0.2s;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
z-index: vars.$bottom-panel-z-index;
|
||||
z-index: globals.$bottom-panel-z-index;
|
||||
|
||||
&__downloads-button {
|
||||
color: vars.$body-color;
|
||||
color: globals.$body-color;
|
||||
border-bottom: solid 1px transparent;
|
||||
|
||||
&:hover {
|
||||
border-bottom: solid 1px vars.$body-color;
|
||||
border-bottom: solid 1px globals.$body-color;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
@use "../../scss/variables" as vars;
|
||||
@use "../../scss/globals.scss";
|
||||
|
||||
.button {
|
||||
padding: vars.$spacing-unit vars.$spacing-unit * 2;
|
||||
background-color: vars.$muted-color;
|
||||
padding: globals.$spacing-unit globals.$spacing-unit * 2;
|
||||
background-color: globals.$muted-color;
|
||||
border-radius: 8px;
|
||||
border: solid 1px transparent;
|
||||
transition: all ease 0.2s;
|
||||
@@ -11,14 +11,14 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: vars.$spacing-unit;
|
||||
gap: globals.$spacing-unit;
|
||||
|
||||
&:active {
|
||||
opacity: vars.$active-opacity;
|
||||
opacity: globals.$active-opacity;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: vars.$disabled-opacity;
|
||||
opacity: globals.$disabled-opacity;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@@ -28,14 +28,14 @@
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: vars.$muted-color;
|
||||
background-color: globals.$muted-color;
|
||||
}
|
||||
}
|
||||
|
||||
&--outline {
|
||||
background-color: transparent;
|
||||
border: solid 1px vars.$border-color;
|
||||
color: vars.$muted-color;
|
||||
border: solid 1px globals.$border-color;
|
||||
color: globals.$muted-color;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
@@ -47,14 +47,14 @@
|
||||
}
|
||||
|
||||
&--dark {
|
||||
background-color: vars.$dark-background-color;
|
||||
color: vars.$muted-color;
|
||||
background-color: globals.$dark-background-color;
|
||||
color: globals.$muted-color;
|
||||
}
|
||||
|
||||
&--danger {
|
||||
border-color: transparent;
|
||||
background-color: vars.$danger-color;
|
||||
color: vars.$muted-color;
|
||||
background-color: globals.$danger-color;
|
||||
color: globals.$muted-color;
|
||||
|
||||
&:hover {
|
||||
background-color: #b3203f;
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
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",
|
||||
});
|
||||
@@ -1,39 +0,0 @@
|
||||
@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;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useId } from "react";
|
||||
import * as styles from "./checkbox-field.css";
|
||||
import { CheckIcon } from "@primer/octicons-react";
|
||||
import "./checkbox-field.scss";
|
||||
|
||||
export interface CheckboxFieldProps
|
||||
extends React.DetailedHTMLProps<
|
||||
@@ -14,19 +14,17 @@ export function CheckboxField({ label, ...props }: CheckboxFieldProps) {
|
||||
const id = useId();
|
||||
|
||||
return (
|
||||
<div className="checkbox-field">
|
||||
<div
|
||||
className={`checkbox-field__checkbox ${props.checked ? "checked" : ""}`}
|
||||
>
|
||||
<div className={styles.checkboxField}>
|
||||
<div className={styles.checkbox({ checked: props.checked })}>
|
||||
<input
|
||||
id={id}
|
||||
type="checkbox"
|
||||
className="checkbox-field__input"
|
||||
className={styles.checkboxInput}
|
||||
{...props}
|
||||
/>
|
||||
{props.checked && <CheckIcon />}
|
||||
</div>
|
||||
<label htmlFor={id} className="checkbox-field__label">
|
||||
<label htmlFor={id} className={styles.checkboxLabel}>
|
||||
{label}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
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",
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
@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;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button } from "../button/button";
|
||||
import { Modal, type ModalProps } from "../modal/modal";
|
||||
|
||||
import "./confirmation-modal.scss";
|
||||
import * as styles from "./confirmation-modal.css";
|
||||
|
||||
export interface ConfirmationModalProps extends Omit<ModalProps, "children"> {
|
||||
confirmButtonLabel: string;
|
||||
@@ -31,10 +31,10 @@ export function ConfirmationModal({
|
||||
|
||||
return (
|
||||
<Modal {...props}>
|
||||
<div className="confirmation-modal">
|
||||
<p className="confirmation-modal__description">{descriptionText}</p>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "16px" }}>
|
||||
<p className={styles.descriptionText}>{descriptionText}</p>
|
||||
|
||||
<div className="confirmation-modal__actions">
|
||||
<div className={styles.actions}>
|
||||
<Button theme="outline" onClick={handleCancelClick}>
|
||||
{cancelButtonLabel}
|
||||
</Button>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
@use "../../scss/variables" as vars;
|
||||
@use "../../scss/globals.scss";
|
||||
|
||||
.dropdown-menu {
|
||||
&__content {
|
||||
background-color: vars.$dark-background-color;
|
||||
border: 1px solid vars.$border-color;
|
||||
background-color: globals.$dark-background-color;
|
||||
border: 1px solid globals.$border-color;
|
||||
border-radius: 6px;
|
||||
min-width: 200px;
|
||||
flex-direction: column;
|
||||
@@ -20,13 +20,13 @@
|
||||
padding: 4px 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: vars.$muted-color;
|
||||
color: globals.$muted-color;
|
||||
}
|
||||
|
||||
&__separator {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: vars.$border-color;
|
||||
background-color: globals.$border-color;
|
||||
}
|
||||
|
||||
&__item {
|
||||
@@ -49,12 +49,12 @@
|
||||
}
|
||||
|
||||
&:not(&__item--disabled) &__item:hover {
|
||||
background-color: vars.$background-color;
|
||||
color: vars.$muted-color;
|
||||
background-color: globals.$background-color;
|
||||
color: globals.$muted-color;
|
||||
}
|
||||
|
||||
&__item:focus {
|
||||
background-color: vars.$background-color;
|
||||
background-color: globals.$background-color;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
|
||||
106
src/renderer/src/components/game-card/game-card.css.ts
Normal file
106
src/renderer/src/components/game-card/game-card.css.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
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",
|
||||
});
|
||||
@@ -1,102 +0,0 @@
|
||||
@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);
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,7 @@ import type { GameStats } from "@types";
|
||||
|
||||
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||
|
||||
import "./game-card.scss";
|
||||
|
||||
import * as styles from "./game-card.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Badge } from "../badge/badge";
|
||||
import { useCallback, useState } from "react";
|
||||
@@ -20,7 +19,7 @@ export interface GameCardProps
|
||||
}
|
||||
|
||||
const shopIcon = {
|
||||
steam: <SteamLogo className="game-card__shop-icon" />,
|
||||
steam: <SteamLogo className={styles.shopIcon} />,
|
||||
};
|
||||
|
||||
export function GameCard({ game, ...props }: GameCardProps) {
|
||||
@@ -49,25 +48,25 @@ export function GameCard({ game, ...props }: GameCardProps) {
|
||||
<button
|
||||
{...props}
|
||||
type="button"
|
||||
className="game-card"
|
||||
className={styles.card}
|
||||
onMouseEnter={handleHover}
|
||||
>
|
||||
<div className="game-card__backdrop">
|
||||
<div className={styles.backdrop}>
|
||||
<img
|
||||
src={steamUrlBuilder.library(game.objectId)}
|
||||
alt={game.title}
|
||||
className="game-card__cover"
|
||||
className={styles.cover}
|
||||
loading="lazy"
|
||||
/>
|
||||
|
||||
<div className="game-card__content">
|
||||
<div className="game-card__title-container">
|
||||
<div className={styles.content}>
|
||||
<div className={styles.titleContainer}>
|
||||
{shopIcon[game.shop]}
|
||||
<p className="game-card__title">{game.title}</p>
|
||||
<p className={styles.title}>{game.title}</p>
|
||||
</div>
|
||||
|
||||
{uniqueRepackers.length > 0 ? (
|
||||
<ul className="game-card__download-options">
|
||||
<ul className={styles.downloadOptions}>
|
||||
{uniqueRepackers.map((repacker) => (
|
||||
<li key={repacker}>
|
||||
<Badge>{repacker}</Badge>
|
||||
@@ -75,17 +74,17 @@ export function GameCard({ game, ...props }: GameCardProps) {
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="game-card__no-download-label">{t("no_downloads")}</p>
|
||||
<p className={styles.noDownloadsLabel}>{t("no_downloads")}</p>
|
||||
)}
|
||||
<div className="game-card__specifics">
|
||||
<div className="game-card__specifics-item">
|
||||
<div className={styles.specifics}>
|
||||
<div className={styles.specificsItem}>
|
||||
<DownloadIcon />
|
||||
<span>
|
||||
{stats ? numberFormatter.format(stats.downloadCount) : "…"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="game-card__specifics-item">
|
||||
<div className={styles.specificsItem}>
|
||||
<PeopleIcon />
|
||||
<span>
|
||||
{stats ? numberFormatter.format(stats?.playerCount) : "…"}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
@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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { useEffect, useState } from "react";
|
||||
import { SyncIcon } from "@primer/octicons-react";
|
||||
import { Link } from "../link/link";
|
||||
import "./auto-update-header.scss";
|
||||
import * as styles from "./header.css";
|
||||
import type { AppUpdaterEvent } from "@types";
|
||||
|
||||
export const releasesPageUrl =
|
||||
@@ -45,15 +45,9 @@ export function AutoUpdateSubHeader() {
|
||||
|
||||
if (!isAutoInstallAvailable) {
|
||||
return (
|
||||
<header className="auto-update-sub-header">
|
||||
<Link
|
||||
to={releasesPageUrl}
|
||||
className="auto-update-sub-header__new-version-link"
|
||||
>
|
||||
<SyncIcon
|
||||
className="auto-update-sub-header__new-version-icon"
|
||||
size={12}
|
||||
/>
|
||||
<header className={styles.subheader}>
|
||||
<Link to={releasesPageUrl} className={styles.newVersionLink}>
|
||||
<SyncIcon className={styles.newVersionIcon} size={12} />
|
||||
{t("version_available_download", { version: newVersion })}
|
||||
</Link>
|
||||
</header>
|
||||
@@ -62,16 +56,13 @@ export function AutoUpdateSubHeader() {
|
||||
|
||||
if (isReadyToInstall) {
|
||||
return (
|
||||
<header className="auto-update-sub-header">
|
||||
<header className={styles.subheader}>
|
||||
<button
|
||||
type="button"
|
||||
className="auto-update-sub-header__new-version-button"
|
||||
className={styles.newVersionButton}
|
||||
onClick={handleClickInstallUpdate}
|
||||
>
|
||||
<SyncIcon
|
||||
className="auto-update-sub-header__new-version-icon"
|
||||
size={12}
|
||||
/>
|
||||
<SyncIcon className={styles.newVersionIcon} size={12} />
|
||||
{t("version_available_install", { version: newVersion })}
|
||||
</button>
|
||||
</header>
|
||||
|
||||
182
src/renderer/src/components/header/header.css.ts
Normal file
182
src/renderer/src/components/header/header.css.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
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,
|
||||
});
|
||||
@@ -1,145 +0,0 @@
|
||||
@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;
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,9 @@ import { ArrowLeftIcon, SearchIcon, XIcon } from "@primer/octicons-react";
|
||||
|
||||
import { useAppDispatch, useAppSelector } from "@renderer/hooks";
|
||||
|
||||
import "./header.scss";
|
||||
import * as styles from "./header.css";
|
||||
import { AutoUpdateSubHeader } from "./auto-update-sub-header";
|
||||
import { setFilters } from "@renderer/features";
|
||||
import cn from "classnames";
|
||||
|
||||
const pathTitle: Record<string, string> = {
|
||||
"/": "home",
|
||||
@@ -76,16 +75,16 @@ export function Header() {
|
||||
return (
|
||||
<>
|
||||
<header
|
||||
className={cn("header", {
|
||||
"header--dragging-disabled": draggingDisabled,
|
||||
"header--is-windows": window.electron.platform === "win32",
|
||||
className={styles.header({
|
||||
draggingDisabled,
|
||||
isWindows: window.electron.platform === "win32",
|
||||
})}
|
||||
>
|
||||
<section className="header__section" style={{ flex: 1 }}>
|
||||
<section className={styles.section} style={{ flex: 1 }}>
|
||||
<button
|
||||
type="button"
|
||||
className={cn("header__back-button", {
|
||||
"header__back-button--enabled": location.key !== "default",
|
||||
className={styles.backButton({
|
||||
enabled: location.key !== "default",
|
||||
})}
|
||||
onClick={handleBackButtonClick}
|
||||
disabled={location.key === "default"}
|
||||
@@ -94,23 +93,19 @@ export function Header() {
|
||||
</button>
|
||||
|
||||
<h3
|
||||
className={cn("header__title", {
|
||||
"header__title--has-back-button": location.key !== "default",
|
||||
className={styles.title({
|
||||
hasBackButton: location.key !== "default",
|
||||
})}
|
||||
>
|
||||
{title}
|
||||
</h3>
|
||||
</section>
|
||||
|
||||
<section className="header__section">
|
||||
<div
|
||||
className={cn("header__search", {
|
||||
"header__search--focused": isFocused,
|
||||
})}
|
||||
>
|
||||
<section className={styles.section}>
|
||||
<div className={styles.search({ focused: isFocused })}>
|
||||
<button
|
||||
type="button"
|
||||
className="header__action-button"
|
||||
className={styles.actionButton}
|
||||
onClick={focusInput}
|
||||
>
|
||||
<SearchIcon />
|
||||
@@ -122,7 +117,7 @@ export function Header() {
|
||||
name="search"
|
||||
placeholder={t("search")}
|
||||
value={searchValue}
|
||||
className="header__search-input"
|
||||
className={styles.searchInput}
|
||||
onChange={(event) => handleSearch(event.target.value)}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={handleBlur}
|
||||
@@ -132,7 +127,7 @@ export function Header() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => dispatch(setFilters({ title: "" }))}
|
||||
className="header__action-button"
|
||||
className={styles.actionButton}
|
||||
>
|
||||
<XIcon />
|
||||
</button>
|
||||
|
||||
60
src/renderer/src/components/hero/hero.css.ts
Normal file
60
src/renderer/src/components/hero/hero.css.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
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",
|
||||
});
|
||||
@@ -1,57 +0,0 @@
|
||||
@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;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import * as styles from "./hero.css";
|
||||
import { useEffect, useState } from "react";
|
||||
import type { TrendingGame } from "@types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Skeleton from "react-loading-skeleton";
|
||||
import "./hero.scss";
|
||||
|
||||
export function Hero() {
|
||||
const [featuredGameDetails, setFeaturedGameDetails] = useState<
|
||||
@@ -29,7 +29,7 @@ export function Hero() {
|
||||
}, [i18n.language]);
|
||||
|
||||
if (isLoading) {
|
||||
return <Skeleton className="hero" />;
|
||||
return <Skeleton className={styles.hero} />;
|
||||
}
|
||||
|
||||
if (featuredGameDetails?.length) {
|
||||
@@ -37,17 +37,17 @@ export function Hero() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate(game.uri)}
|
||||
className="hero"
|
||||
className={styles.hero}
|
||||
key={index}
|
||||
>
|
||||
<div className="hero__backdrop">
|
||||
<div className={styles.backdrop}>
|
||||
<img
|
||||
src={game.background}
|
||||
alt={game.description}
|
||||
className="hero__media"
|
||||
className={styles.heroMedia}
|
||||
/>
|
||||
|
||||
<div className="hero__content">
|
||||
<div className={styles.content}>
|
||||
{game.logo && (
|
||||
<img
|
||||
src={game.logo}
|
||||
@@ -56,7 +56,7 @@ export function Hero() {
|
||||
loading="eager"
|
||||
/>
|
||||
)}
|
||||
<p className="hero__description">{game.description}</p>
|
||||
<p className={styles.description}>{game.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
9
src/renderer/src/components/link/link.css.ts
Normal file
9
src/renderer/src/components/link/link.css.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
export const link = style({
|
||||
textDecoration: "none",
|
||||
color: "#C0C1C7",
|
||||
":hover": {
|
||||
textDecoration: "underline",
|
||||
},
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
@use "../../scss/variables" as vars;
|
||||
|
||||
.link {
|
||||
text-decoration: none;
|
||||
color: vars.$muted-color;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Link as ReactRouterDomLink, LinkProps } from "react-router-dom";
|
||||
import cn from "classnames";
|
||||
import "./link.scss";
|
||||
import * as styles from "./link.css";
|
||||
|
||||
export function Link({ children, to, className, ...props }: LinkProps) {
|
||||
const openExternal = (event: React.MouseEvent) => {
|
||||
@@ -12,7 +12,7 @@ export function Link({ children, to, className, ...props }: LinkProps) {
|
||||
return (
|
||||
<a
|
||||
href={to}
|
||||
className={cn("link", className)}
|
||||
className={cn(styles.link, className)}
|
||||
onClick={openExternal}
|
||||
{...props}
|
||||
>
|
||||
@@ -22,7 +22,11 @@ export function Link({ children, to, className, ...props }: LinkProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<ReactRouterDomLink className={cn("link", className)} to={to} {...props}>
|
||||
<ReactRouterDomLink
|
||||
className={cn(styles.link, className)}
|
||||
to={to}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</ReactRouterDomLink>
|
||||
);
|
||||
|
||||
78
src/renderer/src/components/modal/modal.css.ts
Normal file
78
src/renderer/src/components/modal/modal.css.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
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,
|
||||
});
|
||||
@@ -1,77 +0,0 @@
|
||||
@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;
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,10 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { XIcon } from "@primer/octicons-react";
|
||||
|
||||
import "./modal.scss";
|
||||
import * as styles from "./modal.css";
|
||||
|
||||
import { Backdrop } from "../backdrop/backdrop";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import cn from "classnames";
|
||||
|
||||
export interface ModalProps {
|
||||
visible: boolean;
|
||||
@@ -109,17 +108,14 @@ export function Modal({
|
||||
return createPortal(
|
||||
<Backdrop isClosing={isClosing}>
|
||||
<div
|
||||
className={cn("modal", {
|
||||
modal__closing: isClosing,
|
||||
modal__large: large,
|
||||
})}
|
||||
className={styles.modal({ closing: isClosing, large })}
|
||||
role="dialog"
|
||||
aria-labelledby={title}
|
||||
aria-describedby={description}
|
||||
ref={modalContentRef}
|
||||
data-hydra-dialog
|
||||
>
|
||||
<div className="modal__header">
|
||||
<div className={styles.modalHeader}>
|
||||
<div style={{ display: "flex", gap: 4, flexDirection: "column" }}>
|
||||
<h3>{title}</h3>
|
||||
{description && <p>{description}</p>}
|
||||
@@ -128,13 +124,13 @@ export function Modal({
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCloseClick}
|
||||
className="modal__close-button"
|
||||
className={styles.closeModalButton}
|
||||
aria-label={t("close")}
|
||||
>
|
||||
<XIcon className="modal__close-button-icon" size={24} />
|
||||
<XIcon className={styles.closeModalButtonIcon} size={24} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="modal__content">{children}</div>
|
||||
<div className={styles.modalContent}>{children}</div>
|
||||
</div>
|
||||
</Backdrop>,
|
||||
document.body
|
||||
|
||||
59
src/renderer/src/components/select-field/select-field.css.ts
Normal file
59
src/renderer/src/components/select-field/select-field.css.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
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,
|
||||
});
|
||||
@@ -1,49 +0,0 @@
|
||||
@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;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
import { useId, useState } from "react";
|
||||
import "./select-field.scss";
|
||||
import cn from "classnames";
|
||||
import type { RecipeVariants } from "@vanilla-extract/recipes";
|
||||
import * as styles from "./select-field.css";
|
||||
|
||||
export interface SelectProps
|
||||
extends React.DetailedHTMLProps<
|
||||
React.SelectHTMLAttributes<HTMLSelectElement>,
|
||||
HTMLSelectElement
|
||||
> {
|
||||
theme?: "primary" | "dark";
|
||||
theme?: NonNullable<RecipeVariants<typeof styles.select>>["theme"];
|
||||
label?: string;
|
||||
options?: { key: string; value: string; label: string }[];
|
||||
}
|
||||
@@ -25,20 +25,16 @@ export function SelectField({
|
||||
return (
|
||||
<div style={{ flex: 1 }}>
|
||||
{label && (
|
||||
<label htmlFor={id} className="select-field__label">
|
||||
<label htmlFor={id} className={styles.label}>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={cn("select-field", `select-field--${theme}`, {
|
||||
"select-field__focused": isFocused,
|
||||
})}
|
||||
>
|
||||
<div className={styles.select({ focused: isFocused, theme })}>
|
||||
<select
|
||||
id={id}
|
||||
value={value}
|
||||
className="select-field__option"
|
||||
className={styles.option}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
onChange={onChange}
|
||||
|
||||
79
src/renderer/src/components/sidebar/sidebar-profile.css.ts
Normal file
79
src/renderer/src/components/sidebar/sidebar-profile.css.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
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",
|
||||
});
|
||||
@@ -1,79 +0,0 @@
|
||||
@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;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { PeopleIcon } from "@primer/octicons-react";
|
||||
import * as styles from "./sidebar-profile.css";
|
||||
import { useAppSelector, useUserDetails } from "@renderer/hooks";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -7,7 +8,6 @@ import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-mo
|
||||
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||
import { Avatar } from "../avatar/avatar";
|
||||
import { AuthPage } from "@shared";
|
||||
import "./sidebar-profile.scss";
|
||||
|
||||
const LONG_POLLING_INTERVAL = 120_000;
|
||||
|
||||
@@ -50,14 +50,14 @@ export function SidebarProfile() {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="sidebar-profile__friends-button"
|
||||
className={styles.friendsButton}
|
||||
onClick={() =>
|
||||
showFriendsModal(UserFriendModalTab.AddFriend, userDetails.id)
|
||||
}
|
||||
title={t("friends")}
|
||||
>
|
||||
{friendRequestCount > 0 && (
|
||||
<small className="sidebar-profile__friends-button-badge">
|
||||
<small className={styles.friendsButtonBadge}>
|
||||
{friendRequestCount > 99 ? "99+" : friendRequestCount}
|
||||
</small>
|
||||
)}
|
||||
@@ -85,21 +85,21 @@ export function SidebarProfile() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="sidebar-profile">
|
||||
<div className={styles.profileContainer}>
|
||||
<button
|
||||
type="button"
|
||||
className="sidebar-profile__button"
|
||||
className={styles.profileButton}
|
||||
onClick={handleProfileClick}
|
||||
>
|
||||
<div className="sidebar-profile__button-content">
|
||||
<div className={styles.profileButtonContent}>
|
||||
<Avatar
|
||||
size={35}
|
||||
src={userDetails?.profileImageUrl}
|
||||
alt={userDetails?.displayName}
|
||||
/>
|
||||
|
||||
<div className="sidebar-profile__button-information">
|
||||
<p className="sidebar-profile__button-title">
|
||||
<div className={styles.profileButtonInformation}>
|
||||
<p className={styles.profileButtonTitle}>
|
||||
{userDetails ? userDetails.displayName : t("sign_in")}
|
||||
</p>
|
||||
|
||||
|
||||
152
src/renderer/src/components/sidebar/sidebar.css.ts
Normal file
152
src/renderer/src/components/sidebar/sidebar.css.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
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%",
|
||||
});
|
||||
@@ -1,136 +0,0 @@
|
||||
@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%;
|
||||
}
|
||||
}
|
||||
@@ -14,14 +14,12 @@ import {
|
||||
|
||||
import { routes } from "./routes";
|
||||
|
||||
import "./sidebar.scss";
|
||||
|
||||
import * as styles from "./sidebar.css";
|
||||
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||
|
||||
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||
import { SidebarProfile } from "./sidebar-profile";
|
||||
import { sortBy } from "lodash-es";
|
||||
import cn from "classnames";
|
||||
import { CommentDiscussionIcon } from "@primer/octicons-react";
|
||||
|
||||
const SIDEBAR_MIN_WIDTH = 200;
|
||||
@@ -170,9 +168,9 @@ export function Sidebar() {
|
||||
return (
|
||||
<aside
|
||||
ref={sidebarRef}
|
||||
className={cn("sidebar", {
|
||||
sidebar__resizing: isResizing,
|
||||
sidebar__darwin: window.electron.platform === "darwin",
|
||||
className={styles.sidebar({
|
||||
resizing: isResizing,
|
||||
darwin: window.electron.platform === "darwin",
|
||||
})}
|
||||
style={{
|
||||
width: sidebarWidth,
|
||||
@@ -185,19 +183,19 @@ export function Sidebar() {
|
||||
>
|
||||
<SidebarProfile />
|
||||
|
||||
<div className="sidebar__content">
|
||||
<section className="sidebar__section">
|
||||
<ul className="sidebar__menu">
|
||||
<div className={styles.content}>
|
||||
<section className={styles.section}>
|
||||
<ul className={styles.menu}>
|
||||
{routes.map(({ nameKey, path, render }) => (
|
||||
<li
|
||||
key={nameKey}
|
||||
className={cn("sidebar__menu-item", {
|
||||
"sidebar__menu-item--active": location.pathname === path,
|
||||
className={styles.menuItem({
|
||||
active: location.pathname === path,
|
||||
})}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="sidebar__menu-item-button"
|
||||
className={styles.menuItemButton}
|
||||
onClick={() => handleSidebarItemClick(path)}
|
||||
>
|
||||
{render()}
|
||||
@@ -208,8 +206,8 @@ export function Sidebar() {
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="sidebar__section">
|
||||
<small className="sidebar__section-title">{t("my_library")}</small>
|
||||
<section className={styles.section}>
|
||||
<small className={styles.sectionTitle}>{t("my_library")}</small>
|
||||
|
||||
<TextField
|
||||
ref={filterRef}
|
||||
@@ -218,34 +216,34 @@ export function Sidebar() {
|
||||
theme="dark"
|
||||
/>
|
||||
|
||||
<ul className="sidebar__menu">
|
||||
<ul className={styles.menu}>
|
||||
{filteredLibrary.map((game) => (
|
||||
<li
|
||||
key={game.id}
|
||||
className={cn("sidebar__menu-item", {
|
||||
"sidebar__menu-item--active":
|
||||
className={styles.menuItem({
|
||||
active:
|
||||
location.pathname ===
|
||||
`/game/${game.shop}/${game.objectID}`,
|
||||
"sidebar__menu-item--muted": game.status === "removed",
|
||||
muted: game.status === "removed",
|
||||
})}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="sidebar__menu-item-button"
|
||||
className={styles.menuItemButton}
|
||||
onClick={(event) => handleSidebarGameClick(event, game)}
|
||||
>
|
||||
{game.iconUrl ? (
|
||||
<img
|
||||
className="sidebar__game-icon"
|
||||
className={styles.gameIcon}
|
||||
src={game.iconUrl}
|
||||
alt={game.title}
|
||||
loading="lazy"
|
||||
/>
|
||||
) : (
|
||||
<SteamLogo className="sidebar__game-icon" />
|
||||
<SteamLogo className={styles.gameIcon} />
|
||||
)}
|
||||
|
||||
<span className="sidebar__menu-item-button-label">
|
||||
<span className={styles.menuItemButtonLabel}>
|
||||
{getGameTitle(game)}
|
||||
</span>
|
||||
</button>
|
||||
@@ -259,10 +257,10 @@ export function Sidebar() {
|
||||
{hasActiveSubscription && (
|
||||
<button
|
||||
type="button"
|
||||
className="sidebar__help-button"
|
||||
className={styles.helpButton}
|
||||
data-open-support-chat
|
||||
>
|
||||
<div className="sidebar__help-button-icon">
|
||||
<div className={styles.helpButtonIcon}>
|
||||
<CommentDiscussionIcon size={14} />
|
||||
</div>
|
||||
<span>{t("need_help")}</span>
|
||||
@@ -271,7 +269,7 @@ export function Sidebar() {
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="sidebar__handle"
|
||||
className={styles.handle}
|
||||
onMouseDown={handleMouseDown}
|
||||
/>
|
||||
</aside>
|
||||
|
||||
89
src/renderer/src/components/text-field/text-field.css.ts
Normal file
89
src/renderer/src/components/text-field/text-field.css.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
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,
|
||||
});
|
||||
@@ -1,75 +0,0 @@
|
||||
@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;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
import React, { useId, useMemo, useState } from "react";
|
||||
import type { RecipeVariants } from "@vanilla-extract/recipes";
|
||||
import { EyeClosedIcon, EyeIcon } from "@primer/octicons-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import cn from "classnames";
|
||||
|
||||
import "./text-field.scss";
|
||||
import * as styles from "./text-field.css";
|
||||
|
||||
export interface TextFieldProps
|
||||
extends React.DetailedHTMLProps<
|
||||
React.InputHTMLAttributes<HTMLInputElement>,
|
||||
HTMLInputElement
|
||||
> {
|
||||
theme?: "primary" | "dark";
|
||||
theme?: NonNullable<RecipeVariants<typeof styles.textField>>["theme"];
|
||||
label?: string | React.ReactNode;
|
||||
hint?: string | React.ReactNode;
|
||||
textFieldProps?: React.DetailedHTMLProps<
|
||||
@@ -41,7 +41,9 @@ export const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>(
|
||||
) => {
|
||||
const id = useId();
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
|
||||
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
|
||||
|
||||
const { t } = useTranslation("forms");
|
||||
|
||||
const showPasswordToggleButton = props.type === "password";
|
||||
@@ -52,12 +54,7 @@ export const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>(
|
||||
}, [props.type, isPasswordVisible]);
|
||||
|
||||
const hintContent = useMemo(() => {
|
||||
if (error && typeof error === "object" && "message" in error)
|
||||
return (
|
||||
<small className="text-field__error-label">
|
||||
{error.message as string}
|
||||
</small>
|
||||
);
|
||||
if (error) return <small className={styles.errorLabel}>{error}</small>;
|
||||
|
||||
if (hint) return <small>{hint}</small>;
|
||||
return null;
|
||||
@@ -76,23 +73,22 @@ export const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>(
|
||||
const hasError = !!error;
|
||||
|
||||
return (
|
||||
<div className="text-field-container" {...containerProps}>
|
||||
<div className={styles.textFieldContainer} {...containerProps}>
|
||||
{label && <label htmlFor={id}>{label}</label>}
|
||||
|
||||
<div className="text-field__wrapper">
|
||||
<div className={styles.textFieldWrapper}>
|
||||
<div
|
||||
className={cn("text-field", `text-field__${theme}`, {
|
||||
"text-field__has-error": hasError,
|
||||
"text-field--focused": isFocused,
|
||||
className={styles.textField({
|
||||
theme,
|
||||
hasError,
|
||||
focused: isFocused,
|
||||
})}
|
||||
{...textFieldProps}
|
||||
>
|
||||
<input
|
||||
ref={ref}
|
||||
id={id}
|
||||
className={cn("text-field__input", {
|
||||
"text-field__input__read-only": props.readOnly,
|
||||
})}
|
||||
className={styles.textFieldInput({ readOnly: props.readOnly })}
|
||||
{...props}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
@@ -102,7 +98,7 @@ export const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>(
|
||||
{showPasswordToggleButton && (
|
||||
<button
|
||||
type="button"
|
||||
className="text-field__toggle-password-button"
|
||||
className={styles.togglePasswordButton}
|
||||
onClick={() => setIsPasswordVisible(!isPasswordVisible)}
|
||||
aria-label={t("toggle_password_visibility")}
|
||||
>
|
||||
@@ -124,4 +120,4 @@ export const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>(
|
||||
}
|
||||
);
|
||||
|
||||
TextField.displayName = "TextField";
|
||||
TextField.displayName = "TextField";
|
||||
|
||||
87
src/renderer/src/components/toast/toast.css.ts
Normal file
87
src/renderer/src/components/toast/toast.css.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
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,
|
||||
});
|
||||
@@ -1,92 +0,0 @@
|
||||
@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;
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,8 @@ import {
|
||||
XIcon,
|
||||
} from "@primer/octicons-react";
|
||||
|
||||
import "./toast.scss";
|
||||
import cn from "classnames";
|
||||
import * as styles from "./toast.css";
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
|
||||
export interface ToastProps {
|
||||
visible: boolean;
|
||||
@@ -77,28 +77,22 @@ export function Toast({ visible, message, type, onClose }: ToastProps) {
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("toast", {
|
||||
toast__closing: isClosing,
|
||||
})}
|
||||
>
|
||||
<div className="toast__content">
|
||||
<div className="toast__icon-container">
|
||||
<div className={styles.toast({ closing: isClosing })}>
|
||||
<div className={styles.toastContent}>
|
||||
<div style={{ display: "flex", gap: `${SPACING_UNIT}px` }}>
|
||||
{type === "success" && (
|
||||
<CheckCircleFillIcon className="toast__success-icon" />
|
||||
<CheckCircleFillIcon className={styles.successIcon} />
|
||||
)}
|
||||
|
||||
{type === "error" && (
|
||||
<XCircleFillIcon className="toast__error-icon" />
|
||||
)}
|
||||
{type === "error" && <XCircleFillIcon className={styles.errorIcon} />}
|
||||
|
||||
{type === "warning" && <AlertIcon className="toast__warning-icon" />}
|
||||
{type === "warning" && <AlertIcon className={styles.warningIcon} />}
|
||||
<span style={{ fontWeight: "bold" }}>{message}</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="toast__close-button"
|
||||
className={styles.closeButton}
|
||||
onClick={startAnimateClosing}
|
||||
aria-label="Close toast"
|
||||
>
|
||||
@@ -106,7 +100,7 @@ export function Toast({ visible, message, type, onClose }: ToastProps) {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<progress className="toast__progress" value={progress} max={100} />
|
||||
<progress className={styles.progress} value={progress} max={100} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
3
src/renderer/src/declaration.d.ts
vendored
3
src/renderer/src/declaration.d.ts
vendored
@@ -260,9 +260,6 @@ declare global {
|
||||
|
||||
/* Notifications */
|
||||
publishNewRepacksNotification: (newRepacksCount: number) => Promise<void>;
|
||||
|
||||
/* Editor */
|
||||
openEditorWindow: () => Promise<void>;
|
||||
}
|
||||
|
||||
interface Window {
|
||||
|
||||
@@ -30,7 +30,6 @@ const Downloads = React.lazy(() => import("./pages/downloads/downloads"));
|
||||
const Settings = React.lazy(() => import("./pages/settings/settings"));
|
||||
const Catalogue = React.lazy(() => import("./pages/catalogue/catalogue"));
|
||||
const Profile = React.lazy(() => import("./pages/profile/profile"));
|
||||
const Editor = React.lazy(() => import("./pages/editor/editor"));
|
||||
const Achievements = React.lazy(
|
||||
() => import("./pages/achievements/achievements")
|
||||
);
|
||||
@@ -105,11 +104,6 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
element={<SuspenseWrapper Component={Achievements} />}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
<Route
|
||||
path="/editor"
|
||||
element={<SuspenseWrapper Component={Editor} />}
|
||||
/>
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
</Provider>
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { useDate } from "@renderer/hooks";
|
||||
import type { UserAchievement } from "@types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import * as styles from "./achievements.css";
|
||||
import { EyeClosedIcon } from "@primer/octicons-react";
|
||||
import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
|
||||
import { useSubscription } from "@renderer/hooks/use-subscription";
|
||||
import classNames from "classnames";
|
||||
import "./achievements.scss";
|
||||
import "../../scss/_variables.scss";
|
||||
import { vars } from "@renderer/theme.css";
|
||||
|
||||
interface AchievementListProps {
|
||||
achievements: UserAchievement[];
|
||||
@@ -18,16 +17,16 @@ export function AchievementList({ achievements }: AchievementListProps) {
|
||||
const { formatDateTime } = useDate();
|
||||
|
||||
return (
|
||||
<ul className="achievements__list">
|
||||
<ul className={styles.list}>
|
||||
{achievements.map((achievement) => (
|
||||
<li
|
||||
key={achievement.name}
|
||||
className="achievements__list-item"
|
||||
className={styles.listItem}
|
||||
style={{ display: "flex" }}
|
||||
>
|
||||
<img
|
||||
className={classNames("achievements__list-item-image", {
|
||||
"achievements__list-item-image--unlocked": achievement.unlocked,
|
||||
className={styles.listItemImage({
|
||||
unlocked: achievement.unlocked,
|
||||
})}
|
||||
src={achievement.icon}
|
||||
alt={achievement.displayName}
|
||||
@@ -67,7 +66,7 @@ export function AchievementList({ achievements }: AchievementListProps) {
|
||||
alignItems: "center",
|
||||
gap: "4px",
|
||||
cursor: "pointer",
|
||||
color: "var(--warning-color)",
|
||||
color: vars.color.warning,
|
||||
}}
|
||||
title={t("achievement_earn_points", {
|
||||
points: "???",
|
||||
|
||||
71
src/renderer/src/pages/achievements/achievement-panel.css.ts
Normal file
71
src/renderer/src/pages/achievements/achievement-panel.css.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
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",
|
||||
},
|
||||
});
|
||||
@@ -1,66 +0,0 @@
|
||||
@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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,8 @@ import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
|
||||
import { UserAchievement } from "@types";
|
||||
import { useSubscription } from "@renderer/hooks/use-subscription";
|
||||
import { useUserDetails } from "@renderer/hooks";
|
||||
|
||||
import "./achievement-panel.scss";
|
||||
import { vars } from "@renderer/theme.css";
|
||||
import * as styles from "./achievement-panel.css";
|
||||
|
||||
export interface AchievementPanelProps {
|
||||
achievements: UserAchievement[];
|
||||
@@ -28,17 +28,17 @@ export function AchievementPanel({ achievements }: AchievementPanelProps) {
|
||||
|
||||
if (!hasActiveSubscription) {
|
||||
return (
|
||||
<div className="achievement-panel">
|
||||
<div className="achievement-panel__content">
|
||||
<div className={styles.panel}>
|
||||
<div className={styles.content}>
|
||||
{t("earned_points")} <HydraIcon width={20} height={20} />
|
||||
??? / ???
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => showHydraCloudModal("achievements-points")}
|
||||
className="achievement-panel__link"
|
||||
className={styles.link}
|
||||
>
|
||||
<small style={{ color: "#ffc107" }}>
|
||||
<small style={{ color: vars.color.warning }}>
|
||||
{t("how_to_earn_achievements_points")}
|
||||
</small>
|
||||
</button>
|
||||
@@ -47,8 +47,8 @@ export function AchievementPanel({ achievements }: AchievementPanelProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="achievement-panel">
|
||||
<div className="achievement-panel__content">
|
||||
<div className={styles.panel}>
|
||||
<div className={styles.content}>
|
||||
{t("earned_points")} <HydraIcon width={20} height={20} />
|
||||
{achievementsPointsEarnedSum} / {achievementsPointsTotal}
|
||||
</div>
|
||||
|
||||
@@ -8,19 +8,18 @@ import {
|
||||
formatDownloadProgress,
|
||||
} from "@renderer/helpers";
|
||||
import { LockIcon, PersonIcon, TrophyIcon } from "@primer/octicons-react";
|
||||
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
||||
import { gameDetailsContext } from "@renderer/context";
|
||||
import type { ComparedAchievements } from "@types";
|
||||
import { average } from "color.js";
|
||||
import Color from "color";
|
||||
import { Link } from "@renderer/components";
|
||||
import { ComparedAchievementList } from "./compared-achievement-list";
|
||||
import * as styles from "./achievements.css";
|
||||
import { AchievementList } from "./achievement-list";
|
||||
import { AchievementPanel } from "./achievement-panel";
|
||||
import { ComparedAchievementPanel } from "./compared-achievement-panel";
|
||||
import { useSubscription } from "@renderer/hooks/use-subscription";
|
||||
import classNames from "classnames";
|
||||
import "./achievements.scss";
|
||||
import "../../scss/_variables.scss";
|
||||
|
||||
interface UserInfo {
|
||||
id: string;
|
||||
@@ -49,10 +48,10 @@ function AchievementSummary({ user, isComparison }: AchievementSummaryProps) {
|
||||
user: Pick<UserInfo, "profileImageUrl" | "displayName">
|
||||
) => {
|
||||
return (
|
||||
<div className="achievements__profile-avatar">
|
||||
<div className={styles.profileAvatar}>
|
||||
{user.profileImageUrl ? (
|
||||
<img
|
||||
className="achievements__profile-avatar"
|
||||
className={styles.profileAvatar}
|
||||
src={user.profileImageUrl}
|
||||
alt={user.displayName}
|
||||
/>
|
||||
@@ -65,38 +64,97 @@ function AchievementSummary({ user, isComparison }: AchievementSummaryProps) {
|
||||
|
||||
if (isComparison && userDetails?.id == user.id && !hasActiveSubscription) {
|
||||
return (
|
||||
<div className="achievements__summary achievements__summary--locked">
|
||||
<div className="achievements__summary-overlay">
|
||||
<div
|
||||
style={{
|
||||
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} />
|
||||
<h3>
|
||||
<button
|
||||
className="achievements__subscription-required-button"
|
||||
className={styles.subscriptionRequiredButton}
|
||||
onClick={() => showHydraCloudModal("achievements")}
|
||||
>
|
||||
{t("subscription_needed")}
|
||||
</button>
|
||||
</h3>
|
||||
</div>
|
||||
<div className="achievements__summary-content achievements__summary-content--blurred">
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
alignItems: "center",
|
||||
height: "62px",
|
||||
position: "relative",
|
||||
filter: "blur(4px)",
|
||||
}}
|
||||
>
|
||||
{getProfileImage(user)}
|
||||
<h1 className="achievements__summary-title">{user.displayName}</h1>
|
||||
<h1 style={{ marginBottom: "8px" }}>{user.displayName}</h1>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="achievements__summary">
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
alignItems: "center",
|
||||
padding: `${SPACING_UNIT}px`,
|
||||
}}
|
||||
>
|
||||
{getProfileImage(user)}
|
||||
<div className="achievements__summary-details">
|
||||
<h1 className="achievements__summary-title">{user.displayName}</h1>
|
||||
<div className="achievements__summary-stats">
|
||||
<div className="achievements__summary-count">
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
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} />
|
||||
<span>
|
||||
{user.unlockedAchievementCount} / {user.totalAchievementCount}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span>
|
||||
{formatDownloadProgress(
|
||||
user.unlockedAchievementCount / user.totalAchievementCount
|
||||
@@ -106,7 +164,7 @@ function AchievementSummary({ user, isComparison }: AchievementSummaryProps) {
|
||||
<progress
|
||||
max={1}
|
||||
value={user.unlockedAchievementCount / user.totalAchievementCount}
|
||||
className="achievements__progress-bar"
|
||||
className={styles.achievementsProgressBar}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -143,10 +201,9 @@ export function AchievementsContent({
|
||||
|
||||
setGameColor(backgroundColor);
|
||||
};
|
||||
const HERO_HEIGHT = 150;
|
||||
|
||||
const onScroll: React.UIEventHandler<HTMLElement> = (event) => {
|
||||
const heroHeight = heroRef.current?.clientHeight ?? HERO_HEIGHT;
|
||||
const heroHeight = heroRef.current?.clientHeight ?? styles.HERO_HEIGHT;
|
||||
|
||||
const scrollY = (event.target as HTMLDivElement).scrollTop;
|
||||
if (scrollY >= heroHeight && !isHeaderStuck) {
|
||||
@@ -162,10 +219,10 @@ export function AchievementsContent({
|
||||
user: Pick<UserInfo, "profileImageUrl" | "displayName">
|
||||
) => {
|
||||
return (
|
||||
<div className="achievements__profile-avatar-small">
|
||||
<div className={styles.profileAvatarSmall}>
|
||||
{user.profileImageUrl ? (
|
||||
<img
|
||||
className="achievements__profile-avatar-small"
|
||||
className={styles.profileAvatarSmall}
|
||||
src={user.profileImageUrl}
|
||||
alt={user.displayName}
|
||||
/>
|
||||
@@ -179,10 +236,10 @@ export function AchievementsContent({
|
||||
if (!objectId || !shop || !gameTitle || !userDetails) return null;
|
||||
|
||||
return (
|
||||
<div className="achievements__wrapper">
|
||||
<div className={styles.wrapper}>
|
||||
<img
|
||||
src={steamUrlBuilder.libraryHero(objectId)}
|
||||
className="achievements__hidden-image"
|
||||
style={{ display: "none" }}
|
||||
alt={gameTitle}
|
||||
onLoad={handleHeroLoad}
|
||||
/>
|
||||
@@ -190,29 +247,38 @@ export function AchievementsContent({
|
||||
<section
|
||||
ref={containerRef}
|
||||
onScroll={onScroll}
|
||||
className="achievements__container"
|
||||
className={styles.container}
|
||||
>
|
||||
<div
|
||||
className="achievements__gradient-background"
|
||||
style={{
|
||||
background: `linear-gradient(0deg, #1c1c1c 0%, ${gameColor} 100%)`,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
background: `linear-gradient(0deg, ${vars.color.darkBackground} 0%, ${gameColor} 100%)`,
|
||||
}}
|
||||
>
|
||||
<div ref={heroRef} className="achievements__hero">
|
||||
<div className="achievements__hero-content">
|
||||
<div ref={heroRef} className={styles.hero}>
|
||||
<div className={styles.heroContent}>
|
||||
<Link
|
||||
to={buildGameDetailsPath({ shop, objectId, title: gameTitle })}
|
||||
>
|
||||
<img
|
||||
src={steamUrlBuilder.logo(objectId)}
|
||||
className="achievements__game-logo"
|
||||
className={styles.gameLogo}
|
||||
alt={gameTitle}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="achievements__summary-container">
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
padding: `${SPACING_UNIT}px`,
|
||||
}}
|
||||
>
|
||||
<AchievementSummary
|
||||
user={{
|
||||
...userDetails,
|
||||
@@ -232,24 +298,24 @@ export function AchievementsContent({
|
||||
</div>
|
||||
|
||||
{otherUser && (
|
||||
<div
|
||||
className={classNames("achievements__table-header", {
|
||||
"achievements__table-header--stuck": isHeaderStuck,
|
||||
})}
|
||||
>
|
||||
<div className={styles.tableHeader({ stuck: isHeaderStuck })}>
|
||||
<div
|
||||
className={classNames("achievements__grid-container", {
|
||||
"achievements__grid-container--no-subscription":
|
||||
!hasActiveSubscription,
|
||||
})}
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: hasActiveSubscription
|
||||
? "3fr 1fr 1fr"
|
||||
: "3fr 2fr",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
padding: `${SPACING_UNIT}px ${SPACING_UNIT * 3}px`,
|
||||
}}
|
||||
>
|
||||
<div></div>
|
||||
{hasActiveSubscription && (
|
||||
<div className="achievements__profile-center">
|
||||
<div style={{ display: "flex", justifyContent: "center" }}>
|
||||
{getProfileImage({ ...userDetails })}
|
||||
</div>
|
||||
)}
|
||||
<div className="achievements__profile-center">
|
||||
<div style={{ display: "flex", justifyContent: "center" }}>
|
||||
{getProfileImage(otherUser)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import Skeleton from "react-loading-skeleton";
|
||||
import "./achievements.scss";
|
||||
import * as styles from "./achievements.css";
|
||||
|
||||
export function AchievementsSkeleton() {
|
||||
return (
|
||||
<div className="achievements__container">
|
||||
<div className="achievements__hero">
|
||||
<Skeleton className="achievements__hero-image-skeleton" />
|
||||
<div className={styles.container}>
|
||||
<div className={styles.hero}>
|
||||
<Skeleton className={styles.heroImageSkeleton} />
|
||||
</div>
|
||||
<div className="achievements__hero-panel-skeleton"></div>
|
||||
<div className={styles.heroPanelSkeleton}></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
197
src/renderer/src/pages/achievements/achievements.css.ts
Normal file
197
src/renderer/src/pages/achievements/achievements.css.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
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",
|
||||
},
|
||||
});
|
||||
@@ -1,188 +0,0 @@
|
||||
@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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,7 @@ import { useAppDispatch, useUserDetails } from "@renderer/hooks";
|
||||
import type { ComparedAchievements, GameShop } from "@types";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
|
||||
import "./achievements.scss";
|
||||
import { vars } from "@renderer/theme.css";
|
||||
import {
|
||||
GameDetailsContextConsumer,
|
||||
GameDetailsContextProvider,
|
||||
@@ -76,7 +75,10 @@ export default function Achievements() {
|
||||
(otherUserId && comparedAchievements === null);
|
||||
|
||||
return (
|
||||
<SkeletonTheme baseColor="var(--background-color)" highlightColor="#444">
|
||||
<SkeletonTheme
|
||||
baseColor={vars.color.background}
|
||||
highlightColor="#444"
|
||||
>
|
||||
{showSkeleton ? (
|
||||
<AchievementsSkeleton />
|
||||
) : (
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import type { ComparedAchievements } from "@types";
|
||||
import * as styles from "./achievements.css";
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
EyeClosedIcon,
|
||||
LockIcon,
|
||||
} from "@primer/octicons-react";
|
||||
import { useDate } from "@renderer/hooks";
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import classNames from "classnames";
|
||||
import "./achievements.scss";
|
||||
import "../../scss/_variables.scss";
|
||||
|
||||
export interface ComparedAchievementListProps {
|
||||
achievements: ComparedAchievements;
|
||||
@@ -21,11 +20,11 @@ export function ComparedAchievementList({
|
||||
const { formatDateTime } = useDate();
|
||||
|
||||
return (
|
||||
<ul className="achievements__list">
|
||||
<ul className={styles.list}>
|
||||
{achievements.achievements.map((achievement, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className="achievements__list-item"
|
||||
className={styles.listItem}
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: achievement.ownerStat
|
||||
@@ -38,14 +37,12 @@ export function ComparedAchievementList({
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: `var(--spacing-unit)`,
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
className={classNames("achievements__list-item-image", {
|
||||
"achievements__list-item-image--unlocked":
|
||||
achievement.ownerStat?.unlocked ||
|
||||
achievement.targetStat.unlocked,
|
||||
className={styles.listItemImage({
|
||||
unlocked: true,
|
||||
})}
|
||||
src={achievement.icon}
|
||||
alt={achievement.displayName}
|
||||
@@ -74,7 +71,7 @@ export function ComparedAchievementList({
|
||||
whiteSpace: "nowrap",
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
gap: `var(--spacing-unit)`,
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
justifyContent: "center",
|
||||
}}
|
||||
title={formatDateTime(achievement.ownerStat.unlockTime!)}
|
||||
@@ -85,7 +82,7 @@ export function ComparedAchievementList({
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
padding: `var(--spacing-unit)`,
|
||||
padding: `${SPACING_UNIT}px`,
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
@@ -100,7 +97,7 @@ export function ComparedAchievementList({
|
||||
whiteSpace: "nowrap",
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
gap: `var(--spacing-unit)`,
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
justifyContent: "center",
|
||||
}}
|
||||
title={formatDateTime(achievement.targetStat.unlockTime!)}
|
||||
@@ -111,7 +108,7 @@ export function ComparedAchievementList({
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
padding: `var(--spacing-unit)`,
|
||||
padding: `${SPACING_UNIT}px`,
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import * as styles from "./achievement-panel.css";
|
||||
|
||||
import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
|
||||
import { ComparedAchievements } from "@types";
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
import { useUserDetails } from "@renderer/hooks";
|
||||
import classNames from "classnames";
|
||||
|
||||
import "./achievement-panel.scss";
|
||||
import "../../scss/_variables.scss";
|
||||
|
||||
export interface ComparedAchievementPanelProps {
|
||||
achievements: ComparedAchievements;
|
||||
@@ -20,24 +18,24 @@ export function ComparedAchievementPanel({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames("achievement-panel", {
|
||||
"achievement-panel--subscribed": hasActiveSubscription,
|
||||
})}
|
||||
className={styles.panel}
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: hasActiveSubscription ? "3fr 1fr 1fr" : "3fr 2fr",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
}}
|
||||
>
|
||||
<div className="achievement-panel__points">
|
||||
{t("available_points")}
|
||||
<HydraIcon width={20} height={20} />
|
||||
<div style={{ display: "flex", gap: `${SPACING_UNIT}px` }}>
|
||||
{t("available_points")} <HydraIcon width={20} height={20} />{" "}
|
||||
{achievements.achievementsPointsTotal}
|
||||
</div>
|
||||
|
||||
{hasActiveSubscription && (
|
||||
<div className="achievement-panel__content">
|
||||
<div className={styles.content}>
|
||||
<HydraIcon width={20} height={20} />
|
||||
{achievements.owner.achievementsPointsEarnedSum ?? 0}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="achievement-panel__content">
|
||||
<div className={styles.content}>
|
||||
<HydraIcon width={20} height={20} />
|
||||
{achievements.target.achievementsPointsEarnedSum}
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
@use "../../scss/variables" as vars;
|
||||
@use "../../scss/globals.scss";
|
||||
|
||||
.catalogue {
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(vars.$spacing-unit * 2);
|
||||
gap: calc(globals.$spacing-unit * 2);
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
scroll-behavior: smooth;
|
||||
@@ -13,16 +13,10 @@
|
||||
width: 270px;
|
||||
min-width: 270px;
|
||||
max-width: 270px;
|
||||
background-color: vars.$dark-background-color;
|
||||
background-color: globals.$dark-background-color;
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
border: 1px solid vars.$border-color;
|
||||
border: 1px solid globals.$border-color;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
gap: calc(var(--spacing-unit) * 2);
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ import {
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import "./catalogue.scss";
|
||||
import "../../scss/_variables.scss";
|
||||
|
||||
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
||||
import { downloadSourcesTable } from "@renderer/dexie";
|
||||
import { FilterSection } from "./filter-section";
|
||||
import { setFilters, setPage } from "@renderer/features";
|
||||
@@ -270,7 +270,13 @@ export default function Catalogue() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="catalogue__header">
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: SPACING_UNIT * 2,
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
@@ -281,8 +287,8 @@ export default function Catalogue() {
|
||||
>
|
||||
{isLoading ? (
|
||||
<SkeletonTheme
|
||||
baseColor="var(--dark-background-color)"
|
||||
highlightColor="var(--background-color)"
|
||||
baseColor={vars.color.darkBackground}
|
||||
highlightColor={vars.color.background}
|
||||
>
|
||||
{Array.from({ length: PAGE_SIZE }).map((_, i) => (
|
||||
<Skeleton
|
||||
@@ -290,7 +296,7 @@ export default function Catalogue() {
|
||||
style={{
|
||||
height: 105,
|
||||
borderRadius: 4,
|
||||
border: `solid 1px var(--border-color)`,
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { vars } from "@renderer/theme.css";
|
||||
import { XIcon } from "@primer/octicons-react";
|
||||
import "../../scss/_variables.scss";
|
||||
|
||||
interface FilterItemProps {
|
||||
filter: string;
|
||||
@@ -13,11 +13,11 @@ export function FilterItem({ filter, orbColor, onRemove }: FilterItemProps) {
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
color: "var(--body-color)",
|
||||
backgroundColor: "var(--dark-background-color",
|
||||
color: vars.color.body,
|
||||
backgroundColor: vars.color.darkBackground,
|
||||
padding: "6px 12px",
|
||||
borderRadius: 4,
|
||||
border: `solid 1px var(--border-color)`,
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
@@ -35,7 +35,7 @@ export function FilterItem({ filter, orbColor, onRemove }: FilterItemProps) {
|
||||
type="button"
|
||||
onClick={onRemove}
|
||||
style={{
|
||||
color: "var(--body-color)",
|
||||
color: vars.color.body,
|
||||
marginLeft: 4,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
|
||||
@@ -3,8 +3,8 @@ import { useFormat } from "@renderer/hooks";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
|
||||
import List from "rc-virtual-list";
|
||||
import { vars } from "@renderer/theme.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import "../../scss/_variables.scss";
|
||||
|
||||
export interface FilterSectionProps {
|
||||
title: string;
|
||||
@@ -80,7 +80,7 @@ export function FilterSection({
|
||||
fontSize: 12,
|
||||
marginBottom: 12,
|
||||
display: "block",
|
||||
color: "var(--body-color)",
|
||||
color: vars.color.body,
|
||||
cursor: "pointer",
|
||||
textDecoration: "underline",
|
||||
}}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@use "../../scss/variables" as vars;
|
||||
@use "../../scss/globals.scss";
|
||||
|
||||
.game-item {
|
||||
background-color: vars.$dark-background-color;
|
||||
background-color: globals.$dark-background-color;
|
||||
width: 100%;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
@@ -9,9 +9,9 @@
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
border-radius: 4px;
|
||||
border: 1px solid vars.$border-color;
|
||||
border: 1px solid globals.$border-color;
|
||||
cursor: pointer;
|
||||
gap: calc(vars.$spacing-unit * 2);
|
||||
gap: calc(globals.$spacing-unit * 2);
|
||||
transition: all ease 0.2s;
|
||||
|
||||
&:hover {
|
||||
@@ -22,7 +22,7 @@
|
||||
width: 200px;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-right: 1px solid vars.$border-color;
|
||||
border-right: 1px solid globals.$border-color;
|
||||
}
|
||||
|
||||
&__details {
|
||||
@@ -30,11 +30,11 @@
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
padding: calc(vars.$spacing-unit * 2) 0;
|
||||
padding: calc(globals.$spacing-unit * 2) 0;
|
||||
}
|
||||
|
||||
&__genres {
|
||||
color: vars.$body-color;
|
||||
color: globals.$body-color;
|
||||
font-size: 12px;
|
||||
text-align: left;
|
||||
margin-bottom: 4px;
|
||||
@@ -42,7 +42,7 @@
|
||||
|
||||
&__repackers {
|
||||
display: flex;
|
||||
gap: vars.$spacing-unit;
|
||||
gap: globals.$spacing-unit;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
11
src/renderer/src/pages/downloads/delete-game-modal.css.ts
Normal file
11
src/renderer/src/pages/downloads/delete-game-modal.css.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
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`,
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
@use "../../scss/variables" as vars;
|
||||
|
||||
.delete-game-modal {
|
||||
&__actions-buttons-ctn {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: end;
|
||||
align-items: center;
|
||||
gap: vars.$spacing-unit;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Button, Modal } from "@renderer/components";
|
||||
|
||||
import "./delete-game-modal.scss";
|
||||
import * as styles from "./delete-game-modal.css";
|
||||
|
||||
interface DeleteGameModalProps {
|
||||
visible: boolean;
|
||||
@@ -29,7 +29,7 @@ export function DeleteGameModal({
|
||||
description={t("delete_modal_description")}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className="delete-game-modal__actions-buttons-ctn">
|
||||
<div className={styles.deleteActionsButtonsCtn}>
|
||||
<Button onClick={handleDeleteGame} theme="outline">
|
||||
{t("delete")}
|
||||
</Button>
|
||||
|
||||
109
src/renderer/src/pages/downloads/download-group.css.ts
Normal file
109
src/renderer/src/pages/downloads/download-group.css.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||
|
||||
export const downloadTitleWrapper = style({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
marginBottom: `${SPACING_UNIT}px`,
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
});
|
||||
|
||||
export const downloadTitle = style({
|
||||
fontWeight: "bold",
|
||||
cursor: "pointer",
|
||||
color: vars.color.body,
|
||||
textAlign: "left",
|
||||
fontSize: "16px",
|
||||
display: "block",
|
||||
":hover": {
|
||||
textDecoration: "underline",
|
||||
},
|
||||
});
|
||||
|
||||
export const downloads = style({
|
||||
width: "100%",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
margin: "0",
|
||||
padding: "0",
|
||||
marginTop: `${SPACING_UNIT}px`,
|
||||
});
|
||||
|
||||
export const downloadCover = style({
|
||||
width: "280px",
|
||||
minWidth: "280px",
|
||||
height: "auto",
|
||||
borderRight: `solid 1px ${vars.color.border}`,
|
||||
position: "relative",
|
||||
zIndex: "1",
|
||||
});
|
||||
|
||||
export const downloadCoverContent = style({
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
padding: `${SPACING_UNIT}px`,
|
||||
display: "flex",
|
||||
alignItems: "flex-end",
|
||||
justifyContent: "flex-end",
|
||||
});
|
||||
|
||||
export const downloadCoverBackdrop = style({
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
background: "linear-gradient(0deg, rgba(0, 0, 0, 0.8) 5%, transparent 100%)",
|
||||
display: "flex",
|
||||
overflow: "hidden",
|
||||
zIndex: "1",
|
||||
});
|
||||
|
||||
export const downloadCoverImage = style({
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "absolute",
|
||||
zIndex: "-1",
|
||||
});
|
||||
|
||||
export const download = style({
|
||||
width: "100%",
|
||||
backgroundColor: vars.color.background,
|
||||
display: "flex",
|
||||
borderRadius: "8px",
|
||||
border: `solid 1px ${vars.color.border}`,
|
||||
overflow: "hidden",
|
||||
boxShadow: "0px 0px 5px 0px #000000",
|
||||
transition: "all ease 0.2s",
|
||||
height: "140px",
|
||||
minHeight: "140px",
|
||||
maxHeight: "140px",
|
||||
});
|
||||
|
||||
export const downloadDetails = style({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flex: "1",
|
||||
justifyContent: "center",
|
||||
gap: `${SPACING_UNIT / 2}px`,
|
||||
fontSize: "14px",
|
||||
});
|
||||
|
||||
export const downloadRightContent = style({
|
||||
display: "flex",
|
||||
padding: `${SPACING_UNIT * 2}px`,
|
||||
flex: "1",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
background: "linear-gradient(90deg, transparent 20%, rgb(0 0 0 / 20%) 100%)",
|
||||
});
|
||||
|
||||
export const downloadActions = style({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
});
|
||||
|
||||
export const downloadGroup = style({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
});
|
||||
@@ -1,117 +0,0 @@
|
||||
@use "../../scss/variables" as vars;
|
||||
|
||||
.download-group {
|
||||
&__title-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: vars.$spacing-unit;
|
||||
gap: vars.$spacing-unit;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
color: vars.$body-color;
|
||||
text-align: left;
|
||||
font-size: 16px;
|
||||
display: block;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
&__downloads-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: vars.$spacing-unit * 2;
|
||||
}
|
||||
|
||||
&__downloads {
|
||||
width: 100%;
|
||||
gap: #{vars.$spacing-unit * 2};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
margin-top: vars.$spacing-unit;
|
||||
}
|
||||
|
||||
&__cover {
|
||||
width: 280px;
|
||||
min-width: 280px;
|
||||
height: auto;
|
||||
border-right: solid 1px vars.$border-color;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&__cover-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: vars.$spacing-unit;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
&__cover-backdrop {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(0deg, rgba(0, 0, 0, 0.8) 5%, transparent 100%);
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&__cover-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
&__download {
|
||||
width: 100%;
|
||||
background-color: vars.$background-color;
|
||||
display: flex;
|
||||
border-radius: 8px;
|
||||
border: solid 1px vars.$border-color;
|
||||
overflow: hidden;
|
||||
box-shadow: 0px 0px 5px 0px #000000;
|
||||
transition: all ease 0.2s;
|
||||
height: 140px;
|
||||
min-height: 140px;
|
||||
max-height: 140px;
|
||||
}
|
||||
|
||||
&__details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
gap: calc(vars.$spacing-unit / 2);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&__right-content {
|
||||
display: flex;
|
||||
padding: #{vars.$spacing-unit * 2};
|
||||
flex: 1;
|
||||
gap: vars.$spacing-unit;
|
||||
background: linear-gradient(90deg, transparent 20%, rgb(0 0 0 / 20%) 100%);
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: vars.$spacing-unit;
|
||||
}
|
||||
|
||||
&__group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: #{vars.$spacing-unit * 2};
|
||||
}
|
||||
}
|
||||
@@ -12,10 +12,9 @@ import { Downloader, formatBytes, steamUrlBuilder } from "@shared";
|
||||
import { DOWNLOADER_NAME } from "@renderer/constants";
|
||||
import { useAppSelector, useDownload } from "@renderer/hooks";
|
||||
|
||||
import "./download-group.scss";
|
||||
import "../../scss/_variables.scss";
|
||||
|
||||
import * as styles from "./download-group.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -239,47 +238,54 @@ export function DownloadGroup({
|
||||
if (!library.length) return null;
|
||||
|
||||
return (
|
||||
<div className="download-group">
|
||||
<div className="download-group__downloads-group">
|
||||
<div className={styles.downloadGroup}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: `${SPACING_UNIT * 2}px`,
|
||||
}}
|
||||
>
|
||||
<h2>{title}</h2>
|
||||
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: "var(--border-color)",
|
||||
backgroundColor: vars.color.border,
|
||||
height: "1px",
|
||||
}}
|
||||
/>
|
||||
<h3 style={{ fontWeight: "400" }}>{library.length}</h3>
|
||||
</div>
|
||||
|
||||
<ul className="download-group__downloads">
|
||||
<ul className={styles.downloads}>
|
||||
{library.map((game) => {
|
||||
return (
|
||||
<li
|
||||
key={game.id}
|
||||
className="download-group__download"
|
||||
className={styles.download}
|
||||
style={{ position: "relative" }}
|
||||
>
|
||||
<div className="download-group__cover">
|
||||
<div className="download-group__cover-backdrop">
|
||||
<div className={styles.downloadCover}>
|
||||
<div className={styles.downloadCoverBackdrop}>
|
||||
<img
|
||||
src={steamUrlBuilder.library(game.objectID)}
|
||||
className="download-group__cover-image"
|
||||
className={styles.downloadCoverImage}
|
||||
alt={game.title}
|
||||
/>
|
||||
|
||||
<div className="download-group__cover-content">
|
||||
<div className={styles.downloadCoverContent}>
|
||||
<Badge>{DOWNLOADER_NAME[game.downloader]}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="download-group__right-content">
|
||||
<div className="download-group__details">
|
||||
<div className="download-group__title-wrapper">
|
||||
<div className={styles.downloadRightContent}>
|
||||
<div className={styles.downloadDetails}>
|
||||
<div className={styles.downloadTitleWrapper}>
|
||||
<button
|
||||
type="button"
|
||||
className="download-group__title"
|
||||
className={styles.downloadTitle}
|
||||
onClick={() =>
|
||||
navigate(
|
||||
buildGameDetailsPath({
|
||||
|
||||
37
src/renderer/src/pages/downloads/downloads.css.ts
Normal file
37
src/renderer/src/pages/downloads/downloads.css.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
import { SPACING_UNIT } from "../../theme.css";
|
||||
|
||||
export const downloadsContainer = style({
|
||||
display: "flex",
|
||||
padding: `${SPACING_UNIT * 3}px`,
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
});
|
||||
|
||||
export const downloadGroups = style({
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT * 3}px`,
|
||||
flexDirection: "column",
|
||||
});
|
||||
|
||||
export const arrowIcon = style({
|
||||
width: "60px",
|
||||
height: "60px",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: "rgba(255, 255, 255, 0.06)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
marginBottom: `${SPACING_UNIT * 2}px`,
|
||||
});
|
||||
|
||||
export const noDownloads = style({
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
});
|
||||
@@ -1,37 +0,0 @@
|
||||
@use "../../scss/variables" as vars;
|
||||
|
||||
.downloads {
|
||||
&__container {
|
||||
display: flex;
|
||||
padding: #{vars.$spacing-unit * 3};
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__groups {
|
||||
display: flex;
|
||||
gap: #{vars.$spacing-unit * 3};
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__arrow-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(255, 255, 255, 0.06);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: #{vars.$spacing-unit * 2};
|
||||
}
|
||||
|
||||
&__no-downloads {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
gap: vars.$spacing-unit;
|
||||
}
|
||||
}
|
||||
@@ -4,15 +4,13 @@ import { useDownload, useLibrary } from "@renderer/hooks";
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { BinaryNotFoundModal } from "../shared-modals/binary-not-found-modal";
|
||||
|
||||
import * as styles from "./downloads.css";
|
||||
import { DeleteGameModal } from "./delete-game-modal";
|
||||
import { DownloadGroup } from "./download-group";
|
||||
import type { LibraryGame, SeedingStatus } from "@types";
|
||||
import { orderBy } from "lodash-es";
|
||||
import { ArrowDownIcon } from "@primer/octicons-react";
|
||||
|
||||
import "./downloads.scss";
|
||||
|
||||
export default function Downloads() {
|
||||
const { library, updateLibrary } = useLibrary();
|
||||
|
||||
@@ -123,8 +121,8 @@ export default function Downloads() {
|
||||
/>
|
||||
|
||||
{hasItemsInLibrary ? (
|
||||
<section className="downloads__container">
|
||||
<div className="downloads__groups">
|
||||
<section className={styles.downloadsContainer}>
|
||||
<div className={styles.downloadGroups}>
|
||||
{downloadGroups.map((group) => (
|
||||
<DownloadGroup
|
||||
key={group.title}
|
||||
@@ -138,8 +136,8 @@ export default function Downloads() {
|
||||
</div>
|
||||
</section>
|
||||
) : (
|
||||
<div className="downloads__no-downloads">
|
||||
<div className="downloads__arrow-icon">
|
||||
<div className={styles.noDownloads}>
|
||||
<div className={styles.arrowIcon}>
|
||||
<ArrowDownIcon size={24} />
|
||||
</div>
|
||||
<h2>{t("no_downloads_title")}</h2>
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
@use "../../scss/variables" as vars;
|
||||
|
||||
.editor-header {
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
background-color: vars.$dark-background-color;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: start;
|
||||
padding: 0 calc(vars.$spacing-unit * 2);
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
.editor-header-title {
|
||||
font-size: 7px;
|
||||
font-weight: 500;
|
||||
color: vars.$body-color;
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { useEffect } from "react";
|
||||
import { SkeletonTheme } from "react-loading-skeleton";
|
||||
import "./editor.scss";
|
||||
import "../../scss/_variables.scss";
|
||||
|
||||
export default function Editor() {
|
||||
useEffect(() => {
|
||||
console.log("spectre");
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SkeletonTheme baseColor="var(--background-color)" highlightColor="#444">
|
||||
<div className="editor-header">
|
||||
<div className="editor-header-title">
|
||||
<h1>CSS Editor</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="editor"
|
||||
style={{
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<p>spectre</p>
|
||||
</div>
|
||||
</SkeletonTheme>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
import { SPACING_UNIT, vars } from "../../../theme.css";
|
||||
|
||||
export const mappingMethods = style({
|
||||
display: "grid",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
gridTemplateColumns: "repeat(2, 1fr)",
|
||||
});
|
||||
|
||||
export const fileList = style({
|
||||
listStyle: "none",
|
||||
margin: "0",
|
||||
padding: "0",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
marginTop: `${SPACING_UNIT * 2}px`,
|
||||
});
|
||||
|
||||
export const fileItem = style({
|
||||
flex: 1,
|
||||
color: vars.color.muted,
|
||||
textDecoration: "underline",
|
||||
display: "flex",
|
||||
cursor: "pointer",
|
||||
});
|
||||
@@ -1,27 +0,0 @@
|
||||
@use "../../scss/variables" as vars;
|
||||
|
||||
.cloud-sync-files-modal {
|
||||
&__mapping-methods {
|
||||
display: grid;
|
||||
gap: vars.$spacing-unit;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
&__file-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: vars.$spacing-unit;
|
||||
margin-top: #{vars.$spacing-unit * 2};
|
||||
}
|
||||
|
||||
&__file-item {
|
||||
flex: 1;
|
||||
color: vars.$muted-color;
|
||||
text-decoration: underline;
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { cloudSyncContext, gameDetailsContext } from "@renderer/context";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { CheckCircleFillIcon, FileDirectoryIcon } from "@primer/octicons-react";
|
||||
|
||||
import * as styles from "./cloud-sync-files-modal.css";
|
||||
import { formatBytes } from "@shared";
|
||||
import { useToast } from "@renderer/hooks";
|
||||
import { useForm } from "react-hook-form";
|
||||
@@ -98,7 +99,7 @@ export function CloudSyncFilesModal({
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
<span style={{ marginBottom: 8 }}>{t("mapping_method_label")}</span>
|
||||
|
||||
<div className="clound-sync-files-modal__mapping-methods">
|
||||
<div className={styles.mappingMethods}>
|
||||
{Object.values(FileMappingMethod).map((mappingMethod) => (
|
||||
<Button
|
||||
key={mappingMethod}
|
||||
@@ -141,11 +142,11 @@ export function CloudSyncFilesModal({
|
||||
/>
|
||||
)}
|
||||
|
||||
<ul className="cloud-sync-files-modal__files-list">
|
||||
<ul className={styles.fileList}>
|
||||
{files.map((file) => (
|
||||
<li key={file.path} style={{ display: "flex" }}>
|
||||
<button
|
||||
className="cloud-sync-files-modal__file-item"
|
||||
className={styles.fileItem}
|
||||
onClick={() => window.electron.showItemInFolder(file.path)}
|
||||
>
|
||||
{file.path.split("/").at(-1)}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { keyframes, style } from "@vanilla-extract/css";
|
||||
|
||||
import { SPACING_UNIT, vars } from "../../../theme.css";
|
||||
|
||||
export const rotate = keyframes({
|
||||
"0%": { transform: "rotate(0deg)" },
|
||||
"100%": {
|
||||
transform: "rotate(360deg)",
|
||||
},
|
||||
});
|
||||
|
||||
export const artifacts = style({
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
flexDirection: "column",
|
||||
listStyle: "none",
|
||||
margin: "0",
|
||||
padding: "0",
|
||||
});
|
||||
|
||||
export const artifactButton = style({
|
||||
display: "flex",
|
||||
textAlign: "left",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
color: vars.color.body,
|
||||
padding: `${SPACING_UNIT * 2}px`,
|
||||
backgroundColor: vars.color.darkBackground,
|
||||
border: `1px solid ${vars.color.border}`,
|
||||
borderRadius: "4px",
|
||||
justifyContent: "space-between",
|
||||
});
|
||||
|
||||
export const syncIcon = style({
|
||||
animationName: rotate,
|
||||
animationDuration: "1s",
|
||||
animationIterationCount: "infinite",
|
||||
animationTimingFunction: "linear",
|
||||
});
|
||||
|
||||
export const progress = style({
|
||||
width: "100%",
|
||||
height: "5px",
|
||||
"::-webkit-progress-bar": {
|
||||
backgroundColor: vars.color.darkBackground,
|
||||
},
|
||||
"::-webkit-progress-value": {
|
||||
backgroundColor: vars.color.muted,
|
||||
},
|
||||
});
|
||||
|
||||
export const manageFilesButton = style({
|
||||
margin: "0",
|
||||
padding: "0",
|
||||
alignSelf: "flex-start",
|
||||
fontSize: 14,
|
||||
cursor: "pointer",
|
||||
textDecoration: "underline",
|
||||
color: vars.color.body,
|
||||
":disabled": {
|
||||
cursor: "not-allowed",
|
||||
opacity: vars.opacity.disabled,
|
||||
},
|
||||
});
|
||||
@@ -1,70 +0,0 @@
|
||||
@use "../../../scss/variables" as vars;
|
||||
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.cloud-sync-modal {
|
||||
&__artifacts {
|
||||
display: flex;
|
||||
gap: vars.$spacing-unit;
|
||||
flex-direction: column;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&__artifact-button {
|
||||
display: flex;
|
||||
text-align: left;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: vars.$spacing-unit;
|
||||
color: vars.$body-color;
|
||||
padding: #{vars.$spacing-unit * 2};
|
||||
background-color: vars.$dark-background-color;
|
||||
border: 1px solid vars.$border-color;
|
||||
border-radius: 4px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__sync-icon {
|
||||
animation-name: rotate;
|
||||
animation-duration: 1s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-timing-function: linear;
|
||||
}
|
||||
|
||||
&__progress {
|
||||
width: 100%;
|
||||
height: 5px;
|
||||
|
||||
&::-webkit-progress-bar {
|
||||
background-color: vars.$dark-background-color;
|
||||
}
|
||||
|
||||
&::-webkit-progress-value {
|
||||
background-color: vars.$muted-color;
|
||||
}
|
||||
}
|
||||
|
||||
&__manage-files-button {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
align-self: flex-start;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
color: vars.$body-color;
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: vars.$disabled-opacity;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { Button, Modal, ModalProps } from "@renderer/components";
|
||||
import { useContext, useEffect, useMemo, useState } from "react";
|
||||
import { cloudSyncContext, gameDetailsContext } from "@renderer/context";
|
||||
|
||||
import * as styles from "./cloud-sync-modal.css";
|
||||
import { formatBytes } from "@shared";
|
||||
import { format } from "date-fns";
|
||||
import {
|
||||
@@ -17,9 +18,7 @@ import { useAppSelector, useToast } from "@renderer/hooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { AxiosProgressEvent } from "axios";
|
||||
import { formatDownloadProgress } from "@renderer/helpers";
|
||||
|
||||
import "./cloud-sync-modal.scss";
|
||||
import "../../../scss/_variables.scss";
|
||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||
|
||||
export interface CloudSyncModalProps
|
||||
extends Omit<ModalProps, "children" | "title"> {}
|
||||
@@ -96,7 +95,7 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
||||
if (uploadingBackup) {
|
||||
return (
|
||||
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<SyncIcon className="clound-sync-modal__sync-icon" />
|
||||
<SyncIcon className={styles.syncIcon} />
|
||||
{t("uploading_backup")}
|
||||
</span>
|
||||
);
|
||||
@@ -105,7 +104,7 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
||||
if (restoringBackup) {
|
||||
return (
|
||||
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<SyncIcon className="clound-sync-modal__sync-icon" />
|
||||
<SyncIcon className={styles.syncIcon} />
|
||||
{t("restoring_backup", {
|
||||
progress: formatDownloadProgress(
|
||||
backupDownloadProgress?.progress ?? 0
|
||||
@@ -118,7 +117,7 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
||||
if (loadingPreview) {
|
||||
return (
|
||||
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<SyncIcon className="clound-sync-modal__sync-icon" />
|
||||
<SyncIcon className={styles.syncIcon} />
|
||||
{t("loading_save_preview")}
|
||||
</span>
|
||||
);
|
||||
@@ -172,7 +171,7 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="clound-sync-modal__manage-files-button"
|
||||
className={styles.manageFilesButton}
|
||||
onClick={() => setShowCloudSyncFilesModal(true)}
|
||||
disabled={disableActions}
|
||||
>
|
||||
@@ -200,7 +199,7 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
||||
marginBottom: 16,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "var(--spacing-unit)",
|
||||
gap: SPACING_UNIT,
|
||||
}}
|
||||
>
|
||||
<h2>{t("backups")}</h2>
|
||||
@@ -211,9 +210,9 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
||||
</div>
|
||||
|
||||
{artifacts.length > 0 ? (
|
||||
<ul className="clound-sync-modal__artifacts">
|
||||
<ul className={styles.artifacts}>
|
||||
{artifacts.map((artifact) => (
|
||||
<li key={artifact.id} className="cloud-sync-modal__artifact-button">
|
||||
<li key={artifact.id} className={styles.artifactButton}>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
|
||||
<div
|
||||
style={{
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { style } from "@vanilla-extract/css";
|
||||
|
||||
import { SPACING_UNIT, vars } from "../../../theme.css";
|
||||
|
||||
export const descriptionHeader = style({
|
||||
width: "100%",
|
||||
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 2}px`,
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
backgroundColor: vars.color.background,
|
||||
height: "72px",
|
||||
});
|
||||
|
||||
export const descriptionHeaderInfo = style({
|
||||
display: "flex",
|
||||
gap: `${SPACING_UNIT}px`,
|
||||
flexDirection: "column",
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
@use "../../../scss/variables" as vars;
|
||||
|
||||
.description-header {
|
||||
width: 100%;
|
||||
padding: #{vars.$spacing-unit * 2};
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: vars.$background-color;
|
||||
height: 72px;
|
||||
|
||||
&__info {
|
||||
display: flex;
|
||||
gap: vars.$spacing-unit;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import * as styles from "./description-header.css";
|
||||
import { useContext } from "react";
|
||||
import { gameDetailsContext } from "@renderer/context";
|
||||
|
||||
@@ -11,8 +12,8 @@ export function DescriptionHeader() {
|
||||
if (!shopDetails) return null;
|
||||
|
||||
return (
|
||||
<div className="description-header">
|
||||
<section className="description-header__info">
|
||||
<div className={styles.descriptionHeader}>
|
||||
<section className={styles.descriptionHeaderInfo}>
|
||||
<p>
|
||||
{t("release_date", {
|
||||
date: shopDetails?.release_date.date,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user