mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
Compare commits
103 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ed1960c469 | |||
| dc9e181c54 | |||
| 4a91fa04e7 | |||
| af3e5d1a56 | |||
| 7b4e6340ca | |||
| 2e3febf66a | |||
| a5ff3482e5 | |||
| 8de279ac90 | |||
| 0efc56f8d6 | |||
| b59c37ebbd | |||
| ff194e724b | |||
| ca636c86e6 | |||
| 8b1117543e | |||
| ef123d8513 | |||
| 5aac3ba47e | |||
| d2f3bdf29a | |||
| e70313f66a | |||
| 70926283ff | |||
| 8357c09e9f | |||
| 21f97df788 | |||
| 00d6d7de30 | |||
| b10900618a | |||
| 9b8d6ddda0 | |||
| 4e6948693d | |||
| 6e3c65cced | |||
| c499fb83e7 | |||
| be347dc6ba | |||
| 716f068adf | |||
| e9d2cb1482 | |||
| 132394524e | |||
| 20499b821e | |||
| 803842d579 | |||
| eb119a31f0 | |||
| 9d4acea654 | |||
| 6a2f982292 | |||
| 7c88ea46e0 | |||
| 0f3de55e9e | |||
| d5b7dff7a9 | |||
| 7468554ea0 | |||
| 0d93c984be | |||
| 87f134ebb5 | |||
| 8fe346b1a7 | |||
| e9ca4bb7ac | |||
| eac6192afb | |||
| 679b28e44f | |||
| 36e95d0d96 | |||
| 59bd480445 | |||
| d38a3b5720 | |||
| 9d59bfb956 | |||
| 50322b69cc | |||
| fc212cecb4 | |||
| 3d6a9b59bf | |||
| b97a58c278 | |||
| 6f55f4283d | |||
| 9ee33f2c7d | |||
| 55f45e438c | |||
| cce6b71993 | |||
| 3d24e97abd | |||
| 1eea031438 | |||
| dcb45927a1 | |||
| d234a504b5 | |||
| cac09cf032 | |||
| 6d6f7d492e | |||
| 01bae7c59b | |||
| db5af48250 | |||
| 0576ea0159 | |||
| f73ab4a6a4 | |||
| 501c0d9ae1 | |||
| cc810ae84e | |||
| 3533402197 | |||
| 03a8377773 | |||
| 138002d7a0 | |||
| 18854650ab | |||
| e51d47ac73 | |||
| 32bf20a55f | |||
| b702b33def | |||
| d74a730a04 | |||
| 793af822ce | |||
| c05b9a4b8e | |||
| 883f7594b0 | |||
| 7161f0861e | |||
| 5f35a43098 | |||
| 63ef3acc72 | |||
| 3ff3c5af67 | |||
| 50ebef30fb | |||
| 6d6da765a6 | |||
| 031312e90e | |||
| f0e32bdae1 | |||
| 81071d920e | |||
| ea320c0e08 | |||
| 8335494a9f | |||
| 96fc2b4efb | |||
| 3280ca1984 | |||
| aae23c18d6 | |||
| 7a8eee9266 | |||
| 757069d657 | |||
| 15178f0e60 | |||
| 02d0aef497 | |||
| ee6b49f028 | |||
| 5c383b66a7 | |||
| 50cef104f0 | |||
| 37a44b54f3 | |||
| 0027832bdb |
@@ -59,6 +59,7 @@ module.exports = {
|
||||
"react/function-component-definition": "off",
|
||||
"jsx-a11y/control-has-associated-label": "off",
|
||||
"react/button-has-type": "off",
|
||||
"no-labels": ["error", { "allowLoop": true }]
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: 2020,
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
---
|
||||
name: "[BUG] Bug report"
|
||||
about: Create a report to help us improve
|
||||
title: "[BUG] : "
|
||||
labels: bug
|
||||
assignees: Zagrios
|
||||
---
|
||||
|
||||
## Bug Description
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
## Reproduction Steps
|
||||
<!-- Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
-->
|
||||
|
||||
## Expected Behavior
|
||||
<!-- A clear and concise description of what you expected to happen. -->
|
||||
|
||||
## Screenshots
|
||||
<!-- If applicable, add screenshots to help explain your problem. -->
|
||||
|
||||
## System Specs
|
||||
<!-- **Desktop (please complete the following information):**
|
||||
|
||||
- OS: [e.g. iOS]
|
||||
- Version [e.g. 22]
|
||||
-->
|
||||
|
||||
## Additional context
|
||||
<!-- Add any other context about the problem here. -->
|
||||
@@ -1,22 +0,0 @@
|
||||
---
|
||||
name: "[FEAT.] Feature request"
|
||||
about: Suggest an idea for this project
|
||||
title: "[FEAT.] : "
|
||||
labels: enhancement
|
||||
assignees: Zagrios
|
||||
---
|
||||
|
||||
## Problem
|
||||
<!-- **Is your feature request related to a problem? Please describe.** -->
|
||||
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
|
||||
|
||||
## Solution
|
||||
<!-- **Describe the solution you'd like** -->
|
||||
<!-- A clear and concise description of what you want to happen. -->
|
||||
|
||||
## Alternative solutions (if any)
|
||||
<!-- **Describe alternatives you've considered** -->
|
||||
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
|
||||
|
||||
## Additional context
|
||||
<!-- Add any other context or screenshots about the feature request here. -->
|
||||
@@ -0,0 +1,54 @@
|
||||
|
||||
|
||||
name: "[BUG] Bug report"
|
||||
description: Create a report to help us improve
|
||||
title: "[BUG] : "
|
||||
labels: bug
|
||||
assignees: Zagrios
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Thanks for taking the time to fill out this bug report! 😊
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Issue encountered
|
||||
description: Tell us what issue you've encountered.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: Tell us what should expected to happen.
|
||||
- type: textarea
|
||||
id: replication
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
description: Provide a link to a live example, or an unambiguous set of steps to reproduce this bug. Include code to reproduce, if relevant.
|
||||
placeholder: |
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
- type: dropdown
|
||||
id: os
|
||||
attributes:
|
||||
label: Operating System
|
||||
options:
|
||||
- Windows 10
|
||||
- Windows 11
|
||||
- Linux
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: What version of BSManager are you running?
|
||||
placeholder: eg. 1.4.8 or 1.5.0-alpha-4
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Any other context that you may share about the issue. You may add your log files here.
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
|
||||
name: "[FEAT.] Feature request"
|
||||
description: Suggest an idea for this project
|
||||
title: "[FEAT.] : "
|
||||
labels: enhancement
|
||||
assignees: Zagrios
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Thanks for taking the time to fill out this feature request! 😊
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Problem
|
||||
description: Is your feature request related to a problem? Please describe.
|
||||
placeholder: Ex. I'm always frustrated when [...]
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: Solution
|
||||
description: Describe the solution you'd like.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: alternative-solution
|
||||
attributes:
|
||||
label: Alternative Solution
|
||||
description: Describe alternatives you've considered.
|
||||
- type: dropdown
|
||||
id: os
|
||||
attributes:
|
||||
label: Operating System
|
||||
description: If the feature is only applicable to a specific OS. Leave it to `None` if it's applicable to all OS'es.
|
||||
options:
|
||||
- Windows
|
||||
- Linux
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Add any other context or screenshots about the feature request here.
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
lank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Discord Support
|
||||
url: https://discord.gg/uSqbHVpKdV
|
||||
about: You can join our Discord server for a quick and interactive support.
|
||||
@@ -0,0 +1,20 @@
|
||||
name: Publish wiki
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths:
|
||||
- docs/wiki/**
|
||||
- .github/workflows/docs.yml
|
||||
concurrency:
|
||||
group: publish-wiki
|
||||
cancel-in-progress: true
|
||||
permissions:
|
||||
contents: write
|
||||
jobs:
|
||||
publish-wiki:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: Andrew-Chen-Wang/github-wiki-action@v4
|
||||
with:
|
||||
path: docs/wiki/
|
||||
@@ -0,0 +1,26 @@
|
||||
name: Labeler
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, edited]
|
||||
|
||||
jobs:
|
||||
label-linux:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Search Linux Term
|
||||
uses: actions-ecosystem/action-regex-match@v2
|
||||
id: regex-match
|
||||
with:
|
||||
text: ${{ github.event.issue.body }}
|
||||
regex: '\# Operating System(\s*)Linux'
|
||||
flags: m
|
||||
|
||||
- name: Add Linux label
|
||||
uses: actions-ecosystem/action-add-labels@v1
|
||||
if: ${{ steps.regex-match.outputs.match != '' }}
|
||||
with:
|
||||
github_token: ${{ secrets.GH_TOKEN }}
|
||||
labels: linux
|
||||
@@ -1,6 +1,6 @@
|
||||
# This workflow will...
|
||||
|
||||
name: Realease Linux
|
||||
name: Release Linux
|
||||
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
@@ -546,7 +546,7 @@
|
||||
<div>
|
||||
<h2>Credits</h2>
|
||||
<ul>
|
||||
<li><a href="https://github.com/Zagrios">Zagrios - Mathieu GRIES-PEROZ</a> - Lead Developer & Founder.</li>
|
||||
<li><a href="https://github.com/Zagrios">Zagrios - Mathieu Gries</a> - Lead Developer & Founder.</li>
|
||||
<li><a href="https://github.com/Iluhadesu">Iluhadesu</a> - Co-Developer & Co-Founder, Discord Bot Developer.</li>
|
||||
<li><a href="https://github.com/GaetanGrd">GaetanGrd</a> - Co-Developer & Co-Founder, Documentation Lead.</li>
|
||||
<li><a href="https://github.com/cheddZy">cheddZy</a> - Icon Creator.</li>
|
||||
|
||||
@@ -742,8 +742,7 @@
|
||||
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/4253168598913260422",
|
||||
"ReleaseImg": "https://clan.akamai.steamstatic.com/images/32055887/039be718c1494f948257cd5b09f0082986484a58.png",
|
||||
"ReleaseDate": "1720710529",
|
||||
"year": "2024",
|
||||
"recommended": true
|
||||
"year": "2024"
|
||||
},
|
||||
{
|
||||
"BSVersion": "1.37.2",
|
||||
@@ -778,5 +777,15 @@
|
||||
"OculusBinaryId": "7685108391588873",
|
||||
"ReleaseDate": "1725628356",
|
||||
"year": "2024"
|
||||
},
|
||||
{
|
||||
"BSVersion": "1.38.0",
|
||||
"BSManifest": "3482902979520746178",
|
||||
"OculusBinaryId": "7834818833284494",
|
||||
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/4669760442595134838",
|
||||
"ReleaseImg": "https://clan.akamai.steamstatic.com/images/32055887/57cfa043407c091e67b7e0a8ae86fe5590deae33.png",
|
||||
"ReleaseDate": "1728399815",
|
||||
"year": "2024",
|
||||
"recommended": true
|
||||
}
|
||||
]
|
||||
@@ -108,6 +108,11 @@
|
||||
{
|
||||
"username": "Taurus Arcade",
|
||||
"type": "gold"
|
||||
}
|
||||
|
||||
},
|
||||
{
|
||||
"username": "Mozz_Zm"
|
||||
},
|
||||
{
|
||||
"username": "clapxz"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -45,14 +45,16 @@
|
||||
"filters-btn": "Filter",
|
||||
"dropdown": {
|
||||
"export-maps": "Maps exportieren",
|
||||
"delete-maps": "Maps löschen"
|
||||
"delete-maps": "Maps löschen",
|
||||
"delete-duplicate-maps": "Duplikate löschen"
|
||||
}
|
||||
},
|
||||
"tabs": {
|
||||
"maps": {
|
||||
"actions": {
|
||||
"add-maps": {
|
||||
"text": "Hinzufügen"
|
||||
"drop-down": {
|
||||
"browse-maps": "Karten durchsuchen",
|
||||
"import-maps": "Karten importieren"
|
||||
},
|
||||
"link-maps": {
|
||||
"tooltips": {
|
||||
@@ -64,6 +66,21 @@
|
||||
"empty-maps": {
|
||||
"text": "Keine Maps",
|
||||
"button": "Maps herunterladen"
|
||||
},
|
||||
"drop-zone": {
|
||||
"text": "Deine Karten importieren",
|
||||
"subtext": "Lasse deine ZIP-Dateien hier fallen, um deine Karten zu importieren"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
"drop-down": {
|
||||
"browse-playlists": "Wiedergabelisten durchsuchen",
|
||||
"create-a-playlist": "Eine Wiedergabeliste erstellen",
|
||||
"import-playlists": "Wiedergabelisten importieren"
|
||||
},
|
||||
"drop-zone": {
|
||||
"text": "Importiere deine Wiedergabelisten",
|
||||
"subtext": "Ziehe deine \".bplist\" oder \".json\" Dateien hierhin, um sie zu importieren"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,6 +137,7 @@
|
||||
"title": "Steam & Oculus",
|
||||
"description": "Das Abmelden ermöglicht es dir, beim nächsten Download von Beat Saber den Account zu wechseln.",
|
||||
"logout": "Abmelden",
|
||||
"logout-success": "Abmeldung erfolgreich",
|
||||
"download-platform": {
|
||||
"title": "Standardplattform",
|
||||
"desc": "Wähle die Standardplattform, die für das Herunterladen von Beat Saber-Versionen verwendet werden soll.",
|
||||
@@ -248,11 +266,13 @@
|
||||
"errors": {
|
||||
"titles": {
|
||||
"operation-running": "Vorgang läuft",
|
||||
"no-internet": "Kein Internet"
|
||||
"no-internet": "Kein Internet",
|
||||
"file-not-supported": "Datei wird nicht unterstützt"
|
||||
},
|
||||
"msg": {
|
||||
"operation-running": "Warte auf das Ende des aktuellen Vorgangs und versuche es erneut.",
|
||||
"no-internet": "Überprüfe deine Verbindung und versuche es erneut."
|
||||
"no-internet": "Überprüfe deine Verbindung und versuche es erneut.",
|
||||
"file-not-supported": "Nur {types}-Dateien werden unterstützt."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -441,7 +461,8 @@
|
||||
"CantEditSteam": "Kann nicht bearbeitet werden",
|
||||
"CantRename": "Umbenennen unmöglich",
|
||||
"VersionAlreadExist": "Diese Version existiert bereits",
|
||||
"CantClone": "Klonen unmöglich"
|
||||
"CantClone": "Klonen unmöglich",
|
||||
"UnknownError": "Ein unbekannter Fehler ist aufgetreten"
|
||||
},
|
||||
"msg": {
|
||||
"CantEditSteam": "Die Steam-Version kann nicht bearbeitet werden. Du kannst es aber klonen."
|
||||
@@ -494,6 +515,27 @@
|
||||
"one-click-install": {
|
||||
"success": "Map-Installation abgeschlossen",
|
||||
"error": "Ein Fehler ist während der Installation der Map aufgetreten."
|
||||
},
|
||||
"no-duplicates-maps": {
|
||||
"title": "Keine Duplikate",
|
||||
"msg": "Keine Karten wurden gelöscht"
|
||||
},
|
||||
"duplicates-maps-deleted": {
|
||||
"title": "Duplikate gelöscht",
|
||||
"msg": "Duplikate wurden gelöscht"
|
||||
},
|
||||
"import-map": {
|
||||
"titles": {
|
||||
"success": "Kartenimport abgeschlossen",
|
||||
"error": "Beim Kartenimport ist ein Fehler aufgetreten"
|
||||
},
|
||||
"msgs": {
|
||||
"success": "Die Karten wurden erfolgreich importiert.",
|
||||
"some-success": "Einige Karten wurden erfolgreich importiert.",
|
||||
"only-accept-zip": "Nur Zip-Dateien werden unterstützt.",
|
||||
"invalid-zip": "Die Zip-Datei(en) enthalten keine Karten.",
|
||||
"unknown": "Ein unbekannter Fehler ist aufgetreten."
|
||||
}
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -688,6 +730,11 @@
|
||||
"title": "\"Maps behalten\" kopiert die Maps aus dem geteilten Maps-Ordner in den Ordner der ausgewählten Version. Andernfalls gehen sie verloren."
|
||||
},
|
||||
"valid-btn": "Verknüpfung aufheben"
|
||||
},
|
||||
"delete-duplicate-maps": {
|
||||
"title": "Map entfernen?",
|
||||
"desc": "Nur die Karte \"{map}\" ist ein Duplikat. Bist du sicher, dass du sie löschen möchtest?",
|
||||
"desc-plural": "Es wurden {nb} doppelte Karten gefunden. Bist du sicher, dass du sie löschen möchtest?"
|
||||
}
|
||||
},
|
||||
"download-maps": {
|
||||
@@ -797,42 +844,42 @@
|
||||
"tech": "Tech"
|
||||
},
|
||||
"map-styles": {
|
||||
"dance": "Tanz",
|
||||
"swing": "Swing",
|
||||
"nightcore": "Nightcore",
|
||||
"folk": "Folk",
|
||||
"family": "Familie",
|
||||
"ambient": "Umgebung",
|
||||
"funk": "Funk",
|
||||
"jazz": "Jazz",
|
||||
"soul": "Soul",
|
||||
"speedcore": "Speedcore",
|
||||
"punk": "Punk",
|
||||
"rb": "R&B",
|
||||
"holiday": "Urlaub",
|
||||
"vocaloid": "Vokaloid",
|
||||
"j-rock": "J-Rock",
|
||||
"trance": "Trance",
|
||||
"drumbass": "Basstrommel",
|
||||
"comedy": "Komödie",
|
||||
"instrumental": "Instrumental",
|
||||
"hardcore": "Hardcore",
|
||||
"k-pop": "K-Pop",
|
||||
"indie": "Indie",
|
||||
"techno": "Techno",
|
||||
"house": "House",
|
||||
"game": "Videospiele",
|
||||
"film": "Filme",
|
||||
"alt": "Alternativ",
|
||||
"dubstep": "Dubstep",
|
||||
"metal": "Metal",
|
||||
"anime": "Anime",
|
||||
"hiphop": "HipHop",
|
||||
"j-pop": "J-Pop",
|
||||
"rock": "Rock",
|
||||
"pop": "Pop",
|
||||
"electronic": "Elektronisch",
|
||||
"classical-orchestral": "Klassisch & Orchester"
|
||||
"dance": "tanz",
|
||||
"swing": "swing",
|
||||
"nightcore": "nightcore",
|
||||
"folk-acoustic": "folk & akustisch",
|
||||
"kids-family": "kinder & familie",
|
||||
"ambient": "ambient",
|
||||
"funk-disco": "funk & disco",
|
||||
"jazz": "jazz",
|
||||
"soul": "soul",
|
||||
"speedcore": "speedcore",
|
||||
"punk": "punk",
|
||||
"rb": "r&b",
|
||||
"holiday": "urlaub",
|
||||
"vocaloid": "vocaloid",
|
||||
"j-rock": "j-rock",
|
||||
"trance": "trance",
|
||||
"drum-and-bass": "drum & bass",
|
||||
"comedy-meme": "komödie & meme",
|
||||
"instrumental": "instrumental",
|
||||
"hardcore": "hardcore",
|
||||
"k-pop": "k-pop",
|
||||
"indie": "indie",
|
||||
"techno": "techno",
|
||||
"house": "house",
|
||||
"video-game-soundtrack": "videospiel",
|
||||
"tv-movie-soundtrack": "TV & film",
|
||||
"alternative": "alternativ",
|
||||
"dubstep": "dubstep",
|
||||
"metal": "metal",
|
||||
"anime": "anime",
|
||||
"hip-hop-rap": "hip hop & rap",
|
||||
"j-pop": "j-pop",
|
||||
"rock": "rock",
|
||||
"pop": "pop",
|
||||
"electronic": "elektronisch",
|
||||
"classical-orchestral": "Klassisch & Orchestral"
|
||||
},
|
||||
"map-specificities": {
|
||||
"automapper": "KI",
|
||||
@@ -859,7 +906,8 @@
|
||||
"bsr-code" : "BSR Code",
|
||||
"download" : "Karte herunterladen",
|
||||
"downloading" :"Karte wird heruntergeladen",
|
||||
"cancel-download" : "Download abbrechen"
|
||||
"cancel-download" : "Download abbrechen",
|
||||
"hightlight-difficulty": "Schwierigkeit hervorheben"
|
||||
}
|
||||
},
|
||||
"models": {
|
||||
@@ -1051,6 +1099,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"drop-zone": {
|
||||
"or-browse-files": "Oder Dateien durchsuchen"
|
||||
},
|
||||
"playlist": {
|
||||
"error-playlist-creation-title": "Fehler beim Erstellen der Playlist",
|
||||
"error-playlist-creation-desc": "Beim Erstellen der Playlist ist ein Fehler aufgetreten.",
|
||||
@@ -1165,6 +1216,24 @@
|
||||
"last-week": "Letzte Woche",
|
||||
"last-month": "Letzter Monat",
|
||||
"3-last-month": "Letzte 3 Monate"
|
||||
},
|
||||
"playlists-imported": "Wiedergabelisten importiert",
|
||||
"all-playlists-have-been-successfully-imported": "Alle Wiedergabelisten wurden erfolgreich importiert",
|
||||
"no-playlist-found": "Keine Wiedergabeliste gefunden",
|
||||
"no-playlist-found-in-selected-files": "Keine Wiedergabeliste in den ausgewählten Dateien gefunden",
|
||||
"some-playlists-not-imported": "Einige Wiedergabelisten wurden nicht importiert",
|
||||
"some-playlists-have-been-imported": {
|
||||
"INVALID_SOURCE": "Einige Wiedergabelisten konnten nicht gefunden werden",
|
||||
"INVALID_PLAYLIST_FILE": "Einige Wiedergabelisten sind ungültig",
|
||||
"CANNOT_PARSE_PLAYLIST": "Einige Wiedergabelisten sind nicht lesbar",
|
||||
"unknown": "Einige Wiedergabelisten konnten nicht importiert werden"
|
||||
},
|
||||
"no-playlists-imported": "Keine Wiedergabelisten importiert",
|
||||
"no-playlists-imported-errors": {
|
||||
"INVALID_SOURCE": "Wiedergabelisten konnten nicht gefunden werden",
|
||||
"INVALID_PLAYLIST_FILE": "Wiedergabelisten sind ungültig",
|
||||
"CANNOT_PARSE_PLAYLIST": "Wiedergabelisten sind nicht lesbar",
|
||||
"unknown": "Es konnte keine Wiedergabeliste importiert werden"
|
||||
}
|
||||
},
|
||||
"dateformat": {
|
||||
|
||||
@@ -45,14 +45,16 @@
|
||||
"filters-btn": "Filters",
|
||||
"dropdown": {
|
||||
"export-maps": "Export maps",
|
||||
"delete-maps": "Delete maps"
|
||||
"delete-maps": "Delete maps",
|
||||
"delete-duplicate-maps": "Delete duplicates"
|
||||
}
|
||||
},
|
||||
"tabs": {
|
||||
"maps": {
|
||||
"actions": {
|
||||
"add-maps": {
|
||||
"text": "Add"
|
||||
"drop-down": {
|
||||
"browse-maps": "Browse maps",
|
||||
"import-maps": "Import maps"
|
||||
},
|
||||
"link-maps": {
|
||||
"tooltips": {
|
||||
@@ -64,6 +66,21 @@
|
||||
"empty-maps": {
|
||||
"text": "No maps",
|
||||
"button": "Download maps"
|
||||
},
|
||||
"drop-zone": {
|
||||
"text": "Import your maps",
|
||||
"subtext": "Drop your zip files here to import your maps"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
"drop-down": {
|
||||
"browse-playlists": "Browse playlists",
|
||||
"create-a-playlist": "Create a playlist",
|
||||
"import-playlists": "Import playlists"
|
||||
},
|
||||
"drop-zone": {
|
||||
"text": "Import your playlists",
|
||||
"subtext": "Drop your \".bplist\" or \".json\" files here to import them"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,6 +137,7 @@
|
||||
"title": "Steam & Oculus",
|
||||
"description": "Logging out will allow you to switch accounts on the next Beat Saber download.",
|
||||
"logout": "Log out",
|
||||
"logout-success": "Logout Successful",
|
||||
"download-platform": {
|
||||
"title": "Default Platform",
|
||||
"desc": "Choose the default platform that will be used to download versions of Beat Saber.",
|
||||
@@ -253,11 +271,13 @@
|
||||
"errors": {
|
||||
"titles": {
|
||||
"operation-running": "Operation running",
|
||||
"no-internet": "No internet"
|
||||
"no-internet": "No internet",
|
||||
"file-not-supported": "File not supported"
|
||||
},
|
||||
"msg": {
|
||||
"operation-running": "Wait for the current operation to finish, then try again.",
|
||||
"no-internet": "Check your connection and try again."
|
||||
"no-internet": "Check your connection and try again.",
|
||||
"file-not-supported": "Only {types} files are supported."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -448,7 +468,8 @@
|
||||
"CantEditSteam": "Unable to edit",
|
||||
"CantRename": "Renaming impossible",
|
||||
"VersionAlreadExist": "This version already exists",
|
||||
"CantClone": "Cloning impossible"
|
||||
"CantClone": "Cloning impossible",
|
||||
"UnknownError": "An unknown error occurred"
|
||||
},
|
||||
"msg": {
|
||||
"CantEditSteam": "You can't edit the Steam version. You can clone it though."
|
||||
@@ -501,6 +522,27 @@
|
||||
"one-click-install": {
|
||||
"success": "Map installation complete",
|
||||
"error": "An error occurred while installing the map"
|
||||
},
|
||||
"no-duplicates-maps": {
|
||||
"title": "No Duplicates",
|
||||
"msg": "No maps were deleted"
|
||||
},
|
||||
"duplicates-maps-deleted": {
|
||||
"title": "Duplicates Deleted",
|
||||
"msg": "Duplicates were deleted"
|
||||
},
|
||||
"import-map": {
|
||||
"titles": {
|
||||
"success": "Maps import completed",
|
||||
"error": "An error occurred during the maps import"
|
||||
},
|
||||
"msgs": {
|
||||
"success": "Maps successfully imported.",
|
||||
"some-success": "Some maps were successfully imported.",
|
||||
"only-accept-zip": "Only zip files are supported.",
|
||||
"invalid-zip": "The zip file(s) do not contain any maps.",
|
||||
"unknown": "An unknown error occurred."
|
||||
}
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -695,6 +737,11 @@
|
||||
"title": "Keeping maps will copy all maps from the shared folder to the current version after unlinking. Maps will not be lost if this is disabled."
|
||||
},
|
||||
"valid-btn": "Unlink maps"
|
||||
},
|
||||
"delete-duplicate-maps": {
|
||||
"title": "Delete maps?",
|
||||
"desc": "Only the map \"{map}\" is a duplicate. Are you sure you want to delete it?",
|
||||
"desc-plural": "{nb} duplicate maps have been found. Are you sure you want to delete them?"
|
||||
}
|
||||
},
|
||||
"download-maps": {
|
||||
@@ -807,10 +854,10 @@
|
||||
"dance": "dance",
|
||||
"swing": "swing",
|
||||
"nightcore": "nightcore",
|
||||
"folk": "folk",
|
||||
"family": "family",
|
||||
"folk-acoustic": "folk & acoustic",
|
||||
"kids-family": "kids & family",
|
||||
"ambient": "ambient",
|
||||
"funk": "funk",
|
||||
"funk-disco": "funk & disco",
|
||||
"jazz": "jazz",
|
||||
"soul": "soul",
|
||||
"speedcore": "speedcore",
|
||||
@@ -820,21 +867,21 @@
|
||||
"vocaloid": "vocaloid",
|
||||
"j-rock": "j-rock",
|
||||
"trance": "trance",
|
||||
"drumbass": "drum & bass",
|
||||
"comedy": "comedy",
|
||||
"drum-and-bass": "drum & bass",
|
||||
"comedy-meme": "comedy & meme",
|
||||
"instrumental": "instrumental",
|
||||
"hardcore": "hardcore",
|
||||
"k-pop": "k-pop",
|
||||
"indie": "indie",
|
||||
"techno": "techno",
|
||||
"house": "house",
|
||||
"game": "video game",
|
||||
"film": "film",
|
||||
"alt": "alternative",
|
||||
"video-game-soundtrack": "video game",
|
||||
"tv-movie-soundtrack": "TV & film",
|
||||
"alternative": "alternative",
|
||||
"dubstep": "dubstep",
|
||||
"metal": "metal",
|
||||
"anime": "anime",
|
||||
"hiphop": "hiphop",
|
||||
"hip-hop-rap": "hip hop & rap",
|
||||
"j-pop": "j-pop",
|
||||
"rock": "rock",
|
||||
"pop": "pop",
|
||||
@@ -866,7 +913,8 @@
|
||||
"bsr-code" : "BSR code",
|
||||
"download" : "Download map",
|
||||
"downloading" :"Downloading map",
|
||||
"cancel-download" : "Cancel download"
|
||||
"cancel-download" : "Cancel download",
|
||||
"hightlight-difficulty": "Highlight difficulty"
|
||||
}
|
||||
},
|
||||
"models": {
|
||||
@@ -1058,6 +1106,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"drop-zone": {
|
||||
"or-browse-files": "Or browse files"
|
||||
},
|
||||
"playlist": {
|
||||
"error-playlist-creation-title": "Error creating playlist",
|
||||
"error-playlist-creation-desc": "An error occurred while creating the playlist.",
|
||||
@@ -1172,6 +1223,24 @@
|
||||
"last-week": "Last week",
|
||||
"last-month": "Last month",
|
||||
"3-last-month": "Last 3 months"
|
||||
},
|
||||
"playlists-imported": "Playlists imported",
|
||||
"all-playlists-have-been-successfully-imported": "All playlists have been successfully imported",
|
||||
"no-playlist-found": "No playlist found",
|
||||
"no-playlist-found-in-selected-files": "No playlist found in the selected files",
|
||||
"some-playlists-not-imported": "Some playlists not imported",
|
||||
"some-playlists-have-been-imported": {
|
||||
"INVALID_SOURCE": "Some playlists could not be found",
|
||||
"INVALID_PLAYLIST_FILE": "Some playlists are invalid",
|
||||
"CANNOT_PARSE_PLAYLIST": "Some playlists are unreadable",
|
||||
"unknown": "Some playlists could not be imported"
|
||||
},
|
||||
"no-playlists-imported": "No playlists imported",
|
||||
"no-playlists-imported-errors": {
|
||||
"INVALID_SOURCE": "Playlists could not be found",
|
||||
"INVALID_PLAYLIST_FILE": "Playlists are invalid",
|
||||
"CANNOT_PARSE_PLAYLIST": "Playlists are unreadable",
|
||||
"unknown": "No playlist could be imported"
|
||||
}
|
||||
},
|
||||
"dateformat": {
|
||||
|
||||
@@ -45,14 +45,16 @@
|
||||
"filters-btn": "Filtros",
|
||||
"dropdown": {
|
||||
"export-maps": "Exportar mapas",
|
||||
"delete-maps": "Borrar mapas"
|
||||
"delete-maps": "Borrar mapas",
|
||||
"delete-duplicate-maps": "Borrar duplicados"
|
||||
}
|
||||
},
|
||||
"tabs": {
|
||||
"maps": {
|
||||
"actions": {
|
||||
"add-maps": {
|
||||
"text": "Añadir"
|
||||
"drop-down": {
|
||||
"browse-maps": "Explorar mapas",
|
||||
"import-maps": "Importar mapas"
|
||||
},
|
||||
"link-maps": {
|
||||
"tooltips": {
|
||||
@@ -64,6 +66,21 @@
|
||||
"empty-maps": {
|
||||
"text": "No hay mapas",
|
||||
"button": "Descargar mapas"
|
||||
},
|
||||
"drop-zone": {
|
||||
"text": "Importar tus mapas",
|
||||
"subtext": "Suelta tus archivos zip aquí para importar tus mapas"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
"drop-down": {
|
||||
"browse-playlists": "Explorar listas de reproducción",
|
||||
"create-a-playlist": "Crear una lista de reproducción",
|
||||
"import-playlists": "Importar listas de reproducción"
|
||||
},
|
||||
"drop-zone": {
|
||||
"text": "Importa tus listas de reproducción",
|
||||
"subtext": "Suelta tus archivos \".bplist\" o \".json\" aquí para importarlos"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,6 +137,7 @@
|
||||
"title": "Steam & Oculus",
|
||||
"description": "Desconectarte te permitirá cambiar de cuenta en la próxima descarga de Beat Saber.",
|
||||
"logout": "Cerrar sesión",
|
||||
"logout-success": "Cierre de sesión exitoso",
|
||||
"download-platform": {
|
||||
"title": "Plataforma predeterminada",
|
||||
"desc": "Elige la plataforma predeterminada que se utilizará para descargar las versiones de Beat Saber.",
|
||||
@@ -248,11 +266,13 @@
|
||||
"errors": {
|
||||
"titles": {
|
||||
"operation-running": "Operación en curso",
|
||||
"no-internet": "Sin internet"
|
||||
"no-internet": "Sin internet",
|
||||
"file-not-supported": "Archivo no compatible"
|
||||
},
|
||||
"msg": {
|
||||
"operation-running": "Espera a que termine la operación actual y vuelve a empezar.",
|
||||
"no-internet": "Comprueba tu conexión a Internet e inténtalo de nuevo."
|
||||
"no-internet": "Comprueba tu conexión a Internet e inténtalo de nuevo.",
|
||||
"file-not-supported": "Solo se admiten archivos {types}."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -441,7 +461,8 @@
|
||||
"CantEditSteam": "Imposible editar",
|
||||
"CantRename": "Cambio de nombre imposible",
|
||||
"VersionAlreadExist": "Esta versión ya existe",
|
||||
"CantClone": "Clonación imposible"
|
||||
"CantClone": "Clonación imposible",
|
||||
"UnknownError": "Ocurrió un error desconocido"
|
||||
},
|
||||
"msg": {
|
||||
"CantEditSteam": "No puedes editar la versión de Steam. Sin embargo, puedes clonarla."
|
||||
@@ -494,6 +515,27 @@
|
||||
"one-click-install": {
|
||||
"success": "Instalación del mapa completada",
|
||||
"error": "Se produjo un error durante la instalación del mapa"
|
||||
},
|
||||
"no-duplicates-maps": {
|
||||
"title": "Sin duplicados",
|
||||
"msg": "No se eliminó ninguna carta"
|
||||
},
|
||||
"duplicates-maps-deleted": {
|
||||
"title": "Duplicados eliminados",
|
||||
"msg": "Se eliminaron los duplicados"
|
||||
},
|
||||
"import-map": {
|
||||
"titles": {
|
||||
"success": "Importación de mapas completada",
|
||||
"error": "Ocurrió un error durante la importación de mapas"
|
||||
},
|
||||
"msgs": {
|
||||
"success": "Los mapas se han importado con éxito.",
|
||||
"some-success": "Algunos mapas se han importado con éxito.",
|
||||
"only-accept-zip": "Solo se admiten archivos zip.",
|
||||
"invalid-zip": "El o los archivos zip no contienen mapas.",
|
||||
"unknown": "Se produjo un error desconocido."
|
||||
}
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -688,6 +730,11 @@
|
||||
"title": "Mantener los mapas creará una copia de los mapas compartidos para la versión actual. De lo contrario, no se mantendrá ningún mapa para esta versión."
|
||||
},
|
||||
"valid-btn": "Desvincular mapas"
|
||||
},
|
||||
"delete-duplicate-maps": {
|
||||
"title": "¿Borrar mapas?",
|
||||
"desc": "Solo el mapa \"{map}\" está duplicado. ¿Estás seguro de que quieres eliminarlo?",
|
||||
"desc-plural": "Se han encontrado {nb} mapas duplicados. ¿Estás seguro de que quieres eliminarlos?"
|
||||
}
|
||||
},
|
||||
"download-maps": {
|
||||
@@ -797,13 +844,13 @@
|
||||
"tech": "tech"
|
||||
},
|
||||
"map-styles": {
|
||||
"dance": "baile",
|
||||
"dance": "danza",
|
||||
"swing": "swing",
|
||||
"nightcore": "nightcore",
|
||||
"folk": "folk",
|
||||
"family": "familia",
|
||||
"ambient": "ambiente",
|
||||
"funk": "funk",
|
||||
"folk-acoustic": "folk & acústico",
|
||||
"kids-family": "niños & familia",
|
||||
"ambient": "ambiental",
|
||||
"funk-disco": "funk & disco",
|
||||
"jazz": "jazz",
|
||||
"soul": "soul",
|
||||
"speedcore": "speedcore",
|
||||
@@ -813,26 +860,26 @@
|
||||
"vocaloid": "vocaloid",
|
||||
"j-rock": "j-rock",
|
||||
"trance": "trance",
|
||||
"drumbass": "drum & bass",
|
||||
"comedy": "comedia",
|
||||
"drum-and-bass": "drum & bass",
|
||||
"comedy-meme": "comedia & meme",
|
||||
"instrumental": "instrumental",
|
||||
"hardcore": "hardcore",
|
||||
"k-pop": "k-pop",
|
||||
"indie": "indie",
|
||||
"techno": "tecno",
|
||||
"techno": "techno",
|
||||
"house": "house",
|
||||
"game": "videojuego",
|
||||
"film": "film",
|
||||
"alt": "alternativa",
|
||||
"video-game-soundtrack": "videojuego",
|
||||
"tv-movie-soundtrack": "TV & cine",
|
||||
"alternative": "alternativo",
|
||||
"dubstep": "dubstep",
|
||||
"metal": "metal",
|
||||
"anime": "anime",
|
||||
"hiphop": "hiphop",
|
||||
"hip-hop-rap": "hip hop & rap",
|
||||
"j-pop": "j-pop",
|
||||
"rock": "rock",
|
||||
"pop": "pop",
|
||||
"electronic": "electrónico",
|
||||
"classical-orchestral": "Clásico y orquestal"
|
||||
"electronic": "electrónica",
|
||||
"classical-orchestral": "Clásica & Orquestal"
|
||||
},
|
||||
"map-specificities": {
|
||||
"automapper": "IA",
|
||||
@@ -859,7 +906,8 @@
|
||||
"bsr-code" : "Código BSR",
|
||||
"download" : "Descargar mapa",
|
||||
"downloading" :"Descargando mapa",
|
||||
"cancel-download" : "Cancelar descarga"
|
||||
"cancel-download" : "Cancelar descarga",
|
||||
"hightlight-difficulty": "Resaltar la dificultad"
|
||||
}
|
||||
},
|
||||
"models": {
|
||||
@@ -1051,6 +1099,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"drop-zone": {
|
||||
"or-browse-files": "O navegar por los archivos"
|
||||
},
|
||||
"playlist": {
|
||||
"error-playlist-creation-title": "Error al crear la lista de reproducción",
|
||||
"error-playlist-creation-desc": "Ocurrió un error al crear la lista de reproducción.",
|
||||
@@ -1165,6 +1216,24 @@
|
||||
"last-week": "Última semana",
|
||||
"last-month": "Último mes",
|
||||
"3-last-month": "Últimos 3 meses"
|
||||
},
|
||||
"playlists-imported": "Listas de reproducción importadas",
|
||||
"all-playlists-have-been-successfully-imported": "Todas las listas de reproducción se han importado con éxito",
|
||||
"no-playlist-found": "No se encontró ninguna lista de reproducción",
|
||||
"no-playlist-found-in-selected-files": "No se encontró ninguna lista de reproducción en los archivos seleccionados",
|
||||
"some-playlists-not-imported": "Algunas listas de reproducción no se importaron",
|
||||
"some-playlists-have-been-imported": {
|
||||
"INVALID_SOURCE": "No se pudieron encontrar algunas listas de reproducción",
|
||||
"INVALID_PLAYLIST_FILE": "Algunas listas de reproducción no son válidas",
|
||||
"CANNOT_PARSE_PLAYLIST": "Algunas listas de reproducción no se pueden leer",
|
||||
"unknown": "No se pudieron importar algunas listas de reproducción"
|
||||
},
|
||||
"no-playlists-imported": "No se importaron listas de reproducción",
|
||||
"no-playlists-imported-errors": {
|
||||
"INVALID_SOURCE": "No se pudieron encontrar las listas de reproducción",
|
||||
"INVALID_PLAYLIST_FILE": "Las listas de reproducción no son válidas",
|
||||
"CANNOT_PARSE_PLAYLIST": "Las listas de reproducción no se pueden leer",
|
||||
"unknown": "No se pudo importar ninguna lista de reproducción"
|
||||
}
|
||||
},
|
||||
"dateformat": {
|
||||
|
||||
@@ -45,14 +45,16 @@
|
||||
"filters-btn": "Filtres",
|
||||
"dropdown": {
|
||||
"export-maps": "Exporter les maps",
|
||||
"delete-maps": "Supprimer les maps"
|
||||
"delete-maps": "Supprimer les maps",
|
||||
"delete-duplicate-maps": "Supprimer les doublons"
|
||||
}
|
||||
},
|
||||
"tabs": {
|
||||
"maps": {
|
||||
"actions": {
|
||||
"add-maps": {
|
||||
"text": "Ajouter"
|
||||
"drop-down": {
|
||||
"browse-maps": "Explorer les maps",
|
||||
"import-maps": "Importer des maps"
|
||||
},
|
||||
"link-maps": {
|
||||
"tooltips": {
|
||||
@@ -64,6 +66,21 @@
|
||||
"empty-maps": {
|
||||
"text": "Aucune map",
|
||||
"button": "Télécharger des maps"
|
||||
},
|
||||
"drop-zone": {
|
||||
"text": "Importer vos maps",
|
||||
"subtext": "Déposez vos fichiers zip ici pour importer vos maps"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
"drop-down": {
|
||||
"browse-playlists": "Explorer les playlists",
|
||||
"create-a-playlist": "Créer une playlist",
|
||||
"import-playlists": "Importer des playlists"
|
||||
},
|
||||
"drop-zone": {
|
||||
"text": "Importer vos playlists",
|
||||
"subtext": "Déposez vos fichiers \".bplist\" ou \".json\" ici pour les importer"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,6 +137,7 @@
|
||||
"title": "Steam & Oculus",
|
||||
"description": "Te déconnecter te permettra de changer de compte au prochain téléchargement de Beat Saber.",
|
||||
"logout": "Déconnexion",
|
||||
"logout-success": "Déconnexion réussie",
|
||||
"download-platform": {
|
||||
"title": "Plateforme par défaut",
|
||||
"desc": "Choisi la plateforme par défaut qui sera utilisée pour télécharger les versions de Beat Saber.",
|
||||
@@ -248,11 +266,13 @@
|
||||
"errors": {
|
||||
"titles": {
|
||||
"operation-running": "Opération en cours",
|
||||
"no-internet": "Pas d'accès Internet"
|
||||
"no-internet": "Pas d'accès Internet",
|
||||
"file-not-supported": "Fichier non supporté"
|
||||
},
|
||||
"msg": {
|
||||
"operation-running": "Attends la fin de l'opération en cours puis recommence.",
|
||||
"no-internet": "Vérifie ta connexion internet et ressaye."
|
||||
"no-internet": "Vérifie ta connexion internet et ressaye.",
|
||||
"file-not-supported": "Seuls les fichiers {types} sont pris en charge."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -441,7 +461,8 @@
|
||||
"CantEditSteam": "Modification impossible",
|
||||
"CantRename": "Renommage impossible",
|
||||
"VersionAlreadExist": "Cette version existe déjà",
|
||||
"CantClone": "Clonage impossible"
|
||||
"CantClone": "Clonage impossible",
|
||||
"UnknownError": "Une erreur inconnue s'est produite"
|
||||
},
|
||||
"msg": {
|
||||
"CantEditSteam": "Tu ne peux pas modifier la version Steam, cependant tu peux la cloner."
|
||||
@@ -494,6 +515,27 @@
|
||||
"one-click-install": {
|
||||
"success": "Installation de la map terminée",
|
||||
"error": "Une erreur s'est produite lors de l'installation de la map"
|
||||
},
|
||||
"no-duplicates-maps": {
|
||||
"title": "Pas de doublons",
|
||||
"msg": "Aucune carte n'a été supprimée"
|
||||
},
|
||||
"duplicates-maps-deleted": {
|
||||
"title": "Doublons supprimés",
|
||||
"msg": "Les doublons ont été supprimés"
|
||||
},
|
||||
"import-map": {
|
||||
"titles": {
|
||||
"success": "Importation des maps terminée",
|
||||
"error": "Une erreur est survenue lors de l'importation des maps"
|
||||
},
|
||||
"msgs": {
|
||||
"success": "Les maps ont été importées avec succès.",
|
||||
"some-success": "Certaines maps ont été importées avec succès.",
|
||||
"only-accept-zip": "Seules les fichiers zip sont pris en charge.",
|
||||
"invalid-zip": "Le ou les fichiers zip ne contiennent aucune maps.",
|
||||
"unknown": "Une erreur inconnue s'est produite."
|
||||
}
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -689,6 +731,11 @@
|
||||
"title": "Conserver les maps créera une copie des maps partagées pour la version actuelle. Dans le cas contraire, aucune map ne sera conservée pour cette version."
|
||||
},
|
||||
"valid-btn": "Délier les maps"
|
||||
},
|
||||
"delete-duplicate-maps": {
|
||||
"title": "Supprimer les maps ?",
|
||||
"desc": "Seule la map \"{map}\" est en double. Es-tu sûr de vouloir la supprimer ?",
|
||||
"desc-plural": "{nb} maps en double ont été trouvées. Es-tu sûr de vouloir les supprimer ?"
|
||||
}
|
||||
},
|
||||
"download-maps": {
|
||||
@@ -798,42 +845,42 @@
|
||||
"tech": "tech"
|
||||
},
|
||||
"map-styles": {
|
||||
"dance": "dance",
|
||||
"dance": "danse",
|
||||
"swing": "swing",
|
||||
"nightcore": "nightcore",
|
||||
"folk": "folk",
|
||||
"family": "famille",
|
||||
"ambient": "ambiante",
|
||||
"funk": "funk",
|
||||
"folk-acoustic": "folk & acoustique",
|
||||
"kids-family": "enfants & famille",
|
||||
"ambient": "ambiant",
|
||||
"funk-disco": "funk & disco",
|
||||
"jazz": "jazz",
|
||||
"soul": "soul",
|
||||
"speedcore": "speedcore",
|
||||
"punk": "punk",
|
||||
"rb": "r&b",
|
||||
"holiday": "vacance",
|
||||
"holiday": "vacances",
|
||||
"vocaloid": "vocaloid",
|
||||
"j-rock": "j-rock",
|
||||
"trance": "trance",
|
||||
"drumbass": "drum & bass",
|
||||
"comedy": "comédie",
|
||||
"drum-and-bass": "drum & bass",
|
||||
"comedy-meme": "comédie & meme",
|
||||
"instrumental": "instrumental",
|
||||
"hardcore": "hardcore",
|
||||
"k-pop": "k-pop",
|
||||
"indie": "indé",
|
||||
"indie": "indie",
|
||||
"techno": "techno",
|
||||
"house": "house",
|
||||
"game": "jeu vidéo",
|
||||
"film": "film",
|
||||
"alt": "alternative",
|
||||
"video-game-soundtrack": "jeu vidéo",
|
||||
"tv-movie-soundtrack": "TV & film",
|
||||
"alternative": "alternatif",
|
||||
"dubstep": "dubstep",
|
||||
"metal": "metal",
|
||||
"metal": "métal",
|
||||
"anime": "anime",
|
||||
"hiphop": "hiphop",
|
||||
"hip-hop-rap": "hip hop & rap",
|
||||
"j-pop": "j-pop",
|
||||
"rock": "rock",
|
||||
"pop": "pop",
|
||||
"electronic": "électronique",
|
||||
"classical-orchestral": "classique & orchestrale"
|
||||
"classical-orchestral": "Classique & Orchestral"
|
||||
},
|
||||
"map-specificities": {
|
||||
"automapper": "IA",
|
||||
@@ -1053,6 +1100,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"drop-zone": {
|
||||
"or-browse-files": "Ou parcourir les fichiers"
|
||||
},
|
||||
"playlist": {
|
||||
"error-playlist-creation-title": "Erreur lors de la création de la playlist",
|
||||
"error-playlist-creation-desc": "Une erreur est survenue lors de la création de la playlist.",
|
||||
@@ -1167,7 +1217,26 @@
|
||||
"last-week": "Dernière semaine",
|
||||
"last-month": "Dernier mois",
|
||||
"3-last-month": "3 derniers mois"
|
||||
},
|
||||
"playlists-imported": "Playlists importées",
|
||||
"all-playlists-have-been-successfully-imported": "Toutes les playlists ont été importées avec succès",
|
||||
"no-playlist-found": "Aucune playlist trouvée",
|
||||
"no-playlist-found-in-selected-files": "Aucune playlist trouvée dans les fichiers sélectionnés",
|
||||
"some-playlists-not-imported": "Certaines playlists non importées",
|
||||
"some-playlists-have-been-imported": {
|
||||
"INVALID_SOURCE": "Certaines playlists n'ont pas pu être trouvées",
|
||||
"INVALID_PLAYLIST_FILE": "Certaines playlists ne sont pas valides",
|
||||
"CANNOT_PARSE_PLAYLIST": "Certaines playlists ne sont pas lisibles",
|
||||
"unknown": "Certaines playlists n'ont pas pu être importées"
|
||||
},
|
||||
"no-playlists-imported": "Aucune playlist importée",
|
||||
"no-playlists-imported-errors": {
|
||||
"INVALID_SOURCE": "Les playlists n'ont pas été trouvées",
|
||||
"INVALID_PLAYLIST_FILE": "Les playlists ne sont pas valides",
|
||||
"CANNOT_PARSE_PLAYLIST": "Les playlists ne sont pas lisibles",
|
||||
"unknown": "Aucune playlist n'a pu être importée"
|
||||
}
|
||||
|
||||
},
|
||||
"dateformat": {
|
||||
"dayNames": ["Dim", "Lun", "Mar", "Mer", "Jeu", "Ven", "Sam", "Dimanche", "Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi"],
|
||||
|
||||
@@ -45,14 +45,16 @@
|
||||
"filters-btn": "絞り込み",
|
||||
"dropdown": {
|
||||
"export-maps": "マップをエクスポート",
|
||||
"delete-maps": "マップを削除"
|
||||
"delete-maps": "マップを削除",
|
||||
"delete-duplicate-maps": " 重複を削除"
|
||||
}
|
||||
},
|
||||
"tabs": {
|
||||
"maps": {
|
||||
"actions": {
|
||||
"add-maps": {
|
||||
"text": "追加"
|
||||
"drop-down": {
|
||||
"browse-maps": "マップを閲覧",
|
||||
"import-maps": "マップをインポート"
|
||||
},
|
||||
"link-maps": {
|
||||
"tooltips": {
|
||||
@@ -64,6 +66,21 @@
|
||||
"empty-maps": {
|
||||
"text": "マップがありません",
|
||||
"button": "マップをダウンロードする"
|
||||
},
|
||||
"drop-zone": {
|
||||
"text": "マップをインポート",
|
||||
"subtext": "ZIPファイルをここにドロップしてマップをインポート"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
"drop-down": {
|
||||
"browse-playlists": "プレイリストを閲覧",
|
||||
"create-a-playlist": "プレイリストを作成",
|
||||
"import-playlists": "プレイリストをインポート"
|
||||
},
|
||||
"drop-zone": {
|
||||
"text": "プレイリストをインポート",
|
||||
"subtext": "\".bplist\" または \".json\" ファイルをここにドロップしてインポート"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,6 +137,7 @@
|
||||
"title": "Steam & Oculus",
|
||||
"description": "ログアウトすると、次のBeat Saberダウンロード時にアカウントを切り替えることができます。",
|
||||
"logout": "ログアウト",
|
||||
"logout-success": "ログアウト成功",
|
||||
"download-platform": {
|
||||
"title": "デフォルトプラットフォーム",
|
||||
"desc": "Beat Saberのダウンロードに使用するデフォルトのプラットフォームを選択します。",
|
||||
@@ -248,11 +266,13 @@
|
||||
"errors": {
|
||||
"titles": {
|
||||
"operation-running": "稼働中",
|
||||
"no-internet": "インターネット接続がありません"
|
||||
"no-internet": "インターネット接続がありません",
|
||||
"file-not-supported": "ファイルはサポートされていません"
|
||||
},
|
||||
"msg": {
|
||||
"operation-running": "現在の処理が終了するのを待ってから、もう一度試してください。",
|
||||
"no-internet": "インターネット接続を確認し、もう一度お試しください。"
|
||||
"no-internet": "インターネット接続を確認し、もう一度お試しください。",
|
||||
"file-not-supported": "{types}ファイルのみがサポートされています。"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -441,7 +461,8 @@
|
||||
"CantEditSteam": "編集不可",
|
||||
"CantRename": "改名不可能",
|
||||
"VersionAlreadExist": "このバージョンは既に存在しています!",
|
||||
"CantClone": "クローン作成不可"
|
||||
"CantClone": "クローン作成不可",
|
||||
"UnknownError": "不明なエラーが発生しました"
|
||||
},
|
||||
"msg": {
|
||||
"CantEditSteam": "Steam版は編集できませんがクローンを作ることは可能です。"
|
||||
@@ -494,6 +515,27 @@
|
||||
"one-click-install": {
|
||||
"success": "マップのインストール完了",
|
||||
"error": "マップのインストール中にエラーが発生しました"
|
||||
},
|
||||
"no-duplicates-maps": {
|
||||
"title": "重複なし",
|
||||
"msg": "マップは削除されませんでした"
|
||||
},
|
||||
"duplicates-maps-deleted": {
|
||||
"title": "重複削除",
|
||||
"msg": "重複が削除されました"
|
||||
},
|
||||
"import-map": {
|
||||
"titles": {
|
||||
"success": "マップのインポートが完了しました",
|
||||
"error": "マップのインポート中にエラーが発生しました"
|
||||
},
|
||||
"msgs": {
|
||||
"success": "マップが正常にインポートされました。",
|
||||
"some-success": "一部のマップが正常にインポートされました。",
|
||||
"only-accept-zip": "zipファイルのみがサポートされています。",
|
||||
"invalid-zip": "zipファイルにマップが含まれていません。",
|
||||
"unknown": "不明なエラーが発生しました。"
|
||||
}
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -688,6 +730,11 @@
|
||||
"title": "マップを保持は、リンクを解除した後、共有フォルダから現在のバージョンにすべてのマップをコピーします。これを無効をしても共有マップは失われません。"
|
||||
},
|
||||
"valid-btn": "マップのリンクを解除"
|
||||
},
|
||||
"delete-duplicate-maps": {
|
||||
"title": "マップを削除しますか?",
|
||||
"desc": "マップ「{map}」のみが重複しています。削除してもよろしいですか?",
|
||||
"desc-plural": "{nb}個の重複したマップが見つかりました。削除してもよろしいですか?"
|
||||
}
|
||||
},
|
||||
"download-maps": {
|
||||
@@ -800,39 +847,39 @@
|
||||
"dance": "ダンス",
|
||||
"swing": "スウィング",
|
||||
"nightcore": "ナイトコア",
|
||||
"folk": "フォーク",
|
||||
"family": "ファミリー",
|
||||
"folk-acoustic": "フォーク & アコースティック",
|
||||
"kids-family": "キッズ & ファミリー",
|
||||
"ambient": "アンビエント",
|
||||
"funk": "ファンク",
|
||||
"funk-disco": "ファンク & ディスコ",
|
||||
"jazz": "ジャズ",
|
||||
"soul": "ソウル",
|
||||
"speedcore": "スピードコア ",
|
||||
"speedcore": "スピードコア",
|
||||
"punk": "パンク",
|
||||
"rb": "r&b",
|
||||
"holiday": "休日",
|
||||
"holiday": "ホリデー",
|
||||
"vocaloid": "ボーカロイド",
|
||||
"j-rock": "j-rock",
|
||||
"j-rock": "J-ロック",
|
||||
"trance": "トランス",
|
||||
"drumbass": "ドラムンベース",
|
||||
"comedy": "コメディ",
|
||||
"instrumental": "器楽",
|
||||
"drum-and-bass": "ドラム & ベース",
|
||||
"comedy-meme": "コメディ & ミーム",
|
||||
"instrumental": "インストゥルメンタル",
|
||||
"hardcore": "ハードコア",
|
||||
"k-pop": "k-pop",
|
||||
"indie": "インディー",
|
||||
"techno": "テクノ",
|
||||
"house": "家",
|
||||
"game": "ビデオゲーム",
|
||||
"film": "映画音楽",
|
||||
"alt": "オルタナティヴ",
|
||||
"house": "ハウス",
|
||||
"video-game-soundtrack": "ゲーム音楽",
|
||||
"tv-movie-soundtrack": "TV & 映画",
|
||||
"alternative": "オルタナティブ",
|
||||
"dubstep": "ダブステップ",
|
||||
"metal": "メタル",
|
||||
"anime": "アニメ",
|
||||
"hiphop": "ヒップポップ",
|
||||
"j-pop": "j-pop",
|
||||
"hip-hop-rap": "ヒップホップ & ラップ",
|
||||
"j-pop": "J-ポップ",
|
||||
"rock": "ロック",
|
||||
"pop": "ポップ",
|
||||
"electronic": "エレクトロニック",
|
||||
"classical-orchestral": "Classical & Orchestral"
|
||||
"classical-orchestral": "クラシック & オーケストラ"
|
||||
},
|
||||
"map-specificities": {
|
||||
"automapper": "AI",
|
||||
@@ -859,7 +906,8 @@
|
||||
"bsr-code" : "BSRコード",
|
||||
"download" : "マップをダウンロード",
|
||||
"downloading" :"マップをダウンロード中",
|
||||
"cancel-download" : "ダウンロードをキャンセル"
|
||||
"cancel-download" : "ダウンロードをキャンセル",
|
||||
"hightlight-difficulty": "難易度をハイライト"
|
||||
}
|
||||
},
|
||||
"models": {
|
||||
@@ -1051,6 +1099,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"drop-zone": {
|
||||
"or-browse-files": "またはファイルを参照"
|
||||
},
|
||||
"playlist": {
|
||||
"error-playlist-creation-title": "プレイリストの作成エラー",
|
||||
"error-playlist-creation-desc": "プレイリストの作成中にエラーが発生しました。",
|
||||
@@ -1165,6 +1216,24 @@
|
||||
"last-week": "先週",
|
||||
"last-month": "先月",
|
||||
"3-last-month": "過去3ヶ月"
|
||||
},
|
||||
"playlists-imported": "プレイリストがインポートされました",
|
||||
"all-playlists-have-been-successfully-imported": "すべてのプレイリストが正常にインポートされました",
|
||||
"no-playlist-found": "プレイリストが見つかりません",
|
||||
"no-playlist-found-in-selected-files": "選択されたファイルにプレイリストが見つかりません",
|
||||
"some-playlists-not-imported": "一部のプレイリストはインポートされませんでした",
|
||||
"some-playlists-have-been-imported": {
|
||||
"INVALID_SOURCE": "一部のプレイリストが見つかりませんでした",
|
||||
"INVALID_PLAYLIST_FILE": "一部のプレイリストは無効です",
|
||||
"CANNOT_PARSE_PLAYLIST": "一部のプレイリストは読み取れません",
|
||||
"unknown": "一部のプレイリストをインポートできませんでした"
|
||||
},
|
||||
"no-playlists-imported": "プレイリストはインポートされませんでした",
|
||||
"no-playlists-imported-errors": {
|
||||
"INVALID_SOURCE": "プレイリストが見つかりませんでした",
|
||||
"INVALID_PLAYLIST_FILE": "プレイリストは無効です",
|
||||
"CANNOT_PARSE_PLAYLIST": "プレイリストは読み取れません",
|
||||
"unknown": "プレイリストをインポートできませんでした"
|
||||
}
|
||||
},
|
||||
"dateformat": {
|
||||
|
||||
@@ -45,14 +45,16 @@
|
||||
"filters-btn": "Фильтр",
|
||||
"dropdown": {
|
||||
"export-maps": "Экспорт карт",
|
||||
"delete-maps": "Удалить карты"
|
||||
"delete-maps": "Удалить карты",
|
||||
"delete-duplicate-maps": "Удалить дубликаты"
|
||||
}
|
||||
},
|
||||
"tabs": {
|
||||
"maps": {
|
||||
"actions": {
|
||||
"add-maps": {
|
||||
"text": "Добавить"
|
||||
"drop-down": {
|
||||
"browse-maps": "Просмотр карт",
|
||||
"import-maps": "Импорт карт"
|
||||
},
|
||||
"link-maps": {
|
||||
"tooltips": {
|
||||
@@ -64,6 +66,21 @@
|
||||
"empty-maps": {
|
||||
"text": "Нет карт",
|
||||
"button": "Скачать карты"
|
||||
},
|
||||
"drop-zone": {
|
||||
"text": "Импортируйте свои карты",
|
||||
"subtext": "Перетащите сюда файлы ZIP, чтобы импортировать свои карты"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
"drop-down": {
|
||||
"browse-playlists": "Просмотр плейлистов",
|
||||
"create-a-playlist": "Создать плейлист",
|
||||
"import-playlists": "Импорт плейлистов"
|
||||
},
|
||||
"drop-zone": {
|
||||
"text": "Импортируйте ваши плейлисты",
|
||||
"subtext": "Перетащите сюда файлы \".bplist\" или \".json\" для их импорта"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,6 +137,7 @@
|
||||
"title": "Steam & Oculus",
|
||||
"description": "Выход позволит вам сменить учетную запись при следующей загрузке Beat Saber.",
|
||||
"logout": "Выйти",
|
||||
"logout-success": "Выход выполнен успешно",
|
||||
"download-platform": {
|
||||
"title": "Основная платформа",
|
||||
"desc": "Выберите основную платформу, которая будет использоваться для скачивания версий Beat Saber.",
|
||||
@@ -248,11 +266,13 @@
|
||||
"errors": {
|
||||
"titles": {
|
||||
"operation-running": "Операция в процессе",
|
||||
"no-internet": "Нет интернета"
|
||||
"no-internet": "Нет интернета",
|
||||
"file-not-supported": "Файл не поддерживается"
|
||||
},
|
||||
"msg": {
|
||||
"operation-running": "Дождитесь завершения операции, потом попробуйте снова.",
|
||||
"no-internet": "Проверьте своё соединение и попробуйте снова."
|
||||
"no-internet": "Проверьте своё соединение и попробуйте снова.",
|
||||
"file-not-supported": "Поддерживаются только файлы {types}."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -441,7 +461,8 @@
|
||||
"CantEditSteam": "Не удалось изменить",
|
||||
"CantRename": "Переименование невозможно",
|
||||
"VersionAlreadExist": "Эта версия уже добавлена",
|
||||
"CantClone": "Клонирование невозможно"
|
||||
"CantClone": "Клонирование невозможно",
|
||||
"UnknownError": "Произошла неизвестная ошибка"
|
||||
},
|
||||
"msg": {
|
||||
"CantEditSteam": "Вы не можете изменить версию из Steam, но вы можете её клонировать."
|
||||
@@ -494,6 +515,27 @@
|
||||
"one-click-install": {
|
||||
"success": "Карта установлена",
|
||||
"error": "Ошибка установки карты"
|
||||
},
|
||||
"no-duplicates-maps": {
|
||||
"title": "Нет дубликатов",
|
||||
"msg": "Ни одна карта не была удалена"
|
||||
},
|
||||
"duplicates-maps-deleted": {
|
||||
"title": "Дубликаты удалены",
|
||||
"msg": "Дубликаты были удалены"
|
||||
},
|
||||
"import-map": {
|
||||
"titles": {
|
||||
"success": "Импорт карт завершен",
|
||||
"error": "Произошла ошибка при импорте карт"
|
||||
},
|
||||
"msgs": {
|
||||
"success": "Карты были успешно импортированы.",
|
||||
"some-success": "Некоторые карты успешно импортированы.",
|
||||
"only-accept-zip": "Поддерживаются только zip-файлы.",
|
||||
"invalid-zip": "В zip-файле(ах) нет карт.",
|
||||
"unknown": "Произошла неизвестная ошибка."
|
||||
}
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -688,6 +730,11 @@
|
||||
"title": "Общие карты будут скопированы в папку карт этой версии. Карты не будут потеряны, если это не выбрано."
|
||||
},
|
||||
"valid-btn": "Отвязать карты"
|
||||
},
|
||||
"delete-duplicate-maps": {
|
||||
"title": "Удалить карту?",
|
||||
"desc": "Только карта \"{map}\" является дубликатом. Вы уверены, что хотите ее удалить?",
|
||||
"desc-plural": "Найдено {nb} дубликатов карт. Вы уверены, что хотите их удалить?"
|
||||
}
|
||||
},
|
||||
"download-maps": {
|
||||
@@ -799,36 +846,36 @@
|
||||
"dance": "танец",
|
||||
"swing": "свинг",
|
||||
"nightcore": "найткор",
|
||||
"folk": "народные",
|
||||
"family": "семейные",
|
||||
"folk-acoustic": "фолк и акустика",
|
||||
"kids-family": "дети и семья",
|
||||
"ambient": "эмбиент",
|
||||
"funk": "фанк",
|
||||
"funk-disco": "фанк и диско",
|
||||
"jazz": "джаз",
|
||||
"soul": "соул",
|
||||
"speedcore": "спидкор",
|
||||
"punk": "панк",
|
||||
"rb": "ар-н-би",
|
||||
"holiday": "праздник",
|
||||
"vocaloid": "вокалоиды",
|
||||
"rb": "r&b",
|
||||
"holiday": "праздники",
|
||||
"vocaloid": "вокалоид",
|
||||
"j-rock": "джей-рок",
|
||||
"trance": "транс",
|
||||
"drumbass": "драм-н-бейс",
|
||||
"comedy": "комедия",
|
||||
"instrumental": "инструментальные",
|
||||
"drum-and-bass": "драм-н-бейс",
|
||||
"comedy-meme": "комедия и мемы",
|
||||
"instrumental": "инструментал",
|
||||
"hardcore": "хардкор",
|
||||
"k-pop": "кей-поп",
|
||||
"k-pop": "k-pop",
|
||||
"indie": "инди",
|
||||
"techno": "техно",
|
||||
"house": "хаус",
|
||||
"game": "видеоигры",
|
||||
"film": "фильмы",
|
||||
"alt": "альтернативные",
|
||||
"dubstep": "дапстеп",
|
||||
"video-game-soundtrack": "видеоигра",
|
||||
"tv-movie-soundtrack": "ТВ и кино",
|
||||
"alternative": "альтернатива",
|
||||
"dubstep": "дабстеп",
|
||||
"metal": "метал",
|
||||
"anime": "аниме",
|
||||
"hiphop": "хипхоп",
|
||||
"j-pop": "джей-поп",
|
||||
"rock": "прк",
|
||||
"hip-hop-rap": "хип-хоп и рэп",
|
||||
"j-pop": "j-pop",
|
||||
"rock": "рок",
|
||||
"pop": "поп",
|
||||
"electronic": "электроника",
|
||||
"classical-orchestral": "Классика и Оркестр"
|
||||
@@ -858,7 +905,8 @@
|
||||
"bsr-code" : "Код BSR",
|
||||
"download" : "Скачать карту",
|
||||
"downloading" :"Загрузка карты",
|
||||
"cancel-download" : "Отменить загрузку"
|
||||
"cancel-download" : "Отменить загрузку",
|
||||
"hightlight-difficulty": "Выделить сложность"
|
||||
}
|
||||
},
|
||||
"models": {
|
||||
@@ -1050,6 +1098,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"drop-zone": {
|
||||
"or-browse-files": "Или просмотреть файлы"
|
||||
},
|
||||
"playlist": {
|
||||
"error-playlist-creation-title": "Ошибка при создании плейлиста",
|
||||
"error-playlist-creation-desc": "Произошла ошибка при создании плейлиста.",
|
||||
@@ -1164,6 +1215,24 @@
|
||||
"last-week": "Последняя неделя",
|
||||
"last-month": "Последний месяц",
|
||||
"3-last-month": "Последние 3 месяца"
|
||||
},
|
||||
"playlists-imported": "Плейлисты импортированы",
|
||||
"all-playlists-have-been-successfully-imported": "Все плейлисты успешно импортированы",
|
||||
"no-playlist-found": "Плейлист не найден",
|
||||
"no-playlist-found-in-selected-files": "Плейлист не найден в выбранных файлах",
|
||||
"some-playlists-not-imported": "Некоторые плейлисты не импортированы",
|
||||
"some-playlists-have-been-imported": {
|
||||
"INVALID_SOURCE": "Некоторые плейлисты не найдены",
|
||||
"INVALID_PLAYLIST_FILE": "Некоторые плейлисты недействительны",
|
||||
"CANNOT_PARSE_PLAYLIST": "Некоторые плейлисты нечитаемы",
|
||||
"unknown": "Некоторые плейлисты не удалось импортировать"
|
||||
},
|
||||
"no-playlists-imported": "Плейлисты не импортированы",
|
||||
"no-playlists-imported-errors": {
|
||||
"INVALID_SOURCE": "Плейлисты не найдены",
|
||||
"INVALID_PLAYLIST_FILE": "Плейлисты недействительны",
|
||||
"CANNOT_PARSE_PLAYLIST": "Плейлисты нечитаемы",
|
||||
"unknown": "Не удалось импортировать плейлисты"
|
||||
}
|
||||
},
|
||||
"dateformat": {
|
||||
|
||||
@@ -45,14 +45,16 @@
|
||||
"filters-btn": "篩選",
|
||||
"dropdown": {
|
||||
"export-maps": "導出譜面",
|
||||
"delete-maps": "刪除譜面"
|
||||
"delete-maps": "刪除譜面",
|
||||
"delete-duplicate-maps": "刪除重複項"
|
||||
}
|
||||
},
|
||||
"tabs": {
|
||||
"maps": {
|
||||
"actions": {
|
||||
"add-maps": {
|
||||
"text": "新增"
|
||||
"drop-down": {
|
||||
"browse-maps": "瀏覽地圖",
|
||||
"import-maps": "匯入地圖"
|
||||
},
|
||||
"link-maps": {
|
||||
"tooltips": {
|
||||
@@ -64,6 +66,21 @@
|
||||
"empty-maps": {
|
||||
"text": "無譜面",
|
||||
"button": "下載譜面"
|
||||
},
|
||||
"drop-zone": {
|
||||
"text": "導入您的地圖",
|
||||
"subtext": "將ZIP文件拖放到此處以導入您的地圖"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
"drop-down": {
|
||||
"browse-playlists": "瀏覽播放列表",
|
||||
"create-a-playlist": "建立播放列表",
|
||||
"import-playlists": "匯入播放列表"
|
||||
},
|
||||
"drop-zone": {
|
||||
"text": "匯入您的播放列表",
|
||||
"subtext": "將您的 \".bplist\" 或 \".json\" 檔案拖曳到這裡進行匯入"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,6 +137,7 @@
|
||||
"title": "Steam & Oculus",
|
||||
"description": "登出後,您可以在下一次下載 Beat Saber 時切換帳戶。",
|
||||
"logout": "登出",
|
||||
"logout-success": "登出成功",
|
||||
"download-platform": {
|
||||
"title": ",預設平台",
|
||||
"desc": "選擇要下載 BeatSaber 的預設平台。",
|
||||
@@ -248,11 +266,13 @@
|
||||
"errors": {
|
||||
"titles": {
|
||||
"operation-running": "操作進行中",
|
||||
"no-internet": "無網路"
|
||||
"no-internet": "無網路",
|
||||
"file-not-supported": "文件不受支持"
|
||||
},
|
||||
"msg": {
|
||||
"operation-running": "等待當前操作完成,然後重試。",
|
||||
"no-internet": "檢查你的連接並重試。"
|
||||
"no-internet": "檢查你的連接並重試。",
|
||||
"file-not-supported": "僅支援{types}檔案。"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -441,7 +461,8 @@
|
||||
"CantEditSteam": "無法編輯",
|
||||
"CantRename": "無法重命名",
|
||||
"VersionAlreadExist": "該版本已存在",
|
||||
"CantClone": "無法複製"
|
||||
"CantClone": "無法複製",
|
||||
"UnknownError": "發生了未知錯誤"
|
||||
},
|
||||
"msg": {
|
||||
"CantEditSteam": "你不能編輯 Steam 版本。不過你可以複製它。"
|
||||
@@ -494,6 +515,27 @@
|
||||
"one-click-install": {
|
||||
"success": "譜面安裝完成",
|
||||
"error": "安裝譜面時發生錯誤"
|
||||
},
|
||||
"no-duplicates-maps": {
|
||||
"title": "沒有重複",
|
||||
"msg": "沒有刪除地圖"
|
||||
},
|
||||
"duplicates-maps-deleted": {
|
||||
"title": "重複已刪除",
|
||||
"msg": "重複已刪除"
|
||||
},
|
||||
"import-map": {
|
||||
"titles": {
|
||||
"success": "地圖匯入完成",
|
||||
"error": "地圖匯入時發生錯誤"
|
||||
},
|
||||
"msgs": {
|
||||
"success": "地圖已成功匯入。",
|
||||
"some-success": "部分地圖已成功匯入。",
|
||||
"only-accept-zip": "僅支援zip檔案。",
|
||||
"invalid-zip": "zip檔案中沒有地圖。",
|
||||
"unknown": "發生了未知錯誤。"
|
||||
}
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -688,6 +730,11 @@
|
||||
"title": "保留譜面將會在取消關聯後把共享文件夾的所有譜面複製到當前版本。如果此項被禁用,譜面也不會遺失。"
|
||||
},
|
||||
"valid-btn": "取消關聯譜面"
|
||||
},
|
||||
"delete-duplicate-maps": {
|
||||
"title": "刪除地圖",
|
||||
"desc": "只有地圖「{map}」是重複的。你確定要刪除它嗎?",
|
||||
"desc-plural": "發現了 {nb} 個重複的地圖。你確定要刪除它們嗎?"
|
||||
}
|
||||
},
|
||||
"download-maps": {
|
||||
@@ -797,42 +844,42 @@
|
||||
"tech": "技術"
|
||||
},
|
||||
"map-styles": {
|
||||
"dance": "Dance",
|
||||
"swing": "Swing",
|
||||
"nightcore": "Nightcore",
|
||||
"folk": "Folk",
|
||||
"family": "Family",
|
||||
"ambient": "Ambient",
|
||||
"funk": "Funk",
|
||||
"jazz": "Jazz",
|
||||
"soul": "Soul",
|
||||
"speedcore": "Speedcore",
|
||||
"punk": "Punk",
|
||||
"rb": "R&B",
|
||||
"holiday": "Holiday",
|
||||
"vocaloid": "Vocaloid",
|
||||
"j-rock": "J-rock",
|
||||
"trance": "Trance",
|
||||
"drumbass": "Drum & Bass",
|
||||
"comedy": "Comedy",
|
||||
"instrumental": "Instrumental",
|
||||
"hardcore": "Hardcore",
|
||||
"k-pop": "K-pop",
|
||||
"indie": "Indie",
|
||||
"techno": "Techno",
|
||||
"house": "House",
|
||||
"game": "Video game",
|
||||
"film": "Film",
|
||||
"alt": "Alternative",
|
||||
"dubstep": "Dubstep",
|
||||
"metal": "Metal",
|
||||
"anime": "Anime",
|
||||
"hiphop": "Hiphop",
|
||||
"j-pop": "J-pop",
|
||||
"rock": "Rock",
|
||||
"pop": "Pop",
|
||||
"electronic": "Electronic",
|
||||
"classical-orchestral": "Classical & Orchestral"
|
||||
"dance": "舞蹈",
|
||||
"swing": "搖擺",
|
||||
"nightcore": "夜核",
|
||||
"folk-acoustic": "民謠 & 原聲",
|
||||
"kids-family": "兒童 & 家庭",
|
||||
"ambient": "氛圍音樂",
|
||||
"funk-disco": "放克 & 迪斯科",
|
||||
"jazz": "爵士",
|
||||
"soul": "靈魂",
|
||||
"speedcore": "極速核",
|
||||
"punk": "龐克",
|
||||
"rb": "r&b",
|
||||
"holiday": "假日",
|
||||
"vocaloid": "聲庫",
|
||||
"j-rock": "日式搖滾",
|
||||
"trance": "迷幻",
|
||||
"drum-and-bass": "鼓 & 貝斯",
|
||||
"comedy-meme": "喜劇 & 表情包",
|
||||
"instrumental": "純音樂",
|
||||
"hardcore": "硬核",
|
||||
"k-pop": "k-pop",
|
||||
"indie": "獨立",
|
||||
"techno": "電子舞曲",
|
||||
"house": "浩室音樂",
|
||||
"video-game-soundtrack": "遊戲音樂",
|
||||
"tv-movie-soundtrack": "電視 & 電影",
|
||||
"alternative": "另類",
|
||||
"dubstep": "電音鼓",
|
||||
"metal": "金屬",
|
||||
"anime": "動漫",
|
||||
"hip-hop-rap": "嘻哈 & 說唱",
|
||||
"j-pop": "日式流行",
|
||||
"rock": "搖滾",
|
||||
"pop": "流行",
|
||||
"electronic": "電子",
|
||||
"classical-orchestral": "古典 & 管弦樂"
|
||||
},
|
||||
"map-specificities": {
|
||||
"automapper": "AI",
|
||||
@@ -859,7 +906,8 @@
|
||||
"bsr-code" : "BSR代碼",
|
||||
"download" : "下載地圖",
|
||||
"downloading" :"正在下載地圖",
|
||||
"cancel-download" : "取消下載"
|
||||
"cancel-download" : "取消下載",
|
||||
"hightlight-difficulty": "突出顯示難度"
|
||||
}
|
||||
},
|
||||
"models": {
|
||||
@@ -1051,6 +1099,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"drop-zone": {
|
||||
"or-browse-files": "或瀏覽檔案"
|
||||
},
|
||||
"playlist": {
|
||||
"error-playlist-creation-title": "建立播放清單時發生錯誤",
|
||||
"error-playlist-creation-desc": "建立播放清單時發生錯誤。",
|
||||
@@ -1165,6 +1216,24 @@
|
||||
"last-week": "上週",
|
||||
"last-month": "上個月",
|
||||
"3-last-month": "最近3個月"
|
||||
},
|
||||
"playlists-imported": "播放清單已匯入",
|
||||
"all-playlists-have-been-successfully-imported": "所有播放清單已成功匯入",
|
||||
"no-playlist-found": "未找到播放清單",
|
||||
"no-playlist-found-in-selected-files": "在選定檔案中未找到播放清單",
|
||||
"some-playlists-not-imported": "部分播放清單未匯入",
|
||||
"some-playlists-have-been-imported": {
|
||||
"INVALID_SOURCE": "某些播放清單未找到",
|
||||
"INVALID_PLAYLIST_FILE": "某些播放清單無效",
|
||||
"CANNOT_PARSE_PLAYLIST": "某些播放清單無法解析",
|
||||
"unknown": "某些播放清單無法匯入"
|
||||
},
|
||||
"no-playlists-imported": "未匯入任何播放清單",
|
||||
"no-playlists-imported-errors": {
|
||||
"INVALID_SOURCE": "播放清單未找到",
|
||||
"INVALID_PLAYLIST_FILE": "播放清單無效",
|
||||
"CANNOT_PARSE_PLAYLIST": "播放清單無法解析",
|
||||
"unknown": "無法匯入任何播放清單"
|
||||
}
|
||||
},
|
||||
"dateformat": {
|
||||
|
||||
@@ -45,14 +45,16 @@
|
||||
"filters-btn": "筛选",
|
||||
"dropdown": {
|
||||
"export-maps": "导出谱面",
|
||||
"delete-maps": "删除谱面"
|
||||
"delete-maps": "删除谱面",
|
||||
"delete-duplicate-maps": "删除重复项"
|
||||
}
|
||||
},
|
||||
"tabs": {
|
||||
"maps": {
|
||||
"actions": {
|
||||
"add-maps": {
|
||||
"text": "添加"
|
||||
"drop-down": {
|
||||
"browse-maps": "浏览地图",
|
||||
"import-maps": "导入地图"
|
||||
},
|
||||
"link-maps": {
|
||||
"tooltips": {
|
||||
@@ -64,6 +66,21 @@
|
||||
"empty-maps": {
|
||||
"text": "无谱面",
|
||||
"button": "下载谱面"
|
||||
},
|
||||
"drop-zone": {
|
||||
"text": "导入您的地图",
|
||||
"subtext": "将ZIP文件拖放到此处以导入您的地图"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
"drop-down": {
|
||||
"browse-playlists": "浏览播放列表",
|
||||
"create-a-playlist": "创建播放列表",
|
||||
"import-playlists": "导入播放列表"
|
||||
},
|
||||
"drop-zone": {
|
||||
"text": "导入您的播放列表",
|
||||
"subtext": "将您的 \".bplist\" 或 \".json\" 文件拖放到这里进行导入"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,6 +137,7 @@
|
||||
"title": "Steam & Oculus",
|
||||
"description": "登出后,您可以在下一次下载 Beat Saber 时切换帐户。",
|
||||
"logout": "登出",
|
||||
"logout-success": "注销成功",
|
||||
"download-platform": {
|
||||
"title": ",默认平台",
|
||||
"desc": "选择要下载 BeatSaber 的默认平台。",
|
||||
@@ -248,11 +266,13 @@
|
||||
"errors": {
|
||||
"titles": {
|
||||
"operation-running": "操作进行中",
|
||||
"no-internet": "无网络"
|
||||
"no-internet": "无网络",
|
||||
"file-not-supported": "文件不受支持"
|
||||
},
|
||||
"msg": {
|
||||
"operation-running": "等待当前操作完成,然后重试。",
|
||||
"no-internet": "检查你的连接并重试。"
|
||||
"no-internet": "检查你的连接并重试。",
|
||||
"file-not-supported": "仅支持{types}文件。"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -441,7 +461,8 @@
|
||||
"CantEditSteam": "无法编辑",
|
||||
"CantRename": "无法重命名",
|
||||
"VersionAlreadExist": "该版本已存在",
|
||||
"CantClone": "无法克隆"
|
||||
"CantClone": "无法克隆",
|
||||
"UnknownError": "发生了未知错误"
|
||||
},
|
||||
"msg": {
|
||||
"CantEditSteam": "你不能编辑 Steam 版本。不过你可以克隆它。"
|
||||
@@ -494,6 +515,27 @@
|
||||
"one-click-install": {
|
||||
"success": "谱面安装完成",
|
||||
"error": "安装谱面时发生错误"
|
||||
},
|
||||
"no-duplicates-maps": {
|
||||
"title": "没有重复",
|
||||
"msg": "没有删除地图"
|
||||
},
|
||||
"duplicates-maps-deleted": {
|
||||
"title": "重复已删除",
|
||||
"msg": "重复已删除"
|
||||
},
|
||||
"import-map": {
|
||||
"titles": {
|
||||
"success": "地图导入完成",
|
||||
"error": "导入地图时发生错误"
|
||||
},
|
||||
"msgs": {
|
||||
"success": "地图已成功导入。",
|
||||
"some-success": "部分地图已成功导入。",
|
||||
"only-accept-zip": "仅支持zip文件。",
|
||||
"invalid-zip": "zip文件中没有地图。",
|
||||
"unknown": "发生了未知错误。"
|
||||
}
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -688,6 +730,11 @@
|
||||
"title": "保留谱面将会在取消关联后把共享文件夹的所有谱面复制到当前版本。如果此项被禁用,谱面也不会丢失。"
|
||||
},
|
||||
"valid-btn": "取消关联谱面"
|
||||
},
|
||||
"delete-duplicate-maps": {
|
||||
"title": "删除地图",
|
||||
"desc": "只有地图 \"{map}\" 是重复的。你确定要删除它吗?",
|
||||
"desc-plural": "发现了 {nb} 个重复的地图。你确定要删除它们吗?"
|
||||
}
|
||||
},
|
||||
"download-maps": {
|
||||
@@ -797,42 +844,42 @@
|
||||
"tech": "技术"
|
||||
},
|
||||
"map-styles": {
|
||||
"dance": "dance",
|
||||
"swing": "swing",
|
||||
"nightcore": "nightcore",
|
||||
"folk": "folk",
|
||||
"family": "family",
|
||||
"ambient": "ambient",
|
||||
"funk": "funk",
|
||||
"jazz": "jazz",
|
||||
"soul": "soul",
|
||||
"speedcore": "speedcore",
|
||||
"punk": "punk",
|
||||
"dance": "舞蹈",
|
||||
"swing": "摇摆",
|
||||
"nightcore": "夜核",
|
||||
"folk-acoustic": "民谣 & 原声",
|
||||
"kids-family": "儿童 & 家庭",
|
||||
"ambient": "氛围音乐",
|
||||
"funk-disco": "放克 & 迪斯科",
|
||||
"jazz": "爵士",
|
||||
"soul": "灵魂",
|
||||
"speedcore": "极速核",
|
||||
"punk": "朋克",
|
||||
"rb": "r&b",
|
||||
"holiday": "holiday",
|
||||
"vocaloid": "vocaloid",
|
||||
"j-rock": "j-rock",
|
||||
"trance": "trance",
|
||||
"drumbass": "drum & bass",
|
||||
"comedy": "comedy",
|
||||
"instrumental": "instrumental",
|
||||
"hardcore": "hardcore",
|
||||
"holiday": "假日",
|
||||
"vocaloid": "声库",
|
||||
"j-rock": "日式摇滚",
|
||||
"trance": "迷幻",
|
||||
"drum-and-bass": "鼓 & 贝斯",
|
||||
"comedy-meme": "喜剧 & 表情包",
|
||||
"instrumental": "纯音乐",
|
||||
"hardcore": "硬核",
|
||||
"k-pop": "k-pop",
|
||||
"indie": "indie",
|
||||
"techno": "techno",
|
||||
"house": "house",
|
||||
"game": "video game",
|
||||
"film": "film",
|
||||
"alt": "alternative",
|
||||
"dubstep": "dubstep",
|
||||
"metal": "metal",
|
||||
"anime": "anime",
|
||||
"hiphop": "hiphop",
|
||||
"j-pop": "j-pop",
|
||||
"rock": "rock",
|
||||
"pop": "pop",
|
||||
"electronic": "electronic",
|
||||
"classical-orchestral": "Classical & Orchestral"
|
||||
"indie": "独立",
|
||||
"techno": "电子舞曲",
|
||||
"house": "浩室音乐",
|
||||
"video-game-soundtrack": "游戏音乐",
|
||||
"tv-movie-soundtrack": "电视 & 电影",
|
||||
"alternative": "另类",
|
||||
"dubstep": "电音鼓",
|
||||
"metal": "金属",
|
||||
"anime": "动漫",
|
||||
"hip-hop-rap": "嘻哈 & 说唱",
|
||||
"j-pop": "日式流行",
|
||||
"rock": "摇滚",
|
||||
"pop": "流行",
|
||||
"electronic": "电子",
|
||||
"classical-orchestral": "古典 & 管弦乐"
|
||||
},
|
||||
"map-specificities": {
|
||||
"automapper": "AI",
|
||||
@@ -859,7 +906,8 @@
|
||||
"bsr-code" : "BSR代码",
|
||||
"download" : "下载地图",
|
||||
"downloading" :"正在下载地图",
|
||||
"cancel-download" : "取消下载"
|
||||
"cancel-download" : "取消下载",
|
||||
"hightlight-difficulty": "突出显示难度"
|
||||
}
|
||||
},
|
||||
"models": {
|
||||
@@ -1051,6 +1099,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"drop-zone": {
|
||||
"or-browse-files": "或浏览文件"
|
||||
},
|
||||
"playlist": {
|
||||
"error-playlist-creation-title": "创建播放列表时出错",
|
||||
"error-playlist-creation-desc": "创建播放列表时发生错误。",
|
||||
@@ -1165,6 +1216,24 @@
|
||||
"last-week": "上周",
|
||||
"last-month": "上个月",
|
||||
"3-last-month": "最近3个月"
|
||||
},
|
||||
"playlists-imported": "播放列表已导入",
|
||||
"all-playlists-have-been-successfully-imported": "所有播放列表已成功导入",
|
||||
"no-playlist-found": "未找到播放列表",
|
||||
"no-playlist-found-in-selected-files": "在选定文件中未找到播放列表",
|
||||
"some-playlists-not-imported": "部分播放列表未导入",
|
||||
"some-playlists-have-been-imported": {
|
||||
"INVALID_SOURCE": "某些播放列表未找到",
|
||||
"INVALID_PLAYLIST_FILE": "某些播放列表无效",
|
||||
"CANNOT_PARSE_PLAYLIST": "某些播放列表无法解析",
|
||||
"unknown": "某些播放列表无法导入"
|
||||
},
|
||||
"no-playlists-imported": "未导入任何播放列表",
|
||||
"no-playlists-imported-errors": {
|
||||
"INVALID_SOURCE": "播放列表未找到",
|
||||
"INVALID_PLAYLIST_FILE": "播放列表无效",
|
||||
"CANNOT_PARSE_PLAYLIST": "播放列表无法解析",
|
||||
"unknown": "无法导入任何播放列表"
|
||||
}
|
||||
},
|
||||
"dateformat": {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,21 @@
|
||||
If you encounter a keyboard shortcut conflict between AMD Software and Oculus Rift, you can either disable or modify the conflicting shortcut.
|
||||
|
||||
_Special thanks to_ `𝔽𝕪𝕟𝕟 (fynn07)` _for its contribution with information and screenshots_
|
||||
### __Follow these steps to disable the shortcut in AMD Software:__
|
||||
# Step 1 - Access Settings:
|
||||
- Open AMD Software and click on the gear icon in the top right corner to go to settings.
|
||||
|
||||

|
||||
# Step 2 - Modify Keyboard Shortcuts:
|
||||
- In the settings menu, select the "Hotkey" tab.
|
||||
|
||||

|
||||
# Step 3 -Disabling Shortcuts:
|
||||
- Turn off the "Use Hotkeys" option to prevent any conflicts with Oculus Rift.
|
||||
|
||||

|
||||
# Step 3 alt - Modify the Shortcut:
|
||||
- If you prefer to keep using keyboard shortcuts, consider changing the specific conflicting shortcut (e.g., "Ctrl + Shift + I") to another combination that is less likely to interfere.
|
||||
|
||||
ℹ️ _**Keep in mind you can't change the full keybind!**_
|
||||
_**You can only Change the "i" to an different Button!**_ ℹ️
|
||||
@@ -0,0 +1,14 @@
|
||||
Welcome to the bs-manager wiki! (wip c:)
|
||||
|
||||
to get you oculus token check here : https://github.com/Zagrios/bs-manager/wiki/How-to-obtain-your-Oculus-Token
|
||||
|
||||
---
|
||||
|
||||
This Wiki accepts contributions!
|
||||
|
||||
To contribute to the wiki:
|
||||
|
||||
- Fork the project
|
||||
- Add/Edit `.md` files in the `docs/wiki/` folder
|
||||
- Create a Pull Request
|
||||
- Once the PR is merged (after review), it will be present in the repository's wiki
|
||||
@@ -0,0 +1,30 @@
|
||||
### Important
|
||||
|
||||
Your token is a confidential piece of information. Possession of this token allows individuals to download applications, send messages, among other actions, under your identity.
|
||||
|
||||
However, you might wonder why it is necessary to provide this token to BSManager. The reason is that BSManager requires the token to continue the download with Oculus. Once you've input the token, it is used exclusively to communicate with Oculus servers to verify that you are the rightful owner of the game.
|
||||
|
||||
## Step 1 - Install and log into the Oculus Rift app
|
||||
- Get the Oculus Rift app setup from the [Meta website](https://www.oculus.com/rift/setup/)
|
||||
- Install the Oculus Rift app
|
||||
|
||||
ℹ️ **If you bought Beat Saber from the Quest store, it won't appear in your Rift library by default. To download it with BSManager, first claim it from its store page** ℹ️
|
||||
|
||||
## Step 2 - Open developer tools
|
||||
|
||||
- Open Oculus app
|
||||
- Open the developer tools by pressing <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>i</kbd>.
|
||||
|
||||
## Step 3 - Copy your Token
|
||||
In the developer tools :
|
||||
- Open the `Network` tab ***(1)***
|
||||
- Filter for `graph` ***(2)***
|
||||
- Click on the first request ***(3)***
|
||||
- Open the `Headers` tab ***(4)***
|
||||
- Scroll to the bottom to locate your token, it should start with `FRL` ***(5)***
|
||||
- Select the token using your mouse and press <kbd>Ctrl</kbd> + <kbd>c</kbd> to copy it
|
||||
|
||||

|
||||
|
||||
# Known bugs
|
||||
- [Nothing opens when I press `Ctrl`+`Shift`+`i`.](https://github.com/Zagrios/bs-manager/wiki/Nothing-opens-when-I-press-%60Ctrl%60%E2%80%90%60Shift%60%E2%80%90%60i%60.)
|
||||
@@ -0,0 +1,4 @@
|
||||
If nothing opens when you press `Ctrl` + `Shift` + `I`, it's possible that the keyboard shortcut to open the development tools is being used by another application.
|
||||
|
||||
Here's a non-exhaustive list of applications that have caused a problem and their suggested solutions:
|
||||
- [AMD software](https://github.com/Zagrios/bs-manager/wiki/AMD-Sofware-%E2%80%90-Nothing-opens-when-I-press-%60Ctrl%60%E2%80%90%60Shift%60%E2%80%90%60I%60)
|
||||
Generated
+51
-28
@@ -25,7 +25,7 @@
|
||||
"electron-debug": "^3.2.0",
|
||||
"electron-log": "^4.4.8",
|
||||
"electron-store": "^8.1.0",
|
||||
"electron-updater": "^6.2.1",
|
||||
"electron-updater": "^6.3.4",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"format-duration": "^3.0.2",
|
||||
"framer-motion": "^11.2.6",
|
||||
@@ -39,14 +39,14 @@
|
||||
"node-fetch": "^3.3.2",
|
||||
"node-stream-zip": "^1.15.0",
|
||||
"pako": "^2.1.0",
|
||||
"protobufjs": "^7.3.2",
|
||||
"qrcode.react": "^3.1.0",
|
||||
"protobufjs": "^7.4.0",
|
||||
"qrcode.react": "^4.0.1",
|
||||
"query-process": "^0.0.3",
|
||||
"react": "^18.2.0",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-range": "^1.8.14",
|
||||
"react-range": "^1.10.0",
|
||||
"react-router-dom": "^6.24.1",
|
||||
"react-virtualized-auto-sizer": "^1.0.24",
|
||||
"react-window": "^1.8.10",
|
||||
@@ -57,7 +57,7 @@
|
||||
"semver": "^7.6.3",
|
||||
"serialize-error": "^11.0.3",
|
||||
"striptags": "^4.0.0-alpha.4",
|
||||
"tailwind-merge": "^2.4.0",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"tailwindcss-scoped-groups": "^2.0.0",
|
||||
"tippy.js": "^6.3.7",
|
||||
"to-ico": "^1.1.5",
|
||||
@@ -106,7 +106,7 @@
|
||||
"css-loader": "^6.10.0",
|
||||
"css-minimizer-webpack-plugin": "^6.0.0",
|
||||
"detect-port": "^1.5.1",
|
||||
"electron": "^31.3.0",
|
||||
"electron": "^32.1.2",
|
||||
"electron-builder": "^24.13.3",
|
||||
"electron-devtools-installer": "^3.2.0",
|
||||
"electronmon": "^2.0.2",
|
||||
@@ -138,7 +138,7 @@
|
||||
"sass": "^1.70.0",
|
||||
"sass-loader": "^14.1.0",
|
||||
"style-loader": "^3.3.4",
|
||||
"tailwindcss": "^3.4.7",
|
||||
"tailwindcss": "^3.4.12",
|
||||
"terser-webpack-plugin": "^5.3.10",
|
||||
"ts-jest": "^29.1.2",
|
||||
"ts-loader": "^9.5.1",
|
||||
@@ -8622,6 +8622,7 @@
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz",
|
||||
"integrity": "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"debug": "^4.3.4",
|
||||
"sax": "^1.2.4"
|
||||
@@ -10718,11 +10719,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/electron": {
|
||||
"version": "31.3.0",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-31.3.0.tgz",
|
||||
"integrity": "sha512-3LMRMmK4UK0A+jYSLGLYdfhc20TgY2v5jD3iGmhRZlDYj0gn7xBj/waRjlNalysZ0D2rgPvoes0wHuf5e/Bguw==",
|
||||
"version": "32.1.2",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-32.1.2.tgz",
|
||||
"integrity": "sha512-CXe6doFzhmh1U7daOvUzmF6Cj8hssdYWMeEPRnRO6rB9/bbwMlWctcQ7P8NJXhLQ88/vYUJQrJvlJPh8qM0BRQ==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@electron/get": "^2.0.0",
|
||||
"@types/node": "^20.9.0",
|
||||
@@ -11048,20 +11050,34 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/electron-updater": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.2.1.tgz",
|
||||
"integrity": "sha512-83eKIPW14qwZqUUM6wdsIRwVKZyjmHxQ4/8G+1C6iS5PdDt7b1umYQyj1/qPpH510GmHEQe4q0kCPe3qmb3a0Q==",
|
||||
"version": "6.3.4",
|
||||
"resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.3.4.tgz",
|
||||
"integrity": "sha512-uZUo7p1Y53G4tl6Cgw07X1yF8Jlz6zhaL7CQJDZ1fVVkOaBfE2cWtx80avwDVi8jHp+I/FWawrMgTAeCCNIfAg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"builder-util-runtime": "9.2.4",
|
||||
"builder-util-runtime": "9.2.5",
|
||||
"fs-extra": "^10.1.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lazy-val": "^1.0.5",
|
||||
"lodash.escaperegexp": "^4.1.2",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"semver": "^7.3.8",
|
||||
"semver": "^7.6.3",
|
||||
"tiny-typed-emitter": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-updater/node_modules/builder-util-runtime": {
|
||||
"version": "9.2.5",
|
||||
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.5.tgz",
|
||||
"integrity": "sha512-HjIDfhvqx/8B3TDN4GbABQcgpewTU4LMRTQPkVpKYV3lsuxEJoIfvg09GyWTNmfVNSUAYf+fbTN//JX4TH20pg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.3.4",
|
||||
"sax": "^1.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-updater/node_modules/fs-extra": {
|
||||
"version": "10.1.0",
|
||||
"license": "MIT",
|
||||
@@ -18634,10 +18650,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/protobufjs": {
|
||||
"version": "7.3.2",
|
||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.3.2.tgz",
|
||||
"integrity": "sha512-RXyHaACeqXeqAKGLDl68rQKbmObRsTIn4TYVUUug1KfS47YWCo5MacGITEryugIgZqORCvJWEk4l449POg5Txg==",
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz",
|
||||
"integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@protobufjs/aspromise": "^1.1.2",
|
||||
"@protobufjs/base64": "^1.1.2",
|
||||
@@ -18712,7 +18729,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/qrcode.react": {
|
||||
"version": "3.1.0",
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.0.1.tgz",
|
||||
"integrity": "sha512-Lpj0tPBn561WiQ3QQWXbkx8xTtB8BZkJeMZWLJIL8iaPBCoWzW1IpCeU3gY5MDqsb0+efCvEGkl9O0naP64crA==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
@@ -18914,11 +18933,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-range": {
|
||||
"version": "1.8.14",
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/react-range/-/react-range-1.10.0.tgz",
|
||||
"integrity": "sha512-kDo0LiBUHIQIP8menx0UoxTnHr7UXBYpIYl/DR9jCaO1o29VwvCLpkP/qOTNQz5hkJadPg1uEM07XJcJ1XGoKw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0-0 || ^17.0.0-0 || ^18.0.0-0",
|
||||
"react-dom": "^16.8.0-0 || ^17.0.0-0 || ^18.0.0-0"
|
||||
"react": "*",
|
||||
"react-dom": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-redux": {
|
||||
@@ -20798,9 +20819,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tailwind-merge": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.4.0.tgz",
|
||||
"integrity": "sha512-49AwoOQNKdqKPd9CViyH5wJoSKsCDjUlzL8DxuGp3P1FsGY36NJDAa18jLZcaHAUUuTj+JB8IAo8zWgBNvBF7A==",
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.2.tgz",
|
||||
"integrity": "sha512-kjEBm+pvD+6eAwzJL2Bi+02/9LFLal1Gs61+QB7HvTfQQ0aXwC5LGT8PEt1gS0CWKktKe6ysPTAy3cBC5MeiIg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/dcastil"
|
||||
@@ -20823,9 +20845,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "3.4.7",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.7.tgz",
|
||||
"integrity": "sha512-rxWZbe87YJb4OcSopb7up2Ba4U82BoiSGUdoDr3Ydrg9ckxFS/YWsvhN323GMcddgU65QRy7JndC7ahhInhvlQ==",
|
||||
"version": "3.4.12",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.12.tgz",
|
||||
"integrity": "sha512-Htf/gHj2+soPb9UayUNci/Ja3d8pTmu9ONTfh4QY8r3MATTZOzmv6UYWF7ZwikEIC8okpfqmGqrmDehua8mF8w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"arg": "^5.0.2",
|
||||
|
||||
+10
-9
@@ -19,7 +19,7 @@
|
||||
"start:preload": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.preload.dev.ts",
|
||||
"start:renderer": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack serve --config ./.erb/configs/webpack.config.renderer.dev.ts",
|
||||
"test": "jest",
|
||||
"publish": "npm run build && electron-builder -c.win.certificateSha1=842a817a51e2a1d360fcd62f54bf5f9193e919e1 --publish always --win --x64",
|
||||
"publish": "npm run build && electron-builder -c.win.certificateSha1=2164d6a7d641ecf6ad57852f665a518ca2bf960f --publish always --win --x64",
|
||||
"publish:linux": "npm run build && electron-builder --publish always --linux --x64"
|
||||
},
|
||||
"lint-staged": {
|
||||
@@ -53,6 +53,7 @@
|
||||
"afterSign": ".erb/scripts/notarize.js",
|
||||
"afterPack": ".erb/scripts/after-pack.js",
|
||||
"win": {
|
||||
"signingHashAlgorithms": ["sha256"],
|
||||
"target": [
|
||||
"nsis",
|
||||
"nsis-web"
|
||||
@@ -183,7 +184,7 @@
|
||||
"css-loader": "^6.10.0",
|
||||
"css-minimizer-webpack-plugin": "^6.0.0",
|
||||
"detect-port": "^1.5.1",
|
||||
"electron": "^31.3.0",
|
||||
"electron": "^32.1.2",
|
||||
"electron-builder": "^24.13.3",
|
||||
"electron-devtools-installer": "^3.2.0",
|
||||
"electronmon": "^2.0.2",
|
||||
@@ -215,7 +216,7 @@
|
||||
"sass": "^1.70.0",
|
||||
"sass-loader": "^14.1.0",
|
||||
"style-loader": "^3.3.4",
|
||||
"tailwindcss": "^3.4.7",
|
||||
"tailwindcss": "^3.4.12",
|
||||
"terser-webpack-plugin": "^5.3.10",
|
||||
"ts-jest": "^29.1.2",
|
||||
"ts-loader": "^9.5.1",
|
||||
@@ -243,7 +244,7 @@
|
||||
"electron-debug": "^3.2.0",
|
||||
"electron-log": "^4.4.8",
|
||||
"electron-store": "^8.1.0",
|
||||
"electron-updater": "^6.2.1",
|
||||
"electron-updater": "^6.3.4",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"format-duration": "^3.0.2",
|
||||
"framer-motion": "^11.2.6",
|
||||
@@ -257,14 +258,14 @@
|
||||
"node-fetch": "^3.3.2",
|
||||
"node-stream-zip": "^1.15.0",
|
||||
"pako": "^2.1.0",
|
||||
"protobufjs": "^7.3.2",
|
||||
"qrcode.react": "^3.1.0",
|
||||
"protobufjs": "^7.4.0",
|
||||
"qrcode.react": "^4.0.1",
|
||||
"query-process": "^0.0.3",
|
||||
"react": "^18.2.0",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-range": "^1.8.14",
|
||||
"react-range": "^1.10.0",
|
||||
"react-router-dom": "^6.24.1",
|
||||
"react-virtualized-auto-sizer": "^1.0.24",
|
||||
"react-window": "^1.8.10",
|
||||
@@ -275,7 +276,7 @@
|
||||
"semver": "^7.6.3",
|
||||
"serialize-error": "^11.0.3",
|
||||
"striptags": "^4.0.0-alpha.4",
|
||||
"tailwind-merge": "^2.4.0",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"tailwindcss-scoped-groups": "^2.0.0",
|
||||
"tippy.js": "^6.3.7",
|
||||
"to-ico": "^1.1.5",
|
||||
@@ -320,6 +321,6 @@
|
||||
"logLevel": "quiet"
|
||||
},
|
||||
"volta": {
|
||||
"node": "20.11.0"
|
||||
"node": "20.17.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import crypto from "crypto";
|
||||
import { execSync } from "child_process";
|
||||
import { tryit } from "../../shared/helpers/error.helpers";
|
||||
import { CustomError } from "shared/models/exceptions/custom-error.class";
|
||||
import { ErrorObject } from "serialize-error";
|
||||
|
||||
export async function pathExist(path: string): Promise<boolean> {
|
||||
try {
|
||||
@@ -92,17 +93,17 @@ export function moveFolderContent(src: string, dest: string, option?: MoveOption
|
||||
|
||||
await ensureFolderExist(dest);
|
||||
|
||||
const files = await readdir(src, { encoding: "utf-8" });
|
||||
const files = await readdir(src, { encoding: "utf-8", withFileTypes: true });
|
||||
progress.total = files.length;
|
||||
|
||||
for(const file of files){
|
||||
const srcFullPath = path.join(src, file);
|
||||
const destFullPath = path.join(dest, file);
|
||||
const srcFullPath = path.join(src, file.name);
|
||||
const destFullPath = path.join(dest, file.name);
|
||||
|
||||
const srcChilds = await readdir(srcFullPath, { encoding: "utf-8", recursive: true });
|
||||
const srcChilds = file.isDirectory() ? await readdir(srcFullPath, { encoding: "utf-8", recursive: true }) : [];
|
||||
const allChildsAlreadyExist = srcChilds.every(child => pathExistsSync(path.join(destFullPath, child)));
|
||||
|
||||
if(!allChildsAlreadyExist){
|
||||
if(file.isFile() || !allChildsAlreadyExist){
|
||||
await move(srcFullPath, destFullPath, option);
|
||||
}
|
||||
|
||||
@@ -279,4 +280,5 @@ export interface Progression<T = unknown, D = unknown> {
|
||||
diff?: number;
|
||||
data?: T;
|
||||
extra?: D;
|
||||
lastError?: ErrorObject;
|
||||
}
|
||||
|
||||
@@ -22,11 +22,21 @@ ipc.on("export-maps", async (args, reply) => {
|
||||
reply(await maps.exportMaps(args.version, args.maps, args.outPath));
|
||||
});
|
||||
|
||||
ipc.on("download-map", async (args, reply) => {
|
||||
ipc.on("bs-maps.import-maps", async (args, reply) => {
|
||||
const maps = LocalMapsManagerService.getInstance();
|
||||
reply(maps.importMaps(args.paths, args.version));
|
||||
})
|
||||
|
||||
ipc.on("bs-maps.download-map", async (args, reply) => {
|
||||
const maps = LocalMapsManagerService.getInstance();
|
||||
reply(from(maps.downloadMap(args.map, args.version)));
|
||||
});
|
||||
|
||||
ipc.on("last-downloaded-map", (_, reply) => {
|
||||
const maps = LocalMapsManagerService.getInstance();
|
||||
reply(maps.lastDownloadedMap$);
|
||||
})
|
||||
|
||||
ipc.on("one-click-install-map", (args, reply) => {
|
||||
const maps = LocalMapsManagerService.getInstance();
|
||||
reply(from(maps.oneClickDownloadMap(args)))
|
||||
|
||||
@@ -16,15 +16,15 @@ ipc.on("get-installed-mods", (args, reply) => {
|
||||
|
||||
ipc.on("install-mods", (args, reply) => {
|
||||
const modsManager = BsModsManagerService.getInstance();
|
||||
reply(from(modsManager.installMods(args.mods, args.version)));
|
||||
reply(modsManager.installMods(args.mods, args.version));
|
||||
});
|
||||
|
||||
ipc.on("uninstall-mods", (args, reply) => {
|
||||
const modsManager = BsModsManagerService.getInstance();
|
||||
reply(from(modsManager.uninstallMods(args.mods, args.version)));
|
||||
reply(modsManager.uninstallMods(args.mods, args.version));
|
||||
});
|
||||
|
||||
ipc.on("uninstall-all-mods", (args, reply) => {
|
||||
const modsManager = BsModsManagerService.getInstance();
|
||||
reply(from(modsManager.uninstallAllMods(args)));
|
||||
reply(modsManager.uninstallAllMods(args));
|
||||
});
|
||||
|
||||
@@ -3,8 +3,6 @@ import { IpcService } from "../services/ipc.service";
|
||||
import { from, lastValueFrom, mergeMap, of } from "rxjs";
|
||||
import { LocalMapsManagerService } from "../services/additional-content/maps/local-maps-manager.service";
|
||||
import { Progression } from "main/helpers/fs.helpers";
|
||||
import { isValidUrl } from "shared/helpers/url.helpers";
|
||||
import { pathToFileURL } from "url";
|
||||
|
||||
const ipc = IpcService.getInstance();
|
||||
|
||||
@@ -31,10 +29,8 @@ ipc.on("is-playlists-deep-links-enabled", (args, reply) => {
|
||||
ipc.on("download-playlist", (args, reply) => {
|
||||
const playlists = LocalPlaylistsManagerService.getInstance();
|
||||
|
||||
const downloadUrl = isValidUrl(args.downloadSource) ? args.downloadSource : pathToFileURL(args.downloadSource).href;
|
||||
|
||||
return reply(playlists.downloadPlaylist({
|
||||
bpListUrl: downloadUrl,
|
||||
bplistSource: args.downloadSource,
|
||||
version: args.version,
|
||||
ignoreSongsHashs: args.ignoreSongsHashs,
|
||||
dest: args.dest
|
||||
@@ -63,6 +59,11 @@ ipc.on("export-playlists", (args, reply) => {
|
||||
reply(playlists.exportPlaylists(args));
|
||||
});
|
||||
|
||||
ipc.on("import-playlists", (args, reply) => {
|
||||
const playlists = LocalPlaylistsManagerService.getInstance();
|
||||
reply(playlists.importPlaylists(args));
|
||||
});
|
||||
|
||||
ipc.on("install-playlist-file", (args, reply) => {
|
||||
const playlists = LocalPlaylistsManagerService.getInstance();
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ const ipc = IpcService.getInstance();
|
||||
|
||||
ipc.on("download-update", (_, reply) => {
|
||||
const updaterService = AutoUpdaterService.getInstance();
|
||||
reply(from(updaterService.downloadUpdate()));
|
||||
reply(updaterService.downloadUpdate());
|
||||
});
|
||||
|
||||
ipc.on("check-update", (_, reply) => {
|
||||
|
||||
@@ -13,6 +13,11 @@ ipc.on("new-window", (args, reply) => {
|
||||
reply(from(shell.openExternal(args)));
|
||||
});
|
||||
|
||||
ipc.on("open-dialog", (args, reply) => {
|
||||
// Use this ipc instead of "choose-folder" or "choose-file" to have more control over the dialog
|
||||
reply(from(dialog.showOpenDialog(args)));
|
||||
})
|
||||
|
||||
ipc.on("choose-folder", (args, reply) => {
|
||||
reply(from(dialog.showOpenDialog({ properties: ["openDirectory"], defaultPath: args ?? "" })));
|
||||
});
|
||||
|
||||
+4
-1
@@ -1,4 +1,4 @@
|
||||
import { contextBridge, ipcRenderer, IpcRendererEvent } from "electron";
|
||||
import { contextBridge, ipcRenderer, IpcRendererEvent, webUtils } from "electron";
|
||||
import { ProviderPlatform } from "shared/models/provider-platform.enum";
|
||||
|
||||
const sep = process.platform === ProviderPlatform.WINDOWS ? "\\" : "/";
|
||||
@@ -30,5 +30,8 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
join: (...args: string[]): string => {
|
||||
return args.join(sep);
|
||||
}
|
||||
},
|
||||
webUtils: {
|
||||
getPathForFile: webUtils.getPathForFile
|
||||
}
|
||||
});
|
||||
|
||||
@@ -16,7 +16,6 @@ import { FileAssociationService } from "../file-association.service";
|
||||
import { SongDetailsCacheService } from "./maps/song-details-cache.service";
|
||||
import { sToMs } from "shared/helpers/time.helpers";
|
||||
import { LocalBPList, LocalBPListsDetails } from "shared/models/playlists/local-playlist.models";
|
||||
import { SongCacheService } from "./maps/song-cache.service";
|
||||
import { InstallationLocationService } from "../installation-location.service";
|
||||
import sanitize from "sanitize-filename";
|
||||
import { isValidUrl } from "shared/helpers/url.helpers";
|
||||
@@ -27,6 +26,7 @@ import { tryit } from "shared/helpers/error.helpers";
|
||||
import recursiveReadDir from "recursive-readdir";
|
||||
import { BsvMapDetail, SongDetails } from "shared/models/maps";
|
||||
import { findHashInString } from "shared/helpers/string.helpers";
|
||||
import { serializeError } from "serialize-error";
|
||||
|
||||
export class LocalPlaylistsManagerService {
|
||||
private static instance: LocalPlaylistsManagerService;
|
||||
@@ -42,6 +42,7 @@ export class LocalPlaylistsManagerService {
|
||||
private readonly DEEP_LINKS = {
|
||||
BeatSaver: "bsplaylist",
|
||||
};
|
||||
private readonly PLAYLIST_FILETYPES = [".bplist", ".json"];
|
||||
|
||||
private readonly versions: BSLocalVersionService;
|
||||
private readonly maps: LocalMapsManagerService;
|
||||
@@ -51,7 +52,6 @@ export class LocalPlaylistsManagerService {
|
||||
private readonly windows: WindowManagerService;
|
||||
private readonly bsaver: BeatSaverService;
|
||||
private readonly songDetails: SongDetailsCacheService;
|
||||
private readonly songCache: SongCacheService;
|
||||
private readonly bsmFs: InstallationLocationService;
|
||||
|
||||
private constructor() {
|
||||
@@ -63,7 +63,6 @@ export class LocalPlaylistsManagerService {
|
||||
this.windows = WindowManagerService.getInstance();
|
||||
this.bsaver = BeatSaverService.getInstance();
|
||||
this.songDetails = SongDetailsCacheService.getInstance();
|
||||
this.songCache = SongCacheService.getInstance();
|
||||
this.bsmFs = InstallationLocationService.getInstance();
|
||||
|
||||
|
||||
@@ -89,11 +88,15 @@ export class LocalPlaylistsManagerService {
|
||||
return fullPath;
|
||||
}
|
||||
|
||||
private acceptPlaylistFiletype(filename: string): boolean {
|
||||
return this.PLAYLIST_FILETYPES.includes(path.extname(filename).toLowerCase());
|
||||
}
|
||||
|
||||
public writeBPListFile(opt: { bpList: BPList, version?: BSVersion, dest?: string }): Observable<LocalBPList> {
|
||||
return new Observable<LocalBPList>(obs => {
|
||||
(async () => {
|
||||
const dest = await (async () => {
|
||||
if(opt.dest && path.isAbsolute(opt.dest) && path.extname(opt.dest) === ".bplist") { return opt.dest; }
|
||||
if(opt.dest && path.isAbsolute(opt.dest) && this.acceptPlaylistFiletype(opt.dest)) { return opt.dest; }
|
||||
const playlistFolder = await this.getPlaylistsFolder(opt.version);
|
||||
const playlistPath = path.join(playlistFolder, `${sanitize(opt.bpList.playlistTitle)}.bplist`);
|
||||
return getUniqueFileNamePath(playlistPath);
|
||||
@@ -111,14 +114,14 @@ export class LocalPlaylistsManagerService {
|
||||
}
|
||||
|
||||
private async installBPListFile(opt: {
|
||||
bslistSource: string,
|
||||
bplistSource: string,
|
||||
version?: BSVersion,
|
||||
dest?: string
|
||||
}): Promise<{path: string, localBPList: LocalBPList}> {
|
||||
const bplist = await this.readPlaylistFromSource(opt.bslistSource);
|
||||
const bplist = await this.readPlaylistFromSource(opt.bplistSource);
|
||||
|
||||
const dest = await (async () => {
|
||||
if(opt.dest && path.isAbsolute(opt.dest) && path.extname(opt.dest) === ".bplist") { return opt.dest; }
|
||||
if(opt.dest && path.isAbsolute(opt.dest) && this.acceptPlaylistFiletype(opt.dest)) { return opt.dest; }
|
||||
const playlistFolder = await this.getPlaylistsFolder(opt.version);
|
||||
return path.join(playlistFolder, `${sanitize(bplist.playlistTitle)}.bplist`);
|
||||
})();
|
||||
@@ -135,13 +138,22 @@ export class LocalPlaylistsManagerService {
|
||||
const isLocalFile = await pathExists(source).catch(e => { log.error(e); return false; });
|
||||
|
||||
if(!isLocalFile && !isValidUrl(source)) {
|
||||
throw new Error(`Invalid source ${source}`);
|
||||
throw new CustomError(`Invalid source (${source})`, "INVALID_SOURCE");
|
||||
}
|
||||
|
||||
const bpList: BPList = isLocalFile ? JSON.parse(readFileSync(source).toString()) : await this.request.getJSON<BPList>(source);
|
||||
const bpList: BPList = await (async () => {
|
||||
if(isLocalFile){
|
||||
const res = await tryit(async () => JSON.parse(readFileSync(source).toString()));
|
||||
if(res.error) {
|
||||
throw CustomError.fromError(res.error, "CANNOT_PARSE_PLAYLIST");
|
||||
}
|
||||
return res.result;
|
||||
}
|
||||
return this.request.getJSON<BPList>(source);
|
||||
})();
|
||||
|
||||
if(!bpList?.playlistTitle) {
|
||||
throw new Error(`Invalid playlist file ${source}`);
|
||||
throw new CustomError(`Invalid playlist file (${source})`, "INVALID_PLAYLIST_FILE");
|
||||
}
|
||||
|
||||
bpList.songs = (bpList.songs ?? []).map(s => s.hash ? (
|
||||
@@ -168,7 +180,7 @@ export class LocalPlaylistsManagerService {
|
||||
}
|
||||
|
||||
const ignoreFunc = (file: string, stats: Stats): boolean => {
|
||||
if(stats.isFile() && path.extname(file) !== ".bplist") { return true; }
|
||||
if(stats.isFile() && !this.acceptPlaylistFiletype(file)) { return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -347,8 +359,8 @@ export class LocalPlaylistsManagerService {
|
||||
});
|
||||
}
|
||||
|
||||
public downloadPlaylist({ bpListUrl, version, ignoreSongsHashs = [], dest }: {
|
||||
bpListUrl: string,
|
||||
public downloadPlaylist({ bplistSource, version, ignoreSongsHashs = [], dest }: {
|
||||
bplistSource: string,
|
||||
version?: BSVersion
|
||||
ignoreSongsHashs?: string[]
|
||||
dest?: string
|
||||
@@ -359,7 +371,7 @@ export class LocalPlaylistsManagerService {
|
||||
return new Observable<Progression<DownloadPlaylistProgressionData>>(obs => {
|
||||
(async () => {
|
||||
|
||||
const { localBPList } = await this.installBPListFile({ bslistSource: bpListUrl, version, dest });
|
||||
const { localBPList } = await this.installBPListFile({ bplistSource, version, dest });
|
||||
|
||||
await lastValueFrom(this.downloadPlaylistSongs(localBPList, ignoreSongsHashs, version).pipe(
|
||||
tap({ next: p => obs.next(p) }),
|
||||
@@ -428,13 +440,56 @@ export class LocalPlaylistsManagerService {
|
||||
return archive.finalize();
|
||||
}
|
||||
|
||||
public oneClickInstallPlaylist(bpListUrl: string): Observable<Progression<DownloadPlaylistProgressionData>> {
|
||||
public importPlaylists({ version, paths }: { version?: BSVersion, paths: string[] }): Observable<Progression<LocalBPListsDetails>> {
|
||||
return new Observable<Progression<LocalBPListsDetails>>(obs => {
|
||||
|
||||
let canceled = false;
|
||||
|
||||
(async () => {
|
||||
const bplistPaths = paths.filter(p => this.acceptPlaylistFiletype(p));
|
||||
const progress: Progression<LocalBPListsDetails> = { current: 0, total: bplistPaths.length, data: null };
|
||||
|
||||
obs.next(progress);
|
||||
|
||||
for(const playlistPath of bplistPaths) {
|
||||
if(canceled) {
|
||||
log.info("Playlist import canceled");
|
||||
return;
|
||||
}
|
||||
|
||||
const { result: localBPList, error } = await tryit(() => this.installBPListFile({ bplistSource: playlistPath, version }));
|
||||
|
||||
if(error) {
|
||||
progress.lastError = serializeError(CustomError.fromError(error));
|
||||
obs.next(progress);
|
||||
|
||||
log.error(error);
|
||||
continue;
|
||||
}
|
||||
|
||||
const bpListDetails = this.getLocalBPListDetails(localBPList.localBPList);
|
||||
|
||||
progress.current += 1;
|
||||
progress.data = bpListDetails;
|
||||
obs.next(progress);
|
||||
}
|
||||
})()
|
||||
.catch(err => obs.error(err))
|
||||
.finally(() => obs.complete());
|
||||
|
||||
return () => {
|
||||
canceled = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public oneClickInstallPlaylist(bplistUrl: string): Observable<Progression<DownloadPlaylistProgressionData>> {
|
||||
|
||||
return new Observable<Progression<DownloadPlaylistProgressionData>>(obs => {
|
||||
(async () => {
|
||||
const versions = await this.versions.getInstalledVersions();
|
||||
|
||||
const download$ = this.downloadPlaylist({ bpListUrl, version: versions.pop() }).pipe(tap({
|
||||
const download$ = this.downloadPlaylist({ bplistSource: bplistUrl, version: versions.pop() }).pipe(tap({
|
||||
next: progress => obs.next(progress),
|
||||
error: err => obs.error(err),
|
||||
}));
|
||||
@@ -446,7 +501,7 @@ export class LocalPlaylistsManagerService {
|
||||
const realSourceMapsFolder = await realpath(path.dirname(downloadedMaps[0].path));
|
||||
|
||||
for (const version of versions) {
|
||||
await this.installBPListFile({ bslistSource: playlist.path, version});
|
||||
await this.installBPListFile({ bplistSource: playlist.path, version});
|
||||
|
||||
const versionMapsFolder = await this.maps.getMapsFolderPath(version);
|
||||
const realDestMapsFolder = await realpath(versionMapsFolder).catch(e => {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import path from "path";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { BsvMapDetail, RawMapInfoData } from "shared/models/maps";
|
||||
import { BsvMapDetail } from "shared/models/maps";
|
||||
import { BsmLocalMap, BsmLocalMapsProgress, DeleteMapsProgress } from "shared/models/maps/bsm-local-map.interface";
|
||||
import { BSLocalVersionService } from "../../bs-local-version.service";
|
||||
import { InstallationLocationService } from "../../installation-location.service";
|
||||
import { UtilsService } from "../../utils.service";
|
||||
import crypto from "crypto";
|
||||
import crypto, { BinaryLike } from "crypto";
|
||||
import { lstatSync } from "fs";
|
||||
import { copy, createReadStream, ensureDir, pathExists, pathExistsSync, realpath, unlink } from "fs-extra";
|
||||
import StreamZip from "node-stream-zip";
|
||||
@@ -14,7 +14,7 @@ import sanitize from "sanitize-filename";
|
||||
import { DeepLinkService } from "../../deep-link.service";
|
||||
import log from "electron-log";
|
||||
import { WindowManagerService } from "../../window-manager.service";
|
||||
import { Observable, lastValueFrom } from "rxjs";
|
||||
import { Observable, Subject, lastValueFrom } from "rxjs";
|
||||
import { Archive } from "../../../models/archive.class";
|
||||
import { Progression, deleteFolder, ensureFolderExist, getFilesInFolder, getFoldersInFolder, pathExist } from "../../../helpers/fs.helpers";
|
||||
import { readFile } from "fs/promises";
|
||||
@@ -23,10 +23,13 @@ import { allSettled } from "../../../../shared/helpers/promise.helpers";
|
||||
import { splitIntoChunk } from "../../../../shared/helpers/array.helpers";
|
||||
import { SongDetailsCacheService } from "./song-details-cache.service";
|
||||
import { SongCacheService } from "./song-cache.service";
|
||||
import { IpcService } from "../../ipc.service";
|
||||
import { pathToFileURL } from "url";
|
||||
import { sToMs } from "../../../../shared/helpers/time.helpers";
|
||||
import { FieldRequired } from "shared/helpers/type.helpers";
|
||||
import { MapInfo } from "shared/models/maps/info/map-info.model";
|
||||
import { parseMapInfoDat } from "shared/parsers/maps/map-info.parser";
|
||||
import { CustomError } from "shared/models/exceptions/custom-error.class";
|
||||
import { tryit } from "shared/helpers/error.helpers";
|
||||
|
||||
export class LocalMapsManagerService {
|
||||
private static instance: LocalMapsManagerService;
|
||||
@@ -57,7 +60,8 @@ export class LocalMapsManagerService {
|
||||
private readonly linker: FolderLinkerService;
|
||||
private readonly songDetailsCache: SongDetailsCacheService;
|
||||
private readonly songCache: SongCacheService;
|
||||
private readonly ipc: IpcService;
|
||||
|
||||
private readonly _lastDownloadedMap = new Subject<{ map: BsmLocalMap, version?: BSVersion }>();
|
||||
|
||||
private constructor() {
|
||||
this.localVersion = BSLocalVersionService.getInstance();
|
||||
@@ -69,7 +73,6 @@ export class LocalMapsManagerService {
|
||||
this.linker = FolderLinkerService.getInstance();
|
||||
this.songDetailsCache = SongDetailsCacheService.getInstance();
|
||||
this.songCache = SongCacheService.getInstance();
|
||||
this.ipc = IpcService.getInstance();
|
||||
|
||||
const handleOneClick = (mapId: string, isHash = false) => {
|
||||
this.windows.openWindow(`oneclick-download-map.html?mapId=${mapId}&isHash=${isHash}`);
|
||||
@@ -98,24 +101,35 @@ export class LocalMapsManagerService {
|
||||
}
|
||||
|
||||
private async computeMapHash(mapPath: string, rawInfoString: string): Promise<string> {
|
||||
const mapRawInfo: RawMapInfoData = JSON.parse(rawInfoString);
|
||||
const { result: mapInfo, error } = tryit(() => parseMapInfoDat(JSON.parse(rawInfoString)));
|
||||
|
||||
if(!mapInfo || error) {
|
||||
log.error(`Unable to cumpute hash, cannot parse map info at ${mapPath}`, error);
|
||||
throw CustomError.fromError(error, `Unable to cumpute hash, cannot parse map info at ${mapPath}`, "cannot-parse-map-info");
|
||||
}
|
||||
|
||||
const shasum = crypto.createHash("sha1");
|
||||
shasum.update(rawInfoString);
|
||||
|
||||
const hashFile = (filePath: string): Promise<void> => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const stream = createReadStream(filePath);
|
||||
stream.on("data", data => shasum.update(data));
|
||||
stream.on("data", data => shasum.update(data as BinaryLike));
|
||||
stream.on("error", reject);
|
||||
stream.on("close", resolve);
|
||||
});
|
||||
};
|
||||
|
||||
for (const set of mapRawInfo._difficultyBeatmapSets) {
|
||||
for (const diff of set._difficultyBeatmaps) {
|
||||
const diffFilePath = path.join(mapPath, diff._beatmapFilename);
|
||||
for (const diff of mapInfo.difficulties) {
|
||||
if(diff.beatmapFilename){
|
||||
const diffFilePath = path.join(mapPath, diff.beatmapFilename);
|
||||
await hashFile(diffFilePath);
|
||||
}
|
||||
|
||||
if(diff.lightshowDataFilename) {
|
||||
const lightshowFilePath = path.join(mapPath, diff.lightshowDataFilename);
|
||||
await hashFile(lightshowFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
return shasum.digest("hex");
|
||||
@@ -123,16 +137,16 @@ export class LocalMapsManagerService {
|
||||
|
||||
public async loadMapInfoFromPath(mapPath: string): Promise<BsmLocalMap> {
|
||||
|
||||
const getUrlsAndReturn = (rawInfo: RawMapInfoData, hash: string, mapPath: string) => {
|
||||
const coverUrl = pathToFileURL(path.join(mapPath, rawInfo._coverImageFilename)).href;
|
||||
const songUrl = pathToFileURL(path.join(mapPath, rawInfo._songFilename)).href;
|
||||
return { rawInfo, coverUrl, songUrl, hash, path: mapPath, songDetails: this.songDetailsCache.getSongDetails(hash) } as BsmLocalMap;
|
||||
const getUrlsAndReturn = (mapInfo: MapInfo, hash: string, mapPath: string): BsmLocalMap => {
|
||||
const coverUrl = pathToFileURL(path.join(mapPath, mapInfo.coverImageFilename)).href;
|
||||
const songUrl = pathToFileURL(path.join(mapPath, mapInfo.songFilename)).href;
|
||||
return { mapInfo, coverUrl, songUrl, hash, path: mapPath, songDetails: this.songDetailsCache.getSongDetails(hash) };
|
||||
};
|
||||
|
||||
const cachedInfos = this.songCache.getMapInfoFromDirname(path.basename(mapPath));
|
||||
const cachedMapInfos = this.songCache.getMapInfoFromDirname(path.basename(mapPath));
|
||||
|
||||
if (cachedInfos) {
|
||||
return getUrlsAndReturn(cachedInfos.rawInfo, cachedInfos.hash, mapPath);
|
||||
if (cachedMapInfos) {
|
||||
return getUrlsAndReturn(cachedMapInfos.mapInfo, cachedMapInfos.hash, mapPath);
|
||||
}
|
||||
|
||||
const files = await getFilesInFolder(mapPath);
|
||||
@@ -143,10 +157,16 @@ export class LocalMapsManagerService {
|
||||
}
|
||||
|
||||
const rawInfoString = await readFile(infoFile, { encoding: "utf-8" });
|
||||
const rawInfo: RawMapInfoData = JSON.parse(rawInfoString);
|
||||
const { result: mapInfo, error } = tryit(() => parseMapInfoDat(JSON.parse(rawInfoString)));
|
||||
|
||||
if (error) {
|
||||
log.error(`Cannot parse map info.dat. Map path: ${mapPath}`, error);
|
||||
throw CustomError.fromError(error, `Cannot read map info.dat. Map path: ${mapPath}`, "cannot-parse-map-info");
|
||||
}
|
||||
|
||||
const hash = await this.computeMapHash(mapPath, rawInfoString);
|
||||
|
||||
return getUrlsAndReturn(rawInfo, hash, mapPath);
|
||||
return getUrlsAndReturn(mapInfo, hash, mapPath);
|
||||
}
|
||||
|
||||
private async downloadMapZip(zipUrl: string): Promise<{ zip: StreamZip.StreamZipAsync; zipPath: string }> {
|
||||
@@ -194,7 +214,7 @@ export class LocalMapsManagerService {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.songCache.setMapInfoFromDirname(path.basename(mapPath), { rawInfo: mapInfo.rawInfo, hash: mapInfo.hash });
|
||||
this.songCache.setMapInfoFromDirname(path.basename(mapPath), { mapInfo: mapInfo.mapInfo, hash: mapInfo.hash });
|
||||
|
||||
progression.loaded++;
|
||||
observer.next(progression);
|
||||
@@ -268,8 +288,8 @@ export class LocalMapsManagerService {
|
||||
const mapsPaths = await getFoldersInFolder(versionMapsPath);
|
||||
|
||||
for (const mapPath of mapsPaths) {
|
||||
const mapInfo = await this.loadMapInfoFromPath(mapPath);
|
||||
if (hashs.includes(mapInfo.hash)) {
|
||||
const { result: mapInfo } = await tryit(() => this.loadMapInfoFromPath(mapPath));
|
||||
if (mapInfo && hashs.includes(mapInfo.hash)) {
|
||||
await deleteFolder(mapPath);
|
||||
this.songCache.deleteMapInfoFromDirname(path.basename(mapPath));
|
||||
progress.current++;
|
||||
@@ -297,8 +317,8 @@ export class LocalMapsManagerService {
|
||||
const mapsPaths = await getFoldersInFolder(versionMapsPath);
|
||||
|
||||
for (const mapPath of mapsPaths) {
|
||||
const mapInfo = await this.loadMapInfoFromPath(mapPath);
|
||||
if (mapInfo.hash === hash) {
|
||||
const { result: mapInfo } = await tryit(() => this.loadMapInfoFromPath(mapPath));
|
||||
if (mapInfo?.hash === hash) {
|
||||
return mapInfo;
|
||||
}
|
||||
}
|
||||
@@ -306,6 +326,111 @@ export class LocalMapsManagerService {
|
||||
return null;
|
||||
}
|
||||
|
||||
public importMaps(zipPaths: string[], version?: BSVersion): Observable<Progression<BsmLocalMap>> {
|
||||
return new Observable<Progression<BsmLocalMap>>(obs => {
|
||||
let unsubscribed = false;
|
||||
let progress: Progression<BsmLocalMap> = { total: 0, current: 0 };
|
||||
let nbImportedMaps = 0;
|
||||
|
||||
(async () => {
|
||||
const mapsPath = await this.getMapsFolderPath(version);
|
||||
for(const zipPath of zipPaths) {
|
||||
|
||||
if(unsubscribed) {
|
||||
log.info("Maps importation from zip has been cancelled");
|
||||
return;
|
||||
}
|
||||
|
||||
progress = { total: 0, current: 0 };
|
||||
obs.next(progress); // reset progress for each zip
|
||||
|
||||
if(!pathExistsSync(zipPath)) { continue; }
|
||||
|
||||
const zip = new StreamZip.async({ file: zipPath });
|
||||
const { result: zipEntries, error } = await tryit(() => zip.entries());
|
||||
|
||||
if(error) {
|
||||
const res = await tryit(() => zip.close());
|
||||
log.error("Could not read zip entries", zipPath, error, res?.error);
|
||||
continue;
|
||||
}
|
||||
|
||||
const zipEntriesValues = Object.values(zipEntries);
|
||||
|
||||
const mapsFolders = zipEntriesValues.reduce((acc, entry) => {
|
||||
if(!/(^|\/)[Ii]nfo\.dat$/.test(entry.name)){ return acc; }
|
||||
acc.push(path.dirname(entry.name));
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
if(mapsFolders.length === 0) {
|
||||
log.warn("No maps \"info.dat\" found in zip", zipPath);
|
||||
}
|
||||
|
||||
progress.total = mapsFolders.length;
|
||||
obs.next(progress);
|
||||
|
||||
for(const folder of mapsFolders) {
|
||||
|
||||
if(unsubscribed) {
|
||||
log.info("Maps importation from zip has been cancelled");
|
||||
await zip.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const isRoot = folder === ".";
|
||||
const dest = isRoot ? path.join(mapsPath, path.basename(zipPath, ".zip")) : path.join(mapsPath, folder);
|
||||
|
||||
let extract: () => Promise<BsmLocalMap>;
|
||||
|
||||
if(isRoot){
|
||||
const entries = zipEntriesValues.filter(entry => entry.isFile && path.dirname(entry.name) === ".");
|
||||
extract = async () => {
|
||||
await Promise.all(entries.map(entry => {
|
||||
log.info("Extracting", `"${entry.name}"`, "from", `"${zipPath}"`, "into", `"${path.join(dest, path.basename(entry.name))}"`);
|
||||
return zip.extract(entry.name, path.join(dest, path.basename(entry.name)));
|
||||
}));
|
||||
return this.loadMapInfoFromPath(dest);
|
||||
};
|
||||
} else {
|
||||
extract = async () => {
|
||||
log.info("Extracting", `"${folder}"`, "from", `"${zipPath}"`, "into", `"${mapsPath}"`);
|
||||
await zip.extract(folder, dest);
|
||||
return this.loadMapInfoFromPath(dest);
|
||||
}
|
||||
}
|
||||
|
||||
await ensureDir(dest);
|
||||
const { result: bsmMap, error } = await tryit(extract);
|
||||
|
||||
if(error) {
|
||||
log.error("Could not extract map", zipPath, folder, mapsPath, error);
|
||||
continue;
|
||||
}
|
||||
|
||||
nbImportedMaps++;
|
||||
progress.current++;
|
||||
progress.data = bsmMap;
|
||||
obs.next(progress);
|
||||
}
|
||||
|
||||
await zip.close();
|
||||
}
|
||||
})()
|
||||
.then(() => {
|
||||
if(!nbImportedMaps){
|
||||
throw new CustomError("No \"Info.dat\" file located in any of the zip files", "invalid-zip");
|
||||
}
|
||||
return log.info("Successfully imported", nbImportedMaps, "maps from", zipPaths.length, "zips");
|
||||
})
|
||||
.catch(e => obs.error(e))
|
||||
.finally(() => obs.complete());
|
||||
|
||||
return () => {
|
||||
unsubscribed = true;
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public async downloadMap(map: BsvMapDetail, version?: BSVersion): Promise<BsmLocalMap> {
|
||||
if (!map.versions.at(0).hash) {
|
||||
@@ -344,7 +469,7 @@ export class LocalMapsManagerService {
|
||||
const localMap = await this.loadMapInfoFromPath(mapPath);
|
||||
localMap.songDetails = this.songDetailsCache.getSongDetails(localMap.hash);
|
||||
|
||||
this.ipc.send<{map: BsmLocalMap, version?: BSVersion}>("map-downloaded", this.windows.getWindows("index.html").at(0), { map: localMap, version });
|
||||
this._lastDownloadedMap.next({ map: localMap, version });
|
||||
|
||||
return localMap;
|
||||
}
|
||||
@@ -392,4 +517,8 @@ export class LocalMapsManagerService {
|
||||
public isDeepLinksEnabled(): boolean {
|
||||
return Array.from(Object.values(this.DEEP_LINKS)).every(link => this.deepLink.isDeepLinkRegistered(link));
|
||||
}
|
||||
|
||||
public get lastDownloadedMap$(): Observable<{ map: BsmLocalMap, version?: BSVersion }> {
|
||||
return this._lastDownloadedMap.asObservable();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CACHE_PATH } from "main/constants";
|
||||
import { JsonCache } from "main/models/json-cache.class";
|
||||
import path from "path";
|
||||
import { RawMapInfoData } from "shared/models/maps";
|
||||
import { MapInfo } from "shared/models/maps/info/map-info.model";
|
||||
|
||||
export class SongCacheService {
|
||||
|
||||
@@ -14,34 +14,35 @@ export class SongCacheService {
|
||||
return SongCacheService.instance;
|
||||
}
|
||||
|
||||
private readonly RAW_INFOS_CACHE_PATH = path.join(CACHE_PATH, "song-raw-info-cache.json");
|
||||
private readonly MAPS_INFO_CACHE_PATH = path.join(CACHE_PATH, "map-info-cache.json");
|
||||
|
||||
private readonly rawInfosCache: JsonCache<CachedRawInfoWithHash>;
|
||||
private readonly mapsInfoCache: JsonCache<CachedMapInfoWithHash>;
|
||||
|
||||
private constructor(){
|
||||
this.rawInfosCache = new JsonCache(this.RAW_INFOS_CACHE_PATH);
|
||||
console.log(this.MAPS_INFO_CACHE_PATH);
|
||||
this.mapsInfoCache = new JsonCache(this.MAPS_INFO_CACHE_PATH);
|
||||
}
|
||||
|
||||
public getMapInfoFromDirname(dirname: string): CachedRawInfoWithHash {
|
||||
return this.rawInfosCache.get(dirname);
|
||||
public getMapInfoFromDirname(dirname: string): CachedMapInfoWithHash {
|
||||
return this.mapsInfoCache.get(dirname);
|
||||
}
|
||||
|
||||
public getMapInfoFromHash(hash: string): { dirname: string, info: CachedRawInfoWithHash } | undefined {
|
||||
const res = Object.entries(this.rawInfosCache.cache).find(([, info]) => info.hash === hash);
|
||||
public getMapInfoFromHash(hash: string): { dirname: string, info: CachedMapInfoWithHash } | undefined {
|
||||
const res = Object.entries(this.mapsInfoCache.cache).find(([, info]) => info.hash === hash);
|
||||
return res ? { dirname: res[0], info: res[1] } : undefined;
|
||||
}
|
||||
|
||||
public setMapInfoFromDirname(dirname: string, info: CachedRawInfoWithHash): void {
|
||||
this.rawInfosCache.set(dirname, info);
|
||||
public setMapInfoFromDirname(dirname: string, info: CachedMapInfoWithHash): void {
|
||||
this.mapsInfoCache.set(dirname, info);
|
||||
}
|
||||
|
||||
public deleteMapInfoFromDirname(dirname: string): void {
|
||||
this.rawInfosCache.delete(dirname);
|
||||
this.mapsInfoCache.delete(dirname);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export type CachedRawInfoWithHash = {
|
||||
export type CachedMapInfoWithHash = {
|
||||
hash: string;
|
||||
rawInfo: RawMapInfoData;
|
||||
mapInfo: MapInfo;
|
||||
};
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { autoUpdater } from "electron-updater";
|
||||
import { autoUpdater, CancellationToken, ProgressInfo } from "electron-updater";
|
||||
import log from "electron-log";
|
||||
import { UtilsService } from "./utils.service";
|
||||
import { gt } from "semver";
|
||||
import { Progression } from "main/helpers/fs.helpers";
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
export class AutoUpdaterService {
|
||||
private static instance: AutoUpdaterService;
|
||||
|
||||
private readonly utilsService: UtilsService;
|
||||
|
||||
public static getInstance(): AutoUpdaterService {
|
||||
if (!AutoUpdaterService.instance) {
|
||||
AutoUpdaterService.instance = new AutoUpdaterService();
|
||||
@@ -18,37 +17,46 @@ export class AutoUpdaterService {
|
||||
constructor() {
|
||||
autoUpdater.logger = log;
|
||||
autoUpdater.autoDownload = false;
|
||||
|
||||
this.utilsService = UtilsService.getInstance();
|
||||
}
|
||||
|
||||
public isUpdateAvailable(): Promise<boolean> {
|
||||
return new Promise(resolve => {
|
||||
autoUpdater
|
||||
.checkForUpdates()
|
||||
.then(info => {
|
||||
const needUpdate = (() => {
|
||||
if (!info?.updateInfo) {
|
||||
return false;
|
||||
}
|
||||
return gt(info.updateInfo.version, autoUpdater.currentVersion.version);
|
||||
})();
|
||||
resolve(needUpdate);
|
||||
})
|
||||
.catch(() => resolve(false));
|
||||
});
|
||||
return autoUpdater.checkForUpdates().then(info => {
|
||||
return !!info?.updateInfo && gt(info.updateInfo.version, autoUpdater.currentVersion.version);
|
||||
}).catch(() => false);
|
||||
}
|
||||
|
||||
public downloadUpdate(): Promise<boolean> {
|
||||
autoUpdater.removeAllListeners("download-progress");
|
||||
autoUpdater.addListener("download-progress", info => {
|
||||
this.utilsService.ipcSend("update-download-progress", { success: true, data: info.percent });
|
||||
});
|
||||
public downloadUpdate(): Observable<Progression> {
|
||||
return new Observable<Progression>(observer => {
|
||||
|
||||
return autoUpdater.downloadUpdate().then(res => !!res && !!res.length);
|
||||
observer.next({ current: 0, total: 0 });
|
||||
|
||||
const progressListener = (progress: ProgressInfo) => {
|
||||
observer.next({ current: progress.transferred, total: progress.total });
|
||||
};
|
||||
|
||||
const downloadedListener = () => {
|
||||
observer.next({ current: 100, total: 100 });
|
||||
};
|
||||
|
||||
autoUpdater.addListener("download-progress", progressListener);
|
||||
autoUpdater.addListener("update-downloaded", downloadedListener);
|
||||
|
||||
const cancelToken = new CancellationToken();
|
||||
|
||||
autoUpdater.downloadUpdate(cancelToken)
|
||||
.catch(err => observer.error(err))
|
||||
.finally(() => observer.complete());
|
||||
|
||||
return () => {
|
||||
cancelToken.cancel();
|
||||
autoUpdater.removeListener("download-progress", progressListener);
|
||||
autoUpdater.removeListener("update-downloaded", downloadedListener);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public quitAndInstall() {
|
||||
autoUpdater.quitAndInstall();
|
||||
log.info("Quit and install");
|
||||
return autoUpdater.quitAndInstall();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,6 +140,8 @@ export class SteamLauncherService extends AbstractLauncherService implements Sto
|
||||
"STEAM_COMPAT_INSTALL_PATH": bsFolderPath,
|
||||
"STEAM_COMPAT_CLIENT_INSTALL_PATH": steamPath,
|
||||
"STEAM_COMPAT_APP_ID": BS_APP_ID,
|
||||
// Run game in steam environment; fixes #585 for unicode song titles
|
||||
"SteamEnv": "1",
|
||||
// Uncomment these to create a proton log file in the Beat Saber install directory.
|
||||
// "PROTON_LOG": 1,
|
||||
// "PROTON_LOG_DIR": bsFolderPath,
|
||||
|
||||
@@ -6,14 +6,13 @@ import { BS_APP_ID, OCULUS_BS_BACKUP_DIR, OCULUS_BS_DIR } from "../constants";
|
||||
import path from "path";
|
||||
import { ConfigurationService } from "./configuration.service";
|
||||
import { lstat, rename } from "fs/promises";
|
||||
import { BsmException } from "shared/models/bsm-exception.model";
|
||||
import log from "electron-log";
|
||||
import { OculusService } from "./oculus.service";
|
||||
import { DownloadLinkType } from "shared/models/mods";
|
||||
import sanitize from "sanitize-filename";
|
||||
import { Progression, copyDirectoryWithJunctions, deleteFolder, ensurePathNotAlreadyExist, getFoldersInFolder, rxCopy } from "../helpers/fs.helpers";
|
||||
import { FolderLinkerService } from "./folder-linker.service";
|
||||
import { ReadStream, createReadStream, pathExists, readFile, writeFile } from "fs-extra";
|
||||
import { ReadStream, createReadStream, pathExists, pathExistsSync, readFile, writeFile } from "fs-extra";
|
||||
import readline from "readline";
|
||||
import { Observable, Subject, catchError, finalize, from, map, switchMap, throwError } from "rxjs";
|
||||
import { BsStore } from "../../shared/models/bs-store.enum";
|
||||
@@ -306,54 +305,58 @@ export class BSLocalVersionService {
|
||||
.catch(() => { return false; })
|
||||
}
|
||||
|
||||
public async editVersion(version: BSVersion, name: string, color: string): Promise<BSVersion>{
|
||||
if(version.steam || version.oculus){ throw {title: "CantEditSteam", message: "CantEditSteam"} as BsmException; }
|
||||
const oldPath = await this.getVersionPath(version);
|
||||
const editedVersion: BSVersion = version.BSVersion === name
|
||||
? {...version, name: undefined, color}
|
||||
: {...version, name: sanitize(name), color};
|
||||
const newPath = await this.getVersionPath(editedVersion);
|
||||
public async editVersion(version: BSVersion, name: string, color: string): Promise<BSVersion>{
|
||||
if(version.steam || version.oculus){ throw new CustomError("Do not edit official Beat Saber versions", "CantEditSteam") }
|
||||
const oldPath = await this.getVersionPath(version);
|
||||
const editedVersion: BSVersion = version.BSVersion === name
|
||||
? {...version, name: undefined, color}
|
||||
: {...version, name: sanitize(name), color};
|
||||
const newPath = await this.getVersionPath(editedVersion);
|
||||
|
||||
if(oldPath === newPath){
|
||||
this.deleteCustomVersion(version);
|
||||
this.addCustomVersion(editedVersion);
|
||||
return editedVersion;
|
||||
}
|
||||
if(oldPath === newPath){
|
||||
this.deleteCustomVersion(version);
|
||||
this.addCustomVersion(editedVersion);
|
||||
return editedVersion;
|
||||
}
|
||||
|
||||
if((await pathExists(newPath)) && newPath === oldPath){ throw {title: "VersionAlreadExist"} as BsmException; }
|
||||
if(pathExistsSync(newPath)){
|
||||
throw new CustomError("Unable to edit the version, path already exist", "VersionAlreadExist");
|
||||
}
|
||||
|
||||
return rename(oldPath, newPath).then(() => {
|
||||
this.deleteCustomVersion(version);
|
||||
this.addCustomVersion(editedVersion);
|
||||
return editedVersion;
|
||||
}).catch((err: Error) => {
|
||||
log.error("edit version error", err, version, name, color);
|
||||
throw {title: "CantRename", ...err} as BsmException;
|
||||
});
|
||||
}
|
||||
return rename(oldPath, newPath).then(() => {
|
||||
this.deleteCustomVersion(version);
|
||||
this.addCustomVersion(editedVersion);
|
||||
return editedVersion;
|
||||
}).catch((err: Error) => {
|
||||
log.error("edit version error", err, version, name, color);
|
||||
throw CustomError.fromError(err, "CantRename");
|
||||
});
|
||||
}
|
||||
|
||||
public async cloneVersion(version: BSVersion, name: string, color: string): Promise<BSVersion>{
|
||||
const originPath = await this.getVersionPath(version);
|
||||
const cloneVersion: BSVersion = version.BSVersion === name
|
||||
? {...version, name: undefined, color, steam: false, oculus: false}
|
||||
: {...version, name: sanitize(name), color, steam: false, oculus: false};
|
||||
const newPath = await this.getVersionPath(cloneVersion);
|
||||
public async cloneVersion(version: BSVersion, name: string, color: string): Promise<BSVersion>{
|
||||
const originPath = await this.getVersionPath(version);
|
||||
const cloneVersion: BSVersion = version.BSVersion === name
|
||||
? {...version, name: undefined, color, steam: false, oculus: false}
|
||||
: {...version, name: sanitize(name), color, steam: false, oculus: false};
|
||||
const newPath = await this.getVersionPath(cloneVersion);
|
||||
|
||||
if(originPath === newPath){
|
||||
this.deleteCustomVersion(version);
|
||||
this.addCustomVersion(cloneVersion);
|
||||
}
|
||||
if(pathExistsSync(newPath)){
|
||||
throw new CustomError("Unable to clone the version, path already exist", "VersionAlreadExist");
|
||||
}
|
||||
|
||||
if(await pathExists(newPath)){ throw {title: "VersionAlreadExist"} as BsmException; }
|
||||
if(originPath === newPath){
|
||||
this.deleteCustomVersion(version);
|
||||
this.addCustomVersion(cloneVersion);
|
||||
}
|
||||
|
||||
return copyDirectoryWithJunctions(originPath, newPath).then(() => {
|
||||
this.addCustomVersion(cloneVersion);
|
||||
return cloneVersion;
|
||||
}).catch((err: Error) => {
|
||||
log.error("clone version error", err, version, name, color);
|
||||
throw {title: "CantClone", ...err} as BsmException
|
||||
})
|
||||
}
|
||||
return copyDirectoryWithJunctions(originPath, newPath).then(() => {
|
||||
this.addCustomVersion(cloneVersion);
|
||||
return cloneVersion;
|
||||
}).catch((err: Error) => {
|
||||
log.error("Error occured while cloning the version", err, version, name, color);
|
||||
throw CustomError.fromError(err, "CantClone");
|
||||
})
|
||||
}
|
||||
|
||||
public importVersion(opt: ImportVersionOptions): Observable<Progression<BSVersion>>{
|
||||
const { fromPath, store } = opt;
|
||||
|
||||
@@ -12,6 +12,7 @@ import { DepotDownloaderArgsOptions, DepotDownloaderErrorEvent, DepotDownloaderE
|
||||
import { DepotDownloader } from "../../models/depot-downloader.class";
|
||||
import { app } from "electron";
|
||||
import { BsStore } from "../../../shared/models/bs-store.enum";
|
||||
import { CustomError } from "shared/models/exceptions/custom-error.class";
|
||||
|
||||
export class BsSteamDownloaderService {
|
||||
private static instance: BsSteamDownloaderService;
|
||||
@@ -96,7 +97,7 @@ export class BsSteamDownloaderService {
|
||||
depotDownloader.$events().pipe(
|
||||
map(event => {
|
||||
if(event.type === DepotDownloaderEventType.Error){
|
||||
throw event;
|
||||
throw new CustomError(JSON.stringify(event.data) ?? "An error occur in the DepotDownloader process", event?.subType ?? DepotDownloaderErrorEvent.Unknown, event)
|
||||
}
|
||||
return event;
|
||||
}),
|
||||
|
||||
@@ -49,7 +49,7 @@ export class IpcService {
|
||||
const sub = observable.subscribe({
|
||||
next: data => this.send(channel, window, data),
|
||||
error: error => {
|
||||
log.error(error, error?.code);
|
||||
log.error(error, error?.code, error?.data);
|
||||
this.send(this.getErrorChannel(channel), window, serializeError(error));
|
||||
},
|
||||
complete: () => this.send(this.getCompleteChannel(channel), window)
|
||||
|
||||
@@ -1,39 +1,32 @@
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { DownloadLink, InstallModsResult, Mod, ModInstallProgression, UninstallModsResult } from "shared/models/mods";
|
||||
import { DownloadLink, Mod } from "shared/models/mods";
|
||||
import { BeatModsApiService } from "./beat-mods-api.service";
|
||||
import { BSLocalVersionService } from "../bs-local-version.service";
|
||||
import path from "path";
|
||||
import { UtilsService } from "../utils.service";
|
||||
import md5File from "md5-file";
|
||||
import { RequestService } from "../request.service";
|
||||
import { spawn } from "child_process";
|
||||
import { BS_EXECUTABLE } from "../../constants";
|
||||
import log from "electron-log";
|
||||
import { deleteFolder, pathExist, unlinkPath } from "../../helpers/fs.helpers";
|
||||
import { lastValueFrom } from "rxjs";
|
||||
import { deleteFolder, pathExist, Progression, unlinkPath } from "../../helpers/fs.helpers";
|
||||
import { lastValueFrom, Observable } from "rxjs";
|
||||
import JSZip from "jszip";
|
||||
import { extractZip } from "../../helpers/zip.helpers";
|
||||
import recursiveReadDir from "recursive-readdir";
|
||||
import { sToMs } from "../../../shared/helpers/time.helpers";
|
||||
import { ensureDir, pathExistsSync } from "fs-extra";
|
||||
import { CustomError } from "shared/models/exceptions/custom-error.class";
|
||||
import { popElement } from "shared/helpers/array.helpers";
|
||||
|
||||
export class BsModsManagerService {
|
||||
private static instance: BsModsManagerService;
|
||||
|
||||
private readonly beatModsApi: BeatModsApiService;
|
||||
private readonly bsLocalService: BSLocalVersionService;
|
||||
private readonly utilsService: UtilsService;
|
||||
private readonly requestService: RequestService;
|
||||
|
||||
private manifestMatches: Mod[];
|
||||
|
||||
private nbModsToInstall = 0;
|
||||
private nbInstalledMods = 0;
|
||||
|
||||
private nbModsToUninstall = 0;
|
||||
private nbUninstalledMods = 0;
|
||||
|
||||
public static getInstance(): BsModsManagerService {
|
||||
if (!BsModsManagerService.instance) {
|
||||
BsModsManagerService.instance = new BsModsManagerService();
|
||||
@@ -44,7 +37,6 @@ export class BsModsManagerService {
|
||||
private constructor() {
|
||||
this.beatModsApi = BeatModsApiService.getInstance();
|
||||
this.bsLocalService = BSLocalVersionService.getInstance();
|
||||
this.utilsService = UtilsService.getInstance();
|
||||
this.requestService = RequestService.getInstance();
|
||||
}
|
||||
|
||||
@@ -187,7 +179,6 @@ export class BsModsManagerService {
|
||||
|
||||
private async installMod(mod: Mod, version: BSVersion): Promise<boolean> {
|
||||
log.info("INSTALL MOD", mod.name, "for version", `${version.BSVersion} - ${version.name}`);
|
||||
this.utilsService.ipcSend<ModInstallProgression>("mod-installed", { success: true, data: { name: mod.name, progression: ((this.nbInstalledMods + 1) / this.nbModsToInstall) * 100 } });
|
||||
|
||||
const download = this.getModDownload(mod, version);
|
||||
|
||||
@@ -246,10 +237,6 @@ export class BsModsManagerService {
|
||||
}))
|
||||
: extracted;
|
||||
|
||||
if(res){
|
||||
this.nbInstalledMods++;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -275,9 +262,6 @@ export class BsModsManagerService {
|
||||
}
|
||||
|
||||
private async uninstallMod(mod: Mod, version: BSVersion): Promise<void> {
|
||||
this.nbUninstalledMods++;
|
||||
this.utilsService.ipcSend<ModInstallProgression>("mod-uninstalled", { success: true, data: { name: mod.name, progression: (this.nbUninstalledMods / this.nbModsToUninstall) * 100 } });
|
||||
|
||||
if (mod.name.toLowerCase() === "bsipa") {
|
||||
return this.uninstallBSIPA(mod, version);
|
||||
}
|
||||
@@ -322,81 +306,92 @@ export class BsModsManagerService {
|
||||
return Array.from(modsDict.values());
|
||||
}
|
||||
|
||||
public async installMods(mods: Mod[], version: BSVersion): Promise<InstallModsResult> {
|
||||
if (!mods?.length) {
|
||||
throw CustomError.fromError(new Error("No mods to install"), "no-mods");
|
||||
}
|
||||
public installMods(mods: Mod[], version: BSVersion): Observable<Progression> {
|
||||
const progress = { current: 0, total: mods.length };
|
||||
|
||||
const bsipa = mods.find(mod => mod.name.toLowerCase() === "bsipa");
|
||||
if (bsipa) {
|
||||
mods = mods.filter(mod => mod.name.toLowerCase() !== "bsipa");
|
||||
}
|
||||
return new Observable<Progression>(obs => {
|
||||
(async () => {
|
||||
if (!mods?.length) {
|
||||
throw CustomError.throw(new Error("No mods to install"), "no-mods", mods);
|
||||
}
|
||||
|
||||
this.nbModsToInstall = mods.length + (bsipa ? 1 : 0);
|
||||
this.nbInstalledMods = 0;
|
||||
obs.next(progress);
|
||||
|
||||
if (bsipa) {
|
||||
const installed = await this.installMod(bsipa, version).catch(err => {
|
||||
log.error("INSTALL BSIPA", err);
|
||||
return false;
|
||||
});
|
||||
if (!installed) {
|
||||
throw CustomError.fromError(new Error("Unable to install BSIPA"), "cannot-install-bsipa");
|
||||
}
|
||||
}
|
||||
const bsipa = popElement(mod => mod.name.toLowerCase() === "bsipa", mods);
|
||||
|
||||
for (const mod of mods) {
|
||||
await this.installMod(mod, version);
|
||||
}
|
||||
if(bsipa){
|
||||
const bsipaInstalled = await this.installMod(bsipa, version).catch(err => {
|
||||
log.error("Error while installing BSIPA", err);
|
||||
});
|
||||
|
||||
return {
|
||||
nbModsToInstall: this.nbModsToInstall,
|
||||
nbInstalledMods: this.nbInstalledMods,
|
||||
};
|
||||
if(!bsipaInstalled){
|
||||
throw CustomError.throw(new Error("BSIPA failed to install"), "cannot-install-bsipa");
|
||||
}
|
||||
|
||||
progress.current++;
|
||||
obs.next(progress);
|
||||
}
|
||||
|
||||
for (const mod of mods) {
|
||||
await this.installMod(mod, version);
|
||||
progress.current++;
|
||||
obs.next(progress);
|
||||
}
|
||||
})()
|
||||
.catch(err => obs.error(err))
|
||||
.finally(() => obs.complete());
|
||||
});
|
||||
}
|
||||
|
||||
public async uninstallMods(mods: Mod[], version: BSVersion): Promise<UninstallModsResult> {
|
||||
if (!mods?.length) {
|
||||
throw CustomError.fromError(new Error("No mods to uninstall"), "no-mods");
|
||||
}
|
||||
public uninstallMods(mods: Mod[], version: BSVersion): Observable<Progression> {
|
||||
const progress = { current: 0, total: mods.length };
|
||||
|
||||
this.nbModsToUninstall = mods.length;
|
||||
this.nbUninstalledMods = 0;
|
||||
return new Observable<Progression>(obs => {
|
||||
(async () => {
|
||||
if (!mods?.length) {
|
||||
throw CustomError.throw(new Error("No mods to uninstall"), "no-mods", mods);
|
||||
}
|
||||
|
||||
for (const mod of mods) {
|
||||
await this.uninstallMod(mod, version);
|
||||
}
|
||||
obs.next(progress);
|
||||
|
||||
return {
|
||||
nbModsToUninstall: this.nbModsToUninstall,
|
||||
nbUninstalledMods: this.nbUninstalledMods,
|
||||
};
|
||||
for (const mod of mods) {
|
||||
await this.uninstallMod(mod, version);
|
||||
progress.current++;
|
||||
obs.next(progress);
|
||||
}
|
||||
})()
|
||||
.catch(err => obs.error(err))
|
||||
.finally(() => obs.complete());
|
||||
});
|
||||
}
|
||||
|
||||
public async uninstallAllMods(version: BSVersion): Promise<UninstallModsResult> {
|
||||
const mods = await this.getInstalledMods(version);
|
||||
public uninstallAllMods(version: BSVersion): Observable<Progression> {
|
||||
return new Observable<Progression>(obs => {
|
||||
(async () => {
|
||||
const mods = await this.getInstalledMods(version).catch(err => {
|
||||
log.error(err);
|
||||
return [];
|
||||
});
|
||||
|
||||
if (!mods?.length) {
|
||||
throw CustomError.fromError(new Error("This version has to mods to uninstall"), "no-mods");
|
||||
}
|
||||
const progress = { current: 0, total: mods.length };
|
||||
|
||||
this.nbModsToUninstall = mods.length;
|
||||
this.nbUninstalledMods = 0;
|
||||
obs.next(progress);
|
||||
|
||||
for (const mod of mods) {
|
||||
await this.uninstallMod(mod, version);
|
||||
}
|
||||
for (const mod of mods) {
|
||||
await this.uninstallMod(mod, version);
|
||||
progress.current++;
|
||||
obs.next(progress);
|
||||
}
|
||||
|
||||
const versionPath = await this.bsLocalService.getVersionPath(version);
|
||||
const versionPath = await this.bsLocalService.getVersionPath(version);
|
||||
|
||||
await deleteFolder(path.join(versionPath, ModsInstallFolder.PLUGINS));
|
||||
await deleteFolder(path.join(versionPath, ModsInstallFolder.LIBS));
|
||||
await deleteFolder(path.join(versionPath, ModsInstallFolder.IPA));
|
||||
|
||||
return {
|
||||
nbModsToUninstall: this.nbModsToUninstall,
|
||||
nbUninstalledMods: this.nbUninstalledMods,
|
||||
};
|
||||
await deleteFolder(path.join(versionPath, ModsInstallFolder.PLUGINS));
|
||||
await deleteFolder(path.join(versionPath, ModsInstallFolder.LIBS));
|
||||
await deleteFolder(path.join(versionPath, ModsInstallFolder.IPA));
|
||||
})()
|
||||
.catch(err => obs.error(err))
|
||||
.finally(() => obs.complete());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,9 @@ export class BeatSaverApiService {
|
||||
|
||||
const params: Record<string, string> = this.objectToStringRecord({...filter, tags});
|
||||
|
||||
delete params.enabledTags;
|
||||
delete params.excludedTags;
|
||||
|
||||
return new URLSearchParams(params);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import path from "path";
|
||||
import { app, BrowserWindow } from "electron";
|
||||
import { IpcResponse } from "shared/models/ipc";
|
||||
import log from "electron-log";
|
||||
import { app } from "electron";
|
||||
|
||||
// TODO : REFACTOR
|
||||
|
||||
export class UtilsService {
|
||||
private static instance: UtilsService;
|
||||
|
||||
private assetsPath: string = app.isPackaged ? path.join(process.resourcesPath, "assets") : path.join(__dirname, "../../assets");
|
||||
private assetsPath: string = app.isPackaged
|
||||
? path.join(process.resourcesPath, "assets")
|
||||
: path.join(path.dirname(path.dirname(__dirname)), "assets");
|
||||
private readonly buildPath: string = app.isPackaged
|
||||
? path.join(process.resourcesPath, "build")
|
||||
: path.join(path.dirname(path.dirname(__dirname)), "build");
|
||||
|
||||
private constructor() {}
|
||||
|
||||
@@ -36,11 +39,7 @@ export class UtilsService {
|
||||
return path.join(app.getPath("temp"), app.getName());
|
||||
}
|
||||
|
||||
public ipcSend<T = unknown>(channel: string, response: IpcResponse<T>): void {
|
||||
try {
|
||||
BrowserWindow.getAllWindows().forEach(window => window?.webContents?.send(channel, response));
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
public getBuildPath(filepath: string): string {
|
||||
return path.join(this.buildPath, filepath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,9 @@ export class WindowManagerService {
|
||||
|
||||
private readonly baseWindowOption: BrowserWindowConstructorOptions = {
|
||||
title: APP_NAME,
|
||||
icon: this.utilsService.getAssetsPath("favicon.ico"),
|
||||
icon: process.platform === "linux"
|
||||
? this.utilsService.getBuildPath(path.join("icons", "png", "256x256.png"))
|
||||
: this.utilsService.getBuildPath(path.join("icons", "win", "favicon.ico")),
|
||||
show: false,
|
||||
frame: false,
|
||||
titleBarOverlay: false,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createContext, useRef, useState } from "react";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { LocalMapsListPanel } from "./maps/local-maps-list-panel.component";
|
||||
import { LocalMapsListPanel, LocalMapsListPanelRef } from "./maps/local-maps-list-panel.component";
|
||||
import { BsmDropdownButton, DropDownItem } from "../shared/bsm-dropdown-button.component";
|
||||
import { FilterPanel } from "./maps/filter-panel.component";
|
||||
import { MapFilter } from "shared/models/maps/beat-saver.model";
|
||||
@@ -10,11 +10,10 @@ import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { FolderLinkState } from "renderer/services/version-folder-linker.service";
|
||||
import { useService } from "renderer/hooks/use-service.hook";
|
||||
import { BsContentTabPanel } from "../shared/bs-content-tab-panel/bs-content-tab-panel.component";
|
||||
import { BsmButton } from "../shared/bsm-button.component";
|
||||
import { MapIcon } from "../svgs/icons/map-icon.component";
|
||||
import { PlaylistIcon } from "../svgs/icons/playlist-icon.component";
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
import { BehaviorSubject, lastValueFrom, of } from "rxjs";
|
||||
import { LocalPlaylistsListPanel, LocalPlaylistsListRef } from "./playlists/local-playlists-list-panel.component";
|
||||
import { PlaylistsManagerService } from "renderer/services/playlists-manager.service";
|
||||
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
|
||||
@@ -23,6 +22,8 @@ import { LocalBPListsDetails } from "shared/models/playlists/local-playlist.mode
|
||||
import { PlaylistDownloaderService } from "renderer/services/playlist-downloader.service";
|
||||
import { LocalPlaylistFilter, LocalPlaylistFilterPanel } from "./playlists/local-playlist-filter-panel.component";
|
||||
import { noop } from "shared/helpers/function.helpers";
|
||||
import { Dropzone } from "../shared/dropzone.component";
|
||||
import { logRenderError } from "renderer";
|
||||
|
||||
type Props = {
|
||||
readonly version?: BSVersion;
|
||||
@@ -46,6 +47,9 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
|
||||
const t = useTranslation();
|
||||
const [tabIndex, setTabIndex] = useState(0);
|
||||
|
||||
const [mapsDropZoneOpen, setMapsDropZoneOpen] = useState(false);
|
||||
const [playlistsDropZoneOpen, setPlaylistsDropZoneOpen] = useState(false);
|
||||
|
||||
const maps$ = useConstant(() => new BehaviorSubject<BsmLocalMap[]>(undefined));
|
||||
const playlists$ = useConstant(() => new BehaviorSubject<LocalBPListsDetails[]>(undefined));
|
||||
|
||||
@@ -56,7 +60,7 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
|
||||
setPlaylists: playlists$.next.bind(playlists$),
|
||||
}));
|
||||
|
||||
const mapsRef = useRef<any>();
|
||||
const mapsRef = useRef<LocalMapsListPanelRef>();
|
||||
const playlistsRef = useRef<LocalPlaylistsListRef>();
|
||||
|
||||
const [mapFilter, setMapFilter] = useState<MapFilter>({});
|
||||
@@ -73,11 +77,16 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
|
||||
return playlistsManager.$playlistsFolderLinkState(version);
|
||||
}, FolderLinkState.Unlinked, [version]);
|
||||
|
||||
const handleAddClick = () => {
|
||||
const handleBrowse = () => {
|
||||
switch (tabIndex) {
|
||||
case 0: return mapsDownloader.openDownloadMapModal(version, maps$.value);
|
||||
case 1: return playlistsDownloader.openDownloadPlaylistModal(version, playlists$, maps$);
|
||||
default: return noop();
|
||||
case 0:
|
||||
mapsDownloader.openDownloadMapModal(version, maps$.value);
|
||||
return;
|
||||
case 1:
|
||||
playlistsDownloader.openDownloadPlaylistModal(version, playlists$, maps$);
|
||||
return;
|
||||
default:
|
||||
return noop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,16 +110,41 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
|
||||
return playlistsManager.unlinkVersion(version);
|
||||
}
|
||||
|
||||
const importMaps = async (paths: string[]) => {
|
||||
setMapsDropZoneOpen(() => false);
|
||||
return lastValueFrom(mapsManager.importMaps(paths, version)).catch(logRenderError);
|
||||
}
|
||||
|
||||
const importPlaylists = async (paths: string[]) => {
|
||||
setPlaylistsDropZoneOpen(() => false);
|
||||
return lastValueFrom(playlistsManager.importPlaylists({ version, paths })).catch(logRenderError);
|
||||
}
|
||||
|
||||
const addDropDownItems = ((): DropDownItem[] => {
|
||||
if(tabIndex === 0){
|
||||
return [
|
||||
{ icon: "browse", text: "pages.version-viewer.maps.tabs.maps.actions.drop-down.browse-maps", onClick: handleBrowse },
|
||||
{ icon: "download", text: "pages.version-viewer.maps.tabs.maps.actions.drop-down.import-maps", onClick: () => setMapsDropZoneOpen(true) }
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
{ icon: "browse", text: "pages.version-viewer.maps.tabs.playlists.drop-down.browse-playlists", onClick: handleBrowse },
|
||||
{ icon: "add-file", text: "pages.version-viewer.maps.tabs.playlists.drop-down.create-a-playlist", onClick: () => playlistsRef?.current?.createPlaylist?.() },
|
||||
{ icon: "download", text: "pages.version-viewer.maps.tabs.playlists.drop-down.import-playlists", onClick: () => setPlaylistsDropZoneOpen(true) }
|
||||
];
|
||||
})();
|
||||
|
||||
const dropDownItems = ((): DropDownItem[] => {
|
||||
if (tabIndex === 0) {
|
||||
return [
|
||||
{ icon: "export", text: "pages.version-viewer.maps.search-bar.dropdown.export-maps", onClick: () => mapsRef.current.exportMaps?.() },
|
||||
{ icon: "trash", text: "pages.version-viewer.maps.search-bar.dropdown.delete-maps", onClick: () => mapsRef.current.deleteMaps?.() },
|
||||
{ icon: "clean", text: "pages.version-viewer.maps.search-bar.dropdown.delete-duplicate-maps", onClick: () => mapsRef.current.removeDuplicates?.() },
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
{ icon: "add", text: t("playlist.create-a-playlist"), onClick: () => playlistsRef?.current?.createPlaylist?.() },
|
||||
{ icon: "sync", text: t("playlist.synchronize-playlists"), onClick: () => playlistsRef?.current?.syncPlaylists?.() },
|
||||
{ icon: "export", text: t("playlist.export-playlists"), onClick: () => playlistsRef?.current?.exportPlaylists?.() },
|
||||
{ icon: "trash", text: t("playlist.delete-playlists"), onClick: () => playlistsRef?.current?.deletePlaylists?.() },
|
||||
@@ -118,15 +152,22 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
|
||||
})();
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col items-center justify-center gap-4">
|
||||
<nav className="w-full shrink-0 flex h-9 justify-center px-40 gap-2 text-main-color-1 dark:text-white">
|
||||
<BsmButton
|
||||
className="flex items-center justify-center w-fit rounded-full px-2 py-1 font-bold whitespace-nowrap"
|
||||
<>
|
||||
<nav className="w-full shrink-0 flex h-9 justify-center px-40 gap-2 text-main-color-1 dark:text-white mb-4">
|
||||
<BsmDropdownButton
|
||||
classNames={{
|
||||
mainContainer: "h-full relative z-[1] flex justify-center w-fit",
|
||||
button: "flex items-center justify-center h-full rounded-full px-2 py-1 font-bold whitespace-nowrap shadow-none",
|
||||
itemsContainer: "bg-theme-3",
|
||||
}}
|
||||
textClassName="whitespace-nowrap"
|
||||
icon="add"
|
||||
buttonColor="primary"
|
||||
text="misc.add"
|
||||
typeColor="primary"
|
||||
align="center"
|
||||
menuTranslationY="6px"
|
||||
withBar={false}
|
||||
onClick={handleAddClick}
|
||||
items={addDropDownItems}
|
||||
/>
|
||||
<div className="h-full rounded-full bg-light-main-color-2 dark:bg-main-color-2 grow p-[6px]">
|
||||
<input
|
||||
@@ -174,10 +215,39 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
|
||||
]}
|
||||
>
|
||||
<InstalledMapsContext.Provider value={mapsContextValue}>
|
||||
<LocalMapsListPanel className="w-full h-full shrink-0" isActive={isActive && tabIndex === 0} ref={mapsRef} version={version} filter={mapFilter} search={search} linkedState={mapsLinkedState} />
|
||||
<LocalPlaylistsListPanel className="w-full h-full shrink-0" isActive={isActive && tabIndex === 1} ref={playlistsRef} version={version} linkedState={playlistLinkedState} filter={playlistFilter} search={search}/>
|
||||
|
||||
<Dropzone
|
||||
className="w-full h-full shrink-0"
|
||||
onFiles={importMaps}
|
||||
text={t("pages.version-viewer.maps.tabs.maps.drop-zone.text")}
|
||||
subtext={t("pages.version-viewer.maps.tabs.maps.drop-zone.subtext")}
|
||||
open={mapsDropZoneOpen}
|
||||
onClose={mapsDropZoneOpen ? () => setMapsDropZoneOpen(() => false) : undefined}
|
||||
filters={[{ name: ".zip", extensions: ["zip"] }]}
|
||||
dialogOptions={{ dialog: {
|
||||
properties: ["openFile", "multiSelections"],
|
||||
}}}
|
||||
>
|
||||
<LocalMapsListPanel className="w-full h-full shrink-0" isActive={isActive && tabIndex === 0} ref={mapsRef} version={version} filter={mapFilter} search={search} linkedState={mapsLinkedState} />
|
||||
</Dropzone>
|
||||
|
||||
<Dropzone
|
||||
className="w-full h-full shrink-0"
|
||||
onFiles={importPlaylists}
|
||||
text={t("pages.version-viewer.maps.tabs.playlists.drop-zone.text")}
|
||||
subtext={t("pages.version-viewer.maps.tabs.playlists.drop-zone.subtext")}
|
||||
open={playlistsDropZoneOpen}
|
||||
onClose={playlistsDropZoneOpen ? () => setPlaylistsDropZoneOpen(() => false) : undefined}
|
||||
filters={[{ name: ".bplist, .json", extensions: ["bplist", "json"] }]}
|
||||
dialogOptions={{ dialog: {
|
||||
properties: ["openFile", "multiSelections"],
|
||||
}}}
|
||||
>
|
||||
<LocalPlaylistsListPanel className="w-full h-full shrink-0" isActive={isActive && tabIndex === 1} ref={playlistsRef} version={version} linkedState={playlistLinkedState} filter={playlistFilter} search={search}/>
|
||||
</Dropzone>
|
||||
|
||||
</InstalledMapsContext.Provider>
|
||||
</BsContentTabPanel>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { GlowEffect } from "../../shared/glow-effect.component";
|
||||
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
|
||||
import { SongDetails } from "shared/models/maps";
|
||||
import formatDuration from "format-duration";
|
||||
import { MapInfo } from "shared/models/maps/info/map-info.model";
|
||||
|
||||
export type Props = {
|
||||
className?: string;
|
||||
@@ -302,9 +303,10 @@ function isFitVerified(filter: MapFilter, verified: boolean): boolean {
|
||||
return verified;
|
||||
}
|
||||
|
||||
function isFitSearch(search: string, {songName, songAuthorName, levelAuthorName}: {songName: string, songAuthorName: string, levelAuthorName: string}): boolean {
|
||||
function isFitSearch(search: string, {songName, songAuthorName, levelMappers}: {songName: MapInfo["songName"], songAuthorName: MapInfo["songAuthorName"], levelMappers: MapInfo["levelMappers"]}): boolean {
|
||||
if (!search) { return true; }
|
||||
return songName?.toLowerCase().includes(search.toLowerCase()) || songAuthorName?.toLowerCase().includes(search.toLowerCase()) || levelAuthorName?.toLowerCase().includes(search.toLowerCase());
|
||||
if(levelMappers?.some(mapper => mapper?.toLowerCase().includes(search.toLowerCase()))) { return true; }
|
||||
return songName?.toLowerCase().includes(search.toLowerCase()) || songAuthorName?.toLowerCase().includes(search.toLowerCase());
|
||||
}
|
||||
|
||||
export const isLocalMapFitMapFilter = ({filter, map, search}: { filter: MapFilter, map: BsmLocalMap, search: string }): boolean => {
|
||||
@@ -323,7 +325,7 @@ export const isLocalMapFitMapFilter = ({filter, map, search}: { filter: MapFilte
|
||||
if (!isFitRanked(filter, map.songDetails?.ranked || map.songDetails?.blRanked)) { return false; }
|
||||
if (!isFitCurated(filter, map.songDetails?.curated)) { return false; }
|
||||
if (!isFitVerified(filter, map.songDetails?.uploader.verified)) { return false; }
|
||||
if (!isFitSearch(search, {songName: map.rawInfo?._songName, songAuthorName: map.rawInfo?._songAuthorName, levelAuthorName: map.rawInfo?._levelAuthorName})) { return false; }
|
||||
if (!isFitSearch(search, {songName: map.mapInfo?.songName, songAuthorName: map.mapInfo?.songAuthorName, levelMappers: map.mapInfo?.levelMappers})) { return false; }
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -344,7 +346,7 @@ export const isBsvMapFitMapFilter = ({filter, map, search}: { filter: MapFilter,
|
||||
if (!isFitRanked(filter, map.ranked || map.blRanked)) { return false; }
|
||||
if (!isFitCurated(filter, !!map.curator)) { return false; }
|
||||
if (!isFitVerified(filter, !!map.curatedAt)) { return false; }
|
||||
if (!isFitSearch(search, {songName: map.name, songAuthorName: map.metadata.songAuthorName, levelAuthorName: map.metadata.levelAuthorName})) { return false; }
|
||||
if (!isFitSearch(search, {songName: map.name, songAuthorName: map.metadata.songAuthorName, levelMappers: [map.metadata.levelAuthorName]})) { return false; }
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -364,12 +366,12 @@ export const isSongDetailsFitMapFilter = ({filter, map, search}: { filter: MapFi
|
||||
if (!isFitRanked(filter, map.ranked || map.blRanked)) { return false; }
|
||||
if (!isFitCurated(filter, map.curated)) { return false; }
|
||||
if (!isFitVerified(filter, map.uploader.verified)) { return false; }
|
||||
if (!isFitSearch(search, {songName: map.name, songAuthorName: map.uploader.name, levelAuthorName: map.uploader.name})) { return false; }
|
||||
if (!isFitSearch(search, {songName: map.name, songAuthorName: map.uploader.name, levelMappers: [map.uploader.name]})) { return false; }
|
||||
return true;
|
||||
}
|
||||
|
||||
export const isMapFitFilter = ({filter, map, search}: { filter: MapFilter, map: BsmLocalMap | BsvMapDetail | SongDetails, search: string }): boolean => {
|
||||
if ((map as BsmLocalMap)?.rawInfo) { return isLocalMapFitMapFilter({filter, map: (map as BsmLocalMap), search}); }
|
||||
if ((map as BsmLocalMap)?.mapInfo) { return isLocalMapFitMapFilter({filter, map: (map as BsmLocalMap), search}); }
|
||||
if ((map as BsvMapDetail)?.metadata) { return isBsvMapFitMapFilter({filter, map: (map as BsvMapDetail), search}); }
|
||||
if ((map as SongDetails).hash) { return isSongDetailsFitMapFilter({filter, map: (map as SongDetails), search}); }
|
||||
return false;
|
||||
|
||||
+34
-14
@@ -2,10 +2,10 @@ import { MapsManagerService } from "renderer/services/maps-manager.service";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useState } from "react";
|
||||
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
|
||||
import { Subscription, BehaviorSubject } from "rxjs";
|
||||
import { Subscription, BehaviorSubject, lastValueFrom } from "rxjs";
|
||||
import { MapFilter } from "shared/models/maps/beat-saver.model";
|
||||
import { MapsDownloaderService } from "renderer/services/maps-downloader.service";
|
||||
import { last, tap } from "rxjs/operators";
|
||||
import { bufferTime, last, tap } from "rxjs/operators";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import BeatConflict from "../../../../../assets/images/apngs/beat-conflict.png";
|
||||
import { BsmImage } from "../../shared/bsm-image.component";
|
||||
@@ -34,7 +34,13 @@ type Props = {
|
||||
isActive?: boolean;
|
||||
};
|
||||
|
||||
export const LocalMapsListPanel = forwardRef<unknown, Props>(({ version, className, filter, search, linkedState, isActive }, forwardRef) => {
|
||||
export type LocalMapsListPanelRef = {
|
||||
deleteMaps: () => void;
|
||||
exportMaps: () => void;
|
||||
removeDuplicates: () => void;
|
||||
}
|
||||
|
||||
export const LocalMapsListPanel = forwardRef<LocalMapsListPanelRef, Props>(({ version, className, filter, search, linkedState, isActive }, forwardRef) => {
|
||||
const mapsManager = useService(MapsManagerService);
|
||||
const mapsDownloader = useService(MapsDownloaderService);
|
||||
|
||||
@@ -63,6 +69,12 @@ export const LocalMapsListPanel = forwardRef<unknown, Props>(({ version, classNa
|
||||
},
|
||||
exportMaps() {
|
||||
mapsManager.exportMaps(version, selectedMaps);
|
||||
},
|
||||
removeDuplicates() {
|
||||
return lastValueFrom(mapsManager.deleteDuplicateMaps(maps$.value)).then(res => {
|
||||
if(!res?.current){ return; }
|
||||
loadMaps();
|
||||
}).catch(noop);
|
||||
}
|
||||
}),[selectedMaps, maps, version]);
|
||||
|
||||
@@ -87,25 +99,32 @@ export const LocalMapsListPanel = forwardRef<unknown, Props>(({ version, classNa
|
||||
setMaps(null);
|
||||
loadPercent$.next(0);
|
||||
subs.forEach(s => s.unsubscribe());
|
||||
mapsDownloader.removeOnMapDownloadedListener(loadMaps);
|
||||
};
|
||||
}, [isActiveOnce, version, linked]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const subs: Subscription[] = [];
|
||||
|
||||
if(isActiveOnce){
|
||||
mapsDownloader.addOnMapDownloadedListener((map, targetVersion) => {
|
||||
subs.push(mapsDownloader.lastDownloadedMap$.subscribe({ next: ({map, version: targetVersion}) => {
|
||||
if (!equal(targetVersion, version)) {
|
||||
return;
|
||||
}
|
||||
setMaps((maps ? [map, ...maps] : [map]));
|
||||
});
|
||||
setMaps((maps$.value ? [map, ...maps$.value] : [map]));
|
||||
}}));
|
||||
|
||||
subs.push(mapsManager.$onMapImported(version).pipe(bufferTime(500)).subscribe({ next: newMaps => {
|
||||
const maps = (maps$.value ?? []).filter(map => !newMaps.some(newMap => newMap.path === map.path));
|
||||
setMaps([...newMaps, ...maps]);
|
||||
}}));
|
||||
}
|
||||
|
||||
return () => {
|
||||
mapsDownloader.removeOnMapDownloadedListener(loadMaps);
|
||||
subs.forEach(s => s.unsubscribe());
|
||||
}
|
||||
|
||||
}, [isActiveOnce, version, maps])
|
||||
}, [isActiveOnce, version])
|
||||
|
||||
const loadMaps = () => {
|
||||
setMaps(null);
|
||||
@@ -162,17 +181,18 @@ export const LocalMapsListPanel = forwardRef<unknown, Props>(({ version, classNa
|
||||
<MapItem
|
||||
key={map.path}
|
||||
hash={map.hash}
|
||||
title={map.rawInfo._songName}
|
||||
title={map.mapInfo.songName}
|
||||
coverUrl={map.coverUrl}
|
||||
songUrl={map.songUrl}
|
||||
autor={map.rawInfo._levelAuthorName}
|
||||
songAutor={map.rawInfo._songAuthorName}
|
||||
bpm={map.rawInfo._beatsPerMinute}
|
||||
autor={map.mapInfo.levelMappers.at(0)}
|
||||
songAutor={map.mapInfo.songAuthorName}
|
||||
bpm={map.mapInfo.beatsPerMinute}
|
||||
duration={map.songDetails?.duration}
|
||||
selected={renderableMap.selected}
|
||||
diffs={MapItemComponentPropsMapper.extractMapDiffs({ rawMapInfo: map.rawInfo, songDetails: map.songDetails })}
|
||||
diffs={MapItemComponentPropsMapper.extractMapDiffs({ mapInfo: map.mapInfo, songDetails: map.songDetails })}
|
||||
mapId={map.songDetails?.id}
|
||||
ranked={map.songDetails?.ranked}
|
||||
blRanked={map.songDetails?.blRanked}
|
||||
autorId={map.songDetails?.uploader.id}
|
||||
likes={map.songDetails?.upVotes}
|
||||
createdAt={map.songDetails?.uploadedAt}
|
||||
|
||||
+23
-11
@@ -6,7 +6,7 @@ import { useOnUpdate } from "renderer/hooks/use-on-update.hook";
|
||||
import { useService } from "renderer/hooks/use-service.hook";
|
||||
import { PlaylistsManagerService } from "renderer/services/playlists-manager.service";
|
||||
import { FolderLinkState } from "renderer/services/version-folder-linker.service";
|
||||
import { BehaviorSubject, Observable, combineLatest, distinctUntilChanged, filter, finalize, lastValueFrom, map, tap } from "rxjs";
|
||||
import { BehaviorSubject, Observable, Subscription, bufferTime, combineLatest, distinctUntilChanged, filter, finalize, lastValueFrom, map, tap } from "rxjs";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { noop } from "shared/helpers/function.helpers";
|
||||
import { LocalBPList, LocalBPListsDetails } from "shared/models/playlists/local-playlist.models";
|
||||
@@ -52,7 +52,7 @@ type Props = {
|
||||
|
||||
export type LocalPlaylistsListRef = {
|
||||
createPlaylist: () => Promise<void>;
|
||||
syncPlaylists: () => Promise<void>;
|
||||
syncPlaylists: (playlists?: LocalBPList[]) => Promise<void>;
|
||||
deletePlaylists: () => Promise<void>;
|
||||
exportPlaylists: () => Promise<void>;
|
||||
}
|
||||
@@ -111,9 +111,9 @@ export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(
|
||||
}
|
||||
|
||||
},
|
||||
syncPlaylists: async () => {
|
||||
syncPlaylists: async (playlists?: LocalBPList[]) => {
|
||||
if(!isOnline){ return; }
|
||||
const toSync = selectedPlaylists$.value?.length ? selectedPlaylists$.value : playlists$.value;
|
||||
const toSync = playlists ?? (selectedPlaylists$.value?.length ? selectedPlaylists$.value : playlists$.value);
|
||||
if(!toSync.length){ return; }
|
||||
|
||||
const modalRes = await modals.openModal(SyncPlaylistModal, { data: toSync });
|
||||
@@ -151,7 +151,7 @@ export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(
|
||||
|
||||
const obs$ = playlistService.exportPlaylists({ version, bpLists: toExport, dest: folderRes.filePaths.at(0), playlistsMaps: mapsToExport });
|
||||
|
||||
progess.show(obs$, true);
|
||||
progess.show(obs$);
|
||||
|
||||
const { error } = await tryit(() => lastValueFrom(obs$));
|
||||
|
||||
@@ -163,7 +163,7 @@ export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(
|
||||
|
||||
notification.notifySuccess({ title: "playlist.playlists-exported-title", desc: exportMaps ? "playlist.playlists-exported-desc" : "playlist.playlists-with-maps-exported-desc", duration: 5000 });
|
||||
|
||||
progess.hide(true);
|
||||
progess.hide();
|
||||
},
|
||||
deletePlaylists: () => {
|
||||
const toDelete = selectedPlaylists$.value?.length ? selectedPlaylists$.value : playlists$.value;
|
||||
@@ -211,10 +211,22 @@ export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(
|
||||
setPlaylists(newPlaylist);
|
||||
}
|
||||
|
||||
const sub = playlistDownloader.currentDownload$.pipe(filter(download => download?.downloaded && equal(download?.info.version, version))).subscribe(download => onPlaylistDownloadedCB(download.downloaded));
|
||||
const subs: Subscription[] = [];
|
||||
|
||||
subs.push(playlistDownloader.currentDownload$.pipe(
|
||||
filter(download => download?.downloaded && equal(download?.info.version, version))
|
||||
).subscribe(download => onPlaylistDownloadedCB(download.downloaded)));
|
||||
|
||||
subs.push(playlistService.$onPlaylistImported(version).pipe(bufferTime(500)).subscribe({
|
||||
next: newPlaylists => {
|
||||
if(!newPlaylists.length){ return; }
|
||||
const playlists = (playlists$.value ?? []).filter(p => !newPlaylists.some(np => np.path === p.path));
|
||||
setPlaylists([...newPlaylists, ...playlists]);
|
||||
}
|
||||
}));
|
||||
|
||||
return () => {
|
||||
sub.unsubscribe();
|
||||
subs.forEach(s => s.unsubscribe());
|
||||
}
|
||||
|
||||
}, [isActiveOnce, version, linked]);
|
||||
@@ -247,7 +259,7 @@ export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(
|
||||
if(exitCode !== ModalExitCode.COMPLETED){ return; }
|
||||
|
||||
const progess$ = new BehaviorSubject<ProgressionInterface>({ progression: 0 });
|
||||
progess.show(progess$, true)
|
||||
progess.show(progess$)
|
||||
|
||||
for(const [i, bpList] of enumerate(bpLists)){
|
||||
|
||||
@@ -256,7 +268,7 @@ export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(
|
||||
if(error){
|
||||
logRenderError("Error occured while deleting playlist", error);
|
||||
notification.notifyError({ title: "playlist.playlist-delete-error-title", desc: "playlist.playlist-delete-error-desc" });
|
||||
progess.hide(true);
|
||||
progess.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -270,7 +282,7 @@ export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(
|
||||
|
||||
notification.notifySuccess({ title: "playlist.playlists-deleted-title", desc: "playlist.playlists-deleted-desc", duration: 5000 });
|
||||
|
||||
progess.hide(true);
|
||||
progess.hide();
|
||||
};
|
||||
|
||||
const openPlaylistDetails = (playlistPath: string) => {
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { ModalComponent, ModalExitCode, ModalService } from "renderer/services/modale.service";
|
||||
import { WhySteamCredentialsModal } from "./why-steam-credentials-modal.component";
|
||||
import { useService } from "renderer/hooks/use-service.hook";
|
||||
import { Observable } from "rxjs";
|
||||
import { catchError, Observable, of } from "rxjs";
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
import { QRCodeSVG } from "qrcode.react";
|
||||
import { BsmCheckbox } from "renderer/components/shared/bsm-checkbox.component";
|
||||
@@ -21,7 +21,7 @@ export const LoginToSteamModal: ModalComponent<
|
||||
const [password, setPassword] = useState("");
|
||||
const [stay, setStay] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const qrCodeUrl = useObservable(() => data.qrCode$);
|
||||
const qrCodeUrl = useObservable(() => data.qrCode$.pipe(catchError(() => of(null))));
|
||||
const t = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { BsmButton } from "renderer/components/shared/bsm-button.component";
|
||||
import { BsmImage } from "renderer/components/shared/bsm-image.component";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { ModalComponent, ModalExitCode } from "renderer/services/modale.service";
|
||||
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
|
||||
import BeatConflict from "../../../../../assets/images/apngs/beat-conflict.png";
|
||||
import { useConstant } from "renderer/hooks/use-constant.hook";
|
||||
|
||||
export const DeleteDuplicateMapsModal: ModalComponent<void, { maps: BsmLocalMap[] }> = ({ resolver, options: {data : { maps }}}) => {
|
||||
|
||||
const t = useTranslation();
|
||||
const multiple = useConstant(() => maps.length > 1);
|
||||
|
||||
return (
|
||||
<form className="text-gray-800 dark:text-gray-200 max-w-sm">
|
||||
<h1 className="text-3xl uppercase tracking-wide w-full text-center">{t("modals.maps-actions.delete-duplicate-maps.title")}</h1>
|
||||
<BsmImage className="mx-auto h-24" image={BeatConflict} />
|
||||
<p>{
|
||||
multiple
|
||||
? t("modals.maps-actions.delete-duplicate-maps.desc-plural", { nb: `${maps.length}` })
|
||||
: t("modals.maps-actions.delete-duplicate-maps.desc", { map: `${maps.at(0).mapInfo.songName}` })
|
||||
}</p>
|
||||
<div className="grid grid-flow-col grid-cols-2 gap-4 mt-4 h-8">
|
||||
<BsmButton typeColor="cancel" className="rounded-md flex justify-center items-center transition-all" onClick={() => resolver({ exitCode: ModalExitCode.CANCELED })} withBar={false} text="misc.cancel" />
|
||||
<BsmButton typeColor="primary" className="rounded-md flex justify-center items-center transition-all" onClick={() => resolver({ exitCode: ModalExitCode.COMPLETED })} withBar={false} text="misc.delete" />
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -34,7 +34,7 @@ export const DeleteMapsModal: ModalComponent<void, { linked: boolean; maps: BsmL
|
||||
<form className="text-gray-800 dark:text-gray-200">
|
||||
<h1 className="text-3xl uppercase tracking-wide w-full text-center">{t(titleText)}</h1>
|
||||
<BsmImage className="mx-auto h-24" image={BeatConflict} />
|
||||
<p className="max-w-sm w-full">{t(descText, multiple ? { nb: maps.length.toString() } : { name: maps.at(0).rawInfo._songName })}</p>
|
||||
<p className="max-w-sm w-full">{t(descText, multiple ? { nb: maps.length.toString() } : { name: maps.at(0).mapInfo.songName })}</p>
|
||||
{linked && (
|
||||
<p className="text-sm italic mt-2 cursor-help w-fit" title={t(infoTitleText)}>
|
||||
{t(infoText)}
|
||||
|
||||
@@ -22,9 +22,11 @@ import { useConstant } from "renderer/hooks/use-constant.hook";
|
||||
import { getLocalTimeZone, parseAbsolute, toCalendarDateTime } from "@internationalized/date";
|
||||
import { VirtualScroll } from "renderer/components/shared/virtual-scroll/virtual-scroll.component";
|
||||
import { MapItemComponentPropsMapper } from "shared/mappers/map/map-item-component-props.mapper";
|
||||
import { ConfigurationService } from "renderer/services/configuration.service";
|
||||
|
||||
export const DownloadMapsModal: ModalComponent<void, { version: BSVersion; ownedMaps: BsmLocalMap[] }> = ({ options: {data : { ownedMaps, version }} }) => {
|
||||
const beatSaver = useService(BeatSaverService);
|
||||
const config = useService(ConfigurationService);
|
||||
const mapsDownloader = useService(MapsDownloaderService);
|
||||
const progressBar = useService(ProgressBarService);
|
||||
const os = useService(OsDiagnosticService);
|
||||
@@ -37,12 +39,11 @@ export const DownloadMapsModal: ModalComponent<void, { version: BSVersion; owned
|
||||
const [query, setQuery] = useState("");
|
||||
const [maps, setMaps] = useState<BsvMapDetail[]>([]);
|
||||
const [downloadbleMaps, setDownloadbleMaps] = useState<DownloadableMap[]>([]);
|
||||
const [sortOrder, setSortOrder] = useState<BsvSearchOrder>(BsvSearchOrder.Latest);
|
||||
const [ownedMapHashs, setOwnedMapHashs] = useState<string[]>(ownedMaps?.map(map => map.hash) ?? []);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const isOnline = useObservable(() => os.isOnline$);
|
||||
const [searchParams, setSearchParams] = useState<SearchParams>({
|
||||
sortOrder,
|
||||
sortOrder: config.get("map-sort-order"),
|
||||
filter,
|
||||
page: 0,
|
||||
q: query,
|
||||
@@ -67,22 +68,21 @@ export const DownloadMapsModal: ModalComponent<void, { version: BSVersion; owned
|
||||
}, [searchParams]);
|
||||
|
||||
useEffect(() => {
|
||||
const onMapDownloaded = (map: BsmLocalMap, targetVersion: BSVersion) => {
|
||||
|
||||
const sub = mapsDownloader.lastDownloadedMap$.subscribe({ next: ({ map, version: targetVersion }) => {
|
||||
if (!equal(targetVersion, version)) {
|
||||
return;
|
||||
}
|
||||
const downloadedHash = map.hash;
|
||||
setOwnedMapHashs(prev => [...prev, downloadedHash]);
|
||||
};
|
||||
|
||||
mapsDownloader.addOnMapDownloadedListener(onMapDownloaded);
|
||||
}});
|
||||
|
||||
if (mapsDownloader.isDownloading) {
|
||||
progressBar.setStyle(mapsDownloader.progressBarStyle);
|
||||
}
|
||||
|
||||
return () => {
|
||||
mapsDownloader.removeOnMapDownloadedListener(onMapDownloaded);
|
||||
sub.unsubscribe();
|
||||
progressBar.setStyle(null);
|
||||
};
|
||||
}, []);
|
||||
@@ -146,7 +146,9 @@ export const DownloadMapsModal: ModalComponent<void, { version: BSVersion; owned
|
||||
duration={map.metadata.duration}
|
||||
hash={map.versions.at(0).hash}
|
||||
likes={map.stats.upvotes}
|
||||
mapId={map.id} ranked={map.ranked}
|
||||
mapId={map.id}
|
||||
ranked={map.ranked}
|
||||
blRanked={map.blRanked}
|
||||
title={map.name}
|
||||
songAutor={map.metadata.songAuthorName}
|
||||
diffs={MapItemComponentPropsMapper.extractMapDiffs({ bsvMap: map })}
|
||||
@@ -169,21 +171,21 @@ export const DownloadMapsModal: ModalComponent<void, { version: BSVersion; owned
|
||||
}, []);
|
||||
|
||||
const handleSortChange = (newSort: BsvSearchOrder) => {
|
||||
setSortOrder(() => newSort);
|
||||
config.set("map-sort-order", newSort);
|
||||
setMaps(() => []);
|
||||
setSearchParams(() => ({ ...searchParams, sortOrder: newSort }));
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
const searchParams: SearchParams = {
|
||||
sortOrder,
|
||||
const searchParamsLocal: SearchParams = {
|
||||
sortOrder: searchParams.sortOrder,
|
||||
filter,
|
||||
q: query.trim(),
|
||||
page: 0,
|
||||
};
|
||||
|
||||
setMaps(() => []);
|
||||
setSearchParams(() => searchParams);
|
||||
setSearchParams(() => searchParamsLocal);
|
||||
};
|
||||
|
||||
const handleLoadMore = () => {
|
||||
@@ -218,7 +220,7 @@ export const DownloadMapsModal: ModalComponent<void, { version: BSVersion; owned
|
||||
handleSearch();
|
||||
}}
|
||||
/>
|
||||
<BsmSelect className="bg-light-main-color-1 dark:bg-main-color-1 rounded-full px-1 pb-0.5 text-center" options={sortOptions} onChange={sort => handleSortChange(sort)} />
|
||||
<BsmSelect className="bg-light-main-color-1 dark:bg-main-color-1 rounded-full px-1 pb-0.5 text-center" options={sortOptions} selected={searchParams.sortOrder} onChange={sort => handleSortChange(sort)} />
|
||||
</div>
|
||||
|
||||
{(() => {
|
||||
|
||||
+6
-3
@@ -5,7 +5,7 @@ import { BSVersion } from "shared/bs-version.interface"
|
||||
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface"
|
||||
import { LocalBPListsDetails } from "shared/models/playlists/local-playlist.models"
|
||||
import { DownloadPlaylistModalHeader } from "./download-playlist-modal-header.component"
|
||||
import { BsvPlaylist, BsvSearchOrder, PlaylistSearchParams } from "shared/models/maps/beat-saver.model"
|
||||
import { BsvPlaylist, PlaylistSearchParams } from "shared/models/maps/beat-saver.model"
|
||||
import { useCallback, useState } from "react"
|
||||
import { useOnUpdate } from "renderer/hooks/use-on-update.hook"
|
||||
import { useService } from "renderer/hooks/use-service.hook"
|
||||
@@ -20,6 +20,7 @@ import BeatConflict from "../../../../../../../assets/images/apngs/beat-conflict
|
||||
import { cn } from "renderer/helpers/css-class.helpers"
|
||||
import { VirtualScroll } from "renderer/components/shared/virtual-scroll/virtual-scroll.component"
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook"
|
||||
import { ConfigurationService } from "renderer/services/configuration.service"
|
||||
|
||||
export const DownloadPlaylistModal: ModalComponent<void, {version: BSVersion, ownedPlaylists$: Observable<LocalBPListsDetails[]>, ownedMaps$: Observable<BsmLocalMap[]>}> = (
|
||||
{ options: { data: { version, ownedPlaylists$, ownedMaps$ }} }
|
||||
@@ -27,8 +28,9 @@ export const DownloadPlaylistModal: ModalComponent<void, {version: BSVersion, ow
|
||||
|
||||
const t = useTranslation();
|
||||
|
||||
const modal = useService(ModalService);
|
||||
const beatSaver = useService(BeatSaverService);
|
||||
const config = useService(ConfigurationService);
|
||||
const modal = useService(ModalService);
|
||||
const playlistDownloader = useService(PlaylistDownloaderService);
|
||||
|
||||
const [playlists, setPlaylists] = useState<BsvPlaylist[]>(null);
|
||||
@@ -41,7 +43,7 @@ export const DownloadPlaylistModal: ModalComponent<void, {version: BSVersion, ow
|
||||
const [error, setError] = useState(false);
|
||||
const [searchParams, setSearchParams] = useState<PlaylistSearchParams>({
|
||||
q: "",
|
||||
sortOrder: BsvSearchOrder.Relevance,
|
||||
sortOrder: config.get("playlist-sort-order"),
|
||||
page: 0,
|
||||
});
|
||||
|
||||
@@ -63,6 +65,7 @@ export const DownloadPlaylistModal: ModalComponent<void, {version: BSVersion, ow
|
||||
|
||||
const handleNewSearch = (value: Omit<PlaylistSearchParams, "page">) => {
|
||||
setPlaylists(() => undefined);
|
||||
config.set("playlist-sort-order", value.sortOrder);
|
||||
setSearchParams(() => ({ ...value, page: 0}));
|
||||
};
|
||||
|
||||
|
||||
+3
-3
@@ -303,7 +303,7 @@ export const EditPlaylistModal: ModalComponent<BPList, Props> = ({ resolver, opt
|
||||
availabledHashsSelected$.value.map(hash => tmpLocalMaps.find(map => getHashOfMap(map) === hash)).filter(Boolean)
|
||||
) : tmpLocalMaps;
|
||||
|
||||
const newPlaylistMaps = {...playlistMaps$.value} ?? {};
|
||||
const newPlaylistMaps = {...playlistMaps$.value ?? {}};
|
||||
mapsToAdd.forEach(map =>{ newPlaylistMaps[getHashOfMap(map)] = { map }; });
|
||||
|
||||
playlistMaps$.next(newPlaylistMaps);
|
||||
@@ -348,8 +348,8 @@ export const EditPlaylistModal: ModalComponent<BPList, Props> = ({ resolver, opt
|
||||
|
||||
const { map } = playlistMap;
|
||||
|
||||
if((map as BsmLocalMap).rawInfo?._levelAuthorName){
|
||||
mappersSet.add((map as BsmLocalMap).rawInfo._levelAuthorName);
|
||||
if((map as BsmLocalMap).mapInfo?.levelMappers){
|
||||
(map as BsmLocalMap).mapInfo.levelMappers.forEach(mapper => mappersSet.add(mapper));
|
||||
}
|
||||
else if((map as SongDetails).uploader?.name){
|
||||
mappersSet.add((map as SongDetails).uploader.name);
|
||||
|
||||
+3
-1
@@ -80,7 +80,9 @@ export const BsvPlaylistDetailsModal: ModalComponent<void, Props> = ({ resolver,
|
||||
duration={map.metadata.duration}
|
||||
hash={map.versions.at(0).hash}
|
||||
likes={map.stats.upvotes}
|
||||
mapId={map.id} ranked={map.ranked}
|
||||
mapId={map.id}
|
||||
ranked={map.ranked}
|
||||
blRanked={map.blRanked}
|
||||
title={map.name}
|
||||
songAutor={map.metadata.songAuthorName}
|
||||
diffs={MapItemComponentPropsMapper.extractMapDiffs({ bsvMap: map })}
|
||||
|
||||
+13
-8
@@ -34,8 +34,12 @@ export const LocalPlaylistDetailsModal: ModalComponent<void, Props> = ({resolver
|
||||
const installedMaps = useObservable(() => options.data.installedMaps$, null);
|
||||
|
||||
const isMissingMaps$ = useConstant(() => combineLatest([options.data.installedMaps$, options.data.localPlaylist$]).pipe(map(([maps, playlist]) => maps.length !== playlist.songs.length)));
|
||||
const isPlaylistDownloading$ = useConstant(() => options.data.localPlaylist$.pipe(switchMap(playlist => playlistDownloader.$isPlaylistDownloading(playlist.path, options.data.version))));
|
||||
const isPlaylistInQueue$ = useConstant(() => options.data.localPlaylist$.pipe(switchMap(playlist => playlistDownloader.$isPlaylistInQueue(playlist.path, options.data.version))));
|
||||
const isPlaylistDownloading$ = useConstant(() => options.data.localPlaylist$.pipe(switchMap(
|
||||
playlist => playlistDownloader.$isPlaylistDownloading(playlist?.customData?.syncURL ?? playlist.path, options.data.version)
|
||||
)));
|
||||
const isPlaylistInQueue$ = useConstant(() => options.data.localPlaylist$.pipe(switchMap(
|
||||
playlist => playlistDownloader.$isPlaylistInQueue(playlist?.customData?.syncURL ?? playlist.path, options.data.version)
|
||||
)));
|
||||
|
||||
const isInQueue = useObservable(() => isPlaylistInQueue$, false);
|
||||
|
||||
@@ -44,7 +48,7 @@ export const LocalPlaylistDetailsModal: ModalComponent<void, Props> = ({resolver
|
||||
const ignoreSongsHashs = installedMaps?.map(map => map.hash);
|
||||
|
||||
const obs$ = playlistDownloader.downloadPlaylist({
|
||||
downloadSource: localPlaylist.path,
|
||||
downloadSource: localPlaylist?.customData?.syncURL ?? localPlaylist.path,
|
||||
dest: localPlaylist.path,
|
||||
version: options.data.version,
|
||||
ignoreSongsHashs,
|
||||
@@ -58,16 +62,17 @@ export const LocalPlaylistDetailsModal: ModalComponent<void, Props> = ({resolver
|
||||
<MapItem
|
||||
key={map.path}
|
||||
hash={map.hash}
|
||||
title={map.rawInfo._songName}
|
||||
title={map.mapInfo.songName}
|
||||
coverUrl={map.coverUrl}
|
||||
songUrl={map.songUrl}
|
||||
autor={map.rawInfo._levelAuthorName}
|
||||
songAutor={map.rawInfo._songAuthorName}
|
||||
bpm={map.rawInfo._beatsPerMinute}
|
||||
autor={map.mapInfo.levelMappers.at(0)}
|
||||
songAutor={map.mapInfo.songAuthorName}
|
||||
bpm={map.mapInfo.beatsPerMinute}
|
||||
duration={map.songDetails?.duration}
|
||||
diffs={MapItemComponentPropsMapper.extractMapDiffs({ rawMapInfo: map.rawInfo, songDetails: map.songDetails })}
|
||||
diffs={MapItemComponentPropsMapper.extractMapDiffs({ mapInfo: map.mapInfo, songDetails: map.songDetails })}
|
||||
mapId={map.songDetails?.id}
|
||||
ranked={map.songDetails?.ranked}
|
||||
blRanked={map.songDetails?.blRanked}
|
||||
autorId={map.songDetails?.uploader.id}
|
||||
likes={map.songDetails?.upVotes}
|
||||
createdAt={map.songDetails?.uploadedAt}
|
||||
|
||||
@@ -17,7 +17,7 @@ export function Modal() {
|
||||
|
||||
useEffect(() => {
|
||||
const onEscape = (e: KeyboardEvent) => {
|
||||
if (currentModal.options.closable === false || e.key !== "Escape") {
|
||||
if (currentModal.options?.closable === false || e.key !== "Escape") {
|
||||
return;
|
||||
}
|
||||
currentModal.resolver({ exitCode: ModalExitCode.CLOSED });
|
||||
@@ -65,7 +65,7 @@ export function Modal() {
|
||||
}
|
||||
|
||||
const onOverlayClicked = () => {
|
||||
if (currentModal.options.closable === false) {
|
||||
if (currentModal.options?.closable === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -25,13 +25,13 @@ export const BsContentTabItem: BsContentTabItemComponent = ({ text, icon: Icon,
|
||||
}
|
||||
|
||||
return (
|
||||
<li className={`relative w-full cursor-pointer flex-1 text-center text-lg font-bold flex justify-center items-center content-center px-7 bg-light-main-color-2 dark:bg-main-color-2 hover:bg-light-main-color-1 dark:hover:bg-main-color-1 ${active && "!bg-light-main-color-1 dark:!bg-main-color-1"}`} onClick={handleClick}>
|
||||
<li className={`relative w-full cursor-pointer text-center font-bold flex justify-center items-center content-center px-7 bg-theme-2 hover:bg-theme-1 rounded-l-md ${active && "!bg-theme-1"}`} onClick={handleClick}>
|
||||
<div className="flex flex-col gap-0.5 justify-start items-center text-main-color-1 dark:text-gray-200">
|
||||
<Icon className="w-7 h-7"/>
|
||||
<Icon className="size-7"/>
|
||||
<span className=" font-thin italic text-xs">{t(text)}</span>
|
||||
</div>
|
||||
{linkProps && (
|
||||
<div className="flex items-center absolute top-1.5 left-1.5">
|
||||
<div className="absolute top-1.5 left-1.5">
|
||||
<LinkButton
|
||||
state={linkProps.state}
|
||||
className={linkProps.className ?? "block w-6 h-6 aspect-square blur-0 cursor-pointer hover:brightness-75"}
|
||||
|
||||
+7
-5
@@ -15,8 +15,8 @@ export function BsContentTabPanel({className, tabIndex, tabs, onTabChange, child
|
||||
const sliderColor = useThemeColor("second-color");
|
||||
|
||||
return (
|
||||
<div className={className ?? "w-full h-full flex flex-row bg-light-main-color-1 dark:bg-main-color-1 rounded-md shadow-black shadow-md overflow-hidden"}>
|
||||
<nav className="h-full grid grid-flow-row !rounded-none shadow-sm flex-shrink-0">
|
||||
<div className={className ?? "flex-grow basis-0 min-h-0 w-full flex flex-row bg-light-main-color-1 dark:bg-main-color-1 rounded-md shadow-black shadow-md"}>
|
||||
<nav className="h-full grid grid-flow-row shadow-sm flex-shrink-0">
|
||||
{tabs.map((tab, i) => (
|
||||
<BsContentTabItem
|
||||
key={`${tab.text}${tab.icon}`}
|
||||
@@ -26,9 +26,11 @@ export function BsContentTabPanel({className, tabIndex, tabs, onTabChange, child
|
||||
/>
|
||||
))}
|
||||
</nav>
|
||||
<LaserSlider className="h-full w-1 relative shrink-0" mode="vertical" color={sliderColor} nbSteps={tabs.length} step={tabIndex}/>
|
||||
<div className="flex-grow h-full flex flex-col transition-all duration-300" style={{ translate: `0 ${0 - tabIndex * 100}%` }}>
|
||||
{children}
|
||||
<LaserSlider className="h-full w-1 shrink-0" mode="vertical" color={sliderColor} nbSteps={tabs.length} step={tabIndex}/>
|
||||
<div className="flex-grow overflow-y-hidden">
|
||||
<div className="size-full flex flex-col transition-transform duration-300" style={{ transform: `translateY(${0 - tabIndex * 100}%)` }}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -79,7 +79,7 @@ export const BsmDropdownButton = forwardRef(({ className, classNames, buttonColo
|
||||
{items?.map(
|
||||
i =>
|
||||
i && (
|
||||
<div key={crypto.randomUUID()} onClick={() => i.onClick?.()} className="flex w-full px-3 py-2 hover:backdrop-brightness-150">
|
||||
<div key={crypto.randomUUID()} onClick={() => { setExpanded(() => false); i.onClick?.()}} className="flex w-full px-3 py-2 hover:backdrop-brightness-150">
|
||||
{i.icon && <BsmIcon icon={i.icon} className="h-5 w-5 mr-1 text-inherit" />}
|
||||
<span className="w-max">{t(i.text)}</span>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { DragEvent, ReactNode, useCallback, useMemo, useState } from "react";
|
||||
import { BsmImage } from "./bsm-image.component";
|
||||
import BeatImpatient from "../../../../assets/images/apngs/beat-impatient.png";
|
||||
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
|
||||
import Color from "color";
|
||||
import { cn } from "renderer/helpers/css-class.helpers";
|
||||
import { CloseIcon } from "../svgs/icons/close-icon.component";
|
||||
import { OpenDialogOptions } from "electron";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { useService } from "renderer/hooks/use-service.hook";
|
||||
import { IpcService } from "renderer/services/ipc.service";
|
||||
import { NotificationService } from "renderer/services/notification.service";
|
||||
import { lastValueFrom } from "rxjs";
|
||||
|
||||
type Props = Readonly<{
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
open?: boolean;
|
||||
text?: string;
|
||||
subtext?: string;
|
||||
onFiles?: (paths: string[]) => void;
|
||||
onClose?: () => void;
|
||||
filters?: OpenDialogOptions["filters"];
|
||||
dialogOptions?: {
|
||||
text?: string;
|
||||
dialog?: Exclude<OpenDialogOptions, "filters">;
|
||||
}
|
||||
}>;
|
||||
|
||||
// Supports drop and drop functionality for files.
|
||||
export function Dropzone({
|
||||
children,
|
||||
className,
|
||||
open,
|
||||
text,
|
||||
subtext,
|
||||
filters,
|
||||
dialogOptions,
|
||||
onFiles,
|
||||
onClose
|
||||
}: Props) {
|
||||
|
||||
const ipc = useService(IpcService);
|
||||
const notification = useService(NotificationService);
|
||||
|
||||
const t = useTranslation();
|
||||
|
||||
const [dragCount, setDragCount] = useState(0);
|
||||
const [dropZoneDragging, setDropZoneDragging] = useState(false);
|
||||
const themeColor = useThemeColor("first-color");
|
||||
const color = new Color(themeColor).lighten(.25).saturate(.8).hex();
|
||||
|
||||
const supportedExtensions = useMemo(() => {
|
||||
if(!filters?.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return filters.flatMap(filter => filter.extensions ?? []);
|
||||
}, [filters]);
|
||||
|
||||
const handleDrop = useCallback((event: DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
setDragCount(() => 0);
|
||||
setDropZoneDragging(() => false);
|
||||
|
||||
if (!onFiles || !event.dataTransfer?.files?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const paths = Array.from(event.dataTransfer.files).reduce<string[]>((acc, file) => {
|
||||
const path = window.electron.webUtils.getPathForFile(file);
|
||||
|
||||
if(supportedExtensions.some(ext => path.endsWith(ext))) {
|
||||
acc.push(path);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
if(!paths.length) {
|
||||
notification.notifyError({
|
||||
title: t("notifications.shared.errors.titles.file-not-supported"),
|
||||
desc: t("notifications.shared.errors.msg.file-not-supported", { types: supportedExtensions.join(", ") }),
|
||||
duration: 8000
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
onFiles(paths);
|
||||
}, [supportedExtensions]);
|
||||
|
||||
const onDragOver = useCallback((event: DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}, []);
|
||||
|
||||
const onDragEnter = useCallback((event: DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setDragCount(prev => prev + 1);
|
||||
}, []);
|
||||
|
||||
const onDragLeave = useCallback((event: DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setDragCount(prev => prev - 1);
|
||||
}, []);
|
||||
|
||||
const openDialog = useCallback(async () => {
|
||||
if(!dialogOptions?.dialog) {
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await lastValueFrom(ipc.sendV2("open-dialog", { ...dialogOptions.dialog, filters }));
|
||||
|
||||
if(res?.canceled || !res?.filePaths?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
onFiles?.(res.filePaths);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={cn("relative", className)} onDragEnter={onDragEnter} onDragLeave={onDragLeave}>
|
||||
<AnimatePresence>
|
||||
{(dragCount || open) ? (
|
||||
<div onDragEnter={onDragEnter} onDragLeave={onDragLeave} onDrop={onDragLeave} className="absolute size-full top-0 right-0 z-10">
|
||||
<motion.div className="absolute size-full top-0 right-0 bg-black" initial={{ opacity: 0 }} animate={{ opacity: .6 }} exit={{ opacity: 0 }}/>
|
||||
<motion.div className="absolute size-full top-0 right-0 flex justify-center items-center p-5" initial={{ y: "100%" }} animate={{ y: "0" }} exit={{ y: "100%" }}>
|
||||
<div className="relative h-72 max-h-full w-full max-w-lg sm:h-96 sm:max-w-2xl bg-theme-3 rounded-md shadow-black shadow-lg p-7">
|
||||
{onClose && (
|
||||
<button className="absolute top-0.5 right-0.5 size-7" onClick={onClose}>
|
||||
<CloseIcon className="size-full"/>
|
||||
</button>
|
||||
)}
|
||||
<div
|
||||
onDragOver={onDragOver}
|
||||
onDrop={handleDrop}
|
||||
onDragEnter={() => setDropZoneDragging(true)}
|
||||
onDragLeave={() => setDropZoneDragging(false)}
|
||||
className="relative size-full z-[1] border-dashed border-3 rounded-md flex flex-col gap-1 justify-center items-center"
|
||||
style={{ borderColor: color }}
|
||||
>
|
||||
<div className="absolute size-full top-0 right-0 pointer-events-none -z-[1] transition-opacity duration-150" style={{
|
||||
backgroundColor: "transparent",
|
||||
backgroundImage: `radial-gradient(${color} 1px, transparent 0)`,
|
||||
backgroundSize: "60px 60px",
|
||||
backgroundPosition: "5px",
|
||||
opacity: dropZoneDragging ? .7 : .2,
|
||||
}}/>
|
||||
<BsmImage className="size-28 pointer-events-none" image={BeatImpatient}/>
|
||||
<span className="text-2xl font-bold pointer-events-none">{text}</span>
|
||||
<span className="pointer-events-none">{subtext}</span>
|
||||
{dialogOptions?.dialog && (
|
||||
<button className="underline hover:no-underline" onClick={openDialog}>{dialogOptions?.text ?? t("drop-zone.or-browse-files")}</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
): undefined}
|
||||
</AnimatePresence>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { CSSProperties } from "react";
|
||||
import { cn } from "renderer/helpers/css-class.helpers";
|
||||
|
||||
type Props = Readonly<{
|
||||
mode: "horizontal" | "vertical";
|
||||
@@ -27,7 +28,7 @@ export function LaserSlider({mode, color, className, nbSteps, step}: Props) {
|
||||
})();
|
||||
|
||||
return (
|
||||
<div className={className} style={{ color }}>
|
||||
<div className={cn("relative", className)} style={{ color }}>
|
||||
<span className="absolute h-full w-full bg-current brightness-50" />
|
||||
<span className="absolute block bg-current transition-transform duration-300 shadow-center shadow-current" style={sliderStyle} />
|
||||
</div>
|
||||
|
||||
@@ -61,9 +61,12 @@ import { EyeCrossIcon } from "./icons/eye-cross-icon.component";
|
||||
import { ShortcutIcon } from "./icons/shortcut-icon.component";
|
||||
import { BackupRestoreIcon } from "./icons/backup-restore-icon.component";
|
||||
import { SongDetailDiffCharactertistic } from "shared/models/maps/song-details-cache/song-details-cache.model";
|
||||
import { CleanIcon } from "./icons/clean-icon.component";
|
||||
import { BrowseIcon } from "./icons/browse-icon.component";
|
||||
import { AddFileIcon } from "./icons/add-file-icon.component";
|
||||
|
||||
|
||||
export type BsmIconType = SongDetailDiffCharactertistic | ("settings" | "trash" | "favorite" | "folder" | "bsNote" | "check" | "three-dots" | "twitch" | "eye" | "play" | "checkCircleIcon" | "discord" | "info" | "eye-cross" | "terminal" | "desktop" | "oculus" | "add" | "cross" | "task" | "github" | "close" | "thumbUpFill" | "timerFill" | "pause" | "twitter" | "sync" | "chevron-top" | "copy" | "steam" | "edit" | "export" | "patreon" | "search" | "bsMapDifficulty" | "link" | "unlink" | "download" | "filter" | "mee6" | "volume-up" | "volume-off" | "volume-down" | "shortcut" | "backup-restore" | "web-site" | "fr-FR-flag" | "es-ES-flag" | "en-US-flag" | "en-EN-flag" | "de-DE-flag" | "ru-RU-flag" | "zh-CN-flag" | "zh-TW-flag" | "ja-JP-flag");
|
||||
export type BsmIconType = SongDetailDiffCharactertistic | ("settings" | "trash" | "favorite" | "folder" | "bsNote" | "check" | "three-dots" | "twitch" | "eye" | "play" | "checkCircleIcon" | "discord" | "info" | "eye-cross" | "terminal" | "desktop" | "oculus" | "add" | "cross" | "task" | "github" | "close" | "thumbUpFill" | "timerFill" | "pause" | "twitter" | "sync" | "chevron-top" | "copy" | "steam" | "edit" | "export" | "patreon" | "search" | "bsMapDifficulty" | "link" | "unlink" | "download" | "filter" | "mee6" | "volume-up" | "volume-off" | "volume-down" | "shortcut" | "backup-restore" | "web-site" | "clean" | "browse" | "add-file" | "fr-FR-flag" | "es-ES-flag" | "en-US-flag" | "en-EN-flag" | "de-DE-flag" | "ru-RU-flag" | "zh-CN-flag" | "zh-TW-flag" | "ja-JP-flag");
|
||||
|
||||
export const BsmIcon = memo(({ className, icon, style }: { className?: string; icon: BsmIconType; style?: CSSProperties }) => {
|
||||
// TODO : Very ugly very messy, need to find a better way to do this
|
||||
@@ -256,6 +259,19 @@ export const BsmIcon = memo(({ className, icon, style }: { className?: string; i
|
||||
if(icon === "web-site") {
|
||||
return <WebSiteIcon className={className} style={style} />;
|
||||
}
|
||||
|
||||
if(icon === "clean") {
|
||||
return <CleanIcon className={className} style={style} />;
|
||||
}
|
||||
|
||||
if (icon === "browse") {
|
||||
return <BrowseIcon className={className} style={style} />;
|
||||
}
|
||||
|
||||
if(icon === "add-file") {
|
||||
return <AddFileIcon className={className} style={style} />;
|
||||
}
|
||||
|
||||
return <TrashIcon className={className} style={style} />;
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { createSvgIcon } from "../svg-icon.type";
|
||||
|
||||
export const AddFileIcon = createSvgIcon(( props, ref ) => {
|
||||
return (
|
||||
<svg ref={ref} {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor">
|
||||
<path d="M440-357.37v80q0 17 11.5 28.5t28.5 11.5q17 0 28.5-11.5t11.5-28.5v-80h80q17 0 28.5-11.5t11.5-28.5q0-17-11.5-28.5t-28.5-11.5h-80v-80q0-17-11.5-28.5t-28.5-11.5q-17 0-28.5 11.5t-11.5 28.5v80h-80q-17 0-28.5 11.5t-11.5 28.5q0 17 11.5 28.5t28.5 11.5h80ZM242.87-71.87q-37.78 0-64.39-26.61t-26.61-64.39v-634.26q0-37.78 26.61-64.39t64.39-26.61H525.8q18.22 0 34.72 6.84 16.5 6.83 29.18 19.51L781.78-669.7q12.68 12.68 19.51 29.18 6.84 16.5 6.84 34.72v442.93q0 37.78-26.61 64.39t-64.39 26.61H242.87Zm274.26-570.76v-154.5H242.87v634.26h474.26v-434.26h-154.5q-19.15 0-32.33-13.17-13.17-13.18-13.17-32.33Zm-274.26-154.5V-597.13v-200 634.26-634.26Z"/>
|
||||
</svg>
|
||||
);
|
||||
})
|
||||
@@ -0,0 +1,11 @@
|
||||
import { createSvgIcon } from "../svg-icon.type";
|
||||
|
||||
export const BrowseIcon = createSvgIcon(( props, ref ) => {
|
||||
|
||||
return (
|
||||
<svg ref={ref} {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor">
|
||||
<path d="M73.38-480q0-84.36 32.02-158.55 32.02-74.2 87.06-129.15 55.03-54.96 129.23-87.06 74.19-32.1 158.55-32.1 134.3 0 238.05 76.51t145.17 196.84q5.8 15.77-1.12 30.78-6.93 15.01-22.85 19.75-14.93 4.2-28.19-4.11-13.25-8.3-18.95-22.99-22.75-67.42-71.58-119.41-48.83-52-116.66-79.69v17.01q0 34.1-23.83 59.02-23.84 24.93-57.93 24.93h-83.87v84.11q0 16.64-12.84 27.87-12.83 11.23-29.75 11.23h-81.28v84.53h60.85q16 0 26.94 11.02 10.93 11.02 10.93 27.02v84.42h-57.2L157.96-556.35q-4.33 19.26-6.5 38.13-2.16 18.87-2.16 37.74 0 122.43 77.84 214.51 77.85 92.08 197.26 111.19 14.87 2.74 23.8 14.97 8.92 12.22 7.57 27.5-1.6 15.83-13.52 25.68-11.92 9.84-27.64 6.72-145.92-23.35-243.57-135.47Q73.38-327.5 73.38-480Zm749.37 355.88L717.04-229.17q-20.76 13.44-44.66 21.04-23.89 7.6-49.14 7.6-73.46 0-125.14-51.53-51.67-51.54-51.67-125.07 0-73.37 51.61-124.79 51.62-51.41 124.99-51.41 73.53 0 124.94 51.55 51.42 51.56 51.42 125.02 0 25.4-7.93 49.26-7.94 23.86-20.71 45.28l105.13 105.05q11.26 11.03 11.47 26.53.22 15.5-10.81 26.52-11.1 11.26-26.89 11.26-15.79 0-26.9-11.26ZM622.86-276.28q42.13 0 71.37-29.27 29.25-29.28 29.25-71.41 0-42.13-29.15-71.37-29.16-29.25-71.29-29.25-42.13 0-71.41 29.15-29.29 29.16-29.29 71.29 0 42.13 29.19 71.49 29.2 29.37 71.33 29.37Z"/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { createSvgIcon } from "../svg-icon.type";
|
||||
|
||||
export const CleanIcon = createSvgIcon((props, ref) => {
|
||||
return (
|
||||
<svg ref={ref} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"color="currentColor" fill="none" {...props}>
|
||||
<path d="m21 3-8 8.5m-3.554-.415c-2.48.952-4.463.789-6.446.003.5 6.443 3.504 8.92 7.509 9.912 0 0 3.017-2.134 3.452-7.193.047-.548.07-.821-.043-1.13-.114-.309-.338-.53-.785-.973-.736-.728-1.103-1.092-1.54-1.184-.437-.09-1.007.128-2.147.565" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M4.5 16.446S7 16.93 9.5 15" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M8.5 7.25a1.25 1.25 0 1 1-2.5 0 1.25 1.25 0 0 1 2.5 0Z" stroke="currentColor" strokeWidth="2"/>
|
||||
<path d="M11 4v.1" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
);
|
||||
});
|
||||
@@ -1,4 +1,3 @@
|
||||
import { motion } from "framer-motion";
|
||||
import { ChangeEvent, useEffect, useState } from "react";
|
||||
import { BsmButton } from "renderer/components/shared/bsm-button.component";
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
@@ -15,6 +14,7 @@ import { lastValueFrom } from "rxjs";
|
||||
import { BsDownloaderService } from "renderer/services/bs-version-download/bs-downloader.service";
|
||||
import equal from "fast-deep-equal";
|
||||
import { GlowEffect } from "renderer/components/shared/glow-effect.component";
|
||||
import { cn } from "renderer/helpers/css-class.helpers";
|
||||
|
||||
type Props = { version: BSVersion };
|
||||
|
||||
@@ -44,6 +44,7 @@ export function LaunchSlide({ version }: Props) {
|
||||
}, [desktopMode]);
|
||||
|
||||
const setMode = (mode: LaunchMods, value: boolean) => {
|
||||
|
||||
if (mode === LaunchMods.DEBUG_MOD) {
|
||||
setDebugMode(value);
|
||||
} else if (mode === LaunchMods.OCULUS_MOD) {
|
||||
@@ -64,7 +65,7 @@ export function LaunchSlide({ version }: Props) {
|
||||
oculus: version.oculus ? false : oculusMode,
|
||||
desktop: desktopMode,
|
||||
debug: debugMode,
|
||||
additionalArgs,
|
||||
additionalArgs: advancedLaunch ? additionalArgs : [],
|
||||
protonPath: bsLauncherService.getProtonPath(),
|
||||
});
|
||||
|
||||
@@ -84,9 +85,9 @@ export function LaunchSlide({ version }: Props) {
|
||||
</div>
|
||||
<div className="pt-4 w-2/3 flex flex-col items-center gap-3">
|
||||
<div className="relative">
|
||||
<GlowEffect className="!rounded-full" visible={!!additionalArgsString}/>
|
||||
<GlowEffect className="!rounded-full" visible={!!(advancedLaunch && additionalArgsString)}/>
|
||||
<BsmButton
|
||||
className="rounded-full w-fit text-lg py-1 px-7 shadow-md shadow-black bg-light-main-color-2 dark:bg-main-color-2 text-gray-800 dark:text-white"
|
||||
className={cn("rounded-full w-fit text-lg py-1 px-7 bg-theme-2 text-gray-800 dark:text-white", (advancedLaunch && additionalArgsString) ? "" : "shadow-md shadow-black")}
|
||||
text="pages.version-viewer.launch-mods.advanced-launch.button"
|
||||
withBar={false}
|
||||
onClick={e => {
|
||||
@@ -95,9 +96,9 @@ export function LaunchSlide({ version }: Props) {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<motion.div className="bg-light-main-color-2 dark:bg-main-color-2 h-9 rounded-full overflow-hidden flex items-center justify-center" initial={{ width: "0px" }} animate={{ width: advancedLaunch ? "100%" : "0px" }}>
|
||||
<div className="bg-light-main-color-2 dark:bg-main-color-2 h-9 rounded-full overflow-hidden flex items-center justify-center transition-all duration-100 ease-in-out w-full origin-top shadow-black shadow-sm" style={{ scale: advancedLaunch ? "100% 100%" : "0 0" }}>
|
||||
<input className="w-[calc(100%-12px)] h-[calc(100%-12px)] bg-light-main-color-1 dark:bg-main-color-1 text-black dark:text-white rounded-full outline-none text-center" type="text" placeholder={t("pages.version-viewer.launch-mods.advanced-launch.placeholder")} value={additionalArgsString} onChange={handleAdditionalArgsChange} />
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='grow flex justify-center items-center'>
|
||||
<BsmButton
|
||||
|
||||
@@ -3,22 +3,26 @@ import { BsmCheckbox } from "renderer/components/shared/bsm-checkbox.component";
|
||||
import { Mod } from "shared/models/mods/mod.interface";
|
||||
import { CSSProperties, MouseEvent, useMemo, useRef } from "react";
|
||||
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
|
||||
import { BsModsManagerService } from "renderer/services/bs-mods-manager.service";
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
import { PageStateService } from "renderer/services/page-state.service";
|
||||
import useDoubleClick from "use-double-click";
|
||||
import { gt } from "semver";
|
||||
import { useService } from "renderer/hooks/use-service.hook";
|
||||
import { useOnUpdate } from "renderer/hooks/use-on-update.hook";
|
||||
|
||||
type Props = { className?: string; mod: Mod; installedVersion: string; isDependency?: boolean; isSelected?: boolean; onChange?: (val: boolean) => void; wantInfo?: boolean; onWantInfo?: (mod: Mod) => void };
|
||||
type Props = {
|
||||
className?: string;
|
||||
mod: Mod;
|
||||
installedVersion: string;
|
||||
isDependency?: boolean;
|
||||
isSelected?: boolean;
|
||||
onChange?: (val: boolean) => void;
|
||||
wantInfo?: boolean;
|
||||
onWantInfo?: (mod: Mod) => void;
|
||||
disabled?: boolean;
|
||||
onUninstall?: () => void;
|
||||
};
|
||||
|
||||
export function ModItem({ className, mod, installedVersion, isDependency, isSelected, onChange, wantInfo, onWantInfo }: Props) {
|
||||
const modsManager = useService(BsModsManagerService);
|
||||
const pageState = useService(PageStateService);
|
||||
export function ModItem({ className, mod, installedVersion, isDependency, isSelected, onChange, wantInfo, onWantInfo, disabled, onUninstall }: Props) {
|
||||
|
||||
const themeColor = useThemeColor("second-color");
|
||||
const uninstalling = useObservable(() => modsManager.isUninstalling$);
|
||||
const clickRef = useRef();
|
||||
|
||||
const isChecked = useMemo(() => isDependency || isSelected || mod.required, [isDependency, isSelected, mod.required]);
|
||||
@@ -37,10 +41,6 @@ export function ModItem({ className, mod, installedVersion, isDependency, isSele
|
||||
const wantInfoStyle: CSSProperties = wantInfo ? { borderColor: themeColor } : { borderColor: "transparent" };
|
||||
const isOutDated = installedVersion ? gt(mod.version, installedVersion) : false;
|
||||
|
||||
const uninstall = () => {
|
||||
modsManager.uninstallMod(mod, pageState.getState());
|
||||
};
|
||||
|
||||
const handleWantInfo = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
onWantInfo(mod);
|
||||
@@ -53,7 +53,7 @@ export function ModItem({ className, mod, installedVersion, isDependency, isSele
|
||||
return (
|
||||
<li ref={clickRef} className={`${className} group`}>
|
||||
<div className="h-full aspect-square flex items-center justify-center p-[7px] rounded-l-md bg-inherit ml-3 border-2 border-r-0 z-[1] group-hover:brightness-90" style={wantInfoStyle}>
|
||||
<BsmCheckbox className="h-full aspect-square z-[1] relative bg-inherit" onChange={() => onChange(!isChecked)} disabled={mod.required || isDependency} checked={isChecked} />
|
||||
<BsmCheckbox className="h-full aspect-square z-[1] relative bg-inherit" onChange={() => onChange(!isChecked)} disabled={mod.required || isDependency || disabled} checked={isChecked} />
|
||||
</div>
|
||||
<span className="bg-inherit py-2 pl-3 font-bold text-sm whitespace-nowrap border-t-2 border-b-2 blur-none group-hover:brightness-90" style={wantInfoStyle}>
|
||||
{mod.name}
|
||||
@@ -72,11 +72,11 @@ export function ModItem({ className, mod, installedVersion, isDependency, isSele
|
||||
<BsmButton
|
||||
className="z-[1] h-7 w-7 p-[5px] rounded-full group-hover:brightness-90"
|
||||
icon="trash"
|
||||
disabled={uninstalling}
|
||||
disabled={disabled}
|
||||
withBar={false}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
uninstall();
|
||||
onUninstall?.();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -3,18 +3,22 @@ import { useState } from "react";
|
||||
import { BsmButton } from "renderer/components/shared/bsm-button.component";
|
||||
import { BsmDropdownButton } from "renderer/components/shared/bsm-dropdown-button.component";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { BsModsManagerService } from "renderer/services/bs-mods-manager.service";
|
||||
import { PageStateService } from "renderer/services/page-state.service";
|
||||
import { Mod } from "shared/models/mods/mod.interface";
|
||||
import { ModItem } from "./mod-item.component";
|
||||
import { useService } from "renderer/hooks/use-service.hook";
|
||||
|
||||
type Props = { modsMap: Map<string, Mod[]>; installed: Map<string, Mod[]>; modsSelected: Mod[]; onModChange: (selected: boolean, mod: Mod) => void; moreInfoMod?: Mod; onWantInfos: (mod: Mod) => void };
|
||||
type Props = {
|
||||
modsMap: Map<string, Mod[]>;
|
||||
installed: Map<string, Mod[]>;
|
||||
modsSelected: Mod[];
|
||||
onModChange: (selected: boolean, mod: Mod) => void;
|
||||
moreInfoMod?: Mod;
|
||||
onWantInfos: (mod: Mod) => void
|
||||
disabled?: boolean;
|
||||
uninstallMod?: (mods: Mod) => void;
|
||||
uninstallAllMods?: () => void;
|
||||
};
|
||||
|
||||
export function ModsGrid({ modsMap, installed, modsSelected, onModChange, moreInfoMod, onWantInfos }: Props) {
|
||||
|
||||
const pageState = useService(PageStateService);
|
||||
const modsManager = useService(BsModsManagerService);
|
||||
export function ModsGrid({ modsMap, installed, modsSelected, onModChange, moreInfoMod, onWantInfos, disabled, uninstallMod, uninstallAllMods }: Props) {
|
||||
|
||||
const [filter, setFilter] = useState("");
|
||||
const [filterEnabled, setFilterEnabled] = useState(false);
|
||||
@@ -50,10 +54,6 @@ export function ModsGrid({ modsMap, installed, modsSelected, onModChange, moreIn
|
||||
setFilterEnabled(b => !b);
|
||||
};
|
||||
|
||||
const handleUninstallAll = () => {
|
||||
modsManager.uninstallAllMods(pageState.getState());
|
||||
};
|
||||
|
||||
return (
|
||||
modsMap && (
|
||||
<div className="grid gap-y-1 grid-cols-[40px_min-content_min-content_min-content_1fr_min-content] bg-light-main-color-2 dark:bg-main-color-2 text-main-color-1 dark:text-light-main-color-1">
|
||||
@@ -66,7 +66,7 @@ export function ModsGrid({ modsMap, installed, modsSelected, onModChange, moreIn
|
||||
<span className="z-10 sticky flex items-center justify-center top-0 bg-inherit border-b-2 border-main-color-1 h-8 px-2 whitespace-nowrap">{t("pages.version-viewer.mods.mods-grid.header-bar.latest")}</span>
|
||||
<span className="z-10 sticky flex items-center justify-center top-0 bg-inherit border-b-2 border-main-color-1 h-8 whitespace-nowrap">{t("pages.version-viewer.mods.mods-grid.header-bar.description")}</span>
|
||||
<span className="z-10 sticky top-0 bg-inherit border-b-2 border-main-color-1 h-8 flex justify-start items-center py-1 pl-[3px] min-w-[50px]">
|
||||
<BsmDropdownButton className="h-full aspect-square relative rounded-full bg-light-main-color-1 dark:bg-main-color-3" withBar={false} icon="three-dots" buttonClassName="!rounded-full !p-[2px] !bg-light-main-color-2 dark:!bg-main-color-2 hover:!bg-light-main-color-1 dark:hover:!bg-main-color-3" menuTranslationY="5px" items={[{ text: "pages.version-viewer.mods.mods-grid.header-bar.dropdown.uninstall-all", icon: "trash", onClick: handleUninstallAll }]} />
|
||||
<BsmDropdownButton className="h-full aspect-square relative rounded-full bg-light-main-color-1 dark:bg-main-color-3" withBar={false} icon="three-dots" buttonClassName="!rounded-full !p-[2px] !bg-light-main-color-2 dark:!bg-main-color-2 hover:!bg-light-main-color-1 dark:hover:!bg-main-color-3" menuTranslationY="5px" items={[{ text: "pages.version-viewer.mods.mods-grid.header-bar.dropdown.uninstall-all", icon: "trash", onClick: () => uninstallAllMods?.() }]} />
|
||||
</span>
|
||||
|
||||
{Array.from(modsMap.keys()).map(
|
||||
@@ -74,7 +74,19 @@ export function ModsGrid({ modsMap, installed, modsSelected, onModChange, moreIn
|
||||
modsMap.get(key).some(mod => mod.name.toLowerCase().includes(filter)) && (
|
||||
<ul key={key} className="contents">
|
||||
<h2 className="col-span-full py-1 font-bold pl-3">{key}</h2>
|
||||
{modsMap.get(key).map(mod => mod.name?.toLowerCase().includes(filter) && <ModItem key={mod.name} className="contents bg-light-main-color-3 dark:bg-main-color-1 text-main-color-1 dark:text-light-main-color-1 hover:cursor-pointer" mod={mod} installedVersion={installedModVersion(key, mod)} isDependency={isDependency(mod)} isSelected={isSelected(mod)} onChange={val => onModChange(val, mod)} onWantInfo={onWantInfos} wantInfo={mod.name === moreInfoMod?.name} />)}
|
||||
{modsMap.get(key).map(mod => mod.name?.toLowerCase().includes(filter) && (
|
||||
<ModItem
|
||||
key={mod.name}
|
||||
className="contents bg-light-main-color-3 dark:bg-main-color-1 text-main-color-1 dark:text-light-main-color-1 hover:cursor-pointer"
|
||||
mod={mod} installedVersion={installedModVersion(key, mod)}
|
||||
isDependency={isDependency(mod)}
|
||||
isSelected={isSelected(mod)}
|
||||
onChange={val => onModChange(val, mod)}
|
||||
onWantInfo={onWantInfos}
|
||||
wantInfo={mod.name === moreInfoMod?.name}
|
||||
disabled={disabled}
|
||||
onUninstall={() => uninstallMod?.(mod)} />
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
)}
|
||||
|
||||
@@ -9,8 +9,7 @@ import { BsmButton } from "renderer/components/shared/bsm-button.component";
|
||||
import BeatWaitingImg from "../../../../../../assets/images/apngs/beat-waiting.png";
|
||||
import BeatConflictImg from "../../../../../../assets/images/apngs/beat-conflict.png";
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
import { skip, filter } from "rxjs/operators";
|
||||
import { Subscription, lastValueFrom, noop } from "rxjs";
|
||||
import { lastValueFrom } from "rxjs";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { LinkOpenerService } from "renderer/services/link-opener.service";
|
||||
import { useInView } from "framer-motion";
|
||||
@@ -20,6 +19,8 @@ import { OsDiagnosticService } from "renderer/services/os-diagnostic.service";
|
||||
import { lt } from "semver";
|
||||
import { useService } from "renderer/hooks/use-service.hook";
|
||||
import { NotificationService } from "renderer/services/notification.service";
|
||||
import { noop } from "shared/helpers/function.helpers";
|
||||
import { UninstallAllModsModal } from "renderer/components/modal/modal-types/uninstall-all-mods-modal.component";
|
||||
|
||||
export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion; onDisclamerDecline: () => void }) {
|
||||
const ACCEPTED_DISCLAIMER_KEY = "accepted-mods-disclaimer";
|
||||
@@ -39,7 +40,8 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
|
||||
const [moreInfoMod, setMoreInfoMod] = useState(null as Mod);
|
||||
const [reinstallAllMods, setReinstallAllMods] = useState(false);
|
||||
const isOnline = useObservable(() => os.isOnline$);
|
||||
const installing = useObservable(() => modsManager.isInstalling$);
|
||||
const [installing, setInstalling] = useState(false);
|
||||
const [uninstalling, setUninstalling] = useState(false);
|
||||
|
||||
const downloadRef = useRef(null);
|
||||
const [downloadWith, setDownloadWidth] = useState(0);
|
||||
@@ -103,11 +105,34 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
|
||||
return;
|
||||
}
|
||||
|
||||
modsManager.installMods(modsToInstall, version).then(() => {
|
||||
setInstalling(() => true)
|
||||
lastValueFrom(modsManager.installMods(modsToInstall, version)).then(() => {
|
||||
loadMods();
|
||||
}).catch(noop).finally(() => setInstalling(() => false));
|
||||
};
|
||||
|
||||
const uninstallMod = (mod: Mod): void => {
|
||||
setUninstalling(() => true);
|
||||
lastValueFrom(modsManager.uninstallMod(mod, version)).catch(noop).finally(() => {
|
||||
loadMods();
|
||||
setUninstalling(() => false);
|
||||
});
|
||||
};
|
||||
|
||||
const uninstallAllMods = async () => {
|
||||
const res = await modals.openModal(UninstallAllModsModal, {data: version});
|
||||
|
||||
if (res.exitCode !== ModalExitCode.COMPLETED) {
|
||||
return;
|
||||
}
|
||||
|
||||
setUninstalling(() => true);
|
||||
lastValueFrom(modsManager.uninstallAllMods(version)).catch(noop).finally(() => {
|
||||
loadMods();
|
||||
setUninstalling(() => false);
|
||||
})
|
||||
};
|
||||
|
||||
const loadMods = (): Promise<void> => {
|
||||
if (os.isOffline) {
|
||||
return Promise.resolve();
|
||||
@@ -128,7 +153,6 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const subs: Subscription[] = [];
|
||||
|
||||
if(!isVisible || !isOnline){
|
||||
return noop();
|
||||
@@ -153,22 +177,12 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
|
||||
}
|
||||
|
||||
loadMods();
|
||||
|
||||
subs.push(
|
||||
modsManager.isUninstalling$.pipe(
|
||||
skip(1),
|
||||
filter(uninstalling => !uninstalling)
|
||||
).subscribe(() => {
|
||||
loadMods();
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
return () => {
|
||||
setMoreInfoMod(null);
|
||||
setModsAvailable(null);
|
||||
setModsInstalled(null);
|
||||
subs.forEach(s => s.unsubscribe());
|
||||
};
|
||||
}, [isVisible, isOnline, version]);
|
||||
|
||||
@@ -195,7 +209,7 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
|
||||
return (
|
||||
<>
|
||||
<div className="grow overflow-y-scroll w-full min-h-0 scrollbar-default p-0 m-0">
|
||||
<ModsGrid modsMap={modsAvailable} installed={modsInstalled} modsSelected={modsSelected} onModChange={handleModChange} moreInfoMod={moreInfoMod} onWantInfos={handleMoreInfo} />
|
||||
<ModsGrid modsMap={modsAvailable} installed={modsInstalled} modsSelected={modsSelected} onModChange={handleModChange} moreInfoMod={moreInfoMod} onWantInfos={handleMoreInfo} disabled={uninstalling || installing} uninstallMod={uninstallMod} uninstallAllMods={uninstallAllMods}/>
|
||||
</div>
|
||||
<div className="shrink-0 flex items-center justify-between px-3 py-2">
|
||||
<BsmButton className="flex items-center justify-center rounded-md px-1 h-8" text="pages.version-viewer.mods.buttons.more-infos" typeColor="cancel" withBar={false} disabled={!moreInfoMod} onClick={handleOpenMoreInfo} style={{ width: downloadWith }}/>
|
||||
|
||||
@@ -1,4 +1,24 @@
|
||||
export const defaultConfiguration: { [key in DefaultConfigKey]: any } = {
|
||||
import { BsvSearchOrder } from "shared/models/maps/beat-saver.model";
|
||||
|
||||
// NOTE: To refactor. Rename to LocalStorageConfigKeyValues since these are stored in the
|
||||
// localStorage in the browser, and for readability
|
||||
export interface DefaultConfigKeyValues {
|
||||
"first-color": string;
|
||||
"second-color": string;
|
||||
theme: ThemeConfig;
|
||||
language: string;
|
||||
supported_languages: string[];
|
||||
default_mods: string[];
|
||||
"default-shared-folders": string[];
|
||||
"playlist-sort-order": BsvSearchOrder;
|
||||
"map-sort-order": BsvSearchOrder;
|
||||
};
|
||||
|
||||
export type DefaultConfigKey = keyof DefaultConfigKeyValues;
|
||||
|
||||
export const defaultConfiguration: {
|
||||
[key in DefaultConfigKey]: DefaultConfigKeyValues[key]
|
||||
} = {
|
||||
"first-color": "#3b82ff",
|
||||
"second-color": "#ff4444",
|
||||
theme: "os",
|
||||
@@ -8,10 +28,9 @@ export const defaultConfiguration: { [key in DefaultConfigKey]: any } = {
|
||||
"default-shared-folders": [
|
||||
window.electron.path.join("Beat Saber_Data", "CustomLevels"),
|
||||
window.electron.path.join("Beat Saber_Data", "CustomWIPLevels"),
|
||||
"DLC"
|
||||
],
|
||||
"playlist-sort-order": BsvSearchOrder.Relevance,
|
||||
"map-sort-order": BsvSearchOrder.Latest,
|
||||
};
|
||||
|
||||
export type DefaultConfigKey = "first-color" | "second-color" | "theme" | "language" | "supported_languages" | "default_mods" | "default-shared-folders";
|
||||
|
||||
export type ThemeConfig = "dark" | "light" | "os";
|
||||
|
||||
@@ -136,7 +136,14 @@ export function SettingsPage() {
|
||||
}
|
||||
|
||||
const clearDownloadersSession = () => {
|
||||
if (!hasDownloaderSession) {
|
||||
return;
|
||||
}
|
||||
|
||||
steamDownloader.deleteSteamSession();
|
||||
notificationService.notifyInfo({
|
||||
title: "pages.settings.steam-and-oculus.logout-success",
|
||||
});
|
||||
loadDownloadersSession();
|
||||
}
|
||||
|
||||
@@ -207,7 +214,7 @@ export function SettingsPage() {
|
||||
lastValueFrom(installationLocationService.setInstallationFolder(fileChooserRes.filePaths[0], true)).then(res => {
|
||||
|
||||
progressBarService.complete();
|
||||
progressBarService.hide(true);
|
||||
progressBarService.hide();
|
||||
|
||||
setInstallationFolder(res);
|
||||
|
||||
@@ -219,7 +226,7 @@ export function SettingsPage() {
|
||||
});
|
||||
|
||||
}).catch((err: BsmException) => {
|
||||
progressBarService.hide(true);
|
||||
progressBarService.hide();
|
||||
|
||||
if (err?.code === "COPY_TO_SUBPATH") {
|
||||
notificationService.notifyError({ title: "notifications.settings.move-folder.errors.titles.transfer-failed", desc: "notifications.settings.move-folder.errors.descs.COPY_TO_SUBPATH", duration: 10_000 });
|
||||
|
||||
Vendored
+5
@@ -1,3 +1,5 @@
|
||||
import { webUtils } from "electron";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
electron: {
|
||||
@@ -13,6 +15,9 @@ declare global {
|
||||
basename: (path: string) => string;
|
||||
join: (...args: string[]) => string;
|
||||
};
|
||||
webUtils: {
|
||||
getPathForFile: typeof webUtils.getPathForFile;
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { map } from "rxjs/operators";
|
||||
import { IpcService } from "./ipc.service";
|
||||
import { ProgressBarService } from "./progress-bar.service";
|
||||
import { ModalService } from "renderer/services/modale.service";
|
||||
import { ChangelogModal } from "renderer/components/modal/modal-types/chabgelog-modal/changelog-modal.component";
|
||||
import { ConfigurationService } from "./configuration.service";
|
||||
import { Observable, lastValueFrom } from "rxjs";
|
||||
import { Progression } from "main/helpers/fs.helpers";
|
||||
|
||||
|
||||
export interface Changelog {
|
||||
@@ -22,8 +22,6 @@ export class AutoUpdaterService {
|
||||
private progressService: ProgressBarService;
|
||||
private ipcService: IpcService;
|
||||
|
||||
private downloadProgress$: Observable<number>;
|
||||
|
||||
private modal: ModalService;
|
||||
|
||||
private configurationService: ConfigurationService;
|
||||
@@ -42,22 +40,28 @@ export class AutoUpdaterService {
|
||||
this.ipcService = IpcService.getInstance();
|
||||
this.modal = ModalService.getInstance();
|
||||
this.configurationService = ConfigurationService.getInstance();
|
||||
|
||||
this.downloadProgress$ = this.ipcService.watch<number>("update-download-progress").pipe(map(res => (res.success ? res.data : 0)));
|
||||
}
|
||||
|
||||
public isUpdateAvailable(): Promise<boolean> {
|
||||
return lastValueFrom(this.ipcService.sendV2("check-update")).catch(() => false);
|
||||
}
|
||||
|
||||
public downloadUpdate(): Promise<boolean> {
|
||||
const promise = lastValueFrom(this.ipcService.sendV2("download-update")).then(() => true).catch(() => false);
|
||||
this.progressService.show(this.downloadProgress$, true);
|
||||
return promise;
|
||||
public downloadUpdate(): Observable<Progression> {
|
||||
return new Observable<Progression>(obs => {
|
||||
const download$ = this.ipcService.sendV2("download-update");
|
||||
this.progressService.show(download$);
|
||||
|
||||
const sub = download$.subscribe(obs);
|
||||
|
||||
return () => {
|
||||
sub.unsubscribe();
|
||||
this.progressService.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public quitAndInstall() {
|
||||
lastValueFrom(this.ipcService.sendV2("install-update"));
|
||||
public quitAndInstall(): Promise<void> {
|
||||
return lastValueFrom(this.ipcService.sendV2("install-update"));
|
||||
}
|
||||
|
||||
public getLastAppVersion(): string {
|
||||
@@ -104,6 +108,10 @@ export class AutoUpdaterService {
|
||||
}
|
||||
|
||||
public async showChangelog(version:string): Promise<void>{
|
||||
if (version.toLowerCase().includes("alpha")) {
|
||||
throw new Error("Alpha version, skipping changelog");
|
||||
}
|
||||
|
||||
const changelog = await this.getChangelogVersion(version);
|
||||
|
||||
this.modal.openModal(ChangelogModal, {data: changelog});
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
import { UninstallAllModsModal } from "renderer/components/modal/modal-types/uninstall-all-mods-modal.component";
|
||||
import { UninstallModModal } from "renderer/components/modal/modal-types/uninstall-mod-modal.component";
|
||||
import { Observable, BehaviorSubject, lastValueFrom } from "rxjs";
|
||||
import { map } from "rxjs/operators";
|
||||
import { Observable, BehaviorSubject, throwError, of } from "rxjs";
|
||||
import { catchError, tap } from "rxjs/operators";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { Mod, ModInstallProgression } from "shared/models/mods";
|
||||
import { ProgressionInterface } from "shared/models/progress-bar";
|
||||
import { Mod } from "shared/models/mods";
|
||||
import { IpcService } from "./ipc.service";
|
||||
import { ModalExitCode, ModalService } from "./modale.service";
|
||||
import { NotificationType } from "../../shared/models/notification/notification.model";
|
||||
import { OsDiagnosticService } from "./os-diagnostic.service";
|
||||
import { ProgressBarService } from "./progress-bar.service";
|
||||
import { NotificationService } from "./notification.service";
|
||||
import { CustomError } from "shared/models/exceptions/custom-error.class";
|
||||
import { Progression } from "main/helpers/fs.helpers";
|
||||
|
||||
export class BsModsManagerService {
|
||||
private static instance: BsModsManagerService;
|
||||
@@ -20,11 +14,8 @@ export class BsModsManagerService {
|
||||
|
||||
private readonly ipcService: IpcService;
|
||||
private readonly progressBar: ProgressBarService;
|
||||
private readonly modals: ModalService;
|
||||
private readonly notifications: NotificationService;
|
||||
private readonly os: OsDiagnosticService;
|
||||
|
||||
public readonly isInstalling$: BehaviorSubject<boolean> = new BehaviorSubject(false);
|
||||
public readonly isUninstalling$: BehaviorSubject<boolean> = new BehaviorSubject(false);
|
||||
|
||||
public static getInstance(): BsModsManagerService {
|
||||
@@ -37,9 +28,7 @@ export class BsModsManagerService {
|
||||
private constructor() {
|
||||
this.ipcService = IpcService.getInstance();
|
||||
this.progressBar = ProgressBarService.getInstance();
|
||||
this.modals = ModalService.getInstance();
|
||||
this.notifications = NotificationService.getInstance();
|
||||
this.os = OsDiagnosticService.getInstance();
|
||||
}
|
||||
|
||||
public getAvailableMods(version: BSVersion): Observable<Mod[]> {
|
||||
@@ -50,91 +39,98 @@ export class BsModsManagerService {
|
||||
return this.ipcService.sendV2("get-installed-mods", version);
|
||||
}
|
||||
|
||||
public installMods(mods: Mod[], version: BSVersion): Promise<void> {
|
||||
if (this.os.isOffline) {
|
||||
this.notifications.notifyError({
|
||||
title: "notifications.shared.errors.titles.no-internet",
|
||||
desc: "notifications.shared.errors.msg.no-internet",
|
||||
});
|
||||
return Promise.resolve();
|
||||
}
|
||||
public installMods(mods: Mod[], version: BSVersion): Observable<Progression> {
|
||||
|
||||
if (!this.progressBar.require()) {
|
||||
return Promise.resolve();
|
||||
return throwError(() => new Error("Action already in progress"));
|
||||
}
|
||||
|
||||
const progress$: Observable<ProgressionInterface> = this.ipcService.watch<ModInstallProgression>("mod-installed").pipe(
|
||||
map(res => {
|
||||
return { progression: res.data.progression, label: res.data.name } as ProgressionInterface;
|
||||
})
|
||||
);
|
||||
return new Observable<Progression>(obs => {
|
||||
const install$ = this.ipcService.sendV2("install-mods", { mods, version });
|
||||
this.progressBar.show(install$.pipe(catchError(() => of({ current: 0, total: 0} as Progression))), { paddingLeft: "190px", paddingRight: "190px", bottom: "20px" });
|
||||
|
||||
this.progressBar.show(progress$, true, { paddingLeft: "190px", paddingRight: "190px", bottom: "20px" });
|
||||
const sub = install$.pipe(
|
||||
tap({
|
||||
error: err => {
|
||||
if(err?.code){
|
||||
this.notifications.notifyError({ title: "notifications.types.error", desc: `notifications.mods.install-mods.msg.errors.${err?.code}`, duration: this.NOTIFICATION_DURATION });
|
||||
} else {
|
||||
this.notifications.notifyError({ title: "notifications.types.error", desc: err?.message ?? err, duration: this.NOTIFICATION_DURATION });
|
||||
}
|
||||
},
|
||||
complete: () => {
|
||||
this.notifications.notifySuccess({ title: "notifications.mods.install-mods.titles.success", desc: "notifications.mods.install-mods.msg.success", duration: this.NOTIFICATION_DURATION });
|
||||
},
|
||||
})
|
||||
).subscribe(obs);
|
||||
|
||||
this.isInstalling$.next(true);
|
||||
|
||||
return lastValueFrom(this.ipcService.sendV2("install-mods", { mods, version })).then(res => {
|
||||
const isFullyInstalled = res.nbInstalledMods === res.nbModsToInstall;
|
||||
|
||||
const title = `notifications.mods.install-mods.titles.${isFullyInstalled ? "success" : "warning"}`;
|
||||
const desc = `notifications.mods.install-mods.msg.${isFullyInstalled ? "success" : "warning"}`;
|
||||
|
||||
this.notifications.notify({ type: isFullyInstalled ? NotificationType.SUCCESS : NotificationType.WARNING, title, desc, duration: this.NOTIFICATION_DURATION });
|
||||
}).catch((e: CustomError) => {
|
||||
this.notifications.notifyError({ title: "notifications.types.error", desc: `notifications.mods.install-mods.msg.errors.${e?.code}`, duration: this.NOTIFICATION_DURATION });
|
||||
}).finally(() => {
|
||||
this.isInstalling$.next(false);
|
||||
this.progressBar.hide();
|
||||
})
|
||||
}
|
||||
public async uninstallMod(mod: Mod, version: BSVersion): Promise<void> {
|
||||
if (!this.progressBar.require()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const modalRes = await this.modals.openModal(UninstallModModal, {data: mod});
|
||||
|
||||
if (modalRes.exitCode !== ModalExitCode.COMPLETED) {
|
||||
return;
|
||||
}
|
||||
|
||||
const progress$ = this.ipcService.watch<ModInstallProgression>("mod-uninstalled").pipe(map(res => res.data.progression));
|
||||
this.progressBar.show(progress$, true, { paddingLeft: "190px", paddingRight: "190px", bottom: "20px" });
|
||||
|
||||
this.isUninstalling$.next(true);
|
||||
|
||||
return lastValueFrom(this.ipcService.sendV2("uninstall-mods", { mods: [mod], version })).then(() => {
|
||||
this.notifications.notifySuccess({ title: "notifications.mods.uninstall-mod.titles.success", duration: this.NOTIFICATION_DURATION });
|
||||
}).catch((e: CustomError) => {
|
||||
this.notifications.notifyError({ title: "notifications.types.error", desc: `notifications.mods.uninstall-mod.msg.errors.${e?.code}`, duration: this.NOTIFICATION_DURATION });
|
||||
}).finally(() => {
|
||||
this.isUninstalling$.next(false);
|
||||
this.progressBar.hide();
|
||||
})
|
||||
return () => {
|
||||
sub.unsubscribe();
|
||||
this.progressBar.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async uninstallAllMods(version: BSVersion) {
|
||||
public uninstallMod(mod: Mod, version: BSVersion): Observable<Progression> {
|
||||
if (!this.progressBar.require()) {
|
||||
return;
|
||||
return throwError(() => new Error("Action already in progress"));
|
||||
}
|
||||
|
||||
const modalRes = await this.modals.openModal(UninstallAllModsModal, {data: version});
|
||||
return new Observable<Progression>(obs => {
|
||||
const uninstall$ = this.ipcService.sendV2("uninstall-mods", { mods: [mod], version });
|
||||
this.progressBar.show(uninstall$.pipe(catchError(() => of({ current: 0, total: 0} as Progression))), { paddingLeft: "190px", paddingRight: "190px", bottom: "20px" });
|
||||
|
||||
if (modalRes.exitCode !== ModalExitCode.COMPLETED) {
|
||||
return;
|
||||
const sub = uninstall$.pipe(
|
||||
tap({
|
||||
error: err => {
|
||||
if(err?.code){
|
||||
this.notifications.notifyError({ title: "notifications.types.error", desc: `notifications.mods.uninstall-mod.msg.errors.${err?.code}`, duration: this.NOTIFICATION_DURATION });
|
||||
} else {
|
||||
this.notifications.notifyError({ title: "notifications.types.error", desc: err?.message ?? err, duration: this.NOTIFICATION_DURATION });
|
||||
}
|
||||
},
|
||||
complete: () => {
|
||||
this.notifications.notifySuccess({ title: "notifications.mods.uninstall-mod.titles.success", duration: this.NOTIFICATION_DURATION });
|
||||
},
|
||||
})
|
||||
).subscribe(obs);
|
||||
|
||||
return () => {
|
||||
sub.unsubscribe();
|
||||
this.progressBar.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public uninstallAllMods(version: BSVersion): Observable<Progression> {
|
||||
if (!this.progressBar.require()) {
|
||||
return throwError(() => new Error("Action already in progress"));
|
||||
}
|
||||
|
||||
const progress$ = this.ipcService.watch<ModInstallProgression>("mod-uninstalled").pipe(map(res => res.data.progression));
|
||||
this.progressBar.show(progress$, true, { paddingLeft: "190px", paddingRight: "190px", bottom: "20px" });
|
||||
return new Observable<Progression>(obs => {
|
||||
const uninstall$ = this.ipcService.sendV2("uninstall-all-mods", version);
|
||||
this.progressBar.show(uninstall$.pipe(catchError(() => of({ current: 0, total: 0} as Progression))), { paddingLeft: "190px", paddingRight: "190px", bottom: "20px" });
|
||||
|
||||
this.isUninstalling$.next(true);
|
||||
return lastValueFrom(this.ipcService.sendV2("uninstall-all-mods", version)).then(() => {
|
||||
this.notifications.notifySuccess({ title: "notifications.mods.uninstall-all-mods.titles.success", desc: "notifications.mods.uninstall-all-mods.msg.success", duration: this.NOTIFICATION_DURATION });
|
||||
}).catch((e: CustomError) => {
|
||||
this.notifications.notifyError({ title: "notifications.types.error", desc: `notifications.mods.uninstall-all-mods.msg.errors.${e?.code}`, duration: this.NOTIFICATION_DURATION });
|
||||
}).finally(() => {
|
||||
this.isUninstalling$.next(false);
|
||||
this.progressBar.hide();
|
||||
})
|
||||
const sub = uninstall$.pipe(
|
||||
tap({
|
||||
error: err => {
|
||||
if(err?.code){
|
||||
this.notifications.notifyError({ title: "notifications.types.error", desc: `notifications.mods.uninstall-all-mods.msg.errors.${err?.code}`, duration: this.NOTIFICATION_DURATION });
|
||||
} else {
|
||||
this.notifications.notifyError({ title: "notifications.types.error", desc: err?.message ?? err, duration: this.NOTIFICATION_DURATION });
|
||||
}
|
||||
},
|
||||
complete: () => {
|
||||
this.notifications.notifySuccess({ title: "notifications.mods.uninstall-all-mods.titles.success", desc: "notifications.mods.uninstall-all-mods.msg.success", duration: this.NOTIFICATION_DURATION });
|
||||
},
|
||||
})
|
||||
).subscribe(obs);
|
||||
|
||||
return () => {
|
||||
sub.unsubscribe();
|
||||
this.progressBar.hide();
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ export class OculusDownloaderService extends AbstractBsDownloaderService impleme
|
||||
|
||||
private handleDownload(download: Observable<Progression<BSVersion>>, ingoreErrorCodes?: string[]): Observable<Progression<BSVersion>> {
|
||||
const progress$ = download.pipe(map(progress => (progress.current / progress.total) * 100), catchError(() => of(0)));
|
||||
this.progressBar.show(progress$, true);
|
||||
this.progressBar.show(progress$);
|
||||
|
||||
const subs: Subscription[] = [];
|
||||
|
||||
@@ -106,7 +106,7 @@ export class OculusDownloaderService extends AbstractBsDownloaderService impleme
|
||||
}
|
||||
|
||||
return bsVersion;
|
||||
}).finally(() => this.progressBar.hide(true));
|
||||
}).finally(() => this.progressBar.hide());
|
||||
}
|
||||
|
||||
public downloadBsVersion(version: BSVersion): Promise<BSVersion> {
|
||||
|
||||
@@ -13,6 +13,7 @@ import { SteamMobileApproveModal } from "renderer/components/modal/modal-types/b
|
||||
import { DownloaderServiceInterface } from "./bs-store-downloader.interface";
|
||||
import { AbstractBsDownloaderService } from "./abstract-bs-downloader.service";
|
||||
import { addFilterStringLog } from "renderer";
|
||||
import { CustomError } from "shared/models/exceptions/custom-error.class";
|
||||
|
||||
export class SteamDownloaderService extends AbstractBsDownloaderService implements DownloaderServiceInterface{
|
||||
|
||||
@@ -120,11 +121,11 @@ export class SteamDownloaderService extends AbstractBsDownloaderService implemen
|
||||
return subs;
|
||||
}
|
||||
|
||||
private hanndleErrorEvent(errorEvent: DepotDownloaderEvent) {
|
||||
private hanndleErrorEvent(errorEvent: CustomError) {
|
||||
const handledErrors = Object.values(DepotDownloaderErrorEvent);
|
||||
|
||||
if(handledErrors.includes(errorEvent?.subType as DepotDownloaderErrorEvent)){
|
||||
return this.notificationService.notifyError({title: "notifications.types.error", desc: `notifications.bs-download.steam-download.errors.msg.${errorEvent.subType}`, duration: 10_000});
|
||||
if(handledErrors.includes(errorEvent?.code as DepotDownloaderErrorEvent)){
|
||||
return this.notificationService.notifyError({title: "notifications.types.error", desc: `notifications.bs-download.steam-download.errors.msg.${errorEvent.code}`, duration: 10_000});
|
||||
}
|
||||
|
||||
return this.notificationService.notifyError({title: "notifications.types.error", desc: `notifications.bs-download.steam-download.errors.msg.${DepotDownloaderErrorEvent.Unknown}`, duration: 10_000});
|
||||
@@ -189,7 +190,7 @@ export class SteamDownloaderService extends AbstractBsDownloaderService implemen
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
this.progressBarService.show(this.downloadProgress$, true);
|
||||
this.progressBarService.show(this.downloadProgress$);
|
||||
|
||||
const downloadPromise = (async () => {
|
||||
|
||||
@@ -225,7 +226,7 @@ export class SteamDownloaderService extends AbstractBsDownloaderService implemen
|
||||
|
||||
return downloadPromise.then(() => {}).finally(() => {
|
||||
this.downloadProgress$.next(0);
|
||||
this.progressBarService.hide(true);
|
||||
this.progressBarService.hide();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { EditVersionModal } from "renderer/components/modal/modal-types/edit-ver
|
||||
import { popElement } from "shared/helpers/array.helpers";
|
||||
import { ImportVersionModal } from "renderer/components/modal/modal-types/import-version-modal.component";
|
||||
import { Progression } from "main/helpers/fs.helpers";
|
||||
import { CustomError } from "shared/models/exceptions/custom-error.class";
|
||||
|
||||
export class BSVersionManagerService {
|
||||
private static instance: BSVersionManagerService;
|
||||
@@ -77,10 +78,19 @@ export class BSVersionManagerService {
|
||||
this.askInstalledVersions();
|
||||
return res;
|
||||
}).catch(e => {
|
||||
this.notification.notifyError({
|
||||
title: `notifications.custom-version.errors.titles.${e.error.title}`,
|
||||
...(e.error.message && { desc: `notifications.custom-version.errors.msg.${e.error.message}` }),
|
||||
});
|
||||
|
||||
const knownErrorTitlesCodes = ["CantEditSteam", "VersionAlreadExist", "CantRename"];
|
||||
const knownErrorMessagesCodes = ["CantEditSteam"];
|
||||
|
||||
if(knownErrorTitlesCodes.includes(e.code)){
|
||||
this.notification.notifyError({
|
||||
title: `notifications.custom-version.errors.titles.${e.code}`,
|
||||
desc: knownErrorMessagesCodes.includes(e.code) ? `notifications.custom-version.errors.msg.${e.code}` : null
|
||||
});
|
||||
} else {
|
||||
this.notification.notifyError({ title: `notifications.custom-version.errors.titles.UnknownError` });
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
}
|
||||
@@ -103,14 +113,19 @@ export class BSVersionManagerService {
|
||||
this.notification.notifySuccess({ title: "notifications.custom-version.success.titles.CloningFinished" });
|
||||
this.askInstalledVersions();
|
||||
return res;
|
||||
}).catch(e => {
|
||||
this.notification.notifyError({
|
||||
title: `notifications.custom-version.errors.titles.${e.error.title}`,
|
||||
...(e.error.message && { desc: `notifications.custom-version.errors.msg.${e.error.message}` }),
|
||||
});
|
||||
}).catch((e: CustomError) => {
|
||||
|
||||
const knownErrorTitlesCodes = ["VersionAlreadExist", "CantClone"];
|
||||
|
||||
if(knownErrorTitlesCodes.includes(e.code)){
|
||||
this.notification.notifyError({ title: `notifications.custom-version.errors.titles.${e.code}` });
|
||||
} else {
|
||||
this.notification.notifyError({ title: `notifications.custom-version.errors.titles.UnknownError` });
|
||||
}
|
||||
|
||||
return null;
|
||||
}).finally(() => {
|
||||
this.progressBar.hide(true)
|
||||
this.progressBar.hide()
|
||||
});
|
||||
}
|
||||
|
||||
@@ -152,7 +167,7 @@ export class BSVersionManagerService {
|
||||
obs.error(err)
|
||||
}).finally(() => {
|
||||
this.askInstalledVersions();
|
||||
this.progressBar.hide(true);
|
||||
this.progressBar.hide();
|
||||
});
|
||||
|
||||
return () => {
|
||||
@@ -162,7 +177,7 @@ export class BSVersionManagerService {
|
||||
shareReplay({ bufferSize: 1, refCount: true })
|
||||
);
|
||||
|
||||
this.progressBar.show(obs$, true);
|
||||
this.progressBar.show(obs$);
|
||||
|
||||
return obs$;
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export class ConfigurationService {
|
||||
public get<Type>(key: string | DefaultConfigKey): Type {
|
||||
const rawValue = (window.sessionStorage.getItem(key) ?? window.localStorage.getItem(key));
|
||||
const tryParse = tryit<Type>(() => JSON.parse(rawValue));
|
||||
|
||||
|
||||
const res = (tryParse.error ? rawValue : tryParse.result) as Type;
|
||||
|
||||
if(!res && Object.keys(defaultConfiguration).includes(key)){
|
||||
@@ -40,7 +40,7 @@ export class ConfigurationService {
|
||||
return res;
|
||||
}
|
||||
|
||||
public set(key: string, value: unknown, persistant = true) {
|
||||
public set(key: string | DefaultConfigKey, value: unknown, persistant = true) {
|
||||
|
||||
if(value != null){
|
||||
this.getPropperStorage(persistant).setItem(key, JSON.stringify(value));
|
||||
@@ -51,7 +51,7 @@ export class ConfigurationService {
|
||||
this.emitChange(key);
|
||||
}
|
||||
|
||||
public delete(key: string) {
|
||||
public delete(key: string | DefaultConfigKey) {
|
||||
window.localStorage.removeItem(key);
|
||||
window.sessionStorage.removeItem(key);
|
||||
this.emitChange(key);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { defaultIfEmpty, share } from "rxjs/operators";
|
||||
import { Observable, ReplaySubject, identity } from "rxjs";
|
||||
import { IpcRequest, IpcResponse } from "shared/models/ipc";
|
||||
import { IpcRequest } from "shared/models/ipc";
|
||||
import { deserializeError } from 'serialize-error';
|
||||
import { IpcCompleteChannel, IpcErrorChannel, IpcTearDownChannel } from "shared/models/ipc/ipc-response.interface";
|
||||
import { IpcChannels, IpcRequestType, IpcResponseType } from "shared/models/ipc/ipc-routes";
|
||||
@@ -8,8 +8,6 @@ import { IpcChannels, IpcRequestType, IpcResponseType } from "shared/models/ipc/
|
||||
export class IpcService {
|
||||
private static instance: IpcService;
|
||||
|
||||
private readonly channelObservables: Map<string, Observable<unknown>>;
|
||||
|
||||
public static getInstance(): IpcService {
|
||||
if (!IpcService.instance) {
|
||||
IpcService.instance = new IpcService();
|
||||
@@ -17,53 +15,12 @@ export class IpcService {
|
||||
return IpcService.instance;
|
||||
}
|
||||
|
||||
private constructor() {
|
||||
this.channelObservables = new Map<string, Observable<IpcResponse<unknown>>>();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use sendV2 instead
|
||||
*/
|
||||
public send<T, U = unknown>(channel: string, request?: IpcRequest<U>): Promise<IpcResponse<T>> {
|
||||
if (!request) {
|
||||
request = { args: null, responceChannel: null };
|
||||
}
|
||||
if (!request.responceChannel) {
|
||||
request.responceChannel = `${channel}_responce_${new Date().getTime()}`;
|
||||
}
|
||||
|
||||
const promise = new Promise<IpcResponse<T>>(resolve => {
|
||||
window.electron.ipcRenderer.once(request.responceChannel, (response: IpcResponse<T>) => resolve(response));
|
||||
});
|
||||
|
||||
window.electron.ipcRenderer.sendMessage(channel, request);
|
||||
|
||||
return promise;
|
||||
}
|
||||
private constructor() {}
|
||||
|
||||
public sendLazy<T = unknown>(channel: string, request?: IpcRequest<T>): void {
|
||||
window.electron.ipcRenderer.sendMessage(channel, request);
|
||||
}
|
||||
|
||||
// Also need a rework
|
||||
public watch<T>(channel: string): Observable<T> {
|
||||
if (this.channelObservables.has(channel)) {
|
||||
return this.channelObservables.get(channel) as Observable<T>;
|
||||
}
|
||||
|
||||
const obs = new Observable<T>(observer => {
|
||||
window.electron.ipcRenderer.on(channel, (res) => {
|
||||
observer.next(res);
|
||||
});
|
||||
});
|
||||
|
||||
this.channelObservables.set(channel, obs);
|
||||
|
||||
return obs;
|
||||
}
|
||||
|
||||
// TODO : Convert all IPCs calls to V2
|
||||
|
||||
public sendV2<C extends IpcChannels>(channel: C, data?: IpcRequestType<C>, defaultValue?: IpcResponseType<C>): Observable<IpcResponseType<C>> {
|
||||
|
||||
const request: IpcRequest<IpcRequestType<C>> = {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DownloadMapsModal } from "renderer/components/modal/modal-types/download-maps-modal.component";
|
||||
import { map, filter } from "rxjs/operators";
|
||||
import { map, filter, share } from "rxjs/operators";
|
||||
import { BehaviorSubject, timer, Observable, lastValueFrom } from "rxjs";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { ModalResponse, ModalService } from "./modale.service";
|
||||
@@ -30,9 +30,10 @@ export class MapsDownloaderService {
|
||||
private readonly mapsQueue$: BehaviorSubject<MapDownload[]> = new BehaviorSubject([]);
|
||||
private readonly currentDownload$: BehaviorSubject<MapDownload> = new BehaviorSubject(null);
|
||||
private queueMaxLenght = 0;
|
||||
private downloadedListerners: ((map: BsmLocalMap, version: BSVersion) => void)[] = [];
|
||||
public readonly progressBarStyle: CSSProperties = { zIndex: 100000, position: "fixed", bottom: "10px", right: 0 };
|
||||
|
||||
private _lastDownloadedMap$: Observable<{map: BsmLocalMap, version?: BSVersion}>;
|
||||
|
||||
private constructor() {
|
||||
this.modals = ModalService.getInstance();
|
||||
this.progressBar = ProgressBarService.getInstance();
|
||||
@@ -43,14 +44,10 @@ export class MapsDownloaderService {
|
||||
this.mapsQueue$.pipe(filter(queue => queue.length === 0)).subscribe(() => {
|
||||
this.queueMaxLenght = 0;
|
||||
});
|
||||
|
||||
this.ipc.watch<{map: BsmLocalMap, version?: BSVersion}>("map-downloaded").subscribe((data) => {
|
||||
this.downloadedListerners.forEach(func => func(data.map, data.version));
|
||||
});
|
||||
}
|
||||
|
||||
private async startDownloadMaps() {
|
||||
this.progressBar.show(this.downloadProgress$, true, this.progressBarStyle);
|
||||
this.progressBar.show(this.downloadProgress$, this.progressBarStyle);
|
||||
|
||||
await timer(2000).toPromise();
|
||||
|
||||
@@ -67,14 +64,14 @@ export class MapsDownloaderService {
|
||||
await timer(500).toPromise();
|
||||
|
||||
this.currentDownload$.next(null);
|
||||
this.progressBar.hide(true);
|
||||
this.progressBar.hide();
|
||||
}
|
||||
|
||||
private downloadMap(map: BsvMapDetail, version: BSVersion): Observable<BsmLocalMap> {
|
||||
if (this.os.isOffline) {
|
||||
return null;
|
||||
}
|
||||
return this.ipc.sendV2("download-map", { map, version });
|
||||
return this.ipc.sendV2("bs-maps.download-map", { map, version });
|
||||
}
|
||||
|
||||
public async openDownloadMapModal(version?: BSVersion, ownedMaps: BsmLocalMap[] = []): Promise<ModalResponse<void>> {
|
||||
@@ -119,16 +116,19 @@ export class MapsDownloaderService {
|
||||
return this.mapsQueue$.asObservable();
|
||||
}
|
||||
|
||||
public addOnMapDownloadedListener(func: (map: BsmLocalMap, version: BSVersion) => void) {
|
||||
this.downloadedListerners.push(func);
|
||||
}
|
||||
public get lastDownloadedMap$(): Observable<{map: BsmLocalMap, version?: BSVersion}> {
|
||||
if(!this._lastDownloadedMap$){
|
||||
this._lastDownloadedMap$ = new Observable<{map: BsmLocalMap, version?: BSVersion}>(observer => {
|
||||
const sub = this.ipc.sendV2("last-downloaded-map").subscribe(observer);
|
||||
|
||||
public removeOnMapDownloadedListener(func: (map: BsmLocalMap, version: BSVersion) => void) {
|
||||
const funcIndex = this.downloadedListerners.indexOf(func);
|
||||
if (funcIndex < 0) {
|
||||
return;
|
||||
return () => {
|
||||
this._lastDownloadedMap$ = null;
|
||||
sub.unsubscribe();
|
||||
}
|
||||
}).pipe(share());
|
||||
}
|
||||
this.downloadedListerners.splice(funcIndex, 1);
|
||||
|
||||
return this._lastDownloadedMap$;
|
||||
}
|
||||
|
||||
public async oneClickInstallMap(map: BsvMapDetail): Promise<void> {
|
||||
@@ -145,3 +145,5 @@ export interface MapDownload {
|
||||
map: BsvMapDetail;
|
||||
version: BSVersion;
|
||||
}
|
||||
|
||||
export type MapsDownloadedListener = (map: BsmLocalMap, version: BSVersion) => void;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { LinkMapsModal } from "renderer/components/modal/modal-types/link-maps-modal.component";
|
||||
import { UnlinkMapsModal } from "renderer/components/modal/modal-types/unlink-maps-modal.component";
|
||||
import { Subject, Observable, of, lastValueFrom } from "rxjs";
|
||||
import { Subject, Observable, of, lastValueFrom, Subscription, throwError } from "rxjs";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { BsmLocalMapsProgress, BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
|
||||
import { IpcService } from "./ipc.service";
|
||||
@@ -9,10 +9,13 @@ import { DeleteMapsModal } from "renderer/components/modal/modal-types/delete-ma
|
||||
import { ProgressBarService } from "./progress-bar.service";
|
||||
import { NotificationService } from "./notification.service";
|
||||
import { ConfigurationService } from "./configuration.service";
|
||||
import { map, last, catchError } from "rxjs/operators";
|
||||
import { map, last, catchError, filter, distinctUntilChanged } from "rxjs/operators";
|
||||
import { ProgressionInterface } from "shared/models/progress-bar";
|
||||
import { FolderLinkState, VersionFolderLinkerService } from "./version-folder-linker.service";
|
||||
import { SongDetails } from "shared/models/maps";
|
||||
import { Progression } from "main/helpers/fs.helpers";
|
||||
import { DeleteDuplicateMapsModal } from "renderer/components/modal/modal-types/delete-duplicate-maps-modal.component";
|
||||
import equal from "fast-deep-equal";
|
||||
|
||||
export class MapsManagerService {
|
||||
private static instance: MapsManagerService;
|
||||
@@ -37,6 +40,8 @@ export class MapsManagerService {
|
||||
private readonly lastLinkedVersion$: Subject<BSVersion> = new Subject();
|
||||
private readonly lastUnlinkedVersion$: Subject<BSVersion> = new Subject();
|
||||
|
||||
private readonly lastImportedMap$: Subject<{ version: BSVersion, map: BsmLocalMap }> = new Subject();
|
||||
|
||||
private constructor() {
|
||||
this.ipcService = IpcService.getInstance();
|
||||
this.modal = ModalService.getInstance();
|
||||
@@ -99,10 +104,10 @@ export class MapsManagerService {
|
||||
const progress$ = this.ipcService.sendV2("delete-maps", maps ).pipe(map(progress => (progress.deleted / progress.total) * 100));
|
||||
|
||||
if (showProgressBar) {
|
||||
this.progressBar.show(progress$, true);
|
||||
this.progressBar.show(progress$);
|
||||
}
|
||||
|
||||
progress$.toPromise().finally(() => this.progressBar.hide(true));
|
||||
progress$.toPromise().finally(() => this.progressBar.hide());
|
||||
|
||||
return progress$
|
||||
.pipe(
|
||||
@@ -131,7 +136,7 @@ export class MapsManagerService {
|
||||
})
|
||||
);
|
||||
|
||||
this.progressBar.show(exportProgress$, true);
|
||||
this.progressBar.show(exportProgress$);
|
||||
|
||||
await exportProgress$
|
||||
.toPromise()
|
||||
@@ -142,10 +147,116 @@ export class MapsManagerService {
|
||||
// TODO TRANSLATE
|
||||
this.notifications.notifySuccess({ title: "Export terminé 🎉", duration: 3000 });
|
||||
this.progressBar.complete();
|
||||
this.progressBar.hide(true);
|
||||
this.progressBar.hide();
|
||||
});
|
||||
}
|
||||
|
||||
public deleteDuplicateMaps(maps: BsmLocalMap[]): Observable<Progression> {
|
||||
const hashMap = Object.groupBy(maps, m => m.hash);
|
||||
const mapsToDelete: BsmLocalMap[] = [];
|
||||
|
||||
for (const maps of Object.values(hashMap)) {
|
||||
if(!Array.isArray(maps)){ continue; }
|
||||
maps.sort((a, b) => b.path.length - a.path.length).pop(); // The longest paths are most likely to be the duplicate ones (path: '../Awesome maps (copy)')
|
||||
mapsToDelete.push(...maps);
|
||||
}
|
||||
|
||||
if(mapsToDelete.length === 0){
|
||||
this.notifications.notifySuccess({ title: "notifications.maps.no-duplicates-maps.title", desc: "notifications.maps.no-duplicates-maps.msg", duration: 4000 });
|
||||
return of({ current: 0, total: 0 });
|
||||
}
|
||||
|
||||
if(!this.progressBar.require()){
|
||||
return throwError(() => new Error("Progress bar is required to delete duplicate maps"));
|
||||
}
|
||||
|
||||
return new Observable<Progression>(obs => {
|
||||
let sub: Subscription;
|
||||
(async () => {
|
||||
const modalRes = await this.modal.openModal(DeleteDuplicateMapsModal, { data: { maps: mapsToDelete } });
|
||||
|
||||
if(modalRes.exitCode !== ModalExitCode.COMPLETED){
|
||||
throw new Error("Operation to delete duplicate maps as been canceled by the user");
|
||||
}
|
||||
|
||||
const progress$: Observable<Progression> = this.ipcService.sendV2("delete-maps", mapsToDelete).pipe(map(p => ({ total: p.total, current: p.deleted })));
|
||||
|
||||
this.progressBar.show(progress$);
|
||||
sub = progress$.subscribe(obs);
|
||||
|
||||
await lastValueFrom(progress$);
|
||||
})()
|
||||
.then(() => this.notifications.notifySuccess({ title: "notifications.maps.duplicates-maps-deleted.title", desc: "notifications.maps.duplicates-maps-deleted.msg", duration: 4000 }))
|
||||
.catch(e => obs.error(e))
|
||||
.finally(() => obs.complete());
|
||||
|
||||
return () => {
|
||||
if(sub){
|
||||
this.progressBar.hide();
|
||||
sub.unsubscribe();
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public importMaps(paths: string[], version?: BSVersion): Observable<Progression<BsmLocalMap>> {
|
||||
if(!this.progressBar.require()){
|
||||
return throwError(() => new Error("Another operation is already in progress (importMaps)"));
|
||||
}
|
||||
|
||||
return new Observable<Progression<BsmLocalMap>>(obs => {
|
||||
const subs: Subscription[] = [];
|
||||
(async () => {
|
||||
const import$ = this.ipcService.sendV2("bs-maps.import-maps", { paths, version });
|
||||
|
||||
this.progressBar.show(import$.pipe(
|
||||
catchError(() => of()),
|
||||
map(progress => ({
|
||||
progression: (progress.current / progress.total) * 100,
|
||||
label: progress.data?.songDetails?.name
|
||||
} as ProgressionInterface))
|
||||
));
|
||||
|
||||
subs.push(import$.subscribe(obs));
|
||||
|
||||
subs.push(import$.pipe(catchError(() => of()), map(progress => progress?.data), filter(Boolean), distinctUntilChanged((a, b) => equal(a, b))).subscribe({ next: map => {
|
||||
this.lastImportedMap$.next({ version, map });
|
||||
}}));
|
||||
|
||||
return lastValueFrom(import$);
|
||||
})()
|
||||
.then(progress => {
|
||||
if (progress.current === progress.total) {
|
||||
this.notifications.notifySuccess({
|
||||
title: "notifications.maps.import-map.titles.success",
|
||||
desc: "notifications.maps.import-map.msgs.success",
|
||||
});
|
||||
} else if (progress.current > 0) {
|
||||
this.notifications.notifySuccess({
|
||||
title: "notifications.maps.import-map.titles.success",
|
||||
desc: "notifications.maps.import-map.msgs.some-success",
|
||||
});
|
||||
}
|
||||
}).catch(err => {
|
||||
this.notifications.notifyError({
|
||||
title: "notifications.maps.import-map.titles.error",
|
||||
desc: ["invalid-zip"].includes(err?.code)
|
||||
? `notifications.maps.import-map.msgs.${err.code}`
|
||||
: "notifications.maps.import-map.msgs.unknown"
|
||||
});
|
||||
obs.error(err);
|
||||
})
|
||||
.finally(() => obs.complete());
|
||||
|
||||
return () => {
|
||||
if(subs.length){
|
||||
this.progressBar.hide();
|
||||
subs.forEach(s => s.unsubscribe());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public getMapsInfoFromHashs(hashs: string[]): Observable<SongDetails[]> {
|
||||
return this.ipcService.sendV2("get-maps-info-from-cache", hashs);
|
||||
}
|
||||
@@ -170,6 +281,10 @@ export class MapsManagerService {
|
||||
return this.lastUnlinkedVersion$.asObservable();
|
||||
}
|
||||
|
||||
public $onMapImported(version: BSVersion): Observable<BsmLocalMap> {
|
||||
return this.lastImportedMap$.pipe(filter(newMap => equal(newMap.version, version)), map(m => m.map));
|
||||
}
|
||||
|
||||
public $mapsFolderLinkState(version: BSVersion): Observable<FolderLinkState> {
|
||||
return this.linker.$folderLinkedState(version, MapsManagerService.RELATIVE_MAPS_FOLDER);
|
||||
}
|
||||
|
||||
@@ -53,13 +53,13 @@ export class ModelsDownloaderService {
|
||||
}),
|
||||
startWith({ progression: 0.1, label: download.model.name })
|
||||
);
|
||||
this.progress.show(progress$, true);
|
||||
this.progress.show(progress$);
|
||||
}
|
||||
|
||||
const downloaded = await lastValueFrom(download$);
|
||||
this.lastDownload$.next(downloaded.data);
|
||||
|
||||
this.progress.hide(true);
|
||||
this.progress.hide();
|
||||
|
||||
this.queue$.next(this.queue$.value.filter(m => m.model.hash !== download.model.hash));
|
||||
}
|
||||
@@ -127,7 +127,7 @@ export class ModelsDownloaderService {
|
||||
|
||||
this.progress.complete();
|
||||
await lastValueFrom(timer(500));
|
||||
this.progress.hide(true);
|
||||
this.progress.hide();
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ export class ModelsManagerService {
|
||||
})
|
||||
);
|
||||
|
||||
this.progressBar.show(exportProgress$, true);
|
||||
this.progressBar.show(exportProgress$);
|
||||
|
||||
lastValueFrom(exportProgress$)
|
||||
.then(() => {
|
||||
@@ -133,7 +133,7 @@ export class ModelsManagerService {
|
||||
this.notifications.notifyError({ title: "notifications.types.error", desc: "notifications.common.msg.error-occurred", duration: 3000 });
|
||||
})
|
||||
.finally(() => {
|
||||
this.progressBar.hide(true);
|
||||
this.progressBar.hide();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -182,7 +182,7 @@ export class ModelsManagerService {
|
||||
this.notifications.notifyError({ title: "notifications.types.error", desc: "notifications.common.msg.error-occurred", duration: 3000 });
|
||||
return [];
|
||||
})
|
||||
.finally(() => this.progressBar.hide(true));
|
||||
.finally(() => this.progressBar.hide());
|
||||
}
|
||||
|
||||
public isDeepLinksEnabled(): Promise<boolean> {
|
||||
|
||||
@@ -57,7 +57,7 @@ export class PlaylistDownloaderService {
|
||||
|
||||
canShowProgress = !this.progress.isVisible;
|
||||
if (canShowProgress) {
|
||||
this.progress.show(download$, true);
|
||||
this.progress.show(download$);
|
||||
}
|
||||
|
||||
await lastValueFrom(download$.pipe(tap(subscriber)));
|
||||
@@ -68,7 +68,7 @@ export class PlaylistDownloaderService {
|
||||
this._currentDownload$.next(null);
|
||||
this.downloadQueue$.next(this.downloadQueue$.value.filter(qInfo => !equal(qInfo.info.version, info.version) || !equal(qInfo.info.downloadSource, info.downloadSource)));
|
||||
if (canShowProgress) {
|
||||
this.progress.hide(true);
|
||||
this.progress.hide();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -115,11 +115,11 @@ export class PlaylistDownloaderService {
|
||||
const download$ = this.ipc.sendV2("one-click-install-playlist", bpListUrl);
|
||||
const progress$ = download$.pipe(map(data => (data.current / data.total) * 100));
|
||||
|
||||
this.progress.show(progress$, true);
|
||||
this.progress.show(progress$);
|
||||
|
||||
return download$.pipe(tap({
|
||||
error: () => this.progress.hide(true),
|
||||
complete: () => this.progress.hide(true)
|
||||
error: () => this.progress.hide(),
|
||||
complete: () => this.progress.hide()
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { IpcService } from "./ipc.service";
|
||||
import { Observable, lastValueFrom } from "rxjs";
|
||||
import { Observable, Subject, Subscription, catchError, distinctUntilChanged, filter, lastValueFrom, map, of } from "rxjs";
|
||||
import { FolderLinkState, VersionFolderLinkerService } from "./version-folder-linker.service";
|
||||
import { Progression } from "main/helpers/fs.helpers";
|
||||
import { LocalBPList, LocalBPListsDetails } from "shared/models/playlists/local-playlist.models";
|
||||
@@ -8,6 +8,11 @@ import { ModalExitCode, ModalService } from "./modale.service";
|
||||
import { UnlinkPlaylistModal } from "renderer/components/modal/modal-types/unlink-playlist-modal.component";
|
||||
import { LinkPlaylistModal } from "renderer/components/modal/modal-types/link-playlist-modal.component";
|
||||
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
|
||||
import { ProgressBarService } from "./progress-bar.service";
|
||||
import equal from "fast-deep-equal";
|
||||
import { noop } from "shared/helpers/function.helpers";
|
||||
import { NotificationService } from "./notification.service";
|
||||
import { CustomError } from "shared/models/exceptions/custom-error.class";
|
||||
|
||||
export class PlaylistsManagerService {
|
||||
private static instance: PlaylistsManagerService;
|
||||
@@ -21,14 +26,20 @@ export class PlaylistsManagerService {
|
||||
|
||||
public static readonly RELATIVE_PLAYLISTS_FOLDER = "Playlists";
|
||||
|
||||
private readonly lastPlaylistImported$ = new Subject<{ version: BSVersion, playlist: LocalBPListsDetails }>();
|
||||
|
||||
private readonly ipc: IpcService;
|
||||
private readonly linker: VersionFolderLinkerService;
|
||||
private readonly modal: ModalService;
|
||||
private readonly progressBar: ProgressBarService;
|
||||
private readonly notification: NotificationService;
|
||||
|
||||
private constructor() {
|
||||
this.ipc = IpcService.getInstance();
|
||||
this.linker = VersionFolderLinkerService.getInstance();
|
||||
this.modal = ModalService.getInstance();
|
||||
this.progressBar = ProgressBarService.getInstance();
|
||||
this.notification = NotificationService.getInstance();
|
||||
}
|
||||
|
||||
public getVersionPlaylistsDetails(version: BSVersion): Observable<Progression<LocalBPListsDetails[]>> {
|
||||
@@ -48,6 +59,69 @@ export class PlaylistsManagerService {
|
||||
});
|
||||
}
|
||||
|
||||
public importPlaylists(opt: { version?: BSVersion, paths: string[] }): Observable<Progression<LocalBPListsDetails>> {
|
||||
return new Observable<Progression<LocalBPListsDetails>>(obs => {
|
||||
if(!this.progressBar.require()){
|
||||
obs.error("A progression is already running");
|
||||
return noop;
|
||||
}
|
||||
|
||||
const subs: Subscription[] = [];
|
||||
|
||||
const import$ = this.ipc.sendV2("import-playlists", opt);
|
||||
subs.push(import$.subscribe(obs));
|
||||
|
||||
subs.push(import$.pipe(catchError(() => of()), map(progress => progress?.data), filter(Boolean), distinctUntilChanged((a, b) => equal(a, b))).subscribe({
|
||||
next: playlist => {
|
||||
this.lastPlaylistImported$.next({ version: opt.version, playlist });
|
||||
}
|
||||
}));
|
||||
|
||||
this.progressBar.show(import$);
|
||||
|
||||
const knownErrors = ["INVALID_SOURCE", "INVALID_PLAYLIST_FILE", "CANNOT_PARSE_PLAYLIST"];
|
||||
|
||||
lastValueFrom(import$).then(res => {
|
||||
if(res.lastError && !res.current){
|
||||
throw res.lastError;
|
||||
}
|
||||
|
||||
if(!res.total){
|
||||
return this.notification.notifyWarning({
|
||||
title: "playlist.no-playlist-found",
|
||||
desc: "playlist.no-playlist-found-in-selected-files"
|
||||
});
|
||||
}
|
||||
|
||||
if(res.current < res.total){
|
||||
return this.notification.notifyWarning({
|
||||
title: "playlist.some-playlists-not-imported",
|
||||
desc: knownErrors.includes(res.lastError?.code) ? `playlist.some-playlists-have-been-imported.${res.lastError.code}` : "playlist.some-playlists-have-been-imported.unknown"
|
||||
});
|
||||
}
|
||||
|
||||
return this.notification.notifySuccess({
|
||||
title: "playlist.playlists-imported",
|
||||
desc: "playlist.all-playlists-have-been-successfully-imported",
|
||||
});
|
||||
}).catch((err: CustomError) => {
|
||||
this.notification.notifyError({
|
||||
title: "playlist.no-playlists-imported",
|
||||
desc: knownErrors.includes(err.code) ? `playlist.no-playlists-imported-errors.${err.code}` : "playlist.no-playlists-imported-errors.unknown"
|
||||
});
|
||||
}).finally(() => {
|
||||
obs.complete();
|
||||
})
|
||||
|
||||
|
||||
return () => {
|
||||
subs.forEach(s => s.unsubscribe());
|
||||
this.progressBar.hide();
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
public async linkVersion(version: BSVersion): Promise<boolean> {
|
||||
const modalRes = await this.modal.openModal(LinkPlaylistModal);
|
||||
|
||||
@@ -91,4 +165,11 @@ export class PlaylistsManagerService {
|
||||
public $playlistsFolderLinkState(version: BSVersion): Observable<FolderLinkState> {
|
||||
return this.linker.$folderLinkedState(version, PlaylistsManagerService.RELATIVE_PLAYLISTS_FOLDER);
|
||||
}
|
||||
|
||||
public $onPlaylistImported(version?: BSVersion): Observable<LocalBPListsDetails> {
|
||||
return this.lastPlaylistImported$.pipe(
|
||||
filter((val) => val?.playlist && equal(val.version, version)),
|
||||
map((val) => val.playlist)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ export class ProgressBarService {
|
||||
lastValueFrom(this.ipcService.sendV2("window.progression", progression));
|
||||
}
|
||||
|
||||
public subscribreTo(obs: Observable<ProgressionInterface | number | Progression>) {
|
||||
private subscribreTo(obs: Observable<ProgressionInterface | number | Progression>) {
|
||||
if (this.subscription) {
|
||||
this.unsubscribe();
|
||||
}
|
||||
@@ -62,16 +62,16 @@ export class ProgressBarService {
|
||||
});
|
||||
}
|
||||
|
||||
public unsubscribe() {
|
||||
private unsubscribe() {
|
||||
this._progression$.next({ progression: 0 });
|
||||
this.subscription?.unsubscribe();
|
||||
this.subscription = null;
|
||||
}
|
||||
|
||||
public show(obs?: Observable<ProgressionInterface | number | Progression>, unsubscribe?: boolean, style?: CSSProperties) {
|
||||
if (unsubscribe) {
|
||||
this.unsubscribe();
|
||||
}
|
||||
public show(obs?: Observable<ProgressionInterface | number | Progression>, style?: CSSProperties) {
|
||||
|
||||
this.unsubscribe();
|
||||
|
||||
if (obs) {
|
||||
this.subscribreTo(obs);
|
||||
}
|
||||
@@ -90,7 +90,7 @@ export class ProgressBarService {
|
||||
return { progression: progress, label } as ProgressionInterface;
|
||||
})
|
||||
);
|
||||
this.show(obs, true, style);
|
||||
this.show(obs, style);
|
||||
}
|
||||
|
||||
public complete(): void {
|
||||
@@ -98,13 +98,11 @@ export class ProgressBarService {
|
||||
}
|
||||
|
||||
public open(): void {
|
||||
this.show(of(0), true, this._style$.value);
|
||||
this.show(of(0), this._style$.value);
|
||||
}
|
||||
|
||||
public hide(unsubscribe = true) {
|
||||
if (unsubscribe) {
|
||||
this.unsubscribe();
|
||||
}
|
||||
public hide() {
|
||||
this.unsubscribe();
|
||||
this._visible$.next(false);
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ export class VersionFolderLinkerService {
|
||||
}
|
||||
|
||||
if (progressOpened) {
|
||||
this.progress.hide(true);
|
||||
this.progress.hide();
|
||||
}
|
||||
|
||||
const newArr = [...this._queue$.value];
|
||||
|
||||
@@ -53,6 +53,18 @@ export default function App() {
|
||||
const appVersion = await lastValueFrom(autoUpdater.getAppVersion());
|
||||
const lastAppVersion = autoUpdater.getLastAppVersion();
|
||||
|
||||
if(!lastAppVersion) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (lastAppVersion.toLowerCase().includes("alpha")) {
|
||||
autoUpdater.setLastAppVersion("0"); // Reset last app version if it's an alpha version for users with alpha versions in the var stored.
|
||||
}
|
||||
|
||||
if (appVersion.toLowerCase().includes("alpha")) {
|
||||
return;
|
||||
}
|
||||
|
||||
autoUpdater.setLastAppVersion(appVersion);
|
||||
|
||||
if (parse(lastAppVersion) && gt(appVersion, lastAppVersion)) {
|
||||
|
||||
@@ -7,6 +7,8 @@ import { useTranslation } from "../hooks/use-translation.hook";
|
||||
import { AutoUpdaterService } from "../services/auto-updater.service";
|
||||
import { WindowManagerService } from "../services/window-manager.service";
|
||||
import { useService } from "renderer/hooks/use-service.hook";
|
||||
import { lastValueFrom } from "rxjs";
|
||||
import { logRenderError } from "renderer";
|
||||
|
||||
export default function Launcher() {
|
||||
const updaterService = useService(AutoUpdaterService);
|
||||
@@ -24,13 +26,11 @@ export default function Launcher() {
|
||||
return windowService.openThenCloseAll("index.html");
|
||||
}
|
||||
setText("auto-update.downloading");
|
||||
updaterService.downloadUpdate().then(installed => {
|
||||
if (!installed) {
|
||||
return windowService.openThenCloseAll("index.html");
|
||||
}
|
||||
updaterService.quitAndInstall();
|
||||
});
|
||||
});
|
||||
|
||||
return lastValueFrom(updaterService.downloadUpdate())
|
||||
.then(() => updaterService.quitAndInstall())
|
||||
.catch((err) => {logRenderError("omg", err); windowService.openThenCloseAll("index.html")});
|
||||
}).catch(() => windowService.openThenCloseAll("index.html"));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { getLocalTimeZone, parseAbsolute, toCalendarDateTime } from "@internationalized/date";
|
||||
import { MapItemComponentProps } from "renderer/components/maps-playlists-panel/maps/map-item.component";
|
||||
import { BsvMapDetail, RawMapInfoData, SongDetailDiffCharactertistic, SongDetails, SongDiffName } from "shared/models/maps";
|
||||
import { BsvMapDetail, SongDetailDiffCharactertistic, SongDetails, SongDiffName } from "shared/models/maps";
|
||||
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
|
||||
import { MapInfo } from "shared/models/maps/info/map-info.model";
|
||||
|
||||
export type ParsedMapDiff = { name: SongDiffName; libelle: string; stars: number, nps: number, njs: number };
|
||||
|
||||
export abstract class MapItemComponentPropsMapper {
|
||||
|
||||
public static extractMapDiffs({rawMapInfo, songDetails, bsvMap}: {rawMapInfo?: RawMapInfoData, songDetails?: SongDetails, bsvMap?: BsvMapDetail}): Map<SongDetailDiffCharactertistic, ParsedMapDiff[]> {
|
||||
public static extractMapDiffs({mapInfo, songDetails, bsvMap}: {mapInfo?: MapInfo, songDetails?: SongDetails, bsvMap?: BsvMapDetail}): Map<SongDetailDiffCharactertistic, ParsedMapDiff[]> {
|
||||
const res = new Map<SongDetailDiffCharactertistic, ParsedMapDiff[]>();
|
||||
|
||||
if (bsvMap?.versions?.at(0)?.diffs) {
|
||||
@@ -22,20 +23,18 @@ export abstract class MapItemComponentPropsMapper {
|
||||
if (songDetails?.difficulties) {
|
||||
songDetails?.difficulties.forEach(diff => {
|
||||
const arr = res.get(diff.characteristic) || [];
|
||||
const diffName = rawMapInfo?._difficultyBeatmapSets?.find(set => set._beatmapCharacteristicName === diff.characteristic)._difficultyBeatmaps.find(rawDiff => rawDiff._difficulty === diff.difficulty)?._customData?._difficultyLabel || diff.difficulty;
|
||||
const diffName = mapInfo?.difficulties?.find(mapDiff => mapDiff.characteristic === diff.characteristic && mapDiff.difficulty === diff.difficulty)?.difficultyLabel || diff.difficulty;
|
||||
arr.push({ libelle: diffName, name: diff.difficulty, stars: diff.stars, nps: diff.nps, njs: diff.njs });
|
||||
res.set(diff.characteristic, arr);
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
if(rawMapInfo?._difficultyBeatmapSets){
|
||||
rawMapInfo._difficultyBeatmapSets.forEach(set => {
|
||||
set._difficultyBeatmaps.forEach(diff => {
|
||||
const arr = res.get(set._beatmapCharacteristicName) || [];
|
||||
arr.push({ libelle: diff._customData?._difficultyLabel || diff._difficulty, name: diff._difficulty, stars: null, nps: null, njs: diff._noteJumpMovementSpeed });
|
||||
res.set(set._beatmapCharacteristicName, arr);
|
||||
});
|
||||
if(mapInfo?.difficulties){
|
||||
mapInfo.difficulties.forEach(diff => {
|
||||
const arr = res.get(diff.characteristic) || [];
|
||||
arr.push({ libelle: diff.difficultyLabel || diff.difficulty, name: diff.difficulty, stars: null, nps: null, njs: diff.noteJumpMovementSpeed });
|
||||
res.set(diff.characteristic, arr);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -45,14 +44,14 @@ export abstract class MapItemComponentPropsMapper {
|
||||
public static fromBsmLocalMap(map: BsmLocalMap): MapItemComponentProps<BsmLocalMap> {
|
||||
return {
|
||||
hash: map.hash,
|
||||
title: map.rawInfo._songName,
|
||||
title: map.mapInfo.songName,
|
||||
coverUrl: map.coverUrl,
|
||||
songUrl: map.songUrl,
|
||||
autor: map.rawInfo._levelAuthorName,
|
||||
songAutor: map.rawInfo._songAuthorName,
|
||||
bpm: map.rawInfo._beatsPerMinute,
|
||||
autor: map.mapInfo.levelMappers.at(0),
|
||||
songAutor: map.mapInfo.songAuthorName,
|
||||
bpm: map.mapInfo.beatsPerMinute,
|
||||
duration: map.songDetails?.duration,
|
||||
diffs: MapItemComponentPropsMapper.extractMapDiffs({ rawMapInfo: map.rawInfo, songDetails: map.songDetails }),
|
||||
diffs: MapItemComponentPropsMapper.extractMapDiffs({ mapInfo: map.mapInfo, songDetails: map.songDetails }),
|
||||
mapId: map.songDetails?.id,
|
||||
ranked: map.songDetails?.ranked,
|
||||
blRanked: map.songDetails?.blRanked,
|
||||
@@ -102,7 +101,7 @@ export abstract class MapItemComponentPropsMapper {
|
||||
}
|
||||
|
||||
public static from(mapDetails: BsmLocalMap|BsvMapDetail|SongDetails): MapItemComponentProps<BsmLocalMap|BsvMapDetail|SongDetails> {
|
||||
if ((mapDetails as BsmLocalMap).rawInfo) {
|
||||
if ((mapDetails as BsmLocalMap).mapInfo) {
|
||||
return MapItemComponentPropsMapper.fromBsmLocalMap(mapDetails as BsmLocalMap) as MapItemComponentProps<BsmLocalMap|BsvMapDetail|SongDetails>;
|
||||
}
|
||||
if ((mapDetails as BsvMapDetail).metadata) {
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
export class CustomError extends Error {
|
||||
|
||||
private readonly _code: string;
|
||||
private readonly _data?: unknown;
|
||||
public readonly code: string;
|
||||
public readonly data?: unknown;
|
||||
|
||||
constructor(message: string, code: string, data?: unknown){
|
||||
super(message);
|
||||
this._code = code;
|
||||
this._data = data;
|
||||
this.code = code;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public get code(): string { return this._code; }
|
||||
public get data(): unknown { return this._data; }
|
||||
|
||||
public static fromError(error: Error, code?: string, data?: unknown): CustomError{
|
||||
|
||||
if(error instanceof CustomError){
|
||||
@@ -33,4 +30,4 @@ export class CustomError extends Error {
|
||||
|
||||
export enum CustomErrorCodes {
|
||||
UNKNOWN_ERROR = "UNKNOWN_ERROR",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,10 +11,10 @@ import { DepotDownloaderEvent } from "../bs-version-download/depot-downloader.mo
|
||||
import { MSGetQuery, MSModel, MSModelType } from "../models/model-saber.model";
|
||||
import { ModelDownload } from "renderer/services/models-management/models-downloader.service";
|
||||
import { BsmLocalModel } from "../models/bsm-local-model.interface";
|
||||
import { InstallModsResult, Mod, UninstallModsResult } from "../mods";
|
||||
import { Mod } from "../mods";
|
||||
import { BPList, DownloadPlaylistProgressionData } from "../playlists/playlist.interface";
|
||||
import { VersionLinkerAction } from "renderer/services/version-folder-linker.service";
|
||||
import { FileFilter, OpenDialogReturnValue } from "electron";
|
||||
import { FileFilter, OpenDialogOptions, OpenDialogReturnValue } from "electron";
|
||||
import { SystemNotificationOptions } from "../notification/system-notification.model";
|
||||
import { Supporter } from "../supporters";
|
||||
import { AppWindow } from "../window-manager/app-window.model";
|
||||
@@ -58,7 +58,9 @@ export interface IpcChannelMapping {
|
||||
"load-version-maps": { request: BSVersion, response: BsmLocalMapsProgress};
|
||||
"delete-maps": { request: BsmLocalMap[], response: DeleteMapsProgress };
|
||||
"export-maps": { request: { version: BSVersion; maps: BsmLocalMap[]; outPath: string }, response: Progression };
|
||||
"download-map": { request: { map: BsvMapDetail; version: BSVersion }, response: BsmLocalMap };
|
||||
"bs-maps.import-maps": { request: { paths: string[]; version: BSVersion | undefined }, response: Progression<BsmLocalMap> };
|
||||
"bs-maps.download-map": { request: { map: BsvMapDetail; version: BSVersion | undefined }, response: BsmLocalMap };
|
||||
"last-downloaded-map": { request: void, response: { version?: BSVersion, map: BsmLocalMap } };
|
||||
"one-click-install-map": { request: BsvMapDetail, response: void };
|
||||
"register-maps-deep-link": { request: void, response: boolean };
|
||||
"unregister-maps-deep-link": { request: void, response: boolean };
|
||||
@@ -78,9 +80,9 @@ export interface IpcChannelMapping {
|
||||
/* ** bs-mods-ipcs ** */
|
||||
"get-available-mods": { request: BSVersion, response: Mod[] };
|
||||
"get-installed-mods": { request: BSVersion, response: Mod[] };
|
||||
"install-mods": { request: { mods: Mod[]; version: BSVersion }, response: InstallModsResult };
|
||||
"uninstall-mods": { request: { mods: Mod[]; version: BSVersion }, response: UninstallModsResult };
|
||||
"uninstall-all-mods": { request: BSVersion, response: UninstallModsResult };
|
||||
"install-mods": { request: { mods: Mod[]; version: BSVersion }, response: Progression };
|
||||
"uninstall-mods": { request: { mods: Mod[]; version: BSVersion }, response: Progression };
|
||||
"uninstall-all-mods": { request: BSVersion, response: Progression };
|
||||
|
||||
/* ** bs-playlist-ipcs ** */
|
||||
"one-click-install-playlist": { request: string, response: Progression<DownloadPlaylistProgressionData> };
|
||||
@@ -91,6 +93,7 @@ export interface IpcChannelMapping {
|
||||
"get-version-playlists-details": {request: BSVersion, response: Progression<LocalBPListsDetails[]>};
|
||||
"delete-playlist": {request: {version: BSVersion, bpList: LocalBPList, deleteMaps?: boolean}, response: Progression};
|
||||
"export-playlists": {request: {version?: BSVersion, bpLists: LocalBPList[], dest: string, playlistsMaps?: BsmLocalMap[]}, response: Progression<string>};
|
||||
"import-playlists": {request: {version?: BSVersion, paths: string[]}, response: Progression<LocalBPListsDetails>};
|
||||
"install-playlist-file": {request: {bplist: BPList, version?: BSVersion, dest?: string}, response: LocalBPListsDetails};
|
||||
|
||||
/* ** bs-uninstall-ipcs ** */
|
||||
@@ -110,7 +113,7 @@ export interface IpcChannelMapping {
|
||||
"relink-all-versions-folders": { request: void, response: void };
|
||||
|
||||
/* ** launcher-ipcs ** */
|
||||
"download-update": { request: void, response: boolean };
|
||||
"download-update": { request: void, response: Progression };
|
||||
"check-update": { request: void, response: boolean };
|
||||
"install-update": { request: void, response: void };
|
||||
|
||||
@@ -120,6 +123,7 @@ export interface IpcChannelMapping {
|
||||
|
||||
/* ** os-controls-ipcs ** */
|
||||
"new-window": { request: string, response: void };
|
||||
"open-dialog": { request: OpenDialogOptions, response: OpenDialogReturnValue };
|
||||
"choose-folder": { request: string, response: OpenDialogReturnValue };
|
||||
"choose-file": { request: string, response: OpenDialogReturnValue }
|
||||
"choose-image": { request: { multiple?: boolean, base64?: boolean }, response: string[] }
|
||||
|
||||
@@ -147,42 +147,42 @@ export interface BsvMapParitySummary {
|
||||
}
|
||||
|
||||
export enum MapStyle {
|
||||
Dance = "dance",
|
||||
Swing = "swing",
|
||||
Nightcore = "nightcore",
|
||||
Folk = "folk",
|
||||
Family = "family",
|
||||
Alt = "alternative",
|
||||
Ambient = "ambient",
|
||||
Funk = "funk",
|
||||
Anime = "anime",
|
||||
ClassicalOrchestral = "classical-orchestral",
|
||||
Comedy = "comedy-meme",
|
||||
Dance = "dance",
|
||||
DrumBass = "drum-and-bass",
|
||||
Dubstep = "dubstep",
|
||||
Electronic = "electronic",
|
||||
Folk = "folk-acoustic",
|
||||
Funk = "funk-disco",
|
||||
Hardcore = "hardcore",
|
||||
Hiphop = "hip-hop-rap",
|
||||
Holiday = "holiday",
|
||||
House = "house",
|
||||
Indie = "indie",
|
||||
Instrumental = "instrumental",
|
||||
JPop = "j-pop",
|
||||
JRock = "j-rock",
|
||||
Jazz = "jazz",
|
||||
Soul = "soul",
|
||||
Speedcore = "speedcore",
|
||||
KPop = "k-pop",
|
||||
Family = "kids-family",
|
||||
Metal = "metal",
|
||||
Nightcore = "nightcore",
|
||||
Pop = "pop",
|
||||
Punk = "punk",
|
||||
Rb = "rb",
|
||||
Holiday = "holiday",
|
||||
Vocaloid = "vocaloid",
|
||||
JRock = "j-rock",
|
||||
Trance = "trance",
|
||||
DrumBass = "drumbass",
|
||||
Comedy = "comedy",
|
||||
Instrumental = "instrumental",
|
||||
Hardcore = "hardcore",
|
||||
KPop = "k-pop",
|
||||
Indie = "indie",
|
||||
Techno = "techno",
|
||||
House = "house",
|
||||
Game = "game",
|
||||
Film = "film",
|
||||
Alt = "alt",
|
||||
Dubstep = "dubstep",
|
||||
Metal = "metal",
|
||||
Anime = "anime",
|
||||
Hiphop = "hiphop",
|
||||
JPop = "j-pop",
|
||||
Rock = "rock",
|
||||
Pop = "pop",
|
||||
Electronic = "electronic",
|
||||
ClassicalOrchestral = "classical-orchestral"
|
||||
Soul = "soul",
|
||||
Speedcore = "speedcore",
|
||||
Swing = "swing",
|
||||
Film = "tv-movie-soundtrack",
|
||||
Techno = "techno",
|
||||
Trance = "trance",
|
||||
Game = "video-game-soundtrack",
|
||||
Vocaloid = "vocaloid",
|
||||
}
|
||||
|
||||
// MapType Enum
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { RawMapInfoData } from "./raw-map.model";
|
||||
import { MapInfo } from "./info/map-info.model";
|
||||
import { SongDetails } from "./song-details-cache/song-details-cache.model";
|
||||
|
||||
export interface BsmLocalMap {
|
||||
hash: string;
|
||||
coverUrl: string;
|
||||
songUrl: string;
|
||||
rawInfo: RawMapInfoData;
|
||||
mapInfo: MapInfo;
|
||||
songDetails?: SongDetails;
|
||||
path: string;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
export { RawMapInfoData, RawMapDifficulty, RawDifficultySet } from "./raw-map.model";
|
||||
export { BsvInstant, BsvMapDetail, BsvMapDetailMetadata, BsvMapDifficulty, BsvMapParitySummary, BsvMapStats, BsvMapTestplay, BsvMapVersion, BsvUserDetail } from "./beat-saver.model";
|
||||
export { SongDetails, SongDifficulty, SongUploader, SongDetailDiffCharactertistic, SongDiffName } from "./song-details-cache/song-details-cache.model";
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { SongDetailDiffCharactertistic, SongDiffName } from "../song-details-cache/song-details-cache.model";
|
||||
import { RawMapInfoDataV2 } from "./raw-map-info-v2.model";
|
||||
import { RawMapInfoDataV4 } from "./raw-map-info-v4.model";
|
||||
|
||||
export type AnyRawMapInfo = RawMapInfoDataV2 | RawMapInfoDataV4;
|
||||
|
||||
// interfaces/mapInfo.ts
|
||||
export interface MapInfo {
|
||||
version: string;
|
||||
songName: string;
|
||||
songSubName?: string;
|
||||
songAuthorName: string;
|
||||
levelMappers: string[];
|
||||
levelLighters: string[];
|
||||
beatsPerMinute: number;
|
||||
shuffle?: number;
|
||||
shufflePeriod?: number;
|
||||
previewStartTime: number;
|
||||
previewDuration: number;
|
||||
songFilename: string;
|
||||
songPreviewFilename: string;
|
||||
coverImageFilename: string;
|
||||
environmentNames: string[];
|
||||
difficulties: MapDifficulty[];
|
||||
}
|
||||
|
||||
export interface MapDifficulty {
|
||||
characteristic: SongDetailDiffCharactertistic;
|
||||
difficulty: SongDiffName;
|
||||
difficultyLabel?: string;
|
||||
beatmapFilename: string;
|
||||
noteJumpMovementSpeed: number;
|
||||
noteJumpStartBeatOffset: number;
|
||||
beatmapColorSchemeIdx?: number;
|
||||
environmentNameIdx?: number;
|
||||
// Additional fields from version 4.0.0
|
||||
beatmapAuthors?: {
|
||||
mappers: string[];
|
||||
lighters: string[];
|
||||
};
|
||||
lightshowDataFilename?: string;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user